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
CODEBLOCK0
Dashboard runs at http://localhost:3333
Configuration
Edit .env.local:
CODEBLOCK1
Service Installation (macOS)
CODEBLOCK2
Environment Variables
| Variable | Description | Default |
|---|
| INLINECODE1 | Server port | INLINECODE2 |
| INLINECODE3 |
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
CODEBLOCK3
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 INLINECODE11
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 |
|---|
| INLINECODE12 | string | Non-empty, human-readable |
| INLINECODE13 |
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
CODEBLOCK4
⚠️ Widget Creation Checklist (MANDATORY)
Every widget must complete ALL steps before being considered done:
CODEBLOCK5
Quick Reference
Widget Package Structure
CODEBLOCK6
Fetch Type Decision Tree
CODEBLOCK7
| Scenario | Fetch Type | Who Collects Data? |
|---|
| Public/authenticated API | INLINECODE36 | 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 |
|---|
| INLINECODE42 | INLINECODE43 | Create widget definition |
| INLINECODE44 |
/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 |
|---|
| INLINECODE52 | INLINECODE53 | Add widget to dashboard |
| INLINECODE54 |
/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 |
|---|
| INLINECODE60 | INLINECODE61 | List credentials + status |
| INLINECODE62 |
/api/credentials | Store credential |
|
DELETE |
/api/credentials/:id | Delete credential |
Creating a Widget
Full Widget Package Structure
CODEBLOCK8
Fetch Types
| Type | When to Use | Data Flow |
|---|
| INLINECODE66 | Widget can call API directly | Widget → server_code → API |
| INLINECODE67 |
Agent must fetch/compute data | Agent → POST /cache → Widget reads |
|
webhook | External service pushes data | External → POST /cache → Widget reads |
Most widgets should use agent_refresh — the agent fetches data on a schedule and pushes to the cache endpoint.
Step 1: Create Widget Definition
CODEBLOCK9
data_schema (REQUIRED) defines the data contract between the fetcher and the widget. Cache POSTs are validated against it — malformed data returns 400.
⚠️ Always include data_schema when creating widgets. This ensures:
- 1. Data validation on cache POSTs (400 on schema mismatch)
- Clear documentation of expected data structure
- AI agents know the exact format to produce
Step 2: Add to Dashboard
CODEBLOCK10
Step 3: Populate Cache (for agent_refresh)
CODEBLOCK11
⚠️ 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:
CODEBLOCK12
Step 4: Browser Verification (REQUIRED)
⚠️ MANDATORY: Every widget creation and refresh MUST end with browser verification.
Never consider a widget "done" until you've visually confirmed it renders correctly on the dashboard.
CODEBLOCK13
Verification checklist (must ALL be true):
- - [ ] Widget visible on dashboard grid
- [ ] Title displays correctly
- [ ] Data renders (not stuck on loading)
- [ ] Values match cached data
- [ ] No error states or broken layouts
- [ ] "Updated X ago" footer shows recent timestamp
Common issues and fixes:
| Symptom | Cause | Fix |
|---|
| "Waiting for data..." | Cache empty | POST data to INLINECODE73 |
| Widget not visible |
Not added to dashboard |
POST /api/widgets/instances |
| Wrong/old data | Slug mismatch | Check slug matches between definition and cache POST |
| Broken layout | Bad JSX in source_code | Check widget code for syntax errors |
| "No data" after POST | Schema validation failed | Check data matches
data_schema |
If verification fails, fix the issue before reporting success.
Widget Code Template (agent_refresh)
For agent_refresh widgets, use serverData prop (NOT useData hook):
CODEBLOCK14
Important: The widget wrapper (CustomWidgetWrapper) provides:
- - Outer
<Card> container with header (widget title) - Refresh button and "Updated X ago" footer
- Loading/error states
Your widget code should just render the content — no Card, no CardHeader, no footer.
Key difference: agent_refresh widgets receive data via serverData prop, NOT by calling useData(). The agent pushes data to /api/widgets/{slug}/cache.
Server Code (Legacy Alternative)
Prefer agent_refresh over server_code. Only use server_code when the widget MUST execute code at render time (rare).
CODEBLOCK15
Available: fetch, getCredential(provider), params, console
Blocked: require, eval, fs, process, INLINECODE95
Agent Refresh Contract
⚠️ CRITICAL: For agent_refresh widgets, YOU (the OpenClaw agent) are the data collector.
This is NOT an external API or service. YOU must:
- 1. Set up a cron job to remind yourself to collect data on a schedule
- Use your own tools (PTY, exec, browser, etc.) to gather the data
- Parse the output into structured JSON
- POST to the cache endpoint so the widget can display it
The Pattern
CODEBLOCK16
Step-by-Step for agent_refresh Widgets
- 1. Create the widget with
fetch.type = "agent_refresh" and detailed INLINECODE98 - Set up a cron job targeting YOUR main session (message is just the slug):
cron.add({
name: "Widget: My Data Refresh",
schedule: { kind: "cron", expr: "*/15 * * * *" },
payload: {
kind: "systemEvent",
text: "⚡ WIDGET REFRESH: my-widget" // Just the slug!
},
sessionTarget: "main" // Reminds YOU, not an isolated session
})
- 3. When you receive the refresh message, look up
fetch.instructions from the DB and spawn a subagent:
// Parse slug from message
const slug = message.replace('⚡ WIDGET REFRESH:', '').trim();
// Query widget's fetch.instructions
const widget = db.query('SELECT fetch FROM custom_widgets WHERE slug = ?', slug);
// Spawn subagent with the instructions
sessions_spawn({ task: widget.fetch.instructions, model: 'haiku' });
- 4. The subagent collects the data using your tools:
-
exec for shell commands
- PTY for interactive CLI tools (like
claude /status)
-
browser for web scraping
- API calls via
web_fetch
- 4. POST the data to the cache:
CODEBLOCK19
Writing Excellent fetch.instructions
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.
Required sections:
CODEBLOCK20
Good example:
## Data Collection
bash
gog gmail search "in:inbox" --json
## Data Transformation
Take first 5-8 emails, generate AI summary (3-5 words) for each:
json
{
"emails": [{"id": "...", "from": "...", "subject": "...", "summary": "AI summary here", "unread": true}],
"fetchedAt": "ISO timestamp"
}
CODEBLOCK23
Bad example (too vague):
CODEBLOCK24
Real Example: Claude Max Usage Widget
This widget shows Claude CLI usage stats. The data comes from running claude in a PTY and navigating to /status → Usage.
The agent's job every 15 minutes:
CODEBLOCK25
This is YOUR responsibility as the agent. The widget just displays whatever data is in the cache.
Subagent Task Template for Refreshes
When spawning subagents for widget refreshes, always include browser verification:
CODEBLOCK26
Cache Endpoint
CODEBLOCK27
Immediate Refresh via Webhook
For agent_refresh widgets, users can trigger immediate refreshes via the UI refresh button.
When configured with OPENCLAW_GATEWAY_URL and OPENCLAW_TOKEN environment variables, clicking the refresh button will:
- 1. Store a refresh request in the database (fallback for polling)
- Immediately POST a wake notification to OpenClaw via INLINECODE110
- The agent receives a prompt to refresh that specific widget now
This eliminates the delay of waiting for the next heartbeat poll.
Environment variables (add to .env.local):
CODEBLOCK28
How it works:
- 1. User clicks refresh button on widget
- Glance POSTs to INLINECODE112
- If webhook configured, Glance immediately notifies OpenClaw: INLINECODE113
- Agent wakes up, collects fresh data, POSTs to cache
- Widget re-renders with updated data
Response includes webhook status:
CODEBLOCK29
If webhook fails or isn't configured, the DB fallback ensures the next heartbeat/poll will pick it up.
Rules
- - Always include
fetchedAt timestamp - Don't overwrite on errors - let widget use stale data
- Use main session cron so YOU handle the collection, not an isolated agent
## Credential Requirements Format
### Credential Types
| Type | Storage | Description | Use For |
|------|---------|-------------|---------|
| `api_key` | Glance DB (encrypted) | API tokens stored in Glance | GitHub PAT, OpenWeather key |
| `local_software` | Agent's machine | Software that must be installed | Homebrew, Docker |
| `agent` | Agent environment | Auth that lives on the agent | `gh` CLI auth, `gcloud` auth |
| `oauth` | Glance DB | OAuth tokens (future) | Google Calendar |
### Examples
json
{
"credentials": [
{
"id": "github",
"type": "api_key",
"name": "GitHub Personal Access Token",
"description": "Token with repo scope",
"obtain_url": "https://github.com/settings/tokens",
"obtain_instructions": "Create token with 'repo' scope"
},
{
"id": "homebrew",
"type": "local_software",
"name": "Homebrew",
"check_command": "which brew",
"install_url": "https://brew.sh"
},
{
"id": "github_cli",
"type": "agent",
"name": "GitHub CLI",
"description": "Agent needs gh CLI authenticated to GitHub",
"agent_tool": "gh",
"agent
authcheck": "gh auth status",
"agent
authinstructions": "Run
gh auth login on the machine running OpenClaw"
}
]
}
**When 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.
## Common Credential Providers
| Provider | ID | Description |
|----------|-----|-------------|
| GitHub | `github` | GitHub API (PAT with repo scope) |
| Anthropic | `anthropic` | Claude API (Admin key for usage) |
| OpenAI | `openai` | GPT API (Admin key for usage) |
| OpenWeather | `openweather` | Weather data API |
| Linear | `linear` | Linear API |
| Notion | `notion` | Notion API |
## Export/Import Packages
### Export
http
GET /api/widgets/{slug}/export
Returns: `{ "package": "!GW1!eJxVj8EKwj..." }`
### Import
http
POST /api/widgets/import
Content-Type: application/json
{
"package": "!GW1!eJxVj8EKwj...",
"dry_run": false,
"autoaddto_dashboard": true
}
The `!GW1!` prefix indicates Glance Widget v1 format (compressed base64 JSON).
### Import Response with Cron
json
{
"valid": true,
"widget": { "id": "cw_abc", "slug": "homebrew-status" },
"cronSchedule": {
"expression": "
/15 *",
"instructions": "Run brew list...",
"slug": "homebrew-status"
}
}
When `cronSchedule` is returned, OpenClaw should register a cron job.
## Key UI Components
| Component | Use For |
|-----------|---------|
| `Card` | Widget container (always use `className="h-full"`) |
| `List` | Items with title/subtitle/badge |
| `Stat` | Single metric with trend indicator |
| `Progress` | Progress bars with variants |
| `Badge` | Status labels (success/warning/error) |
| `Stack` | Flexbox layout (row/column) |
| `Grid` | CSS Grid layout |
| `Loading` | Loading spinner |
| `ErrorDisplay` | Error with retry button |
See [references/components.md](references/components.md) for full props.
## Hooks
tsx
// Fetch data (BOTH args required!)
const { data, loading, error, refresh } = useData('github', {});
const { data } = useData('github', { endpoint: '/pulls', params: { state: 'open' } });
// Get widget config
const config = useConfig();
// Widget-local state
const { state, setState } = useWidgetState('counter', 0);
**⚠️ `useData` requires both arguments.** Pass empty `{}` if no query needed.
## Error Handling
tsx
if (error?.code === 'CREDENTIAL_MISSING') {
return
GitHub token required
;
}
Error codes: `CREDENTIAL_MISSING`, `RATE_LIMITED`, `NETWORK_ERROR`, `API_ERROR`
## Best Practices
1. **Always check credentials before creating widgets**
2. **Use meaningful names:** `github-prs-libra` not `widget-1`
3. **Include fetchedAt in all data** for staleness tracking
4. **Handle errors gracefully** with retry options
5. **Confirm actions:** "Done! Widget added to dashboard."
6. **Size appropriately:** Lists 1x1, charts 2x2
## Reading Dashboard Data
To summarize dashboard for user:
- 1. GET /api/widgets/instances → list instances
- For each: POST /api/widgets/:slug/execute
- Combine into natural language summary
``
---
## ⚠️ Rules & Gotchas
1. **Use JSON Schema for generation** — docs/schemas/widget-schema.json
enforces all required fields
2. **Browser verify EVERYTHING** — don't report success until you see the widget render correctly
3. **agent_refresh = YOU collect data** — the widget just displays what you POST to cache
4. **fetch.instructions is the source of truth** — cron jobs just send the slug, you look up instructions
5. **Always include fetchedAt** — widgets need timestamps for "Updated X ago" display
6. **data_schema is REQUIRED** — cache POSTs validate against it, malformed data returns 400
7. **credentials is REQUIRED** — use empty array []
if no credentials needed
8. **Don't wrap in Card** — the framework provides the outer card, you render content only
9. **Use Haiku for refresh subagents** — mechanical data collection doesn't need Opus
10. **Mark refresh requests as processed** — DELETE /api/widgets/{slug}/refresh
after handling
11. **Spawn subagents for refreshes** — don't block main session with PTY/long-running work
## Environment Variables
| Variable | Description | Example |
|----------|-------------|---------|
| GLANCE
URL | Glance server URL | http://localhost:3333 |
| GLANCEDATA
| Path to SQLite database | /tmp/glance-test/data
|
| OPENCLAW
GATEWAYURL
| For webhook refresh notifications | https://localhost:18789
|
| OPENCLAW_TOKEN
| Gateway auth token | d551fe97...
|
## Learnings (Feb 2026)
- **Webhook refresh works** — Glance POSTs to OpenClaw gateway, agent wakes immediately
- **Simple cron messages** — just ⚡ WIDGET REFRESH: {slug}
, agent looks up instructions
- **AI summaries need AI** — for recent-emails, YOU generate the summaries, not some API
- **icalBuddy for iCloud** — gog calendar
doesn't work for iCloud, use /opt/homebrew/bin/icalBuddy
- **wttr.in for weather** — free, no API key, JSON format: wttr.in/City?format=j1`
Glance
AI可扩展的个人仪表盘。使用自然语言创建自定义小组件——AI负责数据收集。
功能特性
- - 自定义小组件 — 通过AI创建小组件,自动生成JSX代码
- 代理刷新 — AI按计划收集数据并推送到缓存
- 仪表盘导出/导入 — 共享小组件配置
- 凭据管理 — 安全的API密钥存储
- 实时更新 — Webhook触发的即时刷新
快速开始
bash
导航到技能目录(如果通过ClawHub安装)
cd $(clawhub list | grep glance | awk {print $2})
或直接克隆
git clone https://github.com/acfranzen/glance ~/.glance
cd ~/.glance
安装依赖
npm install
配置环境
cp .env.example .env.local
编辑 .env.local 填写你的设置
启动开发服务器
npm run dev
或构建并启动生产环境
npm run build && npm start
仪表盘运行在 http://localhost:3333
配置
编辑 .env.local:
bash
服务器
PORT=3333
AUTH_TOKEN=your-secret-token # 可选:Bearer令牌认证
OpenClaw集成(用于小组件即时刷新)
OPENCLAW
GATEWAYURL=https://localhost:18789
OPENCLAW_TOKEN=your-gateway-token
数据库
DATABASE_PATH=./data/glance.db # SQLite数据库位置
服务安装(macOS)
bash
创建launchd plist文件
cat > ~/Library/LaunchAgents/com.glance.dashboard.plist << EOF
Label
com.glance.dashboard
ProgramArguments
/opt/homebrew/bin/npm
run
dev
WorkingDirectory
~/.glance
RunAtLoad
KeepAlive
StandardOutPath
~/.glance/logs/stdout.log
StandardErrorPath
~/.glance/logs/stderr.log
EOF
加载服务
mkdir -p ~/.glance/logs
launchctl load ~/Library/LaunchAgents/com.glance.dashboard.plist
服务命令
launchctl start com.glance.dashboard
launchctl stop com.glance.dashboard
launchctl unload ~/Library/LaunchAgents/com.glance.dashboard.plist
环境变量
| 变量 | 描述 | 默认值 |
|---|
| PORT | 服务器端口 | 3333 |
| AUTH_TOKEN |
API认证的Bearer令牌 | — |
| DATABASE_PATH | SQLite数据库路径 | ./data/glance.db |
| OPENCLAW
GATEWAYURL | Webhook的OpenClaw网关 | — |
| OPENCLAW_TOKEN | OpenClaw认证令牌 | — |
系统要求
- - Node.js 20+
- npm 或 pnpm
- SQLite(已捆绑)
小组件技能
创建和管理仪表盘小组件。大多数小组件使用 agent_refresh——你负责收集数据。
快速开始
bash
检查Glance是否运行(列出小组件)
curl -s -H Origin: $GLANCE
URL $GLANCEURL/api/widgets | jq .custom_widgets[].slug
认证说明:带有Origin头的本地请求绕过Bearer令牌认证
外部访问请使用:-H Authorization: Bearer $GLANCE_TOKEN
刷新小组件(查找指令,收集数据,POST到缓存)
sqlite3 $GLANCE
DATA/glance.db SELECT jsonextract(fetch, $.instructions) FROM custom_widgets WHERE slug = my-widget
按照指令操作,然后:
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)}}
在浏览器中验证
browser action:open targetUrl:$GLANCE_URL
AI结构化输出生成(必需)
生成小组件定义时,使用 docs/schemas/widget-schema.json 中的JSON Schema配合AI模型的结构化输出模式:
- - Anthropic:使用 tooluse 配合schema
- OpenAI:使用 responseformat: { type: json_schema, schema }
该schema在生成时强制执行所有必填字段——无法生成格式错误的小组件。
必填字段检查清单
每个小组件
必须包含以下字段(schema强制执行):
| 字段 | 类型 | 说明 |
|---|
| name | string | 非空,人类可读 |
| slug |
string | 小写kebab-case格式(my-widget) |
| source_code | string | 包含Widget函数的有效JSX |
| default_size | { w: 1-12, h: 1-20 } | 网格单位 |
| min_size | { w: 1-12, h: 1-20 } | 不能缩小到更小 |
| fetch.type | enum | server
code \| webhook \| agentrefresh |
| fetch.instructions | string |
如果type为agent_refresh则必需 |
| fetch.schedule | string |
如果type为agent_refresh则必需(cron表达式) |
| data_schema.type | object | 始终为object |
| data_schema.properties | object | 定义每个字段 |
| data_schema.required | array |
必须包含fetchedAt |
| credentials | array | 如果不需要则使用[] |
示例:最小有效小组件
json
{
name: 我的小组件,
slug: my-widget,
source_code: function Widget({ serverData }) { return
{serverData?.value}
; },
default_size: { w: 2, h: 2 },
min_size: { w: 1, h: 1 },
fetch: {
type: agent_refresh,
schedule:
/15 *,
instructions: ## 数据收集\n收集数据...\n\n## 缓存更新\nPOST到 /api/widgets/my-widget/cache
},
data_schema: {
type: object,
properties: {
value: { type: number },
fetchedAt: { type: string, format: date-time }
},
required: [value, fetchedAt]
},
credentials: []
}
⚠️ 小组件创建检查清单(强制)
每个小组件必须完成所有步骤才能视为完成:
□ 步骤1:创建小组件定义(POST /api/widgets)
- 包含Widget函数的source_code
- data_schema(验证必需)
- fetch配置(agent_refresh需要type + instructions)
□ 步骤2:添加到仪表盘(POST /api/widgets/instances)
- customwidgetid与定义匹配
- 设置title和config
□ 步骤3:填充缓存(针对agent_refresh小组件)
- 数据与data_schema完全匹配
- 包含fetchedAt时间戳
□ 步骤4:设置cron任务(针对agent_refresh小组件)
- 简单消息:⚡ WIDGET REFRESH: {slug}
- 合适的调度(通常为/15或/30)
□ 步骤5:浏览器验证(强制)
- 打开 http://localhost:3333
- 小组件在仪表盘上可见
- 显示实际数据(不是加载旋转器)