Back to Skills
    šŸ¦ž

    glance

    Create, update, and manage Glance dashboard widgets.

    By @acfranzen
    View on GitHub
    SKILL.md
    ---
    name: glance
    description: "Create, update, and manage Glance dashboard widgets. Use when user wants to: add something to their dashboard, create a widget, track data visually, show metrics/stats, display API data, or monitor usage."
    metadata:
      openclaw:
        emoji: "šŸ–„ļø"
        homepage: "https://github.com/acfranzen/glance"
        requires:
          env: ["GLANCE_URL"]
          bins: ["curl"]
        primaryEnv: GLANCE_URL
    ---
    
    # Glance
    
    AI-extensible personal dashboard. Create custom widgets with natural language — the AI handles data collection.
    
    ## Features
    
    - **Custom Widgets** — Create widgets via AI with auto-generated JSX
    - **Agent Refresh** — AI collects data on schedule and pushes to cache
    - **Dashboard Export/Import** — Share widget configurations
    - **Credential Management** — Secure API key storage
    - **Real-time Updates** — Webhook-triggered instant refreshes
    
    ## Quick Start
    
    ```bash
    # Navigate to skill directory (if installed via ClawHub)
    cd "$(clawhub list | grep glance | awk '{print $2}')"
    
    # Or clone directly
    git clone https://github.com/acfranzen/glance ~/.glance
    cd ~/.glance
    
    # Install dependencies
    npm install
    
    # Configure environment
    cp .env.example .env.local
    # Edit .env.local with your settings
    
    # Start development server
    npm run dev
    
    # Or build and start production
    npm run build && npm start
    ```
    
    Dashboard runs at **http://localhost:3333**
    
    ## Configuration
    
    Edit `.env.local`:
    
    ```bash
    # Server
    PORT=3333
    AUTH_TOKEN=your-secret-token        # Optional: Bearer token auth
    
    # OpenClaw Integration (for instant widget refresh)
    OPENCLAW_GATEWAY_URL=https://localhost:18789
    OPENCLAW_TOKEN=your-gateway-token
    
    # Database
    DATABASE_PATH=./data/glance.db      # SQLite database location
    ```
    
    ## Service Installation (macOS)
    
    ```bash
    # Create launchd plist
    cat > ~/Library/LaunchAgents/com.glance.dashboard.plist << 'EOF'
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
        <key>Label</key>
        <string>com.glance.dashboard</string>
        <key>ProgramArguments</key>
        <array>
            <string>/opt/homebrew/bin/npm</string>
            <string>run</string>
            <string>dev</string>
        </array>
        <key>WorkingDirectory</key>
        <string>~/.glance</string>
        <key>RunAtLoad</key>
        <true/>
        <key>KeepAlive</key>
        <true/>
        <key>StandardOutPath</key>
        <string>~/.glance/logs/stdout.log</string>
        <key>StandardErrorPath</key>
        <string>~/.glance/logs/stderr.log</string>
    </dict>
    </plist>
    EOF
    
    # Load service
    mkdir -p ~/.glance/logs
    launchctl load ~/Library/LaunchAgents/com.glance.dashboard.plist
    
    # Service commands
    launchctl start com.glance.dashboard
    launchctl stop com.glance.dashboard
    launchctl unload ~/Library/LaunchAgents/com.glance.dashboard.plist
    ```
    
    ## Environment Variables
    
    | Variable | Description | Default |
    |----------|-------------|---------|
    | `PORT` | Server port | `3333` |
    | `AUTH_TOKEN` | Bearer token for API auth | — |
    | `DATABASE_PATH` | SQLite database path | `./data/glance.db` |
    | `OPENCLAW_GATEWAY_URL` | OpenClaw gateway for webhooks | — |
    | `OPENCLAW_TOKEN` | OpenClaw auth token | — |
    
    ## Requirements
    
    - Node.js 20+
    - npm or pnpm
    - SQLite (bundled)
    
    ---
    
    # Widget Skill
    
    Create and manage dashboard widgets. Most widgets use `agent_refresh` — **you** collect the data.
    
    ## Quick Start
    
    ```bash
    # Check Glance is running (list widgets)
    curl -s -H "Origin: $GLANCE_URL" "$GLANCE_URL/api/widgets" | jq '.custom_widgets[].slug'
    
    # Auth note: Local requests with Origin header bypass Bearer token auth
    # For external access, use: -H "Authorization: Bearer $GLANCE_TOKEN"
    
    # Refresh a widget (look up instructions, collect data, POST to cache)
    sqlite3 $GLANCE_DATA/glance.db "SELECT json_extract(fetch, '$.instructions') FROM custom_widgets WHERE slug = 'my-widget'"
    # Follow the instructions, then:
    curl -X POST "$GLANCE_URL/api/widgets/my-widget/cache" \
      -H "Content-Type: application/json" \
      -H "Origin: $GLANCE_URL" \
      -d '{"data": {"value": 42, "fetchedAt": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}}'
    
    # Verify in browser
    browser action:open targetUrl:"$GLANCE_URL"
    ```
    
    ## AI Structured Output Generation (REQUIRED)
    
    When generating widget definitions, **use the JSON Schema** at `docs/schemas/widget-schema.json` with your AI model's structured output mode:
    - **Anthropic**: Use `tool_use` with the schema
    - **OpenAI**: Use `response_format: { type: "json_schema", schema }`
    
    The schema enforces all required fields at generation time — malformed widgets cannot be produced.
    
    ### Required Fields Checklist
    Every widget **MUST** have these fields (the schema enforces them):
    
    | Field | Type | Notes |
    |-------|------|-------|
    | `name` | string | Non-empty, human-readable |
    | `slug` | string | Lowercase kebab-case (`my-widget`) |
    | `source_code` | string | Valid JSX with Widget function |
    | `default_size` | `{ w: 1-12, h: 1-20 }` | Grid units |
    | `min_size` | `{ w: 1-12, h: 1-20 }` | Cannot resize smaller |
    | `fetch.type` | enum | `"server_code"` \| `"webhook"` \| `"agent_refresh"` |
    | `fetch.instructions` | string | **REQUIRED if type is `agent_refresh`** |
    | `fetch.schedule` | string | **REQUIRED if type is `agent_refresh`** (cron) |
    | `data_schema.type` | `"object"` | Always object |
    | `data_schema.properties` | object | Define each field |
    | `data_schema.required` | array | **MUST include `"fetchedAt"`** |
    | `credentials` | array | Use `[]` if none needed |
    
    ### Example: Minimal Valid Widget
    
    ```json
    {
      "name": "My Widget",
      "slug": "my-widget",
      "source_code": "function Widget({ serverData }) { return <div>{serverData?.value}</div>; }",
      "default_size": { "w": 2, "h": 2 },
      "min_size": { "w": 1, "h": 1 },
      "fetch": {
        "type": "agent_refresh",
        "schedule": "*/15 * * * *",
        "instructions": "## Data Collection\nCollect the data...\n\n## Cache Update\nPOST to /api/widgets/my-widget/cache"
      },
      "data_schema": {
        "type": "object",
        "properties": {
          "value": { "type": "number" },
          "fetchedAt": { "type": "string", "format": "date-time" }
        },
        "required": ["value", "fetchedAt"]
      },
      "credentials": []
    }
    ```
    
    ---
    
    ## āš ļø Widget Creation Checklist (MANDATORY)
    
    Every widget must complete ALL steps before being considered done:
    
    ```
    ā–” Step 1: Create widget definition (POST /api/widgets)
        - source_code with Widget function
        - data_schema (REQUIRED for validation)
        - fetch config (type + instructions for agent_refresh)
        
    ā–” Step 2: Add to dashboard (POST /api/widgets/instances)
        - custom_widget_id matches definition
        - title and config set
        
    ā–” Step 3: Populate cache (for agent_refresh widgets)
        - Data matches data_schema exactly
        - Includes fetchedAt timestamp
        
    ā–” Step 4: Set up cron job (for agent_refresh widgets)
        - Simple message: "⚔ WIDGET REFRESH: {slug}"
        - Appropriate schedule (*/15 or */30 typically)
        
    ā–” Step 5: BROWSER VERIFICATION (MANDATORY)
        - Open http://localhost:3333
        - Widget is visible on dashboard
        - Shows actual data (not loading spinner)
        - Data values match what was cached
        - No errors or broken layouts
        
    ā›” DO NOT report widget as complete until Step 5 passes!
    ```
    
    ## Quick Reference
    
    - **Full SDK docs:** See `docs/widget-sdk.md` in the Glance repo
    - **Component list:** See [references/components.md](references/components.md)
    
    ## Widget Package Structure
    
    ```
    Widget Package
    ā”œā”€ā”€ meta (name, slug, description, author, version)
    ā”œā”€ā”€ widget (source_code, default_size, min_size)
    ā”œā”€ā”€ fetch (server_code | webhook | agent_refresh)
    ā”œā”€ā”€ dataSchema? (JSON Schema for cached data - validates on POST)
    ā”œā”€ā”€ cache (ttl, staleness, fallback)
    ā”œā”€ā”€ credentials[] (API keys, local software requirements)
    ā”œā”€ā”€ config_schema? (user options)
    └── error? (retry, fallback, timeout)
    ```
    
    ## Fetch Type Decision Tree
    
    ```
    Is data available via API that the widget can call?
    ā”œā”€ā”€ YES → Use server_code
    └── NO → Does an external service push data?
        ā”œā”€ā”€ YES → Use webhook
        └── NO → Use agent_refresh (YOU collect it)
    ```
    
    | Scenario | Fetch Type | Who Collects Data? |
    |----------|-----------|-------------------|
    | Public/authenticated API | `server_code` | Widget calls API at render |
    | External service pushes data | `webhook` | External service POSTs to cache |
    | **Local CLI tools** | `agent_refresh` | **YOU (the agent) via PTY/exec** |
    | **Interactive terminals** | `agent_refresh` | **YOU (the agent) via PTY** |
    | **Computed/aggregated data** | `agent_refresh` | **YOU (the agent) on a schedule** |
    
    **āš ļø `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.
    
    ## API Endpoints
    
    ### Widget Definitions
    | Method | Endpoint | Description |
    |--------|----------|-------------|
    | `POST` | `/api/widgets` | Create widget definition |
    | `GET` | `/api/widgets` | List all definitions |
    | `GET` | `/api/widgets/:slug` | Get single definition |
    | `PATCH` | `/api/widgets/:slug` | Update definition |
    | `DELETE` | `/api/widgets/:slug` | Delete definition |
    
    ### Widget Instances (Dashboard)
    | Method | Endpoint | Description |
    |--------|----------|-------------|
    | `POST` | `/api/widgets/instances` | Add widget to dashboard |
    | `GET` | `/api/widgets/instances` | List dashboard widgets |
    | `PATCH` | `/api/widgets/instances/:id` | Update instance (config, position) |
    | `DELETE` | `/api/widgets/instances/:id` | Remove from dashboard |
    
    ### Credentials
    | Method | Endpoint | Description |
    |--------|----------|-------------|
    | `GET` | `/api/credentials` | List credentials + status |
    | `POST` | `/api/credentials` | Store credential |
    | `DELETE` | `/api/credentials/:id` | Delete credential |
    
    ## Creating a Widget
    
    ### Full Widget Package Structure
    
    ```json
    {
      "name": "GitHub PRs",
      "slug": "github-prs",
      "description": "Shows open pull requests",
      
      "source_code": "function Widget({ serverData }) { ... }",
      "default_size": { "w
    
    ... (truncated)