docs: add analytics documentation and screenshot

- Add Analytics section to Control UI docs explaining quota and usage features
- Add screenshot script with dynamic token loading from config
- Update screenshot showing the analytics dashboard

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Romain Beaumont 2026-01-26 03:43:11 +00:00
parent 36523e7746
commit b493adce73
3 changed files with 122 additions and 0 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 59 KiB

View File

@ -44,6 +44,34 @@ The onboarding wizard generates a gateway token by default, so paste it here on
- Debug: status/health/models snapshots + event log + manual RPC calls (`status`, `health`, `models.list`)
- Logs: live tail of gateway file logs with filter/export (`logs.tail`)
- Update: run a package/git update + restart (`update.run`) with a restart report
- Analytics: token usage stats, cost estimates, and plan quota display (`usage.cost`, `usage.status`)
## Analytics
The Analytics tab provides visibility into your token usage and costs:
![Analytics Dashboard](/gateway/images/control-ui-analytics.png)
### Plan Quota
Shows your current usage against plan limits (for providers that expose quota data):
- **Usage bars**: Visual progress bars for each rate limit window (5-hour, weekly, model-specific)
- **Reset timers**: Time remaining until each limit resets
- **Provider support**: Currently supports Anthropic API keys with quota access
For Claude Code OAuth tokens (which use the `user:inference` scope only), plan quota is not available programmatically. The UI displays a link to [claude.ai/settings/usage](https://claude.ai/settings/usage) where you can check your usage directly.
### Usage Summary
Aggregates token consumption from your session transcripts:
- **Summary cards**: Total tokens, input/output breakdown, cache read/write, estimated cost
- **Daily charts**: Bar charts showing token usage and cost over time
- **Time periods**: Select 7, 14, 30, or 90 day views
- **Refresh**: Click to reload data from the gateway
Cost estimates are based on published model pricing and may not reflect actual billing (discounts, commitments, etc.).
## Chat behavior

View File

@ -0,0 +1,94 @@
import { chromium } from 'playwright-core';
import { readdirSync, existsSync, readFileSync } from 'fs';
import { join } from 'path';
function getTokenFromConfig() {
const configPath = join(process.env.HOME, '.clawdbot/clawdbot.json');
if (!existsSync(configPath)) {
throw new Error('Config file not found at ~/.clawdbot/clawdbot.json');
}
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
const token = config?.gateway?.auth?.token;
if (!token) {
throw new Error('No gateway.auth.token found in config');
}
return token;
}
const TOKEN = getTokenFromConfig();
const BASE_URL = process.env.GATEWAY_URL || 'http://127.0.0.1:18789';
function findChromiumExecutable() {
const cacheDir = join(process.env.HOME, '.cache/ms-playwright');
if (!existsSync(cacheDir)) return undefined;
const dirs = readdirSync(cacheDir)
.filter(d => d.startsWith('chromium-') && !d.includes('headless'))
.sort()
.reverse();
if (dirs.length === 0) return undefined;
const chromiumDir = join(cacheDir, dirs[0]);
const linuxPath = join(chromiumDir, 'chrome-linux64/chrome');
const macPath = join(chromiumDir, 'chrome-mac/Chromium.app/Contents/MacOS/Chromium');
if (existsSync(linuxPath)) return linuxPath;
if (existsSync(macPath)) return macPath;
return undefined;
}
async function takeScreenshot() {
const executablePath = findChromiumExecutable();
console.log('Using chromium:', executablePath || 'bundled');
const launchOptions = { headless: true };
if (executablePath) {
launchOptions.executablePath = executablePath;
}
const browser = await chromium.launch(launchOptions);
const context = await browser.newContext({
viewport: { width: 1280, height: 900 }
});
const page = await context.newPage();
page.on('console', msg => console.log('PAGE:', msg.text()));
// Navigate and set token
await page.goto(`${BASE_URL}/?token=${TOKEN}`);
await page.waitForTimeout(2000);
await page.evaluate((token) => {
const settings = JSON.parse(localStorage.getItem('clawdbot-control-ui-settings') || '{}');
settings.token = token;
localStorage.setItem('clawdbot-control-ui-settings', JSON.stringify(settings));
}, TOKEN);
// Navigate to analytics
await page.goto(`${BASE_URL}/analytics`);
await page.waitForTimeout(4000);
// Click refresh
const refreshButton = page.locator('button', { hasText: 'Refresh' });
try {
await refreshButton.click();
console.log('Clicked refresh');
await page.waitForTimeout(3000);
} catch (e) {
console.log('Could not click refresh:', e.message);
}
const outputPath = process.argv[2] || '/tmp/analytics-screenshot.png';
await page.screenshot({ path: outputPath, fullPage: false });
console.log('Screenshot saved to', outputPath);
await browser.close();
}
takeScreenshot().catch(err => {
console.error('Error:', err);
process.exit(1);
});