feat: Claude Code Monitor — lanes, pipelines and a merged workspace
Internal SmartGift build of a Claude Code monitoring dashboard. Lanes: a durable unit of parallel agent work, one per working directory, tracked across session restarts. Managed lanes are git worktrees the dashboard provisions and can reset or remove behind a three-check destroy guard and a counted preflight; adopted lanes are directories you already own and are never destroyable. Pipelines: a lane moves through pipeline stages. A stage the agent declares with evidence renders green; a stage inferred from the tool-event stream renders dashed amber and never counts as done. Detection is forward-only within a 30-minute window, and never writes the declared stage. Workspace: one page at /run with a lane grid, the selected lane's pipeline, and a full Claude console behind a disclosure.
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#6366f1"/>
|
||||
<stop offset="100%" stop-color="#818cf8"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="glow" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#a5b4fc" stop-opacity="0.9"/>
|
||||
<stop offset="100%" stop-color="#c7d2fe" stop-opacity="0.6"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- Hexagon background -->
|
||||
<polygon points="16,2 28,9 28,23 16,30 4,23 4,9" fill="url(#bg)" rx="2"/>
|
||||
<!-- Center node -->
|
||||
<circle cx="16" cy="16" r="3" fill="white" opacity="0.95"/>
|
||||
<!-- Three connector lines radiating outward -->
|
||||
<line x1="16" y1="13" x2="16" y2="7" stroke="white" stroke-width="1.5" stroke-linecap="round" opacity="0.7"/>
|
||||
<line x1="18.6" y1="17.5" x2="24" y2="20.5" stroke="white" stroke-width="1.5" stroke-linecap="round" opacity="0.7"/>
|
||||
<line x1="13.4" y1="17.5" x2="8" y2="20.5" stroke="white" stroke-width="1.5" stroke-linecap="round" opacity="0.7"/>
|
||||
<!-- Outer nodes -->
|
||||
<circle cx="16" cy="6" r="1.8" fill="url(#glow)"/>
|
||||
<circle cx="24.5" cy="21" r="1.8" fill="url(#glow)"/>
|
||||
<circle cx="7.5" cy="21" r="1.8" fill="url(#glow)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"id": "claude-code-agent-dashboard",
|
||||
"name": "Agent Dashboard - Claude Code Monitor",
|
||||
"short_name": "Agent Dashboard",
|
||||
"description": "Real-time monitoring platform for Claude Code agent activity.",
|
||||
"author": "Nguyễn Ngọc Trí Vĩ",
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"theme_color": "#6366f1",
|
||||
"background_color": "#0f1117",
|
||||
"orientation": "any",
|
||||
"categories": ["developer-tools", "productivity"],
|
||||
"icons": [
|
||||
{
|
||||
"src": "/favicon.svg",
|
||||
"sizes": "any",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/favicon.svg",
|
||||
"sizes": "any",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 630" width="1200" height="630">
|
||||
<defs>
|
||||
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#0d0d16"/>
|
||||
<stop offset="100%" stop-color="#12121e"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="hexGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#6366f1"/>
|
||||
<stop offset="100%" stop-color="#818cf8"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="nodeGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#a5b4fc" stop-opacity="0.9"/>
|
||||
<stop offset="100%" stop-color="#c7d2fe" stop-opacity="0.6"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="titleGrad" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stop-color="#e2e2f0"/>
|
||||
<stop offset="55%" stop-color="#a5b4fc"/>
|
||||
<stop offset="100%" stop-color="#818cf8"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="glow1" cx="50%" cy="50%" r="50%">
|
||||
<stop offset="0%" stop-color="#6366f1" stop-opacity="0.18"/>
|
||||
<stop offset="100%" stop-color="#6366f1" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="glow2" cx="50%" cy="50%" r="50%">
|
||||
<stop offset="0%" stop-color="#10b981" stop-opacity="0.10"/>
|
||||
<stop offset="100%" stop-color="#10b981" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<filter id="blur1">
|
||||
<feGaussianBlur stdDeviation="40"/>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<rect width="1200" height="630" fill="url(#bgGrad)"/>
|
||||
<ellipse cx="200" cy="200" rx="340" ry="280" fill="url(#glow1)" filter="url(#blur1)"/>
|
||||
<ellipse cx="1050" cy="480" rx="280" ry="220" fill="url(#glow2)" filter="url(#blur1)"/>
|
||||
|
||||
<g opacity="0.07" fill="#6366f1">
|
||||
<circle cx="80" cy="80" r="1.5"/><circle cx="160" cy="80" r="1.5"/><circle cx="240" cy="80" r="1.5"/><circle cx="320" cy="80" r="1.5"/>
|
||||
<circle cx="80" cy="160" r="1.5"/><circle cx="160" cy="160" r="1.5"/><circle cx="240" cy="160" r="1.5"/><circle cx="320" cy="160" r="1.5"/>
|
||||
<circle cx="80" cy="240" r="1.5"/><circle cx="160" cy="240" r="1.5"/><circle cx="240" cy="240" r="1.5"/><circle cx="320" cy="240" r="1.5"/>
|
||||
<circle cx="900" cy="400" r="1.5"/><circle cx="980" cy="400" r="1.5"/><circle cx="1060" cy="400" r="1.5"/><circle cx="1140" cy="400" r="1.5"/>
|
||||
<circle cx="900" cy="480" r="1.5"/><circle cx="980" cy="480" r="1.5"/><circle cx="1060" cy="480" r="1.5"/><circle cx="1140" cy="480" r="1.5"/>
|
||||
<circle cx="900" cy="560" r="1.5"/><circle cx="980" cy="560" r="1.5"/><circle cx="1060" cy="560" r="1.5"/><circle cx="1140" cy="560" r="1.5"/>
|
||||
</g>
|
||||
|
||||
<polygon points="980,120 1120,200 1120,360 980,440 840,360 840,200"
|
||||
fill="none" stroke="#6366f1" stroke-width="1" opacity="0.12"/>
|
||||
<polygon points="980,150 1095,215 1095,345 980,410 865,345 865,215"
|
||||
fill="none" stroke="#6366f1" stroke-width="0.5" opacity="0.08"/>
|
||||
|
||||
<line x1="60" y1="560" x2="200" y2="460" stroke="#6366f1" stroke-width="1" opacity="0.15"/>
|
||||
<line x1="200" y1="460" x2="380" y2="540" stroke="#6366f1" stroke-width="1" opacity="0.15"/>
|
||||
<circle cx="60" cy="560" r="3" fill="#6366f1" opacity="0.3"/>
|
||||
<circle cx="200" cy="460" r="3" fill="#6366f1" opacity="0.3"/>
|
||||
<circle cx="380" cy="540" r="3" fill="#6366f1" opacity="0.3"/>
|
||||
|
||||
<g transform="translate(128, 265)">
|
||||
<polygon points="64,0 112,28 112,92 64,120 16,92 16,28" fill="url(#hexGrad)"/>
|
||||
<circle cx="64" cy="60" r="11" fill="white" opacity="0.95"/>
|
||||
<line x1="64" y1="49" x2="64" y2="18" stroke="white" stroke-width="5" stroke-linecap="round" opacity="0.7"/>
|
||||
<line x1="73.5" y1="65.5" x2="97" y2="79" stroke="white" stroke-width="5" stroke-linecap="round" opacity="0.7"/>
|
||||
<line x1="54.5" y1="65.5" x2="31" y2="79" stroke="white" stroke-width="5" stroke-linecap="round" opacity="0.7"/>
|
||||
<circle cx="64" cy="14" r="7" fill="url(#nodeGrad)"/>
|
||||
<circle cx="100" cy="80" r="7" fill="url(#nodeGrad)"/>
|
||||
<circle cx="28" cy="80" r="7" fill="url(#nodeGrad)"/>
|
||||
</g>
|
||||
|
||||
<text x="290" y="298"
|
||||
font-family="'Inter', 'Helvetica Neue', Arial, sans-serif"
|
||||
font-size="72" font-weight="800" letter-spacing="-2"
|
||||
fill="url(#titleGrad)">Agent Dashboard</text>
|
||||
|
||||
<text x="292" y="355"
|
||||
font-family="'Inter', 'Helvetica Neue', Arial, sans-serif"
|
||||
font-size="26" font-weight="400"
|
||||
fill="#8888aa">Real-time monitoring platform for Claude Code agent activity</text>
|
||||
|
||||
<line x1="290" y1="385" x2="840" y2="385" stroke="#252538" stroke-width="1"/>
|
||||
|
||||
<g transform="translate(290, 408)" font-family="'Inter', 'Helvetica Neue', Arial, sans-serif" font-size="15" font-weight="600">
|
||||
<rect x="0" y="0" width="88" height="28" rx="6" fill="#33993320" stroke="#33993340"/>
|
||||
<text x="44" y="19" text-anchor="middle" fill="#6dbf6d">Node.js 18+</text>
|
||||
<rect x="100" y="0" width="82" height="28" rx="6" fill="#61dafb18" stroke="#61dafb30"/>
|
||||
<text x="141" y="19" text-anchor="middle" fill="#61dafb">React 18</text>
|
||||
<rect x="194" y="0" width="100" height="28" rx="6" fill="#3178c618" stroke="#3178c630"/>
|
||||
<text x="244" y="19" text-anchor="middle" fill="#5a9fd4">TypeScript</text>
|
||||
<rect x="306" y="0" width="72" height="28" rx="6" fill="#003b5718" stroke="#005f8a30"/>
|
||||
<text x="342" y="19" text-anchor="middle" fill="#5bb8e0">SQLite</text>
|
||||
<rect x="390" y="0" width="100" height="28" rx="6" fill="#10b98118" stroke="#10b98130"/>
|
||||
<text x="440" y="19" text-anchor="middle" fill="#34d399">WebSocket</text>
|
||||
<rect x="502" y="0" width="62" height="28" rx="6" fill="#f59e0b18" stroke="#f59e0b30"/>
|
||||
<text x="533" y="19" text-anchor="middle" fill="#fbbf24">MIT</text>
|
||||
</g>
|
||||
|
||||
<text x="290" y="580"
|
||||
font-family="'Inter', 'Helvetica Neue', Arial, sans-serif"
|
||||
font-size="18" font-weight="500"
|
||||
fill="#55556a">github.com/SmartGift · smartgift.vn</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.8 KiB |
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* @description Service Worker for caching static assets and handling push notifications.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
// Bump this any time the SW logic changes - old clients will install the new
|
||||
// SW, drop their existing caches in `activate`, and `skipWaiting` so the
|
||||
// freshly-built bundle starts being served on the very next request.
|
||||
const CACHE_NAME = "dashboard-v2";
|
||||
|
||||
self.addEventListener("install", () => {
|
||||
// No pre-cache: network-first below means the cache fills lazily, and
|
||||
// there's nothing to "warm" - the v1 SW was pre-caching `/`, which is
|
||||
// exactly the file most likely to go stale after a rebuild.
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
event.waitUntil(
|
||||
caches
|
||||
.keys()
|
||||
.then((keys) =>
|
||||
Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
|
||||
)
|
||||
.then(() => self.clients.claim())
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener("fetch", (event) => {
|
||||
const { request } = event;
|
||||
if (request.method !== "GET") return;
|
||||
const url = new URL(request.url);
|
||||
if (url.origin !== self.location.origin) return;
|
||||
// Skip API, WebSocket, and Vite HMR endpoints
|
||||
if (
|
||||
url.pathname.startsWith("/api/") ||
|
||||
url.pathname.startsWith("/ws") ||
|
||||
url.pathname.includes("__vite")
|
||||
)
|
||||
return;
|
||||
|
||||
// Hashed bundles under /assets/ are immutable for a given URL - cache-first
|
||||
// is safe and fast. A new build emits new filenames, so stale entries simply
|
||||
// don't get re-requested.
|
||||
if (url.pathname.startsWith("/assets/")) {
|
||||
event.respondWith(
|
||||
caches.match(request).then(
|
||||
(cached) =>
|
||||
cached ||
|
||||
fetch(request).then((response) => {
|
||||
if (response.ok && response.type === "basic") {
|
||||
const clone = response.clone();
|
||||
caches.open(CACHE_NAME).then((cache) => cache.put(request, clone));
|
||||
}
|
||||
return response;
|
||||
})
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Everything else (navigations, sw.js, manifest, icons, root /): network-first
|
||||
// with cache fallback. The user always gets the freshest UI while online and
|
||||
// a sensible fallback when offline.
|
||||
event.respondWith(
|
||||
fetch(request)
|
||||
.then((response) => {
|
||||
if (response.ok && response.type === "basic") {
|
||||
const clone = response.clone();
|
||||
caches.open(CACHE_NAME).then((cache) => cache.put(request, clone));
|
||||
}
|
||||
return response;
|
||||
})
|
||||
.catch(() => caches.match(request).then((c) => c || caches.match("/")))
|
||||
);
|
||||
});
|
||||
|
||||
// --- Push notifications (existing) ---
|
||||
|
||||
self.addEventListener("push", (event) => {
|
||||
const data = event.data
|
||||
? event.data.json()
|
||||
: { title: "Agent Monitor", body: "New notification" };
|
||||
const { title, ...options } = data;
|
||||
event.waitUntil(self.registration.showNotification(title, { silent: false, ...options }));
|
||||
});
|
||||
|
||||
self.addEventListener("notificationclick", (event) => {
|
||||
event.notification.close();
|
||||
event.waitUntil(
|
||||
clients.matchAll({ type: "window" }).then((windowClients) => {
|
||||
for (const client of windowClients) {
|
||||
if (client.focus) {
|
||||
return client.focus();
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user