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:
parent
36523e7746
commit
b493adce73
Binary file not shown.
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 59 KiB |
@ -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`)
|
- 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`)
|
- 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
|
- 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:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 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
|
## Chat behavior
|
||||||
|
|
||||||
|
|||||||
94
ui/src/ui/analytics-screenshot.e2e.mjs
Normal file
94
ui/src/ui/analytics-screenshot.e2e.mjs
Normal 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);
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user