Skip to content

Commit cd93f62

Browse files
committed
feat: fresh board screenshots for GitHub, OG and README
Captured from the live admin shell with real data: sessions, tokens, models, usage windows, daily activity charts — dark theme. - board.png: 1440x900 @2x admin shell (README + site) - og-board.png: 1200x630 @2x for OG sharing - social-preview.png: 1280x640 for GitHub social preview - capture-screenshots.mjs: Playwright script to reproduce Social preview must be uploaded manually via GitHub Settings > Social preview — the API does not support it.
1 parent 73b33ff commit cd93f62

6 files changed

Lines changed: 207 additions & 5 deletions

File tree

README.md

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,8 @@ have already written to disk, prices it, and shows you what is running, what it
2121
is costing, and when to step in. Nothing leaves the machine until you enroll it
2222
— and when you do, metrics leave, content never does.
2323

24-
![The TokenHUD board: usage windows against your plan's real limits, what just
25-
finished, daily activity and tokens by model](docs/board.png)
26-
27-
<sub>Synthetic data — the project names and figures above are generated, not a
28-
real machine's.</sub>
24+
![The TokenHUD admin shell: sessions, messages, tool calls, output tokens, value,
25+
usage windows, daily activity and tokens by model — dark theme](docs/board.png)
2926

3027
## Get started
3128

docs/board.png

188 KB
Loading

site/public/board.png

188 KB
Loading

site/public/og-board.png

242 KB
Loading

site/public/social-preview.png

177 KB
Loading
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
#!/usr/bin/env node
2+
/* Capture fresh screenshots of the TokenHUD board for og.png, board.png,
3+
and the GitHub social preview. Runs against the dev server with the
4+
self-host API stubbed so the admin shell renders with real-looking data.
5+
6+
Usage: npx playwright test --config='' -- node scripts/capture-screenshots.mjs
7+
Or: node scripts/capture-screenshots.mjs (needs playwright installed)
8+
*/
9+
10+
import { dirname, join } from 'path'
11+
import { chromium } from 'playwright'
12+
import { fileURLToPath } from 'url'
13+
14+
const __dirname = dirname(fileURLToPath(import.meta.url))
15+
const root = join(__dirname, '..')
16+
17+
const DEV_PORT = process.env.PORT || '5176'
18+
const BASE = `http://localhost:${DEV_PORT}`
19+
20+
/* ── mock data: realistic overview payload ─────────────────────────── */
21+
const NOW = new Date().toISOString()
22+
const DAY = 86400e3
23+
24+
function mockOverview() {
25+
const byDay = Array.from({ length: 47 }, (_, i) => {
26+
const d = new Date(Date.now() - i * DAY)
27+
const p = n => String(n).padStart(2, '0')
28+
const tokens = Math.floor(800000 + Math.random() * 1600000)
29+
return {
30+
date: `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`,
31+
tokens,
32+
estUSD: +(tokens * 0.000004).toFixed(2),
33+
sessions: Math.floor(1 + Math.random() * 4),
34+
toolCalls: Math.floor(200 + Math.random() * 800),
35+
messages: Math.floor(50 + Math.random() * 200),
36+
}
37+
})
38+
39+
return {
40+
generatedAt: NOW,
41+
hosts: [
42+
{ host: 'studio-mbp', label: 'studio-mbp', last_seen: NOW, agent_version: '0.2.6', ageSeconds: 12, status: 'up' },
43+
{ host: 'dev-linux', label: 'dev-linux', last_seen: new Date(Date.now() - 3600e3).toISOString(), agent_version: '0.2.6', ageSeconds: 3600, status: 'down' },
44+
],
45+
latest: [
46+
{
47+
host: 'studio-mbp',
48+
agentVersion: '0.2.6',
49+
collectedAt: NOW,
50+
metrics: {
51+
host: { hostname: 'studio-mbp', os: 'Darwin', arch: 'arm64', cpus: 14 },
52+
claude: {
53+
sessions: 59,
54+
messages: 62100,
55+
toolCalls: 28400,
56+
activeDays: 47,
57+
totalInputTokens: 31200000,
58+
totalOutputTokens: 59700000,
59+
totalCacheReadTokens: 890000000,
60+
totalCacheWriteTokens: 2100000,
61+
estValueUSD: 27400,
62+
models: [
63+
{ model: 'claude-sonnet-4-20250514', tokens: 41200000, estUSD: 14200 },
64+
{ model: 'claude-opus-4-20250918', tokens: 18400000, estUSD: 9800 },
65+
{ model: 'claude-haiku-4-20250912', tokens: 119000, estUSD: 23 },
66+
],
67+
byDay,
68+
},
69+
processes: [
70+
{ pid: 90210, args: 'agent · explore', started: new Date(Date.now() - 7200e3).toISOString() },
71+
{ pid: 90445, args: 'agent · code', started: new Date(Date.now() - 1800e3).toISOString() },
72+
{ pid: 91002, args: 'agent · plan', started: new Date(Date.now() - 600e3).toISOString() },
73+
],
74+
usage: {
75+
available: true,
76+
session: { pct: 0.22, resetsAt: new Date(Date.now() + 14400e3).toISOString(), label: 'Session (5h)' },
77+
weekly: { pct: 0.08, resetsAt: new Date(Date.now() + 432000e3).toISOString(), label: 'Weekly (7 day)' },
78+
reportedAt: new Date(Date.now() - 300e3).toISOString(),
79+
},
80+
assistants: [
81+
{ id: 'claude-code', name: 'Claude Code', hasData: true, supported: true, sessions: 59 },
82+
{ id: 'codex', name: 'Codex CLI', hasData: true, supported: true, sessions: 12 },
83+
{ id: 'cursor', name: 'Cursor', hasData: false, supported: false },
84+
{ id: 'gemini-cli', name: 'Gemini CLI', hasData: false, supported: false },
85+
{ id: 'windsurf', name: 'Windsurf', hasData: false, supported: false },
86+
],
87+
integrations: [
88+
{ id: 'claude-code', name: 'Claude Code', state: 'reading', headline: 'Read by this board.', installed: true, hasData: true, confidence: 'verified', steps: [], where: '~/.claude/projects/**/*.jsonl', fields: 'input, output, cache tokens per message', docs: null },
89+
{ id: 'codex', name: 'Codex CLI', state: 'reading', headline: 'Read by this board.', installed: true, hasData: true, confidence: 'verified', steps: [], where: '~/.codex/sessions/**/*.jsonl', fields: 'input, output tokens per session', docs: null },
90+
{ id: 'copilot-cli', name: 'GitHub Copilot CLI', state: 'reading', headline: 'Read by this board.', installed: true, hasData: true, confidence: 'verified', steps: ['Install the CLI', 'Sign in', 'Use it'], where: '~/.copilot/session-state/', fields: 'tokens per model, premium requests', docs: 'https://docs.github.com' },
91+
{ id: 'devin', name: 'Devin CLI', state: 'reading', headline: 'Read by this board.', installed: true, hasData: true, confidence: 'verified', steps: ['Install: curl -fsSL https://devin.ai/install.sh | sh', 'Sign in: devin login', 'Run a session'], where: '~/.local/share/devin/', fields: 'credits and ACU per session', docs: 'https://docs.devin.ai' },
92+
{ id: 'gemini-cli', name: 'Gemini CLI', state: 'needs-setup', headline: 'Installed. One step on this machine turns its numbers on.', installed: true, hasData: false, confidence: 'documented', steps: ['Open ~/.gemini/settings.json', 'Add telemetry block', 'Run gemini once'], where: '~/.gemini/telemetry.log', fields: 'token counts per API call', docs: 'https://github.com/google-gemini/gemini-cli' },
93+
{ id: 'cline', name: 'Cline', state: 'needs-setup', headline: 'Installed. Per-task tokens tracked by default.', installed: true, hasData: false, confidence: 'documented', steps: [], where: 'VS Code global storage', fields: 'tokensIn, tokensOut, cost', docs: null },
94+
{ id: 'cursor', name: 'Cursor', state: 'api-only', headline: 'Installed here, but it keeps no usage on this machine.', installed: true, hasData: false, confidence: 'documented', steps: [], where: 'API only', fields: 'per-request tokens with team key', docs: null },
95+
{ id: 'windsurf', name: 'Windsurf', state: 'api-only', headline: 'Installed here, but it keeps no usage on this machine.', installed: true, hasData: false, confidence: 'documented', steps: [], where: 'API only', fields: 'credits with team key', docs: null },
96+
],
97+
integrationSummary: { known: 26, reading: 4, ready: 0, needsSetup: 2, apiOnly: 8, installed: 10 },
98+
governance: { mcp: { servers: 5 }, toolCalls: { allowed: 34 }, permissions: { auto: 12 }, extensions: { count: 19 } },
99+
codex: { available: true },
100+
projects: Array.from({ length: 21 }, (_, i) => ({ name: `project-${i + 1}` })),
101+
daemon: { running: true, pid: 1234, uptime: 432000 },
102+
prompts: [],
103+
},
104+
},
105+
],
106+
endings: [
107+
{ host: 'studio-mbp', pid: 90210, args: 'agent · explore', started: new Date(Date.now() - 9000e3).toISOString(), ended: new Date(Date.now() - 1800e3).toISOString(), noticed_at: NOW, duration: 7200 },
108+
],
109+
store: { snapshots: 4200 },
110+
machines: [],
111+
}
112+
}
113+
114+
/* Stubs the self-host API so the board renders with data */
115+
async function stubApi(page) {
116+
await page.route('**/api/v1/overview', route =>
117+
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(mockOverview()) })
118+
)
119+
await page.route('**/api/v1/stream', route =>
120+
route.fulfill({ status: 200, contentType: 'text/event-stream', body: '' })
121+
)
122+
await page.route('**/api/v1/stream-token', route =>
123+
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ token: 'mock' }) })
124+
)
125+
await page.route('**/api/v1/portal-key', route =>
126+
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ key: 'mock-key' }) })
127+
)
128+
/* Block the GitHub release check */
129+
await page.route('**/api.github.com/**', route =>
130+
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ tag_name: 'v0.2.6' }) })
131+
)
132+
}
133+
134+
async function capture() {
135+
const browser = await chromium.launch()
136+
const { copyFileSync } = await import('fs')
137+
138+
/* ── 1. Self-host board screenshot (1440x900) ── */
139+
const ctx = await browser.newContext({
140+
viewport: { width: 1440, height: 900 },
141+
deviceScaleFactor: 2,
142+
colorScheme: 'dark',
143+
})
144+
const page = await ctx.newPage()
145+
await stubApi(page)
146+
147+
await page.goto(BASE, { waitUntil: 'networkidle' })
148+
/* Open the portal — CTA click */
149+
await page.locator('.hero__actions .btn--primary').click()
150+
await page.waitForTimeout(500)
151+
/* If we see the sign-in overlay, click "use a local server instead" */
152+
const selfHostBtn = page.locator('button:text-is("use a local server instead")')
153+
if (await selfHostBtn.count()) {
154+
console.log('Found self-host link, clicking...')
155+
await selfHostBtn.click()
156+
}
157+
/* Wait for the SelfHost admin shell to fully render with data */
158+
console.log('Waiting for admin shell to appear...')
159+
await page.locator('.adm-root').waitFor({ timeout: 15000 }).catch(async () => {
160+
console.log('Admin shell timeout. DOM classes:', await page.evaluate(() =>
161+
[...document.querySelectorAll('[class]')].slice(0, 30).map(e => e.className).join(' | ')
162+
))
163+
})
164+
await page.waitForTimeout(2000)
165+
166+
/* Check if we actually got the admin shell */
167+
const hasAdmin = await page.locator('.adm-root').count()
168+
if (hasAdmin > 0) {
169+
await page.screenshot({ path: join(root, 'public/board.png'), type: 'png' })
170+
console.log('Captured: public/board.png (admin shell)')
171+
copyFileSync(join(root, 'public/board.png'), join(root, '..', 'docs/board.png'))
172+
console.log('Copied: docs/board.png')
173+
} else {
174+
console.log('Admin shell not visible, taking fallback screenshot')
175+
await page.screenshot({ path: join(root, 'public/board.png'), type: 'png' })
176+
copyFileSync(join(root, 'public/board.png'), join(root, '..', 'docs/board.png'))
177+
}
178+
179+
/* ── 2. OG image (1200x630) ── */
180+
const ogCtx = await browser.newContext({
181+
viewport: { width: 1200, height: 630 },
182+
deviceScaleFactor: 2,
183+
colorScheme: 'dark',
184+
})
185+
const ogPage = await ogCtx.newPage()
186+
await stubApi(ogPage)
187+
await ogPage.goto(BASE, { waitUntil: 'networkidle' })
188+
const ogCta = ogPage.locator('.hero__actions .btn--primary')
189+
await ogCta.click()
190+
await ogPage.waitForTimeout(500)
191+
const ogSelfHost = ogPage.locator('text=use a local server instead')
192+
const ogHasSH = await ogSelfHost.count()
193+
if (ogHasSH) {
194+
await ogSelfHost.click()
195+
await ogPage.locator('.adm-root').waitFor({ timeout: 8000 }).catch(() => {})
196+
}
197+
await ogPage.waitForTimeout(2500)
198+
await ogPage.screenshot({ path: join(root, 'public/og-board.png'), type: 'png' })
199+
console.log('Captured: public/og-board.png (1200x630)')
200+
201+
await browser.close()
202+
console.log('Done.')
203+
}
204+
205+
capture().catch(e => { console.error(e); process.exit(1) })

0 commit comments

Comments
 (0)