Skip to content

Commit 9cc1620

Browse files
Ambient Code Botclaude
andcommitted
feat: dark Grafana theme, launch error handling, running status panel
- Restyle dashboard to Grafana-inspired dark theme using dashboard-creator design system (dark gray backgrounds, bright accent colors, high contrast) - Fix benchmark launch: validate env vars, capture early spawn failures, show errors in launch dialog instead of silently failing - Load .env from project root at module init (handles quoted values, export prefix) - Add collapsible running status panel showing active run progress - Add .env.example documenting required variables - Add dashboard section to README Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7147eb5 commit 9cc1620

7 files changed

Lines changed: 470 additions & 393 deletions

File tree

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,9 @@ __pycache__/
1616
node_modules/
1717
.next/
1818

19+
# Secrets
20+
.env
21+
!.env.example
22+
1923
# OS
2024
.DS_Store

README.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,14 +109,40 @@ Analysis script produces:
109109
4. **Rate limit burn rate** — projected req/hr extrapolated from the run
110110
5. **Concurrency knee** — at what parallelism level does latency spike
111111

112+
## Dashboard
113+
114+
A Next.js app for launching runs, monitoring progress, and comparing results.
115+
116+
```bash
117+
cd dashboard
118+
npm install
119+
120+
# Required: pass the same GitHub credentials
121+
GITHUB_TOKEN=ghp_xxx GITHUB_ORG=your-test-org GITHUB_REPO=gh-api-benchmark \
122+
npx next dev -p 3001
123+
```
124+
125+
See `dashboard/.env.example` for required variables. The dashboard reads from
126+
the same `benchmark_results.db` in the project root.
127+
128+
Features:
129+
- **Launch** benchmark runs with preset or custom configurations
130+
- **Live monitoring** — polls every 3s while a run is active
131+
- **Run detail** — per-step latency charts, percentile distributions, rate limit timeline
132+
- **Compare** — side-by-side metrics across multiple runs
133+
112134
## Architecture
113135

114136
```
115137
benchmark.py — orchestrator + instrumented GitHub client
116138
├── Phase 1 — sequential baseline (N cycles, 1 at a time)
117139
├── Phase 2 — parallel ramp (1→MAX_CONCURRENCY workers)
118-
└── SQLite writer — WAL mode, commit-per-step for crash safety
140+
└── SQLite writer — WAL mode, commit-per-cycle for crash safety
119141
120142
analyze.py — reads SQLite, produces stats + CSVs
121143
setup_repo.py — one-time test repo initialization
144+
145+
dashboard/ — Next.js app for visualization and run management
146+
├── src/app/api/ — API routes (runs CRUD, benchmark launcher, compare)
147+
└── src/app/page.tsx — single-page app with runs list, detail, and compare views
122148
```

dashboard/.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
GITHUB_TOKEN=
2+
GITHUB_ORG=
3+
GITHUB_REPO=

dashboard/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ yarn-error.log*
3232

3333
# env files (can opt-in for committing if needed)
3434
.env*
35+
!.env.example
3536

3637
# vercel
3738
.vercel
Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,41 @@
11
import { NextRequest, NextResponse } from "next/server";
22
import { spawn } from "child_process";
3+
import { readFileSync } from "fs";
34
import path from "path";
45

6+
const benchmarkDir = path.resolve(process.cwd(), "..");
7+
8+
// Load .env from project root once at module init
9+
try {
10+
const content = readFileSync(path.join(benchmarkDir, ".env"), "utf-8");
11+
for (const line of content.split("\n")) {
12+
const trimmed = line.trim().replace(/^export\s+/, "");
13+
if (!trimmed || trimmed.startsWith("#")) continue;
14+
const eq = trimmed.indexOf("=");
15+
if (eq === -1) continue;
16+
const key = trimmed.slice(0, eq).trim();
17+
const val = trimmed.slice(eq + 1).trim().replace(/^["']|["']$/g, "");
18+
if (!process.env[key]) process.env[key] = val;
19+
}
20+
} catch {
21+
// .env not found — rely on process env
22+
}
23+
524
export async function POST(req: NextRequest) {
25+
const missing = ["GITHUB_TOKEN", "GITHUB_ORG", "GITHUB_REPO"].filter(
26+
(k) => !process.env[k]
27+
);
28+
if (missing.length > 0) {
29+
return NextResponse.json(
30+
{ error: `Missing server environment variables: ${missing.join(", ")}. Start the dashboard with these set.` },
31+
{ status: 400 }
32+
);
33+
}
34+
635
const body = await req.json();
736

837
const env: NodeJS.ProcessEnv = {
938
...process.env,
10-
GITHUB_TOKEN: process.env.GITHUB_TOKEN || "",
11-
GITHUB_ORG: process.env.GITHUB_ORG || "",
12-
GITHUB_REPO: process.env.GITHUB_REPO || "",
1339
TOTAL_CYCLES: String(body.totalCycles || 4),
1440
SEQUENTIAL_CYCLES: String(body.sequentialCycles || 2),
1541
MAX_CONCURRENCY: String(body.maxConcurrency || 3),
@@ -25,20 +51,50 @@ export async function POST(req: NextRequest) {
2551
env.RUN_ID = body.runId;
2652
}
2753

28-
const benchmarkDir = path.resolve(process.cwd(), "..");
29-
3054
const child = spawn("uv", ["run", "python", "benchmark.py"], {
3155
cwd: benchmarkDir,
3256
env,
3357
detached: true,
34-
stdio: "ignore",
58+
stdio: ["ignore", "ignore", "pipe"],
3559
});
3660

37-
child.unref();
61+
const result = await new Promise<{ ok: boolean; error?: string; pid?: number }>((resolve) => {
62+
let stderr = "";
63+
let resolved = false;
64+
const done = (r: { ok: boolean; error?: string; pid?: number }) => {
65+
if (resolved) return;
66+
resolved = true;
67+
resolve(r);
68+
};
69+
70+
child.stderr!.on("data", (chunk: Buffer) => {
71+
stderr += chunk.toString();
72+
});
73+
74+
child.on("error", (err) => {
75+
done({ ok: false, error: err.message });
76+
});
77+
78+
child.on("exit", (code) => {
79+
if (code !== null && code !== 0) {
80+
done({ ok: false, error: stderr || `Process exited with code ${code}` });
81+
}
82+
});
83+
84+
setTimeout(() => {
85+
child.stderr!.destroy();
86+
child.unref();
87+
done({ ok: true, pid: child.pid });
88+
}, 2000);
89+
});
90+
91+
if (!result.ok) {
92+
return NextResponse.json({ error: result.error }, { status: 500 });
93+
}
3894

3995
return NextResponse.json({
4096
status: "launched",
41-
pid: child.pid,
97+
pid: result.pid,
4298
runId: env.RUN_ID || "(auto-generated)",
4399
});
44100
}

dashboard/src/app/globals.css

Lines changed: 57 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -49,72 +49,56 @@
4949
}
5050

5151
:root {
52-
--background: oklch(1 0 0);
53-
--foreground: oklch(0.145 0 0);
54-
--card: oklch(1 0 0);
55-
--card-foreground: oklch(0.145 0 0);
56-
--popover: oklch(1 0 0);
57-
--popover-foreground: oklch(0.145 0 0);
58-
--primary: oklch(0.205 0 0);
59-
--primary-foreground: oklch(0.985 0 0);
60-
--secondary: oklch(0.97 0 0);
61-
--secondary-foreground: oklch(0.205 0 0);
62-
--muted: oklch(0.97 0 0);
63-
--muted-foreground: oklch(0.556 0 0);
64-
--accent: oklch(0.97 0 0);
65-
--accent-foreground: oklch(0.205 0 0);
66-
--destructive: oklch(0.577 0.245 27.325);
67-
--border: oklch(0.922 0 0);
68-
--input: oklch(0.922 0 0);
69-
--ring: oklch(0.708 0 0);
70-
--chart-1: oklch(0.87 0 0);
71-
--chart-2: oklch(0.556 0 0);
72-
--chart-3: oklch(0.439 0 0);
73-
--chart-4: oklch(0.371 0 0);
74-
--chart-5: oklch(0.269 0 0);
75-
--radius: 0.625rem;
76-
--sidebar: oklch(0.985 0 0);
77-
--sidebar-foreground: oklch(0.145 0 0);
78-
--sidebar-primary: oklch(0.205 0 0);
79-
--sidebar-primary-foreground: oklch(0.985 0 0);
80-
--sidebar-accent: oklch(0.97 0 0);
81-
--sidebar-accent-foreground: oklch(0.205 0 0);
82-
--sidebar-border: oklch(0.922 0 0);
83-
--sidebar-ring: oklch(0.708 0 0);
84-
}
52+
/* Grafana-inspired dark theme */
53+
--background: #1a1d24;
54+
--foreground: #eef0f4;
55+
--card: #22262e;
56+
--card-foreground: #eef0f4;
57+
--popover: #22262e;
58+
--popover-foreground: #eef0f4;
59+
--primary: #7aadff;
60+
--primary-foreground: #1a1d24;
61+
--secondary: #2a2f38;
62+
--secondary-foreground: #eef0f4;
63+
--muted: #2a2f38;
64+
--muted-foreground: #a4a8b2;
65+
--accent: #2a2f38;
66+
--accent-foreground: #eef0f4;
67+
--destructive: #ff6b7f;
68+
--border: #3a3f4a;
69+
--input: #3a3f4a;
70+
--ring: #7aadff;
71+
--chart-1: #7aadff;
72+
--chart-2: #96d88d;
73+
--chart-3: #ffb347;
74+
--chart-4: #d4a4f5;
75+
--chart-5: #a8d4ff;
76+
--radius: 0.25rem;
77+
--sidebar: #22262e;
78+
--sidebar-foreground: #eef0f4;
79+
--sidebar-primary: #7aadff;
80+
--sidebar-primary-foreground: #1a1d24;
81+
--sidebar-accent: #2a2f38;
82+
--sidebar-accent-foreground: #eef0f4;
83+
--sidebar-border: #3a3f4a;
84+
--sidebar-ring: #7aadff;
8585

86-
.dark {
87-
--background: oklch(0.145 0 0);
88-
--foreground: oklch(0.985 0 0);
89-
--card: oklch(0.205 0 0);
90-
--card-foreground: oklch(0.985 0 0);
91-
--popover: oklch(0.205 0 0);
92-
--popover-foreground: oklch(0.985 0 0);
93-
--primary: oklch(0.922 0 0);
94-
--primary-foreground: oklch(0.205 0 0);
95-
--secondary: oklch(0.269 0 0);
96-
--secondary-foreground: oklch(0.985 0 0);
97-
--muted: oklch(0.269 0 0);
98-
--muted-foreground: oklch(0.708 0 0);
99-
--accent: oklch(0.269 0 0);
100-
--accent-foreground: oklch(0.985 0 0);
101-
--destructive: oklch(0.704 0.191 22.216);
102-
--border: oklch(1 0 0 / 10%);
103-
--input: oklch(1 0 0 / 15%);
104-
--ring: oklch(0.556 0 0);
105-
--chart-1: oklch(0.87 0 0);
106-
--chart-2: oklch(0.556 0 0);
107-
--chart-3: oklch(0.439 0 0);
108-
--chart-4: oklch(0.371 0 0);
109-
--chart-5: oklch(0.269 0 0);
110-
--sidebar: oklch(0.205 0 0);
111-
--sidebar-foreground: oklch(0.985 0 0);
112-
--sidebar-primary: oklch(0.488 0.243 264.376);
113-
--sidebar-primary-foreground: oklch(0.985 0 0);
114-
--sidebar-accent: oklch(0.269 0 0);
115-
--sidebar-accent-foreground: oklch(0.985 0 0);
116-
--sidebar-border: oklch(1 0 0 / 10%);
117-
--sidebar-ring: oklch(0.556 0 0);
86+
/* Dashboard-creator accent palette */
87+
--g-bg-canvas: #1a1d24;
88+
--g-bg-primary: #22262e;
89+
--g-bg-secondary: #2a2f38;
90+
--g-border: #3a3f4a;
91+
--g-text-primary: #eef0f4;
92+
--g-text-secondary: #a4a8b2;
93+
--g-text-disabled: #7a7e88;
94+
--g-green: #96d88d;
95+
--g-red: #ff6b7f;
96+
--g-orange: #ffb347;
97+
--g-blue: #7aadff;
98+
--g-yellow: #ffe66d;
99+
--g-purple: #d4a4f5;
100+
--g-cyan: #a8d4ff;
101+
--g-teal: #6ddcb2;
118102
}
119103

120104
@layer base {
@@ -123,8 +107,15 @@
123107
}
124108
body {
125109
@apply bg-background text-foreground;
110+
font-size: 16px;
126111
}
127112
html {
128113
@apply font-sans;
129114
}
130-
}
115+
tr.g-row-hover:hover {
116+
background-color: var(--g-bg-secondary);
117+
}
118+
.g-hover:hover {
119+
background-color: var(--g-bg-secondary);
120+
}
121+
}

0 commit comments

Comments
 (0)