{
  "schemaVersion": "1.0",
  "item": {
    "slug": "glance",
    "name": "Glance",
    "source": "tencent",
    "type": "skill",
    "category": "开发工具",
    "sourceUrl": "https://clawhub.ai/acfranzen/glance",
    "canonicalUrl": "https://clawhub.ai/acfranzen/glance",
    "targetPlatform": "OpenClaw"
  },
  "install": {
    "downloadMode": "redirect",
    "downloadUrl": "/downloads/glance",
    "sourceDownloadUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=glance",
    "sourcePlatform": "tencent",
    "targetPlatform": "OpenClaw",
    "installMethod": "Manual import",
    "extraction": "Extract archive",
    "prerequisites": [
      "OpenClaw"
    ],
    "packageFormat": "ZIP package",
    "includedAssets": [
      "widget-sdk.md",
      "README.md",
      "dashboard-api.md",
      "SKILL.md"
    ],
    "primaryDoc": "SKILL.md",
    "quickSetup": [
      "Download the package from Yavira.",
      "Extract the archive and review SKILL.md first.",
      "Import or place the package into your OpenClaw setup."
    ],
    "agentAssist": {
      "summary": "Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.",
      "steps": [
        "Download the package from Yavira.",
        "Extract it into a folder your agent can access.",
        "Paste one of the prompts below and point your agent at the extracted folder."
      ],
      "prompts": [
        {
          "label": "New install",
          "body": "I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Then review README.md for any prerequisites, environment setup, or post-install checks. Tell me what you changed and call out any manual steps you could not complete."
        },
        {
          "label": "Upgrade existing",
          "body": "I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Then review README.md for any prerequisites, environment setup, or post-install checks. Summarize what changed and any follow-up checks I should run."
        }
      ]
    },
    "sourceHealth": {
      "source": "tencent",
      "status": "healthy",
      "reason": "direct_download_ok",
      "recommendedAction": "download",
      "checkedAt": "2026-04-30T16:55:25.780Z",
      "expiresAt": "2026-05-07T16:55:25.780Z",
      "httpStatus": 200,
      "finalUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=network",
      "contentType": "application/zip",
      "probeMethod": "head",
      "details": {
        "probeUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=network",
        "contentDisposition": "attachment; filename=\"network-1.0.0.zip\"",
        "redirectLocation": null,
        "bodySnippet": null
      },
      "scope": "source",
      "summary": "Source download looks usable.",
      "detail": "Yavira can redirect you to the upstream package for this source.",
      "primaryActionLabel": "Download for OpenClaw",
      "primaryActionHref": "/downloads/glance"
    },
    "validation": {
      "installChecklist": [
        "Use the Yavira download entry.",
        "Review SKILL.md after the package is downloaded.",
        "Confirm the extracted package contains the expected setup assets."
      ],
      "postInstallChecks": [
        "Confirm the extracted package includes the expected docs or setup files.",
        "Validate the skill or prompts are available in your target agent workspace.",
        "Capture any manual follow-up steps the agent could not complete."
      ]
    },
    "downloadPageUrl": "https://openagent3.xyz/downloads/glance",
    "agentPageUrl": "https://openagent3.xyz/skills/glance/agent",
    "manifestUrl": "https://openagent3.xyz/skills/glance/agent.json",
    "briefUrl": "https://openagent3.xyz/skills/glance/agent.md"
  },
  "agentAssist": {
    "summary": "Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.",
    "steps": [
      "Download the package from Yavira.",
      "Extract it into a folder your agent can access.",
      "Paste one of the prompts below and point your agent at the extracted folder."
    ],
    "prompts": [
      {
        "label": "New install",
        "body": "I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Then review README.md for any prerequisites, environment setup, or post-install checks. Tell me what you changed and call out any manual steps you could not complete."
      },
      {
        "label": "Upgrade existing",
        "body": "I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Then review README.md for any prerequisites, environment setup, or post-install checks. Summarize what changed and any follow-up checks I should run."
      }
    ]
  },
  "documentation": {
    "source": "clawhub",
    "primaryDoc": "SKILL.md",
    "sections": [
      {
        "title": "Glance",
        "body": "AI-extensible personal dashboard. Create custom widgets with natural language — the AI handles data collection."
      },
      {
        "title": "Features",
        "body": "Custom Widgets — Create widgets via AI with auto-generated JSX\nAgent Refresh — AI collects data on schedule and pushes to cache\nDashboard Export/Import — Share widget configurations\nCredential Management — Secure API key storage\nReal-time Updates — Webhook-triggered instant refreshes"
      },
      {
        "title": "Quick Start",
        "body": "# Navigate to skill directory (if installed via ClawHub)\ncd \"$(clawhub list | grep glance | awk '{print $2}')\"\n\n# Or clone directly\ngit clone https://github.com/acfranzen/glance ~/.glance\ncd ~/.glance\n\n# Install dependencies\nnpm install\n\n# Configure environment\ncp .env.example .env.local\n# Edit .env.local with your settings\n\n# Start development server\nnpm run dev\n\n# Or build and start production\nnpm run build && npm start\n\nDashboard runs at http://localhost:3333"
      },
      {
        "title": "Configuration",
        "body": "Edit .env.local:\n\n# Server\nPORT=3333\nAUTH_TOKEN=your-secret-token        # Optional: Bearer token auth\n\n# OpenClaw Integration (for instant widget refresh)\nOPENCLAW_GATEWAY_URL=https://localhost:18789\nOPENCLAW_TOKEN=your-gateway-token\n\n# Database\nDATABASE_PATH=./data/glance.db      # SQLite database location"
      },
      {
        "title": "Service Installation (macOS)",
        "body": "# Create launchd plist\ncat > ~/Library/LaunchAgents/com.glance.dashboard.plist << 'EOF'\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n    <key>Label</key>\n    <string>com.glance.dashboard</string>\n    <key>ProgramArguments</key>\n    <array>\n        <string>/opt/homebrew/bin/npm</string>\n        <string>run</string>\n        <string>dev</string>\n    </array>\n    <key>WorkingDirectory</key>\n    <string>~/.glance</string>\n    <key>RunAtLoad</key>\n    <true/>\n    <key>KeepAlive</key>\n    <true/>\n    <key>StandardOutPath</key>\n    <string>~/.glance/logs/stdout.log</string>\n    <key>StandardErrorPath</key>\n    <string>~/.glance/logs/stderr.log</string>\n</dict>\n</plist>\nEOF\n\n# Load service\nmkdir -p ~/.glance/logs\nlaunchctl load ~/Library/LaunchAgents/com.glance.dashboard.plist\n\n# Service commands\nlaunchctl start com.glance.dashboard\nlaunchctl stop com.glance.dashboard\nlaunchctl unload ~/Library/LaunchAgents/com.glance.dashboard.plist"
      },
      {
        "title": "Environment Variables",
        "body": "VariableDescriptionDefaultPORTServer port3333AUTH_TOKENBearer token for API auth—DATABASE_PATHSQLite database path./data/glance.dbOPENCLAW_GATEWAY_URLOpenClaw gateway for webhooks—OPENCLAW_TOKENOpenClaw auth token—"
      },
      {
        "title": "Requirements",
        "body": "Node.js 20+\nnpm or pnpm\nSQLite (bundled)"
      },
      {
        "title": "Widget Skill",
        "body": "Create and manage dashboard widgets. Most widgets use agent_refresh — you collect the data."
      },
      {
        "title": "Quick Start",
        "body": "# Check Glance is running (list widgets)\ncurl -s -H \"Origin: $GLANCE_URL\" \"$GLANCE_URL/api/widgets\" | jq '.custom_widgets[].slug'\n\n# Auth note: Local requests with Origin header bypass Bearer token auth\n# For external access, use: -H \"Authorization: Bearer $GLANCE_TOKEN\"\n\n# Refresh a widget (look up instructions, collect data, POST to cache)\nsqlite3 $GLANCE_DATA/glance.db \"SELECT json_extract(fetch, '$.instructions') FROM custom_widgets WHERE slug = 'my-widget'\"\n# Follow the instructions, then:\ncurl -X POST \"$GLANCE_URL/api/widgets/my-widget/cache\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Origin: $GLANCE_URL\" \\\n  -d '{\"data\": {\"value\": 42, \"fetchedAt\": \"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'\"}}'\n\n# Verify in browser\nbrowser action:open targetUrl:\"$GLANCE_URL\""
      },
      {
        "title": "AI Structured Output Generation (REQUIRED)",
        "body": "When generating widget definitions, use the JSON Schema at docs/schemas/widget-schema.json with your AI model's structured output mode:\n\nAnthropic: Use tool_use with the schema\nOpenAI: Use response_format: { type: \"json_schema\", schema }\n\nThe schema enforces all required fields at generation time — malformed widgets cannot be produced."
      },
      {
        "title": "Required Fields Checklist",
        "body": "Every widget MUST have these fields (the schema enforces them):\n\nFieldTypeNotesnamestringNon-empty, human-readableslugstringLowercase kebab-case (my-widget)source_codestringValid JSX with Widget functiondefault_size{ w: 1-12, h: 1-20 }Grid unitsmin_size{ w: 1-12, h: 1-20 }Cannot resize smallerfetch.typeenum\"server_code\" | \"webhook\" | \"agent_refresh\"fetch.instructionsstringREQUIRED if type is agent_refreshfetch.schedulestringREQUIRED if type is agent_refresh (cron)data_schema.type\"object\"Always objectdata_schema.propertiesobjectDefine each fielddata_schema.requiredarrayMUST include \"fetchedAt\"credentialsarrayUse [] if none needed"
      },
      {
        "title": "Example: Minimal Valid Widget",
        "body": "{\n  \"name\": \"My Widget\",\n  \"slug\": \"my-widget\",\n  \"source_code\": \"function Widget({ serverData }) { return <div>{serverData?.value}</div>; }\",\n  \"default_size\": { \"w\": 2, \"h\": 2 },\n  \"min_size\": { \"w\": 1, \"h\": 1 },\n  \"fetch\": {\n    \"type\": \"agent_refresh\",\n    \"schedule\": \"*/15 * * * *\",\n    \"instructions\": \"## Data Collection\\nCollect the data...\\n\\n## Cache Update\\nPOST to /api/widgets/my-widget/cache\"\n  },\n  \"data_schema\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"value\": { \"type\": \"number\" },\n      \"fetchedAt\": { \"type\": \"string\", \"format\": \"date-time\" }\n    },\n    \"required\": [\"value\", \"fetchedAt\"]\n  },\n  \"credentials\": []\n}"
      },
      {
        "title": "⚠️ Widget Creation Checklist (MANDATORY)",
        "body": "Every widget must complete ALL steps before being considered done:\n\n□ Step 1: Create widget definition (POST /api/widgets)\n    - source_code with Widget function\n    - data_schema (REQUIRED for validation)\n    - fetch config (type + instructions for agent_refresh)\n    \n□ Step 2: Add to dashboard (POST /api/widgets/instances)\n    - custom_widget_id matches definition\n    - title and config set\n    \n□ Step 3: Populate cache (for agent_refresh widgets)\n    - Data matches data_schema exactly\n    - Includes fetchedAt timestamp\n    \n□ Step 4: Set up cron job (for agent_refresh widgets)\n    - Simple message: \"⚡ WIDGET REFRESH: {slug}\"\n    - Appropriate schedule (*/15 or */30 typically)\n    \n□ Step 5: BROWSER VERIFICATION (MANDATORY)\n    - Open http://localhost:3333\n    - Widget is visible on dashboard\n    - Shows actual data (not loading spinner)\n    - Data values match what was cached\n    - No errors or broken layouts\n    \n⛔ DO NOT report widget as complete until Step 5 passes!"
      },
      {
        "title": "Quick Reference",
        "body": "Full SDK docs: See docs/widget-sdk.md in the Glance repo\nComponent list: See references/components.md"
      },
      {
        "title": "Widget Package Structure",
        "body": "Widget Package\n├── meta (name, slug, description, author, version)\n├── widget (source_code, default_size, min_size)\n├── fetch (server_code | webhook | agent_refresh)\n├── dataSchema? (JSON Schema for cached data - validates on POST)\n├── cache (ttl, staleness, fallback)\n├── credentials[] (API keys, local software requirements)\n├── config_schema? (user options)\n└── error? (retry, fallback, timeout)"
      },
      {
        "title": "Fetch Type Decision Tree",
        "body": "Is data available via API that the widget can call?\n├── YES → Use server_code\n└── NO → Does an external service push data?\n    ├── YES → Use webhook\n    └── NO → Use agent_refresh (YOU collect it)\n\nScenarioFetch TypeWho Collects Data?Public/authenticated APIserver_codeWidget calls API at renderExternal service pushes datawebhookExternal service POSTs to cacheLocal CLI toolsagent_refreshYOU (the agent) via PTY/execInteractive terminalsagent_refreshYOU (the agent) via PTYComputed/aggregated dataagent_refreshYOU (the agent) on a schedule\n\n⚠️ agent_refresh means YOU are the data source. You set up a cron to remind yourself, then YOU collect the data using your tools (exec, PTY, browser, etc.) and POST it to the cache."
      },
      {
        "title": "Widget Definitions",
        "body": "MethodEndpointDescriptionPOST/api/widgetsCreate widget definitionGET/api/widgetsList all definitionsGET/api/widgets/:slugGet single definitionPATCH/api/widgets/:slugUpdate definitionDELETE/api/widgets/:slugDelete definition"
      },
      {
        "title": "Widget Instances (Dashboard)",
        "body": "MethodEndpointDescriptionPOST/api/widgets/instancesAdd widget to dashboardGET/api/widgets/instancesList dashboard widgetsPATCH/api/widgets/instances/:idUpdate instance (config, position)DELETE/api/widgets/instances/:idRemove from dashboard"
      },
      {
        "title": "Credentials",
        "body": "MethodEndpointDescriptionGET/api/credentialsList credentials + statusPOST/api/credentialsStore credentialDELETE/api/credentials/:idDelete credential"
      },
      {
        "title": "Full Widget Package Structure",
        "body": "{\n  \"name\": \"GitHub PRs\",\n  \"slug\": \"github-prs\",\n  \"description\": \"Shows open pull requests\",\n  \n  \"source_code\": \"function Widget({ serverData }) { ... }\",\n  \"default_size\": { \"w\": 2, \"h\": 2 },\n  \"min_size\": { \"w\": 1, \"h\": 1 },\n  \"refresh_interval\": 300,\n  \n  \"credentials\": [\n    {\n      \"id\": \"github\",\n      \"type\": \"api_key\",\n      \"name\": \"GitHub Personal Access Token\",\n      \"description\": \"Token with repo scope\",\n      \"obtain_url\": \"https://github.com/settings/tokens\"\n    }\n  ],\n  \n  \"fetch\": {\n    \"type\": \"agent_refresh\",\n    \"schedule\": \"*/5 * * * *\",\n    \"instructions\": \"Fetch open PRs from GitHub API and POST to cache endpoint\",\n    \"expected_freshness_seconds\": 300,\n    \"max_staleness_seconds\": 900\n  },\n  \n  \"cache\": {\n    \"ttl_seconds\": 300,\n    \"max_staleness_seconds\": 900,\n    \"storage\": \"sqlite\",\n    \"on_error\": \"use_stale\"\n  },\n  \n  \"setup\": {\n    \"description\": \"Configure GitHub token\",\n    \"agent_skill\": \"Store GitHub PAT via /api/credentials\",\n    \"verification\": {\n      \"type\": \"cache_populated\",\n      \"target\": \"github-prs\"\n    },\n    \"idempotent\": true\n  }\n}"
      },
      {
        "title": "Fetch Types",
        "body": "TypeWhen to UseData Flowserver_codeWidget can call API directlyWidget → server_code → APIagent_refreshAgent must fetch/compute dataAgent → POST /cache → Widget readswebhookExternal service pushes dataExternal → POST /cache → Widget reads\n\nMost widgets should use agent_refresh — the agent fetches data on a schedule and pushes to the cache endpoint."
      },
      {
        "title": "Step 1: Create Widget Definition",
        "body": "POST /api/widgets\nContent-Type: application/json\n\n{\n  \"name\": \"GitHub PRs\",\n  \"slug\": \"github-prs\",\n  \"description\": \"Shows open pull requests\",\n  \"source_code\": \"function Widget({ serverData }) { ... }\",\n  \"default_size\": { \"w\": 2, \"h\": 2 },\n  \"credentials\": [...],\n  \"fetch\": { \"type\": \"agent_refresh\", \"schedule\": \"*/5 * * * *\", ... },\n  \"data_schema\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"prs\": { \"type\": \"array\", \"description\": \"List of PR objects\" },\n      \"fetchedAt\": { \"type\": \"string\", \"format\": \"date-time\" }\n    },\n    \"required\": [\"prs\", \"fetchedAt\"]\n  },\n  \"cache\": { \"ttl_seconds\": 300, ... }\n}\n\ndata_schema (REQUIRED) defines the data contract between the fetcher and the widget. Cache POSTs are validated against it — malformed data returns 400.\n\n⚠️ Always include data_schema when creating widgets. This ensures:\n\nData validation on cache POSTs (400 on schema mismatch)\nClear documentation of expected data structure\nAI agents know the exact format to produce"
      },
      {
        "title": "Step 2: Add to Dashboard",
        "body": "POST /api/widgets/instances\nContent-Type: application/json\n\n{\n  \"type\": \"custom\",\n  \"title\": \"GitHub PRs\",\n  \"custom_widget_id\": \"cw_abc123\",\n  \"config\": { \"owner\": \"acfranzen\", \"repo\": \"libra\" }\n}"
      },
      {
        "title": "Step 3: Populate Cache (for agent_refresh)",
        "body": "POST /api/widgets/github-prs/cache\nContent-Type: application/json\n\n{\n  \"data\": {\n    \"prs\": [...],\n    \"fetchedAt\": \"2026-02-03T14:00:00Z\"\n  }\n}\n\n⚠️ If the widget has a dataSchema, the cache endpoint validates your data against it. Bad data returns 400 with details. Always check the widget's schema before POSTing:\n\nGET /api/widgets/github-prs\n# Response includes dataSchema showing required fields and types"
      },
      {
        "title": "Step 4: Browser Verification (REQUIRED)",
        "body": "⚠️ MANDATORY: Every widget creation and refresh MUST end with browser verification.\n\nNever consider a widget \"done\" until you've visually confirmed it renders correctly on the dashboard.\n\n// REQUIRED: Open dashboard and verify widget renders\nbrowser({ \n  action: 'open', \n  targetUrl: 'http://localhost:3333',\n  profile: 'openclaw'\n});\n\n// Take a snapshot and check the widget\nbrowser({ action: 'snapshot' });\n\n// Look for:\n// 1. Widget is visible on the dashboard\n// 2. Shows actual data, NOT \"Waiting for data...\" or loading spinner\n// 3. Data values match what was pushed to cache\n// 4. No error messages displayed\n// 5. Layout looks correct (not broken/overlapping)\n\nVerification checklist (must ALL be true):\n\nWidget visible on dashboard grid\n Title displays correctly\n Data renders (not stuck on loading)\n Values match cached data\n No error states or broken layouts\n \"Updated X ago\" footer shows recent timestamp\n\nCommon issues and fixes:\n\nSymptomCauseFix\"Waiting for data...\"Cache emptyPOST data to /api/widgets/{slug}/cacheWidget not visibleNot added to dashboardPOST /api/widgets/instancesWrong/old dataSlug mismatchCheck slug matches between definition and cache POSTBroken layoutBad JSX in source_codeCheck widget code for syntax errors\"No data\" after POSTSchema validation failedCheck data matches data_schema\n\nIf verification fails, fix the issue before reporting success."
      },
      {
        "title": "Widget Code Template (agent_refresh)",
        "body": "For agent_refresh widgets, use serverData prop (NOT useData hook):\n\nfunction Widget({ serverData }) {\n  const data = serverData;\n  const loading = !serverData;\n  const error = serverData?.error;\n  \n  if (loading) return <Loading message=\"Waiting for data...\" />;\n  if (error) return <ErrorDisplay message={error} />;\n  \n  // NOTE: Do NOT wrap in <Card> - the framework wrapper (CustomWidgetWrapper) \n  // already provides the outer card with title, refresh button, and footer.\n  // Just render your content directly.\n  return (\n    <div className=\"space-y-3\">\n      <List items={data.prs?.map(pr => ({\n        title: pr.title,\n        subtitle: `#${pr.number} by ${pr.author}`,\n        badge: pr.state\n      })) || []} />\n    </div>\n  );\n}\n\nImportant: The widget wrapper (CustomWidgetWrapper) provides:\n\nOuter <Card> container with header (widget title)\nRefresh button and \"Updated X ago\" footer\nLoading/error states\n\nYour widget code should just render the content — no Card, no CardHeader, no footer.\n\nKey difference: agent_refresh widgets receive data via serverData prop, NOT by calling useData(). The agent pushes data to /api/widgets/{slug}/cache."
      },
      {
        "title": "Server Code (Legacy Alternative)",
        "body": "Prefer agent_refresh over server_code. Only use server_code when the widget MUST execute code at render time (rare).\n\n// Only for fetch.type = \"server_code\" widgets\nconst token = await getCredential('github');\nconst response = await fetch('https://api.github.com/repos/owner/repo/pulls', {\n  headers: { 'Authorization': `Bearer ${token}` }\n});\nreturn await response.json();\n\nAvailable: fetch, getCredential(provider), params, console\nBlocked: require, eval, fs, process, global"
      },
      {
        "title": "Agent Refresh Contract",
        "body": "⚠️ CRITICAL: For agent_refresh widgets, YOU (the OpenClaw agent) are the data collector.\n\nThis is NOT an external API or service. YOU must:\n\nSet up a cron job to remind yourself to collect data on a schedule\nUse your own tools (PTY, exec, browser, etc.) to gather the data\nParse the output into structured JSON\nPOST to the cache endpoint so the widget can display it"
      },
      {
        "title": "The Pattern",
        "body": "┌─────────────────────────────────────────────────────────────┐\n│  Cron fires → Agent wakes up → Agent collects data →        │\n│  Agent POSTs to /cache → Widget displays fresh data         │\n└─────────────────────────────────────────────────────────────┘"
      },
      {
        "title": "Step-by-Step for agent_refresh Widgets",
        "body": "Create the widget with fetch.type = \"agent_refresh\" and detailed fetch.instructions\nSet up a cron job targeting YOUR main session (message is just the slug):\ncron.add({\n  name: \"Widget: My Data Refresh\",\n  schedule: { kind: \"cron\", expr: \"*/15 * * * *\" },\n  payload: { \n    kind: \"systemEvent\", \n    text: \"⚡ WIDGET REFRESH: my-widget\"  // Just the slug!\n  },\n  sessionTarget: \"main\"  // Reminds YOU, not an isolated session\n})\n\n\nWhen you receive the refresh message, look up fetch.instructions from the DB and spawn a subagent:\n// Parse slug from message\nconst slug = message.replace('⚡ WIDGET REFRESH:', '').trim();\n// Query widget's fetch.instructions\nconst widget = db.query('SELECT fetch FROM custom_widgets WHERE slug = ?', slug);\n// Spawn subagent with the instructions\nsessions_spawn({ task: widget.fetch.instructions, model: 'haiku' });\n\n\nThe subagent collects the data using your tools:\n\nexec for shell commands\nPTY for interactive CLI tools (like claude /status)\nbrowser for web scraping\nAPI calls via web_fetch\n\n\nPOST the data to the cache:\nPOST /api/widgets/{slug}/cache\nContent-Type: application/json\n\n{\n  \"data\": {\n    \"myValue\": 42,\n    \"fetchedAt\": \"2026-02-03T18:30:00.000Z\"\n  }\n}"
      },
      {
        "title": "Writing Excellent fetch.instructions",
        "body": "The fetch.instructions field is the single source of truth for how to collect widget data. Write them clearly so any subagent can follow them.\n\nRequired sections:\n\n## Data Collection\nExact commands to run with full paths and flags.\nInclude PTY requirements if interactive.\n\n## Data Transformation\nExact JSON structure expected.\nInclude field descriptions and examples.\n\n## Cache Update\nFull URL, required headers, body format.\n\n## Browser Verification\nConfirm the widget renders correctly.\n\nGood example:\n\n## Data Collection\n```bash\ngog gmail search \"in:inbox\" --json"
      },
      {
        "title": "Data Transformation",
        "body": "Take first 5-8 emails, generate AI summary (3-5 words) for each:\n\n{\n  \"emails\": [{\"id\": \"...\", \"from\": \"...\", \"subject\": \"...\", \"summary\": \"AI summary here\", \"unread\": true}],\n  \"fetchedAt\": \"ISO timestamp\"\n}"
      },
      {
        "title": "Cache Update",
        "body": "POST to: http://localhost:3333/api/widgets/recent-emails/cache\nHeader: Origin: http://localhost:3333\nBody: { \"data\": <object above> }"
      },
      {
        "title": "Browser Verification",
        "body": "Open http://localhost:3333 and confirm widget shows emails with AI summaries.\n\n**Bad example (too vague):**\n\nGet emails and post to cache.\n\n### Real Example: Claude Max Usage Widget\n\nThis widget shows Claude CLI usage stats. The data comes from running `claude` in a PTY and navigating to `/status → Usage`.\n\n**The agent's job every 15 minutes:**\n\nSpawn PTY: exec(\"claude\", { pty: true })\nSend: \"/status\" + Enter\nNavigate to Usage tab (Right arrow keys)\nParse the output: Session %, Week %, Extra %\nPOST to /api/widgets/claude-code-usage/cache\nKill the PTY session\n⚠️ VERIFY: Open browser to http://localhost:3333 and confirm widget displays new data\n\n**This is YOUR responsibility as the agent.** The widget just displays whatever data is in the cache.\n\n### Subagent Task Template for Refreshes\n\nWhen spawning subagents for widget refreshes, always include browser verification:\n\n```javascript\nsessions_spawn({\n  task: `${fetchInstructions}\n\n## REQUIRED: Browser Verification\nAfter posting to cache, verify the widget renders correctly:\n1. Open http://localhost:3333 in browser\n2. Find the widget on the dashboard\n3. Confirm it shows the data you just posted\n4. Report any rendering issues\n\nDo NOT report success until browser verification passes.`,\n  model: 'haiku',\n  label: `${slug}-refresh`\n});"
      },
      {
        "title": "Cache Endpoint",
        "body": "POST /api/widgets/{slug}/cache\nContent-Type: application/json\n\n{\n  \"data\": {\n    \"packages\": 142,\n    \"fetchedAt\": \"2026-02-03T18:30:00.000Z\"\n  }\n}"
      },
      {
        "title": "Immediate Refresh via Webhook",
        "body": "For agent_refresh widgets, users can trigger immediate refreshes via the UI refresh button.\n\nWhen configured with OPENCLAW_GATEWAY_URL and OPENCLAW_TOKEN environment variables, clicking the refresh button will:\n\nStore a refresh request in the database (fallback for polling)\nImmediately POST a wake notification to OpenClaw via /api/sessions/wake\nThe agent receives a prompt to refresh that specific widget now\n\nThis eliminates the delay of waiting for the next heartbeat poll.\n\nEnvironment variables (add to .env.local):\n\nOPENCLAW_GATEWAY_URL=http://localhost:18789\nOPENCLAW_TOKEN=your-gateway-token\n\nHow it works:\n\nUser clicks refresh button on widget\nGlance POSTs to /api/widgets/{slug}/refresh\nIf webhook configured, Glance immediately notifies OpenClaw: ⚡ WIDGET REFRESH: Refresh the \"{slug}\" widget now and POST to cache\nAgent wakes up, collects fresh data, POSTs to cache\nWidget re-renders with updated data\n\nResponse includes webhook status:\n\n{\n  \"status\": \"refresh_requested\",\n  \"webhook_sent\": true,\n  \"fallback_queued\": true\n}\n\nIf webhook fails or isn't configured, the DB fallback ensures the next heartbeat/poll will pick it up."
      },
      {
        "title": "Rules",
        "body": "Always include fetchedAt timestamp\nDon't overwrite on errors - let widget use stale data\nUse main session cron so YOU handle the collection, not an isolated agent\n\n## Credential Requirements Format\n\n### Credential Types\n\n| Type | Storage | Description | Use For |\n|------|---------|-------------|---------|\n| `api_key` | Glance DB (encrypted) | API tokens stored in Glance | GitHub PAT, OpenWeather key |\n| `local_software` | Agent's machine | Software that must be installed | Homebrew, Docker |\n| `agent` | Agent environment | Auth that lives on the agent | `gh` CLI auth, `gcloud` auth |\n| `oauth` | Glance DB | OAuth tokens (future) | Google Calendar |\n\n### Examples\n\n```json\n{\n  \"credentials\": [\n    {\n      \"id\": \"github\",\n      \"type\": \"api_key\",\n      \"name\": \"GitHub Personal Access Token\",\n      \"description\": \"Token with repo scope\",\n      \"obtain_url\": \"https://github.com/settings/tokens\",\n      \"obtain_instructions\": \"Create token with 'repo' scope\"\n    },\n    {\n      \"id\": \"homebrew\",\n      \"type\": \"local_software\",\n      \"name\": \"Homebrew\",\n      \"check_command\": \"which brew\",\n      \"install_url\": \"https://brew.sh\"\n    },\n    {\n      \"id\": \"github_cli\",\n      \"type\": \"agent\",\n      \"name\": \"GitHub CLI\",\n      \"description\": \"Agent needs gh CLI authenticated to GitHub\",\n      \"agent_tool\": \"gh\",\n      \"agent_auth_check\": \"gh auth status\",\n      \"agent_auth_instructions\": \"Run `gh auth login` on the machine running OpenClaw\"\n    }\n  ]\n}\n\nWhen to use agent type: Use for agent_refresh widgets where the agent collects data using CLI tools that have their own auth (like gh, gcloud, aws). These credentials aren't stored in Glance — they exist in the agent's environment."
      },
      {
        "title": "Common Credential Providers",
        "body": "ProviderIDDescriptionGitHubgithubGitHub API (PAT with repo scope)AnthropicanthropicClaude API (Admin key for usage)OpenAIopenaiGPT API (Admin key for usage)OpenWeatheropenweatherWeather data APILinearlinearLinear APINotionnotionNotion API"
      },
      {
        "title": "Export",
        "body": "GET /api/widgets/{slug}/export\n\nReturns: { \"package\": \"!GW1!eJxVj8EKwj...\" }"
      },
      {
        "title": "Import",
        "body": "POST /api/widgets/import\nContent-Type: application/json\n\n{\n  \"package\": \"!GW1!eJxVj8EKwj...\",\n  \"dry_run\": false,\n  \"auto_add_to_dashboard\": true\n}\n\nThe !GW1! prefix indicates Glance Widget v1 format (compressed base64 JSON)."
      },
      {
        "title": "Import Response with Cron",
        "body": "{\n  \"valid\": true,\n  \"widget\": { \"id\": \"cw_abc\", \"slug\": \"homebrew-status\" },\n  \"cronSchedule\": {\n    \"expression\": \"*/15 * * * *\",\n    \"instructions\": \"Run brew list...\",\n    \"slug\": \"homebrew-status\"\n  }\n}\n\nWhen cronSchedule is returned, OpenClaw should register a cron job."
      },
      {
        "title": "Key UI Components",
        "body": "ComponentUse ForCardWidget container (always use className=\"h-full\")ListItems with title/subtitle/badgeStatSingle metric with trend indicatorProgressProgress bars with variantsBadgeStatus labels (success/warning/error)StackFlexbox layout (row/column)GridCSS Grid layoutLoadingLoading spinnerErrorDisplayError with retry button\n\nSee references/components.md for full props."
      },
      {
        "title": "Hooks",
        "body": "// Fetch data (BOTH args required!)\nconst { data, loading, error, refresh } = useData('github', {});\nconst { data } = useData('github', { endpoint: '/pulls', params: { state: 'open' } });\n\n// Get widget config\nconst config = useConfig();\n\n// Widget-local state\nconst { state, setState } = useWidgetState('counter', 0);\n\n⚠️ useData requires both arguments. Pass empty {} if no query needed."
      },
      {
        "title": "Error Handling",
        "body": "if (error?.code === 'CREDENTIAL_MISSING') {\n  return <Card><CardContent>\n    <Icons.Lock className=\"h-8 w-8\" />\n    <p>GitHub token required</p>\n  </CardContent></Card>;\n}\n\nError codes: CREDENTIAL_MISSING, RATE_LIMITED, NETWORK_ERROR, API_ERROR"
      },
      {
        "title": "Best Practices",
        "body": "Always check credentials before creating widgets\nUse meaningful names: github-prs-libra not widget-1\nInclude fetchedAt in all data for staleness tracking\nHandle errors gracefully with retry options\nConfirm actions: \"Done! Widget added to dashboard.\"\nSize appropriately: Lists 1x1, charts 2x2"
      },
      {
        "title": "Reading Dashboard Data",
        "body": "To summarize dashboard for user:\n\n1. GET /api/widgets/instances → list instances\n2. For each: POST /api/widgets/:slug/execute\n3. Combine into natural language summary"
      },
      {
        "title": "⚠️ Rules & Gotchas",
        "body": "Use JSON Schema for generation — docs/schemas/widget-schema.json enforces all required fields\nBrowser verify EVERYTHING — don't report success until you see the widget render correctly\nagent_refresh = YOU collect data — the widget just displays what you POST to cache\nfetch.instructions is the source of truth — cron jobs just send the slug, you look up instructions\nAlways include fetchedAt — widgets need timestamps for \"Updated X ago\" display\ndata_schema is REQUIRED — cache POSTs validate against it, malformed data returns 400\ncredentials is REQUIRED — use empty array [] if no credentials needed\nDon't wrap in Card — the framework provides the outer card, you render content only\nUse Haiku for refresh subagents — mechanical data collection doesn't need Opus\nMark refresh requests as processed — DELETE /api/widgets/{slug}/refresh after handling\nSpawn subagents for refreshes — don't block main session with PTY/long-running work"
      },
      {
        "title": "Environment Variables",
        "body": "VariableDescriptionExampleGLANCE_URLGlance server URLhttp://localhost:3333GLANCE_DATAPath to SQLite database/tmp/glance-test/dataOPENCLAW_GATEWAY_URLFor webhook refresh notificationshttps://localhost:18789OPENCLAW_TOKENGateway auth tokend551fe97..."
      },
      {
        "title": "Learnings (Feb 2026)",
        "body": "Webhook refresh works — Glance POSTs to OpenClaw gateway, agent wakes immediately\nSimple cron messages — just ⚡ WIDGET REFRESH: {slug}, agent looks up instructions\nAI summaries need AI — for recent-emails, YOU generate the summaries, not some API\nicalBuddy for iCloud — gog calendar doesn't work for iCloud, use /opt/homebrew/bin/icalBuddy\nwttr.in for weather — free, no API key, JSON format: wttr.in/City?format=j1"
      }
    ],
    "body": "Glance\n\nAI-extensible personal dashboard. Create custom widgets with natural language — the AI handles data collection.\n\nFeatures\nCustom Widgets — Create widgets via AI with auto-generated JSX\nAgent Refresh — AI collects data on schedule and pushes to cache\nDashboard Export/Import — Share widget configurations\nCredential Management — Secure API key storage\nReal-time Updates — Webhook-triggered instant refreshes\nQuick Start\n# Navigate to skill directory (if installed via ClawHub)\ncd \"$(clawhub list | grep glance | awk '{print $2}')\"\n\n# Or clone directly\ngit clone https://github.com/acfranzen/glance ~/.glance\ncd ~/.glance\n\n# Install dependencies\nnpm install\n\n# Configure environment\ncp .env.example .env.local\n# Edit .env.local with your settings\n\n# Start development server\nnpm run dev\n\n# Or build and start production\nnpm run build && npm start\n\n\nDashboard runs at http://localhost:3333\n\nConfiguration\n\nEdit .env.local:\n\n# Server\nPORT=3333\nAUTH_TOKEN=your-secret-token        # Optional: Bearer token auth\n\n# OpenClaw Integration (for instant widget refresh)\nOPENCLAW_GATEWAY_URL=https://localhost:18789\nOPENCLAW_TOKEN=your-gateway-token\n\n# Database\nDATABASE_PATH=./data/glance.db      # SQLite database location\n\nService Installation (macOS)\n# Create launchd plist\ncat > ~/Library/LaunchAgents/com.glance.dashboard.plist << 'EOF'\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n    <key>Label</key>\n    <string>com.glance.dashboard</string>\n    <key>ProgramArguments</key>\n    <array>\n        <string>/opt/homebrew/bin/npm</string>\n        <string>run</string>\n        <string>dev</string>\n    </array>\n    <key>WorkingDirectory</key>\n    <string>~/.glance</string>\n    <key>RunAtLoad</key>\n    <true/>\n    <key>KeepAlive</key>\n    <true/>\n    <key>StandardOutPath</key>\n    <string>~/.glance/logs/stdout.log</string>\n    <key>StandardErrorPath</key>\n    <string>~/.glance/logs/stderr.log</string>\n</dict>\n</plist>\nEOF\n\n# Load service\nmkdir -p ~/.glance/logs\nlaunchctl load ~/Library/LaunchAgents/com.glance.dashboard.plist\n\n# Service commands\nlaunchctl start com.glance.dashboard\nlaunchctl stop com.glance.dashboard\nlaunchctl unload ~/Library/LaunchAgents/com.glance.dashboard.plist\n\nEnvironment Variables\nVariable\tDescription\tDefault\nPORT\tServer port\t3333\nAUTH_TOKEN\tBearer token for API auth\t—\nDATABASE_PATH\tSQLite database path\t./data/glance.db\nOPENCLAW_GATEWAY_URL\tOpenClaw gateway for webhooks\t—\nOPENCLAW_TOKEN\tOpenClaw auth token\t—\nRequirements\nNode.js 20+\nnpm or pnpm\nSQLite (bundled)\nWidget Skill\n\nCreate and manage dashboard widgets. Most widgets use agent_refresh — you collect the data.\n\nQuick Start\n# Check Glance is running (list widgets)\ncurl -s -H \"Origin: $GLANCE_URL\" \"$GLANCE_URL/api/widgets\" | jq '.custom_widgets[].slug'\n\n# Auth note: Local requests with Origin header bypass Bearer token auth\n# For external access, use: -H \"Authorization: Bearer $GLANCE_TOKEN\"\n\n# Refresh a widget (look up instructions, collect data, POST to cache)\nsqlite3 $GLANCE_DATA/glance.db \"SELECT json_extract(fetch, '$.instructions') FROM custom_widgets WHERE slug = 'my-widget'\"\n# Follow the instructions, then:\ncurl -X POST \"$GLANCE_URL/api/widgets/my-widget/cache\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Origin: $GLANCE_URL\" \\\n  -d '{\"data\": {\"value\": 42, \"fetchedAt\": \"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'\"}}'\n\n# Verify in browser\nbrowser action:open targetUrl:\"$GLANCE_URL\"\n\nAI Structured Output Generation (REQUIRED)\n\nWhen generating widget definitions, use the JSON Schema at docs/schemas/widget-schema.json with your AI model's structured output mode:\n\nAnthropic: Use tool_use with the schema\nOpenAI: Use response_format: { type: \"json_schema\", schema }\n\nThe schema enforces all required fields at generation time — malformed widgets cannot be produced.\n\nRequired Fields Checklist\n\nEvery widget MUST have these fields (the schema enforces them):\n\nField\tType\tNotes\nname\tstring\tNon-empty, human-readable\nslug\tstring\tLowercase kebab-case (my-widget)\nsource_code\tstring\tValid JSX with Widget function\ndefault_size\t{ w: 1-12, h: 1-20 }\tGrid units\nmin_size\t{ w: 1-12, h: 1-20 }\tCannot resize smaller\nfetch.type\tenum\t\"server_code\" | \"webhook\" | \"agent_refresh\"\nfetch.instructions\tstring\tREQUIRED if type is agent_refresh\nfetch.schedule\tstring\tREQUIRED if type is agent_refresh (cron)\ndata_schema.type\t\"object\"\tAlways object\ndata_schema.properties\tobject\tDefine each field\ndata_schema.required\tarray\tMUST include \"fetchedAt\"\ncredentials\tarray\tUse [] if none needed\nExample: Minimal Valid Widget\n{\n  \"name\": \"My Widget\",\n  \"slug\": \"my-widget\",\n  \"source_code\": \"function Widget({ serverData }) { return <div>{serverData?.value}</div>; }\",\n  \"default_size\": { \"w\": 2, \"h\": 2 },\n  \"min_size\": { \"w\": 1, \"h\": 1 },\n  \"fetch\": {\n    \"type\": \"agent_refresh\",\n    \"schedule\": \"*/15 * * * *\",\n    \"instructions\": \"## Data Collection\\nCollect the data...\\n\\n## Cache Update\\nPOST to /api/widgets/my-widget/cache\"\n  },\n  \"data_schema\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"value\": { \"type\": \"number\" },\n      \"fetchedAt\": { \"type\": \"string\", \"format\": \"date-time\" }\n    },\n    \"required\": [\"value\", \"fetchedAt\"]\n  },\n  \"credentials\": []\n}\n\n⚠️ Widget Creation Checklist (MANDATORY)\n\nEvery widget must complete ALL steps before being considered done:\n\n□ Step 1: Create widget definition (POST /api/widgets)\n    - source_code with Widget function\n    - data_schema (REQUIRED for validation)\n    - fetch config (type + instructions for agent_refresh)\n    \n□ Step 2: Add to dashboard (POST /api/widgets/instances)\n    - custom_widget_id matches definition\n    - title and config set\n    \n□ Step 3: Populate cache (for agent_refresh widgets)\n    - Data matches data_schema exactly\n    - Includes fetchedAt timestamp\n    \n□ Step 4: Set up cron job (for agent_refresh widgets)\n    - Simple message: \"⚡ WIDGET REFRESH: {slug}\"\n    - Appropriate schedule (*/15 or */30 typically)\n    \n□ Step 5: BROWSER VERIFICATION (MANDATORY)\n    - Open http://localhost:3333\n    - Widget is visible on dashboard\n    - Shows actual data (not loading spinner)\n    - Data values match what was cached\n    - No errors or broken layouts\n    \n⛔ DO NOT report widget as complete until Step 5 passes!\n\nQuick Reference\nFull SDK docs: See docs/widget-sdk.md in the Glance repo\nComponent list: See references/components.md\nWidget Package Structure\nWidget Package\n├── meta (name, slug, description, author, version)\n├── widget (source_code, default_size, min_size)\n├── fetch (server_code | webhook | agent_refresh)\n├── dataSchema? (JSON Schema for cached data - validates on POST)\n├── cache (ttl, staleness, fallback)\n├── credentials[] (API keys, local software requirements)\n├── config_schema? (user options)\n└── error? (retry, fallback, timeout)\n\nFetch Type Decision Tree\nIs data available via API that the widget can call?\n├── YES → Use server_code\n└── NO → Does an external service push data?\n    ├── YES → Use webhook\n    └── NO → Use agent_refresh (YOU collect it)\n\nScenario\tFetch Type\tWho Collects Data?\nPublic/authenticated API\tserver_code\tWidget calls API at render\nExternal service pushes data\twebhook\tExternal service POSTs to cache\nLocal CLI tools\tagent_refresh\tYOU (the agent) via PTY/exec\nInteractive terminals\tagent_refresh\tYOU (the agent) via PTY\nComputed/aggregated data\tagent_refresh\tYOU (the agent) on a schedule\n\n⚠️ agent_refresh means YOU are the data source. You set up a cron to remind yourself, then YOU collect the data using your tools (exec, PTY, browser, etc.) and POST it to the cache.\n\nAPI Endpoints\nWidget Definitions\nMethod\tEndpoint\tDescription\nPOST\t/api/widgets\tCreate widget definition\nGET\t/api/widgets\tList all definitions\nGET\t/api/widgets/:slug\tGet single definition\nPATCH\t/api/widgets/:slug\tUpdate definition\nDELETE\t/api/widgets/:slug\tDelete definition\nWidget Instances (Dashboard)\nMethod\tEndpoint\tDescription\nPOST\t/api/widgets/instances\tAdd widget to dashboard\nGET\t/api/widgets/instances\tList dashboard widgets\nPATCH\t/api/widgets/instances/:id\tUpdate instance (config, position)\nDELETE\t/api/widgets/instances/:id\tRemove from dashboard\nCredentials\nMethod\tEndpoint\tDescription\nGET\t/api/credentials\tList credentials + status\nPOST\t/api/credentials\tStore credential\nDELETE\t/api/credentials/:id\tDelete credential\nCreating a Widget\nFull Widget Package Structure\n{\n  \"name\": \"GitHub PRs\",\n  \"slug\": \"github-prs\",\n  \"description\": \"Shows open pull requests\",\n  \n  \"source_code\": \"function Widget({ serverData }) { ... }\",\n  \"default_size\": { \"w\": 2, \"h\": 2 },\n  \"min_size\": { \"w\": 1, \"h\": 1 },\n  \"refresh_interval\": 300,\n  \n  \"credentials\": [\n    {\n      \"id\": \"github\",\n      \"type\": \"api_key\",\n      \"name\": \"GitHub Personal Access Token\",\n      \"description\": \"Token with repo scope\",\n      \"obtain_url\": \"https://github.com/settings/tokens\"\n    }\n  ],\n  \n  \"fetch\": {\n    \"type\": \"agent_refresh\",\n    \"schedule\": \"*/5 * * * *\",\n    \"instructions\": \"Fetch open PRs from GitHub API and POST to cache endpoint\",\n    \"expected_freshness_seconds\": 300,\n    \"max_staleness_seconds\": 900\n  },\n  \n  \"cache\": {\n    \"ttl_seconds\": 300,\n    \"max_staleness_seconds\": 900,\n    \"storage\": \"sqlite\",\n    \"on_error\": \"use_stale\"\n  },\n  \n  \"setup\": {\n    \"description\": \"Configure GitHub token\",\n    \"agent_skill\": \"Store GitHub PAT via /api/credentials\",\n    \"verification\": {\n      \"type\": \"cache_populated\",\n      \"target\": \"github-prs\"\n    },\n    \"idempotent\": true\n  }\n}\n\nFetch Types\nType\tWhen to Use\tData Flow\nserver_code\tWidget can call API directly\tWidget → server_code → API\nagent_refresh\tAgent must fetch/compute data\tAgent → POST /cache → Widget reads\nwebhook\tExternal service pushes data\tExternal → POST /cache → Widget reads\n\nMost widgets should use agent_refresh — the agent fetches data on a schedule and pushes to the cache endpoint.\n\nStep 1: Create Widget Definition\nPOST /api/widgets\nContent-Type: application/json\n\n{\n  \"name\": \"GitHub PRs\",\n  \"slug\": \"github-prs\",\n  \"description\": \"Shows open pull requests\",\n  \"source_code\": \"function Widget({ serverData }) { ... }\",\n  \"default_size\": { \"w\": 2, \"h\": 2 },\n  \"credentials\": [...],\n  \"fetch\": { \"type\": \"agent_refresh\", \"schedule\": \"*/5 * * * *\", ... },\n  \"data_schema\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"prs\": { \"type\": \"array\", \"description\": \"List of PR objects\" },\n      \"fetchedAt\": { \"type\": \"string\", \"format\": \"date-time\" }\n    },\n    \"required\": [\"prs\", \"fetchedAt\"]\n  },\n  \"cache\": { \"ttl_seconds\": 300, ... }\n}\n\n\ndata_schema (REQUIRED) defines the data contract between the fetcher and the widget. Cache POSTs are validated against it — malformed data returns 400.\n\n⚠️ Always include data_schema when creating widgets. This ensures:\n\nData validation on cache POSTs (400 on schema mismatch)\nClear documentation of expected data structure\nAI agents know the exact format to produce\nStep 2: Add to Dashboard\nPOST /api/widgets/instances\nContent-Type: application/json\n\n{\n  \"type\": \"custom\",\n  \"title\": \"GitHub PRs\",\n  \"custom_widget_id\": \"cw_abc123\",\n  \"config\": { \"owner\": \"acfranzen\", \"repo\": \"libra\" }\n}\n\nStep 3: Populate Cache (for agent_refresh)\nPOST /api/widgets/github-prs/cache\nContent-Type: application/json\n\n{\n  \"data\": {\n    \"prs\": [...],\n    \"fetchedAt\": \"2026-02-03T14:00:00Z\"\n  }\n}\n\n\n⚠️ If the widget has a dataSchema, the cache endpoint validates your data against it. Bad data returns 400 with details. Always check the widget's schema before POSTing:\n\nGET /api/widgets/github-prs\n# Response includes dataSchema showing required fields and types\n\nStep 4: Browser Verification (REQUIRED)\n\n⚠️ MANDATORY: Every widget creation and refresh MUST end with browser verification.\n\nNever consider a widget \"done\" until you've visually confirmed it renders correctly on the dashboard.\n\n// REQUIRED: Open dashboard and verify widget renders\nbrowser({ \n  action: 'open', \n  targetUrl: 'http://localhost:3333',\n  profile: 'openclaw'\n});\n\n// Take a snapshot and check the widget\nbrowser({ action: 'snapshot' });\n\n// Look for:\n// 1. Widget is visible on the dashboard\n// 2. Shows actual data, NOT \"Waiting for data...\" or loading spinner\n// 3. Data values match what was pushed to cache\n// 4. No error messages displayed\n// 5. Layout looks correct (not broken/overlapping)\n\n\nVerification checklist (must ALL be true):\n\n Widget visible on dashboard grid\n Title displays correctly\n Data renders (not stuck on loading)\n Values match cached data\n No error states or broken layouts\n \"Updated X ago\" footer shows recent timestamp\n\nCommon issues and fixes:\n\nSymptom\tCause\tFix\n\"Waiting for data...\"\tCache empty\tPOST data to /api/widgets/{slug}/cache\nWidget not visible\tNot added to dashboard\tPOST /api/widgets/instances\nWrong/old data\tSlug mismatch\tCheck slug matches between definition and cache POST\nBroken layout\tBad JSX in source_code\tCheck widget code for syntax errors\n\"No data\" after POST\tSchema validation failed\tCheck data matches data_schema\n\nIf verification fails, fix the issue before reporting success.\n\nWidget Code Template (agent_refresh)\n\nFor agent_refresh widgets, use serverData prop (NOT useData hook):\n\nfunction Widget({ serverData }) {\n  const data = serverData;\n  const loading = !serverData;\n  const error = serverData?.error;\n  \n  if (loading) return <Loading message=\"Waiting for data...\" />;\n  if (error) return <ErrorDisplay message={error} />;\n  \n  // NOTE: Do NOT wrap in <Card> - the framework wrapper (CustomWidgetWrapper) \n  // already provides the outer card with title, refresh button, and footer.\n  // Just render your content directly.\n  return (\n    <div className=\"space-y-3\">\n      <List items={data.prs?.map(pr => ({\n        title: pr.title,\n        subtitle: `#${pr.number} by ${pr.author}`,\n        badge: pr.state\n      })) || []} />\n    </div>\n  );\n}\n\n\nImportant: The widget wrapper (CustomWidgetWrapper) provides:\n\nOuter <Card> container with header (widget title)\nRefresh button and \"Updated X ago\" footer\nLoading/error states\n\nYour widget code should just render the content — no Card, no CardHeader, no footer.\n\nKey difference: agent_refresh widgets receive data via serverData prop, NOT by calling useData(). The agent pushes data to /api/widgets/{slug}/cache.\n\nServer Code (Legacy Alternative)\n\nPrefer agent_refresh over server_code. Only use server_code when the widget MUST execute code at render time (rare).\n\n// Only for fetch.type = \"server_code\" widgets\nconst token = await getCredential('github');\nconst response = await fetch('https://api.github.com/repos/owner/repo/pulls', {\n  headers: { 'Authorization': `Bearer ${token}` }\n});\nreturn await response.json();\n\n\nAvailable: fetch, getCredential(provider), params, console Blocked: require, eval, fs, process, global\n\nAgent Refresh Contract\n\n⚠️ CRITICAL: For agent_refresh widgets, YOU (the OpenClaw agent) are the data collector.\n\nThis is NOT an external API or service. YOU must:\n\nSet up a cron job to remind yourself to collect data on a schedule\nUse your own tools (PTY, exec, browser, etc.) to gather the data\nParse the output into structured JSON\nPOST to the cache endpoint so the widget can display it\nThe Pattern\n┌─────────────────────────────────────────────────────────────┐\n│  Cron fires → Agent wakes up → Agent collects data →        │\n│  Agent POSTs to /cache → Widget displays fresh data         │\n└─────────────────────────────────────────────────────────────┘\n\nStep-by-Step for agent_refresh Widgets\nCreate the widget with fetch.type = \"agent_refresh\" and detailed fetch.instructions\nSet up a cron job targeting YOUR main session (message is just the slug):\ncron.add({\n  name: \"Widget: My Data Refresh\",\n  schedule: { kind: \"cron\", expr: \"*/15 * * * *\" },\n  payload: { \n    kind: \"systemEvent\", \n    text: \"⚡ WIDGET REFRESH: my-widget\"  // Just the slug!\n  },\n  sessionTarget: \"main\"  // Reminds YOU, not an isolated session\n})\n\nWhen you receive the refresh message, look up fetch.instructions from the DB and spawn a subagent:\n// Parse slug from message\nconst slug = message.replace('⚡ WIDGET REFRESH:', '').trim();\n// Query widget's fetch.instructions\nconst widget = db.query('SELECT fetch FROM custom_widgets WHERE slug = ?', slug);\n// Spawn subagent with the instructions\nsessions_spawn({ task: widget.fetch.instructions, model: 'haiku' });\n\nThe subagent collects the data using your tools:\nexec for shell commands\nPTY for interactive CLI tools (like claude /status)\nbrowser for web scraping\nAPI calls via web_fetch\nPOST the data to the cache:\nPOST /api/widgets/{slug}/cache\nContent-Type: application/json\n\n{\n  \"data\": {\n    \"myValue\": 42,\n    \"fetchedAt\": \"2026-02-03T18:30:00.000Z\"\n  }\n}\n\nWriting Excellent fetch.instructions\n\nThe fetch.instructions field is the single source of truth for how to collect widget data. Write them clearly so any subagent can follow them.\n\nRequired sections:\n\n## Data Collection\nExact commands to run with full paths and flags.\nInclude PTY requirements if interactive.\n\n## Data Transformation\nExact JSON structure expected.\nInclude field descriptions and examples.\n\n## Cache Update\nFull URL, required headers, body format.\n\n## Browser Verification\nConfirm the widget renders correctly.\n\n\nGood example:\n\n## Data Collection\n```bash\ngog gmail search \"in:inbox\" --json\n\nData Transformation\n\nTake first 5-8 emails, generate AI summary (3-5 words) for each:\n\n{\n  \"emails\": [{\"id\": \"...\", \"from\": \"...\", \"subject\": \"...\", \"summary\": \"AI summary here\", \"unread\": true}],\n  \"fetchedAt\": \"ISO timestamp\"\n}\n\nCache Update\n\nPOST to: http://localhost:3333/api/widgets/recent-emails/cache Header: Origin: http://localhost:3333 Body: { \"data\": <object above> }\n\nBrowser Verification\n\nOpen http://localhost:3333 and confirm widget shows emails with AI summaries.\n\n\n**Bad example (too vague):**\n\n\nGet emails and post to cache.\n\n\n### Real Example: Claude Max Usage Widget\n\nThis widget shows Claude CLI usage stats. The data comes from running `claude` in a PTY and navigating to `/status → Usage`.\n\n**The agent's job every 15 minutes:**\n\nSpawn PTY: exec(\"claude\", { pty: true })\nSend: \"/status\" + Enter\nNavigate to Usage tab (Right arrow keys)\nParse the output: Session %, Week %, Extra %\nPOST to /api/widgets/claude-code-usage/cache\nKill the PTY session\n⚠️ VERIFY: Open browser to http://localhost:3333 and confirm widget displays new data\n\n**This is YOUR responsibility as the agent.** The widget just displays whatever data is in the cache.\n\n### Subagent Task Template for Refreshes\n\nWhen spawning subagents for widget refreshes, always include browser verification:\n\n```javascript\nsessions_spawn({\n  task: `${fetchInstructions}\n\n## REQUIRED: Browser Verification\nAfter posting to cache, verify the widget renders correctly:\n1. Open http://localhost:3333 in browser\n2. Find the widget on the dashboard\n3. Confirm it shows the data you just posted\n4. Report any rendering issues\n\nDo NOT report success until browser verification passes.`,\n  model: 'haiku',\n  label: `${slug}-refresh`\n});\n\nCache Endpoint\nPOST /api/widgets/{slug}/cache\nContent-Type: application/json\n\n{\n  \"data\": {\n    \"packages\": 142,\n    \"fetchedAt\": \"2026-02-03T18:30:00.000Z\"\n  }\n}\n\nImmediate Refresh via Webhook\n\nFor agent_refresh widgets, users can trigger immediate refreshes via the UI refresh button.\n\nWhen configured with OPENCLAW_GATEWAY_URL and OPENCLAW_TOKEN environment variables, clicking the refresh button will:\n\nStore a refresh request in the database (fallback for polling)\nImmediately POST a wake notification to OpenClaw via /api/sessions/wake\nThe agent receives a prompt to refresh that specific widget now\n\nThis eliminates the delay of waiting for the next heartbeat poll.\n\nEnvironment variables (add to .env.local):\n\nOPENCLAW_GATEWAY_URL=http://localhost:18789\nOPENCLAW_TOKEN=your-gateway-token\n\n\nHow it works:\n\nUser clicks refresh button on widget\nGlance POSTs to /api/widgets/{slug}/refresh\nIf webhook configured, Glance immediately notifies OpenClaw: ⚡ WIDGET REFRESH: Refresh the \"{slug}\" widget now and POST to cache\nAgent wakes up, collects fresh data, POSTs to cache\nWidget re-renders with updated data\n\nResponse includes webhook status:\n\n{\n  \"status\": \"refresh_requested\",\n  \"webhook_sent\": true,\n  \"fallback_queued\": true\n}\n\n\nIf webhook fails or isn't configured, the DB fallback ensures the next heartbeat/poll will pick it up.\n\nRules\nAlways include fetchedAt timestamp\nDon't overwrite on errors - let widget use stale data\nUse main session cron so YOU handle the collection, not an isolated agent\n\n## Credential Requirements Format\n\n### Credential Types\n\n| Type | Storage | Description | Use For |\n|------|---------|-------------|---------|\n| `api_key` | Glance DB (encrypted) | API tokens stored in Glance | GitHub PAT, OpenWeather key |\n| `local_software` | Agent's machine | Software that must be installed | Homebrew, Docker |\n| `agent` | Agent environment | Auth that lives on the agent | `gh` CLI auth, `gcloud` auth |\n| `oauth` | Glance DB | OAuth tokens (future) | Google Calendar |\n\n### Examples\n\n```json\n{\n  \"credentials\": [\n    {\n      \"id\": \"github\",\n      \"type\": \"api_key\",\n      \"name\": \"GitHub Personal Access Token\",\n      \"description\": \"Token with repo scope\",\n      \"obtain_url\": \"https://github.com/settings/tokens\",\n      \"obtain_instructions\": \"Create token with 'repo' scope\"\n    },\n    {\n      \"id\": \"homebrew\",\n      \"type\": \"local_software\",\n      \"name\": \"Homebrew\",\n      \"check_command\": \"which brew\",\n      \"install_url\": \"https://brew.sh\"\n    },\n    {\n      \"id\": \"github_cli\",\n      \"type\": \"agent\",\n      \"name\": \"GitHub CLI\",\n      \"description\": \"Agent needs gh CLI authenticated to GitHub\",\n      \"agent_tool\": \"gh\",\n      \"agent_auth_check\": \"gh auth status\",\n      \"agent_auth_instructions\": \"Run `gh auth login` on the machine running OpenClaw\"\n    }\n  ]\n}\n\n\nWhen to use agent type: Use for agent_refresh widgets where the agent collects data using CLI tools that have their own auth (like gh, gcloud, aws). These credentials aren't stored in Glance — they exist in the agent's environment.\n\nCommon Credential Providers\nProvider\tID\tDescription\nGitHub\tgithub\tGitHub API (PAT with repo scope)\nAnthropic\tanthropic\tClaude API (Admin key for usage)\nOpenAI\topenai\tGPT API (Admin key for usage)\nOpenWeather\topenweather\tWeather data API\nLinear\tlinear\tLinear API\nNotion\tnotion\tNotion API\nExport/Import Packages\nExport\nGET /api/widgets/{slug}/export\n\n\nReturns: { \"package\": \"!GW1!eJxVj8EKwj...\" }\n\nImport\nPOST /api/widgets/import\nContent-Type: application/json\n\n{\n  \"package\": \"!GW1!eJxVj8EKwj...\",\n  \"dry_run\": false,\n  \"auto_add_to_dashboard\": true\n}\n\n\nThe !GW1! prefix indicates Glance Widget v1 format (compressed base64 JSON).\n\nImport Response with Cron\n{\n  \"valid\": true,\n  \"widget\": { \"id\": \"cw_abc\", \"slug\": \"homebrew-status\" },\n  \"cronSchedule\": {\n    \"expression\": \"*/15 * * * *\",\n    \"instructions\": \"Run brew list...\",\n    \"slug\": \"homebrew-status\"\n  }\n}\n\n\nWhen cronSchedule is returned, OpenClaw should register a cron job.\n\nKey UI Components\nComponent\tUse For\nCard\tWidget container (always use className=\"h-full\")\nList\tItems with title/subtitle/badge\nStat\tSingle metric with trend indicator\nProgress\tProgress bars with variants\nBadge\tStatus labels (success/warning/error)\nStack\tFlexbox layout (row/column)\nGrid\tCSS Grid layout\nLoading\tLoading spinner\nErrorDisplay\tError with retry button\n\nSee references/components.md for full props.\n\nHooks\n// Fetch data (BOTH args required!)\nconst { data, loading, error, refresh } = useData('github', {});\nconst { data } = useData('github', { endpoint: '/pulls', params: { state: 'open' } });\n\n// Get widget config\nconst config = useConfig();\n\n// Widget-local state\nconst { state, setState } = useWidgetState('counter', 0);\n\n\n⚠️ useData requires both arguments. Pass empty {} if no query needed.\n\nError Handling\nif (error?.code === 'CREDENTIAL_MISSING') {\n  return <Card><CardContent>\n    <Icons.Lock className=\"h-8 w-8\" />\n    <p>GitHub token required</p>\n  </CardContent></Card>;\n}\n\n\nError codes: CREDENTIAL_MISSING, RATE_LIMITED, NETWORK_ERROR, API_ERROR\n\nBest Practices\nAlways check credentials before creating widgets\nUse meaningful names: github-prs-libra not widget-1\nInclude fetchedAt in all data for staleness tracking\nHandle errors gracefully with retry options\nConfirm actions: \"Done! Widget added to dashboard.\"\nSize appropriately: Lists 1x1, charts 2x2\nReading Dashboard Data\n\nTo summarize dashboard for user:\n\n1. GET /api/widgets/instances → list instances\n2. For each: POST /api/widgets/:slug/execute\n3. Combine into natural language summary\n\n⚠️ Rules & Gotchas\nUse JSON Schema for generation — docs/schemas/widget-schema.json enforces all required fields\nBrowser verify EVERYTHING — don't report success until you see the widget render correctly\nagent_refresh = YOU collect data — the widget just displays what you POST to cache\nfetch.instructions is the source of truth — cron jobs just send the slug, you look up instructions\nAlways include fetchedAt — widgets need timestamps for \"Updated X ago\" display\ndata_schema is REQUIRED — cache POSTs validate against it, malformed data returns 400\ncredentials is REQUIRED — use empty array [] if no credentials needed\nDon't wrap in Card — the framework provides the outer card, you render content only\nUse Haiku for refresh subagents — mechanical data collection doesn't need Opus\nMark refresh requests as processed — DELETE /api/widgets/{slug}/refresh after handling\nSpawn subagents for refreshes — don't block main session with PTY/long-running work\nEnvironment Variables\nVariable\tDescription\tExample\nGLANCE_URL\tGlance server URL\thttp://localhost:3333\nGLANCE_DATA\tPath to SQLite database\t/tmp/glance-test/data\nOPENCLAW_GATEWAY_URL\tFor webhook refresh notifications\thttps://localhost:18789\nOPENCLAW_TOKEN\tGateway auth token\td551fe97...\nLearnings (Feb 2026)\nWebhook refresh works — Glance POSTs to OpenClaw gateway, agent wakes immediately\nSimple cron messages — just ⚡ WIDGET REFRESH: {slug}, agent looks up instructions\nAI summaries need AI — for recent-emails, YOU generate the summaries, not some API\nicalBuddy for iCloud — gog calendar doesn't work for iCloud, use /opt/homebrew/bin/icalBuddy\nwttr.in for weather — free, no API key, JSON format: wttr.in/City?format=j1"
  },
  "trust": {
    "sourceLabel": "tencent",
    "provenanceUrl": "https://clawhub.ai/acfranzen/glance",
    "publisherUrl": "https://clawhub.ai/acfranzen/glance",
    "owner": "acfranzen",
    "version": "1.0.0",
    "license": null,
    "verificationStatus": "Indexed source record"
  },
  "links": {
    "detailUrl": "https://openagent3.xyz/skills/glance",
    "downloadUrl": "https://openagent3.xyz/downloads/glance",
    "agentUrl": "https://openagent3.xyz/skills/glance/agent",
    "manifestUrl": "https://openagent3.xyz/skills/glance/agent.json",
    "briefUrl": "https://openagent3.xyz/skills/glance/agent.md"
  }
}