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:
2026-07-29 17:07:45 +07:00
commit f78c7f9a2e
783 changed files with 221663 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
---
name: mcp-maintainer
description: Operate and maintain the local MCP server for this repository. Use for MCP tool updates, policy-guard changes, host configuration, and MCP runtime troubleshooting.
---
# MCP Maintainer Skill
## Workflow
- Confirm dashboard API availability (`/api/health`).
- Inspect affected MCP domain modules under `mcp/src/tools/domains/`.
- Preserve safety gates in `mcp/src/policy/tool-guards.ts`.
- Validate with `npm run mcp:typecheck` and `npm run mcp:build`.
## Safety rules
- Keep loopback-only target checks enabled.
- Keep mutating and destructive tools behind explicit flags.
- Do not log protocol data to stdout.
## References
- `references/tool-domain-map.md`
- `references/operations-runbook.md`
@@ -0,0 +1,4 @@
interface:
display_name: "MCP Maintainer"
short_description: "Maintain MCP tools, policy gates, and host integration."
default_prompt: "Use mcp-maintainer to update MCP tooling safely and verify runtime integrity."
@@ -0,0 +1,14 @@
# MCP Operations Runbook
## Modes
- Read-only:
- `MCP_DASHBOARD_ALLOW_MUTATIONS=false`
- `MCP_DASHBOARD_ALLOW_DESTRUCTIVE=false`
- Admin:
- Set mutations true for controlled maintenance operations.
- Destructive:
- Set both true and require `confirmation_token = CLEAR_ALL_DATA`.
## Verification
- `npm run mcp:typecheck`
- `npm run mcp:build`
@@ -0,0 +1,8 @@
# MCP Tool Domain Map
- `observability-tools.ts`: health, stats, analytics, snapshots, export.
- `session-tools.ts`: list/get/create/update sessions.
- `agent-tools.ts`: list/get/create/update agents.
- `event-tools.ts`: event listing and hook ingestion.
- `pricing-tools.ts`: pricing CRUD and cost calculations.
- `maintenance-tools.ts`: cleanup, reimport, reinstall hooks, destructive clear.
+21
View File
@@ -0,0 +1,21 @@
---
name: release-guard
description: Run release-readiness checks for this repository. Use when validating docs, scripts, verification coverage, and operational safety before merge or release.
---
# Release Guard Skill
## Workflow
- Check command consistency across docs and `package.json`.
- Verify architecture docs align with current code paths.
- Validate that safety controls are still documented and enforced.
- Report pass/fail with concrete file references.
## Focus areas
- Hook flow and failure behavior.
- Session/agent lifecycle semantics.
- MCP safety gates and host setup instructions.
- Troubleshooting accuracy.
## References
- `references/release-checklist.md`
@@ -0,0 +1,4 @@
interface:
display_name: "Release Guard"
short_description: "Audit release readiness across code, docs, and safety controls."
default_prompt: "Use release-guard to audit this branch for release readiness and report concrete findings."
@@ -0,0 +1,7 @@
# Release Checklist
- Commands in docs exist in root `package.json`.
- Validation steps are documented for backend, frontend, and MCP.
- Behavior-changing diffs mention migration/compatibility impacts.
- Safety-sensitive operations remain guarded by explicit flags.
- Troubleshooting sections reflect the current architecture.
+21
View File
@@ -0,0 +1,21 @@
---
name: repo-onboarding
description: Understand this repository quickly before making changes. Use for architecture discovery, ownership mapping, command selection, and initial implementation planning.
---
# Repo Onboarding Skill
## Workflow
- Read `AGENTS.md`, `README.md`, and `ARCHITECTURE.md`.
- Determine target layer: `server/`, `client/`, `mcp/`, or docs.
- Identify the minimal file set needed for the task.
- Select verification commands before editing.
## Verification defaults
- Backend: `npm run test:server`
- Frontend: `npm run test:client`
- MCP: `npm run mcp:typecheck` and `npm run mcp:build`
## References
- `references/module-map.md`
- `references/verification-map.md`
@@ -0,0 +1,4 @@
interface:
display_name: "Repo Onboarding"
short_description: "Map architecture, ownership, and verification strategy before coding."
default_prompt: "Use repo-onboarding to analyze scope, affected modules, and validation commands for this task."
@@ -0,0 +1,9 @@
# Module Map
- `server/index.js`: app startup and route mounting.
- `server/routes/*.js`: API contracts and route behavior.
- `server/db.js`: schema and statement layer.
- `server/websocket.js`: live update broadcast path.
- `client/src/pages/`: route-level UI.
- `client/src/components/`: reusable UI primitives.
- `mcp/src/tools/domains/`: MCP tool families.
@@ -0,0 +1,11 @@
# Verification Map
- Backend changes:
- `npm run test:server`
- Frontend changes:
- `npm run test:client`
- MCP changes:
- `npm run mcp:typecheck`
- `npm run mcp:build`
- Docs-only changes:
- validate command consistency against root `package.json`
+72
View File
@@ -0,0 +1,72 @@
{
"name": "claude-code-agent-monitor-plugins",
"description": "Official plugin marketplace for Claude Code Agent Monitor — 10 plugins for analytics, cost guardrails, productivity, developer tools, AI insights, session forensics, workflow/fleet intelligence, reliability/SLOs, config & memory governance, and dashboard connectivity. Every plugin is powered by the local Agent Monitor API.",
"owner": {
"name": "smartgift",
"url": "https://git.smartgift.io.vn/Smartgift-AI"
},
"homepage": "https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor",
"repository": "https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor",
"plugins": [
{
"name": "ccam-analytics",
"path": "plugins/ccam-analytics",
"description": "Deep analytics on Claude Code sessions — token usage (total_input/total_output/total_cache_read/total_cache_write with compaction baselines pre-summed), cost breakdowns via the pricing engine, usage trends over 365 days, cache efficiency, model mix, and productivity scoring.",
"tags": ["analytics", "tokens", "cost", "productivity"]
},
{
"name": "ccam-cost-guard",
"path": "plugins/ccam-cost-guard",
"description": "Budget guardrails — set spend limits, forecast week/month-end cost from daily trends, surface cost-threshold alerts via the alert-rule API, and quantify model-routing savings.",
"tags": ["budget", "cost", "forecast", "alerts"]
},
{
"name": "ccam-productivity",
"path": "plugins/ccam-productivity",
"description": "Productivity automation — daily standups, weekly/monthly reviews, sprint summaries, time-of-day focus analysis, and workflow optimization using the workflow intelligence API's 11 datasets.",
"tags": ["productivity", "reports", "workflows", "standup"]
},
{
"name": "ccam-devtools",
"path": "plugins/ccam-devtools",
"description": "Developer tools for the Agent Monitor — session debugging with full event-chain inspection, data-integrity inspection, event tracing, transcript search, hook diagnostics, data export (JSON/CSV/Markdown), and system health checks.",
"tags": ["devtools", "debug", "diagnostics", "export"]
},
{
"name": "ccam-insights",
"path": "plugins/ccam-insights",
"description": "AI-powered insights — pattern detection via workflow intelligence (tool flow transitions, agent co-occurrence), anomaly alerting, trend forecasting, regression watch, benchmarking, optimization recommendations, and session comparison.",
"tags": ["insights", "patterns", "anomaly", "optimization"]
},
{
"name": "ccam-sessions",
"path": "plugins/ccam-sessions",
"description": "Session forensics — search by project/model/status, reconstruct event timelines, replay transcripts turn-by-turn, roll up activity per working directory, and review stale sessions before cleanup.",
"tags": ["sessions", "search", "transcript", "replay"]
},
{
"name": "ccam-workflows",
"path": "plugins/ccam-workflows",
"description": "Multi-agent orchestration & fleet intelligence — map the subagent DAG, audit model delegation and effectiveness, report concurrency lanes, trace error propagation by depth, and review Workflow-tool fleet runs.",
"tags": ["workflows", "orchestration", "subagents", "fleet"]
},
{
"name": "ccam-quality",
"path": "plugins/ccam-quality",
"description": "Reliability & SLOs — scan for API errors and tool failures, audit hook delivery health (PreToolUse/PostToolUse balance), track completion/error-rate SLOs with error budgets, and alert on reliability regressions.",
"tags": ["reliability", "errors", "slo", "quality"]
},
{
"name": "ccam-config",
"path": "plugins/ccam-config",
"description": "Claude Code config & memory governance — audit skills/subagents/commands/MCP servers/hooks/settings via the Config Explorer API and curate the file-based memory store (CLAUDE.md + per-project auto-memory files).",
"tags": ["config", "memory", "governance", "mcp"]
},
{
"name": "ccam-dashboard",
"path": "plugins/ccam-dashboard",
"description": "Dashboard connector — quick status checks, one-line metrics, live activity watch, endpoint probing, and MCP server integration for direct tool access to the Agent Monitor API.",
"tags": ["dashboard", "status", "mcp", "connector"]
}
]
}
+20
View File
@@ -0,0 +1,20 @@
---
name: backend-reviewer
description: Review backend route and hook logic for regressions, data integrity risks, and missing tests.
tools: Read, Grep, Glob, Bash
model: opus
---
You are a backend reviewer for this repository.
Focus on:
- Hook event lifecycle correctness.
- Session/agent state-machine regressions.
- API contract compatibility.
- Transaction and persistence correctness.
- Missing or weak verification coverage.
Output:
- Prioritized findings.
- File references.
- Reproduction or validation notes.
+20
View File
@@ -0,0 +1,20 @@
---
name: frontend-reviewer
description: Review React UI changes for behavior regressions, state consistency, and UX breakage.
tools: Read, Grep, Glob, Bash
model: sonnet
---
You are a frontend reviewer for this repository.
Focus on:
- Routing and navigation consistency.
- State updates from websocket and API responses.
- Loading/empty/error state correctness.
- Breaking visual or interaction regressions.
- Missing tests for changed UI behavior.
Output:
- Prioritized findings.
- File references.
- Suggested verification steps.
+20
View File
@@ -0,0 +1,20 @@
---
name: mcp-reviewer
description: Review MCP server changes for tool safety, schema quality, and host integration correctness.
tools: Read, Grep, Glob, Bash
model: opus
---
You are an MCP-focused reviewer for this repository.
Focus on:
- Tool naming and schema strictness.
- Safety gate enforcement for mutating/destructive operations.
- API client timeout/retry/error handling.
- Stdio protocol safety (stderr-only logs).
- Host configuration and runbook documentation accuracy.
Output:
- Prioritized findings.
- File references.
- Verification commands to run.
+14
View File
@@ -0,0 +1,14 @@
---
paths:
- "server/**/*.js"
- "scripts/**/*.js"
---
# Backend and Hook Rules
- Keep API responses backward-compatible unless a breaking change is explicitly requested.
- Maintain deterministic, non-blocking hook ingestion behavior.
- Preserve transaction boundaries and data integrity in event processing logic.
- For route changes, validate input thoroughly and return structured errors.
- Prefer prepared-statement usage patterns already established in `server/db.js`.
- If touching status transitions, verify session and agent lifecycle state machines still make sense.
+12
View File
@@ -0,0 +1,12 @@
---
paths:
- "**/*.md"
---
# Documentation Rules
- Keep command examples executable and aligned with actual scripts.
- Use absolute or clearly rooted paths when discussing project files.
- When adding architecture claims, reflect current code behavior.
- Update all affected docs together (`README`, `ARCHITECTURE`, `SETUP`, `INSTALL`, `mcp/README`) when workflows change.
- Prefer concise sections and concrete troubleshooting steps.
+6
View File
@@ -0,0 +1,6 @@
# File Header Rules (binding for every coding agent)
- Every applicable source file (`.js/.ts/.tsx/.cjs/.mjs/.py/.sh/.css` — excluding `node_modules/`, `dist/`, `data/`, minified/vendored, snapshots) MUST start with the header comment: a truthful file overview plus the exact line `@author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>`.
- Creating a new applicable file → write the header before any code (after the shebang in scripts).
- Editing a file that lacks the header → add it in the same change; if the edit changes the file's purpose, update the overview.
- Formats and the repo-wide audit script live in `.claude/skills/file-headers/` (`bash .claude/skills/file-headers/scripts/check-headers.sh` must exit 0).
+14
View File
@@ -0,0 +1,14 @@
---
paths:
- "client/src/**/*.{ts,tsx,css}"
- "client/public/**/*.tsx"
---
# Frontend Rules
- Preserve existing UI information hierarchy unless redesign is requested.
- Keep component props and API typing explicit; avoid implicit `any`.
- Match existing page/component patterns for loading, empty, and error states.
- When adding UI behavior, ensure it degrades safely if websocket updates are delayed.
- Keep routes consistent with current navigation model.
- Prefer focused UI diffs over broad stylistic rewrites.
+17
View File
@@ -0,0 +1,17 @@
---
paths:
- "mcp/src/**/*.ts"
- "mcp/package.json"
- "mcp/tsconfig.json"
- "mcp/.env.example"
- "mcp/README.md"
---
# MCP Server Rules
- Keep MCP tool names stable and descriptive.
- Keep destructive operations behind explicit guardrails.
- Route all logs to stderr only; never write protocol logs to stdout.
- Keep tool input schemas strict and bounded.
- Preserve loopback-only API target enforcement unless security posture changes by request.
- Verify MCP with `npm run mcp:typecheck` and `npm run mcp:build` after code edits.
+42
View File
@@ -0,0 +1,42 @@
{
"permissions": {
"allow": [
"Bash(npm run:*)",
"Bash(cd client:*)",
"Bash(git add:*)",
"Bash(git:*)",
"Bash(node --test server/lib/__tests__/transcript-cache.test.js)",
"Bash(node -e \"const r = require\\(''''./server/routes/hooks''''\\); console.log\\(''''router type:'''', typeof r\\); console.log\\(''''transcriptCache exists:'''', !!r.transcriptCache\\); console.log\\(''''transcriptCache has extract:'''', typeof r.transcriptCache?.extract\\);\")",
"Bash(node -e \"const r = require\\(''''./server/routes/hooks''''\\); console.log\\(''''router type:'''', typeof r\\); console.log\\(''''transcriptCache exists:'''', Boolean\\(r.transcriptCache\\)\\); console.log\\(''''transcriptCache has extract:'''', typeof \\(r.transcriptCache && r.transcriptCache.extract\\)\\);\")",
"Bash(node -e \"const { transcriptCache } = require\\(''./server/routes/hooks''\\); console.log\\(''transcriptCache exists:'', Boolean\\(transcriptCache\\)\\); console.log\\(''has extract:'', typeof \\(transcriptCache && transcriptCache.extract\\)\\); console.log\\(''has extractCompactions:'', typeof \\(transcriptCache && transcriptCache.extractCompactions\\)\\); console.log\\(''has invalidate:'', typeof \\(transcriptCache && transcriptCache.invalidate\\)\\); console.log\\(''has stats:'', typeof \\(transcriptCache && transcriptCache.stats\\)\\);\")",
"Bash(npx tsc:*)",
"Bash(npx --prefix client tsc --noEmit -p client/tsconfig.json)",
"Bash(node -e \":*)",
"Bash(wc:*)",
"Bash(grep -E \"\\\\.js$\")",
"Bash(npm install:*)",
"Bash(./node_modules/.bin/tsc --noEmit)",
"Bash(npx vite:*)",
"Bash(node -e \"const p = require\\(''./package.json''\\); console.log\\(''deps:'', Object.keys\\(p.dependencies || {}\\).length\\); console.log\\(Object.keys\\(p.dependencies || {}\\).join\\('', ''\\)\\)\")",
"Bash(npx prettier:*)",
"Bash(node scripts/seed.js)",
"Bash(lsof -ti:4820)",
"Bash(xargs kill:*)",
"Bash(lsof -ti:5173)",
"Bash(lsof -ti:5174)",
"Bash(curl -s http://localhost:4820/api/health)",
"Bash(node -e \"require\\('./server/lib/transcript-cache'\\); console.log\\('transcript-cache OK'\\)\")",
"Bash(python3:*)",
"Bash(node -c scripts/import-history.js)",
"Bash(node -e \"const m = require\\('./scripts/import-history'\\); console.log\\(Object.keys\\(m\\).sort\\(\\).join\\(', '\\)\\)\")",
"Bash(node -e \"const TC = require\\('./server/lib/transcript-cache'\\); const tc = new TC\\(\\); console.log\\('transcript-cache loaded OK, methods:', Object.getOwnPropertyNames\\(TC.prototype\\).filter\\(n => n !== 'constructor'\\).join\\(', '\\)\\)\")",
"Bash(node -e \"try { require\\('./server/routes/hooks'\\); } catch\\(e\\) { console.log\\('Expected error \\(db not available in isolation\\):', e.message.slice\\(0, 100\\)\\); }\")",
"Bash(node --check server/routes/hooks.js)",
"Bash(node --check server/lib/transcript-cache.js)",
"Bash(node -e ':*)",
"Bash(node -e \"require\\('./server/lib/transcript-cache'\\); require\\('./scripts/import-history'\\); console.log\\('All modules load OK'\\)\")",
"Bash(claude --version)",
"Bash(gh pr *)"
]
}
}
+23
View File
@@ -0,0 +1,23 @@
---
name: debug-live-issue
description: Debug production-like issues in this repository with disciplined evidence gathering. Use when fixing failing workflows, regressions, flaky behavior, or data inconsistencies across hooks, API, DB, websocket, and UI.
---
# Debug Live Issue
Use this workflow for incident-style debugging.
## Steps
- Capture symptom, expected behavior, and reproducible path.
- Isolate subsystem first: hook ingestion, API route, DB state, websocket, or UI rendering.
- Reproduce with minimal surface area.
- Prove root cause before changing code.
- Apply minimal fix and re-verify.
## Evidence standards
- Prefer direct logs, API responses, DB state checks, and deterministic repro steps.
- Avoid speculative fixes without root-cause evidence.
- If not fully reproducible, state uncertainty and strongest hypothesis.
## References
- Investigation template: `references/investigation-template.md`
@@ -0,0 +1,32 @@
# Investigation Template
## Problem statement
- Symptom:
- Expected behavior:
- First observed:
- Scope:
## Reproduction
- Preconditions:
- Exact steps:
- Actual result:
## Evidence
- Logs:
- API responses:
- Database observations:
- Websocket behavior:
- UI behavior:
## Root cause
- Confirmed cause:
- Why it happens:
## Fix
- Change summary:
- Why this fix is minimal and safe:
## Verification
- Commands/tests run:
- Manual verification:
- Residual risk:
+116
View File
@@ -0,0 +1,116 @@
---
name: file-headers
description: MANDATORY for every coding agent (Claude Code, Codex, or any other) on every change-set — every applicable source file the agent creates or updates MUST start with the project's copyright/authorship header (file overview + exact author line). Use automatically whenever writing a new file or editing an existing one; do not wait to be asked. Covers JS/TS/TSX/CJS/MJS, Python, shell, and CSS. Includes the audit script to verify repo-wide compliance.
---
# File Headers — Copyright Comment + File Overview
Every applicable source file in this repository starts with a header comment
containing a **file overview** and the **exact author line**:
```
@author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
```
The name and email must be exactly as above — no variations, no substitutions,
no other names. This applies to **every coding agent** working in this repo
(Claude Code, Codex, or any other tool): when you **create** a new applicable
file, write the header first; when you **update** an existing applicable file
that is missing the header, add it as part of the same change.
## Applicable files
| Included | Excluded |
| -------- | -------- |
| `*.js`, `*.ts`, `*.tsx`, `*.cjs`, `*.mjs` | anything under `node_modules/`, `dist/`, `build/`, `data/` |
| `*.py`, `*.sh` | vendored/minified files (`*.min.js`) |
| `*.css` | generated files (they carry their own AUTO-GENERATED banner) |
| | snapshots (`__snapshots__/`), lockfiles, JSON/YAML/Markdown |
## Header formats by file type
**JS / TS / TSX — server & scripts style** (overview inline in `@file`):
```js
/**
* @file One-to-few-sentence overview of what this file does and why it
* exists. Mention the key contracts or invariants the file owns.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
```
**JS / TS / TSX — client style** (`@file` name + `@description` overview), used
under `client/src/`:
```ts
/**
* @file ComponentName.tsx
* @description What the component/module renders or provides and how it fits
* into the app.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
```
**CSS** (same block-comment shape as `client/src/index.css`):
```css
/**
* @file file.css
* @description What these styles cover.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
```
**Shell** (`#` block right after the shebang; existing overview comments count —
just make sure the `@author` line is in the block):
```bash
#!/usr/bin/env bash
# script-name.sh — what the script does, one to few lines.
# @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
```
**Python** (inside the module docstring):
```python
"""
module.py — what the module does.
@author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
"""
```
## Rules
1. **New file → header first.** Any applicable file you create starts with the
header before any code (after the shebang for scripts).
2. **Touched file missing header → add it.** If you edit a file that lacks the
header, add one in the same commit. Write a real overview — describe what
the file actually does; never a placeholder like "TODO" or "utility file".
3. **Exact author line.** `@author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>`
byte-exact, in every file type (shell and Python use it inside `#` / docstring
comments).
4. **Don't churn existing headers.** If a file already has a compliant header,
leave it alone unless the file's purpose changed (then update the overview).
5. **Overviews must stay truthful.** When an edit changes what a file does,
update its `@file`/`@description` overview in the same change.
## Audit
Run the bundled checker to list any applicable file missing the header:
```bash
bash .claude/skills/file-headers/scripts/check-headers.sh
```
Exit code `0` = fully compliant; `1` = the printed files are missing headers.
Run it before finishing any change-set that adds files, and during reviews.
On every pull request, GitHub Actions runs
`.claude/skills/file-headers/scripts/check-headers-pr.sh` against only the
files changed in the PR diff (added, copied, renamed, or modified). Test locally
before pushing:
```bash
bash .claude/skills/file-headers/scripts/check-headers-pr.sh origin/master HEAD
```
+151
View File
@@ -0,0 +1,151 @@
#!/usr/bin/env bash
# check-headers-pr.sh — verify that applicable files touched in a git diff carry
# the mandatory copyright/authorship header. Used locally before opening a PR and
# by the file-headers GitHub Actions workflow on every pull request.
#
# Usage:
# check-headers-pr.sh [<base-sha> <head-sha>]
#
# When omitted, compares the current branch against origin/master (or master).
# @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)"
AUTHOR_MARK="@author Nguyễn Ngọc Trí Vĩ"
AUTHOR_EMAIL="vinnt@smartgift.vn"
usage() {
cat <<'EOF'
Usage: check-headers-pr.sh [<base-sha> <head-sha>]
Checks only added/copied/renamed/modified files in the diff between base and
head. Applicable extensions: .js .ts .tsx .cjs .mjs .py .sh .css
The author line must appear in the file header using the syntax for that type:
JS/TS/CSS — block comment (/** ... @author ... */)
Shell — # comment after the shebang
Python — module docstring (""" ... @author ... """)
EOF
}
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
usage
exit 0
fi
BASE_SHA="${1:-}"
HEAD_SHA="${2:-}"
cd "$ROOT"
if [[ -z "$BASE_SHA" || -z "$HEAD_SHA" ]]; then
if git show-ref --verify --quiet refs/remotes/origin/master; then
BASE_SHA="$(git merge-base HEAD origin/master)"
elif git show-ref --verify --quiet refs/heads/master; then
BASE_SHA="$(git merge-base HEAD master)"
else
echo "error: could not resolve base ref; pass <base-sha> <head-sha>" >&2
exit 1
fi
HEAD_SHA="HEAD"
fi
# Return 0 when the path is subject to the header policy (keep in sync with
# check-headers.sh exclusions).
is_applicable_file() {
local f="$1"
case "$f" in
*/node_modules/*|*/dist/*|*/build/*|*/.git/*|*/data/*)
return 1
;;
*/monitoring/.bin/*|*/monitoring/.data/*|*/__snapshots__/*)
return 1
;;
esac
case "$f" in
esac
case "$f" in
*.js|*.ts|*.tsx|*.cjs|*.mjs|*.py|*.sh|*.css)
return 0
;;
*)
return 1
;;
esac
}
# Best-effort hint for contributors when a file fails.
header_hint_for() {
local f="$1"
case "$f" in
*.py)
echo ' expected: module docstring with @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>'
;;
*.sh)
echo ' expected: # block after shebang with @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>'
;;
*.css)
echo ' expected: /** @file ... @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn> */'
;;
*)
echo ' expected: /** @file ... @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn> */'
;;
esac
}
# Require the exact author mark anywhere in the file (same rule as check-headers.sh).
has_author_header() {
local f="$1"
grep -q "$AUTHOR_MARK" "$f" && grep -q "$AUTHOR_EMAIL" "$f"
}
BASE_SHORT="$(git rev-parse --short "${BASE_SHA}" 2>/dev/null || echo "${BASE_SHA}")"
HEAD_SHORT="$(git rev-parse --short "${HEAD_SHA}" 2>/dev/null || echo "${HEAD_SHA}")"
checked=0
missing=0
skipped=0
echo "Checking authorship headers for files changed between ${BASE_SHORT}..${HEAD_SHORT}"
while IFS= read -r f; do
[[ -z "$f" ]] && continue
if ! is_applicable_file "$f"; then
skipped=$((skipped + 1))
continue
fi
if [[ ! -f "$f" ]]; then
echo "SKIP (missing on disk): $f"
skipped=$((skipped + 1))
continue
fi
checked=$((checked + 1))
if ! has_author_header "$f"; then
echo "MISSING HEADER: $f"
header_hint_for "$f"
missing=1
fi
done < <(git diff --name-only --diff-filter=ACMR "${BASE_SHA}" "${HEAD_SHA}")
if [[ "$checked" -eq 0 ]]; then
echo "✔ No applicable source files changed in this diff (skipped ${skipped} path(s))."
exit 0
fi
if [[ "$missing" -eq 0 ]]; then
echo "✔ All ${checked} applicable changed file(s) carry the authorship header."
exit 0
fi
echo
echo "Add the project header to each file listed above."
echo "See .claude/skills/file-headers/SKILL.md for per-type examples."
exit 1
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
# check-headers.sh — audit the repo for applicable source files missing the
# mandatory copyright/authorship header (see .claude/skills/file-headers).
# Prints each non-compliant file; exits 0 when fully compliant, 1 otherwise.
# @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)"
AUTHOR_MARK="@author Nguyễn Ngọc Trí Vĩ"
missing=0
while IFS= read -r f; do
if ! grep -q "$AUTHOR_MARK" "$f"; then
echo "MISSING HEADER: ${f#"$ROOT"/}"
missing=1
fi
done < <(
find "$ROOT" \
\( -name node_modules -o -name dist -o -name build -o -name .git \
-o -path "$ROOT/data" -o -path "$ROOT/monitoring/.bin" \
-o -path "$ROOT/monitoring/.data" -o -name "__snapshots__" \) -prune -o \
-type f \( -name "*.js" -o -name "*.ts" -o -name "*.tsx" -o -name "*.cjs" \
-o -name "*.mjs" -o -name "*.py" -o -name "*.sh" -o -name "*.css" \) \
! -name "*.min.js" -print
)
if [ "$missing" -eq 0 ]; then
echo "✔ All applicable files carry the authorship header."
fi
exit "$missing"
+27
View File
@@ -0,0 +1,27 @@
---
name: mcp-operations
description: Operate and maintain the local MCP server for this project. Use when creating MCP host config, troubleshooting tool connectivity, modifying tool domains, or adjusting safety policy flags.
---
# MCP Operations
Use this skill whenever work touches `mcp/` behavior or MCP host integration.
## Core workflow
- Confirm dashboard API is running (`/api/health`).
- Confirm MCP server build status.
- Validate env flags for mutation/destructive modes.
- Verify host configuration path and command.
## Safe operations policy
- Default to read-only mode (`MCP_DASHBOARD_ALLOW_MUTATIONS=false`).
- Enable mutations only for explicit admin tasks.
- Enable destructive mode only transiently and require explicit confirmation token.
## Required verification for code changes
- `npm run mcp:typecheck`
- `npm run mcp:build`
## References
- Host config examples: `references/host-config.md`
- Operations runbook: `references/runbook.md`
@@ -0,0 +1,18 @@
# MCP Host Config
## Command
- `node`
## Args
- Absolute path to `mcp/build/index.js`
## Example env
- `MCP_DASHBOARD_BASE_URL=http://127.0.0.1:4820`
- `MCP_DASHBOARD_ALLOW_MUTATIONS=false`
- `MCP_DASHBOARD_ALLOW_DESTRUCTIVE=false`
- `MCP_LOG_LEVEL=info`
## Common mistakes
- Relative path to MCP build entry.
- Dashboard not running while MCP starts.
- Mutating tools used while mutation flag is false.
@@ -0,0 +1,19 @@
# MCP Runbook
## Read-only daily mode
- Keep both mutation flags false.
- Use read tools for observability and reporting.
## Admin mode
- Set `MCP_DASHBOARD_ALLOW_MUTATIONS=true`.
- Run maintenance/pricing operations.
- Reset mutation flag to false after completion.
## Destructive mode
- Set both mutation and destructive flags true.
- Execute destructive command only with explicit confirmation token.
- Immediately disable destructive flag after operation.
## Verification commands
- `npm run mcp:typecheck`
- `npm run mcp:build`
+26
View File
@@ -0,0 +1,26 @@
---
name: repo-onboarding
description: Onboard quickly to this repository. Use when asked to understand architecture, locate ownership, choose the right module, or identify the correct commands and verification strategy before coding.
---
# Repo Onboarding
Use this workflow when a task begins with discovery.
## Steps
- Read `README.md` and `ARCHITECTURE.md` for system-level context.
- Identify target layer:
- `server/` for API, hooks, DB, websocket
- `client/` for UI and routing
- `mcp/` for local MCP tools and policy gates
- Select the smallest set of files required to answer the task.
- Confirm verification commands before implementation.
## Verification defaults
- Backend: `npm run test:server`
- Frontend: `npm run test:client`
- MCP: `npm run mcp:typecheck` and `npm run mcp:build`
## References
- Module map: `references/module-map.md`
- Command map: `references/command-map.md`
@@ -0,0 +1,25 @@
# Command Map
## Root commands
- `npm run setup`
- `npm run dev`
- `npm run build`
- `npm start`
- `npm run test:server`
- `npm run test:client`
- `npm run install-hooks`
- `npm run import-history`
- `npm run clear-data`
## MCP helper commands (root)
- `npm run mcp:install`
- `npm run mcp:build`
- `npm run mcp:start`
- `npm run mcp:dev`
- `npm run mcp:typecheck`
## Direct MCP package commands
- `npm --prefix mcp install`
- `npm --prefix mcp run build`
- `npm --prefix mcp run start`
- `npm --prefix mcp run typecheck`
@@ -0,0 +1,20 @@
# Module Map
## Backend
- `server/index.js`: app composition, startup behavior, periodic maintenance.
- `server/db.js`: schema and prepared statement ownership.
- `server/routes/*.js`: endpoint contracts by domain.
- `server/websocket.js`: WS lifecycle and broadcast behavior.
## Frontend
- `client/src/pages/`: route-level screens.
- `client/src/components/`: reusable UI building blocks.
- `client/src/lib/api.ts`: client API access patterns.
- `client/src/hooks/useWebSocket.ts`: live update pipeline.
## MCP
- `mcp/src/index.ts`: runtime entrypoint.
- `mcp/src/server.ts`: MCP assembly.
- `mcp/src/tools/domains/`: domain tool registration.
- `mcp/src/clients/dashboard-api-client.ts`: resilient API bridge.
- `mcp/src/policy/tool-guards.ts`: mutation/destructive gates.
+28
View File
@@ -0,0 +1,28 @@
---
name: ship-feature
description: Implement a feature safely end-to-end in this repository. Use when adding or changing functionality across backend, frontend, or MCP with required verification and documentation updates.
---
# Ship Feature
Use this workflow for medium or large implementation tasks.
## Steps
- Explore impacted modules first.
- Write a short implementation plan before editing.
- Implement smallest coherent diff that satisfies requirements.
- Run relevant verification commands.
- Update docs when commands, paths, architecture, or behavior changed.
## Required quality checks
- Keep API and websocket contracts stable unless intentionally changed.
- Keep destructive operations behind explicit guardrails.
- Avoid broad refactors in feature tickets unless requested.
## Finish checklist
- Tests/build/typecheck completed or explicitly reported as not run.
- Changed file set is scoped and intentional.
- User-facing docs updated if behavior changed.
## References
- Checklist template: `references/feature-checklist.md`
@@ -0,0 +1,22 @@
# Feature Checklist
- Scope
- Problem and success criteria are explicit.
- Impacted layers identified (server/client/mcp/docs/scripts).
- Implementation
- Input validation and error handling are explicit.
- Existing behavior preserved where not in scope.
- Safety controls preserved.
- Verification
- Backend: `npm run test:server` when backend changed.
- Frontend: `npm run test:client` when UI changed.
- MCP: `npm run mcp:typecheck` + `npm run mcp:build` when MCP changed.
- Documentation
- `README.md`, `ARCHITECTURE.md`, `SETUP.md`, `INSTALL.md`, `mcp/README.md` updated as needed.
- Commands in docs match `package.json`.
- Delivery
- Known risks and unrun checks are clearly stated.
@@ -0,0 +1,63 @@
---
name: update-project-docs
description: MANDATORY for every coding agent (Claude Code, Codex, or any other) — keep this repository's documentation in sync after any change to behavior, configuration, interfaces, events, schema, or features. Use automatically (without being asked) at the end of ANY change-set that adds or alters an env var, event type, hook behavior, session/agent state transition, API route or response shape, DB schema, WebSocket message, MCP tool, CLI command, or user-facing feature — and whenever the user asks to "update the docs / README / architecture". Knows the full doc surface (README, ARCHITECTURE, server/client READMEs, docs/*) and which docs each kind of change touches.
---
# Update Project Docs
This repository keeps a large doc set and docs drift silently, because one change often belongs in several files at once. This skill encodes **which docs exist, which change-types touch which docs, and how to propagate consistently**. This build ships English only — the translated READMEs, the wiki and the root landing page were removed; do not recreate them.
Authoritative inventory with exact section anchors lives in [`references/doc-map.md`](references/doc-map.md) — read it when deciding where a specific change lands. The repo rule [`.claude/rules/docs-markdown.md`](../../rules/docs-markdown.md) ("update all affected docs together") is binding.
## When to update (including without being asked)
Update docs **in the same change-set (PR/commit) as the code**, before claiming done — do not wait for the user to ask — whenever the change is observable from outside the module:
- **New/changed env var** → every env-var table + `.env.example`.
- **New event type** (e.g. an `events.event_type` value) → every event-type list/table.
- **New/changed hook behavior or session/agent state transition** → hook docs + every state-machine diagram.
- **New/changed API route or response shape** → API docs + route tables + OpenAPI.
- **DB schema change** (table/column/index) → database docs + ERD.
- **New WebSocket message type** → client/server WS docs.
- **New MCP tool** → MCP docs.
- **New CLI command / script / renamed file referenced in docs** → command lists + onboarding guides.
- **New user-facing feature / page / background service** → feature tables + architecture.
**Do NOT** auto-update for: pure internal refactors with no observable/interface/config change, test-only changes, comment/typo fixes, or work the user explicitly scoped as "no docs". When unsure whether a change is observable, check the mapping below; if it touches any row, update.
## Change → docs mapping
| Change type | Docs to update |
|---|---|
| **Env var** | `README.md` (env table), `ARCHITECTURE.md` (inline), `server/README.md`, `.env.example` |
| **Event type** | `README.md`+VN+CN (hook-event table), `ARCHITECTURE.md` (Event types line), `docs/PLUGINS.md`, + i18n, `docs/DATABASE.md` (if it enumerates types) |
| **Hook behavior / state transition** | `docs/HOOKS.md`, state-machine **mermaid** diagrams in `README.md`+VN+CN + `server/README.md` + `docs/DATABASE.md` + , `ARCHITECTURE.md` (hooks.js row) |
| **API route / response** | `docs/API.md`, `server/README.md` (routes), `ARCHITECTURE.md` (routes row), `server/openapi*.js` (code) |
| **DB schema** | `docs/DATABASE.md`, `ARCHITECTURE.md` (ERD/schema) |
| **WebSocket message** | `client/README.md` (Event Types), `server/README.md`, |
| **MCP tool** | `mcp/README.md`, `docs/MCP.md` |
| **Feature / page / background service** | `README.md` (feature table + data-flow list), `ARCHITECTURE.md` (module table), `server/README.md` or `client/README.md` |
| **CLI command / script** | `README.md` commands, `CLAUDE.md` / `AGENTS.md`, `INSTALL.md` / `SETUP.md` |
| **New language** | `docs/I18N.md`, `client/src/i18n/locales/<xx>/*`, `client/src/i18n/index.ts` (add to `supportedLngs` AND the `resources` map) |
## Procedure
1. **Classify** the change against the table above. A change can hit multiple rows (a new feature with a new env var hits both).
2. **Write the canonical English version first** — usually `README.md` and/or `ARCHITECTURE.md`. Get the wording right there; it anchors everything else.
6. **Area READMEs / docs/**: update `server/README.md`, `client/README.md`, and the relevant `docs/*.md` per the mapping.
7. **Diagrams**: when a state transition changes, edit every mermaid `stateDiagram-v2` block that models it (they are duplicated across README, server/README and docs/DATABASE). Keep transition labels consistent.
## Verify (do not skip)
- **Coverage**: run `scripts/doc-coverage.sh <new-term> [...]` (e.g. the new env var / event type / identifier) and confirm every doc the mapping flags shows a HIT. The matrix is advisory — not every term belongs in every file — but a flagged doc reading `0` is a miss to fix.
- **Tables**: markdown tables stay pipe-balanced (header column count == every row).
- **Mermaid**: each edited block still parses (valid `source --> target: label`).
- **i18n**: every new English string has a `vi` entry in `client/src/i18n/locales/vi/`.
- **Format/tests**: run `npm run format` (or `prettier --check` on touched files); for any code touched, run the verification from `CLAUDE.md` (`npm run test:server` / `test:client` / `mcp:typecheck`).
- State exactly which docs were updated and which were intentionally skipped (with reason), mirroring the repo's verification policy.
## Tips
- The fastest way to find where something already lives: `grep -n "<existing-neighbor-term>" <doc>` (e.g. grep an adjacent env var to find the env table). `references/doc-map.md` lists the stable anchors per file.
- Parallelize translations + HTML across subagents when the change is large, but write the canonical English edit yourself first so the translations have a faithful source.
- One language/area per subagent keeps edits reviewable and tables un-corrupted.
@@ -0,0 +1,65 @@
# Documentation Map
Authoritative inventory of this repository's documentation surface: every doc that must be kept in sync, what each contains, and the stable anchors to grep for when placing an edit. Section line numbers drift — grep the anchor strings, don't trust line numbers.
## Tier 1 — primary, always consider
### `README.md` (English, canonical)
The source of truth most other docs mirror. Key sections:
- **Feature table** — rows like `**Kanban Board**`, `**Transcript Cache**`, `**Pre-Existing Session Detection**`, `**Continuous Project Sync**`. Grep a neighboring row label.
- **Data-flow numbered list** — bullets describing hook ingestion, the watchdog, periodic sweep, continuous sync. Grep `Error detection watchdog` / `periodic server sweep`.
- **Agent State Machine** + **Session State Machine** — two `mermaid stateDiagram-v2` blocks. Grep `stateDiagram-v2`.
- **Hook Events table** — `| Hook Type | Trigger | Dashboard Action |`. Lists `SessionStart``SessionEnd`, plus synthetic `Compaction`, `APIError`, `TurnDuration`, `ToolError`, `Interrupted`. Grep `## Hook Events`.
- **Configuration / Environment Variables table** — `| Environment Variable | Default | Description |`. Grep `DASHBOARD_PORT` or `DASHBOARD_HOST`.
### Translations
This build ships English only. `README-VN.md`, `README-CN.md` and `README-KO.md` were removed, as were the `zh` and `ko` UI locales — do not recreate them.
Standalone full translations of `README.md`. **Every** README change must be mirrored here at the corresponding section. Conventions:
- Keep in English/code: identifiers, env-var names, event-type names, `awaiting_input_since`, `pendingInterrupt`, "watchdog", `fs.watch`, model IDs, mermaid transition labels.
- Translate prose. "Waiting" → **Đang chờ** (vi) / **等待中** (zh) / **대기 중** (ko). "watchdog" often kept; in zh sometimes 看门狗.
### `ARCHITECTURE.md`
- **Module responsibility table** — one row per source file (`scripts/import-history.js`, `lib/transcript-cache.js`, `routes/hooks.js`, `server/index.js`, …). Update the row whose file you changed. Grep the file path.
- **Data-flow + sequence diagrams**, **state machines**, **Continuous background sync** prose block (grep `Continuous background sync`).
- **Event types line** — grep `| Event types |`.
- **ERD / schema** mermaid + `event_type "PreToolUse|PostToolUse|Stop|etc"`.
### `server/README.md`
Backend reference: routes table, **Error Detection Watchdog** / **User-Interrupt (Esc) Recovery** / **Continuous Project Sync** sections, Agent/Session lifecycle mermaid diagrams, Environment Variables bash block under `## Deployment`. Update for any backend behavior, route, state, env var, or background service.
### `client/README.md`
Frontend reference: component list, **Event Types** table (WebSocket broadcast message types like `session_created`, `agent_updated`), session/agent status TypeScript unions. Update for new WS message types or client-facing behavior. NOT needed for server-only changes the UI already renders generically.
### `docs/HOOKS.md`
Per-hook deep reference (`### 1. SessionStart``### 8. SessionEnd`), the `awaiting_input_since` overlay rules, the "User interrupts (Esc) — no hook fires" section, transcript-derived sync. Update for any hook semantics or state behavior.
### `docs/DATABASE.md`
Schema reference: `sessions` / `agents` / `events` tables, column docs, status CHECK constraints, lifecycle mermaid diagrams. Update for schema or state-machine changes.
### `docs/API.md`
REST API reference (endpoints, params, example responses). Update for route/response changes. Pair with `server/openapi*.js` (code, not docs).
### `docs/PLUGINS.md`
Plugin/marketplace docs incl. an **Event Types** enumeration line — keep it in sync with the canonical event-type list.
### `docs/MCP.md` + `mcp/README.md`
MCP server + tool reference. Update for new/changed MCP tools.
### `docs/I18N.md`
i18n architecture: **Supported languages** list, `supportedLngs`, the 15 namespaces. Update when adding a language or namespace. Client UI strings live in `client/src/i18n/locales/{en,zh,vi}/*.json` (code).
## Tier 3 — situational
- `.env.example` — every env var belongs here with a sane default + comment.
- `INSTALL.md`, `SETUP.md`, `DEPLOYMENT.md`, `docs/DEPLOYMENT.md` — install/run/deploy commands.
- `CLAUDE.md`, `AGENTS.md` — agent working guides; update when commands, file locations, or workflows change.
- `docs/README.md` — docs index; add a link when a new `docs/*.md` is created.
- `desktop/README.md`, `vscode-extension/README.md`, `statusline/README.md` — surface-specific; update only when that surface changes.
## Consistency invariants
- The **event-type set** must match across: `README` hook table, `ARCHITECTURE` Event types line, `docs/PLUGINS.md`. When adding one, grep the existing set (e.g. `TurnDuration`) across all and add everywhere it appears.
- **Env-var set** must match across: README tables, `server/README.md`, `.env.example`, and any inline `ARCHITECTURE` mention.
- **State-machine diagrams** are duplicated across README, `server/README.md` and `docs/DATABASE.md`. A transition change touches all of them.
- Run `scripts/doc-coverage.sh <term>` to confirm a new identifier/var/event reached every doc that should mention it.
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env bash
# doc-coverage.sh — verify that one or more terms (a new env var, event type,
# route, identifier, feature name, …) are documented across this repo's
# canonical doc surface. Prints a HIT/miss matrix so a docs update can be
# checked for "full coverage" before finishing.
#
# Usage:
# .claude/skills/update-project-docs/scripts/doc-coverage.sh DASHBOARD_SESSION_SYNC_MS
# .claude/skills/update-project-docs/scripts/doc-coverage.sh Interrupted pendingInterrupt
#
# Run from the repo root. Exit code is non-zero if any term is missing from a
# doc that the change-type mapping (see references/doc-map.md) says it belongs
# in — but treat the matrix as advisory: not every term belongs in every file.
# @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
set -u
# The canonical doc set kept in sync. Translations + HTML + per-area READMEs.
DOCS=(
"README.md"
"ARCHITECTURE.md"
"server/README.md"
"client/README.md"
"docs/HOOKS.md"
"docs/DATABASE.md"
"docs/API.md"
"docs/PLUGINS.md"
"docs/MCP.md"
"mcp/README.md"
"docs/I18N.md"
".env.example"
)
if [ "$#" -eq 0 ]; then
echo "usage: $0 <term> [term2 ...]" >&2
exit 2
fi
missing_any=0
for term in "$@"; do
echo "── coverage for: $term ──────────────────────────────"
for doc in "${DOCS[@]}"; do
if [ ! -f "$doc" ]; then
printf " %-26s (absent)\n" "$doc"
continue
fi
n=$(grep -Fc -- "$term" "$doc" 2>/dev/null || true)
n=${n:-0}
if [ "$n" -gt 0 ]; then
printf " ✅ %-26s %s\n" "$doc" "$n"
else
printf " · %-26s 0\n" "$doc"
fi
done
echo
done
exit $missing_any
+29
View File
@@ -0,0 +1,29 @@
# Codex Agent Setup
This directory contains all project-scoped Codex extensions:
- instruction baseline via root [`AGENTS.md`](../AGENTS.md)
- execution policy rules in [`rules/default.rules`](./rules/default.rules)
- custom subagent definitions in [`agents/`](./agents)
- reusable skills in [`skills/`](./skills)
- runtime configuration in [`config.toml`](./config.toml)
## What Codex reads
- `AGENTS.md` from repository root
- `.codex/config.toml` for runtime settings
- `.codex/agents/*.toml` for custom agents
- `.codex/skills/*/SKILL.md` for project skills
- `.codex/rules/*.rules` for execution policy
## Included custom agents
- `reviewer`: read-only, high-rigor review agent
- `implementer`: workspace-write implementation agent
- `release_auditor`: read-only release readiness checker
## Included skills
- `repo-onboarding` — architecture discovery and verification selection
- `mcp-maintainer` — MCP server operations and troubleshooting
- `release-guard` — release readiness checks
+12
View File
@@ -0,0 +1,12 @@
name = "implementer"
description = "Execution-focused agent for contained feature and bug-fix implementation."
model = "gpt-5.3-codex-spark"
model_reasoning_effort = "medium"
sandbox_mode = "workspace-write"
developer_instructions = """
Implement requested changes with minimal scope and strong validation.
Preserve existing contracts unless change is explicitly requested.
Run targeted verification for modified areas and report what was run.
Avoid unrelated refactors.
"""
nickname_candidates = ["Nova", "Forge", "Kite"]
+12
View File
@@ -0,0 +1,12 @@
name = "release_auditor"
description = "Read-only release gate checker for docs, scripts, tests, and risk reporting."
model = "gpt-5.4-mini"
model_reasoning_effort = "medium"
sandbox_mode = "read-only"
developer_instructions = """
Audit release readiness for this repository.
Check command consistency between docs and package scripts.
Look for missing verification, stale architecture notes, and risky behavior changes.
Produce a concise pass/fail summary with exact file references.
"""
nickname_candidates = ["Lumen", "Harbor", "Beacon"]
+12
View File
@@ -0,0 +1,12 @@
name = "reviewer"
description = "Read-only reviewer focused on correctness, regressions, security, and missing tests."
model = "gpt-5.4"
model_reasoning_effort = "high"
sandbox_mode = "read-only"
developer_instructions = """
Review like an owner.
Prioritize behavior regressions, correctness, security risks, and missing tests.
Lead with concrete findings and file references.
Avoid style-only suggestions unless they hide a functional risk.
"""
nickname_candidates = ["Atlas", "Delta", "Echo"]
+7
View File
@@ -0,0 +1,7 @@
project_doc_fallback_filenames = ["TEAM_GUIDE.md", ".agents.md"]
project_doc_max_bytes = 65536
[agents]
max_threads = 6
max_depth = 1
job_max_runtime_seconds = 1800
+71
View File
@@ -0,0 +1,71 @@
# Default execution policy rules for this repository.
# Safe, routine read-only git inspection can run with prompt.
prefix_rule(
pattern = ["git", ["status", "diff", "log", "show"]],
decision = "prompt",
justification = "Git inspection is allowed with approval.",
match = [
"git status",
"git diff",
"git log --oneline -20",
"git show HEAD~1",
],
not_match = [
"git checkout -b feature/new-branch",
],
)
# Destructive reset-style operations are blocked.
prefix_rule(
pattern = ["git", "reset", "--hard"],
decision = "forbidden",
justification = "Hard reset is blocked to prevent data loss. Use explicit file edits or safe restore strategies.",
match = [
"git reset --hard",
"git reset --hard HEAD~1",
],
not_match = [
"git reset --soft HEAD~1",
],
)
# Installing dependencies should always require approval.
prefix_rule(
pattern = ["npm", "install"],
decision = "prompt",
justification = "Dependency installation changes lockfiles and runtime behavior; require explicit approval.",
match = [
"npm install",
"npm install some-package",
],
not_match = [
"npm run build",
],
)
# Potentially destructive filesystem deletes are blocked.
prefix_rule(
pattern = ["rm", "-rf"],
decision = "forbidden",
justification = "Recursive force deletion is blocked. Use targeted edits or safer deletion commands.",
match = [
"rm -rf /tmp/test-folder",
],
not_match = [
"rm -r ./tmp",
],
)
# Network fetch commands should be reviewed each time.
prefix_rule(
pattern = ["curl"],
decision = "prompt",
justification = "Network access should be explicitly reviewed per command.",
match = [
"curl https://example.com",
],
not_match = [
"cat README.md",
],
)
+21
View File
@@ -0,0 +1,21 @@
---
name: mcp-maintainer
description: Operate and maintain the local MCP server for this repository. Use for MCP tool updates, policy-guard changes, host configuration, and MCP runtime troubleshooting.
---
# MCP Maintainer Skill
## Workflow
- Confirm dashboard API availability (`/api/health`).
- Inspect affected MCP domain modules under `mcp/src/tools/domains/`.
- Preserve safety gates in `mcp/src/policy/tool-guards.ts`.
- Validate with `npm run mcp:typecheck` and `npm run mcp:build`.
## Safety rules
- Keep loopback-only target checks enabled.
- Keep mutating and destructive tools behind explicit flags.
- Do not log protocol data to stdout.
## References
- `references/tool-domain-map.md`
- `references/operations-runbook.md`
@@ -0,0 +1,4 @@
interface:
display_name: "MCP Maintainer"
short_description: "Maintain MCP tools, policy gates, and host integration."
default_prompt: "Use mcp-maintainer to update MCP tooling safely and verify runtime integrity."
@@ -0,0 +1,14 @@
# MCP Operations Runbook
## Modes
- Read-only:
- `MCP_DASHBOARD_ALLOW_MUTATIONS=false`
- `MCP_DASHBOARD_ALLOW_DESTRUCTIVE=false`
- Admin:
- Set mutations true for controlled maintenance operations.
- Destructive:
- Set both true and require `confirmation_token = CLEAR_ALL_DATA`.
## Verification
- `npm run mcp:typecheck`
- `npm run mcp:build`
@@ -0,0 +1,8 @@
# MCP Tool Domain Map
- `observability-tools.ts`: health, stats, analytics, snapshots, export.
- `session-tools.ts`: list/get/create/update sessions.
- `agent-tools.ts`: list/get/create/update agents.
- `event-tools.ts`: event listing and hook ingestion.
- `pricing-tools.ts`: pricing CRUD and cost calculations.
- `maintenance-tools.ts`: cleanup, reimport, reinstall hooks, destructive clear.
+21
View File
@@ -0,0 +1,21 @@
---
name: release-guard
description: Run release-readiness checks for this repository. Use when validating docs, scripts, verification coverage, and operational safety before merge or release.
---
# Release Guard Skill
## Workflow
- Check command consistency across docs and `package.json`.
- Verify architecture docs align with current code paths.
- Validate that safety controls are still documented and enforced.
- Report pass/fail with concrete file references.
## Focus areas
- Hook flow and failure behavior.
- Session/agent lifecycle semantics.
- MCP safety gates and host setup instructions.
- Troubleshooting accuracy.
## References
- `references/release-checklist.md`
@@ -0,0 +1,4 @@
interface:
display_name: "Release Guard"
short_description: "Audit release readiness across code, docs, and safety controls."
default_prompt: "Use release-guard to audit this branch for release readiness and report concrete findings."
@@ -0,0 +1,7 @@
# Release Checklist
- Commands in docs exist in root `package.json`.
- Validation steps are documented for backend, frontend, and MCP.
- Behavior-changing diffs mention migration/compatibility impacts.
- Safety-sensitive operations remain guarded by explicit flags.
- Troubleshooting sections reflect the current architecture.
+21
View File
@@ -0,0 +1,21 @@
---
name: repo-onboarding
description: Understand this repository quickly before making changes. Use for architecture discovery, ownership mapping, command selection, and initial implementation planning.
---
# Repo Onboarding Skill
## Workflow
- Read `AGENTS.md`, `README.md`, and `ARCHITECTURE.md`.
- Determine target layer: `server/`, `client/`, `mcp/`, or docs.
- Identify the minimal file set needed for the task.
- Select verification commands before editing.
## Verification defaults
- Backend: `npm run test:server`
- Frontend: `npm run test:client`
- MCP: `npm run mcp:typecheck` and `npm run mcp:build`
## References
- `references/module-map.md`
- `references/verification-map.md`
@@ -0,0 +1,4 @@
interface:
display_name: "Repo Onboarding"
short_description: "Map architecture, ownership, and verification strategy before coding."
default_prompt: "Use repo-onboarding to analyze scope, affected modules, and validation commands for this task."
@@ -0,0 +1,9 @@
# Module Map
- `server/index.js`: app startup and route mounting.
- `server/routes/*.js`: API contracts and route behavior.
- `server/db.js`: schema and statement layer.
- `server/websocket.js`: live update broadcast path.
- `client/src/pages/`: route-level UI.
- `client/src/components/`: reusable UI primitives.
- `mcp/src/tools/domains/`: MCP tool families.
@@ -0,0 +1,11 @@
# Verification Map
- Backend changes:
- `npm run test:server`
- Frontend changes:
- `npm run test:client`
- MCP changes:
- `npm run mcp:typecheck`
- `npm run mcp:build`
- Docs-only changes:
- validate command consistency against root `package.json`
+23
View File
@@ -0,0 +1,23 @@
# Optional dev-container image for Claude Code Agent Monitor.
# Node 22 to match the production Dockerfile and the project's engines.
# Adds the native-addon toolchain (better-sqlite3 builds via node-gyp) plus
# Python 3 (statusline.py and helper scripts) and the sqlite3 CLI.
#
# This image is ONLY used by Dev Containers / Codespaces. Host-based development
# (npm run dev / npm start) is unaffected.
#
# Author: Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
FROM mcr.microsoft.com/devcontainers/javascript-node:22
# node-gyp needs python3 + a C/C++ toolchain to compile better-sqlite3.
# python-is-python3 makes `python` resolve to python3 for node-gyp.
# sqlite3 is handy for inspecting the dashboard DB during development.
RUN export DEBIAN_FRONTEND=noninteractive \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
build-essential \
python3 \
python-is-python3 \
sqlite3 \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
+70
View File
@@ -0,0 +1,70 @@
# Dev Container (optional)
A ready-to-use, **opt-in** development environment for Claude Code Agent Monitor.
It is used **only** when you explicitly choose it — it changes nothing about
host-based development (`npm run dev` / `npm start` still work exactly as before).
## When to use it
Use it if you want a consistent, batteries-included toolchain without installing
Node, build tools, or Python on your machine — or if you're on a GitHub Codespace.
## How to open it
- **VS Code:** install the *Dev Containers* extension, then run
**"Dev Containers: Reopen in Container"** (Command Palette).
- **GitHub Codespaces:** *Code → Create codespace on this branch*.
The first build runs `.devcontainer/post-create.sh`, which installs all workspace
dependencies (`npm run setup`) and builds the MCP server (`npm run mcp:install`,
`npm run mcp:build`).
## What's inside
| Component | Detail |
| ---------------- | ------------------------------------------------------------------- |
| Base image | `mcr.microsoft.com/devcontainers/javascript-node:22` (matches prod) |
| Native toolchain | `build-essential` + `python3` so `better-sqlite3` compiles |
| Python | `python3` / `python` for `statusline.py` and helper scripts |
| sqlite3 CLI | inspect the dashboard DB during development |
| Features | GitHub CLI, Docker-in-Docker (build/run the project's own Dockerfile) |
| Forwarded ports | `4820` (server API + WebSocket), `5173` (Vite client) |
| Editor | ESLint + Prettier (format on save), Vitest, Docker, YAML, Tailwind |
## Everyday commands
```bash
npm run dev # server on :4820 + Vite client on :5173
npm start # production-style server (serves client/dist)
npm run test:server # node --test
npm run test:client # vitest
npm run test:mcp # MCP server tests
npm run openapi:yaml # regenerate openapi.yaml from the live spec
```
## Claude Code hooks are HOST-side (important — issue #193)
Claude Code runs on your **host**, so its hooks must point at a handler path that
exists on the host. This container therefore:
- does **not** bind-mount `~/.claude`, and
- does **not** install hooks — `scripts/install-hooks.js` **refuses to run inside
a container** (it would write a container-internal handler path into your host
settings and break every host hook with `MODULE_NOT_FOUND`).
Install hooks **on your host** instead:
```bash
npm run install-hooks # on the HOST
```
The host hook handler POSTs to `http://localhost:4820`, which this container
forwards — so a host-installed hook reaches the containerized dashboard.
> Escape hatch: if you genuinely run Claude Code *inside* this same container,
> set `CCAM_ALLOW_CONTAINER_HOOKS=1` before `npm run install-hooks`.
## Not supported in the container
Electron desktop builds (`npm run desktop:*`) need a host with a display and are
host-only.
+81
View File
@@ -0,0 +1,81 @@
{
// ─────────────────────────────────────────────────────────────────────────
// Optional, opt-in dev environment for Claude Code Agent Monitor.
// Used only when you choose "Dev Containers: Reopen in Container" (VS Code) or
// open the repo in a GitHub Codespace. It changes nothing for host-based dev.
//
// Covers the full project: the Express server (4820), the React/Vite client
// (5173), the MCP server, and the VS Code extension. The native `better-sqlite3`
// addon builds here (build-essential + python3 are installed in the Dockerfile).
//
// NOTE (issue #193): Claude Code hooks are a HOST-side concern. This container
// intentionally does NOT bind-mount ~/.claude and does NOT install hooks — the
// installer refuses to run inside a container. Run `npm run install-hooks` on
// your host so hooks POST to http://localhost:4820 (forwarded from here).
// ─────────────────────────────────────────────────────────────────────────
"name": "Claude Code Agent Monitor",
"build": {
"dockerfile": "Dockerfile",
"context": ".."
},
"features": {
"ghcr.io/devcontainers/features/github-cli:1": {},
// Lets you build/run the project's own production Dockerfile + docker-compose
// from inside the dev container (e.g. to reproduce issue #193 deliberately).
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
},
// Server (API + WebSocket) and the Vite client dev server. VS Code forwards
// these from the container's localhost, so the secure loopback bind is fine.
"forwardPorts": [4820, 5173],
"portsAttributes": {
"4820": {
"label": "Dashboard server (API + WebSocket)",
"onAutoForward": "notify"
},
"5173": {
"label": "Vite dev client",
"onAutoForward": "openBrowser"
}
},
// Install root + client + vscode-extension deps and build the MCP server.
// Never installs Claude Code hooks (host-only — see note above).
"postCreateCommand": "bash .devcontainer/post-create.sh",
"waitFor": "postCreateCommand",
"remoteUser": "node",
// Dev defaults. NODE_ENV=development so `npm run dev` runs API-only with the
// Vite client on 5173 (production mode would serve the prebuilt client/dist).
"remoteEnv": {
"NODE_ENV": "development"
},
"customizations": {
"vscode": {
"extensions": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"vitest.explorer",
"ms-azuretools.vscode-docker",
"redhat.vscode-yaml",
"yzhang.markdown-all-in-one",
"ms-python.python",
"bradlc.vscode-tailwindcss",
"GitHub.vscode-pull-request-github"
],
"settings": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"],
"files.eol": "\n",
"terminal.integrated.defaultProfile.linux": "bash"
}
}
}
}
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# Dev-container bootstrap. Installs all workspace dependencies and builds the
# MCP server. Runs once, after the container is created.
#
# Deliberately does NOT install Claude Code hooks: hooks are a host-side concern
# (issue #193) and `scripts/install-hooks.js` refuses to run inside a container.
# @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
set -euo pipefail
echo "▶ Installing server + client + vscode-extension dependencies (npm run setup)…"
npm run setup
echo "▶ Installing and building the MCP server…"
npm run mcp:install
npm run mcp:build
cat <<'EOF'
✅ Dev environment ready.
Develop:
npm run dev # server on :4820 + Vite client on :5173
npm start # production-style server (serves client/dist)
Test:
npm run test:server # node --test
npm run test:client # vitest
npm run test:mcp # MCP server tests
npm run mcp:typecheck # MCP type check
Docs:
npm run openapi:yaml # regenerate openapi.yaml from the live spec
⚠ Claude Code hooks are HOST-side. Do NOT run `npm run install-hooks` in this
container — it is refused on purpose (issue #193). Run it on your HOST so the
hook handler path exists there and POSTs to http://localhost:4820 (forwarded
from this container).
Electron desktop builds (npm run desktop:*) also need a host with a display
and are not supported inside this container.
EOF
+15
View File
@@ -0,0 +1,15 @@
node_modules/
client/node_modules/
client/dist/
data/
.git/
.github/
*.md
!README.md
images/
wiki/
index.html
og-image.svg
favicon.svg
.prettierrc
.prettierignore
+111
View File
@@ -0,0 +1,111 @@
# Claude Code home directory (default: ~/.claude)
# Override this if your Claude Code data is in a different location, e.g.:
# CLAUDE_HOME=~/.codefuse/engine/cc
# CLAUDE_HOME=~/.claude
# ── Server / network ────────────────────────────────────────────────────────
# Port to listen on (default: 4820)
# DASHBOARD_PORT=4820
# Interface to bind. SECURITY: defaults to 127.0.0.1 (loopback) so the dashboard
# is NOT reachable from the network out of the box (GHSA-gr74-4xfh-6jw9). The
# server reads transcripts, exports all data, and can spawn `claude`, so binding
# it to the network without auth exposes all of that. Only widen the bind if you
# understand the risk — and set DASHBOARD_TOKEN when you do.
# DASHBOARD_HOST=0.0.0.0 # bind all interfaces (LAN-reachable)
# Optional auth token. When set, every /api/* request and the WebSocket must
# present it (Authorization: Bearer <token>, x-dashboard-token header, or
# ?token=). Strongly recommended whenever DASHBOARD_HOST is non-loopback. Unset
# by default — the loopback bind is the trust boundary.
# DASHBOARD_TOKEN=change-me-to-a-long-random-string
# Extra Host-header names allowed besides loopback (comma-separated). Needed when
# you bind to a LAN and reach the dashboard by hostname/IP — entries here pass
# the anti-DNS-rebinding Host allowlist. Also required when Prometheus-in-Docker
# scrapes a host-native dashboard:
# DASHBOARD_ALLOWED_HOSTS=host.docker.internal
# DASHBOARD_ALLOWED_HOSTS=dashboard.internal,192.168.1.50
# ── Background sweeps / sync ─────────────────────────────────────────────────
# Minutes of inactivity before an active session is marked "abandoned" by the
# periodic maintenance sweep (default: 180 = 3h).
# DASHBOARD_STALE_MINUTES=180
# Idle-working timeout (seconds) for recovering a turn cancelled with Esc BEFORE
# any output (which leaves no transcript marker). When the main agent has been
# "working" with no tool in flight and neither a hook nor the transcript has
# advanced for this long, the watchdog moves the session to Waiting (default: 120).
# DASHBOARD_WORKING_IDLE_SECONDS=120
# Dead-session liveness reap. The watchdog lists running `claude` CLI processes
# (ps + lsof on macOS, /proc on Linux) and completes any active session whose
# cwd has no live claude process — recovering sessions whose SessionEnd hook
# was lost because the dashboard was down when the user quit (e.g. Ctrl+C).
# Set to 0 to disable (do this when hooks arrive from another machine); it is
# auto-disabled on Windows and inside containers (default: enabled).
# DASHBOARD_LIVENESS_PROBE=1
# Idle gate (seconds) for WATCHDOG-TICK liveness reaps: a session is only
# completed when its transcript hasn't been written for at least this long
# (the last hook write is the fallback clock when no transcript exists on
# disk), so mid-turn or just-resumed sessions never flicker out on a transient
# probe miss. The startup passes skip this gate entirely — at boot the probe
# alone decides, so sessions quit moments before launch clear immediately
# (default: 60).
# DASHBOARD_LIVENESS_IDLE_SECONDS=60
# Poll interval (ms) for the continuous ~/.claude/projects sync that surfaces
# projects added after startup whose sessions never flow through hooks. The
# fs.watch watcher fires near-instantly regardless; this poll is the safety net.
# Set to 0 to disable the poll while leaving the watcher running (default: 30000).
# DASHBOARD_SESSION_SYNC_MS=30000
# ── Remote Data Sources (SSH multi-machine collection) ───────────────────────
# The dashboard can pull Claude Code history from other machines over SSH:
# it rsyncs each enabled remote's ~/.claude/projects into a sandboxed staging
# dir, feeds it through the same importer used for local history, and tags the
# imported sessions with the source. Authentication defers entirely to the host's
# own SSH stack (~/.ssh/config, ssh-agent, keys, known_hosts) — no passwords or
# secrets are stored here.
# Poll interval (ms) for the background poller that syncs each enabled remote
# source. Set to 0 to disable the poller (manual / on-demand syncs still work)
# (default: 15000 = 15s).
# DASHBOARD_REMOTE_SYNC_MS=15000
# Per-source timeout (ms) for a single remote sync (rsync pull + import) before
# it is aborted (default: 600000 = 10min).
# DASHBOARD_REMOTE_SYNC_TIMEOUT_MS=600000
# Timeout (ms) for the Test SSH probe that verifies a remote source is
# reachable (default: 15000 = 15s).
# DASHBOARD_REMOTE_TEST_TIMEOUT_MS=15000
# Freshness window (ms) for a remote source session's live status. On each sync,
# a remote session whose mirrored transcript changed within this window is kept
# active; once the mirror stops advancing for longer, it is reconciled to
# completed. Remote sessions get no live hooks, so this replaces the local
# liveness/stale sweeps (which skip them). (default: 600000 = 10min).
# DASHBOARD_REMOTE_ACTIVE_WINDOW_MS=600000
# ── Lanes (durable parallel-work units and managed git worktrees) ────────────
# Directory dashboard-managed worktrees are provisioned under. It is also the
# boundary safety check 2 resolves every destructive lane path against: nothing
# outside it can be reset or removed. Keep it readable only by the service user
# (default: ~/.claude/ccam-lanes).
# LANES_ROOT=~/.claude/ccam-lanes
# Default base branch a new managed worktree resolves against when the request
# omits `base`. An explicit --base / request body `base` always wins
# (default: main).
# LANE_BASE_BRANCH=main
# Prefix for a new managed worktree's feature branch name, as <prefix><slug>
# (default: feat/).
# LANE_BRANCH_PREFIX=feat/
# Seconds of silence after which a lane whose stage matches /watch|poll/i flips
# from `active` to `dead` liveness. A silent *idle* lane is at rest, not dead
# (default: 300 = 5min).
# LANE_DEAD_SEC=300
+25
View File
@@ -0,0 +1,25 @@
# Force LF line endings for all text files, regardless of OS
* text=auto eol=lf
# Explicitly mark binary files to prevent corruption
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.svg binary
*.woff binary
*.woff2 binary
*.ttf binary
*.eot binary
*.db binary
# ── GitHub Linguist overrides — keep language stats honest ──────────────────
# wiki/mermaid.min.js is a vendored, minified third-party library (Mermaid);
# it is not project source code and must not count toward language stats.
wiki/mermaid.min.js linguist-vendored
# wiki/i18n-content.js is machine-assembled translation data (see its header:
# "AUTO-GENERATED wiki body-content translations ... Do not hand-edit").
# It is a data bundle keyed by English innerHTML, not hand-written JavaScript.
wiki/i18n-content.js linguist-generated
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env bash
# commit-msg hook runs Commitlint
npx --no-install commitlint --edit "$1"
+77
View File
@@ -0,0 +1,77 @@
# Dependency directories
node_modules/
jspm_packages/
# Build artifacts
dist/
build/
client/dist/
mcp/build/
desktop/out/
desktop/release/
desktop/assets/icon.iconset/
*.tsbuildinfo
# Database and data
/data/
*.db
*.db-wal
*.db-shm
# Environment variables
.env
.env.local
.env.*.local
.env.development.local
.env.test.local
.env.production.local
# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
*.log
# OS artifacts
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# IDEs/Editors
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.idea/workspace.xml
.vs/
.superpowers/
.omc/
*.swp
*.swo
*.sublime-project
*.sublime-workspace
.helm/
# Coverage and testing
coverage/
*.lcov
# Temporary files
.tmp/
.temp/
# Local-only remote data source test playbook (not shipped)
scripts/remote-test-commands.txt
# Playwright MCP
.playwright_mcp/
.playwright-mcp/
__pycache__/
# JetBrains IDE config — machine-specific, never shared
.idea/
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env sh
#
# Commit-msg: lightweight, dependency-free Conventional Commits advisory.
# This is intentionally NON-blocking — it only prints a hint when the subject
# doesn't look conventional. Commits are gated on tests (see pre-commit), not
# on the message format.
MSG_FILE="$1"
SUBJECT="$(head -n1 "$MSG_FILE" 2>/dev/null)"
# Skip merge/revert/fixup/squash commits — git generates those subjects.
case "$SUBJECT" in
Merge*|Revert*|fixup!*|squash!*) exit 0 ;;
esac
# type(scope)?!: description — e.g. "feat(tray): poll /api/stats"
PATTERN='^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([a-z0-9._/-]+\))?!?: .+'
if ! printf '%s' "$SUBJECT" | grep -Eq "$PATTERN"; then
echo "💡 Tip: commit subjects work best in Conventional Commits form:"
echo " <type>(optional-scope): <description>"
echo " e.g. feat(tray): poll /api/stats for live counts"
echo " (advisory only — your commit will still proceed)"
fi
exit 0
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env sh
#
# Pre-commit: auto-format staged files, then run the full test suite.
# The commit is ABORTED unless every test passes.
#
# Activated via `core.hooksPath=.husky` (set by the package.json "prepare"
# script on `npm install`). No husky/lint-staged runtime required.
# Abort the commit on the first failing command (formatting error or failing test).
set -e
ROOT="$(git rev-parse --show-toplevel)"
cd "$ROOT"
# ── 1. Format staged files with Prettier, then re-stage them ────────────────
# Only Added/Copied/Modified/Renamed paths in the index. --ignore-unknown lets
# Prettier silently skip anything it can't parse, so we can hand it every staged
# path without filtering by extension. NUL-delimited (-z / -0) so paths with
# spaces are handled correctly.
#
# NOTE: this re-adds the *whole* file. If you intentionally staged only part of
# a file, review the result after the auto-format.
STAGED="$(git diff --cached --name-only --diff-filter=ACMR)"
if [ -n "$STAGED" ]; then
PRETTIER="$ROOT/node_modules/.bin/prettier"
if [ -x "$PRETTIER" ]; then
set -- "$PRETTIER"
else
set -- npx --no-install prettier
fi
echo "🎨 Formatting staged files with Prettier..."
git diff --cached --name-only --diff-filter=ACMR -z | xargs -0 "$@" --write --ignore-unknown
git diff --cached --name-only --diff-filter=ACMR -z | xargs -0 git add
else
echo "🎨 No staged files to format."
fi
# ── 2. Run tests — commit is blocked unless all pass ────────────────────────
# Each suite is retried once on failure. The full run executes dozens of test
# files concurrently, each starting its own server (plus the CLI suite's
# spawned child processes with hard kill timeouts), so a loaded machine can
# produce a one-off timing failure unrelated to the commit. A real regression
# is deterministic: it fails both runs and still blocks the commit. This keeps
# the gate strict without making commits a dice roll on machine load.
run_suite() {
# $1 = human label, $2 = npm script
echo "🧪 Running $1 tests..."
if npm run "$2"; then
return 0
fi
echo "⚠️ $1 suite failed — retrying once to rule out machine-load flakiness..."
if npm run "$2"; then
echo "✅ $1 suite passed on retry — treating the first failure as a load flake."
echo " If this keeps happening, the flaky test above deserves a real fix."
return 0
fi
echo "❌ $1 suite failed twice — genuine failure, aborting the commit."
return 1
}
run_suite backend test:server
run_suite frontend test:client
echo "✅ Formatting applied and all tests passed — proceeding with commit."
+26
View File
@@ -0,0 +1,26 @@
node_modules/
data/
client/dist/
client/node_modules/
*.db
*.db-wal
*.db-shm
package-lock.json
client/package-lock.json
.vs/
*.md
index.html
wiki/index.html
*.yml
*.yaml
mcp/build/
desktop/out/
desktop/release/
desktop/node_modules/
desktop/package-lock.json
desktop/assets/*.svg
desktop/assets/*.png
desktop/assets/*.icns
desktop/assets/icon.iconset/
wiki/mermaid.min.js
fonts/*.woff2
+10
View File
@@ -0,0 +1,10 @@
{
"semi": true,
"singleQuote": false,
"trailingComma": "es5",
"printWidth": 100,
"tabWidth": 2,
"bracketSpacing": true,
"arrowParens": "always",
"endOfLine": "lf"
}
+130
View File
@@ -0,0 +1,130 @@
# Superpowers Workspace
This directory contains project-specific configuration and artifacts for the Superpowers workflow.
It acts as the working memory layer for agent-driven development, enabling structured planning, execution, and iteration across sessions.
---
## Purpose
Superpowers transforms coding agents into structured, process-driven collaborators. Instead of jumping straight into code, the agent:
1. Clarifies intent
2. Produces a design
3. Breaks work into executable steps
4. Executes via subagents
5. Verifies and iterates
This directory stores the outputs of that workflow so progress is persistent, inspectable, and reproducible.
---
## Directory Structure
Typical contents may include:
```
.superpowers/
├── brainstorm/ # Design explorations and refined specs
├── plans/ # Task breakdowns and execution plans
├── reviews/ # Code review outputs and feedback
├── runs/ # Execution logs or agent traces
└── README.md # This file
```
> Exact structure may evolve depending on which skills are triggered.
---
## Workflow Overview
Superpowers operates through automatic skill activation:
### 1. Brainstorming
* Refines vague ideas into concrete specs
* Explores alternatives
* Produces structured, reviewable design docs
### 2. Planning
* Converts approved designs into granular tasks
* Each task is:
* Small (25 min)
* Explicit (exact file paths + code)
* Verifiable
### 3. Execution
* Tasks are executed by subagents
* Includes:
* Spec compliance checks
* Code quality review
* Can run sequentially or in parallel
### 4. Verification
* Enforces test-first development (TDD)
* Ensures correctness before completion
* Prevents silent regressions
### 5. Completion
* Validates final state
* Offers merge / PR / discard options
* Cleans up working branches
---
## Key Principles
* **Test-first development** (RED → GREEN → REFACTOR)
* **Small, deterministic tasks**
* **Explicit over implicit**
* **Process over intuition**
* **Verification over assumption**
---
## How to Use
You dont interact with this directory directly most of the time.
Instead:
* Start a task in your coding agent (Claude, Cursor, etc.)
* Let Superpowers skills activate automatically
* Review outputs when prompted (designs, plans, reviews)
Artifacts will be written here as the workflow progresses.
---
## When to Look Here
Check this directory when you want to:
* Review the current plan
* Inspect prior design decisions
* Debug agent behavior
* Resume interrupted work
* Audit what was executed
---
## Notes
* Files here are **source-of-truth for agent state**
* Safe to commit (recommended for team workflows)
* Avoid manual edits unless you understand the workflow implications
---
## Related
* Project root `CLAUDE.md` → global agent context
* `.claude/` → rules, skills, and subagents
* Superpowers upstream docs →
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
{"reason":"idle timeout","timestamp":1774536800234}
@@ -0,0 +1 @@
1542
+9
View File
@@ -0,0 +1,9 @@
{
"recommendations": [
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"ms-vscode.vscode-typescript-next",
"bradlc.vscode-tailwindcss",
"dzhavat.mermaid-preview"
]
}
+29
View File
@@ -0,0 +1,29 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Server",
"program": "${workspaceFolder}/server/index.js",
"skipFiles": ["<node_internals>/**"],
"env": {
"NODE_ENV": "development",
"DASHBOARD_PORT": "4820"
}
},
{
"type": "chrome",
"request": "launch",
"name": "Debug Client (Vite)",
"url": "http://localhost:5173",
"webRoot": "${workspaceFolder}/client/src"
}
],
"compounds": [
{
"name": "Full Stack Debug",
"configurations": ["Debug Server", "Debug Client (Vite)"]
}
]
}
+16
View File
@@ -0,0 +1,16 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"files.exclude": {
"**/.git": true,
"**/.DS_Store": true,
"**/node_modules": true,
"**/dist": true,
"**/build": true
}
}
+48
View File
@@ -0,0 +1,48 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "npm: setup",
"type": "npm",
"script": "setup",
"group": "build",
"problemMatcher": []
},
{
"label": "npm: dev",
"type": "npm",
"script": "dev",
"isBackground": true,
"group": "none",
"problemMatcher": [
{
"owner": "typescript",
"fileLocation": ["relative", "${workspaceFolder}/client"],
"pattern": {
"regexp": "^([^\\s].*)\\((\\d+|\\d+,\\d+|\\d+,\\d+,\\d+,\\d+)\\):\\s+(error|warning|info)\\s+(TS\\d+)\\s*:\\s*(.*)$",
"file": 1,
"location": 2,
"severity": 3,
"code": 4,
"message": 5
},
"background": {
"activeOnStart": true,
"beginsPattern": "VITE v.* ready in .* ms",
"endsPattern": "ready in .* ms"
}
}
]
},
{
"label": "npm: test:server",
"type": "npm",
"script": "test:server",
"group": "test",
"presentation": {
"reveal": "always",
"panel": "new"
}
}
]
}
+35
View File
@@ -0,0 +1,35 @@
# Codex Project Instructions
## Project intent
- Keep this repository a stable, local-first Claude Code monitoring platform.
- Maintain correctness across hooks, API, DB, websocket, UI, and MCP integration.
## Priorities
- Correctness over cleverness.
- Small, scoped, reversible diffs.
- Preserve existing behavior unless change is requested.
- Update docs whenever workflow or architecture changes — follow `.claude/skills/update-project-docs/` automatically at the end of every change-set (README + VN/CN/KO mirrors, ARCHITECTURE, wiki + i18n + cache bump, server/client READMEs, docs/*).
- Every applicable source file you create or update (`.js/.ts/.tsx/.cjs/.mjs/.py/.sh/.css`) must start with the authorship header: a truthful file overview plus the exact line `@author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>`. See `.claude/skills/file-headers/` and `.claude/rules/file-headers.md`; verify with `bash .claude/skills/file-headers/scripts/check-headers.sh`.
## Where to work
- `server/` for API/routes/data processing.
- `client/` for React UI behavior.
- `mcp/` for local MCP server tooling.
- `scripts/` for hook/install/import/cleanup utilities.
## Validation expectations
- Backend changes: run `npm run test:server` when possible.
- Frontend changes: run `npm run test:client` when possible.
- MCP changes: run `npm run mcp:typecheck` and `npm run mcp:build`.
- If any check is skipped, report it explicitly.
## Safety expectations
- Keep destructive capabilities behind explicit configuration gates.
- Never broaden destructive behavior without explicit user request.
- Treat hook execution path as fail-safe and non-blocking.
## Useful commands
- Setup: `npm run setup`
- Dev: `npm run dev`
- Build/start: `npm run build` then `npm start`
- MCP helpers: `npm run mcp:install`, `npm run mcp:build`, `npm run mcp:start`
+2923
View File
File diff suppressed because one or more lines are too long
+70
View File
@@ -0,0 +1,70 @@
# Claude Code Working Guide
## Project mission
- Maintain a reliable local-first dashboard for Claude Code session monitoring.
- Preserve real-time behavior (hooks -> API -> SQLite -> WebSocket -> UI).
- Keep MCP integration production-ready for local use (`mcp/`).
## Repo map
- `server/`: Express API, hook ingestion, SQLite access, websocket broadcast (includes optional git upstream checks and `routes/updates.js`, plus `lib/workflow-ingest.js` which ingests on-disk Workflow-tool run journals — fleets that emit no hooks).
- `client/`: React + Vite UI.
- `scripts/`: hook installer/handler, import, seed, cleanup utilities. (Update detection lives server-side in `server/lib/update-check.js`; the dashboard never restarts itself — users run the printed command, surfaced in the UI and by `ccam update-check`.)
- `mcp/`: local MCP server exposing dashboard operations as tools.
## Lanes
A **lane** is a durable unit of parallel agent work — one working directory, many Claude Code sessions over time. Lanes are keyed by `cwd`, not `session_id`, so they survive session restarts and allow the dashboard to show a pipeline that persists across runs.
**Key points:**
- Sessions are bound to lanes by longest path-boundary prefix match on their `cwd` (set in hook data from `claude` CLI).
- A lane's stage is **declared** by the driving Claude session (via `ccam stage <stage> [--evidence "..."]`), not inferred by the dashboard.
- **The console never writes a lane's stage.** Declared stages come from `ccam stage` (a skill calling the CLI command), and inferred stages come from tool-event detection only. The UI's run console and the Workspace page never call `POST /api/lanes/:id/stage` — that boundary exists because the console observes tool calls but cannot see their outcomes. A lane's stage is the session's truth about progress; the console's observation of tool execution would not be proof of completion.
- **A detection expires, an evidence rule does not.** `recordDetection` skips its forward-only comparison once `detected_at` is older than `DETECTION_TTL_MS` (default 30 min), so a lane can move backwards between work sessions. That window changes only WHICH detection is current — it never relaxes declared-wins (an agent's own claim has no expiry) and never lets an inferred node render `done`.
- **Working-copy facts live at `GET /api/lanes/:id/git`, never inside `GET /api/lanes`.** That endpoint shells out to git three times; the lane list is polled and re-broadcast on every hook. A cwd that is not a readable repo returns `{available:false}` with HTTP 200 — a normal state, not a fault. Cards fetch it themselves every 30s and fail silently.
- **The Workspace console collapses with CSS, never by unmounting.** Unmounting `RunConsole` disposes the run subscription and drops a live run's rendered history.
- **CCAM does not orchestrate:** no chaining, no queue, no retry logic, no gate evaluation. The session in control makes all decisions; the dashboard records the claimed stage and shows evidence.
- Pipeline templates are JSON files (`server/data/pipelines/`) with node definitions; custom templates override built-ins when `DASHBOARD_PIPELINES_DIR` is set.
- Nodes render in five states: `failed` (rejected), `current` (now), `done` (with evidence), `passed-no-evidence` (claimed or skipped, amber), `pending` (not reached).
- Liveness: a silent **watcher** (stage matching `/watch|poll/`) is dead after `LANE_DEAD_SEC` seconds (default 300); a silent **idle** lane is at rest, not dead.
- **Never `rm -rf` a lane.** Destructive lane operations (`reset`, `remove`, `purge`) go through `server/lib/worktree.js`/`server/lib/lanes.js`, never a raw filesystem delete — that's what keeps the three-check destroy guard and the DB bookkeeping in lockstep.
- **Never build a git command as a shell string.** Lane git operations use `execFile("git", [...args])` with an explicit argument array (see `server/lib/worktree.js`'s `git()` helper), never a concatenated/interpolated string passed to a shell.
- **Adopted lanes are not destroyable.** A lane with `kind === "adopted"` may never have its worktree reset or removed; "removing" one only drops the dashboard's own record of it. This is enforced in code (`assertDestroyable`'s first check) — do not add a path that bypasses it.
See `docs/LANES.md` for full guide: stage reporting, custom templates, lane actions, and the five node states.
## Non-negotiable engineering rules
- Preserve existing behavior unless explicitly asked to change it.
- Prefer minimal, reversible diffs.
- Never silently weaken safety controls around destructive actions.
- Keep docs updated when behavior, commands, file locations, or workflows change — apply the `update-project-docs` skill automatically at the end of every change-set that alters behavior, config, interfaces, events, schema, CLI commands, or features (do not wait to be asked).
- Every applicable source file you create or update (`.js/.ts/.tsx/.cjs/.mjs/.py/.sh/.css`) must start with the copyright/authorship header — file overview + the exact line `@author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>`. Formats and audit script: `.claude/skills/file-headers/` (verify with `bash .claude/skills/file-headers/scripts/check-headers.sh`). This binds every coding agent (Claude Code, Codex, or others).
## Commands you should know
- Setup: `npm run setup`
- Dev: `npm run dev`
- Prod build/start: `npm run build` then `npm start`
- Server tests: `npm run test:server`
- Client tests: `npm run test:client`
- MCP install/build/start: `npm run mcp:install`, `npm run mcp:build`, `npm run mcp:start`
- MCP typecheck: `npm run mcp:typecheck`
- CLI (after setup): `ccam <command>` — terminal access to the full dashboard surface (`bin/ccam.js`; `ccam help` lists commands)
## Testing and verification policy
- Backend changes: run `npm run test:server` before finishing.
- Frontend changes: run `npm run test:client` when relevant. This includes per-screen render snapshots (`client/src/pages/__tests__/screens.snapshot.test.tsx`). If a UI change is intentional, review the snapshot diff and regenerate baselines with `cd client && npx vitest run -u`; never blindly update snapshots to make tests pass.
- MCP changes: run `npm run mcp:typecheck` and `npm run mcp:build`.
- If you cannot run a verification step, state exactly what was not run and why.
## Change guidelines by area
- API routes: preserve response shapes unless change is requested and documented.
- Database: avoid schema changes without migration-safe logic.
- Hooks: keep fail-safe and non-blocking behavior.
- WebSocket: keep message types stable and backward-compatible.
- Documentation: include exact commands and paths; keep markdown examples runnable.
## Agent behavior
- Explore first, then implement.
- For larger tasks, propose/check a short plan before broad edits.
- Use file-specific rules in `.claude/rules/` when working in scoped areas.
- Use project skills from `.claude/skills/` for repeatable workflows.
- Use `.claude/agents/` subagents for focused review or investigation passes.
+1080
View File
File diff suppressed because it is too large Load Diff
+253
View File
@@ -0,0 +1,253 @@
# Claude Code Monitor — Desktop App (macOS & Windows)
The dashboard ships with an optional **native desktop app** (built with Electron 35) that wraps the existing server + client into a single application you install once and forget — a macOS `.app` (shipped as a `.dmg`) and a Windows `.exe` (an NSIS installer plus a no-install portable build). Everything you see in the browser at `localhost:4820` lives inside this window, with native OS lifecycle on top: a menu-bar / notification-area (tray) icon, a native application menu, auto-start at login, and a single quit button that cleans up the server.
## Why this exists in addition to the PWA
The PWA (added in #144) makes the dashboard installable in Chromium-based browsers, which is great for users who already keep the server running. The desktop app solves the orthogonal problem: **starting and keeping the server running** without a terminal window. Concretely:
| Capability | PWA | Desktop App |
|---|---|---|
| Installs to dock / Applications | ✅ | ✅ |
| Manages the Express server | ❌ — user must `npm start` separately | ✅ — embedded in-process |
| Auto-starts at login | ❌ | ✅ via macOS Login Items / Windows `HKCU\…\Run` |
| Menu-bar / notification-area (tray) icon for always-on status | ❌ | ✅ |
| Native application menu (⌘ / Ctrl shortcuts, etc.) | ❌ | ✅ |
| Survives browser restart | ⚠️ depends on browser | ✅ |
The two coexist — install whichever fits your workflow.
## Quick install
**Option A — download a pre-built installer** (recommended):
1. Open [**Releases → latest**](https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor/releases/latest) and grab the asset for your platform. Every `master` commit that bumps the version in `package.json` cuts a new `vX.Y.Z` release automatically (CI publishes it), so this link always lands on the current build — no GitHub sign-in required.
| Platform | Asset | Notes |
|---|---|---|
| macOS (Apple Silicon) | `ClaudeCodeMonitor-<ver>-arm64.dmg` | drag into `/Applications` |
| macOS (Intel) | `ClaudeCodeMonitor-<ver>-x64.dmg` | drag into `/Applications` |
| Windows (installer) | `ClaudeCodeMonitor-Setup-<ver>-x64.exe` | per-user install, no admin |
| Windows (portable) | `ClaudeCodeMonitor-<ver>-x64-portable.exe` | run without installing |
2. Want a **per-commit build** instead of waiting for a release? Every green CI run uploads a workflow artifact (sign-in required, 14-day retention) — `ClaudeCodeMonitor-dmg` from the `🍎 macOS Desktop (DMG)` job and `ClaudeCodeMonitor-win` from the `🪟 Windows Desktop (EXE)` job:
```bash
gh run download <run-id> -R Smartgift-AI/Claude-Code-Monitor -n ClaudeCodeMonitor-dmg # or ClaudeCodeMonitor-win
```
3. **macOS:** double-click the DMG → drag `Claude Code Monitor.app` into your `Applications` folder. Open it; macOS may show a Gatekeeper warning the first time — see [Gatekeeper & SmartScreen](#gatekeeper--smartscreen-first-launch) below.
4. **Windows:** run `ClaudeCodeMonitor-Setup-<ver>-x64.exe` (per-user, no admin) and follow the wizard, or just run the `*-portable.exe` to launch without installing. Windows **SmartScreen** may show *"Windows protected your PC"* the first time — see [Gatekeeper & SmartScreen](#gatekeeper--smartscreen-first-launch) below.
**Option B — build locally:**
```bash
# In the project root, after `git clone`:
npm run setup # installs root + client + vscode-extension deps
npm run build # builds the React client
npm run desktop:install # installs Electron + electron-builder
# Build for macOS (run ON macOS) — pick one:
npm run desktop:dmg:arm64 # Apple Silicon only — FAST (~1 min); use this for your own Mac
npm run desktop:dmg:x64 # Intel only — FAST
npm run desktop:dmg # BOTH per-arch DMGs (arm64 + x64) — the release build; slower (packages each arch)
npm run desktop:dmg:universal # ONE merged universal DMG (arm64 + x86_64 in a single file) — optional, slowest
# Build for Windows (run ON Windows) — pick one:
npm run desktop:win # NSIS installer → desktop/release/ClaudeCodeMonitor-Setup-<ver>-x64.exe
npm run desktop:win:portable # no-install portable → desktop/release/ClaudeCodeMonitor-<ver>-x64-portable.exe
# electron-builder packages for the HOST OS — you cannot build a Windows .exe
# on macOS or a macOS .dmg on Windows.
# Open the macOS DMG you just built. desktop:dmg:arm64 / :x64 wipe release/ and emit
# one DMG; desktop:dmg wipes release/ and emits both (…-arm64.dmg + …-x64.dmg).
open desktop/release/ClaudeCodeMonitor-*-arm64.dmg # …-x64.dmg for the Intel build
```
> **`desktop:dmg` builds both architectures, so it takes longer.** It packages
> and ad-hoc-signs the app **twice** — once for `arm64`, once for `x64` — and
> emits two separate DMGs (`…-arm64.dmg` + `…-x64.dmg`). It does **not** merge
> them into a single universal binary; the release ships the two per-arch DMGs.
> For running on **your own Mac**, use the arch-specific command
> (`desktop:dmg:arm64` / `desktop:dmg:x64`) — half the work, and it finishes in
> about a minute. CI runs `desktop:dmg` for you and uploads both DMGs as the
> `ClaudeCodeMonitor-dmg` artifact, so you rarely need to build them locally.
## What happens when you launch the app
1. The Electron main process picks a free port — preferring **4820**, falling back to 48214829, then a random high port if all those are taken.
2. If something already answers `/api/health` on port 4820 (e.g. you ran `npm start` in a terminal), the app **adopts that server** and skips starting a second one. No double-binding, no SQLite contention.
3. Otherwise it `require()`s `server/index.js` directly in-process — same Node runtime as the main process, same memory. Boot is typically under two seconds.
4. On startup the server records its **live port** to `~/.claude/.agent-dashboard.json`. The Claude Code hook handler reads that file, so events still reach the dashboard when the app bound a fallback port instead of 4820.
5. The dashboard window opens — unless the app was launched at login (on macOS via Login Items; on Windows via the `HKCU\…\Run` entry, detected through a `--ccam-hidden` launch arg since Windows has no `wasOpenedAtLogin`), in which case it stays tray-only.
6. A tray icon appears — the macOS **menu bar** or the Windows **notification area**. One click opens a dropdown with a **live status snapshot** (server port, active sessions, working agents, events today — all clickable to jump into the dashboard) plus *Open Dashboard*, *Open in Browser*, *Restart Server*, *Show Logs*, *Open at Login* (toggle), and *Quit*.
## Lifecycle semantics
- **Closing the window hides it.** The server keeps running, the tray icon stays, and (on macOS) the **dock icon stays too** — clicking either re-opens the window. Independent signals that the app is still alive.
- **Quitting** (⌘Q / Ctrl+Q, *Quit* in the application menu, or *Quit* in the tray menu) pops a confirmation dialog — *"Quit Claude Code Monitor? Press ⌘Q again to skip this prompt and quit immediately."* Press **Quit** in the dialog, or **press ⌘Q / Ctrl+Q a second time** to bypass the prompt. Either way the SQLite handle is checkpointed cleanly before the process exits.
- **Tray** — the macOS menu bar / Windows notification area. macOS uses a black template glyph the OS tints for light/dark menu bars; Windows uses the colored `icon.ico`, because a template glyph would vanish on the dark taskbar. A single click (left or right) opens the dropdown, which shows a **live status snapshot** pulled straight from the embedded SQLite handle each time it opens: server port, active sessions, working agents, and events today. Snapshot rows are clickable — they open the dashboard. The tray's *Open Dashboard* reliably **raises** the window even when it is minimized or behind other windows. (The application menu's *File ▸ Open Dashboard* / ⌘1 is **macOS-only** — on Windows/Linux a window-attached menu accelerator can't reopen a hidden window, so reopening is the tray's job there.)
- **Window / taskbar icon** — the `BrowserWindow` sets its `icon` to the colored app logo (`icon.ico` on Windows, `icon.png` elsewhere — the same logo as the macOS Dock, rendered from `assets/icon.svg`), so an unpackaged `desktop:dev` run shows the real app logo in the title bar / taskbar instead of the generic Electron icon. The macOS dev Dock icon is set too; packaged apps already get theirs from the bundle `.icns`/`.exe`.
- **Open-at-login toggle:** flip *Open at Login* in the tray menu (or the app menu). Both platforms go through Electron's first-party `app.*LoginItemSettings` API — no third-party deps. On **macOS** it registers via the `SMAppService` API, so the entry appears under → *System Settings → General → Login Items*. On **Windows** it writes a per-user `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` entry, visible under *Task Manager → Startup*; a login-triggered launch is detected via a `--ccam-hidden` arg (Windows has no `wasOpenedAtLogin`). On Linux the toggle is a no-op (unsupported).
- **Single-instance:** double-launching just focuses the existing window. No second server, no port collision. (Applies on every platform.)
- **Logs** live at `~/Library/Logs/Claude Code Monitor/desktop.log` on macOS and `%APPDATA%\Claude Code Monitor\logs\desktop.log` on Windows (use *Show Logs* in the tray menu to open the folder).
- **Your data** (the SQLite database and VAPID keys) lives outside the app bundle / install dir, so it **survives app reinstalls and updates** — `~/Library/Application Support/Claude Code Monitor/data/` on macOS, `%APPDATA%\Claude Code Monitor\data\` on Windows. The Windows NSIS uninstaller **keeps this data by default** (`deleteAppDataOnUninstall: false`), mirroring how dragging the `.app` to the Trash on macOS never touches your data.
- **The `claude` CLI on PATH.** On **macOS** the app resolves it using your login-shell `PATH`, recovered at startup — so "Run Claude" works even though a Finder/Dock-launched app would otherwise only inherit a minimal `PATH`. On **Windows** the inherited user `PATH` already includes it, so no recovery is needed.
- **Notifications** (including the in-dashboard *Send test notification* button) are delivered as **native OS toasts** on both platforms when running inside the app — the embedded server calls Electron's `Notification` API directly. On Windows the app sets an `AppUserModelId` (`com.vn.smartgift.ccam.desktop`, matching the electron-builder `appId`) so toasts attribute to the app and its taskbar windows group correctly. Web Push doesn't work reliably inside Electron (Chromium-in-Electron ships without Firebase Cloud Messaging credentials, so `pushManager.subscribe` returns endpoints nothing can deliver to), and this path bypasses it entirely. The web dashboard at `npm start` continues to use Web Push as before.
- **Coexists with the web dashboard.** You can run the desktop app and `npm run dev` (or `npm start`) at the same time. Each server writes its `{port, pid, startedAt, dataDir}` entry to a shared discovery file at `~/.claude/.agent-dashboard.json`, and the Claude Code hook handler POSTs to **one ingest target per unique SQLite data directory** (lowest port wins when both share `~/.claude/agent-dashboard`, so events are never double-ingested). Servers with **different** databases (e.g. the desktop app's Application Support dir alongside `npm run dev`) still each receive hooks and stay real-time.
## File layout (for contributors)
```
desktop/
├── package.json # Electron + electron-builder
├── tsconfig.json
├── electron-builder.yml # macOS (dmg) + Windows (nsis/portable) targets; signing/notarization hooks
├── assets/ # icon.svg + generated icon.icns (macOS) + icon.ico (Windows) + tray PNGs
├── src/
│ ├── main.ts # main process entry, lifecycle; setAppUserModelId on win32
│ ├── server-host.ts # in-process Express boot, port discovery, adopt
│ ├── window.ts # BrowserWindow + persisted state
│ ├── tray.ts # tray icon (platform image: template PNG on macOS, icon.ico on Windows) + context menu
│ ├── menu.ts # native application menu
│ ├── login-item.ts # open-at-login (macOS Login Items + Windows HKCU\…\Run startup)
│ ├── shell-path.ts # recover the user's shell PATH (find `claude`)
│ ├── preload.ts # (empty — kept for future renderer bridges)
│ ├── logger.ts # file logger
│ └── constants.ts # incl. APP_ID (matches electron-builder appId)
├── scripts/
│ ├── install.js # `desktop:install` wrapper: runs npm install, then prints actionable native-dep help + exits non-zero on failure
│ ├── preflight.js # shared native-dep check (hasBetterSqliteBinary) + per-OS prerequisite help (printNativeDepHelp)
│ ├── prebuild.js # ensures root + client are built before tsc; shells npm/npx on Windows (.cmd shims); fails fast with setup help when the better-sqlite3 native binary is missing
│ ├── build-icons.sh # SVG → PNG/ICNS + tray PNGs via qlmanage/sips/iconutil (macOS)
│ ├── build-win-icon.ps1 # icon.png → icon.ico for Windows (PowerShell + .NET)
│ └── notarize.js # electron-builder afterSign hook (opt-in; macOS only)
└── tests/
└── smoke.test.mjs # spawn-and-probe /api/health (resolves the real electron binary via createRequire)
```
**Changes outside `desktop/` are deliberately minimal:**
- `server/index.js` — a behavior-preserving refactor: the post-listen bootstrap (one-time legacy-session import, update scheduler, Claude Code config watcher, orphaned-run reconciliation) was extracted into an exported `startBackgroundServices()` so the embedded server runs exactly what `node server/index.js` runs. The standalone server path is functionally unchanged. (The legacy-session import previously sat in the standalone-only `require.main` block, so the desktop dashboard started empty — moving it into `startBackgroundServices()` fixes that.) It also now publishes its live port via `server/lib/server-info.js` on startup.
- `server/lib/server-info.js` *(new)* — writes/reads the `~/.claude/.agent-dashboard.json` port discovery file.
- `scripts/hook-handler.js` — resolves the dashboard port from the discovery file (falling back to `CLAUDE_DASHBOARD_PORT`, then 4820), so hook events reach the server even when it bound a fallback port.
`client/`, `mcp/`, and `vscode-extension/` are untouched. The Electron main process is otherwise just a host for the same code.
## Gatekeeper & SmartScreen (first launch)
### macOS — Gatekeeper
The DMG is **ad-hoc signed** by default — that's all the project can offer without a paid Apple Developer ID. macOS will warn the first time you open it: *"Apple could not verify…"*.
Two ways past it:
```bash
# Easiest: strip the quarantine attribute from the DMG before opening.
xattr -cr ~/Downloads/ClaudeCodeMonitor-*.dmg
```
Or open → *System Settings → Privacy & Security*, scroll to the blocked DMG, click *Open Anyway*.
### Windows — SmartScreen
The Windows `.exe` (both the installer and the portable build) is **unsigned** by default, so Windows **SmartScreen** may show *"Windows protected your PC"* the first time you run it. Click **More info → Run anyway** to launch it.
Authenticode signing is opt-in for the maintainer: provide a code-signing certificate via `CSC_LINK` (a base64-encoded `.p12`) and `CSC_KEY_PASSWORD` and electron-builder signs the `.exe` automatically — no code change required. A signed build skips the SmartScreen prompt.
### Notarization (for the maintainer)
When you're ready to make this go away for everyone, add these three repository secrets:
| Secret | Where it comes from |
|---|---|
| `APPLE_ID` | Your Apple ID email |
| `APPLE_TEAM_ID` | Your Apple Developer team ID |
| `APPLE_APP_SPECIFIC_PASSWORD` | An app-specific password created at appleid.apple.com |
Optionally, also `CSC_LINK` (base64-encoded `.p12`) and `CSC_KEY_PASSWORD` to provide an explicit Developer ID certificate from outside the runner keychain. The CI workflow picks them up automatically — no code change required. See [`desktop/scripts/notarize.js`](desktop/scripts/notarize.js) for the hook.
> Local builds are **always ad-hoc signed**: the `package` script sets `CSC_IDENTITY_AUTO_DISCOVERY=false`, so a code-signing certificate already in your macOS keychain is never auto-discovered (an Apple Development cert would otherwise be picked up and fail distribution-type signing). Real signing activates only through the explicit `CSC_LINK` certificate above — that path is unaffected by the flag.
## Development workflow
```bash
# Hot-iterate on the main process (rebuilds tsc on save would be next steps;
# v1 ships without watch mode — just re-run desktop:dev after changes):
npm run desktop:dev
# Smoke test (also runs in CI on macOS):
npm run desktop:test
# macOS — single-architecture DMG — fast (~1 min):
npm run desktop:dmg:arm64 # or desktop:dmg:x64 for Intel
# macOS — both per-arch DMGs — slower (builds + signs each architecture):
npm run desktop:dmg
# macOS — one merged universal DMG (arm64 + x86_64 in a single file) — optional, slowest:
npm run desktop:dmg:universal
# Windows — NSIS installer / no-install portable (run ON Windows):
npm run desktop:win # NSIS installer .exe
npm run desktop:win:portable # no-install portable .exe
```
> electron-builder packages for the **host OS** — build DMGs on macOS and the
> Windows `.exe`s on Windows. The Windows icon regenerates from `icon.png` with
> `npm run build:win-icon` (PowerShell + .NET); the macOS icns + tray PNGs come
> from `npm run build:icons`. On Windows, `better-sqlite3` is fetched as a
> prebuilt Electron binary by `npm run desktop:install` (its postinstall runs
> `electron-builder install-app-deps`), so no Visual Studio C++ toolchain is
> needed in the common case. If that fetch/rebuild *does* fail (no C++ toolchain,
> or a Node version with no prebuilt binary), `npm run desktop:install` — and any
> `desktop:*` build, gated by `prebuild.js` — prints the exact per-OS fix plus a
> no-toolchain alternative and **fails loudly** rather than crashing at runtime:
>
> ```bash
> cd desktop
> npm install --ignore-scripts
> node node_modules/electron/install.js
> npx electron-builder install-app-deps
> ```
>
> A Node LTS (20/22) ships prebuilt `better-sqlite3` binaries and avoids the
> compile entirely.
> After `npm run clean` in `desktop/`, you must `npm run build` again before
> packaging — `clean` removes `out/`, and `electron-builder` only packages, it
> does not compile. The `desktop:dmg*` scripts chain the build for you; a bare
> `electron-builder` call does not, and fails with
> _"entry file out/main.js does not exist"_.
The smoke test does not exercise the BrowserWindow (no display on headless CI). It spawns Electron, waits for the embedded server to answer `/api/health`, then shuts down. Anything that depends on the renderer is part of the manual QA checklist on the PR.
## Known caveats
- **Bundle size** ≈ 80 MB DMG, ≈ 250 MB on disk. The standard Electron tax. The Windows installer is comparable. Tauri would cut this dramatically but at the cost of a sidecar-process model and a Rust toolchain dependency — fair to revisit in a follow-up PR if bundle size becomes a real complaint.
- **Native modules**: `better-sqlite3` is rebuilt against Electron's Node version automatically via `electron-builder install-app-deps` in the desktop workspace's `postinstall`. On Windows it is fetched as a **prebuilt Electron binary**, so no Visual Studio C++ toolchain is needed in the common case. If that build *does* fail (or the binary is missing afterward), `npm run desktop:install` — and any `desktop:*` build — prints the exact per-OS fix (Windows: Visual Studio Build Tools with the "Desktop development with C++" workload; macOS: `xcode-select --install`; Linux: build-essential + python3) plus a no-toolchain alternative (`npm install --ignore-scripts``node node_modules/electron/install.js``npx electron-builder install-app-deps`), and exits non-zero — failing loudly at install/build time rather than crashing at runtime. Even so, if the module is unavailable the server falls back to `node:sqlite` (per #37), so the app still boots.
- **Per-architecture DMGs**: `npm run desktop:dmg` builds **both** macOS DMGs (one `arm64`, one `x64`) — the release build, and slower because it packages each architecture separately. It does **not** produce a merged universal binary; the release ships the two per-arch DMGs. `npm run desktop:dmg:arm64` and `npm run desktop:dmg:x64` build a single architecture instead — much faster, and roughly half the disk. If you specifically want a **single merged universal binary** (both slices in one `.dmg`, `lipo`-fat), `npm run desktop:dmg:universal` produces one via `@electron/universal` — the slowest option, and not what the release ships, but handy for hand-distributing one file that runs on any Mac.
- **Auto-update**: not wired on either platform. The current update path is *re-download the latest installer* (DMG on macOS, `.exe` on Windows). `electron-updater` + GitHub Releases is the natural follow-up.
## Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| "Apple could not verify…" on first launch (macOS) | Unnotarized DMG | `xattr -cr ~/Downloads/ClaudeCodeMonitor-*.dmg` |
| "Windows protected your PC" on first launch (Windows) | The `.exe` is unsigned by default (SmartScreen) | Click **More info → Run anyway**. To remove the prompt for everyone, the maintainer can enable Authenticode signing via `CSC_LINK` + `CSC_KEY_PASSWORD` |
| macOS prompts to install Rosetta when opening the app | You installed the **x64** build on an Apple Silicon Mac | Check your arch with `uname -m` (`arm64` → Apple Silicon, build with `desktop:dmg:arm64`). The arch-specific `desktop:dmg:arm64` / `desktop:dmg:x64` builds each wipe `release/` and emit a single DMG whose mounted-volume title states the architecture — e.g. *Claude Code Monitor (Apple Silicon)* — so there is no ambiguous window to drag from. (`desktop:dmg` emits both per-arch DMGs at once, for release.) If stale DMGs from an older build linger, clear them with `rm -rf desktop/release` and rebuild |
| Window shows but content is blank (macOS) | Server didn't boot — check `~/Library/Logs/Claude Code Monitor/desktop.log` | Restart from tray → *Restart Server* |
| Window shows but content is blank (Windows) | Server didn't boot — check `%APPDATA%\Claude Code Monitor\logs\desktop.log` | Restart from tray → *Restart Server* |
| Tray icon missing (macOS) | The OS hides tray icons when the menu bar is full | Move other menu-bar items aside, or look in the overflow chevron |
| Tray icon missing (Windows) | Windows tucked it into the notification-area overflow | Click the **^** overflow chevron in the taskbar; drag the icon out to keep it pinned |
| App didn't auto-start at login (macOS) | Login Items entry got revoked by macOS | Toggle *Open at Login* off and on again from the tray menu |
| App didn't auto-start at login (Windows) | The `HKCU\…\Run` startup entry is missing or was disabled | Toggle *Open at Login* off and on again from the tray menu, then confirm the entry under *Task Manager → Startup* is **Enabled** |
| `npm run desktop:win` / `:win:portable` fails or produces nothing | electron-builder packages for the host OS — you ran it on macOS/Linux | Build the Windows `.exe` **on Windows** (and DMGs on macOS) |
| Desktop build/install fails on `better-sqlite3` / native binary missing | No C++ toolchain, or no prebuilt for your Node version | Run `npm run desktop:install` and follow the printed help, or use the no-toolchain alternative (`npm install --ignore-scripts``node node_modules/electron/install.js``npx electron-builder install-app-deps`); or use Node LTS 20/22 |
| Port 4820 already in use, app refuses to start | Something other than the dashboard is on 4820 and it doesn't answer `/api/health` | The app will pick a fallback (48214829, then a random high port) — check the tray menu's port indicator |
| Dashboard stays empty — 0 sessions, 0 agents, no real-time updates | The app bound a fallback port (4820 was taken), and the Claude Code hooks were posting events to the wrong port | Fixed — the server publishes its live port to `~/.claude/.agent-dashboard.json` and the hook handler reads it. After upgrading from a pre-fix build, **start a new Claude Code session** so the updated hooks take effect |
| `desktop:dmg` seems slow | Not stuck — it packages two architectures back-to-back (`arch=x64` then `arch=arm64`) | Wait it out, or build a single architecture with `desktop:dmg:arm64` / `desktop:dmg:x64` |
| Build fails: `entry file out/main.js does not exist` | `electron-builder` was run without compiling TypeScript first | Build via `npm run desktop:dmg*` (chains the build); don't invoke `electron-builder` bare |
| Signing fails with `Application … could not be found` | A code-signing certificate in your keychain was auto-discovered | Fixed — the `package` script sets `CSC_IDENTITY_AUTO_DISCOVERY=false`; build via `npm run desktop:dmg*` |
| "Run Claude" reports the `claude` CLI isn't on your PATH | A Finder/Dock-launched app inherits launchd's minimal PATH, not your shell PATH | Fixed — the app recovers your login-shell PATH at startup. If it persists, ensure `claude` is a real executable (not a shell alias/function) and on your shell PATH |
| Imported history / sessions vanished after updating the app | Older builds stored the database inside the (replaceable) app bundle | Fixed — data now lives in `~/Library/Application Support/Claude Code Monitor/data/` and survives reinstalls. After upgrading from a pre-fix build, re-run **Import History → Rescan** once |
| Signing fails: `Application … could not be found` after retries | A keychain code-signing certificate was auto-discovered | Fixed — the `package` script sets `CSC_IDENTITY_AUTO_DISCOVERY=false`; build via `npm run desktop:dmg*` |
+63
View File
@@ -0,0 +1,63 @@
# Multi-stage Dockerfile for Claude Code Agent Monitor - a Node.js server with a React client.
# This setup optimizes the final image size by separating the build and runtime stages.
# The first stage installs only the production dependencies for the server, while the second stage builds the React client.
# The final stage combines the necessary files and dependencies to run the application in production.
# Compatibility: This setup is compatible with both Podman and Docker. Runnable on any platform that supports Node.js and Alpine Linux.
#
# Author: Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
# ── Stage 1: Install server production deps ───────────────────────────
FROM node:22-alpine AS server-deps
WORKDIR /app
COPY package.json package-lock.json ./
# The root `postinstall` hook (scripts/postinstall.js) fires during `npm ci`, so
# the file must exist here or npm aborts with MODULE_NOT_FOUND before installing
# anything. It self-skips when client/ is absent (as it is in this stage), so
# copying just the one script keeps this deps-cache layer from busting on
# unrelated scripts/ edits. Do NOT use --ignore-scripts: that would also skip
# better-sqlite3's prebuild fetch and silently drop the native SQLite driver.
COPY scripts/postinstall.js ./scripts/postinstall.js
RUN npm ci --omit=dev
# ── Stage 2: Build React client ───────────────────────────────────────
FROM node:22-alpine AS client-build
WORKDIR /app/client
COPY client/package.json client/package-lock.json ./
RUN npm ci
COPY client/ ./
# vite.config.ts stamps the UI version from the repo-root package.json (one level
# up from the client dir). Provide it here so the built client shows the real
# release version; the config falls back gracefully if it is ever absent.
COPY package.json /app/package.json
RUN npm run build
# ── Stage 3: Production runtime ───────────────────────────────────────
FROM node:22-alpine
WORKDIR /app
COPY --from=server-deps /app/node_modules ./node_modules/
COPY package.json ./
COPY server/ ./server/
COPY scripts/ ./scripts/
COPY statusline/ ./statusline/
COPY --from=client-build /app/client/dist ./client/dist/
RUN mkdir -p data
EXPOSE 4820
ENV NODE_ENV=production
# In a container the app MUST bind all interfaces: the server binds loopback by
# default (GHSA-gr74-4xfh-6jw9), but a container's loopback is a separate
# namespace the published port cannot reach, so a loopback bind makes the port
# unreachable. Inside a container the trust boundary is the *host* port publish
# (keep it on 127.0.0.1 — see docker-compose.yml / INSTALL.md), not this bind.
ENV DASHBOARD_HOST=0.0.0.0
# The recommended ~/.claude bind mount is read-only, so the server cannot write
# its default data dir (~/.claude/agent-dashboard). Persist to the mounted
# volume at /app/data instead.
ENV DASHBOARD_DATA_DIR=/app/data
CMD ["node", "server/index.js"]
+504
View File
@@ -0,0 +1,504 @@
# Installation
A step-by-step guide to get the Claude Code Agent Monitor up and running on your machine, with optional sections for importing history, running in a container, and using the native desktop app (macOS & Windows).
## Requirements
| Requirement | Version | Notes |
|---|---|---|
| Node.js | 20+ (22+ recommended) | Required for server and client |
| npm | 9+ | Comes with Node.js |
| Claude Code | 2.x+ | Required for hook integration |
| Python | 3.6+ | Optional — statusline utility only |
| Git | Any | For cloning the repository |
---
## Step 1 — Clone the repository
```bash
git clone https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor.git
cd Claude-Code-Agent-Monitor
```
---
## Step 2 — Install dependencies
```bash
npm run setup
```
This installs all server and client dependencies, plus the VS Code extension, and links the `ccam` CLI.
A plain root install already covers server **and** client — a `postinstall` hook installs the client dependencies automatically, so this alone is enough to build and run the dashboard:
```bash
npm install
```
`npm run setup` additionally installs the VS Code extension and links the `ccam` CLI. (If you install with `--ignore-scripts`, the `postinstall` hook is skipped — run `cd client && npm install` manually in that case.)
Or via Makefile (also installs MCP dependencies):
```bash
make setup
```
---
## Step 3 — Start the dashboard
```bash
npm run dev
```
This starts two processes concurrently:
| Process | URL | Description |
|---|---|---|
| Express server | http://localhost:4820 | API, WebSocket, SQLite |
| Vite dev server | http://localhost:5173 | React frontend with HMR |
Open **http://localhost:5173** in your browser.
> [!TIP]
> When you run the dashboard directly on the host with `npm run dev` or `npm start`, the server automatically writes the Claude Code hook configuration to `~/.claude/settings.json`. If you run the dashboard in Docker or Podman, install hooks from the host with `npm run install-hooks` after the container is up.
---
## Step 4 — Start a Claude Code session
Start a new Claude Code session from any directory **after** the dashboard server is running. The hooks will fire automatically and your sessions, agents, and events will appear in real-time.
```bash
# In a separate terminal, from any project directory:
claude
```
---
## Verification
After starting a Claude Code session, you should see:
- **Sessions page** — your session listed with status `Waiting` (a fresh CLI sitting at the prompt) or `Active` (mid-turn)
- **Agent Board** — a `Main Agent` card in the `Waiting` column until you type your first message; it flips to `Working` on `UserPromptSubmit` / `PreToolUse` and back to `Waiting` after each `Stop`
- **Activity Feed** — events streaming in as Claude Code uses tools
- **Dashboard** — stats updating in real-time
- **Settings page** — model pricing rules, hook configuration status, data export and cleanup tools
If nothing appears after 30 seconds, see [SETUP.md](./SETUP.md#troubleshooting).
### PWA install (optional)
The dashboard is a Progressive Web App. After opening it in a supported browser (Chrome, Edge, Firefox), you can install it to your dock / home screen:
1. Look for the **install icon** (⊕) in the browser address bar, or open the browser menu → "Install app"
2. Once installed, the dashboard launches in its own window with no browser chrome
3. Offline support: previously visited pages and assets are served from the Service Worker cache when the network is unavailable
The landing page and wiki are also installable PWAs with their own manifests and service workers — visit each in a browser to install independently.
---
## Step 5 — (Optional) Import existing Claude Code history
The server **automatically imports** every session under
`~/.claude/projects/` on startup, so if you've used Claude Code on this
machine before, your historical sessions, agents, events, token counts,
and cost totals should already be visible in the Sessions list.
To bring in history from another machine, a backup, or a `.tar.gz`
archive a teammate sent you, use **Settings → Import History** in the
UI. It supports three modes:
```mermaid
flowchart LR
UI["Settings →<br/>Import History"] --> M1["Rescan default folder<br/>~/.claude/projects"]
UI --> M2["Scan a folder<br/>any absolute path"]
UI --> M3["Upload files<br/>.jsonl / .zip / .tar.gz / .gz"]
M1 --> P["Same parser as live<br/>hook ingestion"]
M2 --> P
M3 --> P
P --> DB[("SQLite<br/>sessions + tokens + cost")]
style UI fill:#a855f7,stroke:#c084fc,color:#fff
style P fill:#f59e0b,stroke:#fbbf24,color:#000
style DB fill:#10b981,stroke:#34d399,color:#fff
```
Re-imports are idempotent: sessions are deduplicated by UUID and
compaction baselines preserve pre-compaction token totals, so running
the importer twice never double-counts tokens or cost.
Verify it worked by opening the **Analytics** page and checking that
per-model token totals and estimated cost look correct. Full walkthrough
with per-OS archive commands in
[SETUP.md → Importing existing Claude Code history](./SETUP.md#importing-existing-claude-code-history).
### Optional: tune import limits
If you regularly import very large archives, these environment variables
can be raised (the defaults are generous for typical use):
| Variable | Default | Purpose |
| --------------------------------- | ------- | ----------------------------------------------------------- |
| `CCAM_IMPORT_MAX_BYTES` | 1 GB | Maximum size per uploaded file |
| `CCAM_IMPORT_MAX_FILES` | 2000 | Maximum files per upload request |
| `CCAM_IMPORT_MAX_EXTRACT_BYTES` | 4 GB | Ceiling on uncompressed bytes per archive (zip-bomb guard) |
Set them before `npm run dev` or `npm start`:
```bash
CCAM_IMPORT_MAX_EXTRACT_BYTES=17179869184 npm start # allow 16 GB extraction
```
---
## Production mode
To run as a single process serving the built client:
```bash
npm run build # Build the React client
npm start # Start Express serving client/dist on port 4820
```
Open **http://localhost:4820** in your browser.
---
## Desktop App (macOS & Windows) (optional)
If you'd rather not keep a terminal window open, the project also ships an Electron 35-based **native desktop app** (the `desktop/` workspace), available for both **macOS** and **Windows**. It embeds the Express server in-process, renders the built React client in a `BrowserWindow`, registers a menu-bar / notification-area (tray) icon, and offers a one-click "Open at Login" toggle. Everything you'd see in the browser at `localhost:4820` lives inside a single app you install once — distributed as a macOS `.app` (in a `.dmg`) and a Windows `.exe` (an NSIS installer plus a no-install portable build).
### Prerequisites
| For… | You need |
|---|---|
| Downloading a pre-built installer (macOS) | macOS — nothing else |
| Downloading a pre-built installer (Windows) | Windows 10/11 (x64) — nothing else |
| Building the DMG locally (macOS) | macOS, Node.js 20+ (22+ recommended), npm 9+, and **Xcode command-line tools** (`xcode-select --install`) so the native `better-sqlite3` module can be rebuilt for Electron's ABI |
| Building the `.exe` locally (Windows) | Windows, Node.js 20+ (22+ recommended), npm 9+. `better-sqlite3` is fetched as a **prebuilt Electron binary** by `npm run desktop:install`, so no Visual Studio C++ toolchain is needed in the common case. If the build _does_ fail, `npm run desktop:install` prints the exact fix (Visual Studio Build Tools + "Desktop development with C++") plus a no-toolchain alternative and exits non-zero rather than failing silently |
### Way 1 — Download a pre-built installer
The fastest path. There are two flavours:
**1a. From the latest GitHub Release** *(recommended — public, no sign-in)*
Open [**Releases → latest**](https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor/releases/latest) and download the asset for your platform. CI publishes a new `vX.Y.Z` release automatically every time the version in `package.json` is bumped on `master`, so this link always points at the current shipping build.
| Platform | Asset | Notes |
|---|---|---|
| macOS (Apple Silicon) | `ClaudeCodeMonitor-<ver>-arm64.dmg` | drag into `/Applications` |
| macOS (Intel) | `ClaudeCodeMonitor-<ver>-x64.dmg` | drag into `/Applications` |
| Windows (installer) | `ClaudeCodeMonitor-Setup-<ver>-x64.exe` | per-user install, no admin |
| Windows (portable) | `ClaudeCodeMonitor-<ver>-x64-portable.exe` | run without installing |
**1b. From the per-commit CI artifact** *(useful for testing master before it's tagged — sign-in required, 14-day retention)*
Every green run of the desktop CI jobs uploads a packaged artifact — `ClaudeCodeMonitor-dmg` from the `🍎 macOS Desktop (DMG)` job and `ClaudeCodeMonitor-win` from the `🪟 Windows Desktop (EXE)` job:
- **Via the GitHub UI:** open the latest passing run under [Actions](https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor/actions/workflows/ci.yml?query=branch%3Amaster+is%3Asuccess), scroll to **Artifacts**, and download `ClaudeCodeMonitor-dmg` (macOS) or `ClaudeCodeMonitor-win` (Windows).
- **Via the `gh` CLI:**
```bash
gh run download <run-id> -R Smartgift-AI/Claude-Code-Monitor -n ClaudeCodeMonitor-dmg # macOS
gh run download <run-id> -R Smartgift-AI/Claude-Code-Monitor -n ClaudeCodeMonitor-win # Windows
```
Unzip the macOS artifact to get the `.dmg`s, or the Windows artifact to get the NSIS installer + portable `.exe`s.
Then jump to [Install the app](#install-the-app).
### Way 2 — Build the installer locally
From the project root, after `git clone`. electron-builder packages for the **host OS**, so build the macOS DMG on a Mac and the Windows `.exe` on Windows. The common prelude is the same:
```bash
npm run setup # install root + client + vscode-extension deps
npm run build # build the React client (the SPA the window loads)
npm run desktop:install # install Electron + electron-builder into desktop/
# macOS (run on macOS):
npm run desktop:dmg:arm64 # fast single-arch DMG → desktop/release/
# Windows (run on Windows):
npm run desktop:win # NSIS installer .exe → desktop/release/
```
The artifact lands in `desktop/release/`. Pick the build command that matches your goal:
| Command | Platform / Architecture | Speed | Use when |
|---|---|---|---|
| `npm run desktop:dmg` | macOS — both per-arch DMGs (arm64 + x64) | **Slower** | Building the release DMGs for everyone |
| `npm run desktop:dmg:arm64` | macOS — Apple Silicon only | Fast (~1 min) | Building for your own Apple Silicon Mac |
| `npm run desktop:dmg:x64` | macOS — Intel only | Fast (~1 min) | Building for your own Intel Mac |
| `npm run desktop:dmg:universal` | macOS — one merged universal DMG (arm64 + x86_64) | **Slowest** | Hand-distributing a single file that runs on any Mac (not what the release ships) |
| `npm run desktop:win` | Windows — NSIS installer `.exe` (x64) | — | Building the per-user installer |
| `npm run desktop:win:portable` | Windows — portable `.exe` (x64) | — | Building the no-install portable build |
| `npm run desktop:install` | — | — | Install Electron + electron-builder deps; preflights the native `better-sqlite3` build and prints actionable setup help on failure |
| `npm run desktop:build` | — | — | TypeScript compile only (`out/`) |
| `npm run desktop:dev` | — | — | Build, then launch Electron locally |
| `npm run desktop:test` | — | — | Smoke test (spawn Electron, probe `/api/health`) |
> [!IMPORTANT]
> **DMGs build on macOS; Windows `.exe`s build on Windows** — electron-builder packages for the host OS. On macOS, `npm run desktop:dmg` builds the app **twice** (one tree per architecture) and emits **both** per-arch DMGs (`arm64` + `x64`) — the release build. It does **not** merge them into a single universal binary; the two DMGs are what ship. **When building for your own Mac, use `desktop:dmg:arm64` or `desktop:dmg:x64`** — a single architecture finishes in roughly a minute. CI already builds both DMGs and the Windows `.exe`s for you (see Way 1).
### Install the app
**macOS.** Each `desktop:dmg*` build wipes `release/` first. `desktop:dmg:arm64`
`…-arm64.dmg` and `desktop:dmg:x64``…-x64.dmg` each emit a single DMG whose
mounted-volume title states the architecture (e.g. *Claude Code Monitor (Apple
Silicon)*); `desktop:dmg` emits **both** (`…-arm64.dmg` + `…-x64.dmg`) for
release. Install the one matching your Mac: an x64 build on Apple Silicon makes
macOS prompt for Rosetta.
```bash
open desktop/release/ClaudeCodeMonitor-*-arm64.dmg # the arch you built
```
1. The DMG mounts — drag **Claude Code Monitor** into your `Applications` folder.
2. The DMG is ad-hoc signed, so macOS Gatekeeper shows a warning (*"Apple could not verify…"*) on first launch. Strip the quarantine attribute, then open it:
```bash
xattr -cr "/Applications/Claude Code Monitor.app"
open "/Applications/Claude Code Monitor.app"
```
Alternatively, open → *System Settings → Privacy & Security* and click *Open Anyway*.
**Windows.**
1. Run `ClaudeCodeMonitor-Setup-<ver>-x64.exe`. It installs **per-user** (no administrator elevation) and lets you pick the install directory — or run the `*-portable.exe` to launch without installing.
2. The installer is **unsigned** by default, so Windows **SmartScreen** may show *"Windows protected your PC"* on first launch — click **More info → Run anyway**.
3. Launch from the Start menu / desktop shortcut.
Once running, the embedded server boots on port `4820` (or adopts an already-healthy server on `4820`, or falls back to `4821``4829` / a random high port), the menu-bar / notification-area (tray) icon appears, and the dashboard window opens. **Hooks are installed automatically on first boot** — an install-only user does not need `npm run install-hooks`; just start a new Claude Code session. Closing the window hides it but keeps the server running; **Quit** from the tray exits.
> [!NOTE]
> The packaged app stores its SQLite database and VAPID keys in a per-user app-data directory **outside** the app bundle / install dir — `~/Library/Application Support/Claude Code Monitor/data/` on macOS, `%APPDATA%\Claude Code Monitor\data\` on Windows. Your imported history and events therefore **survive app reinstalls and updates** (the Windows NSIS uninstaller keeps this data by default). (Older macOS builds kept the database inside the bundle, which is read-only once installed and code-signed — that broke History Import; it is now fixed. If you are upgrading from a pre-fix build, there is a one-time data gap: re-run **Settings → Import History → Rescan** once.)
Full user guide: [`DESKTOP.md`](DESKTOP.md). Contributor / architecture reference: [`desktop/README.md`](desktop/README.md). Desktop-specific setup details (logs, auto-start, port adoption) are in [SETUP.md → Desktop App Setup](./SETUP.md#desktop-app-setup).
---
## Optional: Local MCP server
If you want AI agents to call dashboard functionality through MCP tools, run the local MCP server in `mcp/`:
```bash
npm run mcp:install
npm run mcp:build
npm run mcp:start # stdio (for MCP host integration)
npm run mcp:start:http # HTTP + SSE server on port 8819
npm run mcp:start:repl # interactive CLI with tab completion
```
The MCP server supports three transport modes:
```mermaid
graph LR
HOST["MCP Host<br/>(Claude Code / Desktop)"] -->|"stdin/stdout"| STDIO["stdio mode<br/>mcp:start"]
RC["Remote Client"] -->|"POST /mcp<br/>GET /sse"| HTTP["HTTP mode<br/>mcp:start:http<br/>:8819"]
OP["Operator"] -->|"interactive CLI"| REPL["REPL mode<br/>mcp:start:repl"]
STDIO --> API["Dashboard API<br/>http://127.0.0.1:4820/api/*"]
HTTP --> API
REPL --> API
style STDIO fill:#6366f1,stroke:#818cf8,color:#fff
style HTTP fill:#f59e0b,stroke:#fbbf24,color:#000
style REPL fill:#a855f7,stroke:#c084fc,color:#fff
```
See [mcp/README.md](./mcp/README.md) for host config, tool catalog, and safety flags.
To build the MCP server as a container image instead:
```bash
npm run mcp:docker:build
# or
npm run mcp:podman:build
```
---
## Optional: Agent extension packs
This repository includes extension packs for both Claude Code and Codex.
- Claude Code loads project extensions from:
- `CLAUDE.md`
- `.claude/rules/`
- `.claude/skills/`
- `.claude/agents/`
- Codex project packs live under `.codex/`:
- `AGENTS.md`
- `.codex/rules/`
- `.codex/agents/`
- `.codex/skills/`
See [`.codex/README.md`](./.codex/README.md) for Codex extension details.
---
## Optional: VS Code extension
The **Claude Code Agent Monitor** is also available as a dedicated VS Code extension for seamless, integrated monitoring.
<p align="center">
<img src="vscode-extension/vscode.png" alt="VS Code Extension Screenshot" width="100%">
</p>
### Features
- **Real-time Sidebar**: Monitor agent status, health, and usage stats in the Activity Bar.
- **Pulse Status Bar**: High-level session and agent counts in the bottom status bar.
- **Direct Navigation**: Jump to specific dashboard pages or recent sessions.
- **Embedded Dashboard**: Full dashboard interface within a native VS Code tab.
### Installation
1. Open the [vscode-extension](./vscode-extension) folder in VS Code.
2. Install via the Marketplace or package it manually:
```bash
cd vscode-extension
npm install
# Generate .vsix for local install
npm run package
```
3. After installation, ensure the main dashboard server is running (`npm run dev`).
4. Look for the **Radar icon** in your VS Code Activity Bar.
For advanced configuration, refer to the [.vscode](./.vscode) and [vscode-extension](./vscode-extension) directories.
> [!TIP]
> Extension on VS Code Marketplace: [Claude Code Agent Monitor](https://marketplace.visualstudio.com/items?itemName=hoangsonw.claude-code-agent-monitor)
---
## Container mode (Docker / Podman)
The repository includes both a multi-stage `Dockerfile` and a `docker-compose.yml` file. Docker and Podman are both supported.
### Compose
```bash
# Docker Compose
docker compose up -d --build
# Podman Compose
CLAUDE_HOME="$HOME/.claude" podman compose up -d --build
```
Open **http://localhost:4820** in your browser.
### Plain Docker / Podman
```bash
# Docker
docker build -t agent-monitor .
docker run -d --name agent-monitor \
-p 127.0.0.1:4820:4820 \
-v "$HOME/.claude:/root/.claude:ro" \
-v agent-monitor-data:/app/data \
agent-monitor
# Podman
podman build -t agent-monitor .
podman run -d --name agent-monitor \
-p 127.0.0.1:4820:4820 \
-v "$HOME/.claude:/root/.claude:ro" \
-v "$HOME/.claude/agent-dashboard:/app/data" \
agent-monitor
```
### Container notes
| Mount | Purpose |
|---|---|
| `~/.claude:/root/.claude:ro` | Lets the server import legacy Claude session history |
| `~/.claude/agent-dashboard:/app/data` | **Canonical SQLite database** (shared with native installs) |
> [!NOTE]
> The image sets `DASHBOARD_HOST=0.0.0.0` and `DASHBOARD_DATA_DIR=/app/data` internally (baked into the `Dockerfile`). The `0.0.0.0` bind is required because a container's loopback is a separate namespace the published port cannot reach; `/app/data` is bind-mounted to `~/.claude/agent-dashboard` on the host so Docker uses the same database as `npm start`. The trust boundary is the **host** port publish — the examples use `-p 127.0.0.1:4820:4820`, so the dashboard is local-only. To expose it on a LAN, publish on `0.0.0.0` (`-p 4820:4820`) **and** set `DASHBOARD_TOKEN`.
> [!IMPORTANT]
> Claude Code hooks run on the host, not inside the container. After the container is healthy on `http://localhost:4820`, run `npm run install-hooks` on the host so Claude Code posts hook events back to the containerized server. The installer refuses to run inside a container (issue #193) to avoid writing a container-internal handler path into a bind-mounted `~/.claude`; use `CCAM_ALLOW_CONTAINER_HOOKS=1` only if you run Claude Code inside the same container.
---
## Troubleshooting
### `npm run setup` shows `better-sqlite3` errors
This is expected and **non-fatal**. `better-sqlite3` is a native C++ module listed as an optional dependency. If prebuilt binaries are not available for your Node version or platform, npm will print gyp/compilation errors but still complete successfully.
At runtime the server uses this fallback chain:
1. **`better-sqlite3`** — used when prebuilt binaries are available (Node 20/22/24 on Windows x64, macOS arm64/x64, Linux x64/arm64)
2. **`node:sqlite`** — Node.js built-in SQLite module, used automatically on Node 22+ when `better-sqlite3` is unavailable
If you see an error box at startup saying *"SQLite backend not available"*, either:
- **Upgrade to Node.js 22+** (recommended — zero native dependencies needed), or
- **Install build tools** so `better-sqlite3` can compile from source:
- **Windows:** `npm install -g windows-build-tools` or install [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) with the C++ workload
- **macOS:** `xcode-select --install`
- **Linux:** `sudo apt install python3 make g++` (Debian/Ubuntu) or equivalent
Then run: `npm rebuild better-sqlite3`
### Desktop build or install fails on the native dependency
Unlike the root server (which falls back to `node:sqlite`), the desktop app **requires** `better-sqlite3` built for Electron's ABI. If that build can't happen, `npm run desktop:install` (and the desktop `prebuild` gate that runs before every `desktop:*` build) now stops with copy-pasteable setup help instead of a raw node-gyp trace or a runtime crash: it lists the per-OS C++ toolchain prerequisites (Windows: Visual Studio Build Tools + "Desktop development with C++"; macOS: `xcode-select --install`; Linux: build-essential + python3), notes that Node LTS 20/22 ship prebuilt binaries, and offers a no-toolchain alternative:
```bash
cd desktop
npm install --ignore-scripts
node node_modules/electron/install.js
npx electron-builder install-app-deps
```
### `npm run dev` fails immediately
Ensure both server and client dependencies are installed:
```bash
npm run setup
```
If the error mentions a missing module like `express` or `react`, dependencies may be incomplete. Delete `node_modules` in both root and `client/`, then re-run setup:
```bash
rm -rf node_modules client/node_modules
npm run setup
```
### Server starts but client shows a blank page
The Vite dev server and Express server run on different ports. Make sure both are running (`npm run dev` starts both). Open **http://localhost:5173**, not `http://localhost:4820`, during development.
### No sessions appearing after starting Claude Code
See [SETUP.md — Troubleshooting](./SETUP.md#troubleshooting) for detailed hook debugging steps.
### Desktop App (macOS & Windows) issues
| Symptom | Cause | Fix |
|---|---|---|
| *"Apple could not verify…"* on first launch (macOS) | The DMG is ad-hoc signed (no paid Apple Developer ID) | `xattr -cr "/Applications/Claude Code Monitor.app"`, then open it — or use *System Settings → Privacy & Security → Open Anyway* |
| *"Windows protected your PC"* on first launch (Windows) | The `.exe` is unsigned by default, so SmartScreen prompts | Click **More info → Run anyway** |
| `npm run desktop:dmg` seems slow (macOS) | Not hung — it packages two architectures back-to-back (`arch=x64` then `arch=arm64`) | Wait it out, or use `npm run desktop:dmg:arm64` / `npm run desktop:dmg:x64` for a fast single-arch build |
| `entry file out/main.js does not exist` | `npm run clean` (in `desktop/`) deleted `out/`; `electron-builder` only packages, it does not compile | Re-run `npm run desktop:build` (or just use a `desktop:dmg*` / `desktop:win*` script, which chains the build) |
| Desktop window opens but is blank | The embedded server failed `/api/health` within 30 s | Check the desktop log (`~/Library/Logs/Claude Code Monitor/desktop.log` on macOS, `%APPDATA%\Claude Code Monitor\logs\desktop.log` on Windows), then tray → *Restart Server* |
| "Run Claude" says `claude` is not on your PATH | A Finder/Dock-launched macOS app only inherits launchd's minimal PATH, not your login-shell PATH (on Windows the process already inherits the user PATH) | The app recovers your login-shell PATH at startup so it can find and spawn the `claude` CLI. If it still fails, make sure `claude` is a real executable on your shell PATH — not a shell alias or function |
| Imported history vanished after updating the app | Older builds stored the database inside the (replaceable) `.app` bundle | Fixed — data now lives in the per-user app-data dir (`~/Library/Application Support/Claude Code Monitor/data/` on macOS, `%APPDATA%\Claude Code Monitor\data\` on Windows) and survives reinstalls/updates. After upgrading from a pre-fix build, re-run **Settings → Import History → Rescan** once |
---
## Ports
| Service | Default | Override |
|---|---|---|
| Dashboard server | `4820` | `DASHBOARD_PORT=xxxx npm run dev` |
| Client dev server | `5173` | Edit `client/vite.config.ts` |
+156
View File
@@ -0,0 +1,156 @@
# ─────────────────────────────────────────────────────────────────────────────
# Claude Code Agent Monitor — Makefile
# A collection of convenient commands for development, testing, and deployment.
# Prerequisites: GNU Make, Node.js, npm
# Author: Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
# Quickstart:
# make setup # Install all dependencies
# make dev # Start server + client in watch mode
# make build # Build the React client for production
# make start # Start the production server
# make test # Run all tests
# make format # Format all files with Prettier
# make mcp-dev # Start MCP server in watch mode
# make seed # Load deterministic demo data
# For more commands, run `make help` to see descriptions, or see below.
# ─────────────────────────────────────────────────────────────────────────────
.DEFAULT_GOAL := help
SHELL := /bin/bash
# ─── Setup ──────────────────────────────────────────────────────────────────
.PHONY: setup
setup: ## Install all dependencies (root + client + MCP)
npm install
cd client && npm install
npm --prefix mcp install
.PHONY: install-hooks
install-hooks: ## Register Claude Code hooks in ~/.claude/settings.json
node scripts/install-hooks.js
# ─── Development ────────────────────────────────────────────────────────────
.PHONY: dev
dev: ## Start server + client in watch mode (concurrent)
npx concurrently -n server,client -c blue,green "npm run dev:server" "npm run dev:client"
.PHONY: dev-server
dev-server: ## Start only the Express server in watch mode
node --watch server/index.js
.PHONY: dev-client
dev-client: ## Start only the Vite dev server
cd client && npm run dev
# ─── Production ─────────────────────────────────────────────────────────────
.PHONY: build
build: ## Build the React client for production
cd client && npm run build
.PHONY: start
start: ## Start the production server (build first)
node server/index.js
.PHONY: prod
prod: build start ## Build client then start production server
# ─── Testing ────────────────────────────────────────────────────────────────
.PHONY: test
test: ## Run all tests (server + client)
node --test server/__tests__/*.test.js
cd client && npm test
.PHONY: test-server
test-server: ## Run server tests only
node --test server/__tests__/*.test.js
.PHONY: test-client
test-client: ## Run client tests only
cd client && npm test
# ─── Formatting ─────────────────────────────────────────────────────────────
.PHONY: format
format: ## Format all files with Prettier
npx prettier --write .
.PHONY: format-check
format-check: ## Check formatting without writing
npx prettier --check .
# ─── MCP Server ─────────────────────────────────────────────────────────────
.PHONY: mcp-install
mcp-install: ## Install MCP server dependencies
npm --prefix mcp install
.PHONY: mcp-build
mcp-build: ## Compile MCP TypeScript → JavaScript
npm --prefix mcp run build
.PHONY: mcp-typecheck
mcp-typecheck: ## Type-check MCP source without emitting
npm --prefix mcp run typecheck
.PHONY: mcp-start
mcp-start: ## Start the MCP stdio server
npm --prefix mcp run start
.PHONY: mcp-dev
mcp-dev: ## Start MCP server in watch mode
npm --prefix mcp run dev
# ─── Data Management ───────────────────────────────────────────────────────
.PHONY: seed
seed: ## Load deterministic demo data
node scripts/seed.js
.PHONY: import-history
import-history: ## Import sessions from ~/.claude/ history
node scripts/import-history.js
.PHONY: clear-data
clear-data: ## Delete all data rows (preserves schema)
node scripts/clear-data.js
# ─── Docker / Podman ───────────────────────────────────────────────────────
.PHONY: docker-build
docker-build: ## Build dashboard Docker image
docker build -t claude-agent-monitor:local .
.PHONY: docker-up
docker-up: ## Start via docker-compose
docker compose up -d
.PHONY: docker-down
docker-down: ## Stop docker-compose stack
docker compose down
.PHONY: docker-logs
docker-logs: ## Tail docker-compose logs
docker compose logs -f
.PHONY: mcp-docker-build
mcp-docker-build: ## Build MCP Docker image
docker build -f mcp/Dockerfile -t agent-dashboard-mcp:local .
.PHONY: podman-build
podman-build: ## Build dashboard Podman image
podman build -t localhost/claude-agent-monitor:local .
.PHONY: mcp-podman-build
mcp-podman-build: ## Build MCP Podman image
podman build -f mcp/Dockerfile -t localhost/agent-dashboard-mcp:local .
# ─── Help ───────────────────────────────────────────────────────────────────
.PHONY: help
help: ## Show this help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}'
+114
View File
@@ -0,0 +1,114 @@
# Claude Code Monitor
Internal SmartGift build. Local-first dashboard for Claude Code: hooks POST every
tool call to an Express + SQLite server, a React UI updates over WebSocket, and
**lanes** track parallel agent work through a pipeline.
Internal build — all rights reserved.
## What it does
- **Sessions, agents, events.** Everything Claude Code emits, recorded and
searchable: tool calls, token usage, cost, subagent trees, transcripts.
- **Lanes.** One lane per working directory, surviving session restarts. A lane
moves through pipeline stages and the dashboard shows where it is.
- **Stage detection.** The stage is inferred from the tool stream, so a session
that never calls `ccam stage` still shows progress — rendered dashed amber and
never as done, because an inference is not evidence.
- **Run Claude from the browser.** Spawn a session in a lane's directory, stream
its output, send follow-ups, resume any past session.
- **Analytics, alerts, Kanban and a workflow view**, plus an MCP server and a CLI.
## Requirements
Node **>= 20** (`engines` in `package.json`). Node **24** is what the test suites
are verified on — node 25 currently breaks 6 server tests through a
better-sqlite3 ABI mismatch and 20 client tests through a global `localStorage`
change.
## Install and run
```bash
npm run setup # root, client and vscode-extension dependencies
npm run build # builds the client into client/dist
npm start # serves the built client and the API on :4820
```
Open <http://localhost:4820>.
Development, with hot reload:
```bash
npm run dev # server on :4820, Vite client on :5173
```
`DASHBOARD_PORT` overrides the port. `postinstall` writes the Claude Code hook
entries that feed the dashboard.
## The CLI
`ccam` is linked by `npm run setup`; otherwise call `node bin/ccam.js`.
```bash
ccam status # is the dashboard up
ccam start # start it in the background and wait for healthy
ccam sessions # recent sessions
ccam lanes # lanes with stage and progress
ccam stage <name> # declare the current lane's stage
ccam tail # live event feed
```
`ccam --help` lists the rest.
## Lanes
A lane is a working directory the dashboard watches. Two kinds:
- **adopted** — a directory you already had. The dashboard only reads it; it is
never reset or deleted.
- **managed** — a git worktree the dashboard created under `LANES_ROOT`. It owns
the full lifecycle and may reset or remove it, behind a three-check destroy
guard and a counted preflight the caller has to echo back.
```bash
ccam lanes add --cwd /path/to/repo --title "My feature" # adopt
ccam lanes add --repo /path/to/repo --slug my-feature # managed worktree
```
The declared stage comes from `ccam stage`. The inferred stage comes from tool
events and expires after `DETECTION_TTL_MS` (default 30 minutes), so a lane can
move backwards between work sessions. Detection never writes the declared stage,
and an inferred node never renders as done.
[`docs/LANES.md`](docs/LANES.md) has the pipeline model, the destroy guard, the
preflight contract, the Workspace page, and `GET /api/lanes/:id/git`.
## Tests
```bash
npm run test:server # node:test
npm run test:client # Vitest
```
Both must be green before a commit; the pre-commit hook runs them plus Prettier.
## Layout
| Path | What |
|---|---|
| `server/` | Express API, SQLite schema, hook ingest, lane and worktree libraries |
| `client/` | React 18 + Vite + Tailwind dashboard |
| `bin/ccam.js` | CLI |
| `mcp/` | MCP server exposing read-only dashboard tools |
| `desktop/` | Electron wrapper that embeds the server |
| `docs/` | Architecture, API, lanes, database, deployment |
| `plugins/` | Claude Code plugins shipped with the dashboard |
## Docs
- [`ARCHITECTURE.md`](ARCHITECTURE.md) — request flow, schema, WebSocket surface
- [`docs/LANES.md`](docs/LANES.md) — lanes, pipelines, stage detection
- [`docs/API.md`](docs/API.md) — REST endpoints (`openapi.yaml` is generated)
- [`docs/DATABASE.md`](docs/DATABASE.md) — tables and migrations
- [`INSTALL.md`](INSTALL.md) · [`DEPLOYMENT.md`](DEPLOYMENT.md) · [`DESKTOP.md`](DESKTOP.md)
- [`CLAUDE.md`](CLAUDE.md) — the rules an agent working in this repo must follow
+653
View File
@@ -0,0 +1,653 @@
# Setup Guide
A comprehensive guide to setting up and configuring the Agent Dashboard, including how it integrates with Claude Code, environment variables, container deployment, and troubleshooting common issues.
## How it works
Agent Dashboard integrates with Claude Code through its native hook system. When Claude Code performs any action (session start, tool use, turn completion, subagent finish, session exit), it fires a hook that calls a small Node.js script bundled with this project. That script forwards the event over HTTP to the dashboard server, which stores it in SQLite and broadcasts it to the browser over WebSocket.
```
Claude Code → hook fires → hook-handler.js → POST /api/hooks/event
Browser ← WebSocket broadcast ← Express server ← SQLite
```
No extra Claude Code configuration is required in the normal host-run path — when you start the dashboard with `npm run dev` or `npm start`, the server configures the hooks automatically on startup. Container deployments are the exception: after the container is up, run `npm run install-hooks` on the host so Claude Code points at `http://localhost:4820`.
---
## Configuration
### Hook auto-installation
When the dashboard is running directly on the host, the server writes the following to `~/.claude/settings.json` every time it starts:
```json
{
"hooks": {
"SessionStart": [{ "hooks": [{ "type": "command", "command": "node \"/path/to/scripts/hook-handler.js\" SessionStart" }] }],
"PreToolUse": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "node \"/path/to/scripts/hook-handler.js\" PreToolUse" }] }],
"PostToolUse": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "node \"/path/to/scripts/hook-handler.js\" PostToolUse" }] }],
"Stop": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "node \"/path/to/scripts/hook-handler.js\" Stop" }] }],
"SubagentStop": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "node \"/path/to/scripts/hook-handler.js\" SubagentStop" }] }],
"Notification": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "node \"/path/to/scripts/hook-handler.js\" Notification" }] }],
"SessionEnd": [{ "hooks": [{ "type": "command", "command": "node \"/path/to/scripts/hook-handler.js\" SessionEnd" }] }]
}
}
```
> [!NOTE]
> Note: `SessionStart` and `SessionEnd` hooks do not support the `matcher` field — they fire unconditionally on every session start and exit.
Existing hooks in that file are preserved. The dashboard only adds or updates entries that contain `hook-handler.js`.
To re-run hook installation manually:
```bash
npm run install-hooks
```
> [!TIP]
> Container note: do not rely on hook auto-install from inside Docker or Podman. The hook path written by a container would point at the container filesystem, not the host. Start the container first, then run `npm run install-hooks` on the host. As a safeguard (issue #193), the installer now **detects container execution and refuses to run** (exiting non-zero) so it can never poison a bind-mounted host `~/.claude`; the containerized server logs the same guidance instead of silently writing a bad path. If you genuinely run Claude Code inside the same container, override with `CCAM_ALLOW_CONTAINER_HOOKS=1 npm run install-hooks`.
> [!NOTE]
> Prefer a ready-made dev environment? This repo ships an **optional** Dev Container (`.devcontainer/`) for VS Code / GitHub Codespaces — Node 22, native build tools for `better-sqlite3`, Python, and ports `4820`/`5173` preconfigured. It's purely opt-in and changes nothing for host-based development. See [`.devcontainer/README.md`](.devcontainer/README.md). (Hooks remain host-side there too.)
### Container runtime (Docker / Podman)
The repo includes both a multi-stage `Dockerfile` and a `docker-compose.yml` file. The container image serves the built client and API on port `4820`, stores SQLite data under `/app/data`, and can import legacy Claude history from a read-only `~/.claude` mount.
```bash
# Docker Compose
docker compose up -d --build
# Podman Compose
CLAUDE_HOME="$HOME/.claude" podman compose up -d --build
# Plain Docker
docker build -t agent-monitor .
docker run -d --name agent-monitor \
-p 127.0.0.1:4820:4820 \
-v "$HOME/.claude:/root/.claude:ro" \
-v agent-monitor-data:/app/data \
agent-monitor
# Plain Podman
podman build -t agent-monitor .
podman run -d --name agent-monitor \
-p 127.0.0.1:4820:4820 \
-v "$HOME/.claude:/root/.claude:ro" \
-v "$HOME/.claude/agent-dashboard:/app/data" \
agent-monitor
```
Container-specific behavior:
- The dashboard is available at `http://localhost:4820`
- The image sets `DASHBOARD_HOST=0.0.0.0` (bind inside the container — its loopback is a separate namespace the published port cannot reach) and `DASHBOARD_DATA_DIR=/app/data` internally; both are baked into the `Dockerfile`
- The examples publish on `127.0.0.1` only, so the dashboard is local-only. To expose it on a LAN, publish on `0.0.0.0` (`-p 4820:4820`) **and** set `DASHBOARD_TOKEN`
- `~/.claude:/root/.claude:ro` is used for history import only
- `~/.claude/agent-dashboard:/app/data` is the **canonical** SQLite database (shared with native installs)
- Claude Code hooks still execute on the host, so install them from the host with `npm run install-hooks`
### Environment variables
| Variable | Default | Description |
|---|---|---|
| `DASHBOARD_PORT` | `4820` | Port the Express server listens on |
| `CLAUDE_DASHBOARD_PORT` | `4820` | Port the hook handler uses when posting events to the dashboard |
| `DASHBOARD_DB_PATH` | `data/dashboard.db` | Path to the SQLite database file |
| `NODE_ENV` | `development` | Set to `production` to serve built client |
| `CCAM_IMPORT_MAX_BYTES` | `1073741824` (1 GB) | Maximum size per uploaded file on `/api/import/upload` |
| `CCAM_IMPORT_MAX_FILES` | `2000` | Maximum number of files per upload request |
| `CCAM_IMPORT_MAX_EXTRACT_BYTES` | `4294967296` (4 GB) | Maximum uncompressed bytes any single archive is allowed to expand to (zip-bomb defense) |
| `MCP_DASHBOARD_BASE_URL` | `http://127.0.0.1:4820` | Base URL used by the local MCP server to call dashboard APIs |
| `MCP_DASHBOARD_ALLOW_MUTATIONS` | `false` | Enables mutating MCP tools |
| `MCP_DASHBOARD_ALLOW_DESTRUCTIVE` | `false` | Enables destructive MCP tools (in addition to mutations) |
| `MCP_TRANSPORT` | `stdio` | MCP transport mode: `stdio`, `http`, `repl` |
| `MCP_HTTP_PORT` | `8819` | Port for the MCP HTTP+SSE server (only when `MCP_TRANSPORT=http`) |
| `MCP_HTTP_HOST` | `127.0.0.1` | Bind address for the MCP HTTP server |
Example with a custom port:
```bash
DASHBOARD_PORT=9000 npm run dev
```
> [!NOTE]
> You usually do **not** need to set `DASHBOARD_PORT` manually. `npm run dev` is wrapped by `scripts/dev.js`, which probes both `127.0.0.1` and `::1` (so an SSH `LocalForward` bound to one loopback can't slip past) and picks the first free port in `48204859` automatically. The chosen port is propagated to the Vite dev proxy via `DASHBOARD_PORT`, and the Express server writes it to `~/.claude/.agent-dashboard.json` so the Claude Code hook handler discovers it without any env var.
>
> Multiple dashboards can run side by side — for example `npm run dev` and the desktop app (macOS or Windows) at the same time. Each one appends its `{port, pid, startedAt}` entry to the discovery file, and `scripts/hook-handler.js` fan-outs every hook event to every live entry, so both UIs keep their real-time stream.
>
> Setting `CLAUDE_DASHBOARD_PORT=N` overrides discovery entirely and forces the hook handler to a single port — useful for tests and container setups where the in-process discovery file isn't reachable from the host.
>
> If you bypass the picker (e.g. `npm run dev:raw`, container builds, or anything else that calls `node server/index.js` directly), make sure your client is built / proxied against the port the server actually bound.
### MCP server (optional)
The project includes a local MCP server under `mcp/` so AI agents can call dashboard operations through standardized tools. It supports three transport modes: stdio for MCP host integration, HTTP+SSE for networked clients, and an interactive REPL for operator debugging.
```mermaid
graph LR
subgraph "MCP Transport Modes"
STDIO["stdio\n(default)"]
HTTP["HTTP + SSE\n(:8819)"]
REPL["Interactive REPL"]
end
HOST["MCP Host"] -->|"stdin/stdout"| STDIO
RC["Remote Client"] -->|"POST /mcp · GET /sse"| HTTP
OP["Operator"] -->|"interactive CLI"| REPL
STDIO --> API["Dashboard API<br/>http://127.0.0.1:4820/api/*"]
HTTP --> API
REPL --> API
style STDIO fill:#6366f1,stroke:#818cf8,color:#fff
style HTTP fill:#f59e0b,stroke:#fbbf24,color:#000
style REPL fill:#a855f7,stroke:#c084fc,color:#fff
```
Quick start:
```bash
npm run mcp:install
npm run mcp:build
npm run mcp:start # stdio (for Claude Code / Claude Desktop)
npm run mcp:start:http # HTTP + SSE server on port 8819
npm run mcp:start:repl # interactive CLI with tab completion
```
For full host config and tool catalog, see [mcp/README.md](./mcp/README.md).
### Agent extension setup (Claude Code + Codex)
This repository ships extension files for both agent ecosystems:
- Claude Code:
- `CLAUDE.md`
- `.claude/rules/*`
- `.claude/skills/*`
- `.claude/agents/*`
- Codex:
- `AGENTS.md`
- `.codex/config.toml`
- `.codex/rules/default.rules`
- `.codex/agents/*`
- `.codex/skills/*`
See [`.codex/README.md`](./.codex/README.md) for Codex extension details.
### VS Code extension setup
The **Claude Code Agent Monitor** is available as an integrated VS Code extension for seamless monitoring within your editor.
- **Activity Bar View**: Adds a custom "Radar" icon to the activity bar providing real-time agent health, token counts, and session stats.
- **Status Bar Integration**: Displays live session and agent pulse counts in the bottom bar.
- **Embedded Dashboard**: Renders the full web dashboard directly in a VS Code editor tab.
- **Automated Detection**: Automatically finds your dashboard server on ports `5173` or `4820`.
<p align="center">
<img src="vscode-extension/vscode.png" alt="VS Code Extension Screenshot" width="100%">
</p>
To install or develop the extension:
1. Open the [vscode-extension](./vscode-extension) directory in VS Code.
2. Run `npm install` and `npm run package` to generate a local `.vsix` installer.
3. For developer details, see [vscode-extension/README.md](./vscode-extension/README.md).
> [!TIP]
> Extension on VS Code Marketplace: [Claude Code Agent Monitor](https://marketplace.visualstudio.com/items?itemName=hoangsonw.claude-code-agent-monitor)
### PWA configuration (optional)
The dashboard, landing page, and wiki each ship as independent Progressive Web Apps. No configuration is required — manifests and service workers are included out of the box.
**Customising the manifest:** Edit the `manifest.json` in the relevant directory (`client/public/` for dashboard, root for landing, `wiki/` for wiki). Common fields to change:
- `name` / `short_name` — displayed on the home screen / dock
- `theme_color` — address bar / title bar tint (default: `#6366f1`)
- `background_color` — splash screen background
- `start_url` — entry point when launched from home screen
**Updating the service worker cache:** Each SW has a `CACHE_NAME` constant (e.g. `dashboard-v2`). After deploying new assets, bump the version string to force browsers to re-fetch — though for the dashboard this is rarely needed: hashed `/assets/*` URLs are immutable per build, everything else is fetched network-first with cache fallback, and a `controllerchange` listener in the client reloads the page exactly once when a new SW takes over, so a rebuild propagates without a hard refresh.
**Browser support:** PWA install prompts appear in Chrome 107+, Edge 107+, and Firefox 110+ (desktop and Android). Safari supports `apple-mobile-web-app-capable` for iOS home-screen mode but does not show an install banner.
**Verifying PWA status:** Open DevTools → Application → Manifest to confirm the manifest loads. Check the Service Workers section to verify the SW is registered and active. The Lighthouse PWA audit should pass all core checks.
### Desktop App Setup
The `desktop/` workspace ships the dashboard as a **native desktop app** for both **macOS** (a `.app` distributed as a `.dmg`) and **Windows** (an `.exe` — an NSIS installer plus a no-install portable build), built with Electron 35. It is an Electron shell that **embeds the existing Express server in-process** — it does not reimplement anything. For installation (download a pre-built installer from the [latest GitHub Release](https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor/releases/latest) or the per-commit `ClaudeCodeMonitor-dmg` / `ClaudeCodeMonitor-win` CI artifact, or build one locally — then on macOS mount, drag, Gatekeeper bypass; on Windows run the installer / portable, SmartScreen bypass), see [INSTALL.md → Desktop App (macOS & Windows)](./INSTALL.md#desktop-app-macos--windows-optional). The full user guide is [`DESKTOP.md`](./DESKTOP.md); the contributor / architecture reference is [`desktop/README.md`](./desktop/README.md).
This section covers the parts of running the desktop app that matter for setup.
**Building and running.** All commands run from the repo root. electron-builder packages for the **host OS** — build the macOS DMG on a Mac (`desktop:dmg*`) and the Windows `.exe` on Windows (`desktop:win*`):
| Script | Command | Description |
|---|---|---|
| `desktop:install` | `npm run desktop:install` | Install Electron + electron-builder into `desktop/`; fetches `better-sqlite3` as a prebuilt Electron binary for Electron's ABI (no Visual Studio C++ toolchain needed in the common case; on macOS, Xcode CLI tools cover any fallback build). Preflights the native `better-sqlite3` build; on failure prints actionable per-OS setup help plus a no-toolchain alternative and exits non-zero (also enforced by the desktop `prebuild` gate) |
| `desktop:build` | `npm run desktop:build` | Prebuild guard + `tsc``desktop/out/` |
| `desktop:dev` | `npm run desktop:dev` | Build, then launch Electron against `out/main.js` |
| `desktop:test` | `npm run desktop:test` | Build, then run the smoke test (spawn Electron, probe `/api/health`) |
| `desktop:dmg` | `npm run desktop:dmg` | **macOS****both** per-arch DMGs (arm64 + x64) → `desktop/release/`. Correct for release. **Slower** (packages each arch). |
| `desktop:dmg:arm64` | `npm run desktop:dmg:arm64` | **macOS** — Apple-Silicon-only DMG → `desktop/release/`. **Fast (~1 min).** |
| `desktop:dmg:x64` | `npm run desktop:dmg:x64` | **macOS** — Intel-only DMG → `desktop/release/`. **Fast (~1 min).** |
| `desktop:win` | `npm run desktop:win` | **Windows** — NSIS installer `.exe` (x64) → `desktop/release/`. |
| `desktop:win:portable` | `npm run desktop:win:portable` | **Windows** — no-install portable `.exe` (x64) → `desktop/release/`. |
> [!NOTE]
> Every `desktop:dmg*` / `desktop:win*` script chains `npm run build` first. Running `electron-builder` bare skips the TypeScript compile and fails with `entry file out/main.js does not exist`. `npm run clean` inside `desktop/` deletes `out/` and `release/` — after a clean you must `npm run desktop:build` again before packaging.
> [!TIP]
> On macOS, building a DMG rebuilds the native `better-sqlite3` module for the **target** architecture, which can leave it built for the wrong CPU arch for your local machine. The desktop `prebuild` step auto-heals this — it rebuilds `better-sqlite3` for the local machine on the next `desktop:build` — so `npm run desktop:dev` and `npm run desktop:test` keep working after a cross-arch DMG build with no manual `npm run desktop:install` needed.
**Hooks are auto-installed by the app.** On its first **owned-server** boot the desktop app writes the Claude Code hook configuration to `~/.claude/settings.json` itself, then starts the background services (update scheduler, `cc-watcher` config watcher, orphaned-run reconciliation) — the same `startBackgroundServices()` that `node server/index.js` runs. An install-only user (macOS or Windows) therefore never needs `npm run install-hooks` from a checkout: just **start a new Claude Code session** after the app is running. (If the app *adopts* an existing server instead of starting its own, that server already did its own hook bootstrap — see port adoption below.)
**Port-adoption behavior.** When the desktop app launches, its embedded server picks a port:
1. It prefers **`4820`**.
2. If a healthy dashboard server already answers `GET /api/health` on `4820` (for example you ran `npm start` in a terminal), the app **adopts that server** instead of double-binding — no SQLite contention. An adopted server is *not* owned by the app, so quitting the app leaves it running.
3. Otherwise it falls back to `4821``4829`, then to a random high port (`49152``49500`).
The chosen port is shown in the tray menu. The embedded server also honors the dashboard env vars in [Environment variables](#environment-variables) (`DASHBOARD_PORT` is set automatically by the desktop host).
**Data directory.** The packaged app stores its SQLite database and VAPID keys in a per-user app-data directory — `~/Library/Application Support/Claude Code Monitor/data/` on macOS, `%APPDATA%\Claude Code Monitor\data\` on Windows — **outside** the app bundle / install dir. The desktop host sets `DASHBOARD_DATA_DIR` to this per-user location automatically. Keeping writable state out of the bundle means a packaged, code-signed (and therefore read-only) `.app` never tries to write inside itself, and your imported history and events **survive app reinstalls and updates** (the Windows NSIS uninstaller keeps this data by default). (Older macOS builds kept the database inside the bundle, which broke History Import; after upgrading from a pre-fix build, re-run **Settings → Import History → Rescan** once to close the one-time data gap.)
**`claude` CLI resolution.** A Finder/Dock-launched macOS app inherits only launchd's minimal `PATH`, not your login-shell `PATH`. So the app can find and spawn the `claude` CLI for the "Run Claude" feature, the desktop host recovers your login-shell `PATH` at startup. (On Windows the process already inherits the user `PATH`, so no recovery is needed.) If "Run Claude" still reports that `claude` is not on `PATH`, make sure `claude` is a real executable on your shell `PATH` — a shell alias or function cannot be spawned.
**Auto-start at login.** Toggle *Open at Login* from the tray menu or the application menu. On macOS it registers via the first-party `SMAppService` API (Electron's `app.setLoginItemSettings`), so the entry appears under → *System Settings → General → Login Items*. On Windows it writes a per-user `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` entry, visible in *Task Manager → Startup*. When the app is launched at login, it starts **tray-only** — the dashboard window stays hidden until you click the tray icon.
**Logs.** The Electron main process has no terminal when launched from Finder / the Start menu, so it writes to a per-user log file:
```
~/Library/Logs/Claude Code Monitor/desktop.log # macOS
%APPDATA%\Claude Code Monitor\logs\desktop.log # Windows
```
Open it from the tray menu → **Show Logs**. Set `CCAM_DESKTOP_VERBOSE=1` to also mirror `info`/`warn` lines to stdout when running via `npm run desktop:dev`.
**Lifecycle reminder.** Closing the dashboard window only **hides** it — the server and tray keep running. **Quit** (⌘Q or tray → *Quit*) shuts the embedded server down gracefully and exits. Double-launching just focuses the existing window (single-instance lock); it never starts a second server.
---
## Database
The SQLite database is created automatically at `data/dashboard.db` on first run. The directory is created if it does not exist. The database uses WAL mode for concurrent reads and foreign keys for referential integrity.
### Clear all data
To remove all sessions, agents, events, and token usage (useful after running seed data or for a clean start):
```bash
npm run clear-data
```
### Data management via Settings page
The Settings page (`/settings`) provides a UI for:
- **Model Pricing** — view and edit per-model cost rates, reset to defaults, add custom models
- **Hook Configuration** — check which hooks are installed and reinstall them
- **Data Export** — download all sessions, agents, events, and pricing as a JSON file
- **Session Cleanup** — abandon stale active sessions after N hours, purge old completed sessions after N days
- **Clear All Data** — remove all sessions, agents, events, and token usage
- **Data Management** and **About** sections render with loading placeholders while server info is being fetched, so the page is always fully navigable
### Seed demo data
To populate the dashboard with sample sessions, agents, and events for UI exploration:
```bash
npm run seed
```
---
## Importing existing Claude Code history
The dashboard automatically imports sessions from `~/.claude/projects/` on
**every startup**, so if Claude Code has been used on this machine, you'll
see history immediately after the first launch. If you need to bring in
history from another machine, from a backup, or just force a rescan, use
**Settings → Import History** in the UI — it's a guided, drag-and-drop
experience with live progress.
### Pick the right mode
```mermaid
flowchart TD
Q["Where is the history?"] --> Q1{Is it on this machine<br/>under ~/.claude/projects?}
Q1 -->|yes, and I just want<br/>to re-scan| M1["Mode: Rescan default folder<br/>one click"]
Q1 -->|yes, but in another folder<br/>on this machine| M2["Mode: Scan a folder<br/>paste the absolute path"]
Q1 -->|no — it's on another machine<br/>or in an archive file| M3["Mode: Upload files<br/>drag-drop JSONL or archive"]
M3 --> PREP["Archive source:<br/>tar -czf claude-history.tar.gz<br/>-C ~/.claude projects"]
style M1 fill:#10b981,stroke:#34d399,color:#fff
style M2 fill:#f59e0b,stroke:#fbbf24,color:#000
style M3 fill:#a855f7,stroke:#c084fc,color:#fff
```
### Step-by-step: moving history from one machine to another
**On the source machine**, bundle the projects folder:
```bash
# macOS / Linux
tar -czf claude-history.tar.gz -C ~/.claude projects
# Windows (PowerShell, via built-in tar)
tar -czf claude-history.tar.gz -C "$env:USERPROFILE\.claude" projects
```
Transfer the resulting `claude-history.tar.gz` to the destination machine
however you like — AirDrop, `scp`, USB, cloud storage.
**On the destination machine**, in the dashboard:
1. Open **Settings → Import History**.
2. Pick **Upload files** (the third tab).
3. Drag the archive onto the drop zone.
4. Click **Upload & Import** and watch the progress.
5. When the green result card appears, open **Analytics → Cost** to confirm
per-model token totals and estimated cost.
### Supported inputs
Any of the following can be dropped onto the upload zone or found inside a
folder given to **Scan a folder**:
- `.jsonl` — session transcripts
- `.meta.json` — subagent metadata sidecars
- `.zip` — extracted with path-traversal protection
- `.tar`, `.tar.gz`, `.tgz` — extracted via the `tar` package
- `.gz` — single gzipped JSONL (streaming-decompressed)
### Accuracy guarantees
- **Idempotent** — re-importing never double-counts. Sessions are
deduplicated by UUID.
- **Cost-preserving** — the `token_usage` table uses `baseline_*` columns
to preserve pre-compaction token totals, so re-ingesting a compacted
transcript never erases historical cost.
- **Same parser as live**`parseSessionFile` + `importSession` is the
single source of truth for both hook-driven ingestion and manual
import, so imported numbers match captured numbers exactly.
### Safety
Archive extraction is hardened against path traversal and archive bombs.
The defaults are generous for real-world transcripts but tight enough to
stop obvious attacks; see the env vars table above for
`CCAM_IMPORT_MAX_BYTES`, `CCAM_IMPORT_MAX_FILES`, and
`CCAM_IMPORT_MAX_EXTRACT_BYTES`.
### CLI alternative
For scripts and automation, the same logic runs from the terminal:
```bash
# Import (or re-import) everything under ~/.claude/projects
npm run import-history
# Dry run — show what would be imported without writing
node scripts/import-history.js --dry-run
# Scope to a single project dir
node scripts/import-history.js --project my-project
```
---
## Scripts reference
| Script | Command | Description |
|---|---|---|
| `setup` | `npm run setup` | Install all dependencies (server + client) |
| `dev` | `npm run dev` | Start server + client in development mode |
| `start` | `npm start` | Start server in production mode |
| `build` | `npm run build` | Build the React client to `client/dist/` |
| `install-hooks` | `npm run install-hooks` | Write Claude Code hooks to `~/.claude/settings.json` |
| `clear-data` | `npm run clear-data` | Delete all data from the database |
| `seed` | `npm run seed` | Insert demo sessions/agents/events |
| `import-history` | `npm run import-history` | Import legacy sessions from `~/.claude/` (also runs on startup) |
| `mcp:install` | `npm run mcp:install` | Install MCP package dependencies |
| `mcp:build` | `npm run mcp:build` | Build MCP server into `mcp/build/` |
| `mcp:start` | `npm run mcp:start` | Start MCP server (stdio, for MCP hosts) |
| `mcp:start:http` | `npm run mcp:start:http` | Start MCP HTTP+SSE server on port 8819 |
| `mcp:start:repl` | `npm run mcp:start:repl` | Start interactive MCP REPL |
| `mcp:dev` | `npm run mcp:dev` | Start MCP server in dev mode (stdio) |
| `mcp:dev:http` | `npm run mcp:dev:http` | Start MCP HTTP server in dev mode |
| `mcp:dev:repl` | `npm run mcp:dev:repl` | Start MCP REPL in dev mode |
| `mcp:typecheck` | `npm run mcp:typecheck` | Type-check MCP source |
| `mcp:docker:build` | `npm run mcp:docker:build` | Build MCP container image with Docker |
| `mcp:podman:build` | `npm run mcp:podman:build` | Build MCP container image with Podman |
| `test:mcp` | `npm run test:mcp` | Run MCP server unit tests |
| `claude` | Claude CLI | Uses `CLAUDE.md`, `.claude/rules`, and `.claude/skills` automatically |
| `test` | `npm test` | Run all server and client tests |
| `test:server` | `npm run test:server` | Run server integration tests only |
| `test:client` | `npm run test:client` | Run client unit tests only |
| `format` | `npm run format` | Format all files with Prettier |
| `format:check` | `npm run format:check` | Check formatting without writing |
---
## Makefile targets
All npm scripts are mirrored as `make` targets for convenience. Run `make help` to list them:
```bash
make help
```
Commonly used targets:
| Make target | Equivalent npm command | Description |
|---|---|---|
| `make setup` | `npm run setup` + MCP install | Install all dependencies (root + client + MCP) |
| `make dev` | `npm run dev` | Start server + client in watch mode |
| `make build` | `npm run build` | Build the React client for production |
| `make start` | `npm start` | Start the production server |
| `make prod` | `npm run build && npm start` | Build then start in one step |
| `make test` | `npm test` | Run all tests (server + client) |
| `make test-server` | `npm run test:server` | Run server tests only |
| `make test-client` | `npm run test:client` | Run client tests only |
| `make format` | `npm run format` | Format all files with Prettier |
| `make format-check` | `npm run format:check` | Check formatting without writing |
| `make mcp-build` | `npm run mcp:build` | Compile MCP TypeScript |
| `make mcp-typecheck` | `npm run mcp:typecheck` | Type-check MCP source |
| `make seed` | `npm run seed` | Load demo data |
| `make clear-data` | `npm run clear-data` | Delete all data rows |
| `make docker-up` | `docker compose up -d` | Start via docker-compose |
| `make docker-down` | `docker compose down` | Stop docker-compose stack |
---
## Statusline (optional)
The `statusline/` directory contains a standalone terminal statusline for Claude Code showing model, working directory, git branch, context window usage, and token counts. It is independent of the web dashboard.
See [statusline/README.md](./statusline/README.md) for installation instructions.
---
## Troubleshooting
### `better-sqlite3` errors during `npm install` / `npm run setup`
These warnings are **harmless**. `better-sqlite3` is an optional dependency — if it cannot compile, npm skips it and the server falls back to Node.js built-in `node:sqlite` (available on Node 22+).
You do **not** need Python, Visual Studio Build Tools, or any C++ compiler to run this project on Node 22+.
If you are on Node 20 or 21 and `better-sqlite3` prebuilds are not available for your platform (there is no `node:sqlite` fallback below Node 22), you have two options:
1. **Upgrade to Node.js 22+** — the built-in `node:sqlite` fallback requires no native compilation at all
2. **Install build tools** and run `npm rebuild better-sqlite3`:
- **Windows:** install [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) with the C++ workload
- **macOS:** `xcode-select --install`
- **Linux:** `sudo apt install python3 make g++` (Debian/Ubuntu)
### "SQLite backend not available" error on startup
This means neither `better-sqlite3` nor `node:sqlite` could be loaded. The most common cause is running Node.js < 22 without `better-sqlite3` prebuilds. Upgrade to Node.js 22+ to resolve this.
### Database is locked / busy errors
The SQLite database uses WAL mode with a 5-second busy timeout. If you see lock errors:
- Ensure only one dashboard server instance is running
- Check for zombie `node server/index.js` processes: `ps aux | grep server/index`
- Delete `data/dashboard.db-wal` and `data/dashboard.db-shm` if the server was killed uncleanly, then restart
---
### No sessions appearing after starting Claude Code
**Check 1 — Is the server running?**
```bash
curl http://localhost:4820/api/health
# Expected: {"status":"ok","timestamp":"..."}
```
**Check 2 — Are hooks installed?**
Open `~/.claude/settings.json` and confirm it contains a `hooks` section with entries referencing `hook-handler.js`. If not, run:
```bash
npm run install-hooks
```
**Check 3 — Did you start a new Claude Code session after the server started?**
Hooks only apply to sessions started after installation. Restart Claude Code.
**Check 4 — Is Node.js in PATH when Claude Code runs hooks?**
On some systems, the shell environment when Claude Code fires hooks may not include the full PATH. Test with:
```bash
node --version
```
If Node.js is not found, use the full path to `node` in the hook command. Edit `scripts/install-hooks.js`, replace `node` with the absolute path (e.g. `/usr/local/bin/node`), and re-run `npm run install-hooks`.
---
### Dashboard shows "Disconnected" in the sidebar
The WebSocket connection to the server failed. Ensure the server is running:
```bash
npm run dev
```
The client will automatically reconnect every 2 seconds once the server is available.
---
### Events Today shows 0 despite recent activity
This was a known timezone bug (fixed in current version). If you are still seeing this, ensure you are running the latest code and restart the server.
---
### Port 4820 already in use
```bash
DASHBOARD_PORT=4821 npm run dev
```
Then update the Vite proxy in `client/vite.config.ts`:
```ts
proxy: {
"/api": "http://localhost:4821",
"/ws": { target: "ws://localhost:4821", ws: true }
}
```
And make sure Claude Code posts hooks to the new port:
```bash
CLAUDE_DASHBOARD_PORT=4821 claude
# or edit scripts/hook-handler.js and change the default port
```
---
### Docker / Podman container starts but no sessions appear
**Check 1 — Is the container healthy?**
```bash
curl http://localhost:4820/api/health
# Expected: {"status":"ok","timestamp":"..."}
```
**Check 2 — Did you install hooks on the host?**
Hooks run on the host machine, not inside the container. After the container is up:
```bash
npm run install-hooks
```
**Check 3 — Are hooks pointing to the right port?**
Open `~/.claude/settings.json` and verify the hook commands reference `localhost:4820` (or whatever port the container is mapped to). If you changed the port mapping, update hooks accordingly.
---
### Docker build fails during `npm ci`
If the build fails in Stage 1 with `better-sqlite3` errors, this is expected and should not block the build — `better-sqlite3` is an optional dependency. If the build still fails:
- Ensure you are using the latest Dockerfile (it should use `node:22-alpine` and **not** install `python3`, `make`, or `g++`)
- Run `docker build --no-cache -t agent-monitor .` to force a clean rebuild
- Check that `package.json` has `better-sqlite3` under `optionalDependencies`, not `dependencies`
---
### macOS desktop app — `npm run desktop:dmg` is slow
This is expected. `desktop:dmg` compiles, packages, and ad-hoc-signs the app **twice** — once for `arm64`, once for `x64` — and emits **both** per-arch DMGs (`…-arm64.dmg` + `…-x64.dmg`). It does not merge them into a single universal binary; the two per-arch DMGs are what ship. Packaging two architectures back-to-back is what takes the time; it is not hung.
For a build that targets your own Mac, use a single-arch command instead — it builds one architecture and finishes in roughly a minute:
```bash
npm run desktop:dmg:arm64 # Apple Silicon
npm run desktop:dmg:x64 # Intel
```
CI already produces both DMGs — pulled either from the [latest GitHub Release](https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor/releases/latest) (CI auto-publishes a `vX.Y.Z` when `package.json` is bumped on `master`) or from the per-commit `ClaudeCodeMonitor-dmg` workflow artifact — so you rarely need to build them locally.
---
### Desktop app — `entry file out/main.js does not exist`
You ran `electron-builder` without a TypeScript compile. `npm run clean` (in `desktop/`) deletes `out/`, and `electron-builder` only packages — it does not compile. Re-run `npm run desktop:build` first, or use a `desktop:dmg*` / `desktop:win*` script (each one chains `npm run build` for you). Never invoke `electron-builder` bare.
---
### macOS desktop app — Gatekeeper blocks the app on first launch
The DMG is **ad-hoc signed** by default (the project ships no paid Apple Developer ID), so macOS shows *"Apple could not verify…"* the first time you open the app. Strip the quarantine attribute:
```bash
xattr -cr "/Applications/Claude Code Monitor.app"
```
Or open → *System Settings → Privacy & Security* and click *Open Anyway*. Real Developer ID signing and notarization are opt-in via the `CSC_LINK` / `CSC_KEY_PASSWORD` and `APPLE_ID` / `APPLE_TEAM_ID` / `APPLE_APP_SPECIFIC_PASSWORD` repository secrets — see [`DESKTOP.md`](./DESKTOP.md#notarization-for-the-maintainer).
---
### Windows desktop app — SmartScreen blocks the app on first launch
The Windows `.exe` (NSIS installer and portable build) is **unsigned** by default, so Windows SmartScreen shows *"Windows protected your PC"* the first time you run it. Click **More info → Run anyway**. Authenticode signing is opt-in via the `CSC_LINK` / `CSC_KEY_PASSWORD` repository secrets — CI picks them up automatically when provided.
---
### Desktop app — no sessions appearing
The desktop app installs hooks on its **first owned-server boot**, not before. After the app is running, start a **new** Claude Code session and confirm `~/.claude/settings.json` contains entries referencing `hook-handler.js`. If the app adopted an existing server on `4820`, that server's own hook configuration applies instead. For a blank dashboard window, check the desktop log (`~/Library/Logs/Claude Code Monitor/desktop.log` on macOS, `%APPDATA%\Claude Code Monitor\logs\desktop.log` on Windows) via tray → *Show Logs* and use tray → *Restart Server*.
Executable
+2642
View File
File diff suppressed because it is too large Load Diff
+1154
View File
File diff suppressed because it is too large Load Diff
+73
View File
@@ -0,0 +1,73 @@
<!doctype html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="apple-touch-icon" href="/favicon.svg" />
<link rel="manifest" href="/manifest.json" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="theme-color" content="#6366f1" />
<title>Agent Dashboard - Claude Code Monitor</title>
<meta
name="description"
content="Real-time monitoring platform for Claude Code agent activity. Track sessions, monitor agents, visualize analytics, manage cost tracking, and receive browser notifications - all with live WebSocket updates."
/>
<meta
name="keywords"
content="Claude Code, agent dashboard, AI monitoring, real-time dashboard, developer tools, session tracking, kanban, activity feed, analytics, cost tracking, browser notifications, token usage"
/>
<meta name="author" content="SmartGift" />
<meta name="robots" content="index, follow" />
<!-- Open Graph -->
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Agent Dashboard" />
<meta property="og:title" content="Agent Dashboard - Claude Code Monitor" />
<meta
property="og:description"
content="Real-time monitoring platform for Claude Code agent activity. Track sessions, monitor agents, visualize analytics, manage cost tracking, and receive browser notifications - all with live WebSocket updates."
/>
<meta property="og:image" content="/og-image.svg" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:type" content="image/svg+xml" />
<meta property="og:image:alt" content="Agent Dashboard - real-time Claude Code monitoring" />
<!-- Twitter / X Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Agent Dashboard - Claude Code Monitor" />
<meta
name="twitter:description"
content="Real-time monitoring platform for Claude Code agent activity. Sessions, Kanban board, analytics, cost tracking, browser notifications, and WebSocket push."
/>
<meta name="twitter:image" content="/og-image.svg" />
<meta name="twitter:image:alt" content="Agent Dashboard - real-time Claude Code monitoring" />
<meta name="twitter:creator" content="@smartgift" />
<!-- JSON-LD structured data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebApplication",
"name": "Agent Dashboard",
"description": "Real-time monitoring platform for Claude Code agent activity. Tracks sessions, agents, tool events, token usage analytics, cost tracking, and browser notifications via native hooks with live WebSocket push.",
"applicationCategory": "DeveloperApplication",
"operatingSystem": "Windows, macOS, Linux",
"softwareVersion": "1.0.0",
"author": {
"@type": "Person",
"name": "SmartGift",
"url": "https://smartgift.vn"
},
"codeRepository": "https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor",
"featureList": "Real-time dashboard, Kanban board, Session tracking, Activity feed, Token usage analytics, Cost tracking, Browser notifications, WebSocket live updates, CLI statusline"
}
</script>
</head>
<body class="bg-surface-0 text-gray-200 antialiased">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+5067
View File
File diff suppressed because it is too large Load Diff
+43
View File
@@ -0,0 +1,43 @@
{
"name": "agent-dashboard-client",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"@fontsource/inter": "^5.2.8",
"@fontsource/jetbrains-mono": "^5.2.8",
"d3": "^7.9.0",
"d3-sankey": "^0.12.3",
"i18next": "^26.0.8",
"i18next-browser-languagedetector": "^8.2.1",
"lucide-react": "^0.474.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^17.0.4",
"react-router-dom": "^6.28.2"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/d3": "^7.4.3",
"@types/d3-sankey": "^0.12.5",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"jsdom": "^27.0.1",
"postcss": "^8.5.1",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.3",
"vite": "^6.1.0",
"vitest": "^3.2.4"
}
}
+12
View File
@@ -0,0 +1,12 @@
/**
* @file postcss.config.js
* @description PostCSS pipeline for the client build Tailwind CSS and Autoprefixer.
* @author Nguyễn Ngọc Trí <vinnt@smartgift.vn>
*/
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
+24
View File
@@ -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

+28
View File
@@ -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"
}
]
}
+99
View File
@@ -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

+99
View File
@@ -0,0 +1,99 @@
/**
* @description Service Worker for caching static assets and handling push notifications.
* @author Nguyễn Ngọc Trí <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();
}
}
})
);
});
+125
View File
@@ -0,0 +1,125 @@
/**
* @file App.tsx
* @description Top-level React tree for the Claude Code Agent Monitor dashboard.
* Wires together routing, real-time WebSocket ingestion, browser notifications,
* and the splash screen shown on cold load.
*
* ## Data flow
* 1. {@link useWebSocket} connects to the server's `/ws` endpoint.
* 2. Each inbound {@link WSMessage} is published on the in-memory
* {@link eventBus} so any page can subscribe without prop drilling.
* 3. {@link useNotifications} listens for alert-worthy events and surfaces OS
* notifications when permitted.
*
* ## Routing
* All feature pages nest under {@link Layout}, which owns the sidebar and
* passes `wsConnected` for the connection badge. Unknown paths fall through to
* {@link NotFound}.
*
* @author Nguyễn Ngọc Trí <vinnt@smartgift.vn>
*/
/* =============================================================================
* MODULE_GUIDE extended in-file reference (comments only; safe to read, never executed)
* =============================================================================
* **Purpose:** Dashboard module consumed by the React client, MCP tools, or desktop shell depending on deployment mode.
*
* ## Design constraints
* - Local-first: no telemetry leaves the machine unless the user configures webhooks.
* - Fail-safe hooks path on the server must never block Claude Code; UI mirrors that
* philosophy by degrading gracefully (empty states, stale badges, reconnect loops).
* - Destructive flows stay behind explicit confirmation modals and server-side gates.
* - Internationalization: user-visible strings belong in i18n JSON, not literals here.
*
* ## Remote data & SSH
* Remote Data Sources let operators aggregate multiple machines. SSH entries describe
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every
* scoped GET via `?sources=`. Health checks and import history surface in Settings.
*
* ## Observability
* Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four
* provisioned boards (overview, sessions, tools, alerts). Native npm scripts and
* Docker Compose profiles are documented in `monitoring/README.md`.
*
* ## Internal dependencies
* - `./components/Layout`
* - `./components/SplashScreen`
* - `./pages/Dashboard`
* - `./pages/KanbanBoard`
* - `./pages/Sessions`
* - `./pages/SessionDetail`
* - `./pages/ActivityFeed`
* - `./pages/Analytics`
* - `./pages/Workflows`
* - `./pages/Settings`
* - `./pages/CcConfig`
* - `./pages/Workspace`
*
* ## Testing pointers
* - Prefer colocated `__tests__` with Vitest + Testing Library for UI.
* - Server contract changes require `npm run test:server` and OpenAPI sync.
* - MCP edits: `npm run mcp:typecheck` and `npm run mcp:build`.
*
* ## Related docs
* - `ARCHITECTURE.md` hooks API SQLite WebSocket UI pipeline.
* - `docs/API.md` REST reference.
* - `.claude/skills/file-headers/` mandatory `@author` header policy.
* ============================================================================= */
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import { useCallback } from "react";
import { Layout } from "./components/Layout";
import { DocumentTitle } from "./components/DocumentTitle";
import { SplashScreen } from "./components/SplashScreen";
import { Dashboard } from "./pages/Dashboard";
import { KanbanBoard } from "./pages/KanbanBoard";
import { Sessions } from "./pages/Sessions";
import { SessionDetail } from "./pages/SessionDetail";
import { ActivityFeed } from "./pages/ActivityFeed";
import { Analytics } from "./pages/Analytics";
import { Workflows } from "./pages/Workflows";
import { Settings } from "./pages/Settings";
import { CcConfig } from "./pages/CcConfig";
import { Workspace } from "./pages/Workspace";
import { NotFound } from "./pages/NotFound";
import { useWebSocket } from "./hooks/useWebSocket";
import { useNotifications } from "./hooks/useNotifications";
import { eventBus } from "./lib/eventBus";
import type { WSMessage } from "./lib/types";
/**
* Application root component mounted by {@link main.tsx}.
* @returns Routed dashboard UI inside `BrowserRouter`.
*/
export default function App() {
const onMessage = useCallback((msg: WSMessage) => {
eventBus.publish(msg);
}, []);
const { connected } = useWebSocket(onMessage);
useNotifications();
return (
<>
<SplashScreen />
<BrowserRouter>
<DocumentTitle />
<Routes>
<Route element={<Layout wsConnected={connected} />}>
<Route index element={<Dashboard />} />
<Route path="kanban" element={<KanbanBoard />} />
<Route path="sessions" element={<Sessions />} />
<Route path="sessions/:id" element={<SessionDetail />} />
<Route path="activity" element={<ActivityFeed />} />
<Route path="analytics" element={<Analytics />} />
<Route path="workflows" element={<Workflows />} />
<Route path="lanes" element={<Navigate to="/run" replace />} />
<Route path="cc-config" element={<CcConfig />} />
<Route path="run" element={<Workspace />} />
<Route path="settings" element={<Settings />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
</BrowserRouter>
</>
);
}
+261
View File
@@ -0,0 +1,261 @@
/**
* @file AgentCard.tsx
* @description Defines the AgentCard component that displays a summary of an agent's information, including its name, status, task, current tool, and timestamps. The card is clickable and navigates to the agent's session details when clicked. It also visually distinguishes active agents with a border highlight.
* @author Nguyễn Ngọc Trí <vinnt@smartgift.vn>
*/
/* =============================================================================
* MODULE_GUIDE extended in-file reference (comments only; safe to read, never executed)
* =============================================================================
* **Purpose:** Dashboard module consumed by the React client, MCP tools, or desktop shell depending on deployment mode.
*
* ## Design constraints
* - Local-first: no telemetry leaves the machine unless the user configures webhooks.
* - Fail-safe hooks path on the server must never block Claude Code; UI mirrors that
* philosophy by degrading gracefully (empty states, stale badges, reconnect loops).
* - Destructive flows stay behind explicit confirmation modals and server-side gates.
* - Internationalization: user-visible strings belong in i18n JSON, not literals here.
*
* ## Remote data & SSH
* Remote Data Sources let operators aggregate multiple machines. SSH entries describe
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every
* scoped GET via `?sources=`. Health checks and import history surface in Settings.
*
* ## Observability
* Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four
* provisioned boards (overview, sessions, tools, alerts). Native npm scripts and
* Docker Compose profiles are documented in `monitoring/README.md`.
*
* ## Internal dependencies
* - `./StatusBadge`
* - `../lib/types`
* - `../lib/format`
*
* ## Public surface
* - `AgentCard` exported API; see TSDoc on the symbol for behavior.
*
* ## Testing pointers
* - Prefer colocated `__tests__` with Vitest + Testing Library for UI.
* - Server contract changes require `npm run test:server` and OpenAPI sync.
* - MCP edits: `npm run mcp:typecheck` and `npm run mcp:build`.
*
* ## Related docs
* - `ARCHITECTURE.md` hooks API SQLite WebSocket UI pipeline.
* - `docs/API.md` REST reference.
* - `.claude/skills/file-headers/` mandatory `@author` header policy.
* ============================================================================= */
/* -----------------------------------------------------------------------------
* EXPORT CATALOG quick index of symbols defined below (documentation only).
* -----------------------------------------------------------------------------
* **AgentCard**
* Part of this module's public contract. Downstream imports should treat
* the signature and return type as stable unless release notes say otherwise.
* When behavior changes, update the `@file` overview and relevant tests.
*
* ----------------------------------------------------------------------------- */
import { useTranslation } from "react-i18next";
import { Bot, GitBranch, Clock, Wrench, Cpu, Coins } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { AgentStatusBadge } from "./StatusBadge";
import { effectiveAgentStatus, isAgentAwaitingInput, agentAwaitingReason } from "../lib/types";
import type { Agent, Session } from "../lib/types";
import { formatDuration, timeAgo, formatModelName, pathBasename, fmtCost } from "../lib/format";
/**
* Display name for a main agent, swapping its auto-generated placeholder for the
* real session title when one exists. Main agents are created as
* `<prefix> - <placeholder>`, where the placeholder is either `Session <id8>`
* (live hooks) or `<cwd-folder> - <id8>` (import / background sync). Replacing
* everything after the first ` - ` covers BOTH formats the older
* `replace(/Session [0-9a-f]{8}/)` only matched the hook form, so imported
* sessions kept showing `<folder> - <id8>` even after their title was known.
*/
function mainAgentDisplayName(agentName: string, realSessionName: string): string {
if (!realSessionName) return agentName;
const sep = agentName.indexOf(" - ");
return sep >= 0 ? `${agentName.slice(0, sep)} - ${realSessionName}` : agentName;
}
interface AgentCardProps {
agent: Agent;
/** Optional session data for richer main-agent rendering (model, cwd,
* cost). Subagent display ignores this. When omitted, the card falls
* back to the original minimal layout. */
session?: Session;
label?: string;
onClick?: () => void;
}
export function AgentCard({ agent, session, label, onClick }: AgentCardProps) {
const navigate = useNavigate();
const { t } = useTranslation("kanban");
const isWaiting = agent.status === "waiting" || isAgentAwaitingInput(agent);
const status = effectiveAgentStatus(agent);
const isActive = agent.status === "working";
const isMain = agent.type === "main";
// Session-level metadata applies to every card in the session - main and
// subagents alike. Subtitle differs by type: main uses model+cwd (its
// auto-generated name carries no info), subagents stick with their
// subagent_type label (more useful than repeating the session model).
const model = formatModelName(session?.model);
const cwdBase = pathBasename(session?.cwd);
// Cost shown on the card is scoped to what the card represents: a main agent's
// card stands in for the whole session, so it shows the session total; a
// subagent's card shows that subagent's OWN cost (server-computed from its
// token buckets). Showing the session total on a subagent card is misleading —
// it reads as if that one subagent cost the whole session's spend. A subagent
// with no recorded usage shows no cost (the cost > 0 guard below hides it),
// which is truthful rather than misleading.
const cost = isMain
? typeof session?.cost === "number"
? session.cost
: 0
: typeof agent.cost === "number"
? agent.cost
: 0;
// Real (user-given) session name - the auto-generated "Session <id8>"
// fallback carries no extra info next to the ID, so it is suppressed.
const sessionName = session?.name?.trim() || "";
const realSessionName = /^Session [0-9a-f]{8}$/i.test(sessionName) ? "" : sessionName;
// A subagent's own model lives in its metadata (resolved from its transcript,
// not the parent session's — see issue #185). Use it everywhere this card
// shows a model so a Haiku QA agent under an Opus orchestrator reads as
// Haiku, not Opus. Falls back to the session model only for the main agent.
let subagentModel: string | null = null;
if (!isMain && agent.metadata) {
try {
const parsed = JSON.parse(agent.metadata) as { model?: string };
subagentModel = parsed?.model ? formatModelName(parsed.model) : null;
} catch {
subagentModel = null;
}
}
// The model badge (footer) must reflect THIS card's agent: the session model
// for main, the subagent's own model for subagents.
const displayModel = isMain ? model : subagentModel;
// Model now lives in the footer badge, so the subtitle carries project
// context instead: main shows cwd + how many agents the session spawned +
// how many turns it has run; subagents show their type + the project they ran
// in. (No model here — that would duplicate the footer badge, which is what
// main cards used to do.)
const agentCount = typeof session?.agent_count === "number" ? session.agent_count : 0;
// agent_count includes the main agent itself. Show how many SUBAGENTS the
// session spawned instead, so this reconciles with the "Active Subagents"
// dashboard stat (which excludes main agents) — otherwise a card reading
// "29 agents" looks like it should equal a 29-subagent stat when the session
// actually has 28 subagents + 1 main.
const subagentCount = Math.max(0, agentCount - 1);
let sessionTurns = 0;
if (isMain && session?.metadata) {
try {
const m = JSON.parse(session.metadata) as { turn_count?: number };
if (typeof m?.turn_count === "number") sessionTurns = m.turn_count;
} catch {
sessionTurns = 0;
}
}
const subtitle = isMain
? [
cwdBase,
subagentCount > 0 ? t("kanban:session.subagentSummary", { count: subagentCount }) : null,
sessionTurns > 0 ? t("kanban:session.turnSummary", { count: sessionTurns }) : null,
]
.filter(Boolean)
.join(" · ") || null
: [label || agent.subagent_type, cwdBase].filter(Boolean).join(" · ") || null;
function handleClick() {
if (onClick) {
onClick();
} else {
navigate(`/sessions/${agent.session_id}`);
}
}
return (
<div
onClick={handleClick}
className={`card-hover p-4 cursor-pointer overflow-hidden ${
isWaiting
? "border-l-2 border-l-yellow-500/60"
: isActive
? "border-l-2 border-l-emerald-500/50"
: ""
}`}
>
<div className="flex items-start justify-between gap-2 mb-3 min-w-0">
<div className="flex items-center gap-2.5 min-w-0 overflow-hidden">
<div
className={`w-7 h-7 rounded-md flex items-center justify-center flex-shrink-0 ${
isMain ? "bg-accent/15 text-accent" : "bg-violet-500/15 text-violet-400"
}`}
>
{isMain ? <Bot className="w-3.5 h-3.5" /> : <GitBranch className="w-3.5 h-3.5" />}
</div>
<div className="min-w-0 overflow-hidden">
<p className="text-sm font-medium text-gray-200 truncate">
{/* Auto-generated main-agent titles (e.g. "Main Agent - Session
229d93fd" or "Main Agent - work - e3f8e613") swap the
placeholder for the real session name when one exists; custom
(sub)agent names are left untouched. */}
{isMain ? mainAgentDisplayName(agent.name, realSessionName) : agent.name}
</p>
{subtitle && <p className="text-[11px] text-gray-500 truncate">{subtitle}</p>}
</div>
</div>
{/* compact: cards are narrow inline reason chip would squeeze the
title, so the reason stays hover-tooltip-only here. */}
<AgentStatusBadge status={status} reason={agentAwaitingReason(agent)} compact />
</div>
{agent.task && (
<p className="text-xs text-gray-400 mb-3 line-clamp-2 leading-relaxed">{agent.task}</p>
)}
<div className="flex items-center gap-3 text-[11px] text-gray-500 min-w-0 overflow-hidden flex-wrap">
{agent.current_tool && (
<span className="flex items-center gap-1 flex-shrink-0">
<Wrench className="w-3 h-3" />
{agent.current_tool}
</span>
)}
{/* Model badge - shown on every card when no tool is currently
running (avoids clutter on actively-running agents that already
display the running tool name). Uses the agent's OWN model:
session model for main, the subagent's resolved model otherwise. */}
{displayModel && !agent.current_tool && (
<span className="flex items-center gap-1 flex-shrink-0">
<Cpu className="w-3 h-3" />
{displayModel}
</span>
)}
{cost > 0 && (
<span className="flex items-center gap-1 flex-shrink-0">
<Coins className="w-3 h-3" />
{fmtCost(cost)}
</span>
)}
{agent.ended_at ? (
<>
<span className="flex items-center gap-1 flex-shrink-0">
<Clock className="w-3 h-3" />
{t("ran")}
{formatDuration(agent.started_at, agent.ended_at)}
</span>
<span className="text-gray-600 flex-shrink-0">{timeAgo(agent.ended_at)}</span>
</>
) : (
<span className="flex items-center gap-1 flex-shrink-0">
<Clock className="w-3 h-3" />
{timeAgo(agent.updated_at || agent.started_at)}
</span>
)}
<span className="ml-auto flex items-center gap-1 min-w-0 opacity-50">
{realSessionName && <span className="truncate max-w-[10rem]">{realSessionName} ·</span>}
<span className="font-mono flex-shrink-0">{agent.session_id.slice(0, 8)}</span>
</span>
</div>
</div>
);
}
@@ -0,0 +1,846 @@
/**
* @file AlertsNotifications.tsx
* @description Unified "Alerts" control center embedded in the Settings page
* (replaces the standalone /alerts route). A segmented tab UI
* combines three concerns that used to be split across a page and a panel:
* Rules - define what conditions trigger an alert
* Channels - webhook targets that receive fired alerts (Slack/Discord/)
* Activity - the live fired-alert feed with acknowledge controls
* Tab badges reflect live state (rule count, unacked alert count), and the feed
* + counts refetch on alert_triggered / alert_updated WebSocket messages.
* @author Nguyễn Ngọc Trí <vinnt@smartgift.vn>
*/
/* =============================================================================
* MODULE_GUIDE extended in-file reference (comments only; safe to read, never executed)
* =============================================================================
* **Purpose:** Dashboard module consumed by the React client, MCP tools, or desktop shell depending on deployment mode.
*
* ## Design constraints
* - Local-first: no telemetry leaves the machine unless the user configures webhooks.
* - Fail-safe hooks path on the server must never block Claude Code; UI mirrors that
* philosophy by degrading gracefully (empty states, stale badges, reconnect loops).
* - Destructive flows stay behind explicit confirmation modals and server-side gates.
* - Internationalization: user-visible strings belong in i18n JSON, not literals here.
*
* ## Remote data & SSH
* Remote Data Sources let operators aggregate multiple machines. SSH entries describe
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every
* scoped GET via `?sources=`. Health checks and import history surface in Settings.
*
* ## Observability
* Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four
* provisioned boards (overview, sessions, tools, alerts). Native npm scripts and
* Docker Compose profiles are documented in `monitoring/README.md`.
*
* ## Internal dependencies
* - `../lib/api`
* - `../lib/eventBus`
* - `./EmptyState`
* - `./Skeleton`
* - `./WebhookSettings`
* - `./ConfirmModal`
* - `./Checkbox`
* - `./FieldHelp`
* - `../lib/format`
* - `../lib/types`
*
* ## Public surface
* - `AlertsNotifications` exported API; see TSDoc on the symbol for behavior.
*
* ## Testing pointers
* - Prefer colocated `__tests__` with Vitest + Testing Library for UI.
* - Server contract changes require `npm run test:server` and OpenAPI sync.
* - MCP edits: `npm run mcp:typecheck` and `npm run mcp:build`.
*
* ## Related docs
* - `ARCHITECTURE.md` hooks API SQLite WebSocket UI pipeline.
* - `docs/API.md` REST reference.
* - `.claude/skills/file-headers/` mandatory `@author` header policy.
* ============================================================================= */
/* -----------------------------------------------------------------------------
* EXPORT CATALOG quick index of symbols defined below (documentation only).
* -----------------------------------------------------------------------------
* **AlertsNotifications**
* Part of this module's public contract. Downstream imports should treat
* the signature and return type as stable unless release notes say otherwise.
* When behavior changes, update the `@file` overview and relevant tests.
*
* ----------------------------------------------------------------------------- */
import { useCallback, useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { useTranslation } from "react-i18next";
import {
BellRing,
BellOff,
Check,
CheckCheck,
ChevronDown,
ListChecks,
Plus,
RefreshCw,
Trash2,
Webhook,
X,
} from "lucide-react";
import { api } from "../lib/api";
import { eventBus } from "../lib/eventBus";
import { EmptyState } from "./EmptyState";
import { Skeleton } from "./Skeleton";
import { WebhookSettings } from "./WebhookSettings";
import { ConfirmModal } from "./ConfirmModal";
import { Checkbox } from "./Checkbox";
import { FieldHelp } from "./FieldHelp";
import { timeAgo } from "../lib/format";
import type { AlertEvent, AlertRule, AlertRuleType, WSMessage } from "../lib/types";
const PAGE_SIZE = 25;
// Example values surfaced in the field-help tooltips so users know what to type.
// These are the Claude Code hook event types and common built-in tool names.
const EVENT_TYPE_EXAMPLES = [
"PreToolUse",
"PostToolUse",
"Stop",
"SubagentStop",
"Notification",
"SessionStart",
"SessionEnd",
"UserPromptSubmit",
];
const TOOL_NAME_EXAMPLES = [
"Bash",
"Read",
"Edit",
"Write",
"Grep",
"Glob",
"Task",
"WebFetch",
"WebSearch",
"TodoWrite",
];
const SUMMARY_EXAMPLES = ["error", "permission", "timeout", "rate limit", "denied"];
const RULE_TYPES: AlertRuleType[] = [
"event_pattern",
"inactivity",
"status_duration",
"token_threshold",
];
type TabKey = "rules" | "channels" | "activity";
interface RuleFormState {
name: string;
rule_type: AlertRuleType;
event_type: string;
tool_name: string;
summary_contains: string;
count: string;
window_minutes: string;
minutes: string;
status: "working" | "waiting";
total_tokens: string;
cooldown_seconds: string;
}
const EMPTY_FORM: RuleFormState = {
name: "",
rule_type: "event_pattern",
event_type: "",
tool_name: "",
summary_contains: "",
count: "1",
window_minutes: "5",
minutes: "10",
status: "working",
total_tokens: "1000000",
cooldown_seconds: "300",
};
function buildConfig(form: RuleFormState): AlertRule["config"] {
switch (form.rule_type) {
case "event_pattern": {
const config: AlertRule["config"] = {};
if (form.event_type.trim()) config.event_type = form.event_type.trim();
if (form.tool_name.trim()) config.tool_name = form.tool_name.trim();
if (form.summary_contains.trim()) config.summary_contains = form.summary_contains.trim();
const count = parseInt(form.count, 10);
config.count = Number.isFinite(count) && count > 0 ? count : 1;
if (config.count > 1) {
const window = parseFloat(form.window_minutes);
config.window_minutes = Number.isFinite(window) && window > 0 ? window : 5;
}
return config;
}
case "inactivity":
return { minutes: parseFloat(form.minutes) };
case "status_duration":
return { status: form.status, minutes: parseFloat(form.minutes) };
case "token_threshold":
return { total_tokens: parseInt(form.total_tokens, 10) };
}
}
function describeRule(rule: AlertRule, t: (key: string, opts?: Record<string, unknown>) => string) {
const c = rule.config;
switch (rule.rule_type) {
case "event_pattern": {
const parts = [
c.event_type && `event=${c.event_type}`,
c.tool_name && `tool=${c.tool_name}`,
c.summary_contains && `summary~"${c.summary_contains}"`,
].filter(Boolean);
const base = parts.join(" · ");
return (c.count ?? 1) > 1
? t("ruleDesc.eventPatternCount", {
pattern: base,
count: c.count,
window: c.window_minutes,
})
: t("ruleDesc.eventPattern", { pattern: base });
}
case "inactivity":
return t("ruleDesc.inactivity", { minutes: c.minutes });
case "status_duration":
return t("ruleDesc.statusDuration", { status: c.status, minutes: c.minutes });
case "token_threshold":
return t("ruleDesc.tokenThreshold", { tokens: (c.total_tokens ?? 0).toLocaleString() });
}
}
export function AlertsNotifications() {
const { t } = useTranslation("alerts");
const { t: ts } = useTranslation("settings");
const [tab, setTab] = useState<TabKey>("rules");
// Rules
const [rules, setRules] = useState<AlertRule[]>([]);
const [loadingRules, setLoadingRules] = useState(true);
const [formOpen, setFormOpen] = useState(false);
const [form, setForm] = useState<RuleFormState>(EMPTY_FORM);
const [formError, setFormError] = useState<string | null>(null);
const [saving, setSaving] = useState(false);
const [confirmRule, setConfirmRule] = useState<AlertRule | null>(null);
// Feed
const [alerts, setAlerts] = useState<AlertEvent[]>([]);
const [total, setTotal] = useState(0);
const [unacked, setUnacked] = useState(0);
const [unackedOnly, setUnackedOnly] = useState(false);
const [loadingAlerts, setLoadingAlerts] = useState(true);
const loadRules = useCallback(async () => {
setLoadingRules(true);
try {
const res = await api.alerts.rules.list();
setRules(res.rules);
} catch (err) {
console.error("Failed to load alert rules:", err);
} finally {
setLoadingRules(false);
}
}, []);
const loadAlerts = useCallback(async () => {
setLoadingAlerts(true);
try {
const res = await api.alerts.list({
unacked: unackedOnly || undefined,
limit: PAGE_SIZE,
offset: 0,
});
setAlerts(res.alerts);
setTotal(res.total);
setUnacked(res.unacked);
} catch (err) {
console.error("Failed to load alerts:", err);
} finally {
setLoadingAlerts(false);
}
}, [unackedOnly]);
const loadMore = useCallback(async () => {
try {
const res = await api.alerts.list({
unacked: unackedOnly || undefined,
limit: PAGE_SIZE,
offset: alerts.length,
});
setAlerts((prev) => [...prev, ...res.alerts]);
setTotal(res.total);
setUnacked(res.unacked);
} catch (err) {
console.error("Failed to load more alerts:", err);
}
}, [unackedOnly, alerts.length]);
useEffect(() => {
loadRules();
}, [loadRules]);
useEffect(() => {
loadAlerts();
}, [loadAlerts]);
// Live updates: any fired/acked alert refreshes the feed + counts regardless
// of which tab is open, so the Activity badge stays accurate.
useEffect(() => {
return eventBus.subscribe((msg: WSMessage) => {
if (msg.type === "alert_triggered" || msg.type === "alert_updated") {
loadAlerts();
}
});
}, [loadAlerts]);
const set = (patch: Partial<RuleFormState>) => setForm((prev) => ({ ...prev, ...patch }));
const onCreateRule = async () => {
if (saving) return;
setSaving(true);
setFormError(null);
try {
const cooldown = parseInt(form.cooldown_seconds, 10);
await api.alerts.rules.create({
name: form.name.trim(),
rule_type: form.rule_type,
config: buildConfig(form),
cooldown_seconds: Number.isFinite(cooldown) && cooldown >= 0 ? cooldown : 300,
});
setForm(EMPTY_FORM);
setFormOpen(false);
loadRules();
} catch (err) {
setFormError(err instanceof Error ? err.message : String(err));
} finally {
setSaving(false);
}
};
const onToggleRule = async (rule: AlertRule) => {
try {
await api.alerts.rules.update(rule.id, { enabled: !rule.enabled });
loadRules();
} catch (err) {
console.error("Failed to toggle alert rule:", err);
}
};
const onDeleteRule = async (rule: AlertRule) => {
try {
await api.alerts.rules.remove(rule.id);
setConfirmRule(null);
loadRules();
loadAlerts();
} catch (err) {
console.error("Failed to delete alert rule:", err);
}
};
const onAck = async (id: number) => {
try {
await api.alerts.ack(id);
loadAlerts();
} catch (err) {
console.error("Failed to acknowledge alert:", err);
}
};
const onAckAll = async () => {
try {
await api.alerts.ackAll();
loadAlerts();
} catch (err) {
console.error("Failed to acknowledge alerts:", err);
}
};
// Mirror the server-side validation so obviously invalid rules never make it
// to a request.
const minutesVal = parseFloat(form.minutes);
const tokensVal = parseInt(form.total_tokens, 10);
const countVal = parseInt(form.count, 10);
const windowVal = parseFloat(form.window_minutes);
const canSubmit =
form.name.trim().length > 0 &&
(form.rule_type !== "event_pattern" ||
(Boolean(form.event_type.trim() || form.tool_name.trim() || form.summary_contains.trim()) &&
Number.isFinite(countVal) &&
countVal > 0 &&
(countVal <= 1 || (Number.isFinite(windowVal) && windowVal > 0)))) &&
((form.rule_type !== "inactivity" && form.rule_type !== "status_duration") ||
(Number.isFinite(minutesVal) && minutesVal > 0)) &&
(form.rule_type !== "token_threshold" || (Number.isFinite(tokensVal) && tokensVal > 0));
const TABS: { key: TabKey; label: string; icon: typeof ListChecks; badge?: number }[] = [
{
key: "rules",
label: ts("alertsHub.tabRules"),
icon: ListChecks,
badge: rules.length || undefined,
},
{ key: "channels", label: ts("alertsHub.tabChannels"), icon: Webhook },
{
key: "activity",
label: ts("alertsHub.tabActivity"),
icon: BellRing,
badge: unacked || undefined,
},
];
return (
<div className="space-y-4">
{/* Segmented tab control */}
<div className="inline-flex flex-wrap rounded-xl border border-border bg-surface-2 p-1 gap-1">
{TABS.map((tb) => {
const active = tab === tb.key;
const Icon = tb.icon;
return (
<button
key={tb.key}
onClick={() => setTab(tb.key)}
className={`inline-flex items-center gap-2 text-xs font-medium px-3.5 py-2 rounded-lg transition-colors ${
active
? "bg-surface-4 text-gray-100 shadow-sm"
: "text-gray-500 hover:text-gray-300 hover:bg-surface-3"
}`}
>
<Icon className="w-3.5 h-3.5" />
{tb.label}
{tb.badge != null && (
<span
className={`text-[10px] font-semibold rounded-full px-1.5 min-w-[18px] text-center ${
tb.key === "activity"
? "text-amber-300 bg-amber-500/15"
: active
? "text-accent bg-accent/15"
: "text-gray-400 bg-surface-2"
}`}
>
{tb.badge}
</span>
)}
</button>
);
})}
</div>
{/* ── RULES ── */}
{tab === "rules" && (
<div className="card p-4">
<div className="flex items-center justify-between gap-3 mb-3">
<div>
<h4 className="text-sm font-semibold text-gray-200">{t("rules.title")}</h4>
<p className="text-xs text-gray-500 mt-0.5">{ts("alertsHub.rulesHint")}</p>
</div>
<button
onClick={() => {
setFormOpen((open) => !open);
setFormError(null);
}}
className="btn-ghost border border-border inline-flex items-center gap-1.5 text-xs flex-shrink-0"
>
{formOpen ? <X className="w-3.5 h-3.5" /> : <Plus className="w-3.5 h-3.5" />}
{formOpen ? t("rules.cancel") : t("rules.add")}
</button>
</div>
{formOpen && (
<div className="rounded-lg border border-border bg-surface-2 p-3 mb-3 space-y-3">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<label className="block text-xs text-gray-400">
<span className="inline-flex items-center gap-1">
{t("rules.form.name")}
<FieldHelp description={t("rules.help.name")} />
</span>
<input
type="text"
value={form.name}
onChange={(e) => set({ name: e.target.value })}
placeholder={t("rules.form.namePlaceholder")}
className="input mt-1 w-full"
/>
</label>
<label className="block text-xs text-gray-400">
<span className="inline-flex items-center gap-1">
{t("rules.form.type")}
<FieldHelp title={t("rules.form.type")} description={t("rules.help.type")} />
</span>
<div className="relative mt-1">
<select
value={form.rule_type}
onChange={(e) => set({ rule_type: e.target.value as AlertRuleType })}
className="input w-full appearance-none pr-8"
>
{RULE_TYPES.map((type) => (
<option key={type} value={type}>
{t(`ruleTypes.${type}`)}
</option>
))}
</select>
<ChevronDown className="w-3.5 h-3.5 absolute right-2.5 top-1/2 -translate-y-1/2 text-gray-500 pointer-events-none" />
</div>
</label>
</div>
<p className="text-[11px] text-gray-500">{t(`ruleTypeHints.${form.rule_type}`)}</p>
{form.rule_type === "event_pattern" && (
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<label className="block text-xs text-gray-400">
<span className="inline-flex items-center gap-1">
{t("rules.form.eventType")}
<FieldHelp
title={t("rules.form.eventType")}
description={t("rules.help.eventType")}
examples={EVENT_TYPE_EXAMPLES}
/>
</span>
<input
type="text"
value={form.event_type}
onChange={(e) => set({ event_type: e.target.value })}
placeholder="PostToolUse"
className="input mt-1 w-full"
/>
</label>
<label className="block text-xs text-gray-400">
<span className="inline-flex items-center gap-1">
{t("rules.form.toolName")}
<FieldHelp
title={t("rules.form.toolName")}
description={t("rules.help.toolName")}
examples={TOOL_NAME_EXAMPLES}
/>
</span>
<input
type="text"
value={form.tool_name}
onChange={(e) => set({ tool_name: e.target.value })}
placeholder="Bash"
className="input mt-1 w-full"
/>
</label>
<label className="block text-xs text-gray-400">
<span className="inline-flex items-center gap-1">
{t("rules.form.summaryContains")}
<FieldHelp
title={t("rules.form.summaryContains")}
description={t("rules.help.summaryContains")}
examples={SUMMARY_EXAMPLES}
/>
</span>
<input
type="text"
value={form.summary_contains}
onChange={(e) => set({ summary_contains: e.target.value })}
placeholder="error"
className="input mt-1 w-full"
/>
</label>
<label className="block text-xs text-gray-400">
<span className="inline-flex items-center gap-1">
{t("rules.form.count")}
<FieldHelp description={t("rules.help.count")} />
</span>
<input
type="number"
min={1}
value={form.count}
onChange={(e) => set({ count: e.target.value })}
className="input mt-1 w-full"
/>
</label>
{parseInt(form.count, 10) > 1 && (
<label className="block text-xs text-gray-400">
<span className="inline-flex items-center gap-1">
{t("rules.form.windowMinutes")}
<FieldHelp description={t("rules.help.window")} />
</span>
<input
type="number"
min={1}
value={form.window_minutes}
onChange={(e) => set({ window_minutes: e.target.value })}
className="input mt-1 w-full"
/>
</label>
)}
</div>
)}
{(form.rule_type === "inactivity" || form.rule_type === "status_duration") && (
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
{form.rule_type === "status_duration" && (
<label className="block text-xs text-gray-400">
<span className="inline-flex items-center gap-1">
{t("rules.form.agentStatus")}
<FieldHelp description={t("rules.help.status")} />
</span>
<div className="relative mt-1">
<select
value={form.status}
onChange={(e) => set({ status: e.target.value as "working" | "waiting" })}
className="input w-full appearance-none pr-8"
>
<option value="working">working</option>
<option value="waiting">waiting</option>
</select>
<ChevronDown className="w-3.5 h-3.5 absolute right-2.5 top-1/2 -translate-y-1/2 text-gray-500 pointer-events-none" />
</div>
</label>
)}
<label className="block text-xs text-gray-400">
<span className="inline-flex items-center gap-1">
{t("rules.form.minutes")}
<FieldHelp
description={t(
form.rule_type === "inactivity"
? "rules.help.minutesInactivity"
: "rules.help.minutesStatus"
)}
/>
</span>
<input
type="number"
min={1}
value={form.minutes}
onChange={(e) => set({ minutes: e.target.value })}
className="input mt-1 w-full"
/>
</label>
</div>
)}
{form.rule_type === "token_threshold" && (
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<label className="block text-xs text-gray-400">
<span className="inline-flex items-center gap-1">
{t("rules.form.totalTokens")}
<FieldHelp description={t("rules.help.totalTokens")} />
</span>
<input
type="number"
min={1}
value={form.total_tokens}
onChange={(e) => set({ total_tokens: e.target.value })}
className="input mt-1 w-full"
/>
</label>
</div>
)}
<div className="flex flex-wrap items-end justify-between gap-3">
<label className="block text-xs text-gray-400">
<span className="mb-1.5 flex items-center gap-1">
{t("rules.form.cooldown")}
<FieldHelp description={t("rules.help.cooldown")} />
</span>
<input
type="number"
min={0}
value={form.cooldown_seconds}
onChange={(e) => set({ cooldown_seconds: e.target.value })}
className="input w-40"
/>
</label>
<button
onClick={onCreateRule}
disabled={!canSubmit || saving}
className="btn-primary inline-flex items-center gap-2 text-sm disabled:opacity-50 disabled:cursor-not-allowed"
>
<Plus className="w-3.5 h-3.5" />
{saving ? t("rules.saving") : t("rules.create")}
</button>
</div>
{formError && <p className="text-xs text-red-400">{formError}</p>}
</div>
)}
{loadingRules ? (
<div className="space-y-2">
<Skeleton className="h-12 w-full" />
<Skeleton className="h-12 w-full" />
</div>
) : rules.length === 0 ? (
<EmptyState
icon={ListChecks}
title={t("rules.empty")}
description={ts("alertsHub.rulesEmptyHint")}
/>
) : (
<ul className="space-y-2">
{rules.map((rule) => (
<li
key={rule.id}
className="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-border bg-surface-2 px-3 py-2.5"
>
<div className="min-w-0">
<div className="flex items-center gap-2">
<span
className={`text-sm font-medium truncate ${rule.enabled ? "text-gray-200" : "text-gray-500 line-through"}`}
>
{rule.name}
</span>
<span className="text-[10px] font-semibold uppercase tracking-wider text-accent bg-accent/10 border border-accent/20 rounded px-1.5 py-0.5 flex-shrink-0">
{t(`ruleTypes.${rule.rule_type}`)}
</span>
</div>
<p className="text-xs text-gray-500 mt-0.5 truncate">
{describeRule(rule, t)} ·{" "}
{t("rules.cooldown", { seconds: rule.cooldown_seconds })}
</p>
</div>
<div className="flex items-center gap-1.5 flex-shrink-0">
<button
onClick={() => onToggleRule(rule)}
className={`text-xs px-2.5 py-1.5 rounded-md border transition-colors ${
rule.enabled
? "border-emerald-500/30 text-emerald-400 hover:bg-emerald-500/10"
: "border-border text-gray-500 hover:text-gray-300 hover:bg-surface-3"
}`}
title={rule.enabled ? t("rules.disable") : t("rules.enable")}
>
{rule.enabled ? t("rules.enabled") : t("rules.disabled")}
</button>
<button
onClick={() => setConfirmRule(rule)}
className="p-1.5 rounded-md text-gray-500 hover:text-red-400 hover:bg-red-500/10 transition-colors"
title={t("rules.delete")}
aria-label={t("rules.delete")}
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
</li>
))}
</ul>
)}
</div>
)}
{/* ── CHANNELS (webhooks) ── */}
{tab === "channels" && <WebhookSettings />}
{/* ── ACTIVITY (fired-alert feed) ── */}
{tab === "activity" && (
<div className="card p-4">
<div className="flex flex-wrap items-center justify-between gap-3 mb-3">
<h4 className="text-sm font-semibold text-gray-200">
{t("feed.title")}
{unacked > 0 && (
<span className="ml-2 text-[10px] font-semibold text-amber-300 bg-amber-500/10 border border-amber-500/30 rounded-full px-2 py-0.5">
{t("feed.unackedCount", { count: unacked })}
</span>
)}
</h4>
<div className="flex items-center gap-2">
<Checkbox
checked={unackedOnly}
onChange={setUnackedOnly}
label={t("feed.unackedOnly")}
/>
<button
onClick={() => loadAlerts()}
className="btn-ghost border border-border inline-flex items-center gap-1.5 text-xs"
title={t("refresh")}
>
<RefreshCw className="w-3.5 h-3.5" />
</button>
{unacked > 0 && (
<button
onClick={onAckAll}
className="btn-primary inline-flex items-center gap-1.5 text-xs"
>
<CheckCheck className="w-3.5 h-3.5" />
{t("ackAll", { count: unacked })}
</button>
)}
</div>
</div>
{loadingAlerts && alerts.length === 0 ? (
<div className="space-y-2">
<Skeleton className="h-14 w-full" />
<Skeleton className="h-14 w-full" />
<Skeleton className="h-14 w-full" />
</div>
) : alerts.length === 0 ? (
<EmptyState
icon={unackedOnly ? BellOff : BellRing}
title={t("feed.emptyTitle")}
description={unackedOnly ? t("feed.emptyUnacked") : t("feed.emptyDescription")}
/>
) : (
<>
<ul className="space-y-2">
{alerts.map((alert) => (
<li
key={alert.id}
className={`flex flex-wrap items-center justify-between gap-3 rounded-lg border px-3 py-2.5 ${
alert.acknowledged_at
? "border-border bg-surface-2 opacity-70"
: "border-amber-500/30 bg-amber-500/5"
}`}
>
<div className="min-w-0">
<div className="flex items-center gap-2">
<BellRing
className={`w-3.5 h-3.5 flex-shrink-0 ${alert.acknowledged_at ? "text-gray-500" : "text-amber-400"}`}
/>
<span className="text-sm text-gray-200 truncate">{alert.message}</span>
</div>
<p className="text-[11px] text-gray-500 mt-0.5 font-mono">
{timeAgo(alert.triggered_at)} · {alert.rule_name}
{alert.session_id && (
<>
{" · "}
<Link
to={`/sessions/${encodeURIComponent(alert.session_id)}`}
className="text-accent hover:underline"
>
{t("feed.viewSession")}
</Link>
</>
)}
</p>
</div>
{!alert.acknowledged_at && (
<button
onClick={() => onAck(alert.id)}
className="inline-flex items-center gap-1.5 text-xs px-2.5 py-1.5 rounded-md border border-border text-gray-300 hover:text-gray-100 hover:bg-surface-3 transition-colors flex-shrink-0"
>
<Check className="w-3.5 h-3.5" />
{t("feed.ack")}
</button>
)}
</li>
))}
</ul>
{alerts.length < total && (
<div className="flex justify-center mt-4">
<button onClick={loadMore} className="btn-ghost border border-border text-sm">
{t("feed.loadMore", { shown: alerts.length, total })}
</button>
</div>
)}
</>
)}
</div>
)}
<ConfirmModal
open={!!confirmRule}
title={t("rules.deleteTitle", "Delete alert rule?")}
message={confirmRule ? t("rules.confirmDelete", { name: confirmRule.name }) : ""}
confirmLabel={t("rules.delete")}
cancelLabel={t("rules.cancel")}
onCancel={() => setConfirmRule(null)}
onConfirm={() => confirmRule && onDeleteRule(confirmRule)}
/>
</div>
);
}
+114
View File
@@ -0,0 +1,114 @@
/**
* @file Checkbox.tsx
* @description Accessible custom checkbox built on a `<button role="checkbox">`
* instead of a native `<input type="checkbox">` so the control matches the
* dashboard's dark theme (accent fill, rounded square, Lucide check mark).
*
* ## Keyboard & ARIA
* Space and Enter toggle via the native button behavior. `aria-checked` mirrors
* the `checked` prop for screen readers.
*
* ## Usage
* Pass `label` for inline text, or omit it and wrap with an external `<label>`
* when the clickable area should include more than the box itself.
*
* @author Nguyễn Ngọc Trí <vinnt@smartgift.vn>
*/
/* =============================================================================
* MODULE_GUIDE extended in-file reference (comments only; safe to read, never executed)
* =============================================================================
* **Purpose:** Dashboard module consumed by the React client, MCP tools, or desktop shell depending on deployment mode.
*
* ## Design constraints
* - Local-first: no telemetry leaves the machine unless the user configures webhooks.
* - Fail-safe hooks path on the server must never block Claude Code; UI mirrors that
* philosophy by degrading gracefully (empty states, stale badges, reconnect loops).
* - Destructive flows stay behind explicit confirmation modals and server-side gates.
* - Internationalization: user-visible strings belong in i18n JSON, not literals here.
*
* ## Remote data & SSH
* Remote Data Sources let operators aggregate multiple machines. SSH entries describe
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every
* scoped GET via `?sources=`. Health checks and import history surface in Settings.
*
* ## Observability
* Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four
* provisioned boards (overview, sessions, tools, alerts). Native npm scripts and
* Docker Compose profiles are documented in `monitoring/README.md`.
*
* ## Public surface
* - `CheckboxProps` exported API; see TSDoc on the symbol for behavior.
* - `Checkbox` exported API; see TSDoc on the symbol for behavior.
*
* ## Testing pointers
* - Prefer colocated `__tests__` with Vitest + Testing Library for UI.
* - Server contract changes require `npm run test:server` and OpenAPI sync.
* - MCP edits: `npm run mcp:typecheck` and `npm run mcp:build`.
*
* ## Related docs
* - `ARCHITECTURE.md` hooks API SQLite WebSocket UI pipeline.
* - `docs/API.md` REST reference.
* - `.claude/skills/file-headers/` mandatory `@author` header policy.
* ============================================================================= */
/* -----------------------------------------------------------------------------
* EXPORT CATALOG quick index of symbols defined below (documentation only).
* -----------------------------------------------------------------------------
* **CheckboxProps**
* Part of this module's public contract. Downstream imports should treat
* the signature and return type as stable unless release notes say otherwise.
* When behavior changes, update the `@file` overview and relevant tests.
*
* **Checkbox**
* Part of this module's public contract. Downstream imports should treat
* the signature and return type as stable unless release notes say otherwise.
* When behavior changes, update the `@file` overview and relevant tests.
*
* ----------------------------------------------------------------------------- */
import type { ReactNode } from "react";
import { Check } from "lucide-react";
/** Props for {@link Checkbox}. */
export interface CheckboxProps {
/** Controlled checked state. */
checked: boolean;
/** Called with the toggled value when the user activates the control. */
onChange: (v: boolean) => void;
/** Optional label rendered to the right of the box. */
label?: ReactNode;
/** Extra classes on the outer `<button>`. */
className?: string;
/** Classes applied to the label `<span>` when `label` is set. */
labelClassName?: string;
}
/**
* Themed checkbox control.
* @param props See {@link CheckboxProps}.
*/
export function Checkbox({ checked, onChange, label, className, labelClassName }: CheckboxProps) {
return (
<button
type="button"
role="checkbox"
aria-checked={checked}
onClick={() => onChange(!checked)}
className={`group inline-flex items-center gap-2 text-left ${className ?? ""}`}
>
<span
className={`flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-[5px] border transition-colors ${
checked
? "bg-accent border-accent"
: "bg-surface-2 border-border group-hover:border-border-light"
}`}
>
{checked && <Check className="h-3 w-3 text-white" strokeWidth={3} />}
</span>
{label != null && (
<span className={labelClassName ?? "text-xs text-gray-400 group-hover:text-gray-300"}>
{label}
</span>
)}
</button>
);
}
+185
View File
@@ -0,0 +1,185 @@
/**
* @file ConfirmModal.tsx
* @description Centered confirmation dialog for destructive or irreversible
* actions (delete webhook, remove alert rule, etc.). Replaces `window.confirm`
* with themed UI that matches the dashboard and distinguishes a loading (`busy`)
* confirm button from one refused outright (`disabled`).
*
* ## Dismissal
* Clicking the backdrop, pressing Escape, or clicking the X cancels. The confirm
* button can be styled non-destructive for neutral confirmations.
*
* ## Accessibility
* Focus moves to Cancel on open (safer default), Tab cycles within the dialog,
* Escape cancels, and focus restores to the previously focused element on close.
*
* @author Nguyễn Ngọc Trí <vinnt@smartgift.vn>
*/
import { useEffect, useId, useRef, type ReactNode } from "react";
import { AlertTriangle, X } from "lucide-react";
/** Props for {@link ConfirmModal}. */
export interface ConfirmModalProps {
/** When false, nothing is rendered. */
open: boolean;
/** Dialog heading. */
title: string;
/** Optional supporting message below the title. */
message?: string;
/** Primary action label (e.g. "Delete"). */
confirmLabel: string;
/** Secondary cancel label. */
cancelLabel: string;
/** When true (default), confirm button uses red destructive styling. */
destructive?: boolean;
/** Disables confirm while an async delete is in flight. */
busy?: boolean;
/**
* Disables confirm because the action is not permitted right now (a blocker, an
* unmet checkbox, facts that failed to load). Distinct from `busy`: nothing is
* in flight, so callers must not conflate the two passing a refusal as `busy`
* makes a blocked action read as perpetually loading.
*/
disabled?: boolean;
/** Optional action-specific facts shown before the confirmation controls. */
children?: ReactNode;
/** Called when the user confirms. */
onConfirm: () => void;
/** Called on cancel, backdrop click, Escape, or X. */
onCancel: () => void;
}
/**
* Modal confirmation overlay.
* @param props See {@link ConfirmModalProps}.
*/
export function ConfirmModal({
open,
title,
message,
confirmLabel,
cancelLabel,
destructive = true,
busy = false,
disabled = false,
children,
onConfirm,
onCancel,
}: ConfirmModalProps) {
const titleId = useId();
const messageId = useId();
const panelRef = useRef<HTMLDivElement>(null);
const cancelRef = useRef<HTMLButtonElement>(null);
const previouslyFocused = useRef<HTMLElement | null>(null);
useEffect(() => {
if (!open) return;
previouslyFocused.current =
document.activeElement instanceof HTMLElement ? document.activeElement : null;
// Prefer Cancel so Enter/activation doesn't immediately destroy data.
const focusTimer = window.setTimeout(() => cancelRef.current?.focus(), 0);
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") {
e.preventDefault();
onCancel();
return;
}
if (e.key !== "Tab" || !panelRef.current) return;
const focusable = panelRef.current.querySelectorAll<HTMLElement>(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
);
if (focusable.length === 0) return;
const first = focusable.item(0);
const last = focusable.item(focusable.length - 1);
if (!first || !last) return;
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
};
document.addEventListener("keydown", onKey);
return () => {
window.clearTimeout(focusTimer);
document.removeEventListener("keydown", onKey);
previouslyFocused.current?.focus?.();
previouslyFocused.current = null;
};
}, [open, onCancel]);
if (!open) return null;
return (
<div
className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4"
onClick={onCancel}
role="presentation"
>
<div
ref={panelRef}
className="relative w-full max-w-md rounded-xl border border-border bg-surface-1 shadow-xl shadow-black/40"
onClick={(e) => e.stopPropagation()}
role="dialog"
aria-modal="true"
aria-labelledby={titleId}
aria-describedby={message ? messageId : undefined}
>
<div className="flex items-start gap-3 p-5">
{destructive && (
<div className="w-9 h-9 rounded-lg bg-red-500/10 border border-red-500/20 flex items-center justify-center flex-shrink-0">
<AlertTriangle className="w-4.5 h-4.5 text-red-400" />
</div>
)}
<div className="min-w-0 flex-1">
<h3 id={titleId} className="text-sm font-semibold text-gray-100">
{title}
</h3>
{message && (
<p id={messageId} className="text-xs text-gray-400 mt-1 leading-relaxed">
{message}
</p>
)}
{children}
</div>
<button
type="button"
onClick={onCancel}
className="text-gray-500 hover:text-gray-300 p-1 -mt-1 -mr-1"
aria-label={cancelLabel}
>
<X className="w-4 h-4" />
</button>
</div>
<div className="flex items-center justify-end gap-2 px-5 pb-5">
<button
ref={cancelRef}
type="button"
onClick={onCancel}
className="btn-ghost border border-border text-xs"
>
{cancelLabel}
</button>
<button
type="button"
onClick={onConfirm}
disabled={busy || disabled}
className={`inline-flex items-center gap-1.5 text-xs px-3 py-1.5 rounded-md transition-colors disabled:opacity-50 ${
destructive
? "text-red-200 bg-red-500/15 border border-red-500/30 hover:bg-red-500/25"
: "btn-primary"
}`}
>
{confirmLabel}
</button>
</div>
</div>
</div>
);
}

Some files were not shown because too many files have changed in this diff Show More