Skip to content

Commit d63e6b7

Browse files
NOGIT007claude
andauthored
feat: CodeBurn-inspired overview dashboard with dense multi-panel layout (#8)
Replace the old tabbed layout with a dense, all-at-once grid dashboard inspired by CodeBurn. Shows cost, tokens, sessions, cache hit rate, daily activity, projects, models, activity categories, tools, and MCP servers — all visible simultaneously without tab switching. - New OverviewDashboard component with 6 data panels in 2-column grid - HBar gradient component for horizontal bar visualizations - Dashboard/Analytics nav toggle (analytics preserved as secondary view) - Rate limits integrated into dashboard with 60s polling - Project path normalization (merges subdirs like scripts/ into parent) - Version badge in macOS menu bar app - Responsive layout with mobile breakpoint Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0b57f09 commit d63e6b7

7 files changed

Lines changed: 889 additions & 139 deletions

File tree

ClaudeMonitorBar/Sources/main.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ class ServerManager: ObservableObject {
88
@Published var isRunning = false
99
@Published var statusText = "Checking..."
1010

11+
let version: String = {
12+
let pkgPath = "/Users/kennetkusk/code/claude-monitoring/package.json"
13+
guard let data = try? Data(contentsOf: URL(fileURLWithPath: pkgPath)),
14+
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
15+
let v = json["version"] as? String else { return "?" }
16+
return "v\(v)"
17+
}()
18+
1119
private var pollTimer: Timer?
1220
private let port = 4500
1321
private let repoPath = "/Users/kennetkusk/code/claude-monitoring"
@@ -172,6 +180,13 @@ struct MenuBarView: View {
172180
.frame(width: 8, height: 8)
173181
Text("Claude Code Monitor")
174182
.font(.headline)
183+
Text(serverManager.version)
184+
.font(.caption2)
185+
.foregroundColor(.secondary)
186+
.padding(.horizontal, 5)
187+
.padding(.vertical, 2)
188+
.background(Color.secondary.opacity(0.12))
189+
.cornerRadius(4)
175190
Spacer()
176191
Text(serverManager.statusText)
177192
.font(.caption)

src/client/App.tsx

Lines changed: 52 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { useState, useEffect, useRef, useCallback } from "react";
22
import type { ActiveSession, DayEntry, PeriodStats, WsMessage } from "./types";
3+
import { OverviewDashboard } from "./components/OverviewDashboard";
34
import { LiveSessions } from "./components/LiveSessions";
45
import { StatsTabs } from "./components/StatsTabs";
5-
import { RateLimits } from "./components/RateLimits";
66
import { AnalyticsTabs } from "./components/AnalyticsTabs";
77

88
type Period = "today" | "week" | "month";
99
type WsStatus = "connecting" | "connected" | "disconnected";
10+
type View = "dashboard" | "analytics";
1011

1112
export function App() {
1213
const [sessions, setSessions] = useState<ActiveSession[]>([]);
@@ -15,6 +16,7 @@ export function App() {
1516
const [period, setPeriod] = useState<Period>("today");
1617
const [wsStatus, setWsStatus] = useState<WsStatus>("connecting");
1718
const [apiError, setApiError] = useState<string | null>(null);
19+
const [view, setView] = useState<View>("dashboard");
1820
const wsRef = useRef<WebSocket | null>(null);
1921
const reconnectTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
2022

@@ -80,8 +82,6 @@ export function App() {
8082
});
8183

8284
fetchSessions();
83-
84-
// Poll every 15s to pick up closed sessions
8585
const pollInterval = setInterval(fetchSessions, 15000);
8686

8787
fetch("/api/history?days=30")
@@ -102,97 +102,7 @@ export function App() {
102102
}, [connectWs]);
103103

104104
return (
105-
<div style={{ maxWidth: 1200, margin: "0 auto", padding: "2rem 1.5rem 3rem" }}>
106-
{/* Top bar */}
107-
<header
108-
style={{
109-
display: "flex",
110-
alignItems: "center",
111-
justifyContent: "space-between",
112-
marginBottom: "2.5rem",
113-
paddingBottom: "1.25rem",
114-
borderBottom: `1px solid rgba(69, 71, 90, 0.3)`,
115-
}}
116-
>
117-
<div style={{ display: "flex", alignItems: "baseline", gap: "0.75rem" }}>
118-
<h1
119-
style={{
120-
margin: 0,
121-
fontFamily: "var(--font-display)",
122-
fontSize: "1.6rem",
123-
fontWeight: 800,
124-
color: "var(--ctp-lavender)",
125-
letterSpacing: "-0.02em",
126-
}}
127-
>
128-
Claude Monitor
129-
</h1>
130-
{sessions.length > 0 && (
131-
<span
132-
style={{
133-
background: "rgba(166, 227, 161, 0.15)",
134-
color: "var(--ctp-green)",
135-
fontFamily: "var(--font-mono)",
136-
fontSize: "0.7rem",
137-
fontWeight: 600,
138-
padding: "0.2rem 0.6rem",
139-
borderRadius: 20,
140-
border: "1px solid rgba(166, 227, 161, 0.2)",
141-
}}
142-
>
143-
{sessions.length} active
144-
</span>
145-
)}
146-
</div>
147-
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem" }}>
148-
{window.__SNAPSHOT__ ? (
149-
<span
150-
style={{
151-
fontFamily: "var(--font-mono)",
152-
fontSize: "0.6rem",
153-
color: "var(--ctp-yellow)",
154-
}}
155-
title={`Snapshot captured ${new Date(window.__SNAPSHOT__.capturedAt).toLocaleString()}`}
156-
>
157-
snapshot · {new Date(window.__SNAPSHOT__.capturedAt).toLocaleDateString()}
158-
</span>
159-
) : (
160-
<span
161-
style={{
162-
display: "flex",
163-
alignItems: "center",
164-
gap: "0.3rem",
165-
fontFamily: "var(--font-mono)",
166-
fontSize: "0.6rem",
167-
color: wsStatus === "connected" ? "var(--ctp-green)"
168-
: wsStatus === "connecting" ? "var(--ctp-yellow)"
169-
: "var(--ctp-red)",
170-
}}
171-
title={`WebSocket: ${wsStatus}`}
172-
>
173-
<span style={{
174-
width: 6,
175-
height: 6,
176-
borderRadius: "50%",
177-
background: "currentColor",
178-
display: "inline-block",
179-
}} />
180-
{wsStatus === "disconnected" ? "reconnecting…" : "live"}
181-
</span>
182-
)}
183-
<span
184-
style={{
185-
fontFamily: "var(--font-mono)",
186-
fontSize: "0.65rem",
187-
color: "var(--ctp-overlay0)",
188-
letterSpacing: "0.05em",
189-
}}
190-
>
191-
powered by bun
192-
</span>
193-
</div>
194-
</header>
195-
105+
<div>
196106
{/* Error banner */}
197107
{apiError && (
198108
<div
@@ -201,7 +111,7 @@ export function App() {
201111
border: "1px solid rgba(243, 139, 168, 0.3)",
202112
borderRadius: 10,
203113
padding: "0.6rem 1rem",
204-
marginBottom: "1.5rem",
114+
margin: "1rem 1.5rem 0",
205115
fontFamily: "var(--font-mono)",
206116
fontSize: "0.75rem",
207117
color: "var(--ctp-red)",
@@ -212,47 +122,54 @@ export function App() {
212122
</div>
213123
)}
214124

215-
{/* Live Sessions */}
216-
<section style={{ marginBottom: "2.5rem" }}>
217-
<h2 className="section-title">Live Sessions</h2>
218-
<LiveSessions sessions={sessions} />
219-
</section>
220-
221-
{/* Rate Limits */}
222-
<section style={{ marginBottom: "2rem" }}>
223-
<h2 className="section-title">Rate Limits</h2>
224-
<div className="card">
225-
<RateLimits />
125+
{/* Navigation */}
126+
<div style={{ maxWidth: 1200, margin: "0 auto", padding: "1rem 1.5rem 0" }}>
127+
<nav className="ov-nav">
128+
<button
129+
className={`ov-nav-btn ${view === "dashboard" ? "ov-nav-btn--active" : ""}`}
130+
onClick={() => setView("dashboard")}
131+
>
132+
Dashboard
133+
</button>
134+
<button
135+
className={`ov-nav-btn ${view === "analytics" ? "ov-nav-btn--active" : ""}`}
136+
onClick={() => setView("analytics")}
137+
>
138+
Analytics
139+
</button>
140+
</nav>
141+
</div>
142+
143+
{view === "dashboard" ? (
144+
<>
145+
<OverviewDashboard
146+
activeSessions={sessions.length}
147+
wsStatus={wsStatus}
148+
/>
149+
150+
{/* Live Sessions — show below dashboard when active */}
151+
{sessions.length > 0 && (
152+
<div style={{ maxWidth: 1200, margin: "0 auto", padding: "0 1.5rem 2rem" }}>
153+
<h2 className="section-title">Live Sessions</h2>
154+
<LiveSessions sessions={sessions} />
155+
</div>
156+
)}
157+
</>
158+
) : (
159+
<div style={{ maxWidth: 1200, margin: "0 auto", padding: "1rem 1.5rem 3rem" }}>
160+
{/* Stats */}
161+
<section style={{ marginBottom: "2rem" }}>
162+
<h2 className="section-title">Usage Statistics</h2>
163+
<StatsTabs onStatsChange={setCurrentStats} onPeriodChange={setPeriod} />
164+
</section>
165+
166+
{/* Analytics */}
167+
<section style={{ marginBottom: "2.5rem" }}>
168+
<h2 className="section-title">Analytics</h2>
169+
<AnalyticsTabs period={period} currentStats={currentStats} history={history} />
170+
</section>
226171
</div>
227-
</section>
228-
229-
{/* Stats with comparison badges */}
230-
<section style={{ marginBottom: "2rem" }}>
231-
<h2 className="section-title">Usage Statistics</h2>
232-
<StatsTabs onStatsChange={setCurrentStats} onPeriodChange={setPeriod} />
233-
</section>
234-
235-
{/* Analytics */}
236-
<section style={{ marginBottom: "2.5rem" }}>
237-
<h2 className="section-title">Analytics</h2>
238-
<AnalyticsTabs period={period} currentStats={currentStats} history={history} />
239-
</section>
240-
241-
{/* Footer */}
242-
<footer
243-
style={{
244-
marginTop: "3rem",
245-
paddingTop: "1.25rem",
246-
borderTop: "1px solid rgba(69, 71, 90, 0.3)",
247-
textAlign: "center",
248-
fontFamily: "var(--font-mono)",
249-
fontSize: "0.7rem",
250-
color: "var(--ctp-overlay0)",
251-
letterSpacing: "0.03em",
252-
}}
253-
>
254-
made with AI <span style={{ color: "var(--ctp-red)", fontSize: "0.85rem" }}></span>
255-
</footer>
172+
)}
256173
</div>
257174
);
258175
}

src/client/components/HBar.tsx

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/** Horizontal gradient bar — CodeBurn-inspired */
2+
3+
interface Props {
4+
/** 0–1 fraction filled */
5+
value: number;
6+
/** Bar width in px (default 80) */
7+
width?: number;
8+
/** Bar height in px (default 10) */
9+
height?: number;
10+
}
11+
12+
/** Interpolate blue → amber → orange based on percentage */
13+
function gradientColor(pct: number): string {
14+
// blue(137,180,250) → amber(249,226,175) → orange(250,179,135)
15+
if (pct <= 0.5) {
16+
const t = pct / 0.5;
17+
const r = Math.round(137 + (249 - 137) * t);
18+
const g = Math.round(180 + (226 - 180) * t);
19+
const b = Math.round(250 + (175 - 250) * t);
20+
return `rgb(${r},${g},${b})`;
21+
}
22+
const t = (pct - 0.5) / 0.5;
23+
const r = Math.round(249 + (250 - 249) * t);
24+
const g = Math.round(226 + (179 - 226) * t);
25+
const b = Math.round(175 + (135 - 175) * t);
26+
return `rgb(${r},${g},${b})`;
27+
}
28+
29+
export function HBar({ value, width = 80, height = 10 }: Props) {
30+
const clamped = Math.max(0, Math.min(1, value));
31+
const filledW = Math.round(clamped * width);
32+
33+
return (
34+
<div
35+
style={{
36+
display: "inline-flex",
37+
width,
38+
height,
39+
borderRadius: 3,
40+
overflow: "hidden",
41+
background: "rgba(69,71,90,0.3)",
42+
flexShrink: 0,
43+
}}
44+
>
45+
{filledW > 0 && (
46+
<div
47+
style={{
48+
width: filledW,
49+
height: "100%",
50+
background: `linear-gradient(90deg, ${gradientColor(0)}, ${gradientColor(clamped)})`,
51+
borderRadius: 3,
52+
}}
53+
/>
54+
)}
55+
</div>
56+
);
57+
}

0 commit comments

Comments
 (0)