Compare commits

..

2 Commits

Author SHA1 Message Date
nntrivi2001 7357070fb9 chore: remove unused desktop app, cloud deployment infra, and monitoring stack
Deletes desktop/ (Electron wrapper), deployments/ (Helm/Kustomize/
Terraform/CI for cloud deploy), and monitoring/ (Prometheus + Grafana
stack) along with DESKTOP.md, DEPLOYMENT.md, docker-compose.full.yml,
their npm scripts, and every dangling reference across README,
ARCHITECTURE, INSTALL, SETUP, docs/, and the repeated per-file
MODULE_GUIDE "Observability" boilerplate comment. The GET /api/metrics
endpoint itself is untouched — it's the dashboard's own route, not
part of the removed monitoring stack.
2026-08-11 12:16:54 +07:00
nntrivi2001 f0ae876472 feat(lanes): add pipeline-template picker to the Workspace lane header
Lets a lane's pipeline template be switched live from the dashboard
(the same PATCH /api/lanes/:id the CLI's `ccam lanes pipeline` uses),
so lanes created before the picker shipped don't need the terminal.
Both ship-feature skills now force their own template before their
first `ccam stage` call, so the human never has to pick correctly at
lane creation.
2026-08-11 12:15:16 +07:00
316 changed files with 114 additions and 31263 deletions
@@ -13,6 +13,7 @@ You are running the autonomous feature pipeline for **one CCAM lane**. The human
LANE_DIR="$(pwd)" # the lane clone IS your cwd — CCAM resolves the lane from this, never a hardcoded path LANE_DIR="$(pwd)" # the lane clone IS your cwd — CCAM resolves the lane from this, never a hardcoded path
``` ```
- CCAM resolves your lane from `cwd` automatically (longest path-boundary prefix match) — there is no marker file to check and no separate assign step. If `ccam stage` or `ccam feature activate` ever fails with "no lane found", you are not inside a lane's working directory; stop and tell the human. - CCAM resolves your lane from `cwd` automatically (longest path-boundary prefix match) — there is no marker file to check and no separate assign step. If `ccam stage` or `ccam feature activate` ever fails with "no lane found", you are not inside a lane's working directory; stop and tell the human.
- **Pipeline template is this skill's contract, not whatever the lane started on.** Before your first `ccam stage` call, run `ccam lanes pipeline ship-feature` (no id needed — resolves from `cwd` like `ccam stage`; idempotent, a no-op if already set) so every stage below resolves against the 16-node `ship-feature` map. The human is never expected to pick this in Add Lane or anywhere else — you force it to match the skill actually running.
- All stage updates go through `ccam stage <stage> [--status <s>] [--evidence "..."]`**call it at the start of every stage** (this is also the heartbeat, visible on the dashboard). - All stage updates go through `ccam stage <stage> [--status <s>] [--evidence "..."]`**call it at the start of every stage** (this is also the heartbeat, visible on the dashboard).
- **Integration toggles: check, don't assume.** `ccam lanes integration tracker`, `ccam lanes integration dev_qc`, `ccam lanes integration ci_wait` each exit 0 (on) or 1 (off), reading the profile's `integrations.env`. No agent exists yet to actually FILE a ticket or run dev-QC even when a toggle reads on (`ticketer`/`dev-qc` are a later task) — so regardless of the check's result, Stage 9 (ticket) stays skipped, Stage 13's dev-QC and dev-CI-wait halves stay skipped, and Stage 10's CI watch keeps using the plain `gh pr checks` path (`ccam ci` doesn't exist yet). Check the toggle where noted below anyway, so the evidence you record is honest about whether the PROFILE wants the integration on, distinct from whether CCAM can act on it yet. - **Integration toggles: check, don't assume.** `ccam lanes integration tracker`, `ccam lanes integration dev_qc`, `ccam lanes integration ci_wait` each exit 0 (on) or 1 (off), reading the profile's `integrations.env`. No agent exists yet to actually FILE a ticket or run dev-QC even when a toggle reads on (`ticketer`/`dev-qc` are a later task) — so regardless of the check's result, Stage 9 (ticket) stays skipped, Stage 13's dev-QC and dev-CI-wait halves stay skipped, and Stage 10's CI watch keeps using the plain `gh pr checks` path (`ccam ci` doesn't exist yet). Check the toggle where noted below anyway, so the evidence you record is honest about whether the PROFILE wants the integration on, distinct from whether CCAM can act on it yet.
- **Heartbeat during long stages.** Implementing (Stage 1), CI waits (Stage 10), and the watch/post-merge polls (Stages 1213) can run many minutes between stage transitions — bump the heartbeat with `ccam stage <same-stage>` after each commit and on each poll iteration, so the dashboard doesn't false-flag a working lane as stalled. - **Heartbeat during long stages.** Implementing (Stage 1), CI waits (Stage 10), and the watch/post-merge polls (Stages 1213) can run many minutes between stage transitions — bump the heartbeat with `ccam stage <same-stage>` after each commit and on each poll iteration, so the dashboard doesn't false-flag a working lane as stalled.
+7
View File
@@ -19,6 +19,13 @@ Each phase below starts with `ccam stage <node>`, which is what puts the phase
on the lane's pipeline map. The nodes are the `default` template's: on the lane's pipeline map. The nodes are the `default` template's:
`intake → plan → implement → tests → review → gate → ship → done`. `intake → plan → implement → tests → review → gate → ship → done`.
Set the template yourself, don't rely on how the lane was created: before your
first `ccam stage` call, run `ccam lanes pipeline default` (no id needed —
resolves from `cwd`; a no-op if the lane is already on it). This is what makes
the choice invisible to the human — whichever of `ship-feature` or
`ship-feature-lane` actually runs is what decides the template, not a picker
they have to get right in advance.
`ccam stage` needs a lane owning the current directory. If it reports no lane, `ccam stage` needs a lane owning the current directory. If it reports no lane,
this repo was never adopted (`ccam lanes add --cwd $(pwd)` fixes it) — carry on this repo was never adopted (`ccam lanes add --cwd $(pwd)` fixes it) — carry on
with the workflow and skip the stage calls; they are reporting, not control with the workflow and skip the stage calls; they are reporting, not control
@@ -52,10 +52,10 @@ i18n architecture: **Supported languages** list, `supportedLngs`, the 15 namespa
## Tier 3 — situational ## Tier 3 — situational
- `.env.example` — every env var belongs here with a sane default + comment. - `.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. - `INSTALL.md`, `SETUP.md` — install/run commands.
- `CLAUDE.md`, `AGENTS.md` — agent working guides; update when commands, file locations, or workflows change. - `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. - `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. - `vscode-extension/README.md`, `statusline/README.md` — surface-specific; update only when that surface changes.
## Consistency invariants ## Consistency invariants
+2 -391
View File
@@ -35,35 +35,13 @@ Architectural overview and technical reference for the Agent Dashboard system, c
![OpenAPI](https://img.shields.io/badge/OpenAPI-3.0-000000?style=flat-square&logo=openapiinitiative&logoColor=white) ![OpenAPI](https://img.shields.io/badge/OpenAPI-3.0-000000?style=flat-square&logo=openapiinitiative&logoColor=white)
![Swagger](https://img.shields.io/badge/Swagger-3.0-85EA2D?style=flat-square&logo=swagger&logoColor=white) ![Swagger](https://img.shields.io/badge/Swagger-3.0-85EA2D?style=flat-square&logo=swagger&logoColor=white)
![VS Code](https://img.shields.io/badge/VS_Code-Extension-007ACC?style=flat-square&logo=vscodium&logoColor=white) ![VS Code](https://img.shields.io/badge/VS_Code-Extension-007ACC?style=flat-square&logo=vscodium&logoColor=white)
![Electron](https://img.shields.io/badge/Electron-35-47848F?style=flat-square&logo=electron&logoColor=white)
![electron-builder](https://img.shields.io/badge/electron--builder-25.1-2c2e3b?style=flat-square&logo=electron&logoColor=white)
![macOS](https://img.shields.io/badge/macOS-Desktop_App-000000?style=flat-square&logo=apple&logoColor=white)
![Windows](https://img.shields.io/badge/Windows-Desktop_App-0078D6?style=flat-square&logo=windows&logoColor=white)
![SMAppService](https://img.shields.io/badge/SMAppService-Login_Items-000000?style=flat-square&logo=apple&logoColor=white)
![macOS DMG](https://img.shields.io/badge/macOS_DMG-arm64_%2B_x64-7c3aed?style=flat-square&logo=apple&logoColor=white)
![Vitest](https://img.shields.io/badge/Vitest-1.0-646CFF?style=flat-square&logo=vitest&logoColor=white) ![Vitest](https://img.shields.io/badge/Vitest-1.0-646CFF?style=flat-square&logo=vitest&logoColor=white)
![React Testing Library](https://img.shields.io/badge/React_Testing_Library-13.0-FF5733?style=flat-square&logo=testinglibrary&logoColor=white) ![React Testing Library](https://img.shields.io/badge/React_Testing_Library-13.0-FF5733?style=flat-square&logo=testinglibrary&logoColor=white)
![ESLint](https://img.shields.io/badge/ESLint-8.44-4B32C3?style=flat-square&logo=eslint&logoColor=white) ![ESLint](https://img.shields.io/badge/ESLint-8.44-4B32C3?style=flat-square&logo=eslint&logoColor=white)
![Prettier](https://img.shields.io/badge/Prettier-3.8-F7B93E?style=flat-square&logo=prettier&logoColor=white) ![Prettier](https://img.shields.io/badge/Prettier-3.8-F7B93E?style=flat-square&logo=prettier&logoColor=white)
![Docker](https://img.shields.io/badge/Docker-20.10-2496ED?style=flat-square&logo=docker&logoColor=white) ![Docker](https://img.shields.io/badge/Docker-20.10-2496ED?style=flat-square&logo=docker&logoColor=white)
![Podman](https://img.shields.io/badge/Podman-4.0-CC342D?style=flat-square&logo=podman&logoColor=white) ![Podman](https://img.shields.io/badge/Podman-4.0-CC342D?style=flat-square&logo=podman&logoColor=white)
![Terraform](https://img.shields.io/badge/Terraform-%3E%3D1.5-844FBA?style=flat-square&logo=terraform&logoColor=white)
![Kubernetes](https://img.shields.io/badge/Kubernetes-%3E%3D1.24-326CE5?style=flat-square&logo=kubernetes&logoColor=white)
![Helm](https://img.shields.io/badge/Helm-3-0F1689?style=flat-square&logo=helm&logoColor=white)
![Kustomize](https://img.shields.io/badge/Kustomize-5.0-326CE5?style=flat-square&logo=kubernetes&logoColor=white)
![Nginx](https://img.shields.io/badge/Nginx-Ingress-009639?style=flat-square&logo=nginx&logoColor=white)
![Prometheus](https://img.shields.io/badge/Prometheus-2.x-E6522C?style=flat-square&logo=prometheus&logoColor=white)
![Grafana](https://img.shields.io/badge/Grafana-10.x-F46800?style=flat-square&logo=grafana&logoColor=white)
![Coralogix](https://img.shields.io/badge/Coralogix-Observability-1a1a2e?style=flat-square&logo=datadog&logoColor=white)
![OpenTelemetry](https://img.shields.io/badge/OpenTelemetry-Collector-4f46e5?style=flat-square&logo=opentelemetry&logoColor=white)
![AWS](https://img.shields.io/badge/AWS-ECS%20%7C%20RDS-232F3E?style=flat-square&logo=task&logoColor=white)
![Google Cloud](https://img.shields.io/badge/Google_Cloud-GKE%20%7C%20SQL-4285F4?style=flat-square&logo=googlecloud&logoColor=white)
![Azure](https://img.shields.io/badge/Azure-AKS%20%7C%20SQL-0078D4?style=flat-square&logo=cloudflare&logoColor=white)
![Oracle Cloud](https://img.shields.io/badge/Oracle_Cloud-OKE%20%7C%20DB-F80000?style=flat-square&logo=cloudways&logoColor=white)
![GitHub Actions](https://img.shields.io/badge/GitHub_Actions-pipelines-2088FF?style=flat-square&logo=githubactions&logoColor=white)
![GitLab CI](https://img.shields.io/badge/GitLab_CI-pipelines-FC6D26?style=flat-square&logo=gitlab&logoColor=white)
![Make](https://img.shields.io/badge/Make-4.3-000000?style=flat-square&logo=make&logoColor=white) ![Make](https://img.shields.io/badge/Make-4.3-000000?style=flat-square&logo=make&logoColor=white)
![Auto Release](https://img.shields.io/badge/CI-auto--release_to_GitHub-22c55e?style=flat-square&logo=githubactions&logoColor=white)
--- ---
@@ -87,7 +65,6 @@ Architectural overview and technical reference for the Agent Dashboard system, c
- [Update Notifier Subsystem](#update-notifier-subsystem) - [Update Notifier Subsystem](#update-notifier-subsystem)
- [Tabby Companion Subsystem](#tabby-companion-subsystem) - [Tabby Companion Subsystem](#tabby-companion-subsystem)
- [VS Code Extension Architecture](#vs-code-extension-architecture) - [VS Code Extension Architecture](#vs-code-extension-architecture)
- [Desktop App Architecture (macOS & Windows / Electron)](#desktop-app-architecture-macos--windows--electron)
- [Security Considerations](#security-considerations) - [Security Considerations](#security-considerations)
- [Performance Characteristics](#performance-characteristics) - [Performance Characteristics](#performance-characteristics)
- [Deployment Modes](#deployment-modes) - [Deployment Modes](#deployment-modes)
@@ -348,8 +325,7 @@ graph TD
| `routes/agents.js` | CRUD with status/session_id filtering. PATCH broadcasts `agent_updated`. Agent-list responses (`GET /api/agents`, `GET /api/sessions/:id/agents`) attach a per-agent `cost` via `pricing.attachAgentCosts` — each subagent's OWN cost, computed from its `metadata.tokens` at current rates (main agents get 0; their cost is the session total), so a subagent card shows only what that subagent spent rather than the session total | | `routes/agents.js` | CRUD with status/session_id filtering. PATCH broadcasts `agent_updated`. Agent-list responses (`GET /api/agents`, `GET /api/sessions/:id/agents`) attach a per-agent `cost` via `pricing.attachAgentCosts` — each subagent's OWN cost, computed from its `metadata.tokens` at current rates (main agents get 0; their cost is the session total), so a subagent card shows only what that subagent spent rather than the session total |
| `routes/events.js` | Read-only event listing with session_id filter and pagination | | `routes/events.js` | Read-only event listing with session_id filter and pagination |
| `routes/stats.js` | Single aggregate query returning total/active counts + status distributions | | `routes/stats.js` | Single aggregate query returning total/active counts + status distributions |
| `routes/metrics.js` | Prometheus / OpenMetrics text-exposition endpoint (`GET /api/metrics`) — re-exposes the dashboard's live counters (sessions/agents by status, event + token totals, connected WebSocket clients, configured remote sources, process uptime/RSS, build version) in the v0.0.4 text format for scraping into Prometheus / Grafana. Read-only; reads the same `db.js` prepared statements the REST API uses, so numbers match the UI. Status series are enumerated so a gauge never drops out at zero. Mounted under `/api`, so it sits behind the Host-header (DNS-rebinding) guard and the optional `DASHBOARD_TOKEN` guard — a non-loopback scraper needs `DASHBOARD_ALLOWED_HOSTS` (+ token if set). A turnkey Prometheus + Grafana stack with four auto-provisioned dashboards lives in `monitoring/` (`npm run monitoring:up` or `npm run docker:full:up`) | | `routes/metrics.js` | Prometheus / OpenMetrics text-exposition endpoint (`GET /api/metrics`) — re-exposes the dashboard's live counters (sessions/agents by status, event + token totals, connected WebSocket clients, configured remote sources, process uptime/RSS, build version) in the v0.0.4 text format for scraping into Prometheus / Grafana. Read-only; reads the same `db.js` prepared statements the REST API uses, so numbers match the UI. Status series are enumerated so a gauge never drops out at zero. Mounted under `/api`, so it sits behind the Host-header (DNS-rebinding) guard and the optional `DASHBOARD_TOKEN` guard — a non-loopback scraper needs `DASHBOARD_ALLOWED_HOSTS` (+ token if set) |
| `monitoring/` | Optional npm-managed or Docker Compose Prometheus + Grafana stack that scrapes `GET /api/metrics`. Ships four Grafana dashboards (`ccam-overview`, `ccam-sessions-agents`, `ccam-tokens-events`, `ccam-platform`), recording rules (`prometheus/ccam-rules.yml`), a Prometheus 3.x-compatible static HTML console (`prometheus/consoles/index.html`), and lifecycle scripts (`monitoring:install`, `monitoring:up`, `monitoring:verify`). See [`monitoring/README.md`](./monitoring/README.md) |
| `routes/analytics.js` | Extended analytics — token totals, tool usage counts, daily event/session trends, agent type distribution. The client-side analytics heatmap grid is aligned to a Sunday start for correct day-of-week positioning | | `routes/analytics.js` | Extended analytics — token totals, tool usage counts, daily event/session trends, agent type distribution. The client-side analytics heatmap grid is aligned to a Sunday start for correct day-of-week positioning |
| `routes/pricing.js` | Model pricing CRUD (list/upsert/delete) and per-session / global cost calculation with pattern-based model matching. `PUT /api/pricing` upserts a rule and accepts optional time-limited **introductory** rates (`intro_*_per_mtok` + `intro_until`): usage on/before the cutoff date prices at the intro rate, after it at the standard rate — the calculator picks the effective rate per usage day (`ratesForBucket`), so a promo like Sonnet 5's launch discount is correct before AND after the cutoff, retroactively. Intro columns are written only when the caller sends them (a standard-rate edit never disturbs a promo). Cost is computed per token bucket — keyed by (model, speed, inference_geo, service_tier) — applying fast-mode premium, US data-residency (1.1x), and Batch (0.5x) modifiers, the 5m/1h cache-write split, plus server-tool surcharges (web search $10/1k; code execution estimated by container-time with the monthly free-hours allowance; web fetch free). `attachAgentCosts`/`agentOwnCost` reuse the same calculator to price each agent's `metadata.tokens` for the per-agent `cost` on agent-list responses. Feature rates + modifier math live in `lib/pricing-constants.js`; usage normalization in `lib/token-usage.js` | | `routes/pricing.js` | Model pricing CRUD (list/upsert/delete) and per-session / global cost calculation with pattern-based model matching. `PUT /api/pricing` upserts a rule and accepts optional time-limited **introductory** rates (`intro_*_per_mtok` + `intro_until`): usage on/before the cutoff date prices at the intro rate, after it at the standard rate — the calculator picks the effective rate per usage day (`ratesForBucket`), so a promo like Sonnet 5's launch discount is correct before AND after the cutoff, retroactively. Intro columns are written only when the caller sends them (a standard-rate edit never disturbs a promo). Cost is computed per token bucket — keyed by (model, speed, inference_geo, service_tier) — applying fast-mode premium, US data-residency (1.1x), and Batch (0.5x) modifiers, the 5m/1h cache-write split, plus server-tool surcharges (web search $10/1k; code execution estimated by container-time with the monthly free-hours allowance; web fetch free). `attachAgentCosts`/`agentOwnCost` reuse the same calculator to price each agent's `metadata.tokens` for the per-agent `cost` on agent-list responses. Feature rates + modifier math live in `lib/pricing-constants.js`; usage normalization in `lib/token-usage.js` |
| `routes/settings.js` | System info (DB size, hook status, server uptime, transcript cache stats), data export as one versioned JSON bundle and matching import/restore (`POST /api/settings/import` via `server/lib/data-transfer.js` — idempotent, session-atomic, non-destructive; consolidates machines), session cleanup (abandon stale, purge old), clear all data (including the fired-alert feed and webhook delivery log; alert *rules* and webhook *targets* are preserved as user configuration), reset pricing, reinstall hooks | | `routes/settings.js` | System info (DB size, hook status, server uptime, transcript cache stats), data export as one versioned JSON bundle and matching import/restore (`POST /api/settings/import` via `server/lib/data-transfer.js` — idempotent, session-atomic, non-destructive; consolidates machines), session cleanup (abandon stale, purge old), clear all data (including the fired-alert feed and webhook delivery log; alert *rules* and webhook *targets* are preserved as user configuration), reset pricing, reinstall hooks |
@@ -2011,7 +1987,7 @@ Cache versioning is controlled by the `CACHE_NAME` constant (`dashboard-v2`). On
`client/src/main.tsx` snapshots `navigator.serviceWorker.controller` before registration and listens for `controllerchange`: when a new SW activates on an already-controlled page, it reloads exactly once so the page picks up the new asset URLs without a hard refresh. The first install (no previous controller) does **not** reload. `client/src/main.tsx` snapshots `navigator.serviceWorker.controller` before registration and listens for `controllerchange`: when a new SW activates on an already-controlled page, it reloads exactly once so the page picks up the new asset URLs without a hard refresh. The first install (no previous controller) does **not** reload.
These behaviors are reinforced by explicit `Cache-Control` headers from the production Express static middleware in `server/index.js`: `immutable, max-age=31536000` for `/assets/*`; `no-cache, must-revalidate` for `index.html`, `sw.js`, and `manifest.json`; a short revalidation window for other static files. The SPA fallback `sendFile` sends the same `no-cache` header. The native desktop shell (macOS and Windows) loads the dashboard from this same in-process server (`NODE_ENV=production`), so it inherits the policy automatically. These behaviors are reinforced by explicit `Cache-Control` headers from the production Express static middleware in `server/index.js`: `immutable, max-age=31536000` for `/assets/*`; `no-cache, must-revalidate` for `index.html`, `sw.js`, and `manifest.json`; a short revalidation window for other static files. The SPA fallback `sendFile` sends the same `no-cache` header.
--- ---
@@ -2308,299 +2284,6 @@ For the extension source code, refer to the [vscode-extension/](./vscode-extensi
--- ---
## Desktop App Architecture (macOS & Windows / Electron)
The `desktop/` workspace ships the dashboard as a **native desktop app** for **macOS** (`Claude Code Monitor.app`, distributed as a `.dmg`) **and Windows** (`Claude Code Monitor.exe`, distributed as an NSIS installer plus a no-install portable build). It is an Electron shell that **embeds the existing Express server in-process** and renders the already-built React client in a `BrowserWindow`. The desktop app does not reimplement the dashboard -- it `require()`s `server/index.js` directly, in the same Node runtime as the Electron main process, and points a Chromium window at it.
For the user-facing guide (download, install, Gatekeeper / SmartScreen, tray menu, auto-start), see [DESKTOP.md](./DESKTOP.md). For the full contributor/architecture reference -- including build performance, code signing, notarization, and CI details -- see [desktop/README.md](./desktop/README.md).
### Workspace Position
`desktop/` is a **sibling workspace**, not an npm-workspaces conversion. It has its own `package.json`, its own `node_modules`, and its own TypeScript toolchain. It pins **Electron 35** (bundled Node 22.16). It consumes the rest of the repo as plain files and touches no other workspace's runtime behavior.
```mermaid
flowchart TD
subgraph repo["Claude-Code-Agent-Monitor (repo root)"]
server["server/<br/>Express API · SQLite · WebSocket"]
client["client/<br/>React + Vite SPA"]
scripts["scripts/<br/>hook installer/handler, import, seed"]
mcp["mcp/<br/>local MCP server"]
vscode["vscode-extension/"]
desktop["desktop/<br/>Electron shell (sibling workspace)"]
end
desktop -->|"require() in-process"| server
desktop -->|"loads built SPA from"| client
desktop -->|"auto-installs hooks via"| scripts
server -->|"serves static"| client
style desktop fill:#1f6feb,stroke:#1158c7,color:#fff
style server fill:#238636,stroke:#196c2e,color:#fff
```
The **only** change outside `desktop/` is a behavior-preserving refactor of `server/index.js` (see [Background Services & Hook Bootstrap](#background-services--hook-bootstrap-1) below). `client/`, `scripts/`, `mcp/`, and `vscode-extension/` are untouched.
### Process Model
Electron runs a **main process** (Node.js) and one or more **renderer processes** (Chromium). In this app:
- The **main process** hosts the embedded Express server _and_ manages the window, tray, and menus. There is **no child process and no IPC** for the server -- it runs inside the main process's own event loop.
- The **renderer** is plain Chromium loading `http://127.0.0.1:<port>` -- exactly the same origin a normal browser would use. `preload.ts` is intentionally empty (`contextIsolation: true`, `nodeIntegration: false`, `webSecurity: true`), so the renderer has **zero privileged surface**.
```mermaid
flowchart LR
subgraph main["Electron Main Process (Node 22 / Electron 35)"]
boot["main.ts<br/>lifecycle"]
host["server-host.ts<br/>embedded server"]
express["server/index.js<br/>Express + WS + SQLite"]
tray["tray.ts"]
menu["menu.ts"]
host --> express
boot --> host
boot --> tray
boot --> menu
end
subgraph renderer["Renderer Process (Chromium)"]
win["BrowserWindow<br/>React dashboard"]
preload["preload.ts<br/>(empty -- no bridge)"]
end
express -->|"http + ws on 127.0.0.1:port"| win
win -.->|loads| preload
hooks["Claude Code hooks<br/>(separate node processes)"] -->|"POST /api/hooks/event"| express
style main fill:#0d1117,stroke:#30363d,color:#e6edf3
style renderer fill:#161b22,stroke:#30363d,color:#e6edf3
```
### In-Process Server Hosting
`server-host.ts` is the **only file** that imports `server/index.js`. The dashboard server already exports `{ createApp, startServer, startBackgroundServices }` and serves the built React client (`client/dist`) as static assets in production -- so the host imports that module directly, with no child process, no IPC, and no port marshalling.
| Component | Responsibility |
| --- | --- |
| **`main.ts`** | Main-process entry. Single-instance lock, app menu + tray wiring, dashboard window, `Restart Server`, lifecycle (`window-all-closed`, `before-quit`). |
| **`server-host.ts`** | In-process Express boot: port discovery, adoption, `better-sqlite3` ABI patch, `startBackgroundServices()` + hook bootstrap, clean DB close. Returns a `ServerHandle`. |
| **`window.ts`** | `BrowserWindow` with persisted geometry (`userData/window-state.json`). External links open in the system browser. Sets the window/taskbar `icon` to the colored app logo (`assets/icon.ico` on Windows, `icon.png` elsewhere) so an unpackaged `desktop:dev` run no longer shows the generic Electron icon. |
| **`menu.ts` / `tray.ts`** | Native application menu and menu-bar / notification-area (tray) icon. `tray.ts` selects a platform tray image — a macOS template glyph that the OS tints for the menu bar, or the colored `assets/icon.ico` for the Windows notification area (a black template would vanish on the dark taskbar). Tray uses a single-click dropdown (left or right) with a **live status snapshot** queried straight from SQLite at click time — server port, active sessions, working agents, events today — followed by *Open Dashboard*, *Open in Browser*, *Restart Server*, *Show Logs*, *Open at Login* (toggle), and *Quit*. The menu is rebuilt on each open so every value stays current. Snapshot rows are enabled and click-to-open-dashboard rather than disabled (which the OS dims). The application menu's *File ▸ Open Dashboard* (⌘1) is **macOS-only** — there the global menu bar persists after the window hides; on Windows/Linux the window-attached menu can't reopen a hidden window, so reopening is the tray's job. `focusOrCreateWindow` calls `show()` unconditionally so the tray reliably raises a backgrounded/minimized window (a bare `focus()` on Windows often only flashes the taskbar). |
| **`login-item.ts`** | Auto-start-at-login toggle through Electron's first-party `app.setLoginItemSettings` API on every platform. On macOS it drives the modern `SMAppService` Login Items (not a `LaunchAgent` plist); on Windows it writes a per-user `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` entry. Login launches are tagged with a `--ccam-hidden` arg so the app can stay tray-only at startup — Windows has no `wasOpenedAtLogin` signal, so the arg is the cross-platform detection mechanism. |
| **`shell-path.ts`** | (macOS) Recovers the user's login-shell `PATH` at startup and merges it onto `process.env.PATH`, so the embedded server (and the `claude` CLI it spawns) is not limited to launchd's minimal `PATH`. On Windows the process already inherits the full user `PATH`, so no recovery is needed. |
| **`logger.ts`** | File logger to `~/Library/Logs/Claude Code Monitor/desktop.log` (macOS) or `%APPDATA%\Claude Code Monitor\logs\desktop.log` (Windows) -- the main process has no console when launched from Finder / Explorer. |
| **`constants.ts`** | Shared identifiers, including the `APP_ID` (`com.vn.smartgift.ccam.desktop`) that `main.ts` sets as the Windows AppUserModelId. |
`server-host.ts` resolves the directory containing the bundled `server/` and `client/dist/` via `resolveAppRoot()`: `process.resourcesPath/app` when packaged, or the repo root (one directory up from `desktop/`) in development.
The `ServerHandle` returned to `main.ts`:
```ts
interface ServerHandle {
url: string; // e.g. "http://127.0.0.1:4820"
port: number;
ownedByUs: boolean; // false when an existing server was adopted
stop: () => Promise<void>;
}
```
### Port Discovery & Adoption
On startup `server-host.ts` picks a port, then either adopts an already-healthy server or boots its own. **Adoption** -- `probePort()` connects to `:4820`, then checks that the listener answers `GET /api/health` with `{ status: "ok" }`. If a healthy dashboard server is already running there (e.g. the user ran `npm start` in a terminal), the desktop app **adopts** it rather than double-binding -- no SQLite contention. An adopted server is not owned by the app, so quitting the app leaves it running.
```mermaid
flowchart TD
start["startEmbeddedServer()"] --> forced{"CCAM_DESKTOP_BIND_PORT set?"}
forced -->|yes| bind["bind exactly that port<br/>(no adoption, no fallback)"]
forced -->|no| adopt{"healthy server<br/>already on :4820?"}
adopt -->|yes| reuse["adopt it<br/>ownedByUs = false"]
adopt -->|no| pick["pickFreePort()"]
pick --> p1{":4820 free?"}
p1 -->|yes| use4820["use 4820"]
p1 -->|no| p2{"any of<br/>:4821:4829 free?"}
p2 -->|yes| usefb["use that"]
p2 -->|no| p3{"any of<br/>:49152:49500 free?"}
p3 -->|yes| userand["use that"]
p3 -->|no| fail["throw — no free port"]
bind --> bootsrv["createApp() + startServer()"]
use4820 --> bootsrv
usefb --> bootsrv
userand --> bootsrv
bootsrv --> healthy["waitForHealthy()<br/>poll /api/health ≤ 30s"]
healthy --> bg["bootstrapOwnedServer()"]
bg --> handle["ServerHandle ownedByUs = true"]
reuse --> handleR["ServerHandle ownedByUs = false"]
style reuse fill:#9e6a03,stroke:#7d5300,color:#fff
style fail fill:#da3633,stroke:#b62324,color:#fff
```
Port preference order is **4820 → 48214829 → a random port in 4915249500**. Two environment overrides exist primarily for testing: `CCAM_DESKTOP_BIND_PORT` binds an exact port (disabling adoption and fallback, used by the smoke test), and `CCAM_DESKTOP_NO_ADOPT=1` always starts a fresh server. Before `require()`ing the server module, the host sets `NODE_ENV=production`, `DASHBOARD_PORT=<port>`, and `DASHBOARD_DATA_DIR=<userData>/data` (see [Writable Data Directory](#writable-data-directory) below) so the server reads them from `process.env`.
### Writable Data Directory
A packaged install directory is **read-only** in practice: on macOS a `.app` bundle installed under `/Applications`, code-signed, or run through **app translocation** cannot write to `Resources/app/`, and on Windows the NSIS install dir under `%ProgramFiles%` (or the read-only mount a portable build runs from) is no place for mutable state. The dashboard's SQLite database and the VAPID keypair (`server/lib/push.js`) are writable state, so they must not live inside the bundle / install dir. Before booting the embedded server, `server-host.ts` creates `app.getPath('userData')/data` and points the server at it via the `DASHBOARD_DATA_DIR` environment variable:
- `server/db.js` honors `DASHBOARD_DATA_DIR` for the SQLite file.
- `server/lib/push.js` honors it for the persisted VAPID keys.
The resulting per-user location is `~/Library/Application Support/Claude Code Monitor/data/` on macOS and `%APPDATA%\Claude Code Monitor\data\` on Windows. Because this lives outside the bundle / install dir, imported history and persisted events **survive an app reinstall or update** (the Windows NSIS uninstaller keeps this data by default). Without this, writing a database into the read-only install location failed on a packaged build and broke History Import and event persistence.
The standalone `node server/index.js` path is **unaffected**: `DASHBOARD_DATA_DIR` is unset there, and `server-host.ts` only sets it when it is not already defined -- so `server/db.js` falls back to its usual repo-relative default.
### Shell `PATH` Recovery (macOS)
This step is **macOS-only**. A macOS app launched from Finder, the Dock, or Login Items auto-start is spawned by `launchd`, which hands it a **minimal `PATH`** (roughly `/usr/bin:/bin:/usr/sbin:/sbin`) and does **not** source the user's shell profile. The dashboard's "Run Claude" feature (`server/routes/run.js`, `server/lib/run-spawner.js`) spawns the `claude` CLI, which is almost always installed somewhere only the shell `PATH` knows about (`/opt/homebrew/bin`, `~/.local/bin`, `~/.claude/local`, a Node version-manager's bin dir). Under launchd's `PATH`, `claude` cannot be resolved or spawned.
`shell-path.ts` repairs this **before the server boots**: at startup it runs the user's login+interactive shell once (`$SHELL -ilc`, so `.zprofile`/`.zshrc` are sourced), captures the resulting `PATH` between sentinel markers, and merges it -- plus a fallback list of common CLI install directories -- onto `process.env.PATH`. The merge is order-preserving and deduplicated, so it is idempotent. Because the embedded server runs in the same process, it and every `claude` it spawns inherit the corrected `PATH`. (A `claude` shell _alias_ or _function_ still cannot be spawned -- only a real executable on the `PATH` can.)
On **Windows** there is no equivalent step: a process launched from Explorer, the Start menu, or the `HKCU\…\Run` startup entry already inherits the full user `PATH`, so the embedded server can resolve `claude` directly.
### `better-sqlite3` Native-Module Handling
`better-sqlite3` is the only **native** module in the dependency tree, and a native module must be compiled against the exact Node ABI it runs on. The repo-root copy is built for the **system Node** (so `npm run test:server` works for contributors); Electron ships its **own Node ABI**.
The desktop workspace solves this without disturbing the root install: the desktop workspace has its own `better-sqlite3`, rebuilt for Electron's Node ABI by `electron-builder install-app-deps` (run in its `postinstall`). `server-host.ts` then installs a one-time, **process-local** patch to `Module._resolveFilename` that redirects `require("better-sqlite3")` -- from anywhere in the embedded server -- to that ABI-correct copy.
Desktop setup is **guarded** so a missing or unbuilt binary never escapes as a raw node-gyp trace or a runtime crash: `desktop/scripts/preflight.js` (shared by `install.js` and `prebuild.js`) verifies the Electron-ABI binary exists and, when it doesn't, prints actionable, copy-pasteable setup help -- the per-OS C++ toolchain prerequisites (or a no-toolchain alternative that fetches Electron's prebuilt binary directly) -- before exiting non-zero. `desktop:install` runs this on install; the `prebuild` gate enforces it before every `desktop:*` build, turning a would-be runtime failure into a build-time error.
```mermaid
flowchart TD
subgraph desk["desktop/node_modules"]
d1["better-sqlite3<br/>rebuilt for Electron's ABI<br/>(electron-builder install-app-deps)"]
end
subgraph root["node_modules (repo root)"]
r1["better-sqlite3<br/>built for system Node<br/>(used by npm run test:server)"]
end
patch["ensureNativeModulesPatched()<br/>overrides Module._resolveFilename"]
srv["server/db.js<br/>require('better-sqlite3')"]
srv -->|"request intercepted"| patch
patch -->|"redirected to"| d1
patch -.->|"everything else<br/>passes through"| root
style d1 fill:#238636,stroke:#196c2e,color:#fff
style patch fill:#1f6feb,stroke:#1158c7,color:#fff
```
- The patch is installed exactly once, **before** `server/index.js` is `require()`d, and rewrites _only_ `require("better-sqlite3")` -- every other module resolves normally.
- `electron-builder.yml` therefore **excludes** the root `better-sqlite3` from the bundle (it would trip `@electron/universal`'s identical-file detector) and `asarUnpack`s the desktop copy (native `.node` files cannot live inside an `asar` archive).
- The `compat-sqlite` (`node:sqlite`) fallback remains a safety net -- one reason the desktop app pins **Electron 35**, whose bundled Node 22.16 has `node:sqlite`.
### Background Services & Hook Bootstrap
`node server/index.js` runs its production bootstrap from an `if (require.main === module)` block. Because the desktop app **`require()`s** that module, the block never fires -- so the bootstrap was extracted into an exported `startBackgroundServices()` that both paths call. This is a **behavior-preserving refactor** of `server/index.js`: the standalone server path is functionally unchanged.
```mermaid
flowchart LR
subgraph standalone["node server/index.js"]
s1["require.main === module"] --> s2["startBackgroundServices()"]
end
subgraph desktopapp["desktop app"]
d1["server-host.ts<br/>bootstrapOwnedServer()"] --> d2["startBackgroundServices()"]
d1 --> d3["installHooks()"]
end
d2 --> svc
s2 --> svc
subgraph svc["Background services"]
u["update scheduler"]
w["cc-watcher (Claude config watcher)"]
r["orphaned-run reconciliation"]
end
style d1 fill:#1f6feb,stroke:#1158c7,color:#fff
```
`bootstrapOwnedServer()` runs **once** -- guarded by a module-level flag so a `Restart Server` does not double-register schedulers or watchers -- and:
1. Calls `startBackgroundServices()` -- the update scheduler, the `cc-watcher` config watcher, and one-time orphaned-run reconciliation.
2. Calls `installHooks()` -- writes the Claude Code hook configuration to `~/.claude/settings.json`, so an install-only user (DMG on macOS, `.exe` on Windows) gets events flowing without ever running `npm run install-hooks` from a checkout.
It runs only when the server is **owned** by the app -- an adopted server has already done its own bootstrap.
### App Lifecycle
```mermaid
sequenceDiagram
autonumber
participant OS as macOS / Windows
participant Main as main.ts
participant Host as server-host.ts
participant Srv as server/index.js
participant UI as BrowserWindow
OS->>Main: launch app
Main->>Main: setAppUserModelId (win32) · requestSingleInstanceLock()
alt lock not acquired
Main->>OS: exit(0) — focus existing instance
end
Main->>Host: ensureUserPath() — recover login-shell PATH (macOS only)
Main->>Host: startEmbeddedServer()
Host->>Host: probe :4820 — adopt if a healthy server answers
alt no server to adopt
Host->>Host: pickFreePort() · set DASHBOARD_DATA_DIR · patch better-sqlite3 ABI
Host->>Srv: require() · createApp() · startServer(port)
Host->>Srv: waitForHealthy() — poll /api/health ≤ 30s
Host->>Srv: bootstrapOwnedServer() — schedulers, cc-watcher, install hooks
end
Host-->>Main: ServerHandle { url, port, ownedByUs, stop }
Main->>Main: installApplicationMenu() · createTray()
alt launched at login (--ccam-hidden / wasOpenedAtLogin)
Main->>OS: stay tray-only, hide dock (macOS)
else normal launch
Main->>UI: createDashboardWindow(url)
UI->>Srv: GET http://127.0.0.1:port
end
Note over Main: window "close" → hide (server keeps running)
Note over Main: ⌘Q / Ctrl+Q → confirm (second press bypasses)
Note over Main: before-quit → stop owned server + closeEmbeddedDatabase()
```
| Event | Behavior |
| --- | --- |
| **Second launch** | `requestSingleInstanceLock()` (enabled on **every platform**) fails -- the new process exits and the existing window is focused. |
| **Window close** | Intercepted -- the window **hides** (`hide()`); the server and tray keep running. |
| **`window-all-closed`** | App stays alive in tray-only mode (the handler is intentionally a no-op). |
| **Launched at login** | The dashboard window is **not** shown -- only the tray icon. Detected via macOS `wasOpenedAtLogin` (dock hidden, `openAsHidden`) or, on Windows, the `--ccam-hidden` arg written into the `HKCU\…\Run` startup command. |
| **Quit shortcut** | ⌘Q (macOS) / Ctrl+Q (Windows) shows a confirmation dialog; a second press bypasses it. |
| **`before-quit`** | If the server is owned: stop the HTTP server, then `closeEmbeddedDatabase()` for a clean WAL checkpoint, then `app.exit(0)`. The DB handle is closed here -- never on `Restart Server`, where the cached `server/db.js` singleton must stay usable. |
### Packaged App Layout
`electron-builder` produces `Claude Code Monitor.app` on macOS and `Claude Code Monitor.exe` (NSIS installer + portable) on Windows. On both platforms the Electron main-process code is compiled (`tsc``out/`) and packed into `app.asar`; the rest of the repo is shipped as **`extraResources`** -- plain files under the bundle's `Resources/app/` (macOS) or the install dir's `resources\app\` (Windows). The internal layout is the same shape on both:
```mermaid
flowchart TD
appbundle["Claude Code Monitor.app (macOS)<br/>Claude Code Monitor install dir (Windows)"]
appbundle --> contents["Contents/ (macOS)<br/>install root (Windows)"]
contents --> macos["MacOS/ — Electron binary (macOS)<br/>Claude Code Monitor.exe (Windows)"]
contents --> res["Resources/ (macOS)<br/>resources\ (Windows)"]
res --> asar["app.asar<br/>(compiled out/**, package.json)"]
res --> unpacked["app.asar.unpacked/<br/>node_modules/better-sqlite3 (.node)"]
res --> appdir["app/"]
appdir --> a1["server/ — Express server (no tests)"]
appdir --> a2["client/dist/ — built React SPA"]
appdir --> a3["scripts/ — hook-handler, install-hooks"]
appdir --> a4["node_modules/ — server runtime deps"]
appdir --> a5["package.json"]
style asar fill:#1f6feb,stroke:#1158c7,color:#fff
style appdir fill:#238636,stroke:#196c2e,color:#fff
```
At runtime `server-host.ts` resolves this root as `process.resourcesPath/app` when packaged, on both platforms. Everything under the packaged `app/` is **read-only** on a packaged, signed, or app-translocated macOS bundle and on a Windows install under `%ProgramFiles%` (or a portable build's mount) -- so all writable state (the SQLite database, VAPID keys) lives in the per-user data dir (`~/Library/Application Support/Claude Code Monitor/data/` on macOS, `%APPDATA%\Claude Code Monitor\data\` on Windows), **never inside the bundle / install dir** (see [Writable Data Directory](#writable-data-directory)).
On macOS `electron-builder` produces **two per-architecture DMGs** — one `arm64` (Apple Silicon), one `x64` (Intel) — via `--mac --arm64 --x64` (not a merged universal binary; the release ships both), ad-hoc signed by default so anyone can build a working `.dmg` without a paid Apple Developer account; real Developer ID signing and notarization are opt-in via environment variables (`CSC_LINK`, `APPLE_ID`, etc.). On Windows it produces an **NSIS installer `.exe`** and a **no-install portable `.exe`** (both x64), using `assets/icon.ico` (generated from the source PNG by `desktop/scripts/build-win-icon.ps1`) as the application and tray icon. **`electron-builder` packages for the host OS** -- DMGs build on macOS, Windows `.exe`s build on Windows -- so the two artifacts come from two CI jobs (see below). The `desktop/scripts/prebuild.js` guard also **self-heals** a `better-sqlite3` native binary that a prior cross-arch DMG build (`electron-builder --mac --x64/--arm64`) left compiled for the wrong CPU architecture -- it detects the mismatch via `file` and re-runs `electron-builder install-app-deps`, so `desktop:dev` and `desktop:test` do not fail with `ERR_DLOPEN_FAILED`; on Windows it shells the `.cmd` shims for `npm`/`npx`. CI runs a path-filtered `🍎 macOS Desktop (DMG)` job on `macos-latest` (artifact `ClaudeCodeMonitor-dmg`) and a `🪟 Windows Desktop (EXE)` job on `windows-latest` (artifact `ClaudeCodeMonitor-win`); the release attaches both. See [`desktop/README.md`](./desktop/README.md) for the full build pipeline, build-performance notes, and signing details.
### Relation to Standalone Deployment
The desktop app is a fourth deployment mode alongside Development, Production, and Container (see [Deployment Modes](#deployment-modes)). The data path is **identical to the standalone Production path** -- Claude Code hooks `POST /api/hooks/event` to the embedded Express server, which writes to SQLite and broadcasts over WebSocket to the renderer. The only structural difference is that the server runs inside the Electron main process instead of a standalone `node server/index.js`, and the renderer is a `BrowserWindow` rather than a browser tab pointed at the same origin.
---
## Security Considerations ## Security Considerations
| Area | Approach | | Area | Approach |
@@ -2687,26 +2370,6 @@ graph LR
| **File watching** | `node --watch` + Vite HMR | None | | **File watching** | `node --watch` + Vite HMR | None |
| **Source maps** | Inline | External files | | **Source maps** | Inline | External files |
### Desktop App (macOS & Windows)
The native desktop app (macOS `.app`/`.dmg`, Windows NSIS / portable `.exe`) is a self-contained deployment mode: a single Electron process embeds the Express server in-process and renders the React client in a `BrowserWindow`. No terminal, no separate `npm start`.
```mermaid
graph LR
LAUNCH["Open Claude Code Monitor<br/>(.app / .exe)"] --> MAIN["Electron main process<br/>(Node 22 / Electron 35)"]
MAIN --> HOST["server-host.ts<br/>port discovery + adopt"]
HOST --> SERVER["server/index.js (in-process)<br/>Port 4820 → fallback"]
SERVER -->|serves| DIST["client/dist/<br/>(extraResources)"]
MAIN --> WIN["BrowserWindow"]
WIN --> SERVER
style MAIN fill:#1f6feb,stroke:#1158c7,color:#fff
style SERVER fill:#339933,stroke:#5cb85c,color:#fff
style DIST fill:#646CFF,stroke:#818cf8,color:#fff
```
The hook ingestion path (Claude Code hooks → `POST /api/hooks/event` → SQLite → WebSocket) is **identical to the standalone Production path** -- only the process that hosts the server differs. See [Desktop App Architecture](#desktop-app-architecture-macos--windows--electron) for the full design.
### MCP Sidecar (Optional) ### MCP Sidecar (Optional)
The MCP server runs as a sidecar alongside the dashboard, connecting to the same API. It supports three transport modes: The MCP server runs as a sidecar alongside the dashboard, connecting to the same API. It supports three transport modes:
@@ -2792,58 +2455,6 @@ docker run -d -p 4820:4820 \
> [!NOTE] > [!NOTE]
> **Hook note:** Claude Code hooks run on the host, not inside the container. The containerized server still receives hook events via HTTP on `localhost:4820` — run `npm run install-hooks` on the host after the container is up. `scripts/install-hooks.js` detects container execution and refuses there (issue #193) so it cannot write a container-internal handler path into a bind-mounted host `~/.claude`; the containerized server's boot-time auto-install is skipped for the same reason. Override with `CCAM_ALLOW_CONTAINER_HOOKS=1` only when Claude Code itself runs inside the container. > **Hook note:** Claude Code hooks run on the host, not inside the container. The containerized server still receives hook events via HTTP on `localhost:4820` — run `npm run install-hooks` on the host after the container is up. `scripts/install-hooks.js` detects container execution and refuses there (issue #193) so it cannot write a container-internal handler path into a bind-mounted host `~/.claude`; the containerized server's boot-time auto-install is skipped for the same reason. Override with `CCAM_ALLOW_CONTAINER_HOOKS=1` only when Claude Code itself runs inside the container.
### Cloud Deployment
For production cloud deployments, the `deployments/` directory provides enterprise-grade infrastructure supporting four cloud providers and multiple deployment strategies.
```mermaid
graph TB
subgraph "Deployment Pipeline"
direction LR
CI["CI Pipeline<br/>Build · Test · Scan"] --> DEPLOY["Deployment<br/>Helm · Kustomize · Terraform"]
DEPLOY --> VERIFY["Verification<br/>Health Check · Smoke Tests"]
VERIFY -->|Fail| ROLLBACK["Rollback<br/>Instant Revert"]
end
subgraph "Infrastructure"
direction TB
subgraph "Compute"
BLUE["Blue Slot<br/>Current Version"]
GREEN["Green Slot<br/>New Version"]
end
LB["Load Balancer<br/>TLS 1.3 · WebSocket<br/>Weighted Routing"]
PV["Persistent Storage<br/>Encrypted NFS"]
MON["Monitoring<br/>Prometheus · Grafana<br/>13 Alert Rules"]
OTEL["OTel Collector<br/>Coralogix"]
end
LB -->|"Active"| BLUE
LB -.->|"Standby"| GREEN
BLUE & GREEN --> PV
MON -->|"Scrape"| BLUE & GREEN
BLUE & GREEN -->|"logs + metrics + traces"| OTEL
style BLUE fill:#2563eb,color:#fff
style GREEN fill:#16a34a,color:#fff
style LB fill:#7c3aed,color:#fff
style CI fill:#2088ff,color:#fff
style OTEL fill:#4f46e5,color:#fff
```
| Capability | Details |
| --- | --- |
| **Cloud Providers** | AWS (ECS Fargate + ALB), GCP (Cloud Run + GCLB), Azure (ACI + App Gateway), OCI (OKE + LBaaS) |
| **Deployment Methods** | Helm chart, Kustomize overlays, Terraform modules |
| **Release Strategies** | Rolling update, blue-green (instant switchover), canary (automated analysis) |
| **Environments** | Dev, staging, production with per-environment configuration |
| **CI/CD** | GitHub Actions and GitLab CI pipelines with Trivy security scanning |
| **Observability** | Prometheus scraping, 13 alert rules, Grafana dashboard (16 panels), Alertmanager routing, Coralogix full-stack observability (logs, metrics, traces, SLO tracking) via OpenTelemetry Collector |
| **Operations** | Scripts for deploy, rollback, blue-green switch, database backup/restore, teardown |
| **Security** | Restricted PSS, network policies, TLS enforcement, OIDC auth, no long-lived credentials |
> [!NOTE]
> 📘 **Full guide:** See [DEPLOYMENT.md](DEPLOYMENT.md) for step-by-step deployment instructions, and [deployments/README.md](deployments/README.md) for the infrastructure technical reference.
--- ---
## Statusline Utility ## Statusline Utility
-1080
View File
File diff suppressed because it is too large Load Diff
-253
View File
@@ -1,253 +0,0 @@
# 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*` |
+1 -140
View File
@@ -1,6 +1,6 @@
# Installation # 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). A step-by-step guide to get the Claude Code Agent Monitor up and running on your machine, with optional sections for importing history and running in a container.
## Fastest path — install it as a Claude Code plugin ## Fastest path — install it as a Claude Code plugin
@@ -189,122 +189,6 @@ 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 ## Optional: Local MCP server
If you want AI agents to call dashboard functionality through MCP tools, run the local MCP server in `mcp/`: If you want AI agents to call dashboard functionality through MCP tools, run the local MCP server in `mcp/`:
@@ -469,17 +353,6 @@ If you see an error box at startup saying *"SQLite backend not available"*, eith
Then run: `npm rebuild better-sqlite3` 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 ### `npm run dev` fails immediately
Ensure both server and client dependencies are installed: Ensure both server and client dependencies are installed:
@@ -503,18 +376,6 @@ The Vite dev server and Express server run on different ports. Make sure both ar
See [SETUP.md — Troubleshooting](./SETUP.md#troubleshooting) for detailed hook debugging steps. 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 ## Ports
+2 -3
View File
@@ -138,8 +138,7 @@ Both must be green before a commit; the pre-commit hook runs them plus Prettier.
| `client/` | React 18 + Vite + Tailwind dashboard | | `client/` | React 18 + Vite + Tailwind dashboard |
| `bin/ccam.js` | CLI | | `bin/ccam.js` | CLI |
| `mcp/` | MCP server exposing read-only dashboard tools | | `mcp/` | MCP server exposing read-only dashboard tools |
| `desktop/` | Electron wrapper that embeds the server | | `docs/` | Architecture, API, lanes, database |
| `docs/` | Architecture, API, lanes, database, deployment |
| `plugins/` | Claude Code plugins shipped with the dashboard | | `plugins/` | Claude Code plugins shipped with the dashboard |
## Docs ## Docs
@@ -148,5 +147,5 @@ Both must be green before a commit; the pre-commit hook runs them plus Prettier.
- [`docs/LANES.md`](docs/LANES.md) — lanes, pipelines, stage detection - [`docs/LANES.md`](docs/LANES.md) — lanes, pipelines, stage detection
- [`docs/API.md`](docs/API.md) — REST endpoints (`openapi.yaml` is generated) - [`docs/API.md`](docs/API.md) — REST endpoints (`openapi.yaml` is generated)
- [`docs/DATABASE.md`](docs/DATABASE.md) — tables and migrations - [`docs/DATABASE.md`](docs/DATABASE.md) — tables and migrations
- [`INSTALL.md`](INSTALL.md) · [`DEPLOYMENT.md`](DEPLOYMENT.md) · [`DESKTOP.md`](DESKTOP.md) - [`INSTALL.md`](INSTALL.md)
- [`CLAUDE.md`](CLAUDE.md) — the rules an agent working in this repo must follow - [`CLAUDE.md`](CLAUDE.md) — the rules an agent working in this repo must follow
+1 -99
View File
@@ -118,7 +118,7 @@ DASHBOARD_PORT=9000 npm run dev
> [!NOTE] > [!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. > 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. > Multiple dashboards can run side by side — for example two `npm run dev` checkouts, or `npm run dev` alongside `npm start`. 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. > 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.
> >
@@ -217,59 +217,6 @@ The dashboard, landing page, and wiki each ship as independent Progressive Web A
**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. **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 ## Database
@@ -607,48 +554,3 @@ If the build fails in Stage 1 with `better-sqlite3` errors, this is expected and
- Ensure you are using the latest Dockerfile (it should use `node:22-alpine` and **not** install `python3`, `make`, or `g++`) - 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 - Run `docker build --no-cache -t agent-monitor .` to force a clean rebuild
- Check that `package.json` has `better-sqlite3` under `optionalDependencies`, not `dependencies` - 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*.
-5
View File
@@ -35,11 +35,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `./components/Layout` * - `./components/Layout`
* - `./components/SplashScreen` * - `./components/SplashScreen`
-5
View File
@@ -20,11 +20,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `./StatusBadge` * - `./StatusBadge`
* - `../lib/types` * - `../lib/types`
@@ -27,11 +27,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../lib/api` * - `../lib/api`
* - `../lib/eventBus` * - `../lib/eventBus`
-5
View File
@@ -31,11 +31,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Public surface
* - `CheckboxProps` exported API; see TSDoc on the symbol for behavior. * - `CheckboxProps` exported API; see TSDoc on the symbol for behavior.
* - `Checkbox` exported API; see TSDoc on the symbol for behavior. * - `Checkbox` exported API; see TSDoc on the symbol for behavior.
-5
View File
@@ -20,11 +20,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Public surface
* - `DateTimePicker` exported API; see TSDoc on the symbol for behavior. * - `DateTimePicker` exported API; see TSDoc on the symbol for behavior.
* *
-5
View File
@@ -35,11 +35,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Public surface
* - `EmptyStateProps` exported API; see TSDoc on the symbol for behavior. * - `EmptyStateProps` exported API; see TSDoc on the symbol for behavior.
* - `EmptyState` exported API; see TSDoc on the symbol for behavior. * - `EmptyState` exported API; see TSDoc on the symbol for behavior.
-5
View File
@@ -24,11 +24,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../lib/types` * - `../lib/types`
* - `../lib/event-grouping` * - `../lib/event-grouping`
-5
View File
@@ -24,11 +24,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../lib/api` * - `../lib/api`
* - `./DateTimePicker` * - `./DateTimePicker`
@@ -28,11 +28,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `./StatusBadge` * - `./StatusBadge`
* *
-5
View File
@@ -29,11 +29,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Public surface
* - `FieldHelpProps` exported API; see TSDoc on the symbol for behavior. * - `FieldHelpProps` exported API; see TSDoc on the symbol for behavior.
* - `FieldHelp` exported API; see TSDoc on the symbol for behavior. * - `FieldHelp` exported API; see TSDoc on the symbol for behavior.
-5
View File
@@ -29,11 +29,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../lib/api` * - `../lib/api`
* - `../lib/eventBus` * - `../lib/eventBus`
-5
View File
@@ -35,11 +35,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `./Sidebar` * - `./Sidebar`
* - `./UpdateNotifier` * - `./UpdateNotifier`
-5
View File
@@ -30,11 +30,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../lib/api` * - `../lib/api`
* - `../lib/eventBus` * - `../lib/eventBus`
-5
View File
@@ -29,11 +29,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Public surface
* - `SelectOption` exported API; see TSDoc on the symbol for behavior. * - `SelectOption` exported API; see TSDoc on the symbol for behavior.
* - `SelectProps` exported API; see TSDoc on the symbol for behavior. * - `SelectProps` exported API; see TSDoc on the symbol for behavior.
-5
View File
@@ -23,11 +23,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `./StatusBadge` * - `./StatusBadge`
* - `../lib/types` * - `../lib/types`
@@ -24,11 +24,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../lib/api` * - `../lib/api`
* - `../lib/eventBus` * - `../lib/eventBus`
-5
View File
@@ -20,11 +20,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../lib/api` * - `../lib/api`
* - `../lib/eventBus` * - `../lib/eventBus`
-5
View File
@@ -24,11 +24,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Public surface
* - `Skeleton` exported API; see TSDoc on the symbol for behavior. * - `Skeleton` exported API; see TSDoc on the symbol for behavior.
* - `StatValueSkeleton` exported API; see TSDoc on the symbol for behavior. * - `StatValueSkeleton` exported API; see TSDoc on the symbol for behavior.
-5
View File
@@ -27,11 +27,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Public surface
* - `SplashScreen` exported API; see TSDoc on the symbol for behavior. * - `SplashScreen` exported API; see TSDoc on the symbol for behavior.
* *
-5
View File
@@ -28,11 +28,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `./Tip` * - `./Tip`
* - `./Skeleton` * - `./Skeleton`
-5
View File
@@ -20,11 +20,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../lib/types` * - `../lib/types`
* - `./Tip` * - `./Tip`
@@ -26,11 +26,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `./brain` * - `./brain`
* *
@@ -29,11 +29,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Public surface
* - `SpeechBubble` exported API; see TSDoc on the symbol for behavior. * - `SpeechBubble` exported API; see TSDoc on the symbol for behavior.
* *
-5
View File
@@ -29,11 +29,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `./CatAvatar` * - `./CatAvatar`
* - `./SpeechBubble` * - `./SpeechBubble`
@@ -23,11 +23,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `./brain` * - `./brain`
* *
-5
View File
@@ -24,11 +24,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../../lib/types` * - `../../lib/types`
* *
-5
View File
@@ -23,11 +23,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `./brain` * - `./brain`
* *
-5
View File
@@ -22,11 +22,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Public surface
* - `TabbyPos` exported API; see TSDoc on the symbol for behavior. * - `TabbyPos` exported API; see TSDoc on the symbol for behavior.
* - `tabbyPrefs` exported API; see TSDoc on the symbol for behavior. * - `tabbyPrefs` exported API; see TSDoc on the symbol for behavior.
-5
View File
@@ -22,11 +22,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `./brain` * - `./brain`
* *
@@ -23,11 +23,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../../lib/eventBus` * - `../../lib/eventBus`
* - `../../lib/api` * - `../../lib/api`
@@ -25,11 +25,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `./prefs` * - `./prefs`
* *
-5
View File
@@ -32,11 +32,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Public surface
* - `Tip` exported API; see TSDoc on the symbol for behavior. * - `Tip` exported API; see TSDoc on the symbol for behavior.
* *
-5
View File
@@ -33,11 +33,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../lib/api` * - `../lib/api`
* - `../lib/eventBus` * - `../lib/eventBus`
@@ -27,11 +27,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../lib/api` * - `../lib/api`
* - `./Select` * - `./Select`
@@ -22,11 +22,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../../lib/highlight` * - `../../lib/highlight`
* *
@@ -25,11 +25,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../../lib/api` * - `../../lib/api`
* - `../../lib/eventBus` * - `../../lib/eventBus`
@@ -29,11 +29,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `./CodeBlock` * - `./CodeBlock`
* *
@@ -24,11 +24,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../../lib/types` * - `../../lib/types`
* - `./ToolCallBlock` * - `./ToolCallBlock`
@@ -24,11 +24,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../../lib/types` * - `../../lib/types`
* - `./CodeBlock` * - `./CodeBlock`
@@ -22,11 +22,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Public surface
* - `ToolStyle` exported API; see TSDoc on the symbol for behavior. * - `ToolStyle` exported API; see TSDoc on the symbol for behavior.
* - `styleForTool` exported API; see TSDoc on the symbol for behavior. * - `styleForTool` exported API; see TSDoc on the symbol for behavior.
@@ -24,11 +24,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Public surface
* - `TuiSegment` exported API; see TSDoc on the symbol for behavior. * - `TuiSegment` exported API; see TSDoc on the symbol for behavior.
* - `stripAnsi` exported API; see TSDoc on the symbol for behavior. * - `stripAnsi` exported API; see TSDoc on the symbol for behavior.
@@ -24,11 +24,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Public surface
* - `CopyButton` exported API; see TSDoc on the symbol for behavior. * - `CopyButton` exported API; see TSDoc on the symbol for behavior.
* - `Terminal` exported API; see TSDoc on the symbol for behavior. * - `Terminal` exported API; see TSDoc on the symbol for behavior.
@@ -36,11 +36,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `./primitives` * - `./primitives`
* *
-5
View File
@@ -23,11 +23,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../lib/types` * - `../lib/types`
* *
@@ -20,11 +20,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Public surface
* - `AgentCollaborationNetworkProps` exported API; see TSDoc on the symbol for behavior. * - `AgentCollaborationNetworkProps` exported API; see TSDoc on the symbol for behavior.
* - `AgentCollaborationNetwork` exported API; see TSDoc on the symbol for behavior. * - `AgentCollaborationNetwork` exported API; see TSDoc on the symbol for behavior.
@@ -25,11 +25,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../../lib/types` * - `../../lib/types`
* *
@@ -20,11 +20,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../../lib/types` * - `../../lib/types`
* *
@@ -20,11 +20,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../../lib/types` * - `../../lib/types`
* *
@@ -20,11 +20,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../../lib/types` * - `../../lib/types`
* - `../../lib/format` * - `../../lib/format`
@@ -20,11 +20,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../../lib/types` * - `../../lib/types`
* *
@@ -20,11 +20,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../../lib/types` * - `../../lib/types`
* - `../../lib/format` * - `../../lib/format`
@@ -20,11 +20,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../../lib/api` * - `../../lib/api`
* - `../../lib/format` * - `../../lib/format`
@@ -20,11 +20,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../../lib/types` * - `../../lib/types`
* *
@@ -20,11 +20,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../../lib/types` * - `../../lib/types`
* *
@@ -20,11 +20,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../../lib/types` * - `../../lib/types`
* *
@@ -29,11 +29,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../../lib/api` * - `../../lib/api`
* - `../../lib/eventBus` * - `../../lib/eventBus`
@@ -20,11 +20,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../../lib/types` * - `../../lib/types`
* *
-5
View File
@@ -20,11 +20,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../i18n` * - `../i18n`
* - `../lib/eventBus` * - `../lib/eventBus`
-5
View File
@@ -20,11 +20,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../lib/types` * - `../lib/types`
* - `../lib/eventBus` * - `../lib/eventBus`
-5
View File
@@ -35,11 +35,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `./locales/en/common.json` * - `./locales/en/common.json`
* - `./locales/vi/common.json` * - `./locales/vi/common.json`
+2
View File
@@ -94,6 +94,8 @@
"locks.held_one": "{{count}} lock held", "locks.held_one": "{{count}} lock held",
"locks.held_other": "{{count}} locks held", "locks.held_other": "{{count}} locks held",
"moreActions": "More actions", "moreActions": "More actions",
"pipelinePicker.label": "Pipeline template",
"pipelinePicker.stageMismatch": "Stage \"{{stage}}\" matches no node in \"{{pipeline}}\" — declare one of: {{nodes}}",
"preflightError": "Could not load the current lane facts.", "preflightError": "Could not load the current lane facts.",
"preflightErrorWithMessage": "Could not load the current lane facts: {{message}}", "preflightErrorWithMessage": "Could not load the current lane facts: {{message}}",
"runtime.boot": "▶ up", "runtime.boot": "▶ up",
+2
View File
@@ -94,6 +94,8 @@
"locks.held_one": "Đang giữ {{count}} khóa", "locks.held_one": "Đang giữ {{count}} khóa",
"locks.held_other": "Đang giữ {{count}} khóa", "locks.held_other": "Đang giữ {{count}} khóa",
"moreActions": "Thêm hành động", "moreActions": "Thêm hành động",
"pipelinePicker.label": "Mẫu pipeline",
"pipelinePicker.stageMismatch": "Stage \"{{stage}}\" không khớp node nào trong \"{{pipeline}}\" — khai báo một trong: {{nodes}}",
"preflightError": "Không thể tải trạng thái làn đường hiện tại.", "preflightError": "Không thể tải trạng thái làn đường hiện tại.",
"preflightErrorWithMessage": "Không thể tải trạng thái làn đường hiện tại: {{message}}", "preflightErrorWithMessage": "Không thể tải trạng thái làn đường hiện tại: {{message}}",
"runtime.boot": "▶ chạy", "runtime.boot": "▶ chạy",
-5
View File
@@ -78,11 +78,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `./types` * - `./types`
* - `./dataScope` * - `./dataScope`
-5
View File
@@ -36,11 +36,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Public surface
* - `ScopeMode` exported API; see TSDoc on the symbol for behavior. * - `ScopeMode` exported API; see TSDoc on the symbol for behavior.
* - `DataScope` exported API; see TSDoc on the symbol for behavior. * - `DataScope` exported API; see TSDoc on the symbol for behavior.
-5
View File
@@ -51,11 +51,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `./types` * - `./types`
* *
-5
View File
@@ -47,11 +47,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `./types` * - `./types`
* *
-5
View File
@@ -44,11 +44,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `./types` * - `./types`
* *
-5
View File
@@ -41,11 +41,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../i18n` * - `../i18n`
* *
-5
View File
@@ -61,11 +61,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Public surface
* - `TokenType` exported API; see TSDoc on the symbol for behavior. * - `TokenType` exported API; see TSDoc on the symbol for behavior.
* - `Token` exported API; see TSDoc on the symbol for behavior. * - `Token` exported API; see TSDoc on the symbol for behavior.
-5
View File
@@ -40,11 +40,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Public surface
* - `subscribeToPush` exported API; see TSDoc on the symbol for behavior. * - `subscribeToPush` exported API; see TSDoc on the symbol for behavior.
* - `unsubscribeFromPush` exported API; see TSDoc on the symbol for behavior. * - `unsubscribeFromPush` exported API; see TSDoc on the symbol for behavior.
-5
View File
@@ -20,11 +20,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Public surface
* - `SessionStatus` exported API; see TSDoc on the symbol for behavior. * - `SessionStatus` exported API; see TSDoc on the symbol for behavior.
* - `AgentStatus` exported API; see TSDoc on the symbol for behavior. * - `AgentStatus` exported API; see TSDoc on the symbol for behavior.
-5
View File
@@ -33,11 +33,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `./App` * - `./App`
* *
-5
View File
@@ -24,11 +24,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../lib/api` * - `../lib/api`
* - `../lib/eventBus` * - `../lib/eventBus`
-5
View File
@@ -20,11 +20,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../lib/api` * - `../lib/api`
* - `../lib/eventBus` * - `../lib/eventBus`
-5
View File
@@ -27,11 +27,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../lib/eventBus` * - `../lib/eventBus`
* - `../lib/api` * - `../lib/api`
-5
View File
@@ -20,11 +20,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../lib/api` * - `../lib/api`
* - `../lib/eventBus` * - `../lib/eventBus`
-5
View File
@@ -24,11 +24,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../lib/api` * - `../lib/api`
* - `../lib/eventBus` * - `../lib/eventBus`
-5
View File
@@ -26,11 +26,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Public surface
* - `NotFound` exported API; see TSDoc on the symbol for behavior. * - `NotFound` exported API; see TSDoc on the symbol for behavior.
* *
-5
View File
@@ -20,11 +20,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../lib/api` * - `../lib/api`
* - `../lib/eventBus` * - `../lib/eventBus`
-5
View File
@@ -20,11 +20,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../lib/api` * - `../lib/api`
* - `../lib/eventBus` * - `../lib/eventBus`
-5
View File
@@ -20,11 +20,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../lib/api` * - `../lib/api`
* - `../lib/eventBus` * - `../lib/eventBus`
-5
View File
@@ -20,11 +20,6 @@
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * 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. * 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 * ## Internal dependencies
* - `../lib/api` * - `../lib/api`
* - `../lib/eventBus` * - `../lib/eventBus`
+55 -2
View File
@@ -132,6 +132,9 @@ export function Workspace() {
const [addLaneOpen, setAddLaneOpen] = useState(false); const [addLaneOpen, setAddLaneOpen] = useState(false);
const [viewedFeatureSlug, setViewedFeatureSlug] = useState<string | null>(null); const [viewedFeatureSlug, setViewedFeatureSlug] = useState<string | null>(null);
const [features, setFeatures] = useState<LaneFeature[]>([]); const [features, setFeatures] = useState<LaneFeature[]>([]);
const [pipelineTemplates, setPipelineTemplates] = useState<
{ id: string; name: string; nodes: { id: string }[] }[]
>([]);
const [viewedFeature, setViewedFeature] = useState<LaneFeature | null>(null); const [viewedFeature, setViewedFeature] = useState<LaneFeature | null>(null);
const [proofFeatures, setProofFeatures] = useState<ProofFeature[]>([]); const [proofFeatures, setProofFeatures] = useState<ProofFeature[]>([]);
@@ -202,7 +205,10 @@ export function Workspace() {
api.run api.run
.binary() .binary()
.then(setBinaryStatus) .then(setBinaryStatus)
.catch(() => setBinaryStatus({ found: false, path: null })); // Fetch failure (server unreachable, proxy misrouted, etc.) isn't proof
// `claude` is missing from PATH — leave the probe unresolved rather than
// showing a misleading "claude missing" banner for an unrelated fault.
.catch(() => undefined);
api.run api.run
.list() .list()
.then(setActiveRuns) .then(setActiveRuns)
@@ -211,6 +217,10 @@ export function Workspace() {
.history(50) .history(50)
.then((r) => setRunHistory(r.items)) .then((r) => setRunHistory(r.items))
.catch(() => undefined); .catch(() => undefined);
api.lanes
.pipelines()
.then((r) => setPipelineTemplates(r.pipelines))
.catch(() => undefined);
void refreshLanes(); void refreshLanes();
api.run api.run
.cwds() .cwds()
@@ -856,6 +866,28 @@ export function Workspace() {
} }
}; };
// Mirrors `ccam lanes pipeline <template> <id>`: same PATCH, same
// stage-mismatch warning when the current declared stage matches no node
// in the newly chosen template.
const handlePipelineChange = async (id: number, pipeline: string) => {
setLaneActionError(null);
try {
const { lane } = await api.lanes.update(id, { pipeline });
await refreshLanes();
if (!lane.pipeline_nodes.some((n) => n.state === "current")) {
setLaneActionError(
tLanes("pipelinePicker.stageMismatch", {
stage: lane.stage,
pipeline: lane.pipeline,
nodes: lane.pipeline_nodes.map((n) => n.id).join(", "),
})
);
}
} catch (err) {
setLaneActionError(err instanceof Error ? err.message : tLanes("actionErrorUnknown"));
}
};
const consoleSection = ( const consoleSection = (
<> <>
{/* Always attached under the pipeline - no header, no collapse. The {/* Always attached under the pipeline - no header, no collapse. The
@@ -1036,7 +1068,28 @@ export function Workspace() {
<span className="truncate text-sm font-semibold text-fg-primary"> <span className="truncate text-sm font-semibold text-fg-primary">
{currentLane.title || currentLane.cwd} {currentLane.title || currentLane.cwd}
</span> </span>
<span className="text-[11px] text-fg-muted">{currentLane.pipeline_name}</span> <select
data-testid="pipeline-picker"
aria-label={tLanes("pipelinePicker.label")}
className="rounded border border-border bg-surface-1 px-2 py-0.5 text-xs text-fg-secondary disabled:opacity-60"
value={currentLane.pipeline}
disabled={!!viewedFeature}
title={
viewedFeature
? tLanes("features.viewingArchived", { slug: viewedFeature.slug })
: undefined
}
onChange={(e) => void handlePipelineChange(currentLane.id, e.target.value)}
>
{(pipelineTemplates.length
? pipelineTemplates
: [{ id: currentLane.pipeline, name: currentLane.pipeline_name, nodes: [] }]
).map((p) => (
<option key={p.id} value={p.id}>
{p.nodes.length ? `${p.name} (${p.nodes.length})` : p.name}
</option>
))}
</select>
{/* `stage` defaults to the DB sentinel "idle" until the driving {/* `stage` defaults to the DB sentinel "idle" until the driving
session ever calls `ccam stage` that string collides with session ever calls `ccam stage` that string collides with
`status`'s own "idle"/"running" vocabulary, so a lane that is `status`'s own "idle"/"running" vocabulary, so a lane that is
@@ -41,6 +41,10 @@ vi.mock("../../lib/api", async (importOriginal) => {
send: r({ messageId: "m-1" }), send: r({ messageId: "m-1" }),
kill: r({ ok: true }), kill: r({ ok: true }),
}, },
lanes: {
list: r({ lanes: [], counts: { total: 0, running: 0, needs_you: 0, dead: 0 } }),
pipelines: r({ pipelines: [] }),
},
ccConfig: { ccConfig: {
commands: r({ items: [] }), commands: r({ items: [] }),
plugins: r({ plugins: [] }), plugins: r({ plugins: [] }),
@@ -105,6 +105,15 @@ vi.mock("../../lib/api", async (importOriginal) => {
recordCall("POST", `/api/lanes/stage`); recordCall("POST", `/api/lanes/stage`);
return { ok: true }; return { ok: true };
}), }),
pipelines: vi.fn().mockImplementation(async () => {
recordCall("GET", "/api/lanes/pipelines");
return { pipelines: [{ id: "default", name: "default", nodes: [] }] };
}),
update: vi.fn().mockImplementation(async (id: number, patch: Record<string, unknown>) => {
recordCall("PATCH", `/api/lanes/${id}`);
const lane = lanesToReturn.find((l) => l.id === id);
return { lane: { ...lane, ...patch } };
}),
features: { features: {
list: vi.fn().mockImplementation(async (id: number) => { list: vi.fn().mockImplementation(async (id: number) => {
recordCall("GET", `/api/lanes/${id}/features`); recordCall("GET", `/api/lanes/${id}/features`);
@@ -387,6 +387,7 @@ vi.mock("../../lib/api", async (importOriginal) => {
create: r({ lane: {} }), create: r({ lane: {} }),
update: r({ lane: {} }), update: r({ lane: {} }),
stage: r({ lane: {} }), stage: r({ lane: {} }),
pipelines: r({ pipelines: [] }),
// A realistic shape: `{}` would crash blockingReason on `blocked` if any // A realistic shape: `{}` would crash blockingReason on `blocked` if any
// screen ever opened the destructive modal. // screen ever opened the destructive modal.
preflight: r({ preflight: r({
-559
View File
@@ -1,559 +0,0 @@
# Deployments
Production-ready, cloud-agnostic deployment infrastructure for the Claude Code Agent Monitor. Supports AWS, GCP, Azure, and OCI with Helm, Kustomize, and Terraform deployment methods, blue-green and canary release strategies, and full observability.
![Claude Code](https://img.shields.io/badge/Claude_Code-orange?style=flat-square&logo=claude&logoColor=white)
![Terraform](https://img.shields.io/badge/Terraform-%3E%3D1.5-844FBA?style=flat-square&logo=terraform&logoColor=white)
![Kubernetes](https://img.shields.io/badge/Kubernetes-%3E%3D1.24-326CE5?style=flat-square&logo=kubernetes&logoColor=white)
![Helm](https://img.shields.io/badge/Helm-3-0F1689?style=flat-square&logo=helm&logoColor=white)
![Kustomize](https://img.shields.io/badge/Kustomize-5.0-326CE5?style=flat-square&logo=kubernetes&logoColor=white)
![Prometheus](https://img.shields.io/badge/Prometheus-2.x-E6522C?style=flat-square&logo=prometheus&logoColor=white)
![Grafana](https://img.shields.io/badge/Grafana-10.x-F46800?style=flat-square&logo=grafana&logoColor=white)
![Nginx](https://img.shields.io/badge/Nginx-Ingress-009639?style=flat-square&logo=nginx&logoColor=white)
![Coralogix](https://img.shields.io/badge/Coralogix-Observability-1a1a2e?style=flat-square&logo=datadog&logoColor=white)
![OpenTelemetry](https://img.shields.io/badge/OpenTelemetry-Collector-4f46e5?style=flat-square&logo=opentelemetry&logoColor=white)
![AWS](https://img.shields.io/badge/AWS-ECS%20%7C%20RDS-232F3E?style=flat-square&logo=task&logoColor=white)
![Google Cloud](https://img.shields.io/badge/Google_Cloud-GKE%20%7C%20SQL-4285F4?style=flat-square&logo=googlecloud&logoColor=white)
![Azure](https://img.shields.io/badge/Azure-AKS%20%7C%20SQL-0078D4?style=flat-square&logo=cloudflare&logoColor=white)
![Oracle Cloud](https://img.shields.io/badge/Oracle_Cloud-OKE%20%7C%20DB-F80000?style=flat-square&logo=cloudways&logoColor=white)
![GitLab CI](https://img.shields.io/badge/GitLab_CI-pipelines-FC6D26?style=flat-square&logo=gitlab&logoColor=white)
![Make](https://img.shields.io/badge/Make-4.3-000000?style=flat-square&logo=make&logoColor=white)
![GitHub Actions](https://img.shields.io/badge/GitHub_Actions-pipelines-2088FF?style=flat-square&logo=githubactions&logoColor=white)
![VS Code](https://img.shields.io/badge/VS_Code-Extension-007ACC?style=flat-square&logo=vscodium&logoColor=white)
> **User-facing guide:** See [DEPLOYMENT.md](../DEPLOYMENT.md) in the project root for the step-by-step deployment guide with commands and workflows.
>
> This README is the **technical reference** for the infrastructure code in this directory.
---
## Infrastructure Architecture
```mermaid
graph TB
subgraph "deployments/"
direction TB
subgraph "Infrastructure Provisioning"
TF["terraform/<br/>Cloud resource provisioning<br/>AWS · GCP · Azure · OCI"]
end
subgraph "Application Deployment"
HELM["helm/<br/>Parameterized Helm chart<br/>12 templates · 4 value sets"]
KUST["kubernetes/<br/>Kustomize base + overlays<br/>11 resources · 3 envs"]
end
subgraph "Operations"
SCRIPTS["scripts/<br/>7 operational scripts<br/>deploy · rollback · backup"]
CI["ci/<br/>GitHub Actions + GitLab CI<br/>Build · Scan · Deploy"]
end
subgraph "Observability"
MON["monitoring/<br/>Prometheus · Grafana · Alertmanager · Coralogix<br/>13 rules · 16 panels · OTel Collector"]
end
end
TF -->|"Provisions cloud infra"| HELM & KUST
SCRIPTS -->|"Orchestrates"| HELM & KUST & TF
CI -->|"Automates"| SCRIPTS
MON -->|"Monitors"| HELM & KUST
style TF fill:#7b42bc,color:#fff
style HELM fill:#0f1689,color:#fff
style KUST fill:#326ce5,color:#fff
style SCRIPTS fill:#4caf50,color:#fff
style CI fill:#2088ff,color:#fff
style MON fill:#e6522c,color:#fff
```
## Directory Structure
```
deployments/
├── terraform/ # Infrastructure as Code (HashiCorp Terraform)
│ ├── main.tf # Root module — orchestrates all child modules
│ ├── variables.tf # Input variables with validation
│ ├── outputs.tf # Exported values (URLs, IDs, endpoints)
│ ├── versions.tf # Terraform + provider version constraints
│ ├── backend.tf # State backends (S3, GCS, Azure Blob, OCI S3)
│ ├── modules/ # Reusable, cloud-agnostic modules
│ │ ├── networking/ # VPC, subnets, security groups, NAT
│ │ ├── compute/ # Container orchestration (ECS/Cloud Run/ACI/OKE)
│ │ ├── database/ # Persistent storage for SQLite (EFS/Filestore/Azure Files/FSS)
│ │ ├── loadbalancer/ # Application LB with WebSocket + blue-green weighted routing
│ │ ├── monitoring/ # Metrics, logs, alerts, dashboards
│ │ └── secrets/ # Vault integration or cloud-native secret stores
│ ├── providers/ # Cloud-specific root configurations
│ │ ├── aws/ # ECS Fargate + ALB + EFS + CloudWatch
│ │ ├── gcp/ # Cloud Run + GCLB + Filestore + Cloud Monitoring
│ │ ├── azure/ # ACI + App Gateway + Azure Files + Azure Monitor
│ │ └── oci/ # OKE + LBaaS + FSS + OCI Monitoring
│ └── environments/ # Per-environment variable overrides
│ ├── dev/ # 1 replica, 256 CPU, monitoring off
│ ├── staging/ # 2 replicas, 512 CPU, monitoring on
│ └── production/ # 3 replicas, 1024 CPU, HA, blue-green
├── kubernetes/ # Kubernetes-native manifests (Kustomize)
│ ├── base/ # 11 shared base resources
│ ├── overlays/ # Environment-specific patches
│ │ ├── dev/
│ │ ├── staging/
│ │ └── production/
│ ├── strategies/ # Advanced deployment patterns
│ │ ├── blue-green/ # Zero-downtime slot switching
│ │ └── canary/ # Progressive traffic shifting
│ └── components/ # Optional add-ons (Kustomize components)
│ ├── mcp-sidecar/ # MCP server as a sidecar container
│ └── monitoring/ # Prometheus ServiceMonitor
├── helm/ # Helm chart (alternative to Kustomize)
│ └── agent-monitor/
│ ├── templates/ # Kubernetes resource templates
│ ├── values.yaml # Default values
│ ├── values-dev.yaml
│ ├── values-staging.yaml
│ └── values-production.yaml
├── scripts/ # Operational shell scripts
│ ├── deploy.sh # Main deployment orchestrator
│ ├── rollback.sh # Rollback to previous revision
│ ├── blue-green-switch.sh # Switch active blue/green slot
│ ├── health-check.sh # Comprehensive health verification
│ ├── db-backup.sh # SQLite backup (local + cloud upload)
│ ├── db-restore.sh # SQLite restore from backup
│ └── teardown.sh # Full environment teardown
├── monitoring/ # Observability stack configs
│ ├── prometheus/ # Scrape config + alert rules
│ ├── grafana/ # Dashboards + datasources
│ ├── alertmanager/ # Alert routing (Slack, PagerDuty, email)
│ └── coralogix/ # Full-stack observability (logs, metrics, traces, SLOs)
└── ci/ # CI/CD pipeline definitions
├── github-actions/ # GitHub Actions workflows
└── gitlab-ci/ # GitLab CI pipeline
```
## Architecture Overview
```mermaid
graph TB
subgraph Internet
USER["Users / API Clients"]
end
subgraph Cloud["Cloud Provider (AWS / GCP / Azure / OCI)"]
LB["Load Balancer<br/>TLS termination<br/>WebSocket upgrade<br/>Blue/Green routing"]
subgraph Cluster["Container Cluster"]
subgraph Blue["Blue Slot"]
B1["agent-monitor:blue"]
B_MCP["mcp-sidecar:blue"]
end
subgraph Green["Green Slot"]
G1["agent-monitor:green"]
G_MCP["mcp-sidecar:green"]
end
end
PV["Persistent Volume<br/>(EFS / Filestore / Azure Files / FSS)"]
SECRETS["Secret Store<br/>(Vault / Secrets Manager)"]
MON["Monitoring<br/>(Prometheus / Grafana)"]
OTEL["OTel Collector<br/>(Coralogix)"]
end
USER -->|HTTPS + WSS| LB
LB -->|active slot| Blue
LB -.->|standby| Green
B1 --> PV
G1 --> PV
B1 --> SECRETS
B_MCP -->|localhost:4820| B1
G_MCP -->|localhost:4820| G1
MON -->|scrape /api/health| Blue
MON -->|scrape /api/health| Green
Blue -->|logs + metrics| OTEL
Green -->|logs + metrics| OTEL
style Blue fill:#2563eb,stroke:#3b82f6,color:#fff
style Green fill:#16a34a,stroke:#22c55e,color:#fff
style LB fill:#7c3aed,stroke:#a78bfa,color:#fff
```
## Quick Start
### Option A: Helm (recommended for Kubernetes)
```bash
# Dev
helm install agent-monitor ./deployments/helm/agent-monitor \
-f ./deployments/helm/agent-monitor/values-dev.yaml \
-n agent-monitor --create-namespace
# Production
helm install agent-monitor ./deployments/helm/agent-monitor \
-f ./deployments/helm/agent-monitor/values-production.yaml \
-n agent-monitor --create-namespace
```
### Option B: Kustomize
```bash
# Dev
kubectl apply -k ./deployments/kubernetes/overlays/dev
# Production
kubectl apply -k ./deployments/kubernetes/overlays/production
```
### Option C: Terraform (full infra + app)
```bash
cd deployments/terraform/providers/aws # or gcp, azure, oci
terraform init
terraform plan -var-file=../../environments/production/terraform.tfvars
terraform apply -var-file=../../environments/production/terraform.tfvars
```
### Option D: Script orchestrator
```bash
./deployments/scripts/deploy.sh --env production --method helm --strategy rolling
```
## Deployment Strategies
### Rolling Update (default)
Zero-downtime rolling replacement. One pod at a time is replaced with the new version.
```bash
./deployments/scripts/deploy.sh --env production --method helm --strategy rolling
```
### Blue-Green
Two identical environments. Traffic switches instantly from blue to green after validation.
```mermaid
sequenceDiagram
participant Ops as Operator
participant LB as Load Balancer
participant Blue as Blue Slot (current)
participant Green as Green Slot (new)
Ops->>Green: Deploy new version
Ops->>Green: Run health checks
Green-->>Ops: Healthy ✔
Ops->>LB: Switch traffic → Green
LB-->>Blue: Drain connections
LB-->>Green: Route all traffic
Note over Blue: Keep as rollback target
```
```bash
# Deploy to inactive slot
./deployments/scripts/deploy.sh --env production --method helm --strategy blue-green
# Switch traffic
./deployments/scripts/blue-green-switch.sh --env production --target green
# Instant rollback
./deployments/scripts/blue-green-switch.sh --env production --target blue
```
### Canary
Progressive traffic shifting with automated analysis. Rolls back on metric degradation.
```bash
./deployments/scripts/deploy.sh --env production --method helm --strategy canary
```
## Cloud Provider Comparison
| Feature | AWS | GCP | Azure | OCI |
|---|---|---|---|---|
| Compute | ECS Fargate | Cloud Run / GKE | ACI / AKS | OKE |
| Load Balancer | ALB | GCLB | App Gateway | LBaaS |
| Persistent Storage | EFS | Filestore | Azure Files | FSS |
| Secrets | Secrets Manager | Secret Manager | Key Vault | Vault |
| Monitoring | CloudWatch | Cloud Monitoring | Azure Monitor | OCI Monitoring |
| DNS | Route 53 | Cloud DNS | Azure DNS | OCI DNS |
| TLS Certs | ACM | Managed Certs | App Gateway Certs | Certificates |
## Operations
### Health Checks
```bash
./deployments/scripts/health-check.sh --url https://monitor.example.com
./deployments/scripts/health-check.sh --url http://localhost:4820 --retries 30
```
### Backup & Restore
```bash
# Backup SQLite database
./deployments/scripts/db-backup.sh --env production --output ./backups/
./deployments/scripts/db-backup.sh --env production --upload s3://my-bucket/backups/
# Restore from backup
./deployments/scripts/db-restore.sh --env production --input ./backups/dashboard-20240101.db
```
### Rollback
```bash
# Helm rollback
./deployments/scripts/rollback.sh --env production --method helm --revision 3
# Kubernetes rollback
./deployments/scripts/rollback.sh --env production --method kustomize
```
### Teardown
```bash
./deployments/scripts/teardown.sh --env dev --method helm
```
## Monitoring
The monitoring stack provides:
- **Prometheus** scrape configuration and alert rules
- **Grafana** dashboard with request rate, latency, errors, WebSocket connections, resource usage
- **Alertmanager** routing to Slack, PagerDuty, and email
- **Coralogix** full-stack observability with log analytics (DataPrime), metrics, distributed tracing, SLO tracking, and error budget management via OpenTelemetry Collector
```mermaid
graph LR
APP["agent-monitor pods"] -->|metrics| PROM["Prometheus"]
APP -->|"logs + metrics"| OTEL["OTel Collector"]
PROM -->|query| GRAF["Grafana Dashboards"]
PROM -->|evaluate rules| AM["Alertmanager"]
OTEL -->|"OTLP gRPC"| CX["Coralogix"]
AM -->|critical| PD["PagerDuty"]
AM -->|warning| SLACK["Slack"]
AM -->|info| EMAIL["Email"]
CX -->|alerts| PD
CX -->|alerts| SLACK
style PROM fill:#e6522c,stroke:#e6522c,color:#fff
style GRAF fill:#f46800,stroke:#f46800,color:#fff
style AM fill:#e6522c,stroke:#e6522c,color:#fff
style CX fill:#1a1a2e,stroke:#1a1a2e,color:#fff
style OTEL fill:#4f46e5,stroke:#4f46e5,color:#fff
```
Deploy the monitoring stack:
```bash
# Apply Prometheus rules
kubectl apply -f ./deployments/monitoring/prometheus/rules/
# Import Grafana dashboard
# Upload monitoring/grafana/dashboards/agent-monitor.json via Grafana UI or API
# Apply Alertmanager config
kubectl create secret generic alertmanager-config \
--from-file=./deployments/monitoring/alertmanager/alertmanager.yaml
# Deploy Coralogix OTel Collector (optional)
helm repo add coralogix https://cgx.jfrog.io/artifactory/coralogix-charts-virtual
kubectl create secret generic coralogix-keys \
--namespace agent-monitor \
--from-literal=PRIVATE_KEY=<YOUR_CORALOGIX_KEY>
helm install coralogix-otel coralogix/opentelemetry \
--namespace agent-monitor \
-f ./deployments/monitoring/coralogix/values.yaml
```
## CI/CD
### GitHub Actions
Three workflows are provided:
| Workflow | Trigger | Purpose |
|---|---|---|
| `ci.yaml` | Push/PR to main | Lint, test, build, security scan |
| `deploy.yaml` | Tag `v*` or manual | Build → staging (auto) → production (manual) |
| `rollback.yaml` | Manual dispatch | Rollback to a specific revision |
### GitLab CI
Single `.gitlab-ci.yml` covering all stages from test through production deploy.
## Environment Variables
| Variable | Default | Description |
|---|---|---|
| `IMAGE_REGISTRY` | — | Container image registry URL |
| `IMAGE_TAG` | `latest` | Container image tag |
| `DASHBOARD_PORT` | `4820` | Dashboard API + UI port |
| `NODE_ENV` | `production` | Node.js environment |
| `MCP_TRANSPORT` | `stdio` | MCP transport mode (stdio/http/repl) |
| `MCP_HTTP_PORT` | `8819` | MCP HTTP server port |
| `TLS_CERT_ARN` | — | TLS certificate ARN/ID (cloud-specific) |
| `DOMAIN` | — | Public domain for ingress/DNS |
---
## Terraform Module Reference
The Terraform infrastructure is organized as reusable modules that work across all four cloud providers.
### Module Dependency Chain
```mermaid
graph LR
NET[networking/] --> DB[database/]
NET --> COMP[compute/]
NET --> LB[loadbalancer/]
DB --> COMP
COMP --> LB
COMP --> MON[monitoring/]
LB --> MON
style NET fill:#42a5f5,color:#fff
style DB fill:#66bb6a,color:#fff
style COMP fill:#ffa726,color:#fff
style LB fill:#ab47bc,color:#fff
style MON fill:#ef5350,color:#fff
```
### networking/
Provisions the cloud network foundation.
| Output | Description |
|--------|-------------|
| `vpc_id` | VPC / VNet / VCN identifier |
| `public_subnet_ids` | Subnets for load balancers |
| `private_subnet_ids` | Subnets for containers |
| `storage_security_group_ids` | SG allowing NFS (port 2049) |
### database/
Provisions persistent storage for SQLite data.
| Provider | Service | Encryption |
|----------|---------|:----------:|
| AWS | EFS (Elastic File System) | AES-256 at rest + TLS in transit |
| GCP | Filestore (NFS) | Google-managed |
| Azure | Azure Files (SMB/NFS) | SSE with platform key |
| OCI | File Storage Service (NFS) | Oracle-managed |
### compute/
Provisions dual blue/green container slots with auto-scaling.
| Provider | Service | Container Runtime |
|----------|---------|-------------------|
| AWS | ECS Fargate | Docker |
| GCP | Cloud Run v2 | Docker |
| Azure | Container Instances | Docker |
| OCI | Container Instances / OKE | Docker |
### loadbalancer/
Provisions the application load balancer with TLS termination and WebSocket support.
| Feature | Implementation |
|---------|---------------|
| TLS | TLS 1.3 minimum policy |
| WebSocket | Sticky sessions (cookie/ClientIP) |
| Blue-green | Weighted target groups (0-100) |
| Health checks | HTTP GET `/api/health` every 30s |
| Idle timeout | 300s (for long-lived WebSocket) |
### monitoring/
Provisions cloud-native monitoring and alerting, with optional Coralogix full-stack observability.
| Provider | Metrics | Alarms | Logs |
|----------|---------|--------|------|
| AWS | CloudWatch | SNS → Email | CloudWatch Logs |
| GCP | Cloud Monitoring | Notification Channel | Cloud Logging |
| Azure | Azure Monitor | Action Group | Log Analytics |
| OCI | OCI Monitoring | Notification Topic | OCI Logging |
| Coralogix | PromQL + Recording Rules | Coralogix Alerts → PagerDuty/Slack | DataPrime Log Analytics |
### Root Variables
Key variables defined in `terraform/variables.tf`:
| Variable | Type | Validation | Description |
|----------|------|-----------|-------------|
| `cloud_provider` | string | `aws\|gcp\|azure\|oci` | Target cloud |
| `environment` | string | `dev\|staging\|production` | Deployment tier |
| `vpc_cidr` | string | Valid CIDR | Network address space |
| `cpu` | number | `256\|512\|1024\|2048\|4096` | CPU units per container |
| `deployment_strategy` | string | `rolling\|blue-green\|canary` | Release strategy |
| `blue_weight` / `green_weight` | number | `0-100` | Traffic distribution |
---
## Kubernetes Security Posture
All Kubernetes manifests enforce the **Restricted Pod Security Standard**:
```mermaid
graph TB
subgraph "Namespace"
NS["pod-security.kubernetes.io/enforce: restricted"]
end
subgraph "Pod Security Context"
PSC1[runAsNonRoot: true]
PSC2[runAsUser: 1000]
PSC3[fsGroup: 1000]
PSC4["seccompProfile: RuntimeDefault"]
end
subgraph "Container Security Context"
CSC1[readOnlyRootFilesystem: true]
CSC2[allowPrivilegeEscalation: false]
CSC3["capabilities.drop: ALL"]
CSC4[automountServiceAccountToken: false]
end
NS --> PSC1 & PSC2 & PSC3 & PSC4
PSC1 --> CSC1 & CSC2 & CSC3 & CSC4
style NS fill:#f44336,color:#fff
```
---
## Data Flow
```mermaid
sequenceDiagram
participant User as Browser
participant LB as Load Balancer
participant App as Dashboard Pod
participant DB as SQLite (PV)
participant WS as WebSocket
participant Hook as Claude Code Hook
Hook->>App: POST /api/hooks/event
App->>DB: INSERT event
App->>WS: broadcast(new_event)
WS->>User: WebSocket message
User->>LB: GET /api/sessions
LB->>App: Forward (sticky session)
App->>DB: SELECT sessions
App->>LB: JSON response
LB->>User: HTTPS response
User->>LB: WSS upgrade
LB->>App: WebSocket handshake
App->>User: Real-time events
```
---
## Related Documentation
- [DEPLOYMENT.md](../DEPLOYMENT.md) — Step-by-step deployment guide with workflows
- [terraform/README.md](./terraform/README.md) — Terraform module details
- [kubernetes/README.md](./kubernetes/README.md) — Kustomize overlay guide
-250
View File
@@ -1,250 +0,0 @@
# ─────────────────────────────────────────────────────────────────────────────
# GitHub Actions CI Pipeline Claude Code Agent Monitor
#
# Triggers on push to main and PRs. Runs linting, tests, builds Docker
# images, and scans for security vulnerabilities.
# ─────────────────────────────────────────────────────────────────────────────
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
packages: write
security-events: write
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
env:
NODE_VERSION: "22"
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}/agent-monitor
MCP_IMAGE_NAME: ${{ github.repository }}/agent-monitor-mcp
jobs:
# ── Lint & Format Check ─────────────────────────────────────────────────
lint:
name: Lint & Format
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Check formatting
run: npm run format:check
# ── Server Tests ────────────────────────────────────────────────────────
test-server:
name: Server Tests
runs-on: ubuntu-latest
needs: lint
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run server tests
run: npm run test:server
# ── Client Tests ────────────────────────────────────────────────────────
test-client:
name: Client Tests
runs-on: ubuntu-latest
needs: lint
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "npm"
- name: Install root dependencies
run: npm ci
- name: Install client dependencies
run: cd client && npm ci
- name: Run client tests
run: npm run test:client
# ── MCP Tests ───────────────────────────────────────────────────────────
test-mcp:
name: MCP Sidecar Tests
runs-on: ubuntu-latest
needs: lint
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "npm"
- name: Install MCP dependencies
run: npm run mcp:install
- name: Type check MCP
run: npm run mcp:typecheck
- name: Run MCP tests
run: npm run mcp:test
# ── Build Docker Images ────────────────────────────────────────────────
build-image:
name: Build Docker Images
runs-on: ubuntu-latest
needs: [test-server, test-client, test-mcp]
# Only push images on main branch
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
outputs:
image-tag: ${{ steps.meta.outputs.version }}
image-digest: ${{ steps.build-app.outputs.digest }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (app)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=sha,prefix=sha-
type=raw,value=latest,enable={{is_default_branch}}
type=semver,pattern={{version}},enable=${{ startsWith(github.ref, 'refs/tags/v') }}
- name: Build & push app image
id: build-app
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64
- name: Extract metadata (MCP)
id: meta-mcp
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.MCP_IMAGE_NAME }}
tags: |
type=ref,event=branch
type=sha,prefix=sha-
type=raw,value=latest,enable={{is_default_branch}}
- name: Build & push MCP image
id: build-mcp
uses: docker/build-push-action@v5
with:
context: .
file: ./mcp/Dockerfile
push: true
tags: ${{ steps.meta-mcp.outputs.tags }}
labels: ${{ steps.meta-mcp.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64
# ── Security Scan ──────────────────────────────────────────────────────
security-scan:
name: Security Scan
runs-on: ubuntu-latest
needs: build-image
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Login to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Run Trivy vulnerability scanner (app)
uses: aquasecurity/trivy-action@master
with:
image-ref: "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build-image.outputs.image-tag }}"
format: "sarif"
output: "trivy-app-results.sarif"
severity: "CRITICAL,HIGH"
exit-code: "1"
- name: Run Trivy vulnerability scanner (MCP)
uses: aquasecurity/trivy-action@master
with:
image-ref: "${{ env.REGISTRY }}/${{ env.MCP_IMAGE_NAME }}:${{ needs.build-image.outputs.image-tag }}"
format: "sarif"
output: "trivy-mcp-results.sarif"
severity: "CRITICAL,HIGH"
exit-code: "1"
- name: Upload Trivy SARIF (app)
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: "trivy-app-results.sarif"
category: "trivy-app"
- name: Upload Trivy SARIF (MCP)
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: "trivy-mcp-results.sarif"
category: "trivy-mcp"
- name: Run npm audit
run: npm audit --production --audit-level=high
- name: Trivy filesystem scan (IaC)
uses: aquasecurity/trivy-action@master
with:
scan-type: "fs"
scan-ref: "./deployments"
format: "table"
severity: "CRITICAL,HIGH"
exit-code: "1"
-335
View File
@@ -1,335 +0,0 @@
# ─────────────────────────────────────────────────────────────────────────────
# GitHub Actions Deploy Pipeline Claude Code Agent Monitor
#
# Triggers on version tags and manual dispatch. Deploys to staging
# automatically and to production after manual approval.
# ─────────────────────────────────────────────────────────────────────────────
name: Deploy
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
environment:
description: "Target environment"
required: true
type: choice
options:
- staging
- production
image_tag:
description: "Image tag to deploy (default: latest from main)"
required: false
type: string
permissions:
contents: read
packages: read
id-token: write # For OIDC cloud auth
concurrency:
group: deploy-${{ github.event.inputs.environment || 'staging' }}
cancel-in-progress: false
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}/agent-monitor
MCP_IMAGE_NAME: ${{ github.repository }}/agent-monitor-mcp
HELM_CHART_PATH: deployments/helm/agent-monitor
jobs:
# ── Resolve image tag ──────────────────────────────────────────────────
prepare:
name: Prepare Deployment
runs-on: ubuntu-latest
outputs:
image-tag: ${{ steps.resolve.outputs.tag }}
version: ${{ steps.resolve.outputs.version }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Resolve image tag
id: resolve
run: |
if [[ -n "${{ github.event.inputs.image_tag }}" ]]; then
TAG="${{ github.event.inputs.image_tag }}"
elif [[ "${{ github.ref_type }}" == "tag" ]]; then
TAG="${{ github.ref_name }}"
else
TAG="sha-$(git rev-parse --short HEAD)"
fi
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "version=${TAG#v}" >> "$GITHUB_OUTPUT"
echo "Resolved image tag: ${TAG}"
# ── Build (if triggered by tag) ────────────────────────────────────────
build:
name: Build Images
runs-on: ubuntu-latest
needs: prepare
if: github.ref_type == 'tag'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build & push app image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.prepare.outputs.image-tag }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64
- name: Build & push MCP image
uses: docker/build-push-action@v5
with:
context: .
file: ./mcp/Dockerfile
push: true
tags: |
${{ env.REGISTRY }}/${{ env.MCP_IMAGE_NAME }}:${{ needs.prepare.outputs.image-tag }}
${{ env.REGISTRY }}/${{ env.MCP_IMAGE_NAME }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64
# ── Deploy to Staging ──────────────────────────────────────────────────
deploy-staging:
name: Deploy to Staging
runs-on: ubuntu-latest
needs: [prepare, build]
if: |
always() &&
needs.prepare.result == 'success' &&
(needs.build.result == 'success' || needs.build.result == 'skipped') &&
(github.event.inputs.environment == 'staging' || github.event.inputs.environment == '' || github.ref_type == 'tag')
environment:
name: staging
url: https://staging.agent-monitor.example.com
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN_STAGING }}
aws-region: ${{ vars.AWS_REGION || 'us-west-2' }}
- name: Setup kubectl
uses: azure/setup-kubectl@v3
with:
version: "v1.29.0"
- name: Setup Helm
uses: azure/setup-helm@v4
with:
version: "v3.14.0"
- name: Update kubeconfig
run: |
aws eks update-kubeconfig \
--region ${{ vars.AWS_REGION || 'us-west-2' }} \
--name ${{ vars.EKS_CLUSTER_STAGING || 'agent-monitor-staging' }}
- name: Deploy to staging via Helm
run: |
helm upgrade --install agent-monitor ${{ env.HELM_CHART_PATH }} \
--namespace agent-monitor-staging \
--create-namespace \
--set image.repository=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} \
--set image.tag=${{ needs.prepare.outputs.image-tag }} \
--set mcp.image.repository=${{ env.REGISTRY }}/${{ env.MCP_IMAGE_NAME }} \
--set mcp.image.tag=${{ needs.prepare.outputs.image-tag }} \
--set environment=staging \
--set ingress.host=staging.agent-monitor.example.com \
--values ${{ env.HELM_CHART_PATH }}/values-staging.yaml \
--wait \
--atomic \
--timeout 600s
- name: Health check
run: |
echo "Waiting for pods to be ready..."
kubectl wait --for=condition=ready pod \
-l app.kubernetes.io/name=agent-monitor \
-n agent-monitor-staging \
--timeout=300s
# Port forward and check health
kubectl port-forward svc/agent-monitor 14820:4820 -n agent-monitor-staging &
PF_PID=$!
sleep 5
for i in $(seq 1 10); do
if curl -sf http://localhost:14820/api/health | grep -q '"status":"ok"'; then
echo "✔ Health check passed"
kill $PF_PID 2>/dev/null || true
exit 0
fi
echo "Attempt $i/10..."
sleep 5
done
kill $PF_PID 2>/dev/null || true
echo "✖ Health check failed"
exit 1
- name: Notify Slack (staging)
if: always()
uses: slackapi/slack-github-action@v1.26.0
with:
payload: |
{
"text": "${{ job.status == 'success' && '✅' || '❌' }} Staging deployment ${{ job.status }}: `${{ needs.prepare.outputs.image-tag }}`",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "${{ job.status == 'success' && ':white_check_mark:' || ':x:' }} *Staging Deployment ${{ job.status }}*\n*Image:* `${{ needs.prepare.outputs.image-tag }}`\n*Commit:* `${{ github.sha }}`\n*Actor:* ${{ github.actor }}"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
# ── Deploy to Production ───────────────────────────────────────────────
deploy-production:
name: Deploy to Production
runs-on: ubuntu-latest
needs: [prepare, deploy-staging]
if: |
always() &&
needs.prepare.result == 'success' &&
needs.deploy-staging.result == 'success' &&
(github.event.inputs.environment == 'production' || github.ref_type == 'tag')
environment:
name: production
url: https://agent-monitor.example.com
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN_PRODUCTION }}
aws-region: ${{ vars.AWS_REGION || 'us-west-2' }}
- name: Setup kubectl
uses: azure/setup-kubectl@v3
with:
version: "v1.29.0"
- name: Setup Helm
uses: azure/setup-helm@v4
with:
version: "v3.14.0"
- name: Update kubeconfig
run: |
aws eks update-kubeconfig \
--region ${{ vars.AWS_REGION || 'us-west-2' }} \
--name ${{ vars.EKS_CLUSTER_PRODUCTION || 'agent-monitor-production' }}
- name: Create database backup
run: |
chmod +x deployments/scripts/db-backup.sh
# Find a running pod to backup from
POD=$(kubectl get pods -n agent-monitor-production \
-l app.kubernetes.io/name=agent-monitor \
--field-selector=status.phase=Running \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "")
if [[ -n "$POD" ]]; then
echo "Backing up database from pod: $POD"
kubectl exec "$POD" -n agent-monitor-production -- \
sh -c "cp /app/data/dashboard.db /tmp/pre-deploy-backup.db 2>/dev/null || true"
echo "Pre-deploy backup created"
else
echo "⚠ No running pods found skipping backup"
fi
- name: Deploy to production via Helm
run: |
helm upgrade --install agent-monitor ${{ env.HELM_CHART_PATH }} \
--namespace agent-monitor-production \
--create-namespace \
--set image.repository=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} \
--set image.tag=${{ needs.prepare.outputs.image-tag }} \
--set mcp.image.repository=${{ env.REGISTRY }}/${{ env.MCP_IMAGE_NAME }} \
--set mcp.image.tag=${{ needs.prepare.outputs.image-tag }} \
--set environment=production \
--set ingress.host=agent-monitor.example.com \
--values ${{ env.HELM_CHART_PATH }}/values-production.yaml \
--wait \
--atomic \
--timeout 600s
- name: Health check
run: |
echo "Waiting for pods to be ready..."
kubectl wait --for=condition=ready pod \
-l app.kubernetes.io/name=agent-monitor \
-n agent-monitor-production \
--timeout=300s
kubectl port-forward svc/agent-monitor 14820:4820 -n agent-monitor-production &
PF_PID=$!
sleep 5
for i in $(seq 1 15); do
if curl -sf http://localhost:14820/api/health | grep -q '"status":"ok"'; then
echo "✔ Production health check passed"
kill $PF_PID 2>/dev/null || true
exit 0
fi
echo "Attempt $i/15..."
sleep 5
done
kill $PF_PID 2>/dev/null || true
echo "✖ Production health check failed!"
exit 1
- name: Notify Slack (production)
if: always()
uses: slackapi/slack-github-action@v1.26.0
with:
payload: |
{
"text": "${{ job.status == 'success' && '🚀' || '🚨' }} Production deployment ${{ job.status }}: `${{ needs.prepare.outputs.image-tag }}`",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "${{ job.status == 'success' && ':rocket:' || ':rotating_light:' }} *Production Deployment ${{ job.status }}*\n*Image:* `${{ needs.prepare.outputs.image-tag }}`\n*Version:* `${{ needs.prepare.outputs.version }}`\n*Commit:* `${{ github.sha }}`\n*Actor:* ${{ github.actor }}\n*Workflow:* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run>"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
-160
View File
@@ -1,160 +0,0 @@
# ─────────────────────────────────────────────────────────────────────────────
# GitHub Actions Rollback Pipeline Claude Code Agent Monitor
#
# Manual workflow to roll back a Helm deployment to a previous revision.
# ─────────────────────────────────────────────────────────────────────────────
name: Rollback
on:
workflow_dispatch:
inputs:
environment:
description: "Target environment to rollback"
required: true
type: choice
options:
- staging
- production
revision:
description: "Helm revision number (leave empty for previous)"
required: false
type: string
reason:
description: "Reason for rollback"
required: true
type: string
permissions:
contents: read
id-token: write
concurrency:
group: deploy-${{ github.event.inputs.environment }}
cancel-in-progress: false
jobs:
rollback:
name: Rollback ${{ github.event.inputs.environment }}
runs-on: ubuntu-latest
environment:
name: ${{ github.event.inputs.environment }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ github.event.inputs.environment == 'production' && secrets.AWS_ROLE_ARN_PRODUCTION || secrets.AWS_ROLE_ARN_STAGING }}
aws-region: ${{ vars.AWS_REGION || 'us-west-2' }}
- name: Setup kubectl
uses: azure/setup-kubectl@v3
with:
version: "v1.29.0"
- name: Setup Helm
uses: azure/setup-helm@v4
with:
version: "v3.14.0"
- name: Update kubeconfig
run: |
CLUSTER_NAME="${{ github.event.inputs.environment == 'production' && vars.EKS_CLUSTER_PRODUCTION || vars.EKS_CLUSTER_STAGING }}"
CLUSTER_NAME="${CLUSTER_NAME:-agent-monitor-${{ github.event.inputs.environment }}}"
aws eks update-kubeconfig \
--region ${{ vars.AWS_REGION || 'us-west-2' }} \
--name "${CLUSTER_NAME}"
- name: Show Helm history
run: |
NAMESPACE="agent-monitor-${{ github.event.inputs.environment }}"
echo "## Current Helm History"
helm history agent-monitor -n "${NAMESPACE}" --max 10 || echo "No history found"
- name: Execute rollback
run: |
NAMESPACE="agent-monitor-${{ github.event.inputs.environment }}"
REVISION="${{ github.event.inputs.revision }}"
echo "Rolling back in namespace: ${NAMESPACE}"
ROLLBACK_ARGS="helm rollback agent-monitor"
if [[ -n "${REVISION}" ]]; then
ROLLBACK_ARGS="${ROLLBACK_ARGS} ${REVISION}"
echo "Target revision: ${REVISION}"
else
echo "Target revision: previous"
fi
${ROLLBACK_ARGS} -n "${NAMESPACE}" --wait --timeout 300s
echo "✔ Rollback command succeeded"
- name: Health check after rollback
run: |
NAMESPACE="agent-monitor-${{ github.event.inputs.environment }}"
echo "Waiting for pods to be ready..."
kubectl wait --for=condition=ready pod \
-l app.kubernetes.io/name=agent-monitor \
-n "${NAMESPACE}" \
--timeout=300s
kubectl port-forward svc/agent-monitor 14820:4820 -n "${NAMESPACE}" &
PF_PID=$!
sleep 5
HEALTHY=false
for i in $(seq 1 10); do
if curl -sf http://localhost:14820/api/health | grep -q '"status":"ok"'; then
echo "✔ Health check passed after rollback"
HEALTHY=true
break
fi
echo "Attempt $i/10..."
sleep 5
done
kill $PF_PID 2>/dev/null || true
if [[ "$HEALTHY" != true ]]; then
echo "✖ Health check failed after rollback!"
exit 1
fi
- name: Show post-rollback status
if: always()
run: |
NAMESPACE="agent-monitor-${{ github.event.inputs.environment }}"
echo "## Post-Rollback Status"
echo ""
echo "### Helm Status"
helm status agent-monitor -n "${NAMESPACE}" || true
echo ""
echo "### Pod Status"
kubectl get pods -n "${NAMESPACE}" -l app.kubernetes.io/name=agent-monitor || true
echo ""
echo "### Recent Events"
kubectl get events -n "${NAMESPACE}" --sort-by='.lastTimestamp' | tail -20 || true
- name: Notify Slack
if: always()
uses: slackapi/slack-github-action@v1.26.0
with:
payload: |
{
"text": "${{ job.status == 'success' && '⏪' || '🚨' }} Rollback ${{ job.status }} on ${{ github.event.inputs.environment }}",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "${{ job.status == 'success' && ':rewind:' || ':rotating_light:' }} *Rollback ${{ job.status }}*\n*Environment:* `${{ github.event.inputs.environment }}`\n*Revision:* `${{ github.event.inputs.revision || 'previous' }}`\n*Reason:* ${{ github.event.inputs.reason }}\n*Actor:* ${{ github.actor }}\n*Workflow:* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run>"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
-323
View File
@@ -1,323 +0,0 @@
# ─────────────────────────────────────────────────────────────────────────────
# GitLab CI/CD Pipeline Claude Code Agent Monitor
#
# Stages: test → build → deploy-staging → deploy-production
# ─────────────────────────────────────────────────────────────────────────────
# ── Global settings ─────────────────────────────────────────────────────────
default:
image: node:22-alpine
interruptible: true
retry:
max: 1
when:
- runner_system_failure
- stuck_or_timeout_failure
variables:
NODE_VERSION: "22"
REGISTRY: "${CI_REGISTRY}"
IMAGE_NAME: "${CI_REGISTRY_IMAGE}/agent-monitor"
MCP_IMAGE_NAME: "${CI_REGISTRY_IMAGE}/agent-monitor-mcp"
HELM_CHART_PATH: "deployments/helm/agent-monitor"
APP_NAME: "agent-monitor"
# Kaniko cache
KANIKO_CACHE_ARGS: "--cache=true --cache-repo=${CI_REGISTRY_IMAGE}/cache"
stages:
- test
- build
- deploy-staging
- deploy-production
- rollback
# ── Cache configuration ────────────────────────────────────────────────────
.node_cache: &node_cache
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
policy: pull-push
# ── Test stage ──────────────────────────────────────────────────────────────
lint:
stage: test
<<: *node_cache
script:
- npm ci --prefer-offline
- npm run format:check
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
test:server:
stage: test
<<: *node_cache
script:
- npm ci --prefer-offline
- npm run test:server
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
artifacts:
when: on_failure
paths:
- server/__tests__/
expire_in: 7 days
test:client:
stage: test
<<: *node_cache
script:
- npm ci --prefer-offline
- cd client && npm ci --prefer-offline
- npm run test:client
cache:
key:
files:
- client/package-lock.json
paths:
- client/node_modules/
policy: pull-push
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
test:mcp:
stage: test
<<: *node_cache
script:
- npm run mcp:install
- npm run mcp:typecheck
- npm run mcp:test
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
# ── Build stage ─────────────────────────────────────────────────────────────
.kaniko_build: &kaniko_build
stage: build
image:
name: gcr.io/kaniko-project/executor:v1.22.0-debug
entrypoint: [""]
before_script:
- mkdir -p /kaniko/.docker
- echo "{\"auths\":{\"${CI_REGISTRY}\":{\"auth\":\"$(printf "%s:%s" "${CI_REGISTRY_USER}" "${CI_REGISTRY_PASSWORD}" | base64)\"}}}" > /kaniko/.docker/config.json
build:app:
<<: *kaniko_build
script:
- >-
/kaniko/executor
--context "${CI_PROJECT_DIR}"
--dockerfile "${CI_PROJECT_DIR}/Dockerfile"
--destination "${IMAGE_NAME}:${CI_COMMIT_SHORT_SHA}"
--destination "${IMAGE_NAME}:${CI_COMMIT_REF_SLUG}"
--destination "${IMAGE_NAME}:latest"
${KANIKO_CACHE_ARGS}
--label "org.opencontainers.image.revision=${CI_COMMIT_SHA}"
--label "org.opencontainers.image.created=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
--label "org.opencontainers.image.source=${CI_PROJECT_URL}"
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
build:mcp:
<<: *kaniko_build
script:
- >-
/kaniko/executor
--context "${CI_PROJECT_DIR}"
--dockerfile "${CI_PROJECT_DIR}/mcp/Dockerfile"
--destination "${MCP_IMAGE_NAME}:${CI_COMMIT_SHORT_SHA}"
--destination "${MCP_IMAGE_NAME}:${CI_COMMIT_REF_SLUG}"
--destination "${MCP_IMAGE_NAME}:latest"
${KANIKO_CACHE_ARGS}
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
# Security scan
security:scan:
stage: build
needs: ["build:app", "build:mcp"]
image:
name: aquasec/trivy:latest
entrypoint: [""]
script:
- trivy image --exit-code 1 --severity HIGH,CRITICAL --format table "${IMAGE_NAME}:${CI_COMMIT_SHORT_SHA}"
- trivy image --exit-code 1 --severity HIGH,CRITICAL --format table "${MCP_IMAGE_NAME}:${CI_COMMIT_SHORT_SHA}"
- trivy image --exit-code 1 --severity CRITICAL --format json --output trivy-app-report.json "${IMAGE_NAME}:${CI_COMMIT_SHORT_SHA}"
- trivy image --exit-code 1 --severity CRITICAL --format json --output trivy-mcp-report.json "${MCP_IMAGE_NAME}:${CI_COMMIT_SHORT_SHA}"
artifacts:
paths:
- trivy-app-report.json
- trivy-mcp-report.json
expire_in: 30 days
allow_failure: false
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
# ── Deploy Staging ──────────────────────────────────────────────────────────
deploy:staging:
stage: deploy-staging
image:
name: alpine/helm:3.14.0
entrypoint: [""]
needs:
- build:app
- build:mcp
environment:
name: staging
url: https://staging.agent-monitor.example.com
on_stop: stop:staging
before_script:
- apk add --no-cache curl aws-cli kubectl
- aws eks update-kubeconfig --region "${AWS_REGION:-us-west-2}" --name "${EKS_CLUSTER_STAGING:-agent-monitor-staging}"
script:
- |
helm upgrade --install ${APP_NAME} ${HELM_CHART_PATH} \
--namespace agent-monitor-staging \
--create-namespace \
--set image.repository=${IMAGE_NAME} \
--set image.tag=${CI_COMMIT_SHORT_SHA} \
--set mcp.image.repository=${MCP_IMAGE_NAME} \
--set mcp.image.tag=${CI_COMMIT_SHORT_SHA} \
--set environment=staging \
--values ${HELM_CHART_PATH}/values-staging.yaml \
--wait \
--atomic \
--timeout 600s
- |
echo "Running health check..."
kubectl wait --for=condition=ready pod \
-l app.kubernetes.io/name=${APP_NAME} \
-n agent-monitor-staging \
--timeout=300s
echo "✔ Staging deployment successful"
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
stop:staging:
stage: deploy-staging
image:
name: alpine/helm:3.14.0
entrypoint: [""]
environment:
name: staging
action: stop
before_script:
- apk add --no-cache aws-cli kubectl
- aws eks update-kubeconfig --region "${AWS_REGION:-us-west-2}" --name "${EKS_CLUSTER_STAGING:-agent-monitor-staging}"
script:
- helm uninstall ${APP_NAME} -n agent-monitor-staging --wait || true
when: manual
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
# ── Deploy Production ───────────────────────────────────────────────────────
deploy:production:
stage: deploy-production
image:
name: alpine/helm:3.14.0
entrypoint: [""]
needs:
- deploy:staging
environment:
name: production
url: https://agent-monitor.example.com
before_script:
- apk add --no-cache curl aws-cli kubectl
- aws eks update-kubeconfig --region "${AWS_REGION:-us-west-2}" --name "${EKS_CLUSTER_PRODUCTION:-agent-monitor-production}"
script:
# Pre-deploy backup
- |
POD=$(kubectl get pods -n agent-monitor-production \
-l app.kubernetes.io/name=${APP_NAME} \
--field-selector=status.phase=Running \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "")
if [ -n "$POD" ]; then
echo "Creating pre-deploy backup..."
kubectl exec "$POD" -n agent-monitor-production -- \
sh -c "cp /app/data/dashboard.db /tmp/pre-deploy-backup.db" 2>/dev/null || true
fi
# Deploy
- |
helm upgrade --install ${APP_NAME} ${HELM_CHART_PATH} \
--namespace agent-monitor-production \
--create-namespace \
--set image.repository=${IMAGE_NAME} \
--set image.tag=${CI_COMMIT_SHORT_SHA} \
--set mcp.image.repository=${MCP_IMAGE_NAME} \
--set mcp.image.tag=${CI_COMMIT_SHORT_SHA} \
--set environment=production \
--values ${HELM_CHART_PATH}/values-production.yaml \
--wait \
--atomic \
--timeout 600s
# Health check
- |
echo "Running production health check..."
kubectl wait --for=condition=ready pod \
-l app.kubernetes.io/name=${APP_NAME} \
-n agent-monitor-production \
--timeout=300s
echo "✔ Production deployment successful"
when: manual
allow_failure: false
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
# ── Rollback ────────────────────────────────────────────────────────────────
rollback:staging:
stage: rollback
image:
name: alpine/helm:3.14.0
entrypoint: [""]
environment:
name: staging
before_script:
- apk add --no-cache aws-cli kubectl
- aws eks update-kubeconfig --region "${AWS_REGION:-us-west-2}" --name "${EKS_CLUSTER_STAGING:-agent-monitor-staging}"
script:
- echo "Rolling back staging..."
- helm rollback ${APP_NAME} ${ROLLBACK_REVISION:-0} -n agent-monitor-staging --wait --timeout 300s
- |
kubectl wait --for=condition=ready pod \
-l app.kubernetes.io/name=${APP_NAME} \
-n agent-monitor-staging \
--timeout=300s
- echo "✔ Staging rollback complete"
when: manual
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
rollback:production:
stage: rollback
image:
name: alpine/helm:3.14.0
entrypoint: [""]
environment:
name: production
before_script:
- apk add --no-cache aws-cli kubectl
- aws eks update-kubeconfig --region "${AWS_REGION:-us-west-2}" --name "${EKS_CLUSTER_PRODUCTION:-agent-monitor-production}"
script:
- echo "⚠ Rolling back PRODUCTION..."
- helm history ${APP_NAME} -n agent-monitor-production --max 5
- helm rollback ${APP_NAME} ${ROLLBACK_REVISION:-0} -n agent-monitor-production --wait --timeout 300s
- |
kubectl wait --for=condition=ready pod \
-l app.kubernetes.io/name=${APP_NAME} \
-n agent-monitor-production \
--timeout=300s
- echo "✔ Production rollback complete"
when: manual
allow_failure: false
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

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