diff --git a/.claude/skills/update-project-docs/references/doc-map.md b/.claude/skills/update-project-docs/references/doc-map.md index 16ad250..f49da6f 100644 --- a/.claude/skills/update-project-docs/references/doc-map.md +++ b/.claude/skills/update-project-docs/references/doc-map.md @@ -52,10 +52,10 @@ i18n architecture: **Supported languages** list, `supportedLngs`, the 15 namespa ## Tier 3 — situational - `.env.example` — every env var belongs here with a sane default + comment. -- `INSTALL.md`, `SETUP.md`, `DEPLOYMENT.md`, `docs/DEPLOYMENT.md` — install/run/deploy commands. +- `INSTALL.md`, `SETUP.md` — install/run commands. - `CLAUDE.md`, `AGENTS.md` — agent working guides; update when commands, file locations, or workflows change. - `docs/README.md` — docs index; add a link when a new `docs/*.md` is created. -- `desktop/README.md`, `vscode-extension/README.md`, `statusline/README.md` — surface-specific; update only when that surface changes. +- `vscode-extension/README.md`, `statusline/README.md` — surface-specific; update only when that surface changes. ## Consistency invariants diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 1529309..4b92890 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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) ![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) -![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) ![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) ![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) ![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) -![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) - [Tabby Companion Subsystem](#tabby-companion-subsystem) - [VS Code Extension Architecture](#vs-code-extension-architecture) -- [Desktop App Architecture (macOS & Windows / Electron)](#desktop-app-architecture-macos--windows--electron) - [Security Considerations](#security-considerations) - [Performance Characteristics](#performance-characteristics) - [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/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/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`) | -| `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/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) | | `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/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. -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/
Express API · SQLite · WebSocket"] - client["client/
React + Vite SPA"] - scripts["scripts/
hook installer/handler, import, seed"] - mcp["mcp/
local MCP server"] - vscode["vscode-extension/"] - desktop["desktop/
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:` -- 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
lifecycle"] - host["server-host.ts
embedded server"] - express["server/index.js
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
React dashboard"] - preload["preload.ts
(empty -- no bridge)"] - end - - express -->|"http + ws on 127.0.0.1:port"| win - win -.->|loads| preload - - hooks["Claude Code hooks
(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; -} -``` - -### 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
(no adoption, no fallback)"] - forced -->|no| adopt{"healthy server
already on :4820?"} - adopt -->|yes| reuse["adopt it
ownedByUs = false"] - adopt -->|no| pick["pickFreePort()"] - - pick --> p1{":4820 free?"} - p1 -->|yes| use4820["use 4820"] - p1 -->|no| p2{"any of
:4821–:4829 free?"} - p2 -->|yes| usefb["use that"] - p2 -->|no| p3{"any of
: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()
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 → 4821–4829 → a random port in 49152–49500**. 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=`, and `DASHBOARD_DATA_DIR=/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
rebuilt for Electron's ABI
(electron-builder install-app-deps)"] - end - subgraph root["node_modules (repo root)"] - r1["better-sqlite3
built for system Node
(used by npm run test:server)"] - end - - patch["ensureNativeModulesPatched()
overrides Module._resolveFilename"] - srv["server/db.js
require('better-sqlite3')"] - - srv -->|"request intercepted"| patch - patch -->|"redirected to"| d1 - patch -.->|"everything else
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
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)
Claude Code Monitor install dir (Windows)"] - appbundle --> contents["Contents/ (macOS)
install root (Windows)"] - contents --> macos["MacOS/ — Electron binary (macOS)
Claude Code Monitor.exe (Windows)"] - contents --> res["Resources/ (macOS)
resources\ (Windows)"] - res --> asar["app.asar
(compiled out/**, package.json)"] - res --> unpacked["app.asar.unpacked/
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 | Area | Approach | @@ -2687,26 +2370,6 @@ graph LR | **File watching** | `node --watch` + Vite HMR | None | | **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
(.app / .exe)"] --> MAIN["Electron main process
(Node 22 / Electron 35)"] - MAIN --> HOST["server-host.ts
port discovery + adopt"] - HOST --> SERVER["server/index.js (in-process)
Port 4820 → fallback"] - SERVER -->|serves| DIST["client/dist/
(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) 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] > **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
Build · Test · Scan"] --> DEPLOY["Deployment
Helm · Kustomize · Terraform"] - DEPLOY --> VERIFY["Verification
Health Check · Smoke Tests"] - VERIFY -->|Fail| ROLLBACK["Rollback
Instant Revert"] - end - - subgraph "Infrastructure" - direction TB - subgraph "Compute" - BLUE["Blue Slot
Current Version"] - GREEN["Green Slot
New Version"] - end - LB["Load Balancer
TLS 1.3 · WebSocket
Weighted Routing"] - PV["Persistent Storage
Encrypted NFS"] - MON["Monitoring
Prometheus · Grafana
13 Alert Rules"] - OTEL["OTel Collector
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 diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md deleted file mode 100644 index 2744de2..0000000 --- a/DEPLOYMENT.md +++ /dev/null @@ -1,1080 +0,0 @@ -# Deployment Guide - -Production deployment guide for Claude Code Agent Monitor. This document covers every supported deployment path — from a single Docker container to a fully orchestrated, multi-cloud Kubernetes deployment with blue-green releases, automated canary analysis, and comprehensive observability. - -> [!NOTE] -> **Scope.** This guide is for the **server-side dashboard** — the Node + SQLite + React app you host on Docker, Kubernetes, or a cloud VM. The optional **macOS desktop app** is a per-user downloadable that embeds the same server in-process; it ships as a DMG via GitHub Releases (CI auto-publishes a new `vX.Y.Z` whenever the version in `package.json` is bumped on `master`) and needs none of the infrastructure described below — no cluster, no Terraform, no ingress. See [`DESKTOP.md`](./DESKTOP.md) for the user-facing guide, [`INSTALL.md → Desktop App (macOS & Windows)`](./INSTALL.md#desktop-app-macos--windows-optional) for build / install commands, and [Releases → latest](https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor/releases/latest) for the pre-built DMG. - -## Architecture Overview - -```mermaid -graph TB - subgraph "Internet" - U[Users / Browsers] - end - - subgraph "Edge Layer" - LB[Load Balancer
TLS Termination
WebSocket Upgrade] - end - - subgraph "Compute Layer" - subgraph "Blue Slot" - B1[Dashboard Pod 1] - B2[Dashboard Pod 2] - B3[Dashboard Pod N] - end - subgraph "Green Slot" - G1[Dashboard Pod 1] - G2[Dashboard Pod 2] - G3[Dashboard Pod N] - end - end - - subgraph "MCP Sidecar" - MCP1[MCP Server] - end - - subgraph "Persistence" - PV[(SQLite on
Persistent Volume)] - end - - subgraph "Observability" - PROM[Prometheus] - GRAF[Grafana] - ALERT[Alertmanager] - CX[Coralogix] - OTEL[OTel Collector] - end - - U --> LB - LB -->|Active| B1 & B2 & B3 - LB -.->|Standby| G1 & G2 & G3 - B1 --- MCP1 - B1 & B2 & B3 --> PV - PROM -->|Scrape| B1 & B2 & B3 - PROM --> ALERT - OTEL -->|Ship logs, metrics, traces| CX - B1 & B2 & B3 --> OTEL - GRAF --> PROM -``` - -## Deployment Methods - -Three deployment methods are supported, each targeting different operational maturity levels: - -```mermaid -flowchart LR - Start([Choose Method]) --> Q1{Have
Kubernetes?} - Q1 -->|No| Docker["🐳 Docker Compose
Simplest path"] - Q1 -->|Yes| Q2{Need
IaC?} - Q2 -->|No| Q3{Prefer
templating?} - Q2 -->|Yes| TF["🏗️ Terraform
Full cloud provisioning"] - Q3 -->|Helm| Helm["⎈ Helm Chart
Parameterized installs"] - Q3 -->|Raw YAML| Kust["📦 Kustomize
Overlay-based patching"] - - style Docker fill:#0db7ed,color:#fff - style Helm fill:#0f1689,color:#fff - style Kust fill:#326ce5,color:#fff - style TF fill:#7b42bc,color:#fff -``` - -| Method | Best For | Prerequisites | Cloud Agnostic | -|--------|----------|---------------|:--------------:| -| **Docker Compose** | Local dev, single-server | Docker | ✅ | -| **Helm** | Teams with Kubernetes | `helm`, `kubectl` | ✅ | -| **Kustomize** | GitOps, raw YAML fans | `kustomize`, `kubectl` | ✅ | -| **Terraform** | Full infra provisioning | `terraform` | ✅ AWS/GCP/Azure/OCI | - ---- - -## Quick Start: Docker Compose - -The fastest path to a running production instance: - -```bash -# Build and start everything -docker compose up -d --build - -# Verify -curl http://localhost:4820/api/health -# → {"status":"ok","timestamp":"..."} - -# View logs -docker compose logs -f -``` - -The included `docker-compose.yml` at the project root runs the dashboard on port `4820` with a bind mount from `~/.claude/agent-dashboard` to `/app/data` for SQLite — the **same** canonical database path as `npm start` and `npm run dev`. The image binds `0.0.0.0` inside the container and writes data to `/app/data` (both baked into the `Dockerfile`); Compose publishes the port on `127.0.0.1` only, so the dashboard is local-only out of the box. To expose it on a LAN, change the `ports` mapping to `"${DASHBOARD_PORT:-4820}:4820"` and set `DASHBOARD_TOKEN`. - ---- - -## Helm Deployment - -### Prerequisites - -```bash -# Verify tools -helm version # >= 3.12 -kubectl version # >= 1.27 -``` - -### Install - -```bash -# From the repository root: -cd deployments/helm/agent-monitor - -# Dev environment (1 replica, relaxed resources) -helm install agent-monitor . \ - -f values-dev.yaml \ - -n agent-monitor-dev --create-namespace - -# Staging (2 replicas, moderate resources) -helm install agent-monitor . \ - -f values-staging.yaml \ - -n agent-monitor-staging --create-namespace - -# Production (3+ replicas, HPA, strict security) -helm install agent-monitor . \ - -f values-production.yaml \ - -n agent-monitor-production --create-namespace -``` - -### Helm Values Hierarchy - -```mermaid -graph TD - Base["values.yaml
Defaults for all environments"] - Dev["values-dev.yaml
1 replica · 64Mi memory
No HPA · No network policy"] - Stg["values-staging.yaml
2 replicas · 256Mi memory
HPA 2→5 · TLS enabled"] - Prod["values-production.yaml
3 replicas · 512Mi memory
HPA 3→20 · Strict PSS"] - - Base --> Dev - Base --> Stg - Base --> Prod - - style Dev fill:#4caf50,color:#fff - style Stg fill:#ff9800,color:#fff - style Prod fill:#f44336,color:#fff -``` - -### Key Configuration - -| Parameter | Default | Description | -|-----------|---------|-------------| -| `replicaCount` | `2` | Number of pod replicas | -| `image.registry` | `ghcr.io` | Container registry | -| `image.tag` | `""` (appVersion) | Image tag | -| `service.type` | `ClusterIP` | Service type | -| `ingress.enabled` | `false` | Enable Ingress resource | -| `persistence.enabled` | `true` | Enable PVC for SQLite | -| `persistence.size` | `5Gi` | PVC size | -| `autoscaling.enabled` | `true` | Enable HPA | -| `mcp.enabled` | `false` | Deploy MCP sidecar | -| `monitoring.enabled` | `false` | Enable ServiceMonitor | -| `networkPolicy.enabled` | `true` | Enable NetworkPolicy | - -### Upgrade - -```bash -helm upgrade agent-monitor . \ - -f values-production.yaml \ - -n agent-monitor-production \ - --set image.tag=sha-abc1234 -``` - -### Rollback - -```bash -# View history -helm history agent-monitor -n agent-monitor-production - -# Roll back to previous -helm rollback agent-monitor -n agent-monitor-production - -# Roll back to specific revision -helm rollback agent-monitor 3 -n agent-monitor-production -``` - -### Test - -```bash -helm test agent-monitor -n agent-monitor-production -``` - ---- - -## Kustomize Deployment - -### Base + Overlays Structure - -```mermaid -graph TB - subgraph "kubernetes/" - Base["base/
11 resource manifests"] - Dev["overlays/dev/
1 replica, low resources"] - Stg["overlays/staging/
2 replicas, mid resources"] - Prod["overlays/production/
3 replicas, HA, strict HPA"] - - Base --> Dev - Base --> Stg - Base --> Prod - - Comp1["components/mcp-sidecar/
Adds MCP container"] - Comp2["components/monitoring/
Adds ServiceMonitor"] - - Comp1 -.->|Optional| Dev & Stg & Prod - Comp2 -.->|Optional| Stg & Prod - end - - style Base fill:#326ce5,color:#fff - style Dev fill:#4caf50,color:#fff - style Stg fill:#ff9800,color:#fff - style Prod fill:#f44336,color:#fff - style Comp1 fill:#9c27b0,color:#fff - style Comp2 fill:#9c27b0,color:#fff -``` - -### Deploy - -```bash -cd deployments/kubernetes - -# Preview what will be applied -kubectl kustomize overlays/production - -# Apply -kubectl apply -k overlays/dev # Dev -kubectl apply -k overlays/staging # Staging -kubectl apply -k overlays/production # Production -``` - -### Enable MCP Sidecar - -Add the component to your overlay's `kustomization.yaml`: - -```yaml -# overlays/production/kustomization.yaml -components: - - ../../components/mcp-sidecar - - ../../components/monitoring -``` - -Then re-apply: - -```bash -kubectl apply -k overlays/production -``` - -### Base Resources - -The base layer includes all required Kubernetes resources: - -| Resource | File | Purpose | -|----------|------|---------| -| Namespace | `namespace.yaml` | Isolated namespace with Restricted PSS | -| Deployment | `deployment.yaml` | App pods with probes, security context, anti-affinity | -| Service | `service.yaml` | ClusterIP with WebSocket session affinity | -| Ingress | `ingress.yaml` | TLS, HSTS, WebSocket upgrade headers | -| HPA | `hpa.yaml` | CPU/memory auto-scaling with scale-down stabilization | -| PDB | `pdb.yaml` | Disruption budget (`minAvailable: 1`) | -| NetworkPolicy | `networkpolicy.yaml` | Restricted ingress/egress | -| ConfigMap | `configmap.yaml` | Runtime configuration | -| PVC | `pvc.yaml` | Persistent storage for SQLite | -| ServiceAccount | `serviceaccount.yaml` | Dedicated SA, no token mount | - ---- - -## Terraform Deployment - -Full cloud infrastructure provisioning with support for AWS, GCP, Azure, and OCI. - -### Cloud Provider Architecture - -```mermaid -graph TB - subgraph "Terraform Root Module" - TF[main.tf
Module orchestration] - TF --> NET[networking/] - TF --> DB[database/] - TF --> COMP[compute/] - TF --> LB[loadbalancer/] - TF --> MON[monitoring/] - end - - subgraph "Provider Implementations" - AWS["☁️ AWS
VPC → ECS Fargate → EFS → ALB"] - GCP["☁️ GCP
VPC → Cloud Run → Filestore → GCLB"] - AZ["☁️ Azure
VNet → ACI → Files → AppGW"] - OCI["☁️ OCI
VCN → OKE → FSS → LBaaS"] - end - - TF -.-> AWS & GCP & AZ & OCI - - style AWS fill:#ff9900,color:#fff - style GCP fill:#4285f4,color:#fff - style AZ fill:#0078d4,color:#fff - style OCI fill:#f80000,color:#fff -``` - -### Modules - -| Module | Provisions | Key Features | -|--------|-----------|--------------| -| **networking** | VPC/VNet, subnets, NAT, security groups | Multi-AZ, public/private subnet separation | -| **compute** | Container instances, task definitions | Blue-green dual slots, auto-scaling | -| **database** | Managed file storage (EFS/Filestore/Files/FSS) | Encrypted at rest, NFS mount | -| **loadbalancer** | Application load balancer | TLS 1.3, WebSocket sticky sessions, weighted routing | -| **monitoring** | CloudWatch/Stackdriver/Azure Monitor | Alarms, dashboards, log retention | - -### Deploy with Terraform - -```bash -cd deployments/terraform - -# 1. Select a cloud provider -# Copy the provider directory as your working root, or symlink: -cp -r providers/aws/* . -# Or for GCP: cp -r providers/gcp/* . -# Or for Azure: cp -r providers/azure/* . -# Or for OCI: cp -r providers/oci/* . - -# 2. Configure backend (edit backend.tf — uncomment your provider's backend block) -vim backend.tf - -# 3. Initialize -terraform init - -# 4. Plan with environment-specific variables -terraform plan -var-file=environments/dev/terraform.tfvars -out=tfplan - -# 5. Apply -terraform apply tfplan - -# 6. Get outputs -terraform output application_url -``` - -### Environment Configuration - -Each environment has a pre-configured `terraform.tfvars`: - -| Environment | Replicas | CPU | Memory | Monitoring | Strategy | -|-------------|:--------:|:---:|:------:|:----------:|----------| -| **dev** | 1 | 256 | 512 | Off | Rolling | -| **staging** | 2 | 512 | 1024 | On | Rolling | -| **production** | 3 | 1024 | 2048 | On | Blue-green | - -### Blue-Green with Terraform - -```mermaid -sequenceDiagram - participant Op as Operator - participant TF as Terraform - participant LB as Load Balancer - participant Blue as Blue Slot - participant Green as Green Slot - - Note over Blue: Serving 100% traffic - Op->>TF: Update green image tag - TF->>Green: Deploy new version - Green-->>TF: Health checks pass - Op->>TF: Shift weights (blue=0, green=100) - TF->>LB: Update target group weights - LB->>Green: 100% traffic - Note over Green: Now serving all traffic - Note over Blue: Available for instant rollback -``` - -Adjust weights in your tfvars: - -```hcl -# Switch traffic from blue to green -blue_weight = 0 -green_weight = 100 -``` - -Then apply: - -```bash -terraform plan -var-file=environments/production/terraform.tfvars -out=tfplan -terraform apply tfplan -``` - ---- - -## Deployment Strategies - -### Rolling Update (Default) - -Zero-downtime rolling replacement. One pod at a time is replaced while the rest continue serving. - -```mermaid -gantt - title Rolling Update Timeline - dateFormat X - axisFormat %s - - section Pod 1 - Running (old) :done, 0, 3 - Terminating :crit, 3, 4 - Running (new) :active, 4, 10 - - section Pod 2 - Running (old) :done, 0, 5 - Terminating :crit, 5, 6 - Running (new) :active, 6, 10 - - section Pod 3 - Running (old) :done, 0, 7 - Terminating :crit, 7, 8 - Running (new) :active, 8, 10 -``` - -```bash -# Rolling is the default strategy -./deployments/scripts/deploy.sh --env production --method helm -``` - -### Blue-Green - -Two identical environments. Traffic switches instantly between them. Enables instant rollback. - -```mermaid -stateDiagram-v2 - [*] --> BlueActive: Initial state - BlueActive --> GreenActive: Switch to green - GreenActive --> BlueActive: Switch to blue (rollback) - - state BlueActive { - Blue_100: Blue receives 100% traffic - Green_Standby: Green on standby - } - - state GreenActive { - Green_100: Green receives 100% traffic - Blue_Standby: Blue on standby - } -``` - -```bash -# Deploy with blue-green strategy -./deployments/scripts/deploy.sh \ - --env production --method helm --strategy blue-green - -# Switch traffic to green slot -./deployments/scripts/blue-green-switch.sh \ - --env production --target green - -# Instant rollback to blue -./deployments/scripts/blue-green-switch.sh \ - --env production --target blue -``` - -### Canary - -Gradually shift traffic to the new version while monitoring error rates and latency. Automatic rollback if metrics exceed thresholds. - -```mermaid -graph LR - subgraph "Canary Progression" - S1["5% traffic
Monitor 60s"] --> S2["25% traffic
Monitor 60s"] - S2 --> S3["50% traffic
Monitor 60s"] - S3 --> S4["100% traffic
Promotion complete"] - end - - subgraph "Auto-Rollback Triggers" - T1["Success rate < 99%"] - T2["P99 latency > 500ms"] - T3["Error rate > 1%"] - end - - T1 & T2 & T3 -->|"failureLimit: 2"| RB[Automatic Rollback] - - style S4 fill:#4caf50,color:#fff - style RB fill:#f44336,color:#fff -``` - -```bash -# Deploy with canary strategy (requires Argo Rollouts) -./deployments/scripts/deploy.sh \ - --env production --method helm --strategy canary -``` - -Canary analysis is defined in `kubernetes/strategies/canary/canary-analysis.yaml` with three Prometheus queries: - -| Metric | Threshold | Window | -|--------|-----------|--------| -| Success rate | ≥ 99% | 60s | -| P99 latency | < 500ms | 60s | -| Error rate | ≤ 1% | 60s | - ---- - -## Operations Scripts - -All scripts live in `deployments/scripts/` and share consistent flags: - -```mermaid -graph LR - Deploy["deploy.sh
Build, push, deploy"] --> Health["health-check.sh
Verify deployment"] - Deploy --> Rollback["rollback.sh
Undo deployment"] - Deploy --> BGSwitch["blue-green-switch.sh
Switch traffic slots"] - - Backup["db-backup.sh
Backup SQLite"] --> Restore["db-restore.sh
Restore from backup"] - - Teardown["teardown.sh
Destroy environment"] -.->|"Requires confirmation"| Deploy - - style Deploy fill:#4caf50,color:#fff - style Rollback fill:#ff9800,color:#fff - style Teardown fill:#f44336,color:#fff -``` - -### deploy.sh - -The primary deployment orchestrator. Builds images, pushes to registry, and deploys using your chosen method and strategy. - -```bash -# Basic deployment -./deployments/scripts/deploy.sh --env dev --method helm - -# Production with blue-green -./deployments/scripts/deploy.sh \ - --env production \ - --method helm \ - --strategy blue-green \ - --tag v1.2.3 - -# Dry run (preview changes) -./deployments/scripts/deploy.sh \ - --env staging --method kustomize --dry-run - -# Skip image build (use existing image) -./deployments/scripts/deploy.sh \ - --env production --method helm --skip-build --tag sha-abc1234 - -# Terraform deployment -./deployments/scripts/deploy.sh --env production --method terraform -``` - -### health-check.sh - -Comprehensive health verification — HTTP endpoint, WebSocket connectivity, and response time thresholds. - -```bash -# Basic health check -./deployments/scripts/health-check.sh --url http://localhost:4820 - -# With custom thresholds -./deployments/scripts/health-check.sh \ - --url https://monitor.example.com \ - --retries 60 \ - --interval 10 \ - --threshold 1000 - -# JSON output (for CI pipelines) -./deployments/scripts/health-check.sh \ - --url http://localhost:4820 --json - -# Skip WebSocket check -./deployments/scripts/health-check.sh \ - --url http://localhost:4820 --no-websocket -``` - -### rollback.sh - -Roll back to a previous deployment version. - -```bash -# Roll back Helm to previous release -./deployments/scripts/rollback.sh --env production --method helm - -# Roll back to specific revision -./deployments/scripts/rollback.sh --env production --method helm --revision 5 - -# Roll back Kustomize deployment -./deployments/scripts/rollback.sh --env staging --method kustomize -``` - -### blue-green-switch.sh - -Switch live traffic between blue and green deployment slots. - -```bash -# Switch production to green -./deployments/scripts/blue-green-switch.sh --env production --target green - -# Instant rollback to blue -./deployments/scripts/blue-green-switch.sh --env production --target blue - -# Dry run -./deployments/scripts/blue-green-switch.sh \ - --env production --target green --dry-run -``` - -### db-backup.sh / db-restore.sh - -Back up and restore the SQLite database from Kubernetes PVCs. - -```bash -# Backup -./deployments/scripts/db-backup.sh \ - --env production --output ./backups - -# Backup with S3 upload -./deployments/scripts/db-backup.sh \ - --env production --output ./backups \ - --upload s3://my-bucket/backups/ - -# Restore from backup -./deployments/scripts/db-restore.sh \ - --env production --input ./backups/dashboard-20240128-143022.db.gz -``` - -### teardown.sh - -Destroy an entire environment. Requires explicit confirmation for production. - -```bash -# Tear down dev environment -./deployments/scripts/teardown.sh --env dev --method helm - -# Tear down production (requires typing environment name to confirm) -./deployments/scripts/teardown.sh --env production --method terraform - -# Also delete PVCs (permanent data loss) -./deployments/scripts/teardown.sh \ - --env staging --method helm --delete-pvc -``` - ---- - -## CI/CD Pipelines - -Pre-built pipelines for GitHub Actions and GitLab CI. - -### Pipeline Flow - -```mermaid -graph LR - subgraph "CI Pipeline" - Lint["Lint &
Typecheck"] --> Test["Unit
Tests"] - Test --> Build["Build
Images"] - Build --> Scan["Security
Scan (Trivy)"] - end - - subgraph "CD Pipeline" - Scan --> DeployStg["Deploy
Staging"] - DeployStg --> SmokeTest["Smoke
Tests"] - SmokeTest --> Gate["Manual
Approval"] - Gate --> DeployProd["Deploy
Production"] - DeployProd --> HealthCheck["Health
Check"] - end - - subgraph "Safety" - HealthCheck -->|Fail| AutoRollback["Auto
Rollback"] - DeployProd -->|Manual| ManualRollback["Manual
Rollback"] - end - - style Scan fill:#e91e63,color:#fff - style Gate fill:#ff9800,color:#fff - style AutoRollback fill:#f44336,color:#fff -``` - -### GitHub Actions - -Three workflow files in `deployments/ci/github-actions/`: - -| Workflow | Trigger | Actions | -|----------|---------|---------| -| `ci.yaml` | Push, PR | Lint, test, build images, Trivy scan | -| `deploy.yaml` | Tag `v*`, manual | Deploy to staging → approval → production | -| `rollback.yaml` | Manual | Roll back any environment | - -```bash -# Copy workflows to your repo -cp -r deployments/ci/github-actions/*.yaml .github/workflows/ - -# Required GitHub secrets: -# - KUBE_CONFIG (base64 kubeconfig) -# - REGISTRY_USERNAME (container registry user) -# - REGISTRY_PASSWORD (container registry token) -``` - -### GitLab CI - -Single pipeline file in `deployments/ci/gitlab-ci/`: - -```bash -# Copy to repo root -cp deployments/ci/gitlab-ci/.gitlab-ci.yml . - -# Required CI/CD variables: -# - KUBE_CONFIG (base64 kubeconfig, type: File) -# - CI_REGISTRY_USER (auto-provided by GitLab) -# - CI_REGISTRY_PASSWORD (auto-provided by GitLab) -``` - ---- - -## Monitoring & Observability - -### Stack Overview - -```mermaid -graph TB - App[Agent Monitor
Pods] -->|"/metrics"| Prom[Prometheus
Scraping & Storage] - App -->|"logs + metrics"| OTEL[OTel Collector
DaemonSet] - Prom --> Graf[Grafana
Dashboards] - Prom --> AM[Alertmanager
Routing & Notifications] - OTEL -->|"OTLP gRPC"| CX[Coralogix
Full-Stack Observability] - - AM --> Slack[Slack] - AM --> PD[PagerDuty] - AM --> Email[Email] - CX --> CXA[Coralogix Alerts] - CXA --> PD - CXA --> Slack - - subgraph "Grafana Dashboard" - P1[Request Rate] - P2[Error Rate] - P3[Latency P50/P95/P99] - P4[WebSocket Connections] - P5[CPU / Memory Usage] - P6[SQLite Operations] - end - - subgraph "Coralogix Dashboard" - C1[Log Analytics / DataPrime] - C2[Metrics + Recording Rules] - C3[SLO Tracking + Error Budget] - C4[Distributed Tracing] - end - - Graf --- P1 & P2 & P3 & P4 & P5 & P6 - CX --- C1 & C2 & C3 & C4 - - style Prom fill:#e6522c,color:#fff - style Graf fill:#f46800,color:#fff - style AM fill:#e6522c,color:#fff - style CX fill:#1a1a2e,color:#fff - style OTEL fill:#4f46e5,color:#fff - style CXA fill:#dc2626,color:#fff -``` - -### Setup - -```bash -# Import Grafana dashboard -# File: deployments/monitoring/grafana/dashboards/agent-monitor.json -# → Import via Grafana UI: Dashboards → Import → Upload JSON - -# Apply Prometheus rules -kubectl apply -f deployments/monitoring/prometheus/rules/agent-monitor.rules.yaml - -# Apply Prometheus scrape config -# Merge deployments/monitoring/prometheus/prometheus.yaml into your Prometheus config - -# Apply Alertmanager config -# Merge deployments/monitoring/alertmanager/alertmanager.yaml into your Alertmanager config - -# Deploy Coralogix OTel Collector (optional – full-stack observability) -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= -helm install coralogix-otel coralogix/opentelemetry \ - --namespace agent-monitor \ - -f deployments/monitoring/coralogix/values.yaml -``` - -### Alert Rules - -13 alert rules organized by category: - -| Alert | Severity | Condition | -|-------|----------|-----------| -| `AgentMonitorDown` | critical | Instance unreachable > 2min | -| `HighErrorRate` | critical | 5xx rate > 5% for 5min | -| `HighLatency` | warning | P95 latency > 2s for 5min | -| `WebSocketConnectionSpike` | warning | WS connections > 1000 | -| `HighMemoryUsage` | warning | Memory > 85% of limit | -| `HighCpuUsage` | warning | CPU > 80% for 10min | -| `PVNearlyFull` | critical | PV usage > 90% | -| `PodRestartLooping` | critical | > 5 restarts in 15min | -| `HpaMaxedOut` | warning | Replicas at max for 15min | -| `SlowDatabaseQueries` | warning | DB query time > 1s | - -### Grafana Dashboard - -The pre-built dashboard (`agent-monitor.json`) includes 16 panels across 6 rows: - -- **Overview** — Request rate, active sessions, WebSocket connections -- **HTTP Performance** — Latency histograms, status code distribution, error rate -- **WebSocket** — Connection count, message throughput, connection duration -- **Database** — Query duration, row counts, WAL checkpoint time -- **Resources** — CPU, memory, network I/O, filesystem usage -- **Deployment** — Pod status, restart count, HPA scaling events - -### Coralogix Dashboard - -The Coralogix custom dashboard (`monitoring/coralogix/dashboards.yaml`) provides 18 panels across 6 rows with SLO tracking: - -- **Overview** — Active sessions, request rate, WebSocket connections -- **HTTP Performance** — Latency P50/P95/P99, error rate with thresholds, status code distribution -- **Application Logs** — Error log stream via DataPrime, log volume by severity, hook event throughput -- **Infrastructure** — CPU, memory, pod status gauges -- **Database & Storage** — SQLite query duration, PV usage gauge, network I/O -- **SLO Tracking** — Availability SLO (99.9% target), latency SLO (P95 < 500ms), error budget remaining - ---- - -## Security Model - -```mermaid -graph TB - subgraph "Network Security" - NS1[Private subnets for compute] - NS2[NetworkPolicy: restricted egress] - NS3[TLS 1.3 at load balancer] - NS4[HSTS enforcement] - end - - subgraph "Pod Security" - PS1[Restricted PSS enforcement] - PS2[runAsNonRoot / UID 1000] - PS3[readOnlyRootFilesystem] - PS4[Drop ALL capabilities] - PS5[Seccomp RuntimeDefault] - PS6[No privilege escalation] - PS7[No automount SA token] - end - - subgraph "Data Security" - DS1[Encrypted storage at rest] - DS2[Encrypted transit NFS] - DS3[DB backups before destructive ops] - DS4[Production confirmation gates] - end - - subgraph "CI/CD Security" - CS1[OIDC auth - no long-lived creds] - CS2[Trivy vulnerability scanning] - CS3[Pipeline blocks on CRITICAL/HIGH CVEs] - CS4[GitHub Environment protection rules] - end -``` - ---- - -## Directory Reference - -``` -deployments/ -├── ci/ # CI/CD pipeline definitions -│ ├── github-actions/ -│ │ ├── ci.yaml # Build, test, scan -│ │ ├── deploy.yaml # Staged deployment -│ │ └── rollback.yaml # Emergency rollback -│ └── gitlab-ci/ -│ └── .gitlab-ci.yml # Full GitLab pipeline -├── helm/ -│ └── agent-monitor/ # Helm chart -│ ├── Chart.yaml -│ ├── values.yaml # Default values -│ ├── values-dev.yaml # Dev overrides -│ ├── values-staging.yaml # Staging overrides -│ ├── values-production.yaml # Production overrides -│ └── templates/ # 12 Kubernetes templates -├── kubernetes/ # Kustomize manifests -│ ├── base/ # 11 base resources -│ ├── overlays/ -│ │ ├── dev/ -│ │ ├── staging/ -│ │ └── production/ -│ ├── components/ -│ │ ├── mcp-sidecar/ # Optional MCP sidecar -│ │ └── monitoring/ # Optional ServiceMonitor -│ └── strategies/ -│ ├── blue-green/ # Blue-green deployments -│ └── canary/ # Canary with analysis -├── monitoring/ -│ ├── alertmanager/ # Alert routing config -│ ├── coralogix/ # Coralogix full-stack observability -│ │ ├── values.yaml # OTel Collector Helm values -│ │ ├── alerts.yaml # Alert definitions -│ │ ├── dashboards.yaml # Custom dashboard (18 panels) -│ │ └── coralogix-terraform.tf # Terraform-managed resources -│ ├── grafana/ -│ │ ├── dashboards/ # Pre-built dashboard JSON -│ │ └── datasources.yaml -│ └── prometheus/ -│ ├── prometheus.yaml # Scrape configuration -│ └── rules/ # 13 alerting rules -├── scripts/ # Operational scripts -│ ├── deploy.sh # Primary deploy orchestrator -│ ├── rollback.sh # Version rollback -│ ├── blue-green-switch.sh # Traffic slot switching -│ ├── health-check.sh # Deployment verification -│ ├── db-backup.sh # Database backup -│ ├── db-restore.sh # Database restore -│ └── teardown.sh # Environment teardown -└── terraform/ # Infrastructure as Code - ├── main.tf # Root module - ├── variables.tf # Input variables - ├── outputs.tf # Output values - ├── versions.tf # Provider version constraints - ├── backend.tf # State backend configs - ├── modules/ - │ ├── networking/ # VPC, subnets, security groups - │ ├── compute/ # Container orchestration - │ ├── database/ # Persistent storage - │ ├── loadbalancer/ # ALB with TLS & WebSocket - │ └── monitoring/ # Alarms & dashboards - ├── providers/ - │ ├── aws/ # ECS Fargate + ALB + EFS - │ ├── gcp/ # Cloud Run + GCLB + Filestore - │ ├── azure/ # ACI + App Gateway + Files - │ └── oci/ # OKE + LBaaS + FSS - └── environments/ - ├── dev/ - ├── staging/ - └── production/ -``` - ---- - -## Common Workflows - -### First Production Deployment - -```mermaid -graph TD - A[1. Build image] --> B[2. Push to registry] - B --> C[3. Create namespace] - C --> D[4. Deploy with Helm] - D --> E[5. Health check] - E -->|Pass| F[6. Configure DNS] - E -->|Fail| G[6. Rollback] - F --> H[7. Enable monitoring] -``` - -```bash -# 1–2. Build and push -docker build -t ghcr.io/your-org/agent-monitor:v1.0.0 . -docker push ghcr.io/your-org/agent-monitor:v1.0.0 - -# 3–4. Deploy -./deployments/scripts/deploy.sh \ - --env production \ - --method helm \ - --tag v1.0.0 \ - --skip-build - -# 5. Verify -./deployments/scripts/health-check.sh --url https://monitor.example.com - -# 7. Enable monitoring -helm upgrade agent-monitor deployments/helm/agent-monitor \ - -f deployments/helm/agent-monitor/values-production.yaml \ - --set monitoring.enabled=true \ - -n agent-monitor-production -``` - -### Zero-Downtime Release - -```bash -# 1. Deploy new version to green slot -./deployments/scripts/deploy.sh \ - --env production --method helm \ - --strategy blue-green --tag v1.1.0 - -# 2. Verify green is healthy -./deployments/scripts/health-check.sh \ - --url http://green-internal:4820 - -# 3. Switch traffic -./deployments/scripts/blue-green-switch.sh \ - --env production --target green - -# 4. Verify production -./deployments/scripts/health-check.sh \ - --url https://monitor.example.com - -# 5. If something goes wrong — instant rollback -./deployments/scripts/blue-green-switch.sh \ - --env production --target blue -``` - -### Disaster Recovery - -```bash -# 1. Backup current state -./deployments/scripts/db-backup.sh \ - --env production --output ./backups - -# 2. Restore from backup -./deployments/scripts/db-restore.sh \ - --env production \ - --input ./backups/dashboard-latest.db.gz - -# 3. Verify -./deployments/scripts/health-check.sh --url https://monitor.example.com -``` - ---- - -## Troubleshooting - -### Pod not starting - -```bash -# Check pod status -kubectl get pods -n agent-monitor-production - -# Check events -kubectl describe pod -n agent-monitor-production - -# Check logs -kubectl logs -n agent-monitor-production -``` - -### WebSocket connections dropping - -The dashboard requires WebSocket sticky sessions. Verify: - -```bash -# Helm: check service session affinity -kubectl get svc -n agent-monitor-production -o yaml | grep -A5 sessionAffinity - -# Ingress: check WebSocket annotations -kubectl get ingress -n agent-monitor-production -o yaml | grep -A10 annotations -``` - -Required ingress annotations for WebSocket: -```yaml -nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" -nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" -``` - -### Database locked errors - -SQLite supports one writer at a time. Ensure: - -1. PVC access mode is `ReadWriteOnce` (not `ReadWriteMany`) -2. Only one pod writes at a time (replica count or leader election) -3. WAL mode is enabled (default in the application) - -### Terraform state issues - -```bash -# Refresh state -terraform refresh -var-file=environments/production/terraform.tfvars - -# Import existing resource -terraform import -var-file=environments/production/terraform.tfvars \ - module.networking.aws_vpc.main vpc-12345 - -# Unlock state (if locked by a failed run) -terraform force-unlock -``` diff --git a/DESKTOP.md b/DESKTOP.md deleted file mode 100644 index 622c7a1..0000000 --- a/DESKTOP.md +++ /dev/null @@ -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--arm64.dmg` | drag into `/Applications` | - | macOS (Intel) | `ClaudeCodeMonitor--x64.dmg` | drag into `/Applications` | - | Windows (installer) | `ClaudeCodeMonitor-Setup--x64.exe` | per-user install, no admin | - | Windows (portable) | `ClaudeCodeMonitor--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 -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--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--x64.exe -npm run desktop:win:portable # no-install portable → desktop/release/ClaudeCodeMonitor--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 4821–4829, 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 (4821–4829, 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*` | diff --git a/INSTALL.md b/INSTALL.md index 9594ba2..aa46560 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,6 +1,6 @@ # 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 @@ -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--arm64.dmg` | drag into `/Applications` | -| macOS (Intel) | `ClaudeCodeMonitor--x64.dmg` | drag into `/Applications` | -| Windows (installer) | `ClaudeCodeMonitor-Setup--x64.exe` | per-user install, no admin | -| Windows (portable) | `ClaudeCodeMonitor--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 -R Smartgift-AI/Claude-Code-Monitor -n ClaudeCodeMonitor-dmg # macOS - gh run download -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--x64.exe`. It installs **per-user** (no administrator elevation) and lets you pick the install directory — or run the `*-portable.exe` to launch without installing. -2. The installer is **unsigned** by default, so Windows **SmartScreen** may show *"Windows protected your PC"* on first launch — click **More info → Run anyway**. -3. Launch from the Start menu / desktop shortcut. - -Once running, the embedded server boots on port `4820` (or adopts an already-healthy server on `4820`, or falls back to `4821`–`4829` / a random high port), the menu-bar / notification-area (tray) icon appears, and the dashboard window opens. **Hooks are installed automatically on first boot** — an install-only user does not need `npm run install-hooks`; just start a new Claude Code session. Closing the window hides it but keeps the server running; **Quit** from the tray exits. - -> [!NOTE] -> The packaged app stores its SQLite database and VAPID keys in a per-user app-data directory **outside** the app bundle / install dir — `~/Library/Application Support/Claude Code Monitor/data/` on macOS, `%APPDATA%\Claude Code Monitor\data\` on Windows. Your imported history and events therefore **survive app reinstalls and updates** (the Windows NSIS uninstaller keeps this data by default). (Older macOS builds kept the database inside the bundle, which is read-only once installed and code-signed — that broke History Import; it is now fixed. If you are upgrading from a pre-fix build, there is a one-time data gap: re-run **Settings → Import History → Rescan** once.) - -Full user guide: [`DESKTOP.md`](DESKTOP.md). Contributor / architecture reference: [`desktop/README.md`](desktop/README.md). Desktop-specific setup details (logs, auto-start, port adoption) are in [SETUP.md → Desktop App Setup](./SETUP.md#desktop-app-setup). - ---- - ## Optional: Local MCP server If you want AI agents to call dashboard functionality through MCP tools, run the local MCP server in `mcp/`: @@ -469,17 +353,6 @@ If you see an error box at startup saying *"SQLite backend not available"*, eith Then run: `npm rebuild better-sqlite3` -### Desktop build or install fails on the native dependency - -Unlike the root server (which falls back to `node:sqlite`), the desktop app **requires** `better-sqlite3` built for Electron's ABI. If that build can't happen, `npm run desktop:install` (and the desktop `prebuild` gate that runs before every `desktop:*` build) now stops with copy-pasteable setup help instead of a raw node-gyp trace or a runtime crash: it lists the per-OS C++ toolchain prerequisites (Windows: Visual Studio Build Tools + "Desktop development with C++"; macOS: `xcode-select --install`; Linux: build-essential + python3), notes that Node LTS 20/22 ship prebuilt binaries, and offers a no-toolchain alternative: - -```bash -cd desktop -npm install --ignore-scripts -node node_modules/electron/install.js -npx electron-builder install-app-deps -``` - ### `npm run dev` fails immediately Ensure both server and client dependencies are installed: @@ -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. -### 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 diff --git a/README.md b/README.md index 237e235..8ce7522 100644 --- a/README.md +++ b/README.md @@ -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 | | `bin/ccam.js` | CLI | | `mcp/` | MCP server exposing read-only dashboard tools | -| `desktop/` | Electron wrapper that embeds the server | -| `docs/` | Architecture, API, lanes, database, deployment | +| `docs/` | Architecture, API, lanes, database | | `plugins/` | Claude Code plugins shipped with the dashboard | ## 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/API.md`](docs/API.md) — REST endpoints (`openapi.yaml` is generated) - [`docs/DATABASE.md`](docs/DATABASE.md) — tables and migrations -- [`INSTALL.md`](INSTALL.md) · [`DEPLOYMENT.md`](DEPLOYMENT.md) · [`DESKTOP.md`](DESKTOP.md) +- [`INSTALL.md`](INSTALL.md) - [`CLAUDE.md`](CLAUDE.md) — the rules an agent working in this repo must follow diff --git a/SETUP.md b/SETUP.md index 09de175..1d6dd7f 100644 --- a/SETUP.md +++ b/SETUP.md @@ -118,7 +118,7 @@ DASHBOARD_PORT=9000 npm run dev > [!NOTE] > You usually do **not** need to set `DASHBOARD_PORT` manually. `npm run dev` is wrapped by `scripts/dev.js`, which probes both `127.0.0.1` and `::1` (so an SSH `LocalForward` bound to one loopback can't slip past) and picks the first free port in `4820–4859` 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. > @@ -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. -### 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 @@ -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++`) - Run `docker build --no-cache -t agent-monitor .` to force a clean rebuild - Check that `package.json` has `better-sqlite3` under `optionalDependencies`, not `dependencies` - ---- - -### macOS desktop app — `npm run desktop:dmg` is slow - -This is expected. `desktop:dmg` compiles, packages, and ad-hoc-signs the app **twice** — once for `arm64`, once for `x64` — and emits **both** per-arch DMGs (`…-arm64.dmg` + `…-x64.dmg`). It does not merge them into a single universal binary; the two per-arch DMGs are what ship. Packaging two architectures back-to-back is what takes the time; it is not hung. - -For a build that targets your own Mac, use a single-arch command instead — it builds one architecture and finishes in roughly a minute: - -```bash -npm run desktop:dmg:arm64 # Apple Silicon -npm run desktop:dmg:x64 # Intel -``` - -CI already produces both DMGs — pulled either from the [latest GitHub Release](https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor/releases/latest) (CI auto-publishes a `vX.Y.Z` when `package.json` is bumped on `master`) or from the per-commit `ClaudeCodeMonitor-dmg` workflow artifact — so you rarely need to build them locally. - ---- - -### Desktop app — `entry file out/main.js does not exist` - -You ran `electron-builder` without a TypeScript compile. `npm run clean` (in `desktop/`) deletes `out/`, and `electron-builder` only packages — it does not compile. Re-run `npm run desktop:build` first, or use a `desktop:dmg*` / `desktop:win*` script (each one chains `npm run build` for you). Never invoke `electron-builder` bare. - ---- - -### macOS desktop app — Gatekeeper blocks the app on first launch - -The DMG is **ad-hoc signed** by default (the project ships no paid Apple Developer ID), so macOS shows *"Apple could not verify…"* the first time you open the app. Strip the quarantine attribute: - -```bash -xattr -cr "/Applications/Claude Code Monitor.app" -``` - -Or open → *System Settings → Privacy & Security* and click *Open Anyway*. Real Developer ID signing and notarization are opt-in via the `CSC_LINK` / `CSC_KEY_PASSWORD` and `APPLE_ID` / `APPLE_TEAM_ID` / `APPLE_APP_SPECIFIC_PASSWORD` repository secrets — see [`DESKTOP.md`](./DESKTOP.md#notarization-for-the-maintainer). - ---- - -### Windows desktop app — SmartScreen blocks the app on first launch - -The Windows `.exe` (NSIS installer and portable build) is **unsigned** by default, so Windows SmartScreen shows *"Windows protected your PC"* the first time you run it. Click **More info → Run anyway**. Authenticode signing is opt-in via the `CSC_LINK` / `CSC_KEY_PASSWORD` repository secrets — CI picks them up automatically when provided. - ---- - -### Desktop app — no sessions appearing - -The desktop app installs hooks on its **first owned-server boot**, not before. After the app is running, start a **new** Claude Code session and confirm `~/.claude/settings.json` contains entries referencing `hook-handler.js`. If the app adopted an existing server on `4820`, that server's own hook configuration applies instead. For a blank dashboard window, check the desktop log (`~/Library/Logs/Claude Code Monitor/desktop.log` on macOS, `%APPDATA%\Claude Code Monitor\logs\desktop.log` on Windows) via tray → *Show Logs* and use tray → *Restart Server*. diff --git a/client/src/App.tsx b/client/src/App.tsx index 093a814..86f15ab 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -35,11 +35,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `./components/Layout` * - `./components/SplashScreen` diff --git a/client/src/components/AgentCard.tsx b/client/src/components/AgentCard.tsx index 4c176ef..089204c 100644 --- a/client/src/components/AgentCard.tsx +++ b/client/src/components/AgentCard.tsx @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `./StatusBadge` * - `../lib/types` diff --git a/client/src/components/AlertsNotifications.tsx b/client/src/components/AlertsNotifications.tsx index 719b474..d90818f 100644 --- a/client/src/components/AlertsNotifications.tsx +++ b/client/src/components/AlertsNotifications.tsx @@ -27,11 +27,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../lib/api` * - `../lib/eventBus` diff --git a/client/src/components/Checkbox.tsx b/client/src/components/Checkbox.tsx index 93f8313..efeb090 100644 --- a/client/src/components/Checkbox.tsx +++ b/client/src/components/Checkbox.tsx @@ -31,11 +31,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Public surface * - `CheckboxProps` — exported API; see TSDoc on the symbol for behavior. * - `Checkbox` — exported API; see TSDoc on the symbol for behavior. diff --git a/client/src/components/DateTimePicker.tsx b/client/src/components/DateTimePicker.tsx index 8fd4c05..3e9220a 100644 --- a/client/src/components/DateTimePicker.tsx +++ b/client/src/components/DateTimePicker.tsx @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Public surface * - `DateTimePicker` — exported API; see TSDoc on the symbol for behavior. * diff --git a/client/src/components/EmptyState.tsx b/client/src/components/EmptyState.tsx index 063c3c2..8f99f84 100644 --- a/client/src/components/EmptyState.tsx +++ b/client/src/components/EmptyState.tsx @@ -35,11 +35,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Public surface * - `EmptyStateProps` — exported API; see TSDoc on the symbol for behavior. * - `EmptyState` — exported API; see TSDoc on the symbol for behavior. diff --git a/client/src/components/EventDetail.tsx b/client/src/components/EventDetail.tsx index a2ec15a..8951fc1 100644 --- a/client/src/components/EventDetail.tsx +++ b/client/src/components/EventDetail.tsx @@ -24,11 +24,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../lib/types` * - `../lib/event-grouping` diff --git a/client/src/components/EventFilters.tsx b/client/src/components/EventFilters.tsx index 49e682f..f313320 100644 --- a/client/src/components/EventFilters.tsx +++ b/client/src/components/EventFilters.tsx @@ -24,11 +24,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../lib/api` * - `./DateTimePicker` diff --git a/client/src/components/EventFiltersInfo.tsx b/client/src/components/EventFiltersInfo.tsx index f7b7828..4a673a9 100644 --- a/client/src/components/EventFiltersInfo.tsx +++ b/client/src/components/EventFiltersInfo.tsx @@ -28,11 +28,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `./StatusBadge` * diff --git a/client/src/components/FieldHelp.tsx b/client/src/components/FieldHelp.tsx index d093600..6f12ccf 100644 --- a/client/src/components/FieldHelp.tsx +++ b/client/src/components/FieldHelp.tsx @@ -29,11 +29,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Public surface * - `FieldHelpProps` — exported API; see TSDoc on the symbol for behavior. * - `FieldHelp` — exported API; see TSDoc on the symbol for behavior. diff --git a/client/src/components/ImportHistory.tsx b/client/src/components/ImportHistory.tsx index 4151d11..7d1d76f 100644 --- a/client/src/components/ImportHistory.tsx +++ b/client/src/components/ImportHistory.tsx @@ -29,11 +29,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../lib/api` * - `../lib/eventBus` diff --git a/client/src/components/Layout.tsx b/client/src/components/Layout.tsx index d6182c9..9b9c46e 100644 --- a/client/src/components/Layout.tsx +++ b/client/src/components/Layout.tsx @@ -35,11 +35,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `./Sidebar` * - `./UpdateNotifier` diff --git a/client/src/components/RemoteSources.tsx b/client/src/components/RemoteSources.tsx index d98b097..3623586 100644 --- a/client/src/components/RemoteSources.tsx +++ b/client/src/components/RemoteSources.tsx @@ -30,11 +30,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../lib/api` * - `../lib/eventBus` diff --git a/client/src/components/Select.tsx b/client/src/components/Select.tsx index 1104fad..cd1ffa0 100644 --- a/client/src/components/Select.tsx +++ b/client/src/components/Select.tsx @@ -29,11 +29,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Public surface * - `SelectOption` — exported API; see TSDoc on the symbol for behavior. * - `SelectProps` — exported API; see TSDoc on the symbol for behavior. diff --git a/client/src/components/SessionCard.tsx b/client/src/components/SessionCard.tsx index 29851cc..2eb07b3 100644 --- a/client/src/components/SessionCard.tsx +++ b/client/src/components/SessionCard.tsx @@ -23,11 +23,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `./StatusBadge` * - `../lib/types` diff --git a/client/src/components/SessionOverview.tsx b/client/src/components/SessionOverview.tsx index 03f0104..03fac9e 100644 --- a/client/src/components/SessionOverview.tsx +++ b/client/src/components/SessionOverview.tsx @@ -24,11 +24,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../lib/api` * - `../lib/eventBus` diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx index 40e0a2a..a7c53a3 100644 --- a/client/src/components/Sidebar.tsx +++ b/client/src/components/Sidebar.tsx @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../lib/api` * - `../lib/eventBus` diff --git a/client/src/components/Skeleton.tsx b/client/src/components/Skeleton.tsx index 5657e41..347d781 100644 --- a/client/src/components/Skeleton.tsx +++ b/client/src/components/Skeleton.tsx @@ -24,11 +24,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Public surface * - `Skeleton` — exported API; see TSDoc on the symbol for behavior. * - `StatValueSkeleton` — exported API; see TSDoc on the symbol for behavior. diff --git a/client/src/components/SplashScreen.tsx b/client/src/components/SplashScreen.tsx index b0cb762..f0774d1 100644 --- a/client/src/components/SplashScreen.tsx +++ b/client/src/components/SplashScreen.tsx @@ -27,11 +27,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Public surface * - `SplashScreen` — exported API; see TSDoc on the symbol for behavior. * diff --git a/client/src/components/StatCard.tsx b/client/src/components/StatCard.tsx index f8c51ab..b1ebc07 100644 --- a/client/src/components/StatCard.tsx +++ b/client/src/components/StatCard.tsx @@ -28,11 +28,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `./Tip` * - `./Skeleton` diff --git a/client/src/components/StatusBadge.tsx b/client/src/components/StatusBadge.tsx index b312265..e37f576 100644 --- a/client/src/components/StatusBadge.tsx +++ b/client/src/components/StatusBadge.tsx @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../lib/types` * - `./Tip` diff --git a/client/src/components/Tabby/CatAvatar.tsx b/client/src/components/Tabby/CatAvatar.tsx index ca6ac63..7c1802f 100644 --- a/client/src/components/Tabby/CatAvatar.tsx +++ b/client/src/components/Tabby/CatAvatar.tsx @@ -26,11 +26,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `./brain` * diff --git a/client/src/components/Tabby/SpeechBubble.tsx b/client/src/components/Tabby/SpeechBubble.tsx index 87f9a4e..2c879c1 100644 --- a/client/src/components/Tabby/SpeechBubble.tsx +++ b/client/src/components/Tabby/SpeechBubble.tsx @@ -29,11 +29,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Public surface * - `SpeechBubble` — exported API; see TSDoc on the symbol for behavior. * diff --git a/client/src/components/Tabby/Tabby.tsx b/client/src/components/Tabby/Tabby.tsx index e3c0e0b..2c36fde 100644 --- a/client/src/components/Tabby/Tabby.tsx +++ b/client/src/components/Tabby/Tabby.tsx @@ -29,11 +29,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `./CatAvatar` * - `./SpeechBubble` diff --git a/client/src/components/Tabby/TabbyPanel.tsx b/client/src/components/Tabby/TabbyPanel.tsx index 630e068..cbb7e86 100644 --- a/client/src/components/Tabby/TabbyPanel.tsx +++ b/client/src/components/Tabby/TabbyPanel.tsx @@ -23,11 +23,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `./brain` * diff --git a/client/src/components/Tabby/brain.ts b/client/src/components/Tabby/brain.ts index 2872a47..631cb58 100644 --- a/client/src/components/Tabby/brain.ts +++ b/client/src/components/Tabby/brain.ts @@ -24,11 +24,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../../lib/types` * diff --git a/client/src/components/Tabby/intents.ts b/client/src/components/Tabby/intents.ts index 7296e2b..db3d2d1 100644 --- a/client/src/components/Tabby/intents.ts +++ b/client/src/components/Tabby/intents.ts @@ -23,11 +23,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `./brain` * diff --git a/client/src/components/Tabby/prefs.ts b/client/src/components/Tabby/prefs.ts index b2ea5be..54c83c8 100644 --- a/client/src/components/Tabby/prefs.ts +++ b/client/src/components/Tabby/prefs.ts @@ -22,11 +22,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Public surface * - `TabbyPos` — exported API; see TSDoc on the symbol for behavior. * - `tabbyPrefs` — exported API; see TSDoc on the symbol for behavior. diff --git a/client/src/components/Tabby/quips.ts b/client/src/components/Tabby/quips.ts index 8e31220..9905496 100644 --- a/client/src/components/Tabby/quips.ts +++ b/client/src/components/Tabby/quips.ts @@ -22,11 +22,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `./brain` * diff --git a/client/src/components/Tabby/useTabbyBrain.ts b/client/src/components/Tabby/useTabbyBrain.ts index 566471c..d1f7672 100644 --- a/client/src/components/Tabby/useTabbyBrain.ts +++ b/client/src/components/Tabby/useTabbyBrain.ts @@ -23,11 +23,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../../lib/eventBus` * - `../../lib/api` diff --git a/client/src/components/Tabby/useTabbyPosition.ts b/client/src/components/Tabby/useTabbyPosition.ts index 2f07f2f..8986772 100644 --- a/client/src/components/Tabby/useTabbyPosition.ts +++ b/client/src/components/Tabby/useTabbyPosition.ts @@ -25,11 +25,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `./prefs` * diff --git a/client/src/components/Tip.tsx b/client/src/components/Tip.tsx index 6663458..191525c 100644 --- a/client/src/components/Tip.tsx +++ b/client/src/components/Tip.tsx @@ -32,11 +32,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Public surface * - `Tip` — exported API; see TSDoc on the symbol for behavior. * diff --git a/client/src/components/UpdateNotifier.tsx b/client/src/components/UpdateNotifier.tsx index 879365a..42d78ef 100644 --- a/client/src/components/UpdateNotifier.tsx +++ b/client/src/components/UpdateNotifier.tsx @@ -33,11 +33,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../lib/api` * - `../lib/eventBus` diff --git a/client/src/components/WebhookSettings.tsx b/client/src/components/WebhookSettings.tsx index 8c366bf..0d177b5 100644 --- a/client/src/components/WebhookSettings.tsx +++ b/client/src/components/WebhookSettings.tsx @@ -27,11 +27,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../lib/api` * - `./Select` diff --git a/client/src/components/conversation/CodeBlock.tsx b/client/src/components/conversation/CodeBlock.tsx index 3ba1c61..15a8be6 100644 --- a/client/src/components/conversation/CodeBlock.tsx +++ b/client/src/components/conversation/CodeBlock.tsx @@ -22,11 +22,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../../lib/highlight` * diff --git a/client/src/components/conversation/ConversationView.tsx b/client/src/components/conversation/ConversationView.tsx index c0dbd41..d0b0570 100644 --- a/client/src/components/conversation/ConversationView.tsx +++ b/client/src/components/conversation/ConversationView.tsx @@ -25,11 +25,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../../lib/api` * - `../../lib/eventBus` diff --git a/client/src/components/conversation/MarkdownContent.tsx b/client/src/components/conversation/MarkdownContent.tsx index fd2fbec..e0dd8dd 100644 --- a/client/src/components/conversation/MarkdownContent.tsx +++ b/client/src/components/conversation/MarkdownContent.tsx @@ -29,11 +29,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `./CodeBlock` * diff --git a/client/src/components/conversation/MessageList.tsx b/client/src/components/conversation/MessageList.tsx index bbb4f2a..d9ec352 100644 --- a/client/src/components/conversation/MessageList.tsx +++ b/client/src/components/conversation/MessageList.tsx @@ -24,11 +24,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../../lib/types` * - `./ToolCallBlock` diff --git a/client/src/components/conversation/ToolCallBlock.tsx b/client/src/components/conversation/ToolCallBlock.tsx index a058bc5..9fdcb7d 100644 --- a/client/src/components/conversation/ToolCallBlock.tsx +++ b/client/src/components/conversation/ToolCallBlock.tsx @@ -24,11 +24,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../../lib/types` * - `./CodeBlock` diff --git a/client/src/components/conversation/toolStyle.ts b/client/src/components/conversation/toolStyle.ts index 92780bd..253cd83 100644 --- a/client/src/components/conversation/toolStyle.ts +++ b/client/src/components/conversation/toolStyle.ts @@ -22,11 +22,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Public surface * - `ToolStyle` — exported API; see TSDoc on the symbol for behavior. * - `styleForTool` — exported API; see TSDoc on the symbol for behavior. diff --git a/client/src/components/conversation/tuiSegments.ts b/client/src/components/conversation/tuiSegments.ts index 0ff9d5c..72c2cff 100644 --- a/client/src/components/conversation/tuiSegments.ts +++ b/client/src/components/conversation/tuiSegments.ts @@ -24,11 +24,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Public surface * - `TuiSegment` — exported API; see TSDoc on the symbol for behavior. * - `stripAnsi` — exported API; see TSDoc on the symbol for behavior. diff --git a/client/src/components/event-views/primitives.tsx b/client/src/components/event-views/primitives.tsx index 5b31c1b..9be363d 100644 --- a/client/src/components/event-views/primitives.tsx +++ b/client/src/components/event-views/primitives.tsx @@ -24,11 +24,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Public surface * - `CopyButton` — exported API; see TSDoc on the symbol for behavior. * - `Terminal` — exported API; see TSDoc on the symbol for behavior. diff --git a/client/src/components/event-views/tool-views.tsx b/client/src/components/event-views/tool-views.tsx index 5b06932..b8fbf60 100644 --- a/client/src/components/event-views/tool-views.tsx +++ b/client/src/components/event-views/tool-views.tsx @@ -36,11 +36,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `./primitives` * diff --git a/client/src/components/webhookGuides.ts b/client/src/components/webhookGuides.ts index 3beb26b..74d2b6e 100644 --- a/client/src/components/webhookGuides.ts +++ b/client/src/components/webhookGuides.ts @@ -23,11 +23,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../lib/types` * diff --git a/client/src/components/workflows/AgentCollaborationNetwork.tsx b/client/src/components/workflows/AgentCollaborationNetwork.tsx index c61fca8..e6bab79 100644 --- a/client/src/components/workflows/AgentCollaborationNetwork.tsx +++ b/client/src/components/workflows/AgentCollaborationNetwork.tsx @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Public surface * - `AgentCollaborationNetworkProps` — exported API; see TSDoc on the symbol for behavior. * - `AgentCollaborationNetwork` — exported API; see TSDoc on the symbol for behavior. diff --git a/client/src/components/workflows/CompactionImpact.tsx b/client/src/components/workflows/CompactionImpact.tsx index bf2b298..09bcf17 100644 --- a/client/src/components/workflows/CompactionImpact.tsx +++ b/client/src/components/workflows/CompactionImpact.tsx @@ -25,11 +25,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../../lib/types` * diff --git a/client/src/components/workflows/ConcurrencyTimeline.tsx b/client/src/components/workflows/ConcurrencyTimeline.tsx index ee89ad6..9ce2652 100644 --- a/client/src/components/workflows/ConcurrencyTimeline.tsx +++ b/client/src/components/workflows/ConcurrencyTimeline.tsx @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../../lib/types` * diff --git a/client/src/components/workflows/ErrorPropagationMap.tsx b/client/src/components/workflows/ErrorPropagationMap.tsx index 0169f55..a271da4 100644 --- a/client/src/components/workflows/ErrorPropagationMap.tsx +++ b/client/src/components/workflows/ErrorPropagationMap.tsx @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../../lib/types` * diff --git a/client/src/components/workflows/ModelDelegationFlow.tsx b/client/src/components/workflows/ModelDelegationFlow.tsx index bce5679..a93729a 100644 --- a/client/src/components/workflows/ModelDelegationFlow.tsx +++ b/client/src/components/workflows/ModelDelegationFlow.tsx @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../../lib/types` * - `../../lib/format` diff --git a/client/src/components/workflows/OrchestrationDAG.tsx b/client/src/components/workflows/OrchestrationDAG.tsx index 71ca299..fb106f6 100644 --- a/client/src/components/workflows/OrchestrationDAG.tsx +++ b/client/src/components/workflows/OrchestrationDAG.tsx @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../../lib/types` * diff --git a/client/src/components/workflows/SessionComplexityScatter.tsx b/client/src/components/workflows/SessionComplexityScatter.tsx index 2867f09..facc1d8 100644 --- a/client/src/components/workflows/SessionComplexityScatter.tsx +++ b/client/src/components/workflows/SessionComplexityScatter.tsx @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../../lib/types` * - `../../lib/format` diff --git a/client/src/components/workflows/SessionDrillIn.tsx b/client/src/components/workflows/SessionDrillIn.tsx index 30c5fd6..66d2ed8 100644 --- a/client/src/components/workflows/SessionDrillIn.tsx +++ b/client/src/components/workflows/SessionDrillIn.tsx @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../../lib/api` * - `../../lib/format` diff --git a/client/src/components/workflows/SubagentEffectiveness.tsx b/client/src/components/workflows/SubagentEffectiveness.tsx index 36a1fee..f49a1f1 100644 --- a/client/src/components/workflows/SubagentEffectiveness.tsx +++ b/client/src/components/workflows/SubagentEffectiveness.tsx @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../../lib/types` * diff --git a/client/src/components/workflows/ToolExecutionFlow.tsx b/client/src/components/workflows/ToolExecutionFlow.tsx index 01e7f90..df8e2b3 100644 --- a/client/src/components/workflows/ToolExecutionFlow.tsx +++ b/client/src/components/workflows/ToolExecutionFlow.tsx @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../../lib/types` * diff --git a/client/src/components/workflows/WorkflowPatterns.tsx b/client/src/components/workflows/WorkflowPatterns.tsx index dd50f5c..58856b6 100644 --- a/client/src/components/workflows/WorkflowPatterns.tsx +++ b/client/src/components/workflows/WorkflowPatterns.tsx @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../../lib/types` * diff --git a/client/src/components/workflows/WorkflowRunsPanel.tsx b/client/src/components/workflows/WorkflowRunsPanel.tsx index de8a631..92024b6 100644 --- a/client/src/components/workflows/WorkflowRunsPanel.tsx +++ b/client/src/components/workflows/WorkflowRunsPanel.tsx @@ -29,11 +29,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../../lib/api` * - `../../lib/eventBus` diff --git a/client/src/components/workflows/WorkflowStats.tsx b/client/src/components/workflows/WorkflowStats.tsx index 0b33c90..b2e9665 100644 --- a/client/src/components/workflows/WorkflowStats.tsx +++ b/client/src/components/workflows/WorkflowStats.tsx @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../../lib/types` * diff --git a/client/src/hooks/useNotifications.ts b/client/src/hooks/useNotifications.ts index d9ab343..00cecc3 100644 --- a/client/src/hooks/useNotifications.ts +++ b/client/src/hooks/useNotifications.ts @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../i18n` * - `../lib/eventBus` diff --git a/client/src/hooks/useWebSocket.ts b/client/src/hooks/useWebSocket.ts index ba2fb15..a3f0001 100644 --- a/client/src/hooks/useWebSocket.ts +++ b/client/src/hooks/useWebSocket.ts @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../lib/types` * - `../lib/eventBus` diff --git a/client/src/i18n/index.ts b/client/src/i18n/index.ts index 4e45a17..eacd09e 100644 --- a/client/src/i18n/index.ts +++ b/client/src/i18n/index.ts @@ -35,11 +35,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `./locales/en/common.json` * - `./locales/vi/common.json` diff --git a/client/src/lib/api.ts b/client/src/lib/api.ts index 9204a12..268222e 100644 --- a/client/src/lib/api.ts +++ b/client/src/lib/api.ts @@ -78,11 +78,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `./types` * - `./dataScope` diff --git a/client/src/lib/dataScope.ts b/client/src/lib/dataScope.ts index 7706c52..c10a60b 100644 --- a/client/src/lib/dataScope.ts +++ b/client/src/lib/dataScope.ts @@ -36,11 +36,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Public surface * - `ScopeMode` — exported API; see TSDoc on the symbol for behavior. * - `DataScope` — exported API; see TSDoc on the symbol for behavior. diff --git a/client/src/lib/event-grouping.ts b/client/src/lib/event-grouping.ts index e89f51a..c0f1ab3 100644 --- a/client/src/lib/event-grouping.ts +++ b/client/src/lib/event-grouping.ts @@ -51,11 +51,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `./types` * diff --git a/client/src/lib/event-summary.ts b/client/src/lib/event-summary.ts index 10993c5..5650201 100644 --- a/client/src/lib/event-summary.ts +++ b/client/src/lib/event-summary.ts @@ -47,11 +47,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `./types` * diff --git a/client/src/lib/eventBus.ts b/client/src/lib/eventBus.ts index 10b6bc8..2ad6482 100644 --- a/client/src/lib/eventBus.ts +++ b/client/src/lib/eventBus.ts @@ -44,11 +44,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `./types` * diff --git a/client/src/lib/format.ts b/client/src/lib/format.ts index 02596cd..e5c0b76 100644 --- a/client/src/lib/format.ts +++ b/client/src/lib/format.ts @@ -41,11 +41,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../i18n` * diff --git a/client/src/lib/highlight.ts b/client/src/lib/highlight.ts index 11fcf7a..8b197d8 100644 --- a/client/src/lib/highlight.ts +++ b/client/src/lib/highlight.ts @@ -61,11 +61,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Public surface * - `TokenType` — exported API; see TSDoc on the symbol for behavior. * - `Token` — exported API; see TSDoc on the symbol for behavior. diff --git a/client/src/lib/push.ts b/client/src/lib/push.ts index 9678aa6..e97c16e 100644 --- a/client/src/lib/push.ts +++ b/client/src/lib/push.ts @@ -40,11 +40,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Public surface * - `subscribeToPush` — exported API; see TSDoc on the symbol for behavior. * - `unsubscribeFromPush` — exported API; see TSDoc on the symbol for behavior. diff --git a/client/src/lib/types.ts b/client/src/lib/types.ts index 6d2291a..a65d90c 100644 --- a/client/src/lib/types.ts +++ b/client/src/lib/types.ts @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Public surface * - `SessionStatus` — exported API; see TSDoc on the symbol for behavior. * - `AgentStatus` — exported API; see TSDoc on the symbol for behavior. diff --git a/client/src/main.tsx b/client/src/main.tsx index e6cc151..b189bd9 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -33,11 +33,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `./App` * diff --git a/client/src/pages/ActivityFeed.tsx b/client/src/pages/ActivityFeed.tsx index aeba781..a224b0c 100644 --- a/client/src/pages/ActivityFeed.tsx +++ b/client/src/pages/ActivityFeed.tsx @@ -24,11 +24,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../lib/api` * - `../lib/eventBus` diff --git a/client/src/pages/Analytics.tsx b/client/src/pages/Analytics.tsx index 686a8f4..2e0cb98 100644 --- a/client/src/pages/Analytics.tsx +++ b/client/src/pages/Analytics.tsx @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../lib/api` * - `../lib/eventBus` diff --git a/client/src/pages/CcConfig.tsx b/client/src/pages/CcConfig.tsx index 4c42c83..6596a16 100644 --- a/client/src/pages/CcConfig.tsx +++ b/client/src/pages/CcConfig.tsx @@ -27,11 +27,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../lib/eventBus` * - `../lib/api` diff --git a/client/src/pages/Dashboard.tsx b/client/src/pages/Dashboard.tsx index 53c2f69..e7e2146 100644 --- a/client/src/pages/Dashboard.tsx +++ b/client/src/pages/Dashboard.tsx @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../lib/api` * - `../lib/eventBus` diff --git a/client/src/pages/KanbanBoard.tsx b/client/src/pages/KanbanBoard.tsx index 3cd98f8..8d47d3c 100644 --- a/client/src/pages/KanbanBoard.tsx +++ b/client/src/pages/KanbanBoard.tsx @@ -24,11 +24,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../lib/api` * - `../lib/eventBus` diff --git a/client/src/pages/NotFound.tsx b/client/src/pages/NotFound.tsx index 9336efc..b61553d 100644 --- a/client/src/pages/NotFound.tsx +++ b/client/src/pages/NotFound.tsx @@ -26,11 +26,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Public surface * - `NotFound` — exported API; see TSDoc on the symbol for behavior. * diff --git a/client/src/pages/SessionDetail.tsx b/client/src/pages/SessionDetail.tsx index e94a521..d5e9c8f 100644 --- a/client/src/pages/SessionDetail.tsx +++ b/client/src/pages/SessionDetail.tsx @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../lib/api` * - `../lib/eventBus` diff --git a/client/src/pages/Sessions.tsx b/client/src/pages/Sessions.tsx index ef255db..bf88242 100644 --- a/client/src/pages/Sessions.tsx +++ b/client/src/pages/Sessions.tsx @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../lib/api` * - `../lib/eventBus` diff --git a/client/src/pages/Settings.tsx b/client/src/pages/Settings.tsx index dc8206f..350ff86 100644 --- a/client/src/pages/Settings.tsx +++ b/client/src/pages/Settings.tsx @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../lib/api` * - `../lib/eventBus` diff --git a/client/src/pages/Workflows.tsx b/client/src/pages/Workflows.tsx index 13f8456..bdb7593 100644 --- a/client/src/pages/Workflows.tsx +++ b/client/src/pages/Workflows.tsx @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../lib/api` * - `../lib/eventBus` diff --git a/deployments/README.md b/deployments/README.md deleted file mode 100644 index a2d241a..0000000 --- a/deployments/README.md +++ /dev/null @@ -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/
Cloud resource provisioning
AWS · GCP · Azure · OCI"] - end - - subgraph "Application Deployment" - HELM["helm/
Parameterized Helm chart
12 templates · 4 value sets"] - KUST["kubernetes/
Kustomize base + overlays
11 resources · 3 envs"] - end - - subgraph "Operations" - SCRIPTS["scripts/
7 operational scripts
deploy · rollback · backup"] - CI["ci/
GitHub Actions + GitLab CI
Build · Scan · Deploy"] - end - - subgraph "Observability" - MON["monitoring/
Prometheus · Grafana · Alertmanager · Coralogix
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
TLS termination
WebSocket upgrade
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
(EFS / Filestore / Azure Files / FSS)"] - SECRETS["Secret Store
(Vault / Secrets Manager)"] - MON["Monitoring
(Prometheus / Grafana)"] - OTEL["OTel Collector
(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= -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 diff --git a/deployments/ci/github-actions/ci.yaml b/deployments/ci/github-actions/ci.yaml deleted file mode 100644 index b2c2e57..0000000 --- a/deployments/ci/github-actions/ci.yaml +++ /dev/null @@ -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" diff --git a/deployments/ci/github-actions/deploy.yaml b/deployments/ci/github-actions/deploy.yaml deleted file mode 100644 index d182ec7..0000000 --- a/deployments/ci/github-actions/deploy.yaml +++ /dev/null @@ -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 diff --git a/deployments/ci/github-actions/rollback.yaml b/deployments/ci/github-actions/rollback.yaml deleted file mode 100644 index 5db1df2..0000000 --- a/deployments/ci/github-actions/rollback.yaml +++ /dev/null @@ -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 diff --git a/deployments/ci/gitlab-ci/.gitlab-ci.yml b/deployments/ci/gitlab-ci/.gitlab-ci.yml deleted file mode 100644 index 8f62b41..0000000 --- a/deployments/ci/gitlab-ci/.gitlab-ci.yml +++ /dev/null @@ -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 diff --git a/deployments/helm/agent-monitor/.helmignore b/deployments/helm/agent-monitor/.helmignore deleted file mode 100644 index 414bb6e..0000000 --- a/deployments/helm/agent-monitor/.helmignore +++ /dev/null @@ -1,18 +0,0 @@ -# Patterns to ignore when building packages. -.DS_Store -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -*.swp -*.bak -*.tmp -*.orig -*~ -.project -.idea/ -*.tmproj -.vscode/ diff --git a/deployments/helm/agent-monitor/Chart.yaml b/deployments/helm/agent-monitor/Chart.yaml deleted file mode 100644 index 2915a01..0000000 --- a/deployments/helm/agent-monitor/Chart.yaml +++ /dev/null @@ -1,28 +0,0 @@ -apiVersion: v2 -name: agent-monitor -description: Claude Code Agent Monitor - Real-time dashboard for tracking Claude Code agent activity -type: application -version: 1.0.0 -appVersion: "1.0.0" - -home: https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor -sources: - - https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor - -keywords: - - claude - - agent - - monitor - - dashboard - - ai - - devtools - - observability - -maintainers: - - name: David Nguyen - url: https://git.smartgift.io.vn/Smartgift-AI - -icon: https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor/raw/branch/master/favicon.svg - -annotations: - artifacthub.io/category: monitoring-logging diff --git a/deployments/helm/agent-monitor/templates/NOTES.txt b/deployments/helm/agent-monitor/templates/NOTES.txt deleted file mode 100644 index 2002e43..0000000 --- a/deployments/helm/agent-monitor/templates/NOTES.txt +++ /dev/null @@ -1,79 +0,0 @@ -╔══════════════════════════════════════════════════════════════════════╗ -║ Claude Code Agent Monitor - Deployment Notes ║ -╚══════════════════════════════════════════════════════════════════════╝ - -{{- $fullName := include "agent-monitor.fullname" . -}} - -🎉 {{ $fullName }} has been deployed to namespace "{{ .Release.Namespace }}"! - -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -📡 Accessing the Dashboard: -{{- if .Values.ingress.enabled }} -{{- range $host := .Values.ingress.hosts }} - {{- range .paths }} - - URL: http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} - {{- end }} -{{- end }} -{{- else if contains "NodePort" .Values.service.type }} - - export NODE_PORT=$(kubectl get -n {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ $fullName }}) - export NODE_IP=$(kubectl get nodes -o jsonpath="{.items[0].status.addresses[0].address}") - echo "Dashboard URL: http://$NODE_IP:$NODE_PORT" - -{{- else if contains "LoadBalancer" .Values.service.type }} - - NOTE: It may take a few minutes for the LoadBalancer IP to be available. - Watch status with: - kubectl get -n {{ .Release.Namespace }} svc {{ $fullName }} -w - - export SERVICE_IP=$(kubectl get svc -n {{ .Release.Namespace }} {{ $fullName }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') - echo "Dashboard URL: http://$SERVICE_IP:{{ .Values.service.port }}" - -{{- else }} - - Port-forward to access locally: - kubectl port-forward -n {{ .Release.Namespace }} svc/{{ $fullName }} 4820:{{ .Values.service.port }} - - Then open: http://localhost:4820 - -{{- end }} - -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -🔍 Health Check: - kubectl exec -n {{ .Release.Namespace }} deploy/{{ $fullName }} -- wget -qO- http://localhost:{{ .Values.service.targetPort }}/api/health - -🧪 Run Tests: - helm test {{ .Release.Name }} -n {{ .Release.Namespace }} - -📊 View Logs: - kubectl logs -n {{ .Release.Namespace }} -l app.kubernetes.io/name={{ include "agent-monitor.name" . }} -f - -📈 Check Pods: - kubectl get pods -n {{ .Release.Namespace }} -l app.kubernetes.io/name={{ include "agent-monitor.name" . }} - -{{- if .Values.persistence.enabled }} - -💾 Persistent Storage: - SQLite data is stored on PVC: {{ $fullName }}-data ({{ .Values.persistence.size }}) -{{- end }} - -{{- if .Values.mcp.enabled }} - -🔌 MCP Sidecar: - MCP server is running on port {{ .Values.mcp.port }} - Access via: kubectl port-forward -n {{ .Release.Namespace }} svc/{{ $fullName }} {{ .Values.mcp.port }}:{{ .Values.mcp.port }} -{{- end }} - -{{- if .Values.autoscaling.enabled }} - -📐 Autoscaling: - Min replicas: {{ .Values.autoscaling.minReplicas }} - Max replicas: {{ .Values.autoscaling.maxReplicas }} - CPU target: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}% - Memory target: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}% -{{- end }} - -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ diff --git a/deployments/helm/agent-monitor/templates/_helpers.tpl b/deployments/helm/agent-monitor/templates/_helpers.tpl deleted file mode 100644 index 58d6917..0000000 --- a/deployments/helm/agent-monitor/templates/_helpers.tpl +++ /dev/null @@ -1,88 +0,0 @@ -{{/* -Expand the name of the chart. -*/}} -{{- define "agent-monitor.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Create a default fully qualified app name. -We truncate at 63 chars because some Kubernetes name fields are limited to this -(by the DNS naming spec). If release name contains the chart name it will be used -as a full name. -*/}} -{{- define "agent-monitor.fullname" -}} -{{- if .Values.fullnameOverride }} -{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- $name := default .Chart.Name .Values.nameOverride }} -{{- if contains $name .Release.Name }} -{{- .Release.Name | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} -{{- end }} -{{- end }} -{{- end }} - -{{/* -Create chart name and version as used by the chart label. -*/}} -{{- define "agent-monitor.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Common labels -*/}} -{{- define "agent-monitor.labels" -}} -helm.sh/chart: {{ include "agent-monitor.chart" . }} -{{ include "agent-monitor.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} -{{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} -app.kubernetes.io/part-of: claude-code-agent-monitor -{{- end }} - -{{/* -Selector labels -*/}} -{{- define "agent-monitor.selectorLabels" -}} -app.kubernetes.io/name: {{ include "agent-monitor.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} - -{{/* -Create the name of the service account to use -*/}} -{{- define "agent-monitor.serviceAccountName" -}} -{{- if .Values.serviceAccount.create }} -{{- default (include "agent-monitor.fullname" .) .Values.serviceAccount.name }} -{{- else }} -{{- default "default" .Values.serviceAccount.name }} -{{- end }} -{{- end }} - -{{/* -Return the container image string -*/}} -{{- define "agent-monitor.image" -}} -{{- $tag := default .Chart.AppVersion .Values.image.tag -}} -{{- if .Values.image.registry -}} -{{- printf "%s/%s:%s" .Values.image.registry .Values.image.repository $tag -}} -{{- else -}} -{{- printf "%s:%s" .Values.image.repository $tag -}} -{{- end -}} -{{- end }} - -{{/* -Return the MCP sidecar container image string -*/}} -{{- define "agent-monitor.mcpImage" -}} -{{- $tag := default .Chart.AppVersion .Values.mcp.image.tag -}} -{{- if .Values.mcp.image.registry -}} -{{- printf "%s/%s:%s" .Values.mcp.image.registry .Values.mcp.image.repository $tag -}} -{{- else -}} -{{- printf "%s:%s" .Values.mcp.image.repository $tag -}} -{{- end -}} -{{- end }} diff --git a/deployments/helm/agent-monitor/templates/configmap.yaml b/deployments/helm/agent-monitor/templates/configmap.yaml deleted file mode 100644 index 1bd1816..0000000 --- a/deployments/helm/agent-monitor/templates/configmap.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ include "agent-monitor.fullname" . }}-config - labels: - {{- include "agent-monitor.labels" . | nindent 4 }} -data: - {{- range $key, $value := .Values.env }} - {{ $key }}: {{ $value | quote }} - {{- end }} - {{- range $key, $value := .Values.extraEnv }} - {{ $key }}: {{ $value | quote }} - {{- end }} diff --git a/deployments/helm/agent-monitor/templates/deployment.yaml b/deployments/helm/agent-monitor/templates/deployment.yaml deleted file mode 100644 index b16b3e8..0000000 --- a/deployments/helm/agent-monitor/templates/deployment.yaml +++ /dev/null @@ -1,159 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "agent-monitor.fullname" . }} - labels: - {{- include "agent-monitor.labels" . | nindent 4 }} -spec: - {{- if not .Values.autoscaling.enabled }} - replicas: {{ .Values.replicaCount }} - {{- end }} - revisionHistoryLimit: {{ .Values.deployment.revisionHistoryLimit | default 5 }} - {{- with .Values.deployment.strategy }} - strategy: - {{- toYaml . | nindent 4 }} - {{- end }} - selector: - matchLabels: - {{- include "agent-monitor.selectorLabels" . | nindent 6 }} - template: - metadata: - annotations: - checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} - {{- with .Values.podAnnotations }} - {{- toYaml . | nindent 8 }} - {{- end }} - labels: - {{- include "agent-monitor.labels" . | nindent 8 }} - {{- with .Values.podLabels }} - {{- toYaml . | nindent 8 }} - {{- end }} - spec: - {{- with .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml . | nindent 8 }} - {{- end }} - serviceAccountName: {{ include "agent-monitor.serviceAccountName" . }} - automountServiceAccountToken: false - {{- with .Values.podSecurityContext }} - securityContext: - {{- toYaml . | nindent 8 }} - {{- end }} - containers: - # ── Main application container ──────────────────────────────────── - - name: {{ .Chart.Name }} - {{- with .Values.securityContext }} - securityContext: - {{- toYaml . | nindent 12 }} - {{- end }} - image: {{ include "agent-monitor.image" . }} - imagePullPolicy: {{ .Values.image.pullPolicy }} - ports: - - name: http - containerPort: {{ .Values.service.targetPort }} - protocol: TCP - {{- with .Values.livenessProbe }} - livenessProbe: - {{- toYaml . | nindent 12 }} - {{- end }} - {{- with .Values.readinessProbe }} - readinessProbe: - {{- toYaml . | nindent 12 }} - {{- end }} - {{- with .Values.startupProbe }} - startupProbe: - {{- toYaml . | nindent 12 }} - {{- end }} - envFrom: - - configMapRef: - name: {{ include "agent-monitor.fullname" . }}-config - {{- with .Values.extraEnvFrom }} - {{- toYaml . | nindent 12 }} - {{- end }} - {{- with .Values.resources }} - resources: - {{- toYaml . | nindent 12 }} - {{- end }} - lifecycle: - preStop: - exec: - # Allow in-flight requests (including WebSockets) to drain before SIGTERM - command: ["sh", "-c", "sleep 5"] - volumeMounts: - - name: data - mountPath: /app/data - - name: tmp - mountPath: /tmp - {{- if .Values.mcp.enabled }} - # ── MCP sidecar container ───────────────────────────────────────── - - name: mcp - {{- with .Values.mcp.securityContext }} - securityContext: - {{- toYaml . | nindent 12 }} - {{- end }} - image: {{ include "agent-monitor.mcpImage" . }} - imagePullPolicy: {{ .Values.mcp.image.pullPolicy }} - ports: - - name: mcp - containerPort: {{ .Values.mcp.port }} - protocol: TCP - env: - {{- range $key, $value := .Values.mcp.env }} - - name: {{ $key }} - value: {{ $value | quote }} - {{- end }} - startupProbe: - tcpSocket: - port: mcp - failureThreshold: 30 - periodSeconds: 2 - livenessProbe: - tcpSocket: - port: mcp - initialDelaySeconds: 15 - periodSeconds: 20 - timeoutSeconds: 3 - failureThreshold: 3 - readinessProbe: - tcpSocket: - port: mcp - initialDelaySeconds: 5 - periodSeconds: 10 - timeoutSeconds: 3 - failureThreshold: 3 - {{- with .Values.mcp.resources }} - resources: - {{- toYaml . | nindent 12 }} - {{- end }} - volumeMounts: - - name: tmp - mountPath: /tmp - {{- end }} - volumes: - - name: data - {{- if .Values.persistence.enabled }} - persistentVolumeClaim: - claimName: {{ .Values.persistence.existingClaim | default (printf "%s-data" (include "agent-monitor.fullname" .)) }} - {{- else }} - emptyDir: {} - {{- end }} - - name: tmp - emptyDir: - sizeLimit: 100Mi - terminationGracePeriodSeconds: 30 - {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.topologySpreadConstraints }} - topologySpreadConstraints: - {{- toYaml . | nindent 8 }} - {{- end }} diff --git a/deployments/helm/agent-monitor/templates/hpa.yaml b/deployments/helm/agent-monitor/templates/hpa.yaml deleted file mode 100644 index 76aa112..0000000 --- a/deployments/helm/agent-monitor/templates/hpa.yaml +++ /dev/null @@ -1,53 +0,0 @@ -{{- if .Values.autoscaling.enabled -}} -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: {{ include "agent-monitor.fullname" . }} - labels: - {{- include "agent-monitor.labels" . | nindent 4 }} -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: {{ include "agent-monitor.fullname" . }} - minReplicas: {{ .Values.autoscaling.minReplicas }} - maxReplicas: {{ .Values.autoscaling.maxReplicas }} - metrics: - {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} - {{- end }} - {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} - - type: Resource - resource: - name: memory - target: - type: Utilization - averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} - {{- end }} - behavior: - scaleDown: - stabilizationWindowSeconds: {{ .Values.autoscaling.scaleDownStabilizationWindowSeconds | default 300 }} - policies: - - type: Pods - value: 1 - periodSeconds: 60 - - type: Percent - value: 10 - periodSeconds: 60 - selectPolicy: Min - scaleUp: - stabilizationWindowSeconds: 30 - policies: - - type: Pods - value: 2 - periodSeconds: 60 - - type: Percent - value: 50 - periodSeconds: 60 - selectPolicy: Max -{{- end }} diff --git a/deployments/helm/agent-monitor/templates/ingress.yaml b/deployments/helm/agent-monitor/templates/ingress.yaml deleted file mode 100644 index 83340a8..0000000 --- a/deployments/helm/agent-monitor/templates/ingress.yaml +++ /dev/null @@ -1,41 +0,0 @@ -{{- if .Values.ingress.enabled -}} -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: {{ include "agent-monitor.fullname" . }} - labels: - {{- include "agent-monitor.labels" . | nindent 4 }} - {{- with .Values.ingress.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -spec: - {{- if .Values.ingress.className }} - ingressClassName: {{ .Values.ingress.className }} - {{- end }} - {{- if .Values.ingress.tls }} - tls: - {{- range .Values.ingress.tls }} - - hosts: - {{- range .hosts }} - - {{ . | quote }} - {{- end }} - secretName: {{ .secretName }} - {{- end }} - {{- end }} - rules: - {{- range .Values.ingress.hosts }} - - host: {{ .host | quote }} - http: - paths: - {{- range .paths }} - - path: {{ .path }} - pathType: {{ .pathType }} - backend: - service: - name: {{ include "agent-monitor.fullname" $ }} - port: - name: http - {{- end }} - {{- end }} -{{- end }} diff --git a/deployments/helm/agent-monitor/templates/networkpolicy.yaml b/deployments/helm/agent-monitor/templates/networkpolicy.yaml deleted file mode 100644 index 945fb58..0000000 --- a/deployments/helm/agent-monitor/templates/networkpolicy.yaml +++ /dev/null @@ -1,46 +0,0 @@ -{{- if .Values.networkPolicy.enabled -}} -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: {{ include "agent-monitor.fullname" . }} - labels: - {{- include "agent-monitor.labels" . | nindent 4 }} -spec: - podSelector: - matchLabels: - {{- include "agent-monitor.selectorLabels" . | nindent 6 }} - policyTypes: - - Ingress - - Egress - ingress: - # Allow HTTP traffic to the application port from any pod (ingress controllers, etc.) - - ports: - - port: {{ .Values.service.targetPort }} - protocol: TCP - {{- if .Values.mcp.enabled }} - # Allow MCP traffic when sidecar is enabled - - ports: - - port: {{ .Values.mcp.port }} - protocol: TCP - {{- end }} - {{- with .Values.networkPolicy.additionalIngressRules }} - {{- toYaml . | nindent 4 }} - {{- end }} - egress: - # Allow DNS resolution - - ports: - - port: 53 - protocol: UDP - - port: 53 - protocol: TCP - # Allow outbound HTTPS (for external API calls) - - ports: - - port: 443 - protocol: TCP - # Allow internal communication within the cluster - - to: - - podSelector: {} - {{- with .Values.networkPolicy.additionalEgressRules }} - {{- toYaml . | nindent 4 }} - {{- end }} -{{- end }} diff --git a/deployments/helm/agent-monitor/templates/pdb.yaml b/deployments/helm/agent-monitor/templates/pdb.yaml deleted file mode 100644 index 592ba76..0000000 --- a/deployments/helm/agent-monitor/templates/pdb.yaml +++ /dev/null @@ -1,18 +0,0 @@ -{{- if .Values.podDisruptionBudget.enabled -}} -apiVersion: policy/v1 -kind: PodDisruptionBudget -metadata: - name: {{ include "agent-monitor.fullname" . }} - labels: - {{- include "agent-monitor.labels" . | nindent 4 }} -spec: - {{- if .Values.podDisruptionBudget.minAvailable }} - minAvailable: {{ .Values.podDisruptionBudget.minAvailable }} - {{- end }} - {{- if .Values.podDisruptionBudget.maxUnavailable }} - maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }} - {{- end }} - selector: - matchLabels: - {{- include "agent-monitor.selectorLabels" . | nindent 6 }} -{{- end }} diff --git a/deployments/helm/agent-monitor/templates/pvc.yaml b/deployments/helm/agent-monitor/templates/pvc.yaml deleted file mode 100644 index 78aa7e1..0000000 --- a/deployments/helm/agent-monitor/templates/pvc.yaml +++ /dev/null @@ -1,23 +0,0 @@ -{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) -}} -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: {{ include "agent-monitor.fullname" . }}-data - labels: - {{- include "agent-monitor.labels" . | nindent 4 }} - {{- with .Values.persistence.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -spec: - accessModes: - {{- range .Values.persistence.accessModes }} - - {{ . }} - {{- end }} - {{- if .Values.persistence.storageClass }} - storageClassName: {{ .Values.persistence.storageClass | quote }} - {{- end }} - resources: - requests: - storage: {{ .Values.persistence.size }} -{{- end }} diff --git a/deployments/helm/agent-monitor/templates/service.yaml b/deployments/helm/agent-monitor/templates/service.yaml deleted file mode 100644 index 9c031d0..0000000 --- a/deployments/helm/agent-monitor/templates/service.yaml +++ /dev/null @@ -1,33 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ include "agent-monitor.fullname" . }} - labels: - {{- include "agent-monitor.labels" . | nindent 4 }} - {{- with .Values.service.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -spec: - type: {{ .Values.service.type }} - # Sticky sessions for WebSocket connections - sessionAffinity: ClientIP - sessionAffinityConfig: - clientIP: - timeoutSeconds: 10800 - ports: - - name: http - port: {{ .Values.service.port }} - targetPort: http - protocol: TCP - {{- if and (eq .Values.service.type "NodePort") .Values.service.nodePort }} - nodePort: {{ .Values.service.nodePort }} - {{- end }} - {{- if .Values.mcp.enabled }} - - name: mcp - port: {{ .Values.mcp.port }} - targetPort: mcp - protocol: TCP - {{- end }} - selector: - {{- include "agent-monitor.selectorLabels" . | nindent 4 }} diff --git a/deployments/helm/agent-monitor/templates/serviceaccount.yaml b/deployments/helm/agent-monitor/templates/serviceaccount.yaml deleted file mode 100644 index 47b4541..0000000 --- a/deployments/helm/agent-monitor/templates/serviceaccount.yaml +++ /dev/null @@ -1,13 +0,0 @@ -{{- if .Values.serviceAccount.create -}} -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ include "agent-monitor.serviceAccountName" . }} - labels: - {{- include "agent-monitor.labels" . | nindent 4 }} - {{- with .Values.serviceAccount.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken | default false }} -{{- end }} diff --git a/deployments/helm/agent-monitor/templates/servicemonitor.yaml b/deployments/helm/agent-monitor/templates/servicemonitor.yaml deleted file mode 100644 index 6714935..0000000 --- a/deployments/helm/agent-monitor/templates/servicemonitor.yaml +++ /dev/null @@ -1,29 +0,0 @@ -{{- if and .Values.monitoring.enabled .Values.monitoring.serviceMonitor.enabled -}} -apiVersion: monitoring.coreos.com/v1 -kind: ServiceMonitor -metadata: - name: {{ include "agent-monitor.fullname" . }} - {{- if .Values.monitoring.serviceMonitor.namespace }} - namespace: {{ .Values.monitoring.serviceMonitor.namespace }} - {{- end }} - labels: - {{- include "agent-monitor.labels" . | nindent 4 }} - {{- with .Values.monitoring.serviceMonitor.labels }} - {{- toYaml . | nindent 4 }} - {{- end }} -spec: - selector: - matchLabels: - {{- include "agent-monitor.selectorLabels" . | nindent 6 }} - {{- if .Values.monitoring.serviceMonitor.namespace }} - namespaceSelector: - matchNames: - - {{ .Release.Namespace }} - {{- end }} - endpoints: - - port: http - path: {{ .Values.monitoring.serviceMonitor.path | default "/api/metrics" }} - interval: {{ .Values.monitoring.serviceMonitor.interval | default "30s" }} - scrapeTimeout: {{ .Values.monitoring.serviceMonitor.scrapeTimeout | default "10s" }} - honorLabels: {{ .Values.monitoring.serviceMonitor.honorLabels | default false }} -{{- end }} diff --git a/deployments/helm/agent-monitor/templates/tests/test-connection.yaml b/deployments/helm/agent-monitor/templates/tests/test-connection.yaml deleted file mode 100644 index 47eda89..0000000 --- a/deployments/helm/agent-monitor/templates/tests/test-connection.yaml +++ /dev/null @@ -1,34 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: "{{ include "agent-monitor.fullname" . }}-test-connection" - labels: - {{- include "agent-monitor.labels" . | nindent 4 }} - annotations: - "helm.sh/hook": test - "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded -spec: - restartPolicy: Never - securityContext: - runAsNonRoot: true - runAsUser: 65534 - runAsGroup: 65534 - seccompProfile: - type: RuntimeDefault - containers: - - name: wget - image: busybox:1.36 - command: ['wget'] - args: - - '--timeout=10' - - '--tries=3' - - '-qO-' - - 'http://{{ include "agent-monitor.fullname" . }}:{{ .Values.service.port }}/api/health' - securityContext: - runAsNonRoot: true - runAsUser: 65534 - readOnlyRootFilesystem: true - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL diff --git a/deployments/helm/agent-monitor/values-dev.yaml b/deployments/helm/agent-monitor/values-dev.yaml deleted file mode 100644 index 8529530..0000000 --- a/deployments/helm/agent-monitor/values-dev.yaml +++ /dev/null @@ -1,50 +0,0 @@ -# ============================================================================= -# Development Environment Overrides -# ============================================================================= -# Usage: helm install agent-monitor ./agent-monitor -f values-dev.yaml - -replicaCount: 1 - -image: - pullPolicy: Always - -env: - NODE_ENV: development - DASHBOARD_PORT: "4820" - LOG_LEVEL: debug - -resources: - requests: - cpu: 50m - memory: 64Mi - limits: - cpu: 250m - memory: 256Mi - -autoscaling: - enabled: false - -persistence: - enabled: true - size: 1Gi - -podDisruptionBudget: - enabled: false - -networkPolicy: - enabled: false - -monitoring: - enabled: false - -ingress: - enabled: false - -# Relax security for dev debugging -securityContext: - readOnlyRootFilesystem: false - runAsNonRoot: true - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL diff --git a/deployments/helm/agent-monitor/values-production.yaml b/deployments/helm/agent-monitor/values-production.yaml deleted file mode 100644 index fde5ff0..0000000 --- a/deployments/helm/agent-monitor/values-production.yaml +++ /dev/null @@ -1,117 +0,0 @@ -# ============================================================================= -# Production Environment Overrides -# ============================================================================= -# Usage: helm install agent-monitor ./agent-monitor -f values-production.yaml - -replicaCount: 3 - -env: - NODE_ENV: production - DASHBOARD_PORT: "4820" - LOG_LEVEL: warn - -resources: - requests: - cpu: 250m - memory: 256Mi - limits: - cpu: "1" - memory: "1Gi" - -autoscaling: - enabled: true - minReplicas: 3 - maxReplicas: 20 - targetCPUUtilizationPercentage: 70 - targetMemoryUtilizationPercentage: 80 - scaleDownStabilizationWindowSeconds: 600 - -deployment: - strategy: - type: RollingUpdate - rollingUpdate: - maxSurge: 1 - maxUnavailable: 0 - -persistence: - enabled: true - size: 20Gi - storageClass: gp3 - -podDisruptionBudget: - enabled: true - minAvailable: 2 - -networkPolicy: - enabled: true - -# -- Spread pods across nodes (required) and zones (preferred) for high availability -affinity: - podAntiAffinity: - requiredDuringSchedulingIgnoredDuringExecution: - - labelSelector: - matchExpressions: - - key: app.kubernetes.io/name - operator: In - values: - - agent-monitor - topologyKey: kubernetes.io/hostname - preferredDuringSchedulingIgnoredDuringExecution: - - weight: 50 - podAffinityTerm: - labelSelector: - matchExpressions: - - key: app.kubernetes.io/name - operator: In - values: - - agent-monitor - topologyKey: topology.kubernetes.io/zone - -topologySpreadConstraints: - - maxSkew: 1 - topologyKey: topology.kubernetes.io/zone - whenUnsatisfiable: ScheduleAnyway - labelSelector: - matchLabels: - app.kubernetes.io/name: agent-monitor - -ingress: - enabled: true - className: nginx - annotations: - nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" - nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" - nginx.ingress.kubernetes.io/proxy-http-version: "1.1" - nginx.ingress.kubernetes.io/upstream-hash-by: "$remote_addr" - nginx.ingress.kubernetes.io/configuration-snippet: | - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - nginx.ingress.kubernetes.io/ssl-redirect: "true" - nginx.ingress.kubernetes.io/force-ssl-redirect: "true" - cert-manager.io/cluster-issuer: letsencrypt-prod - hosts: - - host: agent-monitor.example.com - paths: - - path: / - pathType: Prefix - tls: - - secretName: agent-monitor-production-tls - hosts: - - agent-monitor.example.com - -monitoring: - enabled: true - serviceMonitor: - enabled: true - interval: 15s - scrapeTimeout: 10s - -mcp: - enabled: true - resources: - requests: - cpu: 100m - memory: 128Mi - limits: - cpu: 500m - memory: 512Mi diff --git a/deployments/helm/agent-monitor/values-staging.yaml b/deployments/helm/agent-monitor/values-staging.yaml deleted file mode 100644 index 90e411e..0000000 --- a/deployments/helm/agent-monitor/values-staging.yaml +++ /dev/null @@ -1,65 +0,0 @@ -# ============================================================================= -# Staging Environment Overrides -# ============================================================================= -# Usage: helm install agent-monitor ./agent-monitor -f values-staging.yaml - -replicaCount: 2 - -env: - NODE_ENV: production - DASHBOARD_PORT: "4820" - LOG_LEVEL: info - -resources: - requests: - cpu: 100m - memory: 128Mi - limits: - cpu: 500m - memory: 512Mi - -autoscaling: - enabled: true - minReplicas: 2 - maxReplicas: 5 - targetCPUUtilizationPercentage: 75 - targetMemoryUtilizationPercentage: 80 - scaleDownStabilizationWindowSeconds: 180 - -persistence: - enabled: true - size: 5Gi - -podDisruptionBudget: - enabled: true - minAvailable: 1 - -networkPolicy: - enabled: true - -ingress: - enabled: true - className: nginx - annotations: - nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" - nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" - nginx.ingress.kubernetes.io/proxy-http-version: "1.1" - nginx.ingress.kubernetes.io/upstream-hash-by: "$remote_addr" - nginx.ingress.kubernetes.io/configuration-snippet: | - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - hosts: - - host: agent-monitor.staging.internal - paths: - - path: / - pathType: Prefix - tls: - - secretName: agent-monitor-staging-tls - hosts: - - agent-monitor.staging.internal - -monitoring: - enabled: true - serviceMonitor: - enabled: true - interval: 30s diff --git a/deployments/helm/agent-monitor/values.yaml b/deployments/helm/agent-monitor/values.yaml deleted file mode 100644 index 5dd1748..0000000 --- a/deployments/helm/agent-monitor/values.yaml +++ /dev/null @@ -1,312 +0,0 @@ -# ============================================================================= -# Claude Code Agent Monitor - Default Helm Values -# ============================================================================= -# Override these values per environment using values-dev.yaml, values-staging.yaml, -# or values-production.yaml. - -# -- Number of pod replicas -replicaCount: 2 - -# -- Container image configuration -image: - registry: ghcr.io - repository: smartgift/claude-code-monitor - tag: "" # Defaults to .Chart.AppVersion if empty - pullPolicy: IfNotPresent - -# -- Image pull secrets for private registries -imagePullSecrets: [] - -# -- Override the release name -nameOverride: "" -# -- Override the full release name -fullnameOverride: "" - -# ============================================================================= -# Service Account -# ============================================================================= -serviceAccount: - # -- Whether to create a ServiceAccount - create: true - # -- Annotations to add to the ServiceAccount - annotations: {} - # -- The name of the ServiceAccount (auto-generated if empty) - name: "" - # -- Automount API credentials - automountServiceAccountToken: false - -# ============================================================================= -# Pod Configuration -# ============================================================================= - -# -- Annotations to add to pods -podAnnotations: {} - -# -- Labels to add to pods -podLabels: {} - -# -- Pod-level security context -podSecurityContext: - runAsNonRoot: true - runAsUser: 1000 - runAsGroup: 1000 - fsGroup: 1000 - fsGroupChangePolicy: OnRootMismatch - seccompProfile: - type: RuntimeDefault - -# -- Container-level security context -securityContext: - runAsNonRoot: true - readOnlyRootFilesystem: true - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - -# ============================================================================= -# Deployment Strategy -# ============================================================================= -deployment: - strategy: - type: RollingUpdate - rollingUpdate: - maxSurge: 1 - maxUnavailable: 0 - -# ============================================================================= -# Service -# ============================================================================= -service: - # -- Service type (ClusterIP, NodePort, LoadBalancer) - type: ClusterIP - # -- Service port (external) - port: 80 - # -- Container port the application listens on - targetPort: 4820 - # -- Node port (only used when type is NodePort) - nodePort: "" - # -- Additional service annotations - annotations: {} - -# ============================================================================= -# Ingress -# ============================================================================= -ingress: - # -- Enable ingress resource - enabled: false - # -- Ingress class name (e.g. nginx, traefik, alb) - className: "" - # -- Ingress annotations - annotations: {} - # nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" - # nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" - # nginx.ingress.kubernetes.io/proxy-http-version: "1.1" - # nginx.ingress.kubernetes.io/proxy-set-headers: "Upgrade=$http_upgrade,Connection=upgrade" - # -- Ingress host definitions - hosts: - - host: agent-monitor.local - paths: - - path: / - pathType: Prefix - # -- TLS configuration - tls: [] - # - secretName: agent-monitor-tls - # hosts: - # - agent-monitor.local - -# ============================================================================= -# Resource Limits -# ============================================================================= -resources: - requests: - cpu: 100m - memory: 128Mi - limits: - cpu: 500m - memory: 512Mi - -# ============================================================================= -# Autoscaling (HPA) -# ============================================================================= -autoscaling: - # -- Enable Horizontal Pod Autoscaler - enabled: true - # -- Minimum number of replicas - minReplicas: 2 - # -- Maximum number of replicas - maxReplicas: 10 - # -- Target CPU utilization percentage - targetCPUUtilizationPercentage: 70 - # -- Target memory utilization percentage - targetMemoryUtilizationPercentage: 80 - # -- Scale-down stabilization window (seconds) - scaleDownStabilizationWindowSeconds: 300 - -# ============================================================================= -# Persistence (SQLite database) -# ============================================================================= -persistence: - # -- Enable persistent storage for SQLite data - enabled: true - # -- Storage class name (empty string uses default) - storageClass: "" - # -- Access modes for the PVC - accessModes: - - ReadWriteOnce - # -- Size of the persistent volume - size: 10Gi - # -- Annotations for the PVC - annotations: {} - # -- Use an existing PVC instead of creating one - existingClaim: "" - -# ============================================================================= -# Scheduling -# ============================================================================= - -# -- Node selector for pod placement -nodeSelector: {} - -# -- Tolerations for pod placement -tolerations: [] - -# -- Affinity rules for pod placement -affinity: {} - -# -- Topology spread constraints -topologySpreadConstraints: [] - -# ============================================================================= -# Environment Variables -# ============================================================================= -env: - # -- Node.js environment - NODE_ENV: production - # -- Port the dashboard server listens on (must match service.targetPort) - DASHBOARD_PORT: "4820" - # -- Log level (debug, info, warn, error) - LOG_LEVEL: info - -# -- Additional environment variables as key-value pairs -extraEnv: {} - # MY_CUSTOM_VAR: my-value - -# -- Extra environment variables from secrets or configmaps -extraEnvFrom: [] - # - secretRef: - # name: my-secret - # - configMapRef: - # name: my-configmap - -# ============================================================================= -# Health Probes -# ============================================================================= -livenessProbe: - httpGet: - path: /api/health - port: http - initialDelaySeconds: 30 - periodSeconds: 15 - timeoutSeconds: 5 - failureThreshold: 3 - successThreshold: 1 - -readinessProbe: - httpGet: - path: /api/health - port: http - initialDelaySeconds: 5 - periodSeconds: 10 - timeoutSeconds: 3 - failureThreshold: 3 - successThreshold: 1 - -startupProbe: - httpGet: - path: /api/health - port: http - initialDelaySeconds: 5 - periodSeconds: 5 - timeoutSeconds: 3 - failureThreshold: 30 - successThreshold: 1 - -# ============================================================================= -# Pod Disruption Budget -# ============================================================================= -podDisruptionBudget: - # -- Enable PodDisruptionBudget - enabled: true - # -- Minimum available pods during voluntary disruptions - minAvailable: 1 - # -- Maximum unavailable pods (alternative to minAvailable) - # maxUnavailable: 1 - -# ============================================================================= -# Network Policy -# ============================================================================= -networkPolicy: - # -- Enable NetworkPolicy - enabled: true - # -- Additional ingress rules - additionalIngressRules: [] - # -- Additional egress rules - additionalEgressRules: [] - -# ============================================================================= -# MCP Sidecar (Model Context Protocol server) -# ============================================================================= -mcp: - # -- Enable MCP sidecar container - enabled: false - image: - registry: ghcr.io - repository: smartgift/claude-code-monitor-mcp - tag: "" # Defaults to .Chart.AppVersion if empty - pullPolicy: IfNotPresent - # -- MCP HTTP transport port - port: 8819 - # -- Resource limits for MCP sidecar - resources: - requests: - cpu: 50m - memory: 64Mi - limits: - cpu: 200m - memory: 256Mi - # -- MCP environment variables - env: - MCP_TRANSPORT: http - MCP_PORT: "8819" - DASHBOARD_URL: "http://localhost:4820" - # -- Container security context for MCP - securityContext: - runAsNonRoot: true - readOnlyRootFilesystem: true - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - -# ============================================================================= -# Monitoring -# ============================================================================= -monitoring: - # -- Enable Prometheus monitoring - enabled: false - serviceMonitor: - # -- Enable ServiceMonitor resource (requires Prometheus Operator) - enabled: false - # -- Namespace for the ServiceMonitor (defaults to release namespace) - namespace: "" - # -- Additional labels for the ServiceMonitor - labels: {} - # -- Scrape interval - interval: 30s - # -- Scrape timeout - scrapeTimeout: 10s - # -- Metric path - path: /api/metrics - # -- Honor labels from the target - honorLabels: false diff --git a/deployments/kubernetes/README.md b/deployments/kubernetes/README.md deleted file mode 100644 index bfed056..0000000 --- a/deployments/kubernetes/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# Kubernetes Manifests - -Production-ready Kubernetes resources using Kustomize for environment management, with optional blue-green and canary deployment strategies. - -## Structure - -``` -kubernetes/ -├── base/ # Shared base (all environments inherit from this) -│ ├── kustomization.yaml -│ ├── namespace.yaml # agent-monitor namespace with Pod Security Standards -│ ├── configmap.yaml # Environment configuration -│ ├── serviceaccount.yaml # Minimal-privilege service account -│ ├── deployment.yaml # Main deployment (2 replicas, 3 health probes) -│ ├── service.yaml # ClusterIP with WebSocket sticky sessions -│ ├── ingress.yaml # NGINX ingress with TLS + WebSocket headers -│ ├── pvc.yaml # 10Gi persistent volume for SQLite -│ ├── hpa.yaml # Horizontal Pod Autoscaler (2–10 pods) -│ ├── pdb.yaml # Pod Disruption Budget (minAvailable: 1) -│ └── networkpolicy.yaml # Ingress restricted to NGINX controller -├── overlays/ -│ ├── dev/ # 1 replica, no HPA, minimal resources -│ ├── staging/ # 2 replicas, standard resources -│ └── production/ # 3 replicas, HPA 3–20, strict anti-affinity -├── strategies/ -│ ├── blue-green/ # Dual-slot deployment with service switching -│ └── canary/ # Progressive rollout with Argo Rollouts analysis -└── components/ - ├── mcp-sidecar/ # Adds MCP server container to pods - └── monitoring/ # Adds Prometheus ServiceMonitor -``` - -## Usage - -```bash -# Apply an environment -kubectl apply -k overlays/dev/ -kubectl apply -k overlays/staging/ -kubectl apply -k overlays/production/ - -# Add MCP sidecar (edit overlay kustomization.yaml): -# components: -# - ../../components/mcp-sidecar - -# Blue-green switch -kubectl patch svc agent-monitor -n agent-monitor \ - -p '{"spec":{"selector":{"slot":"green"}}}' -``` - -## Security - -All manifests enforce: -- `runAsNonRoot: true` -- `readOnlyRootFilesystem: true` -- `drop: [ALL]` capabilities -- `seccompProfile: RuntimeDefault` -- No service account token auto-mount -- NetworkPolicy restricting ingress sources diff --git a/deployments/kubernetes/base/configmap.yaml b/deployments/kubernetes/base/configmap.yaml deleted file mode 100644 index ceb54ce..0000000 --- a/deployments/kubernetes/base/configmap.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: agent-monitor-config - namespace: agent-monitor - labels: - app.kubernetes.io/name: agent-monitor - app.kubernetes.io/instance: agent-monitor - app.kubernetes.io/version: "1.0.0" - app.kubernetes.io/component: config - app.kubernetes.io/managed-by: kustomize -data: - NODE_ENV: "production" - DASHBOARD_PORT: "4820" - LOG_LEVEL: "info" diff --git a/deployments/kubernetes/base/deployment.yaml b/deployments/kubernetes/base/deployment.yaml deleted file mode 100644 index ff0ef9d..0000000 --- a/deployments/kubernetes/base/deployment.yaml +++ /dev/null @@ -1,147 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: agent-monitor - namespace: agent-monitor - labels: - app.kubernetes.io/name: agent-monitor - app.kubernetes.io/instance: agent-monitor - app.kubernetes.io/version: "1.0.0" - app.kubernetes.io/component: server - app.kubernetes.io/managed-by: kustomize -spec: - replicas: 2 - revisionHistoryLimit: 5 - strategy: - type: RollingUpdate - rollingUpdate: - maxSurge: 1 - maxUnavailable: 0 - selector: - matchLabels: - app.kubernetes.io/name: agent-monitor - app.kubernetes.io/instance: agent-monitor - template: - metadata: - labels: - app.kubernetes.io/name: agent-monitor - app.kubernetes.io/instance: agent-monitor - app.kubernetes.io/version: "1.0.0" - app.kubernetes.io/component: server - app.kubernetes.io/managed-by: kustomize - annotations: - # Force rollout on configmap changes via kustomize hash - checksum/config: "placeholder" - spec: - serviceAccountName: agent-monitor - automountServiceAccountToken: false - terminationGracePeriodSeconds: 30 - - securityContext: - runAsNonRoot: true - runAsUser: 1000 - runAsGroup: 1000 - fsGroup: 1000 - fsGroupChangePolicy: OnRootMismatch - seccompProfile: - type: RuntimeDefault - - affinity: - podAntiAffinity: - preferredDuringSchedulingIgnoredDuringExecution: - - weight: 100 - podAffinityTerm: - labelSelector: - matchExpressions: - - key: app.kubernetes.io/name - operator: In - values: - - agent-monitor - topologyKey: kubernetes.io/hostname - - topologySpreadConstraints: - - maxSkew: 1 - topologyKey: kubernetes.io/hostname - whenUnsatisfiable: ScheduleAnyway - labelSelector: - matchLabels: - app.kubernetes.io/name: agent-monitor - - containers: - - name: agent-monitor - image: ${IMAGE_REGISTRY}/agent-monitor:${IMAGE_TAG} - imagePullPolicy: IfNotPresent - - ports: - - name: http - containerPort: 4820 - protocol: TCP - - envFrom: - - configMapRef: - name: agent-monitor-config - - resources: - requests: - memory: "128Mi" - cpu: "100m" - limits: - memory: "512Mi" - cpu: "500m" - - startupProbe: - httpGet: - path: /api/health - port: http - failureThreshold: 30 - periodSeconds: 2 - - readinessProbe: - httpGet: - path: /api/health - port: http - initialDelaySeconds: 5 - periodSeconds: 5 - timeoutSeconds: 3 - successThreshold: 1 - failureThreshold: 3 - - livenessProbe: - httpGet: - path: /api/health - port: http - initialDelaySeconds: 15 - periodSeconds: 15 - timeoutSeconds: 5 - successThreshold: 1 - failureThreshold: 3 - - securityContext: - runAsNonRoot: true - runAsUser: 1000 - runAsGroup: 1000 - readOnlyRootFilesystem: true - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - - volumeMounts: - - name: data - mountPath: /app/data - - name: tmp - mountPath: /tmp - - lifecycle: - preStop: - exec: - # Allow in-flight requests to drain before SIGTERM - command: ["sh", "-c", "sleep 5"] - - volumes: - - name: data - persistentVolumeClaim: - claimName: agent-monitor-data - - name: tmp - emptyDir: - sizeLimit: 100Mi diff --git a/deployments/kubernetes/base/hpa.yaml b/deployments/kubernetes/base/hpa.yaml deleted file mode 100644 index d26c6ce..0000000 --- a/deployments/kubernetes/base/hpa.yaml +++ /dev/null @@ -1,49 +0,0 @@ -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: agent-monitor - namespace: agent-monitor - labels: - app.kubernetes.io/name: agent-monitor - app.kubernetes.io/instance: agent-monitor - app.kubernetes.io/version: "1.0.0" - app.kubernetes.io/component: autoscaling - app.kubernetes.io/managed-by: kustomize -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: agent-monitor - minReplicas: 2 - maxReplicas: 10 - metrics: - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: 70 - - type: Resource - resource: - name: memory - target: - type: Utilization - averageUtilization: 80 - behavior: - scaleDown: - stabilizationWindowSeconds: 300 - policies: - - type: Pods - value: 1 - periodSeconds: 60 - selectPolicy: Min - scaleUp: - stabilizationWindowSeconds: 30 - policies: - - type: Pods - value: 2 - periodSeconds: 60 - - type: Percent - value: 50 - periodSeconds: 60 - selectPolicy: Max diff --git a/deployments/kubernetes/base/ingress.yaml b/deployments/kubernetes/base/ingress.yaml deleted file mode 100644 index 8a6de70..0000000 --- a/deployments/kubernetes/base/ingress.yaml +++ /dev/null @@ -1,51 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: agent-monitor - namespace: agent-monitor - labels: - app.kubernetes.io/name: agent-monitor - app.kubernetes.io/instance: agent-monitor - app.kubernetes.io/version: "1.0.0" - app.kubernetes.io/component: ingress - app.kubernetes.io/managed-by: kustomize - annotations: - # NGINX Ingress Controller annotations - nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" - nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" - # WebSocket upgrade support - nginx.ingress.kubernetes.io/proxy-http-version: "1.1" - nginx.ingress.kubernetes.io/upstream-hash-by: "$remote_addr" - nginx.ingress.kubernetes.io/configuration-snippet: | - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - # Sticky sessions for WebSocket - nginx.ingress.kubernetes.io/affinity: cookie - nginx.ingress.kubernetes.io/affinity-mode: persistent - nginx.ingress.kubernetes.io/session-cookie-name: agent-monitor-affinity - nginx.ingress.kubernetes.io/session-cookie-max-age: "10800" - nginx.ingress.kubernetes.io/session-cookie-samesite: Strict - nginx.ingress.kubernetes.io/session-cookie-secure: "true" - # Security headers - nginx.ingress.kubernetes.io/ssl-redirect: "true" - nginx.ingress.kubernetes.io/force-ssl-redirect: "true" - nginx.ingress.kubernetes.io/hsts: "true" - nginx.ingress.kubernetes.io/hsts-max-age: "31536000" - nginx.ingress.kubernetes.io/hsts-include-subdomains: "true" -spec: - ingressClassName: nginx - tls: - - hosts: - - agent-monitor.example.com - secretName: agent-monitor-tls - rules: - - host: agent-monitor.example.com - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: agent-monitor - port: - name: http diff --git a/deployments/kubernetes/base/kustomization.yaml b/deployments/kubernetes/base/kustomization.yaml deleted file mode 100644 index f26ea4b..0000000 --- a/deployments/kubernetes/base/kustomization.yaml +++ /dev/null @@ -1,23 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -metadata: - name: agent-monitor-base - -commonLabels: - app.kubernetes.io/name: agent-monitor - app.kubernetes.io/instance: agent-monitor - app.kubernetes.io/version: "1.0.0" - app.kubernetes.io/managed-by: kustomize - -resources: - - namespace.yaml - - configmap.yaml - - serviceaccount.yaml - - pvc.yaml - - deployment.yaml - - service.yaml - - ingress.yaml - - hpa.yaml - - pdb.yaml - - networkpolicy.yaml diff --git a/deployments/kubernetes/base/namespace.yaml b/deployments/kubernetes/base/namespace.yaml deleted file mode 100644 index 1a5f12b..0000000 --- a/deployments/kubernetes/base/namespace.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: agent-monitor - labels: - app.kubernetes.io/name: agent-monitor - app.kubernetes.io/instance: agent-monitor - app.kubernetes.io/version: "1.0.0" - app.kubernetes.io/component: namespace - app.kubernetes.io/managed-by: kustomize - # Enable Pod Security Standards (restricted) - pod-security.kubernetes.io/enforce: restricted - pod-security.kubernetes.io/enforce-version: latest - pod-security.kubernetes.io/audit: restricted - pod-security.kubernetes.io/audit-version: latest - pod-security.kubernetes.io/warn: restricted - pod-security.kubernetes.io/warn-version: latest diff --git a/deployments/kubernetes/base/networkpolicy.yaml b/deployments/kubernetes/base/networkpolicy.yaml deleted file mode 100644 index 53986db..0000000 --- a/deployments/kubernetes/base/networkpolicy.yaml +++ /dev/null @@ -1,53 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: agent-monitor - namespace: agent-monitor - labels: - app.kubernetes.io/name: agent-monitor - app.kubernetes.io/instance: agent-monitor - app.kubernetes.io/version: "1.0.0" - app.kubernetes.io/component: network - app.kubernetes.io/managed-by: kustomize -spec: - podSelector: - matchLabels: - app.kubernetes.io/name: agent-monitor - app.kubernetes.io/instance: agent-monitor - policyTypes: - - Ingress - - Egress - ingress: - # Allow traffic from ingress controller - - from: - - namespaceSelector: - matchLabels: - kubernetes.io/metadata.name: ingress-nginx - - podSelector: - matchLabels: - app.kubernetes.io/name: ingress-nginx - ports: - - protocol: TCP - port: 4820 - # Allow intra-namespace traffic (pod-to-pod) - - from: - - podSelector: - matchLabels: - app.kubernetes.io/name: agent-monitor - ports: - - protocol: TCP - port: 4820 - egress: - # Allow DNS resolution (required for service discovery) - - ports: - - port: 53 - protocol: UDP - - port: 53 - protocol: TCP - # Allow outbound HTTPS (for external API calls if needed) - - ports: - - port: 443 - protocol: TCP - # Allow internal communication within the namespace (pod-to-pod) - - to: - - podSelector: {} diff --git a/deployments/kubernetes/base/pdb.yaml b/deployments/kubernetes/base/pdb.yaml deleted file mode 100644 index 560591f..0000000 --- a/deployments/kubernetes/base/pdb.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: policy/v1 -kind: PodDisruptionBudget -metadata: - name: agent-monitor - namespace: agent-monitor - labels: - app.kubernetes.io/name: agent-monitor - app.kubernetes.io/instance: agent-monitor - app.kubernetes.io/version: "1.0.0" - app.kubernetes.io/component: availability - app.kubernetes.io/managed-by: kustomize -spec: - minAvailable: 1 - selector: - matchLabels: - app.kubernetes.io/name: agent-monitor - app.kubernetes.io/instance: agent-monitor diff --git a/deployments/kubernetes/base/pvc.yaml b/deployments/kubernetes/base/pvc.yaml deleted file mode 100644 index 19cbf83..0000000 --- a/deployments/kubernetes/base/pvc.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: agent-monitor-data - namespace: agent-monitor - labels: - app.kubernetes.io/name: agent-monitor - app.kubernetes.io/instance: agent-monitor - app.kubernetes.io/version: "1.0.0" - app.kubernetes.io/component: storage - app.kubernetes.io/managed-by: kustomize -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 10Gi diff --git a/deployments/kubernetes/base/service.yaml b/deployments/kubernetes/base/service.yaml deleted file mode 100644 index f8dd713..0000000 --- a/deployments/kubernetes/base/service.yaml +++ /dev/null @@ -1,29 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: agent-monitor - namespace: agent-monitor - labels: - app.kubernetes.io/name: agent-monitor - app.kubernetes.io/instance: agent-monitor - app.kubernetes.io/version: "1.0.0" - app.kubernetes.io/component: server - app.kubernetes.io/managed-by: kustomize - annotations: - # Document the WebSocket requirement - service.kubernetes.io/topology-mode: Auto -spec: - type: ClusterIP - # Sticky sessions for WebSocket support - sessionAffinity: ClientIP - sessionAffinityConfig: - clientIP: - timeoutSeconds: 10800 - selector: - app.kubernetes.io/name: agent-monitor - app.kubernetes.io/instance: agent-monitor - ports: - - name: http - port: 80 - targetPort: http - protocol: TCP diff --git a/deployments/kubernetes/base/serviceaccount.yaml b/deployments/kubernetes/base/serviceaccount.yaml deleted file mode 100644 index f6f6890..0000000 --- a/deployments/kubernetes/base/serviceaccount.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: v1 -kind: ServiceAccount -metadata: - name: agent-monitor - namespace: agent-monitor - labels: - app.kubernetes.io/name: agent-monitor - app.kubernetes.io/instance: agent-monitor - app.kubernetes.io/version: "1.0.0" - app.kubernetes.io/component: serviceaccount - app.kubernetes.io/managed-by: kustomize -automountServiceAccountToken: false diff --git a/deployments/kubernetes/components/mcp-sidecar/deployment-patch.yaml b/deployments/kubernetes/components/mcp-sidecar/deployment-patch.yaml deleted file mode 100644 index 85e44ea..0000000 --- a/deployments/kubernetes/components/mcp-sidecar/deployment-patch.yaml +++ /dev/null @@ -1,69 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: agent-monitor - namespace: agent-monitor -spec: - template: - spec: - containers: - - name: mcp-sidecar - image: ${IMAGE_REGISTRY}/agent-monitor-mcp:${IMAGE_TAG} - imagePullPolicy: IfNotPresent - - ports: - - name: mcp - containerPort: 8819 - protocol: TCP - - env: - - name: MCP_TRANSPORT - value: "http" - - name: MCP_DASHBOARD_BASE_URL - value: "http://localhost:4820" - - name: MCP_PORT - value: "8819" - - resources: - requests: - memory: "64Mi" - cpu: "50m" - limits: - memory: "256Mi" - cpu: "250m" - - startupProbe: - tcpSocket: - port: mcp - failureThreshold: 30 - periodSeconds: 2 - - readinessProbe: - tcpSocket: - port: mcp - initialDelaySeconds: 5 - periodSeconds: 10 - timeoutSeconds: 3 - failureThreshold: 3 - - livenessProbe: - tcpSocket: - port: mcp - initialDelaySeconds: 10 - periodSeconds: 30 - timeoutSeconds: 5 - failureThreshold: 3 - - securityContext: - runAsNonRoot: true - runAsUser: 1000 - runAsGroup: 1000 - readOnlyRootFilesystem: true - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - - volumeMounts: - - name: tmp - mountPath: /tmp diff --git a/deployments/kubernetes/components/mcp-sidecar/kustomization.yaml b/deployments/kubernetes/components/mcp-sidecar/kustomization.yaml deleted file mode 100644 index 7dc7769..0000000 --- a/deployments/kubernetes/components/mcp-sidecar/kustomization.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1alpha1 -kind: Component - -metadata: - name: mcp-sidecar - -patches: - - path: deployment-patch.yaml - target: - kind: Deployment - name: agent-monitor diff --git a/deployments/kubernetes/components/monitoring/kustomization.yaml b/deployments/kubernetes/components/monitoring/kustomization.yaml deleted file mode 100644 index e0d0cad..0000000 --- a/deployments/kubernetes/components/monitoring/kustomization.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1alpha1 -kind: Component - -metadata: - name: monitoring - -resources: - - servicemonitor.yaml diff --git a/deployments/kubernetes/components/monitoring/servicemonitor.yaml b/deployments/kubernetes/components/monitoring/servicemonitor.yaml deleted file mode 100644 index ce2f59d..0000000 --- a/deployments/kubernetes/components/monitoring/servicemonitor.yaml +++ /dev/null @@ -1,31 +0,0 @@ -apiVersion: monitoring.coreos.com/v1 -kind: ServiceMonitor -metadata: - name: agent-monitor - namespace: agent-monitor - labels: - app.kubernetes.io/name: agent-monitor - app.kubernetes.io/instance: agent-monitor - app.kubernetes.io/version: "1.0.0" - app.kubernetes.io/component: monitoring - app.kubernetes.io/managed-by: kustomize - # Common label for Prometheus Operator discovery - release: prometheus -spec: - selector: - matchLabels: - app.kubernetes.io/name: agent-monitor - app.kubernetes.io/instance: agent-monitor - namespaceSelector: - matchNames: - - agent-monitor - endpoints: - - port: http - path: /api/health - interval: 30s - scrapeTimeout: 10s - honorLabels: true - metricRelabelings: - - sourceLabels: [__name__] - regex: "(http_requests_total|http_request_duration_.*|nodejs_.*|process_.*)" - action: keep diff --git a/deployments/kubernetes/overlays/dev/kustomization.yaml b/deployments/kubernetes/overlays/dev/kustomization.yaml deleted file mode 100644 index 4aad337..0000000 --- a/deployments/kubernetes/overlays/dev/kustomization.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -metadata: - name: agent-monitor-dev - -namespace: agent-monitor - -resources: - - ../../base - -images: - - name: ${IMAGE_REGISTRY}/agent-monitor - newName: agent-monitor - newTag: dev - -configMapGenerator: - - name: agent-monitor-config - behavior: merge - literals: - - NODE_ENV=development - - LOG_LEVEL=debug - -patches: - # Disable HPA in dev (single replica, no autoscaling needed) - - target: - kind: HorizontalPodAutoscaler - name: agent-monitor - patch: | - $patch: delete - apiVersion: autoscaling/v2 - kind: HorizontalPodAutoscaler - metadata: - name: agent-monitor - # Deployment overrides for dev - - path: patches/deployment-patch.yaml diff --git a/deployments/kubernetes/overlays/dev/patches/deployment-patch.yaml b/deployments/kubernetes/overlays/dev/patches/deployment-patch.yaml deleted file mode 100644 index 7121b04..0000000 --- a/deployments/kubernetes/overlays/dev/patches/deployment-patch.yaml +++ /dev/null @@ -1,22 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: agent-monitor - namespace: agent-monitor -spec: - # Single replica for dev - replicas: 1 - template: - spec: - # Remove anti-affinity in dev (single node is fine) - affinity: null - topologySpreadConstraints: [] - containers: - - name: agent-monitor - resources: - requests: - memory: "64Mi" - cpu: "50m" - limits: - memory: "256Mi" - cpu: "250m" diff --git a/deployments/kubernetes/overlays/production/kustomization.yaml b/deployments/kubernetes/overlays/production/kustomization.yaml deleted file mode 100644 index c0af19d..0000000 --- a/deployments/kubernetes/overlays/production/kustomization.yaml +++ /dev/null @@ -1,26 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -metadata: - name: agent-monitor-production - -namespace: agent-monitor - -resources: - - ../../base - -patches: - - path: patches/deployment-patch.yaml - - path: patches/hpa-patch.yaml - -images: - - name: ${IMAGE_REGISTRY}/agent-monitor - newName: agent-monitor - newTag: latest - -configMapGenerator: - - name: agent-monitor-config - behavior: merge - literals: - - NODE_ENV=production - - LOG_LEVEL=warn diff --git a/deployments/kubernetes/overlays/production/patches/deployment-patch.yaml b/deployments/kubernetes/overlays/production/patches/deployment-patch.yaml deleted file mode 100644 index 9f3d0ce..0000000 --- a/deployments/kubernetes/overlays/production/patches/deployment-patch.yaml +++ /dev/null @@ -1,42 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: agent-monitor - namespace: agent-monitor -spec: - replicas: 3 - template: - spec: - # Production: require spreading across nodes - affinity: - podAntiAffinity: - requiredDuringSchedulingIgnoredDuringExecution: - - labelSelector: - matchExpressions: - - key: app.kubernetes.io/name - operator: In - values: - - agent-monitor - topologyKey: kubernetes.io/hostname - topologySpreadConstraints: - - maxSkew: 1 - topologyKey: kubernetes.io/hostname - whenUnsatisfiable: DoNotSchedule - labelSelector: - matchLabels: - app.kubernetes.io/name: agent-monitor - - maxSkew: 1 - topologyKey: topology.kubernetes.io/zone - whenUnsatisfiable: ScheduleAnyway - labelSelector: - matchLabels: - app.kubernetes.io/name: agent-monitor - containers: - - name: agent-monitor - resources: - requests: - memory: "256Mi" - cpu: "200m" - limits: - memory: "1Gi" - cpu: "1000m" diff --git a/deployments/kubernetes/overlays/production/patches/hpa-patch.yaml b/deployments/kubernetes/overlays/production/patches/hpa-patch.yaml deleted file mode 100644 index 2610351..0000000 --- a/deployments/kubernetes/overlays/production/patches/hpa-patch.yaml +++ /dev/null @@ -1,26 +0,0 @@ -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: agent-monitor - namespace: agent-monitor -spec: - minReplicas: 3 - maxReplicas: 20 - behavior: - scaleDown: - stabilizationWindowSeconds: 600 - policies: - - type: Pods - value: 1 - periodSeconds: 120 - selectPolicy: Min - scaleUp: - stabilizationWindowSeconds: 60 - policies: - - type: Pods - value: 4 - periodSeconds: 60 - - type: Percent - value: 100 - periodSeconds: 60 - selectPolicy: Max diff --git a/deployments/kubernetes/overlays/staging/kustomization.yaml b/deployments/kubernetes/overlays/staging/kustomization.yaml deleted file mode 100644 index 0498a96..0000000 --- a/deployments/kubernetes/overlays/staging/kustomization.yaml +++ /dev/null @@ -1,25 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -metadata: - name: agent-monitor-staging - -namespace: agent-monitor - -resources: - - ../../base - -patches: - - path: patches/deployment-patch.yaml - -images: - - name: ${IMAGE_REGISTRY}/agent-monitor - newName: agent-monitor - newTag: staging - -configMapGenerator: - - name: agent-monitor-config - behavior: merge - literals: - - NODE_ENV=staging - - LOG_LEVEL=info diff --git a/deployments/kubernetes/overlays/staging/patches/deployment-patch.yaml b/deployments/kubernetes/overlays/staging/patches/deployment-patch.yaml deleted file mode 100644 index 8eebb24..0000000 --- a/deployments/kubernetes/overlays/staging/patches/deployment-patch.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: agent-monitor - namespace: agent-monitor -spec: - replicas: 2 - template: - spec: - containers: - - name: agent-monitor - resources: - requests: - memory: "128Mi" - cpu: "100m" - limits: - memory: "512Mi" - cpu: "500m" diff --git a/deployments/kubernetes/strategies/blue-green/deployment-blue.yaml b/deployments/kubernetes/strategies/blue-green/deployment-blue.yaml deleted file mode 100644 index 94dd6aa..0000000 --- a/deployments/kubernetes/strategies/blue-green/deployment-blue.yaml +++ /dev/null @@ -1,136 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: agent-monitor-blue - namespace: agent-monitor - labels: - app.kubernetes.io/name: agent-monitor - app.kubernetes.io/instance: agent-monitor - app.kubernetes.io/version: "1.0.0" - app.kubernetes.io/component: server - app.kubernetes.io/managed-by: kustomize - slot: blue -spec: - replicas: 3 - revisionHistoryLimit: 5 - strategy: - type: RollingUpdate - rollingUpdate: - maxSurge: 1 - maxUnavailable: 0 - selector: - matchLabels: - app.kubernetes.io/name: agent-monitor - app.kubernetes.io/instance: agent-monitor - slot: blue - template: - metadata: - labels: - app.kubernetes.io/name: agent-monitor - app.kubernetes.io/instance: agent-monitor - app.kubernetes.io/version: "1.0.0" - app.kubernetes.io/component: server - app.kubernetes.io/managed-by: kustomize - slot: blue - spec: - serviceAccountName: agent-monitor - automountServiceAccountToken: false - terminationGracePeriodSeconds: 30 - - securityContext: - runAsNonRoot: true - runAsUser: 1000 - runAsGroup: 1000 - fsGroup: 1000 - fsGroupChangePolicy: OnRootMismatch - seccompProfile: - type: RuntimeDefault - - affinity: - podAntiAffinity: - requiredDuringSchedulingIgnoredDuringExecution: - - labelSelector: - matchExpressions: - - key: app.kubernetes.io/name - operator: In - values: - - agent-monitor - topologyKey: kubernetes.io/hostname - - containers: - - name: agent-monitor - image: ${IMAGE_REGISTRY}/agent-monitor:blue - imagePullPolicy: IfNotPresent - - ports: - - name: http - containerPort: 4820 - protocol: TCP - - envFrom: - - configMapRef: - name: agent-monitor-config - - resources: - requests: - memory: "256Mi" - cpu: "200m" - limits: - memory: "1Gi" - cpu: "1000m" - - startupProbe: - httpGet: - path: /api/health - port: http - failureThreshold: 30 - periodSeconds: 2 - - readinessProbe: - httpGet: - path: /api/health - port: http - initialDelaySeconds: 5 - periodSeconds: 5 - timeoutSeconds: 3 - successThreshold: 1 - failureThreshold: 3 - - livenessProbe: - httpGet: - path: /api/health - port: http - initialDelaySeconds: 15 - periodSeconds: 15 - timeoutSeconds: 5 - successThreshold: 1 - failureThreshold: 3 - - securityContext: - runAsNonRoot: true - runAsUser: 1000 - runAsGroup: 1000 - readOnlyRootFilesystem: true - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - - volumeMounts: - - name: data - mountPath: /app/data - - name: tmp - mountPath: /tmp - - lifecycle: - preStop: - exec: - command: ["sh", "-c", "sleep 5"] - - volumes: - - name: data - persistentVolumeClaim: - claimName: agent-monitor-data - - name: tmp - emptyDir: - sizeLimit: 100Mi diff --git a/deployments/kubernetes/strategies/blue-green/deployment-green.yaml b/deployments/kubernetes/strategies/blue-green/deployment-green.yaml deleted file mode 100644 index a688a16..0000000 --- a/deployments/kubernetes/strategies/blue-green/deployment-green.yaml +++ /dev/null @@ -1,136 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: agent-monitor-green - namespace: agent-monitor - labels: - app.kubernetes.io/name: agent-monitor - app.kubernetes.io/instance: agent-monitor - app.kubernetes.io/version: "1.0.0" - app.kubernetes.io/component: server - app.kubernetes.io/managed-by: kustomize - slot: green -spec: - replicas: 3 - revisionHistoryLimit: 5 - strategy: - type: RollingUpdate - rollingUpdate: - maxSurge: 1 - maxUnavailable: 0 - selector: - matchLabels: - app.kubernetes.io/name: agent-monitor - app.kubernetes.io/instance: agent-monitor - slot: green - template: - metadata: - labels: - app.kubernetes.io/name: agent-monitor - app.kubernetes.io/instance: agent-monitor - app.kubernetes.io/version: "1.0.0" - app.kubernetes.io/component: server - app.kubernetes.io/managed-by: kustomize - slot: green - spec: - serviceAccountName: agent-monitor - automountServiceAccountToken: false - terminationGracePeriodSeconds: 30 - - securityContext: - runAsNonRoot: true - runAsUser: 1000 - runAsGroup: 1000 - fsGroup: 1000 - fsGroupChangePolicy: OnRootMismatch - seccompProfile: - type: RuntimeDefault - - affinity: - podAntiAffinity: - requiredDuringSchedulingIgnoredDuringExecution: - - labelSelector: - matchExpressions: - - key: app.kubernetes.io/name - operator: In - values: - - agent-monitor - topologyKey: kubernetes.io/hostname - - containers: - - name: agent-monitor - image: ${IMAGE_REGISTRY}/agent-monitor:green - imagePullPolicy: IfNotPresent - - ports: - - name: http - containerPort: 4820 - protocol: TCP - - envFrom: - - configMapRef: - name: agent-monitor-config - - resources: - requests: - memory: "256Mi" - cpu: "200m" - limits: - memory: "1Gi" - cpu: "1000m" - - startupProbe: - httpGet: - path: /api/health - port: http - failureThreshold: 30 - periodSeconds: 2 - - readinessProbe: - httpGet: - path: /api/health - port: http - initialDelaySeconds: 5 - periodSeconds: 5 - timeoutSeconds: 3 - successThreshold: 1 - failureThreshold: 3 - - livenessProbe: - httpGet: - path: /api/health - port: http - initialDelaySeconds: 15 - periodSeconds: 15 - timeoutSeconds: 5 - successThreshold: 1 - failureThreshold: 3 - - securityContext: - runAsNonRoot: true - runAsUser: 1000 - runAsGroup: 1000 - readOnlyRootFilesystem: true - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - - volumeMounts: - - name: data - mountPath: /app/data - - name: tmp - mountPath: /tmp - - lifecycle: - preStop: - exec: - command: ["sh", "-c", "sleep 5"] - - volumes: - - name: data - persistentVolumeClaim: - claimName: agent-monitor-data - - name: tmp - emptyDir: - sizeLimit: 100Mi diff --git a/deployments/kubernetes/strategies/blue-green/service-switch.yaml b/deployments/kubernetes/strategies/blue-green/service-switch.yaml deleted file mode 100644 index 3b2cc48..0000000 --- a/deployments/kubernetes/strategies/blue-green/service-switch.yaml +++ /dev/null @@ -1,32 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: agent-monitor - namespace: agent-monitor - labels: - app.kubernetes.io/name: agent-monitor - app.kubernetes.io/instance: agent-monitor - app.kubernetes.io/version: "1.0.0" - app.kubernetes.io/component: server - app.kubernetes.io/managed-by: kustomize - annotations: - # Document which slot is currently active - # To switch traffic: kubectl patch svc agent-monitor -n agent-monitor \ - # -p '{"spec":{"selector":{"slot":"green"}}}' - agent-monitor.io/active-slot: blue -spec: - type: ClusterIP - sessionAffinity: ClientIP - sessionAffinityConfig: - clientIP: - timeoutSeconds: 10800 - selector: - app.kubernetes.io/name: agent-monitor - app.kubernetes.io/instance: agent-monitor - # Toggle this value between "blue" and "green" to switch traffic - slot: blue - ports: - - name: http - port: 80 - targetPort: http - protocol: TCP diff --git a/deployments/kubernetes/strategies/canary/canary-analysis.yaml b/deployments/kubernetes/strategies/canary/canary-analysis.yaml deleted file mode 100644 index 742c88b..0000000 --- a/deployments/kubernetes/strategies/canary/canary-analysis.yaml +++ /dev/null @@ -1,97 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: AnalysisTemplate -metadata: - name: agent-monitor-canary-analysis - namespace: agent-monitor - labels: - app.kubernetes.io/name: agent-monitor - app.kubernetes.io/instance: agent-monitor - app.kubernetes.io/version: "1.0.0" - app.kubernetes.io/component: canary-analysis - app.kubernetes.io/managed-by: kustomize -spec: - args: - - name: service-name - value: agent-monitor-canary - - name: namespace - value: agent-monitor - metrics: - # Success rate must be above 99% - - name: success-rate - interval: 60s - count: 5 - successCondition: result[0] >= 0.99 - failureLimit: 2 - provider: - prometheus: - address: http://prometheus.monitoring.svc.cluster.local:9090 - query: | - sum( - rate( - http_requests_total{ - namespace="{{args.namespace}}", - service="{{args.service-name}}", - code!~"5.." - }[2m] - ) - ) - / - sum( - rate( - http_requests_total{ - namespace="{{args.namespace}}", - service="{{args.service-name}}" - }[2m] - ) - ) - - # P99 latency must be under 500ms - - name: p99-latency - interval: 60s - count: 5 - successCondition: result[0] < 500 - failureLimit: 2 - provider: - prometheus: - address: http://prometheus.monitoring.svc.cluster.local:9090 - query: | - histogram_quantile( - 0.99, - sum( - rate( - http_request_duration_milliseconds_bucket{ - namespace="{{args.namespace}}", - service="{{args.service-name}}" - }[2m] - ) - ) by (le) - ) - - # Error rate must stay below 1% - - name: error-rate - interval: 60s - count: 5 - successCondition: result[0] <= 0.01 - failureLimit: 2 - provider: - prometheus: - address: http://prometheus.monitoring.svc.cluster.local:9090 - query: | - sum( - rate( - http_requests_total{ - namespace="{{args.namespace}}", - service="{{args.service-name}}", - code=~"5.." - }[2m] - ) - ) - / - sum( - rate( - http_requests_total{ - namespace="{{args.namespace}}", - service="{{args.service-name}}" - }[2m] - ) - ) diff --git a/deployments/kubernetes/strategies/canary/canary-deployment.yaml b/deployments/kubernetes/strategies/canary/canary-deployment.yaml deleted file mode 100644 index 74db553..0000000 --- a/deployments/kubernetes/strategies/canary/canary-deployment.yaml +++ /dev/null @@ -1,124 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: agent-monitor-canary - namespace: agent-monitor - labels: - app.kubernetes.io/name: agent-monitor - app.kubernetes.io/instance: agent-monitor-canary - app.kubernetes.io/version: "1.0.0" - app.kubernetes.io/component: server - app.kubernetes.io/managed-by: kustomize - track: canary -spec: - replicas: 1 - revisionHistoryLimit: 5 - selector: - matchLabels: - app.kubernetes.io/name: agent-monitor - app.kubernetes.io/instance: agent-monitor-canary - track: canary - template: - metadata: - labels: - app.kubernetes.io/name: agent-monitor - app.kubernetes.io/instance: agent-monitor-canary - app.kubernetes.io/version: "1.0.0" - app.kubernetes.io/component: server - app.kubernetes.io/managed-by: kustomize - track: canary - annotations: - prometheus.io/scrape: "true" - prometheus.io/port: "4820" - prometheus.io/path: "/api/health" - spec: - serviceAccountName: agent-monitor - automountServiceAccountToken: false - terminationGracePeriodSeconds: 30 - - securityContext: - runAsNonRoot: true - runAsUser: 1000 - runAsGroup: 1000 - fsGroup: 1000 - fsGroupChangePolicy: OnRootMismatch - seccompProfile: - type: RuntimeDefault - - containers: - - name: agent-monitor - image: ${IMAGE_REGISTRY}/agent-monitor:canary - imagePullPolicy: Always - - ports: - - name: http - containerPort: 4820 - protocol: TCP - - envFrom: - - configMapRef: - name: agent-monitor-config - - resources: - requests: - memory: "256Mi" - cpu: "200m" - limits: - memory: "1Gi" - cpu: "1000m" - - startupProbe: - httpGet: - path: /api/health - port: http - failureThreshold: 30 - periodSeconds: 2 - - readinessProbe: - httpGet: - path: /api/health - port: http - initialDelaySeconds: 5 - periodSeconds: 5 - timeoutSeconds: 3 - successThreshold: 1 - failureThreshold: 3 - - livenessProbe: - httpGet: - path: /api/health - port: http - initialDelaySeconds: 15 - periodSeconds: 15 - timeoutSeconds: 5 - successThreshold: 1 - failureThreshold: 3 - - securityContext: - runAsNonRoot: true - runAsUser: 1000 - runAsGroup: 1000 - readOnlyRootFilesystem: true - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - - volumeMounts: - - name: data - mountPath: /app/data - - name: tmp - mountPath: /tmp - - lifecycle: - preStop: - exec: - command: ["sh", "-c", "sleep 5"] - - volumes: - - name: data - persistentVolumeClaim: - claimName: agent-monitor-data - - name: tmp - emptyDir: - sizeLimit: 100Mi diff --git a/deployments/monitoring/alertmanager/alertmanager.yaml b/deployments/monitoring/alertmanager/alertmanager.yaml deleted file mode 100644 index 6dc95af..0000000 --- a/deployments/monitoring/alertmanager/alertmanager.yaml +++ /dev/null @@ -1,241 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# Alertmanager configuration for Claude Code Agent Monitor -# -# Replace placeholder values (marked with ) before deploying. -# ───────────────────────────────────────────────────────────────────────────── -global: - # SMTP defaults for email notifications - smtp_smarthost: ":587" - smtp_from: "alertmanager@example.com" - smtp_auth_username: "" - smtp_auth_password: "" - smtp_require_tls: true - - # Slack API URL (override per-receiver if needed) - slack_api_url: "" - - # PagerDuty URL - pagerduty_url: "https://events.pagerduty.com/v2/enqueue" - - # Global resolve timeout - resolve_timeout: 5m - -# ── Templates ─────────────────────────────────────────────────────────────── -templates: - - "/etc/alertmanager/templates/*.tmpl" - -# ── Inhibition rules ─────────────────────────────────────────────────────── -# Prevent lower-severity alerts from firing when a higher-severity alert -# for the same service/alertname is already active. -inhibit_rules: - # If a critical alert is firing, suppress warning alerts for the same service - - source_matchers: - - severity = critical - target_matchers: - - severity = warning - equal: - - alertname - - service - - namespace - - # If a critical alert is firing, suppress info alerts for the same service - - source_matchers: - - severity = critical - target_matchers: - - severity = info - equal: - - service - - namespace - - # If a warning alert is firing, suppress info alerts for the same service - - source_matchers: - - severity = warning - target_matchers: - - severity = info - equal: - - service - - namespace - - # If AgentMonitorDown is firing, suppress all other agent-monitor alerts - - source_matchers: - - alertname = AgentMonitorDown - target_matchers: - - service = agent-monitor - equal: - - namespace - -# ── Route tree ────────────────────────────────────────────────────────────── -route: - # Default receiver for unmatched alerts - receiver: slack-default - - # Group alerts by these labels - group_by: - - alertname - - environment - - namespace - - # Wait before sending initial notification (allows grouping) - group_wait: 30s - - # Wait before sending updates to an existing group - group_interval: 5m - - # Wait before re-sending a resolved notification - repeat_interval: 4h - - # Child routes (evaluated top-to-bottom, first match wins) - routes: - # ── Critical → PagerDuty + Slack ────────────────────────────────────── - - matchers: - - severity = critical - receiver: pagerduty-critical - group_wait: 10s - repeat_interval: 1h - continue: true # Also notify Slack - - - matchers: - - severity = critical - receiver: slack-critical - group_wait: 10s - repeat_interval: 1h - - # ── Warning → Slack ─────────────────────────────────────────────────── - - matchers: - - severity = warning - receiver: slack-warning - group_wait: 30s - repeat_interval: 4h - - # ── Info → Email ────────────────────────────────────────────────────── - - matchers: - - severity = info - receiver: email-info - group_wait: 1m - repeat_interval: 12h - - # ── Watchdog (deadman's switch) ─────────────────────────────────────── - - matchers: - - alertname = Watchdog - receiver: "null" - repeat_interval: 5m - -# ── Receivers ─────────────────────────────────────────────────────────────── -receivers: - # ── Null receiver (discard alerts) ──────────────────────────────────────── - - name: "null" - - # ── Slack: Default channel ──────────────────────────────────────────────── - - name: slack-default - slack_configs: - - channel: "#agent-monitor-alerts" - send_resolved: true - username: "AlertManager" - icon_emoji: ":bell:" - title: >- - [{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] - {{ .CommonLabels.alertname }} - text: >- - {{ range .Alerts }} - *Alert:* {{ .Labels.alertname }} - `{{ .Labels.severity }}` - *Environment:* {{ .Labels.namespace }} - *Description:* {{ .Annotations.description }} - *Details:* - {{ range .Labels.SortedPairs }} • *{{ .Name }}:* `{{ .Value }}` - {{ end }} - {{ end }} - actions: - - type: button - text: "Dashboard :grafana:" - url: "{{ (index .Alerts 0).Annotations.dashboard_url }}" - - type: button - text: "Runbook :book:" - url: "{{ (index .Alerts 0).Annotations.runbook_url }}" - - # ── Slack: Critical alerts ──────────────────────────────────────────────── - - name: slack-critical - slack_configs: - - channel: "#agent-monitor-critical" - send_resolved: true - username: "AlertManager" - icon_emoji: ":rotating_light:" - color: >- - {{ if eq .Status "firing" }}danger{{ else }}good{{ end }} - title: >- - :rotating_light: [{{ .Status | toUpper }}] - {{ .CommonLabels.alertname }} - text: >- - {{ range .Alerts }} - *CRITICAL Alert:* {{ .Labels.alertname }} - *Environment:* {{ .Labels.namespace }} - *Description:* {{ .Annotations.description }} - *Summary:* {{ .Annotations.summary }} - *Started:* {{ .StartsAt.Format "2006-01-02 15:04:05 UTC" }} - {{ end }} - actions: - - type: button - text: "Dashboard" - url: "{{ (index .Alerts 0).Annotations.dashboard_url }}" - - type: button - text: "Runbook" - url: "{{ (index .Alerts 0).Annotations.runbook_url }}" - - # ── Slack: Warning alerts ───────────────────────────────────────────────── - - name: slack-warning - slack_configs: - - channel: "#agent-monitor-alerts" - send_resolved: true - username: "AlertManager" - icon_emoji: ":warning:" - color: >- - {{ if eq .Status "firing" }}warning{{ else }}good{{ end }} - title: >- - :warning: [{{ .Status | toUpper }}] - {{ .CommonLabels.alertname }} - text: >- - {{ range .Alerts }} - *Warning:* {{ .Labels.alertname }} - *Environment:* {{ .Labels.namespace }} - *Description:* {{ .Annotations.description }} - {{ end }} - - # ── PagerDuty: Critical alerts ─────────────────────────────────────────── - - name: pagerduty-critical - pagerduty_configs: - - routing_key: "" - severity: >- - {{ if eq .CommonLabels.severity "critical" }}critical{{ else }}warning{{ end }} - description: >- - {{ .CommonAnnotations.summary }} - details: - environment: "{{ .CommonLabels.namespace }}" - alertname: "{{ .CommonLabels.alertname }}" - service: "{{ .CommonLabels.service }}" - description: "{{ .CommonAnnotations.description }}" - num_firing: "{{ .Alerts.Firing | len }}" - - # ── Email: Info-level alerts ────────────────────────────────────────────── - - name: email-info - email_configs: - - to: "" - send_resolved: true - headers: - Subject: >- - [Agent Monitor {{ .Status | toUpper }}] - {{ .CommonLabels.alertname }} - ({{ .CommonLabels.namespace }}) - html: | -

{{ .CommonLabels.alertname }}

-

Status: {{ .Status }}

-

Environment: {{ .CommonLabels.namespace }}

- - - {{ range .Alerts }} - - - - - - - {{ end }} -
AlertSeverityDescriptionStarted
{{ .Labels.alertname }}{{ .Labels.severity }}{{ .Annotations.description }}{{ .StartsAt.Format "2006-01-02 15:04:05 UTC" }}
diff --git a/deployments/monitoring/coralogix/README.md b/deployments/monitoring/coralogix/README.md deleted file mode 100644 index ba404c5..0000000 --- a/deployments/monitoring/coralogix/README.md +++ /dev/null @@ -1,140 +0,0 @@ -# Coralogix Integration - -Full-stack observability for Claude Code Agent Monitor via [Coralogix](https://coralogix.com) — logs, metrics, traces, and SLO tracking through a single platform. - -## Architecture - -```mermaid -graph TB - subgraph "Kubernetes Cluster" - APP["Agent Monitor Pods"] - MCP["MCP Sidecar"] - OTEL["OTel Collector
(DaemonSet)"] - end - - APP -->|"metrics + logs"| OTEL - MCP -->|"metrics + logs"| OTEL - - OTEL -->|"OTLP (gRPC)"| CX["Coralogix Platform"] - - subgraph "Coralogix" - CX --> LOGS["Log Analytics
DataPrime Queries"] - CX --> MET["Metrics
PromQL + Recording Rules"] - CX --> TRACE["Distributed Tracing"] - CX --> ALERT["Alert Engine"] - CX --> DASH["Custom Dashboards"] - CX --> SLO["SLO Management"] - end - - ALERT -->|"Critical"| PD["PagerDuty"] - ALERT -->|"Warning"| SLACK["Slack"] - - style OTEL fill:#4f46e5,color:#fff - style CX fill:#1a1a2e,color:#fff - style LOGS fill:#7c3aed,color:#fff - style MET fill:#e6522c,color:#fff - style TRACE fill:#059669,color:#fff - style ALERT fill:#dc2626,color:#fff - style DASH fill:#f46800,color:#fff - style SLO fill:#0ea5e9,color:#fff -``` - -## Files - -| File | Purpose | -|------|---------| -| `values.yaml` | Helm values for Coralogix OpenTelemetry Collector | -| `alerts.yaml` | Alert definitions (mirrors Prometheus/Alertmanager rules) | -| `dashboards.yaml` | Custom dashboard with 6 rows, 18 panels, SLO tracking | -| `coralogix-terraform.tf` | Terraform-managed alerts, parsing rules, recording rules | - -## Quick Start - -### 1. Add the Helm Repository - -```bash -helm repo add coralogix https://cgx.jfrog.io/artifactory/coralogix-charts-virtual -helm repo update -``` - -### 2. Create the API Key Secret - -```bash -kubectl create secret generic coralogix-keys \ - --namespace agent-monitor \ - --from-literal=PRIVATE_KEY= -``` - -### 3. Deploy the OTel Collector - -```bash -helm install coralogix-otel coralogix/opentelemetry \ - --namespace agent-monitor \ - -f deployments/monitoring/coralogix/values.yaml -``` - -### 4. Import the Dashboard - -Upload `dashboards.yaml` via the Coralogix UI: - -**Dashboards → Custom Dashboards → Import** - -### 5. (Optional) Terraform-managed Alerts - -```bash -cd deployments/monitoring/coralogix -export CORALOGIX_API_KEY="" -export CORALOGIX_ENV="coralogix.com" -terraform init -terraform apply -``` - -## What Gets Collected - -| Signal | Source | Destination | -|--------|--------|-------------| -| **Logs** | Pod stdout/stderr (JSON structured) | Coralogix Log Analytics | -| **Metrics** | Prometheus scrape (`/api/health`) | Coralogix Metrics | -| **K8s Metrics** | kubelet, cAdvisor, host metrics | Coralogix Metrics | -| **Traces** | OTLP from application (if instrumented) | Coralogix Tracing | - -## Alert Parity - -All 10 Prometheus/Alertmanager rules are replicated in Coralogix: - -| Alert | Severity | Prometheus | Coralogix | -|-------|----------|:----------:|:---------:| -| Instance Down | Critical | ✓ | ✓ | -| High Error Rate | Critical | ✓ | ✓ | -| Pod Restart Loop | Critical | ✓ | ✓ | -| PV Nearly Full | Critical | ✓ | ✓ | -| High Latency | Warning | ✓ | ✓ | -| WebSocket Spike | Warning | ✓ | ✓ | -| High Memory | Warning | ✓ | ✓ | -| High CPU | Warning | ✓ | ✓ | -| HPA Maxed Out | Warning | ✓ | ✓ | -| Slow DB Queries | Warning | ✓ | ✓ | - -## Dashboard Panels - -The custom dashboard provides 18 panels across 6 rows: - -1. **Overview** — Active sessions, request rate, WebSocket connections -2. **HTTP Performance** — Latency distribution, error rate, status codes -3. **Application Logs** — Error log stream (DataPrime), log volume by severity, hook throughput -4. **Infrastructure** — CPU, memory, pod status -5. **Database & Storage** — SQLite query duration, PV usage, network I/O -6. **SLO Tracking** — Availability SLO (99.9%), latency SLO (P95 < 500ms), error budget burn - -## Coralogix Regions - -Set `global.domain` in `values.yaml` to match your Coralogix region: - -| Region | Domain | -|--------|--------| -| US1 | `coralogix.us` | -| US2 | `cx498.coralogix.com` | -| EU1 | `coralogix.com` | -| EU2 | `eu2.coralogix.com` | -| AP1 (India) | `coralogix.in` | -| AP2 (Singapore) | `coralogix.sg` | diff --git a/deployments/monitoring/coralogix/alerts.yaml b/deployments/monitoring/coralogix/alerts.yaml deleted file mode 100644 index b24c92f..0000000 --- a/deployments/monitoring/coralogix/alerts.yaml +++ /dev/null @@ -1,194 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# Coralogix Alert Definitions for Claude Code Agent Monitor -# -# These alerts can be imported via the Coralogix Alerts API or Terraform -# provider (coralogix/coralogix). They mirror the Prometheus/Alertmanager -# rules in ../prometheus/rules/ for consistency across observability stacks. -# -# API import: -# curl -X POST "https://api.coralogix.com/api/v1/external/alerts" \ -# -H "Authorization: Bearer $CORALOGIX_API_KEY" \ -# -H "Content-Type: application/json" \ -# -d @deployments/monitoring/coralogix/alerts.yaml -# -# Terraform: -# See the coralogix_alert resources in coralogix-terraform.tf -# ───────────────────────────────────────────────────────────────────────────── - -alerts: - # ── Critical ─────────────────────────────────────────────────────────────── - - - name: "Agent Monitor Down" - description: "No metrics received from agent-monitor pods for > 2 minutes" - severity: critical - type: metric - condition: - metric_name: "up" - filter: - job: "agent-monitor" - threshold: 1 - comparison: less_than - for_duration: "2m" - of_last: "5m" - notifications: - - integration: pagerduty - - integration: slack - channel: "#agent-monitor-critical" - labels: - service: agent-monitor - team: platform - - - name: "High Error Rate" - description: "5xx error rate exceeds 5% of total requests for 5 minutes" - severity: critical - type: ratio - condition: - numerator: - query: 'http_requests_total{job="agent-monitor", status=~"5.."}' - denominator: - query: 'http_requests_total{job="agent-monitor"}' - threshold: 0.05 - comparison: greater_than - for_duration: "5m" - notifications: - - integration: pagerduty - - integration: slack - channel: "#agent-monitor-critical" - labels: - service: agent-monitor - - - name: "Pod Restart Loop" - description: "Agent Monitor pod has restarted > 5 times in 15 minutes" - severity: critical - type: metric - condition: - query: 'increase(kube_pod_container_status_restarts_total{namespace=~"agent-monitor.*", container="agent-monitor"}[15m])' - threshold: 5 - comparison: greater_than - for_duration: "1m" - notifications: - - integration: pagerduty - - integration: slack - channel: "#agent-monitor-critical" - labels: - service: agent-monitor - - - name: "Persistent Volume Nearly Full" - description: "SQLite persistent volume is > 90% full" - severity: critical - type: metric - condition: - query: '(kubelet_volume_stats_used_bytes{namespace=~"agent-monitor.*"} / kubelet_volume_stats_capacity_bytes{namespace=~"agent-monitor.*"}) * 100' - threshold: 90 - comparison: greater_than - for_duration: "5m" - notifications: - - integration: pagerduty - - integration: slack - channel: "#agent-monitor-critical" - labels: - service: agent-monitor - - # ── Warning ──────────────────────────────────────────────────────────────── - - - name: "High Latency" - description: "P95 request latency exceeds 2 seconds for 5 minutes" - severity: warning - type: metric - condition: - query: 'histogram_quantile(0.95, rate(http_request_duration_seconds_bucket{job="agent-monitor"}[5m]))' - threshold: 2 - comparison: greater_than - for_duration: "5m" - notifications: - - integration: slack - channel: "#agent-monitor-alerts" - labels: - service: agent-monitor - - - name: "WebSocket Connection Spike" - description: "Active WebSocket connections exceed 1000" - severity: warning - type: metric - condition: - metric_name: "websocket_connections_active" - filter: - job: "agent-monitor" - threshold: 1000 - comparison: greater_than - for_duration: "2m" - notifications: - - integration: slack - channel: "#agent-monitor-alerts" - labels: - service: agent-monitor - - - name: "High Memory Usage" - description: "Container memory usage exceeds 85% of limit" - severity: warning - type: metric - condition: - query: '(container_memory_working_set_bytes{namespace=~"agent-monitor.*", container="agent-monitor"} / container_spec_memory_limit_bytes{namespace=~"agent-monitor.*", container="agent-monitor"}) * 100' - threshold: 85 - comparison: greater_than - for_duration: "5m" - notifications: - - integration: slack - channel: "#agent-monitor-alerts" - labels: - service: agent-monitor - - - name: "High CPU Usage" - description: "Container CPU usage exceeds 80% for 10 minutes" - severity: warning - type: metric - condition: - query: '(rate(container_cpu_usage_seconds_total{namespace=~"agent-monitor.*", container="agent-monitor"}[5m]) / container_spec_cpu_quota{namespace=~"agent-monitor.*", container="agent-monitor"} * 100000)' - threshold: 80 - comparison: greater_than - for_duration: "10m" - notifications: - - integration: slack - channel: "#agent-monitor-alerts" - labels: - service: agent-monitor - - - name: "HPA Maxed Out" - description: "HPA replicas at max for 15 minutes — may need capacity increase" - severity: warning - type: metric - condition: - query: 'kube_horizontalpodautoscaler_status_current_replicas{namespace=~"agent-monitor.*"} == kube_horizontalpodautoscaler_spec_max_replicas{namespace=~"agent-monitor.*"}' - threshold: 1 - comparison: greater_than_or_equal - for_duration: "15m" - notifications: - - integration: slack - channel: "#agent-monitor-alerts" - labels: - service: agent-monitor - - - name: "Slow Database Queries" - description: "SQLite query duration exceeds 1 second" - severity: warning - type: metric - condition: - metric_name: "sqlite_query_duration_seconds" - filter: - job: "agent-monitor" - threshold: 1 - comparison: greater_than - for_duration: "5m" - notifications: - - integration: slack - channel: "#agent-monitor-alerts" - labels: - service: agent-monitor - -# ── Notification integrations ──────────────────────────────────────────────── -# Configure these in Coralogix UI: Settings → Integrations → Outbound Webhooks -# -# Required integrations: -# - pagerduty: PagerDuty Events API v2 routing key -# - slack: Slack webhook for #agent-monitor-critical and #agent-monitor-alerts -# - email: (optional) Email notification group diff --git a/deployments/monitoring/coralogix/coralogix-terraform.tf b/deployments/monitoring/coralogix/coralogix-terraform.tf deleted file mode 100644 index e3e98ab..0000000 --- a/deployments/monitoring/coralogix/coralogix-terraform.tf +++ /dev/null @@ -1,353 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# Coralogix Terraform Integration for Claude Code Agent Monitor -# -# Provisions Coralogix resources via the official Terraform provider: -# - Alert rules (mirroring Prometheus/Alertmanager rules) -# - Log parsing rules for structured JSON ingestion -# - Recording rules for pre-aggregated SLO metrics -# - Dashboard provisioning -# -# Usage: -# export CORALOGIX_API_KEY="" -# export CORALOGIX_ENV="" # e.g. coralogix.com -# terraform init -# terraform plan -# terraform apply -# -# Requires: hashicorp/terraform >= 1.5, coralogix/coralogix >= 1.10 -# ───────────────────────────────────────────────────────────────────────────── - -terraform { - required_version = ">= 1.5" - required_providers { - coralogix = { - source = "coralogix/coralogix" - version = "~> 1.10" - } - } -} - -provider "coralogix" { - # API key and environment are sourced from: - # CORALOGIX_API_KEY – Send-Your-Data API key - # CORALOGIX_ENV – Domain (e.g. coralogix.com, eu2.coralogix.com) -} - -# ── Variables ──────────────────────────────────────────────────────────────── - -variable "environment" { - description = "Deployment environment (dev, staging, production)" - type = string - default = "production" - validation { - condition = contains(["dev", "staging", "production"], var.environment) - error_message = "environment must be one of: dev, staging, production." - } -} - -variable "notification_group_id" { - description = "Coralogix notification group ID for alert routing" - type = string - default = "" -} - -variable "pagerduty_webhook_id" { - description = "Coralogix outbound webhook ID for PagerDuty integration" - type = string - default = "" -} - -variable "slack_webhook_id" { - description = "Coralogix outbound webhook ID for Slack integration" - type = string - default = "" -} - -locals { - app_name = "agent-monitor" - subsystem = "kubernetes" - alert_prefix = "[Agent Monitor]" -} - -# ── Parsing Rules ──────────────────────────────────────────────────────────── -# Structured JSON log parsing for agent-monitor application logs - -resource "coralogix_rules_group" "agent_monitor_parsing" { - name = "${local.alert_prefix} Log Parsing" - description = "Parse structured JSON logs from Agent Monitor pods" - enabled = true - order = 1 - - rule_subgroups { - rules { - name = "JSON Extract" - description = "Extract structured fields from JSON application logs" - source_field = "text" - enabled = true - - parse_json_field { - destination_field = "json" - keep_source_field = false - keep_destination_field = true - } - } - } - - rule_subgroups { - rules { - name = "Severity Mapping" - description = "Map log level field to Coralogix severity" - source_field = "json.level" - enabled = true - - extract { - regexp = "(?Pdebug|info|warn|error|fatal)" - } - } - } -} - -# ── Recording Rules ────────────────────────────────────────────────────────── -# Pre-aggregate SLO metrics for efficient dashboard queries - -resource "coralogix_recording_rule_group_set" "slo_metrics" { - name = "${local.alert_prefix} SLO Recording Rules" - - groups { - name = "agent_monitor_slo" - interval = 60 # seconds - - rules { - record = "agent_monitor:http_availability:ratio_rate5m" - expr = <<-EOT - 1 - ( - sum(rate(http_requests_total{job="agent-monitor", status=~"5.."}[5m])) - / - sum(rate(http_requests_total{job="agent-monitor"}[5m])) - ) - EOT - labels = { - service = local.app_name - environment = var.environment - } - } - - rules { - record = "agent_monitor:http_latency_p95:seconds_rate5m" - expr = <<-EOT - histogram_quantile(0.95, - sum(rate(http_request_duration_seconds_bucket{job="agent-monitor"}[5m])) by (le) - ) - EOT - labels = { - service = local.app_name - environment = var.environment - } - } - - rules { - record = "agent_monitor:websocket_connections:total" - expr = <<-EOT - sum(websocket_connections_active{job="agent-monitor"}) - EOT - labels = { - service = local.app_name - environment = var.environment - } - } - } -} - -# ── Alert Rules ────────────────────────────────────────────────────────────── - -resource "coralogix_alert" "instance_down" { - name = "${local.alert_prefix} Instance Down" - description = "No metrics received from agent-monitor pods for > 2 minutes" - severity = "Critical" - enabled = true - - metric { - promql { - text = "up{job=\"agent-monitor\"} == 0" - condition = "more_than" - threshold = 0 - } - duration = "2m" - } - - notifications_group { - dynamic "notification" { - for_each = var.pagerduty_webhook_id != "" ? [1] : [] - content { - integration_id = var.pagerduty_webhook_id - } - } - dynamic "notification" { - for_each = var.slack_webhook_id != "" ? [1] : [] - content { - integration_id = var.slack_webhook_id - } - } - } - - labels = { - service = local.app_name - environment = var.environment - team = "platform" - } -} - -resource "coralogix_alert" "high_error_rate" { - name = "${local.alert_prefix} High Error Rate" - description = "5xx error rate exceeds 5% of total requests for 5 minutes" - severity = "Critical" - enabled = true - - metric { - promql { - text = <<-EOT - ( - sum(rate(http_requests_total{job="agent-monitor", status=~"5.."}[5m])) - / - sum(rate(http_requests_total{job="agent-monitor"}[5m])) - ) * 100 > 5 - EOT - condition = "more_than" - threshold = 5 - } - duration = "5m" - } - - notifications_group { - dynamic "notification" { - for_each = var.pagerduty_webhook_id != "" ? [1] : [] - content { - integration_id = var.pagerduty_webhook_id - } - } - } - - labels = { - service = local.app_name - environment = var.environment - } -} - -resource "coralogix_alert" "high_latency" { - name = "${local.alert_prefix} High Latency" - description = "P95 request latency exceeds 2 seconds for 5 minutes" - severity = "Warning" - enabled = true - - metric { - promql { - text = "histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket{job=\"agent-monitor\"}[5m])) by (le)) > 2" - condition = "more_than" - threshold = 2 - } - duration = "5m" - } - - notifications_group { - dynamic "notification" { - for_each = var.slack_webhook_id != "" ? [1] : [] - content { - integration_id = var.slack_webhook_id - } - } - } - - labels = { - service = local.app_name - environment = var.environment - } -} - -resource "coralogix_alert" "high_memory" { - name = "${local.alert_prefix} High Memory Usage" - description = "Container memory usage exceeds 85% of limit" - severity = "Warning" - enabled = true - - metric { - promql { - text = <<-EOT - ( - container_memory_working_set_bytes{namespace=~"agent-monitor.*", container="agent-monitor"} - / - container_spec_memory_limit_bytes{namespace=~"agent-monitor.*", container="agent-monitor"} - ) * 100 > 85 - EOT - condition = "more_than" - threshold = 85 - } - duration = "5m" - } - - notifications_group { - dynamic "notification" { - for_each = var.slack_webhook_id != "" ? [1] : [] - content { - integration_id = var.slack_webhook_id - } - } - } - - labels = { - service = local.app_name - environment = var.environment - } -} - -resource "coralogix_alert" "pod_restart_loop" { - name = "${local.alert_prefix} Pod Restart Loop" - description = "Agent Monitor pod has restarted > 5 times in 15 minutes" - severity = "Critical" - enabled = true - - metric { - promql { - text = "increase(kube_pod_container_status_restarts_total{namespace=~\"agent-monitor.*\", container=\"agent-monitor\"}[15m]) > 5" - condition = "more_than" - threshold = 5 - } - duration = "1m" - } - - notifications_group { - dynamic "notification" { - for_each = var.pagerduty_webhook_id != "" ? [1] : [] - content { - integration_id = var.pagerduty_webhook_id - } - } - } - - labels = { - service = local.app_name - environment = var.environment - } -} - -# ── Outputs ────────────────────────────────────────────────────────────────── - -output "parsing_rule_group_id" { - description = "ID of the Coralogix parsing rule group" - value = coralogix_rules_group.agent_monitor_parsing.id -} - -output "recording_rule_set_id" { - description = "ID of the Coralogix recording rule group set" - value = coralogix_recording_rule_group_set.slo_metrics.id -} - -output "alert_ids" { - description = "IDs of all provisioned Coralogix alerts" - value = { - instance_down = coralogix_alert.instance_down.id - high_error_rate = coralogix_alert.high_error_rate.id - high_latency = coralogix_alert.high_latency.id - high_memory = coralogix_alert.high_memory.id - pod_restart = coralogix_alert.pod_restart_loop.id - } -} diff --git a/deployments/monitoring/coralogix/dashboards.yaml b/deployments/monitoring/coralogix/dashboards.yaml deleted file mode 100644 index ec52ee1..0000000 --- a/deployments/monitoring/coralogix/dashboards.yaml +++ /dev/null @@ -1,245 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# Coralogix Custom Dashboard – Agent Monitor -# -# Import via Coralogix UI: Dashboards → Custom Dashboards → Import -# Or via API: -# curl -X POST "https://api.coralogix.com/api/v1/external/grafana/api/dashboards/db" \ -# -H "Authorization: Bearer $CORALOGIX_API_KEY" \ -# -H "Content-Type: application/json" \ -# -d @dashboards.yaml -# -# This dashboard mirrors the Grafana dashboard (../grafana/dashboards/) while -# leveraging Coralogix-native features: DataPrime queries, log correlation, -# distributed tracing waterfall, and Apdex scoring. -# ───────────────────────────────────────────────────────────────────────────── - -dashboard: - name: "Agent Monitor – Operations" - description: "Claude Code Agent Monitor: real-time operations, SLOs, and infrastructure health" - folder: "Agent Monitor" - tags: - - agent-monitor - - operations - - sre - - # ── Row 1: Overview ────────────────────────────────────────────────────── - rows: - - name: "Overview" - panels: - - title: "Active Sessions" - type: line-chart - query: - type: metrics - promql: 'agent_monitor_active_sessions' - legend: "{{namespace}}" - span: 4 - - - title: "Request Rate (req/s)" - type: line-chart - query: - type: metrics - promql: 'sum(rate(http_requests_total{job="agent-monitor"}[5m]))' - legend: "Requests/sec" - span: 4 - - - title: "WebSocket Connections" - type: line-chart - query: - type: metrics - promql: 'websocket_connections_active{job="agent-monitor"}' - legend: "{{pod}}" - span: 4 - - # ── Row 2: HTTP Performance ──────────────────────────────────────────── - - name: "HTTP Performance" - panels: - - title: "Latency Distribution (P50 / P95 / P99)" - type: line-chart - query: - type: metrics - promql: | - histogram_quantile(0.50, sum(rate(http_request_duration_seconds_bucket{job="agent-monitor"}[5m])) by (le)) - histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket{job="agent-monitor"}[5m])) by (le)) - histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket{job="agent-monitor"}[5m])) by (le)) - span: 6 - - - title: "Error Rate (%)" - type: line-chart - query: - type: metrics - promql: | - sum(rate(http_requests_total{job="agent-monitor", status=~"5.."}[5m])) - / - sum(rate(http_requests_total{job="agent-monitor"}[5m])) * 100 - legend: "5xx %" - thresholds: - - value: 1 - color: yellow - - value: 5 - color: red - span: 3 - - - title: "Status Code Distribution" - type: bar-chart - query: - type: metrics - promql: 'sum by (status) (increase(http_requests_total{job="agent-monitor"}[1h]))' - span: 3 - - # ── Row 3: Logs (DataPrime) ──────────────────────────────────────────── - - name: "Application Logs" - panels: - - title: "Error Logs" - type: dataprime - query: | - source logs - | filter $d.cx.application.name == 'agent-monitor' - | filter $d.severity == 'ERROR' || $d.severity == 'FATAL' - | select $m.timestamp, $d.k8s.pod.name, $d.message - | order by $m.timestamp desc - | limit 100 - span: 6 - - - title: "Log Volume by Severity" - type: bar-chart - query: - type: dataprime - expression: | - source logs - | filter $d.cx.application.name == 'agent-monitor' - | count_group_by $d.severity as count - span: 3 - - - title: "Hook Event Throughput" - type: line-chart - query: - type: dataprime - expression: | - source logs - | filter $d.cx.application.name == 'agent-monitor' - | filter $d.message matches 'hook.*event' - | count_per_time 1m as throughput - span: 3 - - # ── Row 4: Infrastructure ────────────────────────────────────────────── - - name: "Infrastructure" - panels: - - title: "CPU Usage (%)" - type: line-chart - query: - type: metrics - promql: | - rate(container_cpu_usage_seconds_total{ - namespace=~"agent-monitor.*", - container="agent-monitor" - }[5m]) * 100 - legend: "{{pod}}" - span: 4 - - - title: "Memory Usage (MiB)" - type: line-chart - query: - type: metrics - promql: | - container_memory_working_set_bytes{ - namespace=~"agent-monitor.*", - container="agent-monitor" - } / 1024 / 1024 - legend: "{{pod}}" - span: 4 - - - title: "Pod Status" - type: gauge - query: - type: metrics - promql: | - count by (phase) ( - kube_pod_status_phase{namespace=~"agent-monitor.*"} - ) - span: 4 - - # ── Row 5: Database & Storage ────────────────────────────────────────── - - name: "Database & Storage" - panels: - - title: "SQLite Query Duration (ms)" - type: line-chart - query: - type: metrics - promql: 'sqlite_query_duration_seconds{job="agent-monitor"} * 1000' - span: 4 - - - title: "PV Usage (%)" - type: gauge - query: - type: metrics - promql: | - (kubelet_volume_stats_used_bytes{namespace=~"agent-monitor.*"} - / kubelet_volume_stats_capacity_bytes{namespace=~"agent-monitor.*"}) * 100 - thresholds: - - value: 70 - color: yellow - - value: 90 - color: red - span: 4 - - - title: "Network I/O (bytes/s)" - type: line-chart - query: - type: metrics - promql: | - sum by (pod) (rate(container_network_receive_bytes_total{namespace=~"agent-monitor.*"}[5m])) - sum by (pod) (rate(container_network_transmit_bytes_total{namespace=~"agent-monitor.*"}[5m])) - span: 4 - - # ── Row 6: SLO Tracking ──────────────────────────────────────────────── - - name: "SLO Tracking" - panels: - - title: "Availability SLO (99.9% target)" - type: gauge - query: - type: metrics - promql: | - (1 - sum(rate(http_requests_total{job="agent-monitor", status=~"5.."}[30d])) - / sum(rate(http_requests_total{job="agent-monitor"}[30d]))) * 100 - thresholds: - - value: 99.9 - color: green - - value: 99.5 - color: yellow - - value: 99.0 - color: red - span: 4 - - - title: "Latency SLO (P95 < 500ms)" - type: gauge - query: - type: metrics - promql: | - histogram_quantile(0.95, - sum(rate(http_request_duration_seconds_bucket{job="agent-monitor"}[30d])) by (le) - ) * 1000 - thresholds: - - value: 300 - color: green - - value: 500 - color: yellow - - value: 1000 - color: red - span: 4 - - - title: "Error Budget Remaining" - type: gauge - query: - type: metrics - promql: | - (0.001 - sum(rate(http_requests_total{job="agent-monitor", status=~"5.."}[30d])) - / sum(rate(http_requests_total{job="agent-monitor"}[30d]))) - / 0.001 * 100 - thresholds: - - value: 50 - color: green - - value: 25 - color: yellow - - value: 0 - color: red - span: 4 diff --git a/deployments/monitoring/coralogix/values.yaml b/deployments/monitoring/coralogix/values.yaml deleted file mode 100644 index d22ea5f..0000000 --- a/deployments/monitoring/coralogix/values.yaml +++ /dev/null @@ -1,180 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# Coralogix OpenTelemetry Collector – Helm Values -# -# Deploys the Coralogix OTel collector as a DaemonSet + Gateway for shipping -# logs, metrics, and traces from the Agent Monitor cluster. -# -# Prerequisites: -# 1. Add Coralogix Helm repo: -# helm repo add coralogix https://cgx.jfrog.io/artifactory/coralogix-charts-virtual -# helm repo update -# 2. Create the API key secret: -# kubectl create secret generic coralogix-keys \ -# --namespace agent-monitor \ -# --from-literal=PRIVATE_KEY= -# -# Install: -# helm install coralogix-otel coralogix/opentelemetry \ -# --namespace agent-monitor \ -# -f deployments/monitoring/coralogix/values.yaml -# ───────────────────────────────────────────────────────────────────────────── - -global: - # Coralogix domain – set to your region's endpoint - # Options: coralogix.com | eu2.coralogix.com | coralogix.in | coralogix.us | - # cx498.coralogix.com | coralogix.eu | coralogix.sg - domain: "coralogix.com" - - # Reference the API key from the pre-created K8s secret - clusterName: "agent-monitor" - -# ── Secret reference ───────────────────────────────────────────────────────── -secret: - enabled: true - name: "coralogix-keys" - # Key in the secret containing the Coralogix Send-Your-Data API key - privateKeySecretRef: - key: "PRIVATE_KEY" - -# ── Collector – DaemonSet mode (node-level collection) ─────────────────────── -opentelemetry-collector: - mode: daemonset - - presets: - # Collect Kubernetes pod/container logs - logsCollection: - enabled: true - includeCollectorLogs: false - - # Enrich telemetry with Kubernetes metadata - kubernetesAttributes: - enabled: true - extractAllPodLabels: true - extractAllPodAnnotations: false - - # Collect host-level metrics (CPU, memory, disk, network) - hostMetrics: - enabled: true - - # Collect kubelet/cAdvisor metrics - kubeletMetrics: - enabled: true - - config: - receivers: - # Scrape Prometheus metrics from agent-monitor pods - prometheus: - config: - scrape_configs: - - job_name: "agent-monitor" - scrape_interval: 15s - kubernetes_sd_configs: - - role: pod - namespaces: - names: - - agent-monitor - - agent-monitor-staging - - agent-monitor-production - relabel_configs: - - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape] - action: keep - regex: "true" - - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path] - action: replace - target_label: __metrics_path__ - regex: (.+) - - source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port] - action: replace - regex: ([^:]+)(?::\d+)?;(\d+) - replacement: $$1:$$2 - target_label: __address__ - - # Receive OTLP from in-cluster services (gRPC + HTTP) - otlp: - protocols: - grpc: - endpoint: "0.0.0.0:4317" - http: - endpoint: "0.0.0.0:4318" - - processors: - # Batch telemetry for efficient export - batch: - send_batch_size: 1024 - send_batch_max_size: 2048 - timeout: 5s - - # Enrich with resource attributes - resource: - attributes: - - key: cx.application.name - value: "agent-monitor" - action: upsert - - key: cx.subsystem.name - from_attribute: k8s.container.name - action: upsert - - key: k8s.cluster.name - value: "agent-monitor" - action: upsert - - # Memory limiter to prevent OOM - memory_limiter: - check_interval: 5s - limit_percentage: 80 - spike_limit_percentage: 25 - - # Filter out noisy internal logs - filter/drop-internal: - logs: - exclude: - match_type: regexp - bodies: - - ".*kube-probe.*" - - ".*healthz.*" - - exporters: - coralogix: - domain: "${CORALOGIX_DOMAIN}" - private_key: "${PRIVATE_KEY}" - application_name: "agent-monitor" - subsystem_name: "kubernetes" - timeout: 30s - - service: - pipelines: - logs: - receivers: [otlp] - processors: [memory_limiter, resource, filter/drop-internal, batch] - exporters: [coralogix] - metrics: - receivers: [otlp, prometheus] - processors: [memory_limiter, resource, batch] - exporters: [coralogix] - traces: - receivers: [otlp] - processors: [memory_limiter, resource, batch] - exporters: [coralogix] - - resources: - requests: - cpu: 100m - memory: 256Mi - limits: - cpu: 500m - memory: 512Mi - - tolerations: - - operator: Exists - effect: NoSchedule - -# ── Gateway mode (optional – for centralized export) ───────────────────────── -opentelemetry-gateway: - enabled: false - replicaCount: 2 - resources: - requests: - cpu: 250m - memory: 512Mi - limits: - cpu: "1" - memory: "1Gi" diff --git a/deployments/monitoring/grafana/dashboards/agent-monitor.json b/deployments/monitoring/grafana/dashboards/agent-monitor.json deleted file mode 100644 index 99eb1e3..0000000 --- a/deployments/monitoring/grafana/dashboards/agent-monitor.json +++ /dev/null @@ -1,707 +0,0 @@ -{ - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": { "type": "grafana", "uid": "-- Grafana --" }, - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "type": "dashboard" - }, - { - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "enable": true, - "expr": "changes(kube_deployment_status_observed_generation{namespace=~\"$namespace\", deployment=~\"agent-monitor.*\"}[2m]) > 0", - "iconColor": "#6ED0E0", - "name": "Deployments", - "titleFormat": "Deployment updated" - } - ] - }, - "description": "Comprehensive monitoring dashboard for Claude Code Agent Monitor", - "editable": true, - "fiscalYearStartMonth": 0, - "graphTooltip": 1, - "id": null, - "links": [], - "panels": [ - { - "collapsed": false, - "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, - "id": 100, - "title": "Overview", - "type": "row" - }, - { - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "fieldConfig": { - "defaults": { - "color": { "mode": "thresholds" }, - "mappings": [ - { - "options": { - "0": { "color": "red", "text": "DOWN" }, - "1": { "color": "green", "text": "UP" } - }, - "type": "value" - } - ], - "thresholds": { - "steps": [ - { "color": "red", "value": null }, - { "color": "green", "value": 1 } - ] - } - } - }, - "gridPos": { "h": 4, "w": 4, "x": 0, "y": 1 }, - "id": 1, - "options": { - "colorMode": "background", - "graphMode": "none", - "justifyMode": "center", - "textMode": "value", - "reduceOptions": { "calcs": ["lastNotNull"] } - }, - "title": "Uptime Status", - "type": "stat", - "targets": [ - { - "expr": "up{job=\"agent-monitor\", namespace=~\"$namespace\"}", - "legendFormat": "{{instance}}", - "refId": "A" - } - ] - }, - { - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "fieldConfig": { - "defaults": { - "color": { "mode": "thresholds" }, - "unit": "dtdurations", - "thresholds": { "steps": [{ "color": "green", "value": null }] } - } - }, - "gridPos": { "h": 4, "w": 4, "x": 4, "y": 1 }, - "id": 2, - "options": { - "colorMode": "value", - "graphMode": "none", - "justifyMode": "center", - "textMode": "value", - "reduceOptions": { "calcs": ["lastNotNull"] } - }, - "title": "Uptime Duration", - "type": "stat", - "targets": [ - { - "expr": "time() - process_start_time_seconds{job=\"agent-monitor\", namespace=~\"$namespace\"}", - "legendFormat": "", - "refId": "A" - } - ] - }, - { - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "fieldConfig": { - "defaults": { - "color": { "mode": "thresholds" }, - "unit": "short", - "thresholds": { - "steps": [ - { "color": "green", "value": null }, - { "color": "yellow", "value": 2 }, - { "color": "red", "value": 0 } - ] - } - } - }, - "gridPos": { "h": 4, "w": 4, "x": 8, "y": 1 }, - "id": 3, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "center", - "textMode": "value", - "reduceOptions": { "calcs": ["lastNotNull"] } - }, - "title": "Pod Count", - "type": "stat", - "targets": [ - { - "expr": "count(kube_pod_status_ready{namespace=~\"$namespace\", condition=\"true\"} == 1)", - "legendFormat": "Ready", - "refId": "A" - } - ] - }, - { - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "fieldConfig": { - "defaults": { - "color": { "mode": "thresholds" }, - "unit": "short", - "thresholds": { - "steps": [ - { "color": "green", "value": null }, - { "color": "yellow", "value": 1 }, - { "color": "red", "value": 3 } - ] - } - } - }, - "gridPos": { "h": 4, "w": 4, "x": 12, "y": 1 }, - "id": 4, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "center", - "textMode": "value", - "reduceOptions": { "calcs": ["lastNotNull"] } - }, - "title": "Pod Restarts (1h)", - "type": "stat", - "targets": [ - { - "expr": "sum(increase(kube_pod_container_status_restarts_total{namespace=~\"$namespace\", container=\"agent-monitor\"}[1h]))", - "legendFormat": "", - "refId": "A" - } - ] - }, - { - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "fieldConfig": { - "defaults": { - "color": { "mode": "thresholds" }, - "unit": "short", - "thresholds": { "steps": [{ "color": "blue", "value": null }] } - } - }, - "gridPos": { "h": 4, "w": 4, "x": 16, "y": 1 }, - "id": 5, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "center", - "textMode": "value", - "reduceOptions": { "calcs": ["lastNotNull"] } - }, - "title": "Active WebSocket Connections", - "type": "stat", - "targets": [ - { - "expr": "sum(websocket_connections_active{job=\"agent-monitor\", namespace=~\"$namespace\"})", - "legendFormat": "", - "refId": "A" - } - ] - }, - { - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "fieldConfig": { - "defaults": { - "color": { "mode": "thresholds" }, - "unit": "decbytes", - "thresholds": { - "steps": [ - { "color": "green", "value": null }, - { "color": "yellow", "value": 5368709120 }, - { "color": "red", "value": 8589934592 } - ] - } - } - }, - "gridPos": { "h": 4, "w": 4, "x": 20, "y": 1 }, - "id": 6, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "center", - "textMode": "value", - "reduceOptions": { "calcs": ["lastNotNull"] } - }, - "title": "SQLite DB Size", - "type": "stat", - "targets": [ - { - "expr": "kubelet_volume_stats_used_bytes{namespace=~\"$namespace\", persistentvolumeclaim=~\"agent-monitor.*\"}", - "legendFormat": "", - "refId": "A" - } - ] - }, - { - "collapsed": false, - "gridPos": { "h": 1, "w": 24, "x": 0, "y": 5 }, - "id": 101, - "title": "HTTP Traffic", - "type": "row" - }, - { - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "fieldConfig": { - "defaults": { - "color": { "mode": "palette-classic" }, - "custom": { - "axisLabel": "req/s", - "drawStyle": "line", - "fillOpacity": 15, - "lineWidth": 2, - "pointSize": 5, - "spanNulls": false, - "stacking": { "mode": "none" } - }, - "unit": "reqps" - }, - "overrides": [ - { - "matcher": { "id": "byRegexp", "options": "5.." }, - "properties": [{ "id": "color", "value": { "fixedColor": "red", "mode": "fixed" } }] - }, - { - "matcher": { "id": "byRegexp", "options": "4.." }, - "properties": [{ "id": "color", "value": { "fixedColor": "yellow", "mode": "fixed" } }] - }, - { - "matcher": { "id": "byRegexp", "options": "2.." }, - "properties": [{ "id": "color", "value": { "fixedColor": "green", "mode": "fixed" } }] - } - ] - }, - "gridPos": { "h": 8, "w": 12, "x": 0, "y": 6 }, - "id": 10, - "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom" }, - "tooltip": { "mode": "multi", "sort": "desc" } - }, - "title": "Request Rate (RPS)", - "type": "timeseries", - "targets": [ - { - "expr": "sum(rate(http_requests_total{job=\"agent-monitor\", namespace=~\"$namespace\"}[5m])) by (status)", - "legendFormat": "{{ status }}", - "refId": "A" - }, - { - "expr": "sum(rate(http_requests_total{job=\"agent-monitor\", namespace=~\"$namespace\"}[5m]))", - "legendFormat": "Total", - "refId": "B" - } - ] - }, - { - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "fieldConfig": { - "defaults": { - "color": { "mode": "palette-classic" }, - "custom": { - "axisLabel": "%", - "drawStyle": "line", - "fillOpacity": 20, - "lineWidth": 2, - "thresholdsStyle": { "mode": "line" } - }, - "max": 100, - "min": 0, - "unit": "percent", - "thresholds": { - "steps": [ - { "color": "green", "value": null }, - { "color": "yellow", "value": 2 }, - { "color": "red", "value": 5 } - ] - } - } - }, - "gridPos": { "h": 8, "w": 12, "x": 12, "y": 6 }, - "id": 11, - "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom" }, - "tooltip": { "mode": "multi" } - }, - "title": "Error Rate (%)", - "type": "timeseries", - "targets": [ - { - "expr": "100 * sum(rate(http_requests_total{job=\"agent-monitor\", namespace=~\"$namespace\", status=~\"5..\"}[5m])) / sum(rate(http_requests_total{job=\"agent-monitor\", namespace=~\"$namespace\"}[5m]))", - "legendFormat": "5xx Error Rate", - "refId": "A" - }, - { - "expr": "100 * sum(rate(http_requests_total{job=\"agent-monitor\", namespace=~\"$namespace\", status=~\"4..\"}[5m])) / sum(rate(http_requests_total{job=\"agent-monitor\", namespace=~\"$namespace\"}[5m]))", - "legendFormat": "4xx Error Rate", - "refId": "B" - } - ] - }, - { - "collapsed": false, - "gridPos": { "h": 1, "w": 24, "x": 0, "y": 14 }, - "id": 102, - "title": "Latency", - "type": "row" - }, - { - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "fieldConfig": { - "defaults": { - "color": { "mode": "palette-classic" }, - "custom": { - "axisLabel": "seconds", - "drawStyle": "line", - "fillOpacity": 10, - "lineWidth": 2, - "spanNulls": false - }, - "unit": "s", - "thresholds": { - "steps": [ - { "color": "green", "value": null }, - { "color": "yellow", "value": 0.5 }, - { "color": "red", "value": 1 } - ] - } - } - }, - "gridPos": { "h": 8, "w": 12, "x": 0, "y": 15 }, - "id": 20, - "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom" }, - "tooltip": { "mode": "multi", "sort": "desc" } - }, - "title": "Response Latency (p50 / p90 / p99)", - "type": "timeseries", - "targets": [ - { - "expr": "histogram_quantile(0.50, sum(rate(http_request_duration_seconds_bucket{job=\"agent-monitor\", namespace=~\"$namespace\"}[5m])) by (le))", - "legendFormat": "p50", - "refId": "A" - }, - { - "expr": "histogram_quantile(0.90, sum(rate(http_request_duration_seconds_bucket{job=\"agent-monitor\", namespace=~\"$namespace\"}[5m])) by (le))", - "legendFormat": "p90", - "refId": "B" - }, - { - "expr": "histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket{job=\"agent-monitor\", namespace=~\"$namespace\"}[5m])) by (le))", - "legendFormat": "p99", - "refId": "C" - } - ] - }, - { - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "fieldConfig": { - "defaults": { - "color": { "mode": "palette-classic" }, - "custom": { - "drawStyle": "bars", - "fillOpacity": 80, - "lineWidth": 1, - "stacking": { "mode": "normal" } - }, - "unit": "reqps" - } - }, - "gridPos": { "h": 8, "w": 12, "x": 12, "y": 15 }, - "id": 21, - "options": { - "legend": { "calcs": ["mean"], "displayMode": "table", "placement": "bottom" }, - "tooltip": { "mode": "multi" } - }, - "title": "Request Rate by Endpoint", - "type": "timeseries", - "targets": [ - { - "expr": "sum(rate(http_requests_total{job=\"agent-monitor\", namespace=~\"$namespace\"}[5m])) by (handler)", - "legendFormat": "{{ handler }}", - "refId": "A" - } - ] - }, - { - "collapsed": false, - "gridPos": { "h": 1, "w": 24, "x": 0, "y": 23 }, - "id": 103, - "title": "WebSocket & Connections", - "type": "row" - }, - { - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "fieldConfig": { - "defaults": { - "color": { "mode": "palette-classic" }, - "custom": { "drawStyle": "line", "fillOpacity": 25, "lineWidth": 2, "spanNulls": false }, - "unit": "short" - } - }, - "gridPos": { "h": 8, "w": 12, "x": 0, "y": 24 }, - "id": 30, - "options": { - "legend": { - "calcs": ["mean", "max", "last"], - "displayMode": "table", - "placement": "bottom" - }, - "tooltip": { "mode": "multi" } - }, - "title": "Active WebSocket Connections", - "type": "timeseries", - "targets": [ - { - "expr": "websocket_connections_active{job=\"agent-monitor\", namespace=~\"$namespace\"}", - "legendFormat": "{{ pod }}", - "refId": "A" - }, - { - "expr": "sum(websocket_connections_active{job=\"agent-monitor\", namespace=~\"$namespace\"})", - "legendFormat": "Total", - "refId": "B" - } - ] - }, - { - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "fieldConfig": { - "defaults": { - "color": { "mode": "palette-classic" }, - "custom": { "drawStyle": "line", "fillOpacity": 10, "lineWidth": 2 }, - "unit": "short" - } - }, - "gridPos": { "h": 8, "w": 12, "x": 12, "y": 24 }, - "id": 31, - "options": { - "legend": { "calcs": ["sum"], "displayMode": "table", "placement": "bottom" }, - "tooltip": { "mode": "multi" } - }, - "title": "WebSocket Messages (rate/s)", - "type": "timeseries", - "targets": [ - { - "expr": "sum(rate(websocket_messages_sent_total{job=\"agent-monitor\", namespace=~\"$namespace\"}[5m]))", - "legendFormat": "Sent", - "refId": "A" - }, - { - "expr": "sum(rate(websocket_messages_received_total{job=\"agent-monitor\", namespace=~\"$namespace\"}[5m]))", - "legendFormat": "Received", - "refId": "B" - } - ] - }, - { - "collapsed": false, - "gridPos": { "h": 1, "w": 24, "x": 0, "y": 32 }, - "id": 104, - "title": "Resource Usage", - "type": "row" - }, - { - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "fieldConfig": { - "defaults": { - "color": { "mode": "palette-classic" }, - "custom": { - "drawStyle": "line", - "fillOpacity": 20, - "lineWidth": 2, - "thresholdsStyle": { "mode": "line+area" } - }, - "unit": "bytes", - "thresholds": { - "steps": [ - { "color": "transparent", "value": null }, - { "color": "red", "value": 536870912 } - ] - } - } - }, - "gridPos": { "h": 8, "w": 12, "x": 0, "y": 33 }, - "id": 40, - "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom" }, - "tooltip": { "mode": "multi" } - }, - "title": "Memory Usage", - "type": "timeseries", - "targets": [ - { - "expr": "container_memory_working_set_bytes{namespace=~\"$namespace\", container=\"agent-monitor\"}", - "legendFormat": "{{ pod }} (working set)", - "refId": "A" - }, - { - "expr": "container_spec_memory_limit_bytes{namespace=~\"$namespace\", container=\"agent-monitor\"}", - "legendFormat": "{{ pod }} (limit)", - "refId": "B" - } - ] - }, - { - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "fieldConfig": { - "defaults": { - "color": { "mode": "palette-classic" }, - "custom": { - "drawStyle": "line", - "fillOpacity": 20, - "lineWidth": 2, - "thresholdsStyle": { "mode": "line" } - }, - "unit": "percentunit", - "max": 1, - "min": 0, - "thresholds": { - "steps": [ - { "color": "green", "value": null }, - { "color": "yellow", "value": 0.7 }, - { "color": "red", "value": 0.9 } - ] - } - } - }, - "gridPos": { "h": 8, "w": 12, "x": 12, "y": 33 }, - "id": 41, - "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom" }, - "tooltip": { "mode": "multi" } - }, - "title": "CPU Usage", - "type": "timeseries", - "targets": [ - { - "expr": "rate(container_cpu_usage_seconds_total{namespace=~\"$namespace\", container=\"agent-monitor\"}[5m])", - "legendFormat": "{{ pod }}", - "refId": "A" - } - ] - }, - { - "collapsed": false, - "gridPos": { "h": 1, "w": 24, "x": 0, "y": 41 }, - "id": 105, - "title": "Storage", - "type": "row" - }, - { - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "fieldConfig": { - "defaults": { - "color": { "mode": "palette-classic" }, - "custom": { - "drawStyle": "line", - "fillOpacity": 20, - "lineWidth": 2, - "thresholdsStyle": { "mode": "line+area" } - }, - "unit": "decbytes", - "thresholds": { - "steps": [ - { "color": "transparent", "value": null }, - { "color": "yellow", "value": 8589934592 }, - { "color": "red", "value": 9663676416 } - ] - } - } - }, - "gridPos": { "h": 8, "w": 12, "x": 0, "y": 42 }, - "id": 50, - "options": { - "legend": { "calcs": ["last", "max"], "displayMode": "table", "placement": "bottom" }, - "tooltip": { "mode": "multi" } - }, - "title": "SQLite DB Size (PV Usage)", - "type": "timeseries", - "targets": [ - { - "expr": "kubelet_volume_stats_used_bytes{namespace=~\"$namespace\", persistentvolumeclaim=~\"agent-monitor.*\"}", - "legendFormat": "Used ({{ persistentvolumeclaim }})", - "refId": "A" - }, - { - "expr": "kubelet_volume_stats_capacity_bytes{namespace=~\"$namespace\", persistentvolumeclaim=~\"agent-monitor.*\"}", - "legendFormat": "Capacity ({{ persistentvolumeclaim }})", - "refId": "B" - } - ] - }, - { - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "fieldConfig": { - "defaults": { - "color": { "mode": "thresholds" }, - "max": 100, - "min": 0, - "unit": "percent", - "thresholds": { - "steps": [ - { "color": "green", "value": null }, - { "color": "yellow", "value": 75 }, - { "color": "red", "value": 90 } - ] - } - } - }, - "gridPos": { "h": 8, "w": 12, "x": 12, "y": 42 }, - "id": 51, - "options": { - "orientation": "horizontal", - "reduceOptions": { "calcs": ["lastNotNull"] }, - "showThresholdLabels": false, - "showThresholdMarkers": true - }, - "title": "PV Usage (%)", - "type": "gauge", - "targets": [ - { - "expr": "100 * kubelet_volume_stats_used_bytes{namespace=~\"$namespace\", persistentvolumeclaim=~\"agent-monitor.*\"} / kubelet_volume_stats_capacity_bytes{namespace=~\"$namespace\", persistentvolumeclaim=~\"agent-monitor.*\"}", - "legendFormat": "{{ persistentvolumeclaim }}", - "refId": "A" - } - ] - } - ], - "refresh": "30s", - "schemaVersion": 39, - "tags": ["agent-monitor", "nodejs", "websocket"], - "templating": { - "list": [ - { - "current": { "selected": false, "text": "Prometheus", "value": "prometheus" }, - "hide": 0, - "includeAll": false, - "label": "Data Source", - "name": "datasource", - "options": [], - "query": "prometheus", - "type": "datasource" - }, - { - "current": {}, - "datasource": { "type": "prometheus", "uid": "${datasource}" }, - "definition": "label_values(up{job=\"agent-monitor\"}, namespace)", - "hide": 0, - "includeAll": true, - "label": "Namespace", - "multi": false, - "name": "namespace", - "query": { "qryType": 1, "query": "label_values(up{job=\"agent-monitor\"}, namespace)" }, - "refresh": 2, - "type": "query" - } - ] - }, - "time": { "from": "now-6h", "to": "now" }, - "timepicker": { - "refresh_intervals": ["10s", "30s", "1m", "5m", "15m"], - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] - }, - "timezone": "browser", - "title": "Claude Code Agent Monitor", - "uid": "agent-monitor-main", - "version": 1 -} diff --git a/deployments/monitoring/grafana/datasources.yaml b/deployments/monitoring/grafana/datasources.yaml deleted file mode 100644 index 9dc7b87..0000000 --- a/deployments/monitoring/grafana/datasources.yaml +++ /dev/null @@ -1,51 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# Grafana datasource provisioning for Claude Code Agent Monitor -# ───────────────────────────────────────────────────────────────────────────── -apiVersion: 1 - -# Prevent users from deleting provisioned data sources in the UI -deleteDatasources: - - name: Prometheus - orgId: 1 - -datasources: - # ── Primary Prometheus datasource ───────────────────────────────────────── - - name: Prometheus - type: prometheus - uid: prometheus - access: proxy - url: http://prometheus-server:9090 - isDefault: true - editable: false - jsonData: - # Scrape interval matches prometheus.yaml global setting - timeInterval: "15s" - # Query timeout - queryTimeout: "30s" - # HTTP method for queries - httpMethod: POST - # Manage alerts via Prometheus - manageAlerts: true - # Alerting rule settings - prometheusType: Prometheus - prometheusVersion: ">=2.45.0" - # Incremental querying for better performance - incrementalQuerying: true - incrementalQueryOverlapWindow: "10m" - # Exemplar trace support (uncomment if using tracing) - # exemplarTraceIdDestinations: - # - name: traceId - # datasourceUid: tempo - version: 1 - - # ── Alertmanager datasource ─────────────────────────────────────────────── - - name: Alertmanager - type: alertmanager - uid: alertmanager - access: proxy - url: http://alertmanager:9093 - editable: false - jsonData: - implementation: prometheus - handleGrafanaManagedAlerts: false - version: 1 diff --git a/deployments/monitoring/prometheus/prometheus.yaml b/deployments/monitoring/prometheus/prometheus.yaml deleted file mode 100644 index 68deafc..0000000 --- a/deployments/monitoring/prometheus/prometheus.yaml +++ /dev/null @@ -1,226 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# Prometheus configuration for Claude Code Agent Monitor -# ───────────────────────────────────────────────────────────────────────────── -global: - scrape_interval: 15s - scrape_timeout: 10s - evaluation_interval: 15s - - external_labels: - cluster: "${CLUSTER_NAME:agent-monitor}" - environment: "${ENVIRONMENT:production}" - -# ── Rule files ────────────────────────────────────────────────────────────── -rule_files: - - /etc/prometheus/rules/*.rules.yaml - -# ── Alertmanager ──────────────────────────────────────────────────────────── -alerting: - alertmanagers: - - static_configs: - - targets: - - alertmanager:9093 - scheme: http - timeout: 10s - api_version: v2 - -# ── Scrape configs ────────────────────────────────────────────────────────── -scrape_configs: - # ── Agent Monitor application ───────────────────────────────────────────── - - job_name: "agent-monitor" - metrics_path: /api/health - scrape_interval: 15s - scrape_timeout: 5s - scheme: http - - # Static target for standalone deployments - static_configs: - - targets: - - "agent-monitor:4820" - labels: - app: agent-monitor - component: server - - # Relabeling to add standard labels - relabel_configs: - - source_labels: [__address__] - target_label: instance - - target_label: __metrics_path__ - replacement: /api/health - - metric_relabel_configs: - - source_labels: [__name__] - regex: "go_.*" - action: drop - - # ── Agent Monitor MCP sidecar ───────────────────────────────────────────── - - job_name: "agent-monitor-mcp" - scrape_interval: 30s - static_configs: - - targets: - - "agent-monitor-mcp:8819" - labels: - app: agent-monitor - component: mcp - - # ── Kubernetes service discovery (pods) ─────────────────────────────────── - - job_name: "kubernetes-pods" - scrape_interval: 15s - - kubernetes_sd_configs: - - role: pod - namespaces: - own_namespace: false - names: - - agent-monitor - - agent-monitor-dev - - agent-monitor-staging - - agent-monitor-production - - relabel_configs: - # Only scrape pods with annotation prometheus.io/scrape=true - - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape] - action: keep - regex: true - - # Use custom metrics path if annotated - - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path] - action: replace - target_label: __metrics_path__ - regex: (.+) - - # Use custom port if annotated - - source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port] - action: replace - regex: ([^:]+)(?::\d+)?;(\d+) - replacement: $1:$2 - target_label: __address__ - - # Use custom scheme if annotated - - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scheme] - action: replace - target_label: __scheme__ - regex: (.+) - - # Map pod labels to Prometheus labels - - action: labelmap - regex: __meta_kubernetes_pod_label_(.+) - - # Add namespace label - - source_labels: [__meta_kubernetes_namespace] - action: replace - target_label: namespace - - # Add pod name label - - source_labels: [__meta_kubernetes_pod_name] - action: replace - target_label: pod - - # Add node name label - - source_labels: [__meta_kubernetes_pod_node_name] - action: replace - target_label: node - - # ── Kubernetes service discovery (services) ────────────────────────────── - - job_name: "kubernetes-services" - scrape_interval: 15s - - kubernetes_sd_configs: - - role: service - namespaces: - names: - - agent-monitor - - agent-monitor-dev - - agent-monitor-staging - - agent-monitor-production - - relabel_configs: - - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scrape] - action: keep - regex: true - - - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_path] - action: replace - target_label: __metrics_path__ - regex: (.+) - - - source_labels: [__address__, __meta_kubernetes_service_annotation_prometheus_io_port] - action: replace - regex: ([^:]+)(?::\d+)?;(\d+) - replacement: $1:$2 - target_label: __address__ - - - action: labelmap - regex: __meta_kubernetes_service_label_(.+) - - - source_labels: [__meta_kubernetes_namespace] - action: replace - target_label: namespace - - - source_labels: [__meta_kubernetes_service_name] - action: replace - target_label: service - - # ── Kubernetes nodes ────────────────────────────────────────────────────── - - job_name: "kubernetes-nodes" - scrape_interval: 30s - scheme: https - - kubernetes_sd_configs: - - role: node - - tls_config: - ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt - insecure_skip_verify: false - - bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token - - relabel_configs: - - action: labelmap - regex: __meta_kubernetes_node_label_(.+) - - - target_label: __address__ - replacement: kubernetes.default.svc:443 - - - source_labels: [__meta_kubernetes_node_name] - regex: (.+) - target_label: __metrics_path__ - replacement: /api/v1/nodes/$1/proxy/metrics - - # ── Kubernetes cadvisor (container metrics) ─────────────────────────────── - - job_name: "kubernetes-cadvisor" - scrape_interval: 15s - scheme: https - - kubernetes_sd_configs: - - role: node - - tls_config: - ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt - insecure_skip_verify: false - - bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token - - relabel_configs: - - action: labelmap - regex: __meta_kubernetes_node_label_(.+) - - - target_label: __address__ - replacement: kubernetes.default.svc:443 - - - source_labels: [__meta_kubernetes_node_name] - regex: (.+) - target_label: __metrics_path__ - replacement: /api/v1/nodes/$1/proxy/metrics/cadvisor - - metric_relabel_configs: - # Keep only container metrics for agent-monitor pods - - source_labels: [container] - regex: "agent-monitor.*" - action: keep - - # ── Prometheus self-monitoring ──────────────────────────────────────────── - - job_name: "prometheus" - static_configs: - - targets: - - "localhost:9090" diff --git a/deployments/monitoring/prometheus/rules/agent-monitor.rules.yaml b/deployments/monitoring/prometheus/rules/agent-monitor.rules.yaml deleted file mode 100644 index a9b2e9b..0000000 --- a/deployments/monitoring/prometheus/rules/agent-monitor.rules.yaml +++ /dev/null @@ -1,310 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# Prometheus alerting rules for Claude Code Agent Monitor -# ───────────────────────────────────────────────────────────────────────────── -apiVersion: monitoring.coreos.com/v1 -kind: PrometheusRule -metadata: - name: agent-monitor-alerts - namespace: agent-monitor - labels: - app.kubernetes.io/name: agent-monitor - app.kubernetes.io/component: monitoring - prometheus: kube-prometheus - role: alert-rules -spec: - groups: - # ── Availability alerts ───────────────────────────────────────────────── - - name: agent-monitor.availability - rules: - - alert: AgentMonitorDown - expr: up{job="agent-monitor"} == 0 - for: 2m - labels: - severity: critical - service: agent-monitor - team: platform - annotations: - summary: "Agent Monitor is down" - description: >- - The Agent Monitor instance {{ $labels.instance }} has been - unreachable for more than 2 minutes. Immediate investigation - required. - runbook_url: "https://wiki.example.com/runbooks/agent-monitor-down" - dashboard_url: "https://grafana.example.com/d/agent-monitor" - - - alert: AgentMonitorHighAvailabilityDegraded - expr: | - count(up{job="agent-monitor"} == 1) - < count(up{job="agent-monitor"}) - for: 5m - labels: - severity: warning - service: agent-monitor - annotations: - summary: "Agent Monitor partial outage" - description: >- - Not all Agent Monitor replicas are healthy. Only - {{ $value }} of expected replicas are up. - - # ── Error rate alerts ─────────────────────────────────────────────────── - - name: agent-monitor.errors - rules: - - alert: HighErrorRate - expr: | - ( - sum(rate(http_requests_total{job="agent-monitor", status=~"5.."}[5m])) - / - sum(rate(http_requests_total{job="agent-monitor"}[5m])) - ) > 0.05 - for: 5m - labels: - severity: warning - service: agent-monitor - annotations: - summary: "High HTTP error rate (>5%)" - description: >- - The Agent Monitor is returning 5xx errors at a rate of - {{ $value | humanizePercentage }} over the last 5 minutes. - dashboard_url: "https://grafana.example.com/d/agent-monitor?tab=errors" - - - alert: CriticalErrorRate - expr: | - ( - sum(rate(http_requests_total{job="agent-monitor", status=~"5.."}[5m])) - / - sum(rate(http_requests_total{job="agent-monitor"}[5m])) - ) > 0.25 - for: 2m - labels: - severity: critical - service: agent-monitor - annotations: - summary: "Critical HTTP error rate (>25%)" - description: >- - The Agent Monitor is returning 5xx errors at a rate of - {{ $value | humanizePercentage }}. Service may be severely degraded. - - # ── Latency alerts ────────────────────────────────────────────────────── - - name: agent-monitor.latency - rules: - - alert: HighLatency - expr: | - histogram_quantile(0.99, - sum(rate(http_request_duration_seconds_bucket{job="agent-monitor"}[5m])) by (le) - ) > 1 - for: 5m - labels: - severity: warning - service: agent-monitor - annotations: - summary: "High p99 response latency (>1s)" - description: >- - The 99th percentile response time for Agent Monitor is - {{ $value | humanizeDuration }}. Users may experience slow page loads. - - - alert: HighMedianLatency - expr: | - histogram_quantile(0.50, - sum(rate(http_request_duration_seconds_bucket{job="agent-monitor"}[5m])) by (le) - ) > 0.5 - for: 5m - labels: - severity: warning - service: agent-monitor - annotations: - summary: "High median response latency (>500ms)" - description: >- - The median response time is {{ $value | humanizeDuration }}. - This affects the majority of requests. - - # ── Resource alerts ───────────────────────────────────────────────────── - - name: agent-monitor.resources - rules: - - alert: HighMemoryUsage - expr: | - ( - container_memory_working_set_bytes{ - namespace=~"agent-monitor.*", - container="agent-monitor" - } - / - container_spec_memory_limit_bytes{ - namespace=~"agent-monitor.*", - container="agent-monitor" - } - ) > 0.90 - for: 10m - labels: - severity: warning - service: agent-monitor - annotations: - summary: "High memory usage (>90% of limit)" - description: >- - Pod {{ $labels.pod }} is using {{ $value | humanizePercentage }} - of its memory limit. OOMKill risk is elevated. - - - alert: HighCPUUsage - expr: | - ( - rate(container_cpu_usage_seconds_total{ - namespace=~"agent-monitor.*", - container="agent-monitor" - }[5m]) - / - container_spec_cpu_quota{ - namespace=~"agent-monitor.*", - container="agent-monitor" - } - * 100000 - ) > 0.85 - for: 10m - labels: - severity: warning - service: agent-monitor - annotations: - summary: "High CPU usage (>85% of limit)" - description: >- - Pod {{ $labels.pod }} is using {{ $value | humanizePercentage }} - of its CPU limit. Consider scaling up. - - - alert: PersistentVolumeNearlyFull - expr: | - ( - kubelet_volume_stats_used_bytes{ - namespace=~"agent-monitor.*", - persistentvolumeclaim=~"agent-monitor.*" - } - / - kubelet_volume_stats_capacity_bytes{ - namespace=~"agent-monitor.*", - persistentvolumeclaim=~"agent-monitor.*" - } - ) > 0.85 - for: 15m - labels: - severity: warning - service: agent-monitor - annotations: - summary: "Persistent volume nearly full (>85%)" - description: >- - PVC {{ $labels.persistentvolumeclaim }} in namespace - {{ $labels.namespace }} is {{ $value | humanizePercentage }} - full. SQLite writes may fail when volume is exhausted. - - - alert: PersistentVolumeCriticallyFull - expr: | - ( - kubelet_volume_stats_used_bytes{ - namespace=~"agent-monitor.*", - persistentvolumeclaim=~"agent-monitor.*" - } - / - kubelet_volume_stats_capacity_bytes{ - namespace=~"agent-monitor.*", - persistentvolumeclaim=~"agent-monitor.*" - } - ) > 0.95 - for: 5m - labels: - severity: critical - service: agent-monitor - annotations: - summary: "Persistent volume critically full (>95%)" - description: >- - PVC {{ $labels.persistentvolumeclaim }} is at - {{ $value | humanizePercentage }} capacity. - Immediate action required to prevent data loss. - - # ── WebSocket alerts ──────────────────────────────────────────────────── - - name: agent-monitor.websocket - rules: - - alert: WebSocketConnectionsDrop - expr: | - ( - max_over_time(websocket_connections_active{job="agent-monitor"}[10m]) - - websocket_connections_active{job="agent-monitor"} - ) - / - max_over_time(websocket_connections_active{job="agent-monitor"}[10m]) - > 0.50 - for: 5m - labels: - severity: warning - service: agent-monitor - annotations: - summary: "WebSocket connections dropped >50%" - description: >- - Active WebSocket connections have dropped by more than 50% - in the last 5 minutes (from - {{ with printf `max_over_time(websocket_connections_active{instance="%s"}[10m])` .Labels.instance | query }}{{ . | first | value }}{{ end }} - to {{ $value }}). This may indicate connectivity issues. - - - alert: NoWebSocketConnections - expr: | - websocket_connections_active{job="agent-monitor"} == 0 - and on() hour() >= 8 <= 20 - for: 15m - labels: - severity: info - service: agent-monitor - annotations: - summary: "No active WebSocket connections during business hours" - description: >- - There are no active WebSocket connections to Agent Monitor - during expected business hours. - - # ── Pod stability alerts ──────────────────────────────────────────────── - - name: agent-monitor.stability - rules: - - alert: PodRestarting - expr: | - increase( - kube_pod_container_status_restarts_total{ - namespace=~"agent-monitor.*", - container="agent-monitor" - }[15m] - ) > 3 - for: 0m - labels: - severity: critical - service: agent-monitor - annotations: - summary: "Pod restarting frequently (>3 in 15m)" - description: >- - Pod {{ $labels.pod }} in namespace {{ $labels.namespace }} - has restarted {{ $value }} times in the last 15 minutes. - Check logs: kubectl logs {{ $labels.pod }} -n {{ $labels.namespace }} --previous - - - alert: PodNotReady - expr: | - kube_pod_status_ready{ - namespace=~"agent-monitor.*", - condition="true" - } == 0 - for: 5m - labels: - severity: warning - service: agent-monitor - annotations: - summary: "Pod not ready for >5 minutes" - description: >- - Pod {{ $labels.pod }} has been in a not-ready state for - more than 5 minutes. - - - alert: DeploymentReplicasMismatch - expr: | - kube_deployment_spec_replicas{namespace=~"agent-monitor.*"} - != - kube_deployment_status_ready_replicas{namespace=~"agent-monitor.*"} - for: 10m - labels: - severity: warning - service: agent-monitor - annotations: - summary: "Deployment replicas mismatch" - description: >- - Deployment {{ $labels.deployment }} has - {{ with printf `kube_deployment_status_ready_replicas{deployment="%s",namespace="%s"}` .Labels.deployment .Labels.namespace | query }}{{ . | first | value }}{{ end }} - ready replicas but - {{ with printf `kube_deployment_spec_replicas{deployment="%s",namespace="%s"}` .Labels.deployment .Labels.namespace | query }}{{ . | first | value }}{{ end }} - desired. diff --git a/deployments/scripts/blue-green-switch.sh b/deployments/scripts/blue-green-switch.sh deleted file mode 100755 index 73404ac..0000000 --- a/deployments/scripts/blue-green-switch.sh +++ /dev/null @@ -1,294 +0,0 @@ -#!/usr/bin/env bash -# ───────────────────────────────────────────────────────────────────────────── -# blue-green-switch.sh – Switch traffic between blue/green deployment slots -# -# Usage: -# ./blue-green-switch.sh --env production --target green -# ./blue-green-switch.sh --env production --target blue --skip-health -# ./blue-green-switch.sh --help -# ───────────────────────────────────────────────────────────────────────────── -# @author Nguyễn Ngọc Trí Vĩ -set -euo pipefail - -readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -readonly APP_NAME="agent-monitor" -readonly APP_PORT=4820 - -# ── Colors & logging ─────────────────────────────────────────────────────── -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -CYAN='\033[0;36m' -BOLD='\033[1m' -NC='\033[0m' - -log() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $*"; } -info() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${BLUE}ℹ${NC} $*"; } -ok() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${GREEN}✔${NC} $*"; } -warn() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${YELLOW}⚠${NC} $*" >&2; } -err() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${RED}✖${NC} $*" >&2; } -fatal() { err "$@"; exit 1; } - -# ── Defaults ──────────────────────────────────────────────────────────────── -ENVIRONMENT="" -TARGET="" -NAMESPACE="" -SERVICE_NAME="${APP_NAME}" -SKIP_HEALTH_CHECK=false -DRY_RUN=false - -# ── Usage ─────────────────────────────────────────────────────────────────── -usage() { - cat < --target [options] - -${BOLD}Required:${NC} - --env, -e Environment: dev, staging, production - --target, -t Target slot: blue, green - -${BOLD}Options:${NC} - --namespace, -n Kubernetes namespace (default: agent-monitor-) - --service Service name (default: ${APP_NAME}) - --skip-health Skip health check on target before switching - --dry-run Show what would change without applying - --help, -h Show this help message - -${BOLD}Examples:${NC} - $(basename "$0") --env production --target green - $(basename "$0") --env production --target blue # instant rollback - -EOF - exit 0 -} - -# ── Argument parsing ──────────────────────────────────────────────────────── -parse_args() { - [[ $# -eq 0 ]] && usage - - while [[ $# -gt 0 ]]; do - case "$1" in - --env|-e) ENVIRONMENT="$2"; shift 2 ;; - --target|-t) TARGET="$2"; shift 2 ;; - --namespace|-n) NAMESPACE="$2"; shift 2 ;; - --service) SERVICE_NAME="$2"; shift 2 ;; - --skip-health) SKIP_HEALTH_CHECK=true; shift ;; - --dry-run) DRY_RUN=true; shift ;; - --help|-h) usage ;; - *) fatal "Unknown option: $1" ;; - esac - done - - [[ -z "$ENVIRONMENT" ]] && fatal "Missing required argument: --env" - [[ -z "$TARGET" ]] && fatal "Missing required argument: --target" - [[ -z "$NAMESPACE" ]] && NAMESPACE="agent-monitor-${ENVIRONMENT}" - - case "$TARGET" in - blue|green) ;; - *) fatal "Invalid target: $TARGET. Must be 'blue' or 'green'." ;; - esac -} - -# ── Detect current active slot ────────────────────────────────────────────── -detect_current_slot() { - local current - current=$(kubectl get svc "${SERVICE_NAME}" -n "${NAMESPACE}" \ - -o jsonpath='{.spec.selector.slot}' 2>/dev/null || echo "") - - if [[ -z "$current" ]]; then - # Try alternative label names - current=$(kubectl get svc "${SERVICE_NAME}" -n "${NAMESPACE}" \ - -o jsonpath='{.spec.selector.deployment-slot}' 2>/dev/null || echo "") - fi - - if [[ -z "$current" ]]; then - current=$(kubectl get svc "${SERVICE_NAME}" -n "${NAMESPACE}" \ - -o jsonpath='{.spec.selector.color}' 2>/dev/null || echo "unknown") - fi - - echo "$current" -} - -# ── Check target slot is healthy ──────────────────────────────────────────── -check_target_health() { - if [[ "$SKIP_HEALTH_CHECK" == true ]]; then - info "Skipping target health check (--skip-health)" - return 0 - fi - - info "Checking health of ${BOLD}${TARGET}${NC} slot..." - - # Verify pods exist and are ready - local ready_pods - ready_pods=$(kubectl get pods -n "${NAMESPACE}" \ - -l "app.kubernetes.io/name=${APP_NAME},slot=${TARGET}" \ - --field-selector=status.phase=Running \ - -o jsonpath='{.items[*].metadata.name}' 2>/dev/null || echo "") - - if [[ -z "$ready_pods" ]]; then - # Try with 'color' label - ready_pods=$(kubectl get pods -n "${NAMESPACE}" \ - -l "app.kubernetes.io/name=${APP_NAME},color=${TARGET}" \ - --field-selector=status.phase=Running \ - -o jsonpath='{.items[*].metadata.name}' 2>/dev/null || echo "") - fi - - if [[ -z "$ready_pods" ]]; then - # Try with 'deployment-slot' label - ready_pods=$(kubectl get pods -n "${NAMESPACE}" \ - -l "app.kubernetes.io/name=${APP_NAME},deployment-slot=${TARGET}" \ - --field-selector=status.phase=Running \ - -o jsonpath='{.items[*].metadata.name}' 2>/dev/null || echo "") - fi - - if [[ -z "$ready_pods" ]]; then - fatal "No running pods found for ${TARGET} slot. Deploy first." - fi - - info "Found running pods in ${TARGET} slot: ${ready_pods}" - - # Health check via port-forward to first pod - local first_pod - first_pod=$(echo "$ready_pods" | awk '{print $1}') - local local_port=14821 - - kubectl port-forward "pod/${first_pod}" "${local_port}:${APP_PORT}" -n "${NAMESPACE}" & - local pf_pid=$! - sleep 3 - - local healthy=false - for attempt in 1 2 3 4 5; do - if curl -sf --max-time 5 "http://localhost:${local_port}/api/health" | grep -q '"status":"ok"'; then - healthy=true - break - fi - info "Health check attempt ${attempt}/5..." - sleep 2 - done - - kill "$pf_pid" 2>/dev/null || true - - if [[ "$healthy" != true ]]; then - fatal "${TARGET} slot is NOT healthy. Aborting traffic switch." - fi - - ok "${TARGET} slot is healthy" -} - -# ── Switch traffic ────────────────────────────────────────────────────────── -switch_traffic() { - local current_slot - current_slot=$(detect_current_slot) - - info "Current active slot: ${BOLD}${current_slot}${NC}" - info "Switching to: ${BOLD}${TARGET}${NC}" - - if [[ "$current_slot" == "$TARGET" ]]; then - warn "Traffic is already pointing to ${TARGET}. Nothing to do." - return 0 - fi - - if [[ "$ENVIRONMENT" == "production" ]] && [[ "$DRY_RUN" == false ]]; then - echo "" - warn "Switching ${BOLD}PRODUCTION${NC} traffic from ${current_slot} → ${TARGET}" - read -r -p "$(echo -e "${YELLOW}Type 'yes' to confirm:${NC} ")" confirm - [[ "$confirm" == "yes" ]] || fatal "Switch cancelled." - fi - - # Build the patch – try common label conventions - local label_key="slot" - local current_labels - current_labels=$(kubectl get svc "${SERVICE_NAME}" -n "${NAMESPACE}" -o json 2>/dev/null) - - if echo "$current_labels" | grep -q '"color"'; then - label_key="color" - elif echo "$current_labels" | grep -q '"deployment-slot"'; then - label_key="deployment-slot" - fi - - local patch="{\"spec\":{\"selector\":{\"${label_key}\":\"${TARGET}\"}}}" - - if [[ "$DRY_RUN" == true ]]; then - info "[DRY-RUN] Would patch service '${SERVICE_NAME}' with:" - echo " ${patch}" - return - fi - - if ! kubectl patch svc "${SERVICE_NAME}" -n "${NAMESPACE}" -p "${patch}"; then - fatal "Failed to patch service selector!" - fi - - ok "Service '${SERVICE_NAME}' now routing to ${TARGET} slot" - - # Verify the switch - local new_slot - new_slot=$(detect_current_slot) - if [[ "$new_slot" != "$TARGET" ]]; then - err "Verification failed! Service selector shows: ${new_slot}" - warn "Attempting to revert to ${current_slot}..." - kubectl patch svc "${SERVICE_NAME}" -n "${NAMESPACE}" \ - -p "{\"spec\":{\"selector\":{\"${label_key}\":\"${current_slot}\"}}}" \ - && ok "Reverted to ${current_slot}" \ - || fatal "Revert failed! Manual intervention needed." - exit 1 - fi - - ok "Verified: traffic now routes to ${TARGET}" -} - -# ── Post-switch health check ─────────────────────────────────────────────── -post_switch_health() { - if [[ "$DRY_RUN" == true ]] || [[ "$SKIP_HEALTH_CHECK" == true ]]; then - return - fi - - info "Running post-switch health check via service..." - sleep 5 # Let connections drain - - if [[ -x "${SCRIPT_DIR}/health-check.sh" ]]; then - local local_port=14822 - kubectl port-forward "svc/${SERVICE_NAME}" "${local_port}:${APP_PORT}" -n "${NAMESPACE}" & - local pf_pid=$! - sleep 3 - - if "${SCRIPT_DIR}/health-check.sh" --url "http://localhost:${local_port}" --retries 5 --interval 3; then - ok "Post-switch health check passed" - else - warn "Post-switch health check failed – consider switching back!" - fi - - kill "$pf_pid" 2>/dev/null || true - fi -} - -# ── Main ──────────────────────────────────────────────────────────────────── -main() { - echo "" - echo -e "${BOLD}${BLUE}╔══════════════════════════════════════════════════╗${NC}" - echo -e "${BOLD}${BLUE}║ Claude Code Agent Monitor – Blue/Green Switch ║${NC}" - echo -e "${BOLD}${BLUE}╚══════════════════════════════════════════════════╝${NC}" - echo "" - - parse_args "$@" - - info "Configuration:" - echo -e " ${BOLD}Environment:${NC} ${ENVIRONMENT}" - echo -e " ${BOLD}Target slot:${NC} ${TARGET}" - echo -e " ${BOLD}Namespace:${NC} ${NAMESPACE}" - echo -e " ${BOLD}Service:${NC} ${SERVICE_NAME}" - echo "" - - check_target_health - switch_traffic - post_switch_health - - echo "" - ok "${BOLD}Blue-green switch complete!${NC}" - echo -e " ${BOLD}Active slot:${NC} ${TARGET}" - echo -e " ${BOLD}Timestamp:${NC} $(date -u +%Y-%m-%dT%H:%M:%SZ)" - echo -e " ${BOLD}Rollback:${NC} $(basename "$0") --env ${ENVIRONMENT} --target $([ "$TARGET" = "blue" ] && echo "green" || echo "blue")" - echo "" -} - -main "$@" diff --git a/deployments/scripts/db-backup.sh b/deployments/scripts/db-backup.sh deleted file mode 100755 index 720b513..0000000 --- a/deployments/scripts/db-backup.sh +++ /dev/null @@ -1,298 +0,0 @@ -#!/usr/bin/env bash -# ───────────────────────────────────────────────────────────────────────────── -# db-backup.sh – SQLite database backup for Claude Code Agent Monitor -# -# Usage: -# ./db-backup.sh --env production --output ./backups/ -# ./db-backup.sh --env production --output ./backups/ --upload s3://bucket/path -# ./db-backup.sh --help -# ───────────────────────────────────────────────────────────────────────────── -# @author Nguyễn Ngọc Trí Vĩ -set -euo pipefail - -readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -readonly APP_NAME="agent-monitor" -readonly DB_PATH_IN_CONTAINER="/app/data" - -# ── Colors & logging ─────────────────────────────────────────────────────── -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -CYAN='\033[0;36m' -BOLD='\033[1m' -NC='\033[0m' - -log() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $*"; } -info() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${BLUE}ℹ${NC} $*"; } -ok() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${GREEN}✔${NC} $*"; } -warn() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${YELLOW}⚠${NC} $*" >&2; } -err() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${RED}✖${NC} $*" >&2; } -fatal() { err "$@"; exit 1; } - -# ── Defaults ──────────────────────────────────────────────────────────────── -ENVIRONMENT="" -OUTPUT_DIR="" -NAMESPACE="" -UPLOAD_DEST="" -POD_NAME="" -DB_FILENAME="" -COMPRESS=true - -# ── Usage ─────────────────────────────────────────────────────────────────── -usage() { - cat < --output [options] - -${BOLD}Required:${NC} - --env, -e Environment: dev, staging, production - --output, -o Local directory for the backup file - -${BOLD}Options:${NC} - --namespace, -n Kubernetes namespace (default: agent-monitor-) - --pod Specific pod name to copy from (auto-detected if omitted) - --upload Upload backup to S3/GCS (e.g., s3://bucket/backups/) - --no-compress Skip gzip compression - --help, -h Show this help message - -${BOLD}Examples:${NC} - $(basename "$0") --env production --output ./backups/ - $(basename "$0") --env staging --output /tmp/backups --upload s3://my-bucket/db-backups/ - -EOF - exit 0 -} - -# ── Argument parsing ──────────────────────────────────────────────────────── -parse_args() { - [[ $# -eq 0 ]] && usage - - while [[ $# -gt 0 ]]; do - case "$1" in - --env|-e) ENVIRONMENT="$2"; shift 2 ;; - --output|-o) OUTPUT_DIR="$2"; shift 2 ;; - --namespace|-n) NAMESPACE="$2"; shift 2 ;; - --pod) POD_NAME="$2"; shift 2 ;; - --upload) UPLOAD_DEST="$2"; shift 2 ;; - --no-compress) COMPRESS=false; shift ;; - --help|-h) usage ;; - *) fatal "Unknown option: $1" ;; - esac - done - - [[ -z "$ENVIRONMENT" ]] && fatal "Missing required argument: --env" - [[ -z "$OUTPUT_DIR" ]] && fatal "Missing required argument: --output" - [[ -z "$NAMESPACE" ]] && NAMESPACE="agent-monitor-${ENVIRONMENT}" -} - -# ── Find target pod ──────────────────────────────────────────────────────── -find_pod() { - if [[ -n "$POD_NAME" ]]; then - info "Using specified pod: ${POD_NAME}" - return - fi - - info "Finding running pod in namespace '${NAMESPACE}'..." - - POD_NAME=$(kubectl get pods -n "${NAMESPACE}" \ - -l "app.kubernetes.io/name=${APP_NAME}" \ - --field-selector=status.phase=Running \ - -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "") - - if [[ -z "$POD_NAME" ]]; then - fatal "No running pods found for ${APP_NAME} in ${NAMESPACE}" - fi - - info "Selected pod: ${POD_NAME}" -} - -# ── Create backup ────────────────────────────────────────────────────────── -create_backup() { - local timestamp - timestamp=$(date -u +%Y%m%d_%H%M%S) - DB_FILENAME="${APP_NAME}_${ENVIRONMENT}_${timestamp}.db" - - # Create output directory - mkdir -p "${OUTPUT_DIR}" - - info "Creating backup..." - - # Use sqlite3 .backup inside the pod for a consistent snapshot - # This avoids copying a potentially locked/in-flight database - local remote_backup_path="/tmp/${DB_FILENAME}" - - info "Running SQLite backup inside pod (consistent snapshot)..." - if kubectl exec "${POD_NAME}" -n "${NAMESPACE}" -- \ - sh -c " - if command -v sqlite3 >/dev/null 2>&1; then - sqlite3 '${DB_PATH_IN_CONTAINER}/dashboard.db' '.backup ${remote_backup_path}' - else - cp '${DB_PATH_IN_CONTAINER}/dashboard.db' '${remote_backup_path}' - fi - " 2>/dev/null; then - ok "In-pod backup created at ${remote_backup_path}" - else - # Fallback: also copy WAL files if present - warn "sqlite3 not available in pod, falling back to file copy" - kubectl exec "${POD_NAME}" -n "${NAMESPACE}" -- \ - sh -c "cp '${DB_PATH_IN_CONTAINER}/dashboard.db' '${remote_backup_path}'" \ - || fatal "Failed to copy database file" - - # Try to also get WAL and SHM files - kubectl exec "${POD_NAME}" -n "${NAMESPACE}" -- \ - sh -c "cp '${DB_PATH_IN_CONTAINER}/dashboard.db-wal' '${remote_backup_path}-wal' 2>/dev/null || true" - kubectl exec "${POD_NAME}" -n "${NAMESPACE}" -- \ - sh -c "cp '${DB_PATH_IN_CONTAINER}/dashboard.db-shm' '${remote_backup_path}-shm' 2>/dev/null || true" - fi - - # Copy backup from pod to local - info "Copying backup to local filesystem..." - kubectl cp "${NAMESPACE}/${POD_NAME}:${remote_backup_path}" "${OUTPUT_DIR}/${DB_FILENAME}" \ - || fatal "Failed to copy backup from pod" - - # Copy WAL if it exists - kubectl cp "${NAMESPACE}/${POD_NAME}:${remote_backup_path}-wal" "${OUTPUT_DIR}/${DB_FILENAME}-wal" 2>/dev/null || true - - # Cleanup remote temp file - kubectl exec "${POD_NAME}" -n "${NAMESPACE}" -- \ - sh -c "rm -f '${remote_backup_path}' '${remote_backup_path}-wal' '${remote_backup_path}-shm'" 2>/dev/null || true - - local file_size - file_size=$(du -sh "${OUTPUT_DIR}/${DB_FILENAME}" 2>/dev/null | awk '{print $1}') - ok "Backup saved: ${OUTPUT_DIR}/${DB_FILENAME} (${file_size})" -} - -# ── Validate backup integrity ────────────────────────────────────────────── -validate_backup() { - info "Validating backup integrity..." - - local db_file="${OUTPUT_DIR}/${DB_FILENAME}" - - if ! command -v sqlite3 &>/dev/null; then - warn "sqlite3 not found locally – skipping integrity check" - return - fi - - # Check integrity - local integrity - integrity=$(sqlite3 "${db_file}" "PRAGMA integrity_check;" 2>/dev/null || echo "error") - - if [[ "$integrity" == "ok" ]]; then - ok "SQLite integrity check: OK" - else - err "SQLite integrity check failed: ${integrity}" - warn "Backup may be corrupted – consider re-running the backup" - return 1 - fi - - # Show basic stats - local table_count - table_count=$(sqlite3 "${db_file}" "SELECT count(*) FROM sqlite_master WHERE type='table';" 2>/dev/null || echo "?") - local page_count - page_count=$(sqlite3 "${db_file}" "PRAGMA page_count;" 2>/dev/null || echo "?") - local page_size - page_size=$(sqlite3 "${db_file}" "PRAGMA page_size;" 2>/dev/null || echo "?") - - info "Database stats: ${table_count} tables, ${page_count} pages × ${page_size} bytes" -} - -# ── Compress backup ──────────────────────────────────────────────────────── -compress_backup() { - if [[ "$COMPRESS" == false ]]; then - info "Compression skipped" - return - fi - - local db_file="${OUTPUT_DIR}/${DB_FILENAME}" - - if ! command -v gzip &>/dev/null; then - warn "gzip not available – skipping compression" - return - fi - - info "Compressing backup..." - gzip -k "${db_file}" - DB_FILENAME="${DB_FILENAME}.gz" - - local compressed_size - compressed_size=$(du -sh "${OUTPUT_DIR}/${DB_FILENAME}" 2>/dev/null | awk '{print $1}') - ok "Compressed: ${OUTPUT_DIR}/${DB_FILENAME} (${compressed_size})" -} - -# ── Upload to cloud storage ──────────────────────────────────────────────── -upload_backup() { - if [[ -z "$UPLOAD_DEST" ]]; then - return - fi - - local backup_file="${OUTPUT_DIR}/${DB_FILENAME}" - - info "Uploading backup to ${UPLOAD_DEST}..." - - if [[ "$UPLOAD_DEST" == s3://* ]]; then - if ! command -v aws &>/dev/null; then - fatal "AWS CLI not found. Install it to upload to S3." - fi - aws s3 cp "${backup_file}" "${UPLOAD_DEST}${DB_FILENAME}" \ - --storage-class STANDARD_IA \ - || fatal "S3 upload failed" - ok "Uploaded to ${UPLOAD_DEST}${DB_FILENAME}" - - elif [[ "$UPLOAD_DEST" == gs://* ]]; then - if ! command -v gsutil &>/dev/null; then - fatal "gsutil not found. Install Google Cloud SDK to upload to GCS." - fi - gsutil cp "${backup_file}" "${UPLOAD_DEST}${DB_FILENAME}" \ - || fatal "GCS upload failed" - ok "Uploaded to ${UPLOAD_DEST}${DB_FILENAME}" - - elif [[ "$UPLOAD_DEST" == az://* ]] || [[ "$UPLOAD_DEST" == https://*.blob.core.windows.net/* ]]; then - if ! command -v az &>/dev/null; then - fatal "Azure CLI not found. Install it to upload to Azure Blob." - fi - local container_url="${UPLOAD_DEST}" - az storage blob upload \ - --file "${backup_file}" \ - --name "${DB_FILENAME}" \ - --overwrite \ - || fatal "Azure Blob upload failed" - ok "Uploaded to Azure Blob Storage" - - else - warn "Unknown upload destination scheme: ${UPLOAD_DEST}" - warn "Supported: s3://, gs://, az://" - fi -} - -# ── Main ──────────────────────────────────────────────────────────────────── -main() { - echo "" - echo -e "${BOLD}${BLUE}╔══════════════════════════════════════════════════╗${NC}" - echo -e "${BOLD}${BLUE}║ Claude Code Agent Monitor – DB Backup ║${NC}" - echo -e "${BOLD}${BLUE}╚══════════════════════════════════════════════════╝${NC}" - echo "" - - parse_args "$@" - - info "Configuration:" - echo -e " ${BOLD}Environment:${NC} ${ENVIRONMENT}" - echo -e " ${BOLD}Namespace:${NC} ${NAMESPACE}" - echo -e " ${BOLD}Output:${NC} ${OUTPUT_DIR}" - [[ -n "$UPLOAD_DEST" ]] && echo -e " ${BOLD}Upload:${NC} ${UPLOAD_DEST}" - echo "" - - find_pod - create_backup - validate_backup - compress_backup - upload_backup - - echo "" - ok "${BOLD}Backup complete!${NC}" - echo -e " ${BOLD}File:${NC} ${OUTPUT_DIR}/${DB_FILENAME}" - echo -e " ${BOLD}Timestamp:${NC} $(date -u +%Y-%m-%dT%H:%M:%SZ)" - echo "" -} - -main "$@" diff --git a/deployments/scripts/db-restore.sh b/deployments/scripts/db-restore.sh deleted file mode 100755 index 8948a39..0000000 --- a/deployments/scripts/db-restore.sh +++ /dev/null @@ -1,396 +0,0 @@ -#!/usr/bin/env bash -# ───────────────────────────────────────────────────────────────────────────── -# db-restore.sh – SQLite database restore for Claude Code Agent Monitor -# -# Usage: -# ./db-restore.sh --env production --input ./backups/agent-monitor_production_20240101_120000.db -# ./db-restore.sh --help -# ───────────────────────────────────────────────────────────────────────────── -# @author Nguyễn Ngọc Trí Vĩ -set -euo pipefail - -readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -readonly APP_NAME="agent-monitor" -readonly APP_PORT=4820 -readonly DB_PATH_IN_CONTAINER="/app/data" - -# ── Colors & logging ─────────────────────────────────────────────────────── -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -CYAN='\033[0;36m' -BOLD='\033[1m' -NC='\033[0m' - -log() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $*"; } -info() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${BLUE}ℹ${NC} $*"; } -ok() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${GREEN}✔${NC} $*"; } -warn() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${YELLOW}⚠${NC} $*" >&2; } -err() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${RED}✖${NC} $*" >&2; } -fatal() { err "$@"; exit 1; } - -# ── Defaults ──────────────────────────────────────────────────────────────── -ENVIRONMENT="" -INPUT_FILE="" -NAMESPACE="" -SKIP_HEALTH_CHECK=false -FORCE=false -BACKUP_BEFORE_RESTORE=true - -# ── Usage ─────────────────────────────────────────────────────────────────── -usage() { - cat < --input [options] - -${BOLD}Required:${NC} - --env, -e Environment: dev, staging, production - --input, -i Path to backup file (.db or .db.gz) - -${BOLD}Options:${NC} - --namespace, -n Kubernetes namespace (default: agent-monitor-) - --no-backup Skip backing up current DB before restore - --skip-health Skip post-restore health check - --force Skip confirmation prompt - --help, -h Show this help message - -${BOLD}Examples:${NC} - $(basename "$0") --env production --input ./backups/agent-monitor_production_20240101_120000.db - $(basename "$0") --env staging --input ./backups/backup.db.gz --force - -EOF - exit 0 -} - -# ── Argument parsing ──────────────────────────────────────────────────────── -parse_args() { - [[ $# -eq 0 ]] && usage - - while [[ $# -gt 0 ]]; do - case "$1" in - --env|-e) ENVIRONMENT="$2"; shift 2 ;; - --input|-i) INPUT_FILE="$2"; shift 2 ;; - --namespace|-n) NAMESPACE="$2"; shift 2 ;; - --no-backup) BACKUP_BEFORE_RESTORE=false; shift ;; - --skip-health) SKIP_HEALTH_CHECK=true; shift ;; - --force) FORCE=true; shift ;; - --help|-h) usage ;; - *) fatal "Unknown option: $1" ;; - esac - done - - [[ -z "$ENVIRONMENT" ]] && fatal "Missing required argument: --env" - [[ -z "$INPUT_FILE" ]] && fatal "Missing required argument: --input" - [[ -z "$NAMESPACE" ]] && NAMESPACE="agent-monitor-${ENVIRONMENT}" - - # Validate input file exists - [[ -f "$INPUT_FILE" ]] || fatal "Input file not found: ${INPUT_FILE}" -} - -# ── Validate backup file ─────────────────────────────────────────────────── -validate_input() { - info "Validating input file: ${INPUT_FILE}" - - local restore_file="${INPUT_FILE}" - - # Decompress if needed - if [[ "$INPUT_FILE" == *.gz ]]; then - info "Decompressing gzipped backup..." - restore_file="${INPUT_FILE%.gz}" - if [[ -f "$restore_file" ]]; then - warn "Decompressed file already exists: ${restore_file}" - else - gzip -dk "${INPUT_FILE}" || fatal "Failed to decompress ${INPUT_FILE}" - fi - fi - - RESTORE_FILE="$restore_file" - - # Validate with sqlite3 if available - if command -v sqlite3 &>/dev/null; then - local integrity - integrity=$(sqlite3 "${RESTORE_FILE}" "PRAGMA integrity_check;" 2>/dev/null || echo "error") - if [[ "$integrity" == "ok" ]]; then - ok "SQLite integrity check passed" - else - fatal "Input file failed integrity check: ${integrity}" - fi - - local table_count - table_count=$(sqlite3 "${RESTORE_FILE}" "SELECT count(*) FROM sqlite_master WHERE type='table';" 2>/dev/null || echo "?") - info "Backup contains ${table_count} tables" - else - warn "sqlite3 not available – skipping integrity check" - # Basic file header check - local header - header=$(head -c 16 "${RESTORE_FILE}" | strings 2>/dev/null || echo "") - if echo "$header" | grep -q "SQLite format"; then - ok "File appears to be a valid SQLite database" - else - fatal "File does not appear to be a SQLite database" - fi - fi - - local file_size - file_size=$(du -sh "${RESTORE_FILE}" 2>/dev/null | awk '{print $1}') - info "Restore file size: ${file_size}" -} - -# ── Safety confirmation ───────────────────────────────────────────────────── -confirm_restore() { - if [[ "$FORCE" == true ]]; then - return - fi - - echo "" - warn "${BOLD}⚠ DATABASE RESTORE WARNING ⚠${NC}" - echo "" - echo -e " This will ${RED}${BOLD}REPLACE${NC} the current database in ${BOLD}${ENVIRONMENT}${NC}" - echo -e " with the contents of: ${INPUT_FILE}" - echo "" - echo -e " The deployment will be ${BOLD}scaled down${NC} during restore." - echo "" - - if [[ "$ENVIRONMENT" == "production" ]]; then - echo -e " ${RED}${BOLD}THIS IS A PRODUCTION ENVIRONMENT!${NC}" - echo "" - read -r -p "$(echo -e "${YELLOW}Type the environment name to confirm:${NC} ")" confirm - [[ "$confirm" == "$ENVIRONMENT" ]] || fatal "Restore cancelled. You typed '${confirm}', expected '${ENVIRONMENT}'." - else - read -r -p "$(echo -e "${YELLOW}Type 'yes' to confirm:${NC} ")" confirm - [[ "$confirm" == "yes" ]] || fatal "Restore cancelled." - fi -} - -# ── Get deployment info ───────────────────────────────────────────────────── -get_deployment_info() { - info "Getting deployment info..." - - DEPLOYMENT_NAME=$(kubectl get deployment -n "${NAMESPACE}" \ - -l "app.kubernetes.io/name=${APP_NAME}" \ - -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "") - - if [[ -z "$DEPLOYMENT_NAME" ]]; then - fatal "No deployment found for ${APP_NAME} in ${NAMESPACE}" - fi - - ORIGINAL_REPLICAS=$(kubectl get deployment "${DEPLOYMENT_NAME}" -n "${NAMESPACE}" \ - -o jsonpath='{.spec.replicas}' 2>/dev/null || echo "1") - - info "Deployment: ${DEPLOYMENT_NAME} (${ORIGINAL_REPLICAS} replicas)" -} - -# ── Backup current DB before restore ─────────────────────────────────────── -backup_current() { - if [[ "$BACKUP_BEFORE_RESTORE" == false ]]; then - info "Skipping pre-restore backup (--no-backup)" - return - fi - - info "Backing up current database before restore..." - - if [[ -x "${SCRIPT_DIR}/db-backup.sh" ]]; then - local backup_dir="${SCRIPT_DIR}/../../data/pre-restore-backups" - "${SCRIPT_DIR}/db-backup.sh" \ - --env "${ENVIRONMENT}" \ - --output "${backup_dir}" \ - --namespace "${NAMESPACE}" \ - --no-compress \ - && ok "Pre-restore backup created in ${backup_dir}" \ - || warn "Pre-restore backup failed – proceeding anyway" - else - warn "db-backup.sh not found – skipping pre-restore backup" - fi -} - -# ── Scale down deployment ─────────────────────────────────────────────────── -scale_down() { - info "Scaling down deployment to 0 replicas..." - - kubectl scale deployment "${DEPLOYMENT_NAME}" \ - --replicas=0 \ - -n "${NAMESPACE}" \ - || fatal "Failed to scale down deployment" - - # Wait for all pods to terminate - info "Waiting for pods to terminate..." - local wait_count=0 - while [[ $wait_count -lt 60 ]]; do - local running - running=$(kubectl get pods -n "${NAMESPACE}" \ - -l "app.kubernetes.io/name=${APP_NAME}" \ - --field-selector=status.phase=Running \ - --no-headers 2>/dev/null | wc -l | tr -d ' ') - - if [[ "$running" == "0" ]]; then - ok "All pods terminated" - return - fi - - wait_count=$((wait_count + 1)) - sleep 2 - done - - warn "Pods did not terminate within timeout" -} - -# ── Restore database ─────────────────────────────────────────────────────── -restore_database() { - info "Restoring database..." - - # We need a temporary pod to access the PVC - # Create a helper pod that mounts the PVC - local helper_pod="${APP_NAME}-db-restore-helper" - - # Get PVC name - local pvc_name - pvc_name=$(kubectl get pvc -n "${NAMESPACE}" \ - -l "app.kubernetes.io/name=${APP_NAME}" \ - -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "agent-monitor-data") - - info "Creating helper pod to access PVC: ${pvc_name}" - - kubectl apply -n "${NAMESPACE}" -f - </dev/null; then - fatal "Pods did not become ready after restore!" - fi - - if [[ -x "${SCRIPT_DIR}/health-check.sh" ]]; then - local local_port=14823 - kubectl port-forward "svc/${APP_NAME}" "${local_port}:${APP_PORT}" -n "${NAMESPACE}" & - local pf_pid=$! - sleep 3 - - if "${SCRIPT_DIR}/health-check.sh" --url "http://localhost:${local_port}" --retries 10 --interval 3; then - ok "Post-restore health check passed" - else - err "Post-restore health check failed!" - warn "The application may need manual investigation" - fi - - kill "$pf_pid" 2>/dev/null || true - else - ok "Pods are ready" - fi -} - -# ── Main ──────────────────────────────────────────────────────────────────── -main() { - echo "" - echo -e "${BOLD}${YELLOW}╔══════════════════════════════════════════════════╗${NC}" - echo -e "${BOLD}${YELLOW}║ Claude Code Agent Monitor – DB Restore ║${NC}" - echo -e "${BOLD}${YELLOW}╚══════════════════════════════════════════════════╝${NC}" - echo "" - - parse_args "$@" - validate_input - confirm_restore - get_deployment_info - backup_current - scale_down - restore_database - scale_up - run_health_check - - echo "" - ok "${BOLD}Database restore complete!${NC}" - echo -e " ${BOLD}Environment:${NC} ${ENVIRONMENT}" - echo -e " ${BOLD}Source:${NC} ${INPUT_FILE}" - echo -e " ${BOLD}Timestamp:${NC} $(date -u +%Y-%m-%dT%H:%M:%SZ)" - echo "" -} - -main "$@" diff --git a/deployments/scripts/deploy.sh b/deployments/scripts/deploy.sh deleted file mode 100755 index a47049c..0000000 --- a/deployments/scripts/deploy.sh +++ /dev/null @@ -1,572 +0,0 @@ -#!/usr/bin/env bash -# ───────────────────────────────────────────────────────────────────────────── -# deploy.sh – Main deployment orchestrator for Claude Code Agent Monitor -# -# Usage: -# ./deploy.sh --env dev|staging|production --method helm|kustomize|terraform -# ./deploy.sh --env production --method helm --strategy blue-green|canary|rolling -# ./deploy.sh --env staging --method helm --dry-run -# ./deploy.sh --help -# ───────────────────────────────────────────────────────────────────────────── -# @author Nguyễn Ngọc Trí Vĩ -set -euo pipefail - -# ── Constants ─────────────────────────────────────────────────────────────── -readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -readonly PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" -readonly DEPLOY_DIR="${PROJECT_ROOT}/deployments" -readonly APP_NAME="agent-monitor" -readonly APP_PORT=4820 -readonly DEFAULT_REGISTRY="ghcr.io" -readonly DEFAULT_IMAGE_NAME="claude-code-agent-monitor" - -# ── Colors & logging ─────────────────────────────────────────────────────── -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -CYAN='\033[0;36m' -BOLD='\033[1m' -NC='\033[0m' - -log() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $*"; } -info() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${BLUE}ℹ${NC} $*"; } -ok() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${GREEN}✔${NC} $*"; } -warn() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${YELLOW}⚠${NC} $*" >&2; } -err() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${RED}✖${NC} $*" >&2; } -fatal() { err "$@"; exit 1; } - -banner() { - echo "" - echo -e "${BOLD}${BLUE}╔══════════════════════════════════════════════════╗${NC}" - echo -e "${BOLD}${BLUE}║ Claude Code Agent Monitor – Deploy ║${NC}" - echo -e "${BOLD}${BLUE}╚══════════════════════════════════════════════════╝${NC}" - echo "" -} - -# ── Default parameter values ──────────────────────────────────────────────── -ENVIRONMENT="" -METHOD="" -STRATEGY="rolling" -DRY_RUN=false -IMAGE_TAG="" -REGISTRY="${DOCKER_REGISTRY:-$DEFAULT_REGISTRY}" -IMAGE_NAME="${DOCKER_IMAGE_NAME:-$DEFAULT_IMAGE_NAME}" -NAMESPACE="" -HELM_RELEASE="${APP_NAME}" -HELM_CHART_DIR="${DEPLOY_DIR}/helm/agent-monitor" -KUBE_CONTEXT="" -SKIP_BUILD=false -SKIP_HEALTH_CHECK=false -HEALTH_CHECK_RETRIES=30 -HEALTH_CHECK_INTERVAL=5 -VALUES_FILE="" - -# ── Usage ─────────────────────────────────────────────────────────────────── -usage() { - cat < --method [options] - -${BOLD}Required:${NC} - --env, -e Environment: dev, staging, production - --method, -m Deployment method: helm, kustomize, terraform - -${BOLD}Options:${NC} - --strategy, -s Deployment strategy: rolling (default), blue-green, canary - --tag, -t Docker image tag (default: git SHA) - --registry Container registry (default: ${DEFAULT_REGISTRY}) - --image Image name (default: ${DEFAULT_IMAGE_NAME}) - --namespace, -n Kubernetes namespace (default: agent-monitor-) - --release Helm release name (default: ${APP_NAME}) - --context Kubernetes context to use - --values Additional Helm values file - --skip-build Skip container image build/push - --skip-health Skip post-deploy health check - --dry-run Preview changes without applying - --help, -h Show this help message - -${BOLD}Examples:${NC} - $(basename "$0") --env dev --method helm - $(basename "$0") --env production --method helm --strategy blue-green --tag v1.2.3 - $(basename "$0") --env staging --method kustomize --dry-run - $(basename "$0") --env production --method terraform - -EOF - exit 0 -} - -# ── Argument parsing ──────────────────────────────────────────────────────── -parse_args() { - [[ $# -eq 0 ]] && usage - - while [[ $# -gt 0 ]]; do - case "$1" in - --env|-e) ENVIRONMENT="$2"; shift 2 ;; - --method|-m) METHOD="$2"; shift 2 ;; - --strategy|-s) STRATEGY="$2"; shift 2 ;; - --tag|-t) IMAGE_TAG="$2"; shift 2 ;; - --registry) REGISTRY="$2"; shift 2 ;; - --image) IMAGE_NAME="$2"; shift 2 ;; - --namespace|-n) NAMESPACE="$2"; shift 2 ;; - --release) HELM_RELEASE="$2"; shift 2 ;; - --context) KUBE_CONTEXT="$2"; shift 2 ;; - --values) VALUES_FILE="$2"; shift 2 ;; - --skip-build) SKIP_BUILD=true; shift ;; - --skip-health) SKIP_HEALTH_CHECK=true; shift ;; - --dry-run) DRY_RUN=true; shift ;; - --help|-h) usage ;; - *) fatal "Unknown option: $1. Use --help for usage." ;; - esac - done -} - -# ── Validation ────────────────────────────────────────────────────────────── -validate_args() { - [[ -z "$ENVIRONMENT" ]] && fatal "Missing required argument: --env" - [[ -z "$METHOD" ]] && fatal "Missing required argument: --method" - - case "$ENVIRONMENT" in - dev|staging|production) ;; - *) fatal "Invalid environment: $ENVIRONMENT. Must be dev, staging, or production." ;; - esac - - case "$METHOD" in - helm|kustomize|terraform) ;; - *) fatal "Invalid method: $METHOD. Must be helm, kustomize, or terraform." ;; - esac - - case "$STRATEGY" in - rolling|blue-green|canary) ;; - *) fatal "Invalid strategy: $STRATEGY. Must be rolling, blue-green, or canary." ;; - esac - - # Default namespace - [[ -z "$NAMESPACE" ]] && NAMESPACE="agent-monitor-${ENVIRONMENT}" - - # Default image tag from git - if [[ -z "$IMAGE_TAG" ]]; then - IMAGE_TAG="$(git -C "$PROJECT_ROOT" rev-parse --short HEAD 2>/dev/null || echo 'latest')" - fi - - readonly FULL_IMAGE="${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}" - readonly MCP_IMAGE="${REGISTRY}/${IMAGE_NAME}-mcp:${IMAGE_TAG}" -} - -# ── Prerequisite checks ──────────────────────────────────────────────────── -check_prerequisites() { - info "Checking prerequisites..." - - local missing=() - - # Always need docker for building - if [[ "$SKIP_BUILD" == false ]]; then - command -v docker &>/dev/null || missing+=("docker") - fi - - case "$METHOD" in - helm) - command -v kubectl &>/dev/null || missing+=("kubectl") - command -v helm &>/dev/null || missing+=("helm") - ;; - kustomize) - command -v kubectl &>/dev/null || missing+=("kubectl") - command -v kustomize &>/dev/null || missing+=("kustomize") - ;; - terraform) - command -v terraform &>/dev/null || missing+=("terraform") - ;; - esac - - if [[ ${#missing[@]} -gt 0 ]]; then - fatal "Missing required tools: ${missing[*]}. Please install them and retry." - fi - - # Validate kube context if specified - if [[ -n "$KUBE_CONTEXT" ]] && [[ "$METHOD" != "terraform" ]]; then - if ! kubectl config get-contexts "$KUBE_CONTEXT" &>/dev/null; then - fatal "Kubernetes context '$KUBE_CONTEXT' not found." - fi - kubectl config use-context "$KUBE_CONTEXT" - fi - - # Validate Helm chart exists - if [[ "$METHOD" == "helm" ]] && [[ ! -f "${HELM_CHART_DIR}/Chart.yaml" ]]; then - fatal "Helm chart not found at ${HELM_CHART_DIR}" - fi - - ok "All prerequisites satisfied" -} - -# ── Build & push container images ─────────────────────────────────────────── -build_and_push() { - if [[ "$SKIP_BUILD" == true ]]; then - info "Skipping image build (--skip-build)" - return - fi - - info "Building container images..." - - local docker_cmd="docker build" - local push_cmd="docker push" - - if [[ "$DRY_RUN" == true ]]; then - info "[DRY-RUN] Would build: ${FULL_IMAGE}" - info "[DRY-RUN] Would build: ${MCP_IMAGE}" - return - fi - - # Build main application image - log "Building main app image: ${FULL_IMAGE}" - docker build \ - --file "${PROJECT_ROOT}/Dockerfile" \ - --tag "${FULL_IMAGE}" \ - --label "org.opencontainers.image.revision=$(git -C "$PROJECT_ROOT" rev-parse HEAD 2>/dev/null || echo 'unknown')" \ - --label "org.opencontainers.image.created=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ - "${PROJECT_ROOT}" - ok "Main app image built: ${FULL_IMAGE}" - - # Build MCP sidecar image - log "Building MCP sidecar image: ${MCP_IMAGE}" - docker build \ - --file "${PROJECT_ROOT}/mcp/Dockerfile" \ - --tag "${MCP_IMAGE}" \ - "${PROJECT_ROOT}" - ok "MCP sidecar image built: ${MCP_IMAGE}" - - # Push images - log "Pushing images to registry..." - docker push "${FULL_IMAGE}" - docker push "${MCP_IMAGE}" - ok "Images pushed to ${REGISTRY}" -} - -# ── Helm deployment ───────────────────────────────────────────────────────── -deploy_helm() { - info "Deploying via Helm (strategy: ${STRATEGY})..." - - local helm_args=( - upgrade --install "${HELM_RELEASE}" "${HELM_CHART_DIR}" - --namespace "${NAMESPACE}" - --create-namespace - --set "image.repository=${REGISTRY}/${IMAGE_NAME}" - --set "image.tag=${IMAGE_TAG}" - --set "environment=${ENVIRONMENT}" - --set "mcp.image.repository=${REGISTRY}/${IMAGE_NAME}-mcp" - --set "mcp.image.tag=${IMAGE_TAG}" - --timeout 600s - --wait - --atomic - ) - - # Environment-specific values - local env_values="${HELM_CHART_DIR}/values-${ENVIRONMENT}.yaml" - if [[ -f "$env_values" ]]; then - helm_args+=(--values "$env_values") - fi - - # User-provided values file - if [[ -n "$VALUES_FILE" ]] && [[ -f "$VALUES_FILE" ]]; then - helm_args+=(--values "$VALUES_FILE") - fi - - # Strategy-specific settings - case "$STRATEGY" in - blue-green) - helm_args+=(--set "strategy.type=blue-green") - ;; - canary) - helm_args+=(--set "strategy.type=canary") - helm_args+=(--set "strategy.canary.weight=10") - ;; - rolling) - helm_args+=(--set "strategy.type=rolling") - helm_args+=(--set "strategy.rolling.maxUnavailable=25%") - helm_args+=(--set "strategy.rolling.maxSurge=25%") - ;; - esac - - if [[ "$DRY_RUN" == true ]]; then - helm_args+=(--dry-run --debug) - info "[DRY-RUN] Helm command:" - echo " helm ${helm_args[*]}" - helm "${helm_args[@]}" 2>&1 | head -100 - return - fi - - # Store current revision for rollback - local current_revision - current_revision=$(helm history "${HELM_RELEASE}" -n "${NAMESPACE}" --max 1 -o json 2>/dev/null \ - | grep -o '"revision":[0-9]*' | head -1 | cut -d: -f2 || echo "0") - info "Current Helm revision: ${current_revision}" - - # Execute deployment - if ! helm "${helm_args[@]}"; then - err "Helm deployment failed!" - if [[ "$current_revision" != "0" ]]; then - warn "Attempting auto-rollback to revision ${current_revision}..." - helm rollback "${HELM_RELEASE}" "${current_revision}" -n "${NAMESPACE}" --wait --timeout 300s \ - && ok "Auto-rollback to revision ${current_revision} succeeded" \ - || fatal "Auto-rollback also failed! Manual intervention required." - fi - exit 1 - fi - - ok "Helm deployment succeeded" -} - -# ── Kustomize deployment ─────────────────────────────────────────────────── -deploy_kustomize() { - info "Deploying via Kustomize (overlay: ${ENVIRONMENT})..." - - local overlay_dir="${DEPLOY_DIR}/kubernetes/overlays/${ENVIRONMENT}" - if [[ ! -d "$overlay_dir" ]]; then - fatal "Kustomize overlay not found at ${overlay_dir}" - fi - - # Set image in kustomization - local kustomize_cmd="kubectl apply -k ${overlay_dir}" - if [[ "$DRY_RUN" == true ]]; then - info "[DRY-RUN] Kustomize output:" - kubectl kustomize "${overlay_dir}" | head -100 - return - fi - - # Update image reference using kustomize edit - pushd "${overlay_dir}" > /dev/null - kustomize edit set image "${APP_NAME}=${FULL_IMAGE}" 2>/dev/null || true - popd > /dev/null - - # Apply with server-side apply for safety - if ! kubectl apply -k "${overlay_dir}" --server-side --force-conflicts; then - err "Kustomize deployment failed!" - warn "Run: kubectl rollout undo deployment/${APP_NAME} -n ${NAMESPACE}" - exit 1 - fi - - # Wait for rollout - info "Waiting for rollout to complete..." - if ! kubectl rollout status "deployment/${APP_NAME}" -n "${NAMESPACE}" --timeout=600s; then - err "Rollout did not complete in time!" - warn "Attempting auto-rollback..." - kubectl rollout undo "deployment/${APP_NAME}" -n "${NAMESPACE}" \ - && ok "Auto-rollback succeeded" \ - || fatal "Auto-rollback failed! Manual intervention required." - exit 1 - fi - - ok "Kustomize deployment succeeded" -} - -# ── Terraform deployment ─────────────────────────────────────────────────── -deploy_terraform() { - info "Deploying via Terraform (environment: ${ENVIRONMENT})..." - - local tf_dir="${DEPLOY_DIR}/terraform" - local env_vars_file="${tf_dir}/environments/${ENVIRONMENT}/terraform.tfvars" - - if [[ ! -d "$tf_dir" ]]; then - fatal "Terraform directory not found at ${tf_dir}" - fi - - pushd "${tf_dir}" > /dev/null - - # Initialize - info "Running terraform init..." - terraform init -input=false - - # Plan - local plan_args=(-input=false -out=tfplan) - if [[ -f "$env_vars_file" ]]; then - plan_args+=(-var-file="$env_vars_file") - fi - plan_args+=(-var "app_container_image=${FULL_IMAGE}") - plan_args+=(-var "environment=${ENVIRONMENT}") - - info "Running terraform plan..." - terraform plan "${plan_args[@]}" - - if [[ "$DRY_RUN" == true ]]; then - info "[DRY-RUN] Terraform plan complete. Skipping apply." - rm -f tfplan - popd > /dev/null - return - fi - - # Apply - info "Applying terraform plan..." - if ! terraform apply -input=false tfplan; then - err "Terraform apply failed!" - fatal "Review state and run 'terraform plan' to diagnose." - fi - - rm -f tfplan - popd > /dev/null - - ok "Terraform deployment succeeded" -} - -# ── Post-deployment health check ─────────────────────────────────────────── -run_health_check() { - if [[ "$SKIP_HEALTH_CHECK" == true ]] || [[ "$DRY_RUN" == true ]]; then - info "Skipping health check" - return - fi - - info "Running post-deployment health check..." - - # Determine health check URL - local health_url="" - - if [[ "$METHOD" == "terraform" ]]; then - info "For Terraform deployments, verify health via the load balancer URL in terraform output." - return - fi - - # Try to get service URL from cluster - local svc_type - svc_type=$(kubectl get svc "${APP_NAME}" -n "${NAMESPACE}" -o jsonpath='{.spec.type}' 2>/dev/null || echo "") - - case "$svc_type" in - LoadBalancer) - local lb_host - lb_host=$(kubectl get svc "${APP_NAME}" -n "${NAMESPACE}" \ - -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' 2>/dev/null || echo "") - [[ -z "$lb_host" ]] && lb_host=$(kubectl get svc "${APP_NAME}" -n "${NAMESPACE}" \ - -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || echo "") - [[ -n "$lb_host" ]] && health_url="http://${lb_host}:${APP_PORT}/api/health" - ;; - NodePort) - local node_port - node_port=$(kubectl get svc "${APP_NAME}" -n "${NAMESPACE}" \ - -o jsonpath='{.spec.ports[0].nodePort}' 2>/dev/null || echo "") - [[ -n "$node_port" ]] && health_url="http://localhost:${node_port}/api/health" - ;; - *) - # Use port-forward for ClusterIP - info "Service type is ClusterIP – using kubectl port-forward for health check" - local local_port=14820 - kubectl port-forward "svc/${APP_NAME}" "${local_port}:${APP_PORT}" -n "${NAMESPACE}" & - local pf_pid=$! - sleep 3 - health_url="http://localhost:${local_port}/api/health" - ;; - esac - - if [[ -z "$health_url" ]]; then - warn "Could not determine health check URL. Checking pod readiness instead." - if kubectl wait --for=condition=ready pod -l "app.kubernetes.io/name=${APP_NAME}" \ - -n "${NAMESPACE}" --timeout=120s; then - ok "Pods are ready" - else - err "Pods did not become ready" - trigger_auto_rollback - fi - return - fi - - # Run health check script - if [[ -x "${SCRIPT_DIR}/health-check.sh" ]]; then - if ! "${SCRIPT_DIR}/health-check.sh" \ - --url "${health_url}" \ - --retries "${HEALTH_CHECK_RETRIES}" \ - --interval "${HEALTH_CHECK_INTERVAL}"; then - err "Health check failed after deployment!" - # Kill port-forward if running - [[ -n "${pf_pid:-}" ]] && kill "$pf_pid" 2>/dev/null || true - trigger_auto_rollback - fi - else - # Inline health check - local attempt=0 - while [[ $attempt -lt $HEALTH_CHECK_RETRIES ]]; do - if curl -sf --max-time 5 "${health_url}" | grep -q '"status":"ok"'; then - ok "Health check passed" - [[ -n "${pf_pid:-}" ]] && kill "$pf_pid" 2>/dev/null || true - return - fi - attempt=$((attempt + 1)) - info "Health check attempt ${attempt}/${HEALTH_CHECK_RETRIES}..." - sleep "${HEALTH_CHECK_INTERVAL}" - done - err "Health check failed after ${HEALTH_CHECK_RETRIES} attempts!" - [[ -n "${pf_pid:-}" ]] && kill "$pf_pid" 2>/dev/null || true - trigger_auto_rollback - fi - - # Cleanup port-forward - [[ -n "${pf_pid:-}" ]] && kill "$pf_pid" 2>/dev/null || true -} - -trigger_auto_rollback() { - warn "Triggering auto-rollback..." - case "$METHOD" in - helm) - helm rollback "${HELM_RELEASE}" -n "${NAMESPACE}" --wait --timeout 300s \ - && ok "Auto-rollback succeeded" \ - || fatal "Auto-rollback failed! Manual intervention required." - ;; - kustomize) - kubectl rollout undo "deployment/${APP_NAME}" -n "${NAMESPACE}" \ - && ok "Auto-rollback succeeded" \ - || fatal "Auto-rollback failed! Manual intervention required." - ;; - terraform) - warn "Terraform auto-rollback not supported. Review state manually." - ;; - esac - exit 1 -} - -# ── Production safety gate ────────────────────────────────────────────────── -confirm_production() { - if [[ "$ENVIRONMENT" == "production" ]] && [[ "$DRY_RUN" == false ]]; then - echo "" - warn "You are about to deploy to ${BOLD}PRODUCTION${NC}" - echo -e " ${BOLD}Method:${NC} ${METHOD}" - echo -e " ${BOLD}Strategy:${NC} ${STRATEGY}" - echo -e " ${BOLD}Image:${NC} ${FULL_IMAGE}" - echo -e " ${BOLD}Namespace:${NC} ${NAMESPACE}" - echo "" - read -r -p "$(echo -e "${YELLOW}Type 'yes' to confirm:${NC} ")" confirm - [[ "$confirm" == "yes" ]] || fatal "Deployment cancelled." - fi -} - -# ── Main ──────────────────────────────────────────────────────────────────── -main() { - banner - parse_args "$@" - validate_args - - info "Deployment configuration:" - echo -e " ${BOLD}Environment:${NC} ${ENVIRONMENT}" - echo -e " ${BOLD}Method:${NC} ${METHOD}" - echo -e " ${BOLD}Strategy:${NC} ${STRATEGY}" - echo -e " ${BOLD}Image:${NC} ${FULL_IMAGE}" - echo -e " ${BOLD}Namespace:${NC} ${NAMESPACE}" - echo -e " ${BOLD}Dry run:${NC} ${DRY_RUN}" - echo "" - - check_prerequisites - confirm_production - build_and_push - - case "$METHOD" in - helm) deploy_helm ;; - kustomize) deploy_kustomize ;; - terraform) deploy_terraform ;; - esac - - run_health_check - - echo "" - ok "${BOLD}Deployment complete!${NC}" - echo -e " ${BOLD}Environment:${NC} ${ENVIRONMENT}" - echo -e " ${BOLD}Image:${NC} ${FULL_IMAGE}" - echo -e " ${BOLD}Namespace:${NC} ${NAMESPACE}" - echo -e " ${BOLD}Timestamp:${NC} $(date -u +%Y-%m-%dT%H:%M:%SZ)" - echo "" -} - -main "$@" diff --git a/deployments/scripts/health-check.sh b/deployments/scripts/health-check.sh deleted file mode 100755 index 03e04b2..0000000 --- a/deployments/scripts/health-check.sh +++ /dev/null @@ -1,342 +0,0 @@ -#!/usr/bin/env bash -# ───────────────────────────────────────────────────────────────────────────── -# health-check.sh – Comprehensive health check for Claude Code Agent Monitor -# -# Usage: -# ./health-check.sh --url http://localhost:4820 -# ./health-check.sh --url http://host:port --retries 30 --interval 5 -# ./health-check.sh --url http://host:port --json -# ./health-check.sh --help -# ───────────────────────────────────────────────────────────────────────────── -# @author Nguyễn Ngọc Trí Vĩ -set -euo pipefail - -# ── Colors & logging ─────────────────────────────────────────────────────── -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -CYAN='\033[0;36m' -BOLD='\033[1m' -NC='\033[0m' - -log() { [[ "$JSON_OUTPUT" == true ]] && return; echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $*"; } -info() { [[ "$JSON_OUTPUT" == true ]] && return; echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${BLUE}ℹ${NC} $*"; } -ok() { [[ "$JSON_OUTPUT" == true ]] && return; echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${GREEN}✔${NC} $*"; } -warn() { [[ "$JSON_OUTPUT" == true ]] && return; echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${YELLOW}⚠${NC} $*" >&2; } -err() { [[ "$JSON_OUTPUT" == true ]] && return; echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${RED}✖${NC} $*" >&2; } - -# ── Defaults ──────────────────────────────────────────────────────────────── -BASE_URL="" -RETRIES=30 -INTERVAL=5 -TIMEOUT=5 -RESPONSE_THRESHOLD=2000 # milliseconds -JSON_OUTPUT=false -CHECK_WEBSOCKET=true -HEALTH_PATH="/api/health" - -# ── Usage ─────────────────────────────────────────────────────────────────── -usage() { - cat < [options] - -${BOLD}Required:${NC} - --url, -u Base URL (e.g., http://localhost:4820) - -${BOLD}Options:${NC} - --retries, -r Max retry attempts (default: 30) - --interval, -i Seconds between retries (default: 5) - --timeout HTTP request timeout in seconds (default: 5) - --threshold Max response time in ms (default: 2000) - --path Health endpoint path (default: /api/health) - --no-websocket Skip WebSocket connectivity check - --json Output results as JSON - --help, -h Show this help message - -${BOLD}Exit codes:${NC} - 0 All checks passed - 1 One or more checks failed - -${BOLD}Examples:${NC} - $(basename "$0") --url http://localhost:4820 - $(basename "$0") --url https://monitor.example.com --retries 10 --json - $(basename "$0") --url http://10.0.1.5:4820 --threshold 500 --no-websocket - -HELP - exit 0 -} - -# ── Argument parsing ──────────────────────────────────────────────────────── -parse_args() { - [[ $# -eq 0 ]] && usage - - while [[ $# -gt 0 ]]; do - case "$1" in - --url|-u) BASE_URL="$2"; shift 2 ;; - --retries|-r) RETRIES="$2"; shift 2 ;; - --interval|-i) INTERVAL="$2"; shift 2 ;; - --timeout) TIMEOUT="$2"; shift 2 ;; - --threshold) RESPONSE_THRESHOLD="$2"; shift 2 ;; - --path) HEALTH_PATH="$2"; shift 2 ;; - --no-websocket) CHECK_WEBSOCKET=false; shift ;; - --json) JSON_OUTPUT=true; shift ;; - --help|-h) usage ;; - *) echo "Unknown option: $1" >&2; exit 1 ;; - esac - done - - [[ -z "$BASE_URL" ]] && { echo "Missing required argument: --url" >&2; exit 1; } - - # Strip trailing slash - BASE_URL="${BASE_URL%/}" -} - -# ── HTTP health check ────────────────────────────────────────────────────── -check_http_health() { - local url="${BASE_URL}${HEALTH_PATH}" - local attempt=0 - local http_ok=false - local status_code="" - local response_body="" - local response_time_ms=0 - - info "Checking HTTP health: ${url}" - - while [[ $attempt -lt $RETRIES ]]; do - attempt=$((attempt + 1)) - - # Measure response time and capture output - local start_ns - start_ns=$(date +%s%N 2>/dev/null || echo "0") - - local http_response - http_response=$(curl -sf \ - --max-time "${TIMEOUT}" \ - --write-out "\n%{http_code}\n%{time_total}" \ - "${url}" 2>/dev/null) || true - - local end_ns - end_ns=$(date +%s%N 2>/dev/null || echo "0") - - if [[ -n "$http_response" ]]; then - response_body=$(echo "$http_response" | head -n -2) - status_code=$(echo "$http_response" | tail -2 | head -1) - local time_total - time_total=$(echo "$http_response" | tail -1) - # Convert seconds to milliseconds - response_time_ms=$(echo "$time_total" | awk '{printf "%.0f", $1 * 1000}' 2>/dev/null || echo "0") - - if [[ "$status_code" == "200" ]] && echo "$response_body" | grep -q '"status":"ok"'; then - http_ok=true - break - fi - fi - - if [[ $attempt -lt $RETRIES ]]; then - info "Attempt ${attempt}/${RETRIES} – waiting ${INTERVAL}s..." - sleep "${INTERVAL}" - fi - done - - # Results - HTTP_OK="$http_ok" - HTTP_STATUS="$status_code" - HTTP_BODY="$response_body" - HTTP_RESPONSE_TIME_MS="$response_time_ms" - HTTP_ATTEMPTS="$attempt" - - if [[ "$http_ok" == true ]]; then - ok "HTTP health check passed (${response_time_ms}ms, ${attempt} attempt(s))" - else - err "HTTP health check failed after ${attempt} attempts" - fi -} - -# ── Response time check ──────────────────────────────────────────────────── -check_response_time() { - if [[ "$HTTP_OK" != true ]]; then - LATENCY_OK=false - return - fi - - if [[ "$HTTP_RESPONSE_TIME_MS" -le "$RESPONSE_THRESHOLD" ]]; then - LATENCY_OK=true - ok "Response time ${HTTP_RESPONSE_TIME_MS}ms within threshold (${RESPONSE_THRESHOLD}ms)" - else - LATENCY_OK=false - warn "Response time ${HTTP_RESPONSE_TIME_MS}ms exceeds threshold (${RESPONSE_THRESHOLD}ms)" - fi -} - -# ── WebSocket connectivity check ─────────────────────────────────────────── -check_websocket() { - WS_OK=false - - if [[ "$CHECK_WEBSOCKET" == false ]]; then - info "WebSocket check skipped" - WS_OK=true # treat as pass when skipped - return - fi - - # Construct WebSocket URL - local ws_url="${BASE_URL}" - ws_url="${ws_url/http:/ws:}" - ws_url="${ws_url/https:/wss:}" - ws_url="${ws_url}/ws" - - info "Checking WebSocket: ${ws_url}" - - # Check if we have a WebSocket testing tool - if command -v websocat &>/dev/null; then - if echo "" | websocat --one-message -t "${ws_url}" 2>/dev/null; then - WS_OK=true - ok "WebSocket connection succeeded (websocat)" - return - fi - fi - - # Fallback: use curl with upgrade headers to test the handshake - local ws_status - ws_status=$(curl -sf \ - --max-time "${TIMEOUT}" \ - -o /dev/null \ - -w "%{http_code}" \ - -H "Upgrade: websocket" \ - -H "Connection: Upgrade" \ - -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \ - -H "Sec-WebSocket-Version: 13" \ - "${BASE_URL}/ws" 2>/dev/null) || ws_status="000" - - # 101 = Switching Protocols (WebSocket upgrade success) - # 400 = Bad Request (server recognized WS but rejected – still proves WS is available) - case "$ws_status" in - 101) - WS_OK=true - ok "WebSocket handshake succeeded (HTTP 101)" - ;; - 400|426) - WS_OK=true - ok "WebSocket endpoint reachable (HTTP ${ws_status} – server recognized upgrade)" - ;; - *) - # Try Node.js one-liner as last resort - if command -v node &>/dev/null; then - local node_result - node_result=$(node -e " - const ws = new (require('ws'))('${ws_url}'); - const t = setTimeout(() => { process.stdout.write('timeout'); process.exit(1); }, 5000); - ws.on('open', () => { clearTimeout(t); process.stdout.write('ok'); ws.close(); process.exit(0); }); - ws.on('error', (e) => { clearTimeout(t); process.stdout.write('error:' + e.message); process.exit(1); }); - " 2>/dev/null) || node_result="error" - - if [[ "$node_result" == "ok" ]]; then - WS_OK=true - ok "WebSocket connection verified (node)" - else - WS_OK=false - warn "WebSocket check failed: ${node_result}" - fi - else - warn "WebSocket check inconclusive (no ws testing tool available, HTTP status: ${ws_status})" - WS_OK=true # Don't fail the whole check for this - fi - ;; - esac -} - -# ── Output results ────────────────────────────────────────────────────────── -output_results() { - local overall_healthy=true - [[ "$HTTP_OK" != true ]] && overall_healthy=false - [[ "$LATENCY_OK" != true ]] && overall_healthy=false - [[ "$WS_OK" != true ]] && overall_healthy=false - - if [[ "$JSON_OUTPUT" == true ]]; then - cat </dev/null || echo '"null"') - }, - "latency": { - "passed": ${LATENCY_OK}, - "response_time_ms": ${HTTP_RESPONSE_TIME_MS:-0}, - "threshold_ms": ${RESPONSE_THRESHOLD} - }, - "websocket": { - "passed": ${WS_OK}, - "checked": ${CHECK_WEBSOCKET} - } - } -} -JSON - else - echo "" - echo -e "${BOLD}Health Check Summary${NC}" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - - local http_icon=$([[ "$HTTP_OK" == true ]] && echo "${GREEN}✔${NC}" || echo "${RED}✖${NC}") - local latency_icon=$([[ "$LATENCY_OK" == true ]] && echo "${GREEN}✔${NC}" || echo "${YELLOW}⚠${NC}") - local ws_icon=$([[ "$WS_OK" == true ]] && echo "${GREEN}✔${NC}" || echo "${RED}✖${NC}") - - echo -e " ${http_icon} HTTP /api/health (${HTTP_STATUS:-???}, ${HTTP_RESPONSE_TIME_MS:-?}ms, ${HTTP_ATTEMPTS:-?} attempts)" - echo -e " ${latency_icon} Response time (${HTTP_RESPONSE_TIME_MS:-?}ms / ${RESPONSE_THRESHOLD}ms threshold)" - echo -e " ${ws_icon} WebSocket (checked: ${CHECK_WEBSOCKET})" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - - if [[ "$overall_healthy" == true ]]; then - echo -e " ${GREEN}${BOLD}Overall: HEALTHY${NC}" - else - echo -e " ${RED}${BOLD}Overall: UNHEALTHY${NC}" - fi - echo "" - fi - - if [[ "$overall_healthy" == true ]]; then - return 0 - else - return 1 - fi -} - -# ── Main ──────────────────────────────────────────────────────────────────── -main() { - parse_args "$@" - - # Initialize result variables - HTTP_OK=false - HTTP_STATUS="" - HTTP_BODY="" - HTTP_RESPONSE_TIME_MS=0 - HTTP_ATTEMPTS=0 - LATENCY_OK=false - WS_OK=false - - [[ "$JSON_OUTPUT" != true ]] && { - echo "" - echo -e "${BOLD}${GREEN}╔══════════════════════════════════════════════════╗${NC}" - echo -e "${BOLD}${GREEN}║ Claude Code Agent Monitor – Health Check ║${NC}" - echo -e "${BOLD}${GREEN}╚══════════════════════════════════════════════════╝${NC}" - echo "" - info "Target: ${BASE_URL}" - info "Config: retries=${RETRIES}, interval=${INTERVAL}s, threshold=${RESPONSE_THRESHOLD}ms" - echo "" - } - - check_http_health - check_response_time - check_websocket - - output_results -} - -main "$@" diff --git a/deployments/scripts/rollback.sh b/deployments/scripts/rollback.sh deleted file mode 100755 index cfe59ca..0000000 --- a/deployments/scripts/rollback.sh +++ /dev/null @@ -1,221 +0,0 @@ -#!/usr/bin/env bash -# ───────────────────────────────────────────────────────────────────────────── -# rollback.sh – Rollback deployments for Claude Code Agent Monitor -# -# Usage: -# ./rollback.sh --env production --method helm --revision 5 -# ./rollback.sh --env staging --method kustomize -# ./rollback.sh --help -# ───────────────────────────────────────────────────────────────────────────── -# @author Nguyễn Ngọc Trí Vĩ -set -euo pipefail - -readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -readonly APP_NAME="agent-monitor" -readonly APP_PORT=4820 - -# ── Colors & logging ─────────────────────────────────────────────────────── -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -CYAN='\033[0;36m' -BOLD='\033[1m' -NC='\033[0m' - -log() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $*"; } -info() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${BLUE}ℹ${NC} $*"; } -ok() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${GREEN}✔${NC} $*"; } -warn() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${YELLOW}⚠${NC} $*" >&2; } -err() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${RED}✖${NC} $*" >&2; } -fatal() { err "$@"; exit 1; } - -# ── Defaults ──────────────────────────────────────────────────────────────── -ENVIRONMENT="" -METHOD="" -REVISION="" -NAMESPACE="" -HELM_RELEASE="${APP_NAME}" -SKIP_HEALTH_CHECK=false - -# ── Usage ─────────────────────────────────────────────────────────────────── -usage() { - cat < --method [options] - -${BOLD}Required:${NC} - --env, -e Environment: dev, staging, production - --method, -m Method: helm, kustomize - -${BOLD}Options:${NC} - --revision, -r Helm revision or rollout history number to roll back to - --namespace, -n Kubernetes namespace (default: agent-monitor-) - --release Helm release name (default: ${APP_NAME}) - --skip-health Skip post-rollback health check - --help, -h Show this help message - -${BOLD}Examples:${NC} - $(basename "$0") --env production --method helm --revision 5 - $(basename "$0") --env staging --method kustomize - $(basename "$0") --env production --method helm # rolls back to previous - -EOF - exit 0 -} - -# ── Argument parsing ──────────────────────────────────────────────────────── -parse_args() { - [[ $# -eq 0 ]] && usage - - while [[ $# -gt 0 ]]; do - case "$1" in - --env|-e) ENVIRONMENT="$2"; shift 2 ;; - --method|-m) METHOD="$2"; shift 2 ;; - --revision|-r) REVISION="$2"; shift 2 ;; - --namespace|-n) NAMESPACE="$2"; shift 2 ;; - --release) HELM_RELEASE="$2"; shift 2 ;; - --skip-health) SKIP_HEALTH_CHECK=true; shift ;; - --help|-h) usage ;; - *) fatal "Unknown option: $1" ;; - esac - done - - [[ -z "$ENVIRONMENT" ]] && fatal "Missing required argument: --env" - [[ -z "$METHOD" ]] && fatal "Missing required argument: --method" - [[ -z "$NAMESPACE" ]] && NAMESPACE="agent-monitor-${ENVIRONMENT}" -} - -# ── Show release history ─────────────────────────────────────────────────── -show_history() { - info "Release history:" - case "$METHOD" in - helm) - helm history "${HELM_RELEASE}" -n "${NAMESPACE}" --max 10 2>/dev/null || warn "No history found" - ;; - kustomize) - kubectl rollout history "deployment/${APP_NAME}" -n "${NAMESPACE}" 2>/dev/null || warn "No history found" - ;; - esac - echo "" -} - -# ── Confirm rollback ─────────────────────────────────────────────────────── -confirm_rollback() { - if [[ "$ENVIRONMENT" == "production" ]]; then - local target_msg="previous revision" - [[ -n "$REVISION" ]] && target_msg="revision ${REVISION}" - - warn "Rolling back ${BOLD}PRODUCTION${NC} to ${target_msg}" - read -r -p "$(echo -e "${YELLOW}Type 'yes' to confirm:${NC} ")" confirm - [[ "$confirm" == "yes" ]] || fatal "Rollback cancelled." - fi -} - -# ── Helm rollback ─────────────────────────────────────────────────────────── -rollback_helm() { - info "Rolling back Helm release '${HELM_RELEASE}' in namespace '${NAMESPACE}'..." - - local rollback_args=(rollback "${HELM_RELEASE}") - [[ -n "$REVISION" ]] && rollback_args+=("${REVISION}") - rollback_args+=(-n "${NAMESPACE}" --wait --timeout 300s) - - if ! helm "${rollback_args[@]}"; then - fatal "Helm rollback failed! Manual intervention required." - fi - - ok "Helm rollback completed" - - # Show current status - info "Current release status:" - helm status "${HELM_RELEASE}" -n "${NAMESPACE}" 2>/dev/null || true -} - -# ── Kustomize rollback ───────────────────────────────────────────────────── -rollback_kustomize() { - info "Rolling back deployment '${APP_NAME}' in namespace '${NAMESPACE}'..." - - local undo_args=(rollout undo "deployment/${APP_NAME}" -n "${NAMESPACE}") - if [[ -n "$REVISION" ]]; then - undo_args+=(--to-revision="${REVISION}") - fi - - if ! kubectl "${undo_args[@]}"; then - fatal "Kubectl rollback failed! Manual intervention required." - fi - - # Wait for rollout - info "Waiting for rollout to complete..." - if ! kubectl rollout status "deployment/${APP_NAME}" -n "${NAMESPACE}" --timeout=300s; then - fatal "Rollout did not complete in time!" - fi - - ok "Kustomize rollback completed" -} - -# ── Post-rollback health check ───────────────────────────────────────────── -run_health_check() { - if [[ "$SKIP_HEALTH_CHECK" == true ]]; then - info "Skipping health check" - return - fi - - info "Running post-rollback health check..." - - # Wait for pods to be ready - if ! kubectl wait --for=condition=ready pod \ - -l "app.kubernetes.io/name=${APP_NAME}" \ - -n "${NAMESPACE}" --timeout=120s 2>/dev/null; then - fatal "Pods did not become ready after rollback!" - fi - - # Use health-check.sh if available - if [[ -x "${SCRIPT_DIR}/health-check.sh" ]]; then - # Port forward for check - local local_port=14820 - kubectl port-forward "svc/${APP_NAME}" "${local_port}:${APP_PORT}" -n "${NAMESPACE}" & - local pf_pid=$! - sleep 3 - - if "${SCRIPT_DIR}/health-check.sh" --url "http://localhost:${local_port}" --retries 10 --interval 3; then - ok "Health check passed after rollback" - else - err "Health check failed after rollback!" - fi - - kill "$pf_pid" 2>/dev/null || true - else - ok "Pods are ready" - fi -} - -# ── Main ──────────────────────────────────────────────────────────────────── -main() { - echo "" - echo -e "${BOLD}${YELLOW}╔══════════════════════════════════════════════════╗${NC}" - echo -e "${BOLD}${YELLOW}║ Claude Code Agent Monitor – Rollback ║${NC}" - echo -e "${BOLD}${YELLOW}╚══════════════════════════════════════════════════╝${NC}" - echo "" - - parse_args "$@" - show_history - confirm_rollback - - case "$METHOD" in - helm) rollback_helm ;; - kustomize) rollback_kustomize ;; - *) fatal "Rollback not supported for method: ${METHOD}" ;; - esac - - run_health_check - - echo "" - ok "${BOLD}Rollback complete!${NC}" - echo -e " ${BOLD}Environment:${NC} ${ENVIRONMENT}" - echo -e " ${BOLD}Method:${NC} ${METHOD}" - echo -e " ${BOLD}Revision:${NC} ${REVISION:-previous}" - echo -e " ${BOLD}Timestamp:${NC} $(date -u +%Y-%m-%dT%H:%M:%SZ)" - echo "" -} - -main "$@" diff --git a/deployments/scripts/teardown.sh b/deployments/scripts/teardown.sh deleted file mode 100755 index 1da410e..0000000 --- a/deployments/scripts/teardown.sh +++ /dev/null @@ -1,335 +0,0 @@ -#!/usr/bin/env bash -# ───────────────────────────────────────────────────────────────────────────── -# teardown.sh – Clean removal of Claude Code Agent Monitor infrastructure -# -# Usage: -# ./teardown.sh --env dev --method helm -# ./teardown.sh --env production --method terraform -# ./teardown.sh --help -# ───────────────────────────────────────────────────────────────────────────── -# @author Nguyễn Ngọc Trí Vĩ -set -euo pipefail - -readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -readonly PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" -readonly DEPLOY_DIR="${PROJECT_ROOT}/deployments" -readonly APP_NAME="agent-monitor" - -# ── Colors & logging ─────────────────────────────────────────────────────── -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -CYAN='\033[0;36m' -BOLD='\033[1m' -NC='\033[0m' - -log() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $*"; } -info() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${BLUE}ℹ${NC} $*"; } -ok() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${GREEN}✔${NC} $*"; } -warn() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${YELLOW}⚠${NC} $*" >&2; } -err() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${RED}✖${NC} $*" >&2; } -fatal() { err "$@"; exit 1; } - -# ── Defaults ──────────────────────────────────────────────────────────────── -ENVIRONMENT="" -METHOD="" -NAMESPACE="" -HELM_RELEASE="${APP_NAME}" -SKIP_BACKUP=false -FORCE=false -DELETE_NAMESPACE=false -DELETE_PVC=false - -# ── Usage ─────────────────────────────────────────────────────────────────── -usage() { - cat < --method [options] - -${BOLD}Required:${NC} - --env, -e Environment: dev, staging, production - --method, -m Method: helm, kustomize, terraform - -${BOLD}Options:${NC} - --namespace, -n Kubernetes namespace (default: agent-monitor-) - --release Helm release name (default: ${APP_NAME}) - --delete-namespace Also delete the Kubernetes namespace - --delete-pvc Also delete PersistentVolumeClaims (data loss!) - --skip-backup Skip data backup before teardown - --force Skip all confirmation prompts - --help, -h Show this help message - -${BOLD}Examples:${NC} - $(basename "$0") --env dev --method helm - $(basename "$0") --env staging --method kustomize --delete-namespace - $(basename "$0") --env production --method terraform - -EOF - exit 0 -} - -# ── Argument parsing ──────────────────────────────────────────────────────── -parse_args() { - [[ $# -eq 0 ]] && usage - - while [[ $# -gt 0 ]]; do - case "$1" in - --env|-e) ENVIRONMENT="$2"; shift 2 ;; - --method|-m) METHOD="$2"; shift 2 ;; - --namespace|-n) NAMESPACE="$2"; shift 2 ;; - --release) HELM_RELEASE="$2"; shift 2 ;; - --delete-namespace) DELETE_NAMESPACE=true; shift ;; - --delete-pvc) DELETE_PVC=true; shift ;; - --skip-backup) SKIP_BACKUP=true; shift ;; - --force) FORCE=true; shift ;; - --help|-h) usage ;; - *) fatal "Unknown option: $1" ;; - esac - done - - [[ -z "$ENVIRONMENT" ]] && fatal "Missing required argument: --env" - [[ -z "$METHOD" ]] && fatal "Missing required argument: --method" - [[ -z "$NAMESPACE" ]] && NAMESPACE="agent-monitor-${ENVIRONMENT}" -} - -# ── Confirm teardown ─────────────────────────────────────────────────────── -confirm_teardown() { - if [[ "$FORCE" == true ]]; then - return - fi - - echo "" - echo -e " ${RED}${BOLD}╔══════════════════════════════════════════╗${NC}" - echo -e " ${RED}${BOLD}║ ⚠ TEARDOWN WARNING ⚠ ║${NC}" - echo -e " ${RED}${BOLD}╚══════════════════════════════════════════╝${NC}" - echo "" - echo -e " This will ${RED}${BOLD}DESTROY${NC} the following resources:" - echo -e " • Environment: ${BOLD}${ENVIRONMENT}${NC}" - echo -e " • Method: ${BOLD}${METHOD}${NC}" - echo -e " • Namespace: ${BOLD}${NAMESPACE}${NC}" - [[ "$DELETE_NAMESPACE" == true ]] && echo -e " • ${RED}Namespace will be deleted${NC}" - [[ "$DELETE_PVC" == true ]] && echo -e " • ${RED}PVCs will be deleted (DATA LOSS!)${NC}" - echo "" - - if [[ "$ENVIRONMENT" == "production" ]]; then - echo -e " ${RED}${BOLD}THIS IS A PRODUCTION ENVIRONMENT!${NC}" - echo "" - read -r -p "$(echo -e "${RED}Type 'destroy ${ENVIRONMENT}' to confirm:${NC} ")" confirm - [[ "$confirm" == "destroy ${ENVIRONMENT}" ]] || fatal "Teardown cancelled." - - # Second confirmation for production - echo "" - read -r -p "$(echo -e "${RED}Are you absolutely sure? Type 'YES' in caps:${NC} ")" confirm2 - [[ "$confirm2" == "YES" ]] || fatal "Teardown cancelled." - else - read -r -p "$(echo -e "${YELLOW}Type 'yes' to confirm:${NC} ")" confirm - [[ "$confirm" == "yes" ]] || fatal "Teardown cancelled." - fi -} - -# ── Pre-teardown backup ──────────────────────────────────────────────────── -backup_data() { - if [[ "$SKIP_BACKUP" == true ]]; then - info "Skipping pre-teardown backup (--skip-backup)" - return - fi - - info "Creating backup before teardown..." - - if [[ -x "${SCRIPT_DIR}/db-backup.sh" ]]; then - local backup_dir="${PROJECT_ROOT}/data/pre-teardown-backups" - "${SCRIPT_DIR}/db-backup.sh" \ - --env "${ENVIRONMENT}" \ - --output "${backup_dir}" \ - --namespace "${NAMESPACE}" \ - && ok "Pre-teardown backup created" \ - || warn "Backup failed – continuing with teardown" - else - warn "db-backup.sh not found – skipping backup" - fi -} - -# ── Show current resources ────────────────────────────────────────────────── -show_resources() { - info "Current resources in namespace '${NAMESPACE}':" - echo "" - - kubectl get all -n "${NAMESPACE}" 2>/dev/null || warn "Could not list resources" - - if [[ "$METHOD" != "terraform" ]]; then - echo "" - info "PersistentVolumeClaims:" - kubectl get pvc -n "${NAMESPACE}" 2>/dev/null || warn "No PVCs found" - fi - echo "" -} - -# ── Helm teardown ────────────────────────────────────────────────────────── -teardown_helm() { - info "Uninstalling Helm release '${HELM_RELEASE}'..." - - if helm status "${HELM_RELEASE}" -n "${NAMESPACE}" &>/dev/null; then - helm uninstall "${HELM_RELEASE}" -n "${NAMESPACE}" --wait --timeout 300s \ - || fatal "Helm uninstall failed" - ok "Helm release '${HELM_RELEASE}' uninstalled" - else - warn "Helm release '${HELM_RELEASE}' not found in namespace '${NAMESPACE}'" - fi - - # Also try uninstalling blue/green releases - for color in blue green; do - if helm status "${HELM_RELEASE}-${color}" -n "${NAMESPACE}" &>/dev/null; then - info "Uninstalling ${color} slot release..." - helm uninstall "${HELM_RELEASE}-${color}" -n "${NAMESPACE}" --wait --timeout 300s \ - && ok "Release '${HELM_RELEASE}-${color}' uninstalled" \ - || warn "Failed to uninstall ${color} release" - fi - done -} - -# ── Kustomize teardown ───────────────────────────────────────────────────── -teardown_kustomize() { - local overlay_dir="${DEPLOY_DIR}/kubernetes/overlays/${ENVIRONMENT}" - - if [[ -d "$overlay_dir" ]]; then - info "Deleting Kustomize resources..." - kubectl delete -k "${overlay_dir}" --ignore-not-found=true --wait=true --timeout=300s \ - && ok "Kustomize resources deleted" \ - || warn "Some resources may not have been deleted" - else - warn "Kustomize overlay not found at ${overlay_dir}" - info "Deleting resources by label..." - kubectl delete all -l "app.kubernetes.io/name=${APP_NAME}" -n "${NAMESPACE}" --wait=true \ - || warn "Could not delete resources by label" - fi -} - -# ── Terraform teardown ───────────────────────────────────────────────────── -teardown_terraform() { - local tf_dir="${DEPLOY_DIR}/terraform" - local env_vars_file="${tf_dir}/environments/${ENVIRONMENT}/terraform.tfvars" - - info "Destroying Terraform-managed infrastructure..." - - pushd "${tf_dir}" > /dev/null - - terraform init -input=false - - local destroy_args=(-input=false -auto-approve) - if [[ -f "$env_vars_file" ]]; then - destroy_args+=(-var-file="$env_vars_file") - fi - # Need to provide required variables that may not have defaults - destroy_args+=(-var "environment=${ENVIRONMENT}") - - if ! terraform destroy "${destroy_args[@]}"; then - popd > /dev/null - fatal "Terraform destroy failed! Review state manually." - fi - - popd > /dev/null - ok "Terraform infrastructure destroyed" -} - -# ── Cleanup PVCs ──────────────────────────────────────────────────────────── -cleanup_pvcs() { - if [[ "$DELETE_PVC" != true ]]; then - local pvc_count - pvc_count=$(kubectl get pvc -n "${NAMESPACE}" --no-headers 2>/dev/null | wc -l | tr -d ' ') - if [[ "$pvc_count" -gt 0 ]]; then - warn "PersistentVolumeClaims still exist. Use --delete-pvc to remove them." - kubectl get pvc -n "${NAMESPACE}" 2>/dev/null - fi - return - fi - - info "Deleting PersistentVolumeClaims..." - kubectl delete pvc --all -n "${NAMESPACE}" --wait=true \ - && ok "PVCs deleted" \ - || warn "Some PVCs could not be deleted" -} - -# ── Cleanup namespace ────────────────────────────────────────────────────── -cleanup_namespace() { - if [[ "$DELETE_NAMESPACE" != true ]]; then - return - fi - - if [[ "$ENVIRONMENT" == "production" ]]; then - warn "Refusing to delete production namespace automatically." - warn "Delete manually: kubectl delete namespace ${NAMESPACE}" - return - fi - - info "Deleting namespace '${NAMESPACE}'..." - kubectl delete namespace "${NAMESPACE}" --ignore-not-found=true --wait=true --timeout=120s \ - && ok "Namespace '${NAMESPACE}' deleted" \ - || warn "Namespace deletion may be stuck. Check: kubectl get namespace ${NAMESPACE}" -} - -# ── Verify teardown ──────────────────────────────────────────────────────── -verify_teardown() { - info "Verifying teardown..." - - if [[ "$METHOD" == "terraform" ]]; then - ok "Terraform state should reflect no resources" - return - fi - - local remaining - remaining=$(kubectl get all -n "${NAMESPACE}" --no-headers 2>/dev/null | wc -l | tr -d ' ') - - if [[ "$remaining" -eq 0 ]]; then - ok "No resources remaining in namespace '${NAMESPACE}'" - else - warn "${remaining} resources still exist in namespace '${NAMESPACE}':" - kubectl get all -n "${NAMESPACE}" 2>/dev/null - fi -} - -# ── Main ──────────────────────────────────────────────────────────────────── -main() { - echo "" - echo -e "${BOLD}${RED}╔══════════════════════════════════════════════════╗${NC}" - echo -e "${BOLD}${RED}║ Claude Code Agent Monitor – Teardown ║${NC}" - echo -e "${BOLD}${RED}╚══════════════════════════════════════════════════╝${NC}" - echo "" - - parse_args "$@" - - info "Teardown configuration:" - echo -e " ${BOLD}Environment:${NC} ${ENVIRONMENT}" - echo -e " ${BOLD}Method:${NC} ${METHOD}" - echo -e " ${BOLD}Namespace:${NC} ${NAMESPACE}" - echo "" - - if [[ "$METHOD" != "terraform" ]]; then - show_resources - fi - - confirm_teardown - backup_data - - case "$METHOD" in - helm) teardown_helm ;; - kustomize) teardown_kustomize ;; - terraform) teardown_terraform ;; - *) fatal "Invalid method: ${METHOD}" ;; - esac - - if [[ "$METHOD" != "terraform" ]]; then - cleanup_pvcs - cleanup_namespace - verify_teardown - fi - - echo "" - ok "${BOLD}Teardown complete!${NC}" - echo -e " ${BOLD}Environment:${NC} ${ENVIRONMENT}" - echo -e " ${BOLD}Method:${NC} ${METHOD}" - echo -e " ${BOLD}Timestamp:${NC} $(date -u +%Y-%m-%dT%H:%M:%SZ)" - echo "" -} - -main "$@" diff --git a/deployments/terraform/README.md b/deployments/terraform/README.md deleted file mode 100644 index e3bcab7..0000000 --- a/deployments/terraform/README.md +++ /dev/null @@ -1,88 +0,0 @@ -# Terraform Infrastructure - -Cloud-agnostic infrastructure modules for deploying the Claude Code Agent Monitor to AWS, GCP, Azure, or OCI. - -## Architecture - -```mermaid -graph TD - subgraph Modules["Reusable Modules"] - NET["networking"] - COMP["compute"] - DB["database"] - LB["loadbalancer"] - MON["monitoring"] - SEC["secrets"] - end - - subgraph Providers["Provider Implementations"] - AWS["aws/"] - GCP["gcp/"] - AZ["azure/"] - OCI["oci/"] - end - - subgraph Envs["Environments"] - DEV["dev/terraform.tfvars"] - STG["staging/terraform.tfvars"] - PRD["production/terraform.tfvars"] - end - - AWS --> NET & COMP & DB & LB & MON & SEC - GCP --> NET & COMP & DB & LB & MON & SEC - AZ --> NET & COMP & DB & LB & MON & SEC - OCI --> NET & COMP & DB & LB & MON & SEC - Envs -.->|var-file| Providers -``` - -## Usage - -```bash -# 1. Choose your provider -cd providers/aws # or gcp, azure, oci - -# 2. Initialize -terraform init - -# 3. Plan with environment -terraform plan -var-file=../../environments/production/terraform.tfvars - -# 4. Apply -terraform apply -var-file=../../environments/production/terraform.tfvars - -# 5. Get outputs -terraform output -``` - -## Module Reference - -| Module | Purpose | Key Resources | -|---|---|---| -| `networking` | VPC/VNet, subnets, NAT, security groups | VPC, public/private subnets, NAT gateway, firewall rules | -| `compute` | Container orchestration with blue-green slots | ECS tasks / Cloud Run / ACI / OKE deployments | -| `database` | Persistent storage for SQLite | EFS / Filestore / Azure Files / FSS with encryption | -| `loadbalancer` | Application LB with WebSocket + traffic splitting | ALB / GCLB / App Gateway / LBaaS, health checks | -| `monitoring` | Metrics, logs, alerts, dashboards | CloudWatch / Cloud Monitoring / Azure Monitor / OCI Monitoring | -| `secrets` | Secret management | Secrets Manager / Secret Manager / Key Vault / Vault | - -## Remote State - -Each provider is configured to use cloud-native remote state: - -| Provider | Backend | Bucket | -|---|---|---| -| AWS | S3 + DynamoDB locking | `agent-monitor-tfstate-{account_id}` | -| GCP | GCS | `agent-monitor-tfstate-{project_id}` | -| Azure | Azure Blob Storage | `agentmonitortfstate` | -| OCI | OCI Object Storage | `agent-monitor-tfstate` | - -## Environment Sizing - -| Resource | Dev | Staging | Production | -|---|---|---|---| -| Replicas | 1 | 2 | 3 (auto-scale to 10) | -| CPU | 256 | 512 | 1024 | -| Memory | 512 MB | 1 GB | 2 GB | -| Storage | 5 GB | 10 GB | 50 GB (encrypted) | -| Multi-AZ | No | Yes | Yes | -| Monitoring | Basic | Standard | Full + alerts | diff --git a/deployments/terraform/backend.tf b/deployments/terraform/backend.tf deleted file mode 100644 index bd0d165..0000000 --- a/deployments/terraform/backend.tf +++ /dev/null @@ -1,47 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# Remote state backend – uncomment the block matching your cloud provider. -# Only ONE backend may be active at a time. -# ───────────────────────────────────────────────────────────────────────────── - -# ── AWS S3 ────────────────────────────────────────────────────────────────── -# terraform { -# backend "s3" { -# bucket = "ccam-terraform-state" -# key = "claude-agent-monitor/terraform.tfstate" -# region = "us-east-1" -# encrypt = true -# dynamodb_table = "ccam-terraform-locks" -# } -# } - -# ── GCP Cloud Storage ────────────────────────────────────────────────────── -# terraform { -# backend "gcs" { -# bucket = "ccam-terraform-state" -# prefix = "claude-agent-monitor" -# } -# } - -# ── Azure Blob Storage ───────────────────────────────────────────────────── -# terraform { -# backend "azurerm" { -# resource_group_name = "ccam-terraform-state-rg" -# storage_account_name = "ccamtfstate" -# container_name = "tfstate" -# key = "claude-agent-monitor.tfstate" -# } -# } - -# ── OCI Object Storage ───────────────────────────────────────────────────── -# terraform { -# backend "s3" { -# bucket = "ccam-terraform-state" -# key = "claude-agent-monitor/terraform.tfstate" -# region = "us-ashburn-1" -# endpoint = "https://.compat.objectstorage..oraclecloud.com" -# skip_region_validation = true -# skip_credentials_validation = true -# skip_metadata_api_check = true -# force_path_style = true -# } -# } diff --git a/deployments/terraform/environments/dev/terraform.tfvars b/deployments/terraform/environments/dev/terraform.tfvars deleted file mode 100644 index f53f1fd..0000000 --- a/deployments/terraform/environments/dev/terraform.tfvars +++ /dev/null @@ -1,67 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# Development environment – terraform.tfvars -# -# Minimal resources for development/testing. Single replica, small compute, -# monitoring disabled to reduce cost. -# ───────────────────────────────────────────────────────────────────────────── - -# ── Provider ──────────────────────────────────────────────────────────────── -cloud_provider = "aws" -region = "us-east-1" - -# ── Project ───────────────────────────────────────────────────────────────── -project_name = "claude-agent-monitor" -environment = "dev" - -tags = { - team = "platform" - cost_center = "engineering" -} - -# ── Networking ────────────────────────────────────────────────────────────── -vpc_cidr = "10.0.0.0/16" -public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"] -private_subnet_cidrs = ["10.0.11.0/24", "10.0.12.0/24"] - -# ── Compute (small) ──────────────────────────────────────────────────────── -app_container_image = "ghcr.io/anthropics/claude-agent-monitor:latest" -mcp_container_image = "" # MCP sidecar disabled in dev -cpu = 256 # 0.25 vCPU -memory = 512 # 512 MiB - -min_replicas = 1 -max_replicas = 1 -desired_replicas = 1 - -environment_variables = { - NODE_ENV = "development" - DASHBOARD_PORT = "4820" - LOG_LEVEL = "debug" -} - -# ── Deployment ────────────────────────────────────────────────────────────── -deployment_strategy = "rolling" -active_deployment_slot = "blue" -blue_weight = 100 -green_weight = 0 - -# ── TLS (disabled in dev) ────────────────────────────────────────────────── -domain_name = "" -tls_certificate_arn = "" - -# ── Storage ───────────────────────────────────────────────────────────────── -storage_size_gb = 10 -enable_storage_backup = false - -# ── Health check ──────────────────────────────────────────────────────────── -health_check_path = "/api/health" -health_check_interval = 60 # Less frequent in dev - -# ── Auto-scaling (disabled – single replica) ──────────────────────────────── -autoscaling_cpu_target = 80 -autoscaling_memory_target = 90 - -# ── Monitoring (minimal) ─────────────────────────────────────────────────── -enable_monitoring = false -alert_email = "" -log_retention_days = 7 diff --git a/deployments/terraform/environments/production/terraform.tfvars b/deployments/terraform/environments/production/terraform.tfvars deleted file mode 100644 index e1ad7d4..0000000 --- a/deployments/terraform/environments/production/terraform.tfvars +++ /dev/null @@ -1,80 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# Production environment – terraform.tfvars -# -# Full production configuration. 3+ replicas with auto-scaling, large -# compute, comprehensive monitoring, TLS, blue-green deployment ready. -# ───────────────────────────────────────────────────────────────────────────── - -# ── Provider ──────────────────────────────────────────────────────────────── -cloud_provider = "aws" -region = "us-east-1" - -# ── Project ───────────────────────────────────────────────────────────────── -project_name = "claude-agent-monitor" -environment = "production" - -tags = { - team = "platform" - cost_center = "engineering" - criticality = "high" - compliance = "soc2" -} - -# ── Networking (3 AZs for high availability) ─────────────────────────────── -vpc_cidr = "10.2.0.0/16" -public_subnet_cidrs = ["10.2.1.0/24", "10.2.2.0/24", "10.2.3.0/24"] -private_subnet_cidrs = ["10.2.11.0/24", "10.2.12.0/24", "10.2.13.0/24"] - -# ── Compute (large) ──────────────────────────────────────────────────────── -app_container_image = "ghcr.io/anthropics/claude-agent-monitor:latest" -mcp_container_image = "ghcr.io/anthropics/claude-agent-monitor-mcp:latest" -cpu = 1024 # 1 vCPU -memory = 2048 # 2 GiB - -min_replicas = 3 -max_replicas = 10 -desired_replicas = 3 - -environment_variables = { - NODE_ENV = "production" - DASHBOARD_PORT = "4820" - LOG_LEVEL = "warn" -} - -# ── Deployment (blue-green with canary support) ──────────────────────────── -deployment_strategy = "blue-green" -active_deployment_slot = "blue" -blue_weight = 100 -green_weight = 0 - -# During canary deployment, adjust weights: -# blue_weight = 90 -# green_weight = 10 -# Then gradually shift to: -# blue_weight = 0 -# green_weight = 100 -# Finally, flip active_deployment_slot = "green" - -# ── TLS ───────────────────────────────────────────────────────────────────── -domain_name = "" # Set to production FQDN (e.g. "monitor.example.com") -tls_certificate_arn = "" # Set to existing ACM cert ARN or leave empty for auto - -# ── Storage ───────────────────────────────────────────────────────────────── -storage_size_gb = 50 -enable_storage_backup = true - -# ── Health check (strict thresholds) ─────────────────────────────────────── -health_check_path = "/api/health" -health_check_interval = 15 -health_check_timeout = 5 -health_check_healthy_threshold = 2 -health_check_unhealthy_threshold = 2 - -# ── Auto-scaling (aggressive) ────────────────────────────────────────────── -autoscaling_cpu_target = 60 -autoscaling_memory_target = 70 - -# ── Monitoring (comprehensive) ───────────────────────────────────────────── -enable_monitoring = true -alert_email = "" # REQUIRED: Set to ops team email for production alerts -log_retention_days = 90 diff --git a/deployments/terraform/environments/staging/terraform.tfvars b/deployments/terraform/environments/staging/terraform.tfvars deleted file mode 100644 index 028724f..0000000 --- a/deployments/terraform/environments/staging/terraform.tfvars +++ /dev/null @@ -1,70 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# Staging environment – terraform.tfvars -# -# Production-like configuration with moderate resources. Two replicas, -# medium compute, monitoring enabled with relaxed thresholds. -# ───────────────────────────────────────────────────────────────────────────── - -# ── Provider ──────────────────────────────────────────────────────────────── -cloud_provider = "aws" -region = "us-east-1" - -# ── Project ───────────────────────────────────────────────────────────────── -project_name = "claude-agent-monitor" -environment = "staging" - -tags = { - team = "platform" - cost_center = "engineering" -} - -# ── Networking ────────────────────────────────────────────────────────────── -vpc_cidr = "10.1.0.0/16" -public_subnet_cidrs = ["10.1.1.0/24", "10.1.2.0/24", "10.1.3.0/24"] -private_subnet_cidrs = ["10.1.11.0/24", "10.1.12.0/24", "10.1.13.0/24"] - -# ── Compute (medium) ─────────────────────────────────────────────────────── -app_container_image = "ghcr.io/anthropics/claude-agent-monitor:staging" -mcp_container_image = "ghcr.io/anthropics/claude-agent-monitor-mcp:staging" -cpu = 512 # 0.5 vCPU -memory = 1024 # 1 GiB - -min_replicas = 1 -max_replicas = 3 -desired_replicas = 2 - -environment_variables = { - NODE_ENV = "production" - DASHBOARD_PORT = "4820" - LOG_LEVEL = "info" -} - -# ── Deployment (blue-green ready) ────────────────────────────────────────── -deployment_strategy = "blue-green" -active_deployment_slot = "blue" -blue_weight = 100 -green_weight = 0 - -# ── TLS ───────────────────────────────────────────────────────────────────── -domain_name = "" # Set to staging FQDN when available -tls_certificate_arn = "" # Auto-created if domain_name is set - -# ── Storage ───────────────────────────────────────────────────────────────── -storage_size_gb = 20 -enable_storage_backup = true - -# ── Health check ──────────────────────────────────────────────────────────── -health_check_path = "/api/health" -health_check_interval = 30 -health_check_timeout = 5 -health_check_healthy_threshold = 2 -health_check_unhealthy_threshold = 3 - -# ── Auto-scaling ──────────────────────────────────────────────────────────── -autoscaling_cpu_target = 70 -autoscaling_memory_target = 80 - -# ── Monitoring ────────────────────────────────────────────────────────────── -enable_monitoring = true -alert_email = "" # Set to team email for staging alerts -log_retention_days = 14 diff --git a/deployments/terraform/main.tf b/deployments/terraform/main.tf deleted file mode 100644 index e78818c..0000000 --- a/deployments/terraform/main.tf +++ /dev/null @@ -1,185 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# Claude Code Agent Monitor – Root orchestration module -# -# Selects the cloud provider implementation via var.cloud_provider and wires -# the generic modules together. Each provider directory contains a full, -# opinionated implementation that composes the child modules. -# ───────────────────────────────────────────────────────────────────────────── - -locals { - common_tags = merge( - { - project = var.project_name - environment = var.environment - managed_by = "terraform" - repository = "Claude-Code-Agent-Monitor" - }, - var.tags, - ) - - # Normalise resource name prefix (lowercase, hyphens) - name_prefix = lower(replace("${var.project_name}-${var.environment}", "_", "-")) -} - -# ── Networking ────────────────────────────────────────────────────────────── - -module "networking" { - source = "./modules/networking" - - project_name = var.project_name - environment = var.environment - cloud_provider = var.cloud_provider - region = var.region - vpc_cidr = var.vpc_cidr - availability_zones = var.availability_zones - public_subnet_cidrs = var.public_subnet_cidrs - private_subnet_cidrs = var.private_subnet_cidrs - app_port = var.app_port - mcp_port = var.mcp_port - tags = local.common_tags -} - -# ── Persistent storage (SQLite DB) ───────────────────────────────────────── - -module "database" { - source = "./modules/database" - - project_name = var.project_name - environment = var.environment - cloud_provider = var.cloud_provider - region = var.region - storage_size_gb = var.storage_size_gb - enable_backup = var.enable_storage_backup - private_subnet_ids = module.networking.private_subnet_ids - vpc_id = module.networking.vpc_id - allowed_security_group_ids = module.networking.storage_security_group_ids - tags = local.common_tags -} - -# ── Compute (Blue slot) ──────────────────────────────────────────────────── - -module "compute_blue" { - source = "./modules/compute" - - project_name = var.project_name - environment = var.environment - cloud_provider = var.cloud_provider - region = var.region - deployment_slot = "blue" - container_image = var.app_container_image - mcp_container_image = var.mcp_container_image - app_port = var.app_port - mcp_port = var.mcp_port - cpu = var.cpu - memory = var.memory - desired_count = var.active_deployment_slot == "blue" ? var.desired_replicas : 0 - min_count = var.active_deployment_slot == "blue" ? var.min_replicas : 0 - max_count = var.active_deployment_slot == "blue" ? var.max_replicas : 0 - environment_variables = var.environment_variables - health_check_path = var.health_check_path - vpc_id = module.networking.vpc_id - private_subnet_ids = module.networking.private_subnet_ids - security_group_ids = module.networking.private_security_group_ids - storage_filesystem_id = module.database.filesystem_id - storage_mount_targets = module.database.mount_target_ids - autoscaling_cpu_target = var.autoscaling_cpu_target - autoscaling_memory_target = var.autoscaling_memory_target - tags = local.common_tags -} - -# ── Compute (Green slot) ─────────────────────────────────────────────────── - -module "compute_green" { - source = "./modules/compute" - - project_name = var.project_name - environment = var.environment - cloud_provider = var.cloud_provider - region = var.region - deployment_slot = "green" - container_image = var.app_container_image - mcp_container_image = var.mcp_container_image - app_port = var.app_port - mcp_port = var.mcp_port - cpu = var.cpu - memory = var.memory - desired_count = var.active_deployment_slot == "green" ? var.desired_replicas : 0 - min_count = var.active_deployment_slot == "green" ? var.min_replicas : 0 - max_count = var.active_deployment_slot == "green" ? var.max_replicas : 0 - environment_variables = var.environment_variables - health_check_path = var.health_check_path - vpc_id = module.networking.vpc_id - private_subnet_ids = module.networking.private_subnet_ids - security_group_ids = module.networking.private_security_group_ids - storage_filesystem_id = module.database.filesystem_id - storage_mount_targets = module.database.mount_target_ids - autoscaling_cpu_target = var.autoscaling_cpu_target - autoscaling_memory_target = var.autoscaling_memory_target - tags = local.common_tags -} - -# ── Load balancer ─────────────────────────────────────────────────────────── - -module "loadbalancer" { - source = "./modules/loadbalancer" - - project_name = var.project_name - environment = var.environment - cloud_provider = var.cloud_provider - region = var.region - vpc_id = module.networking.vpc_id - public_subnet_ids = module.networking.public_subnet_ids - security_group_ids = module.networking.public_security_group_ids - app_port = var.app_port - mcp_port = var.mcp_port - tls_certificate_arn = var.tls_certificate_arn - domain_name = var.domain_name - - blue_target_group_arn = module.compute_blue.target_group_arn - green_target_group_arn = module.compute_green.target_group_arn - blue_weight = var.blue_weight - green_weight = var.green_weight - - health_check_path = var.health_check_path - health_check_interval = var.health_check_interval - health_check_timeout = var.health_check_timeout - health_check_healthy_threshold = var.health_check_healthy_threshold - health_check_unhealthy_threshold = var.health_check_unhealthy_threshold - - enable_deletion_protection = var.environment == "production" - - tags = local.common_tags -} - -# ── Weight sum validation ─────────────────────────────────────────────────── - -check "blue_green_weight_sum" { - assert { - condition = var.blue_weight + var.green_weight == 100 - error_message = "blue_weight (${var.blue_weight}) + green_weight (${var.green_weight}) must sum to 100." - } -} - -# ── Monitoring ────────────────────────────────────────────────────────────── - -module "monitoring" { - source = "./modules/monitoring" - count = var.enable_monitoring ? 1 : 0 - - project_name = var.project_name - environment = var.environment - cloud_provider = var.cloud_provider - region = var.region - alert_email = var.alert_email - log_retention_days = var.log_retention_days - - loadbalancer_arn = module.loadbalancer.loadbalancer_arn - target_group_arns = [ - module.compute_blue.target_group_arn, - module.compute_green.target_group_arn, - ] - compute_cluster_name = module.compute_blue.cluster_name - filesystem_id = module.database.filesystem_id - - tags = local.common_tags -} diff --git a/deployments/terraform/modules/compute/main.tf b/deployments/terraform/modules/compute/main.tf deleted file mode 100644 index 29b8c4f..0000000 --- a/deployments/terraform/modules/compute/main.tf +++ /dev/null @@ -1,396 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# Compute module – Container orchestration with blue/green slot support -# -# Provisions an ECS Fargate service with: -# - Main application container (Express + React) -# - MCP sidecar container -# - EFS persistent volume for SQLite -# - Auto-scaling policies -# - Blue/green deployment slot via variable -# ───────────────────────────────────────────────────────────────────────────── - -locals { - name_prefix = lower(replace("${var.project_name}-${var.environment}", "_", "-")) - slot_name = "${local.name_prefix}-${var.deployment_slot}" - - common_tags = merge( - { - module = "compute" - deployment_slot = var.deployment_slot - }, - var.tags, - ) -} - -# ───────────────────────────────────────────────────────────────────────────── -# ECS Cluster (shared across slots) -# ───────────────────────────────────────────────────────────────────────────── - -resource "aws_ecs_cluster" "main" { - name = "${local.name_prefix}-cluster" - - setting { - name = "containerInsights" - value = "enabled" - } - - configuration { - execute_command_configuration { - logging = "DEFAULT" - } - } - - tags = merge(local.common_tags, { - Name = "${local.name_prefix}-cluster" - }) - - lifecycle { - prevent_destroy = false - } -} - -resource "aws_ecs_cluster_capacity_providers" "main" { - cluster_name = aws_ecs_cluster.main.name - - capacity_providers = ["FARGATE", "FARGATE_SPOT"] - - default_capacity_provider_strategy { - base = 1 - weight = 1 - capacity_provider = "FARGATE" - } -} - -# ───────────────────────────────────────────────────────────────────────────── -# IAM roles -# ───────────────────────────────────────────────────────────────────────────── - -data "aws_region" "current" {} -data "aws_caller_identity" "current" {} - -resource "aws_iam_role" "task_execution" { - name = "${local.slot_name}-task-exec" - - assume_role_policy = jsonencode({ - Version = "2012-10-17" - Statement = [{ - Action = "sts:AssumeRole" - Effect = "Allow" - Principal = { - Service = "ecs-tasks.amazonaws.com" - } - }] - }) - - tags = local.common_tags -} - -resource "aws_iam_role_policy_attachment" "task_execution" { - role = aws_iam_role.task_execution.name - policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" -} - -resource "aws_iam_role" "task" { - name = "${local.slot_name}-task" - - assume_role_policy = jsonencode({ - Version = "2012-10-17" - Statement = [{ - Action = "sts:AssumeRole" - Effect = "Allow" - Principal = { - Service = "ecs-tasks.amazonaws.com" - } - }] - }) - - tags = local.common_tags -} - -# EFS access policy for the task role -resource "aws_iam_role_policy" "task_efs" { - name = "${local.slot_name}-efs-access" - role = aws_iam_role.task.id - - policy = jsonencode({ - Version = "2012-10-17" - Statement = [{ - Effect = "Allow" - Action = [ - "elasticfilesystem:ClientMount", - "elasticfilesystem:ClientWrite", - "elasticfilesystem:ClientRootAccess", - ] - Resource = "arn:aws:elasticfilesystem:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:file-system/${var.storage_filesystem_id}" - }] - }) -} - -# ───────────────────────────────────────────────────────────────────────────── -# CloudWatch log group -# ───────────────────────────────────────────────────────────────────────────── - -resource "aws_cloudwatch_log_group" "app" { - name = "/ecs/${local.slot_name}" - retention_in_days = 30 - - tags = local.common_tags -} - -# ───────────────────────────────────────────────────────────────────────────── -# Task definition -# ───────────────────────────────────────────────────────────────────────────── - -resource "aws_ecs_task_definition" "app" { - family = local.slot_name - network_mode = "awsvpc" - requires_compatibilities = ["FARGATE"] - cpu = tostring(var.cpu) - memory = tostring(var.memory) - execution_role_arn = aws_iam_role.task_execution.arn - task_role_arn = aws_iam_role.task.arn - - container_definitions = jsonencode(concat( - [ - { - name = "app" - image = var.container_image - essential = true - cpu = var.mcp_container_image != "" ? floor(var.cpu * 0.75) : var.cpu - memory = var.mcp_container_image != "" ? floor(var.memory * 0.75) : var.memory - - portMappings = [ - { - containerPort = var.app_port - protocol = "tcp" - } - ] - - environment = [ - for k, v in var.environment_variables : { - name = k - value = v - } - ] - - mountPoints = [ - { - sourceVolume = "app-data" - containerPath = "/app/data" - readOnly = false - } - ] - - healthCheck = { - command = ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:${var.app_port}${var.health_check_path} || exit 1"] - interval = 30 - timeout = 5 - retries = 3 - startPeriod = 60 - } - - logConfiguration = { - logDriver = "awslogs" - options = { - "awslogs-group" = aws_cloudwatch_log_group.app.name - "awslogs-region" = data.aws_region.current.name - "awslogs-stream-prefix" = "app" - } - } - } - ], - var.mcp_container_image != "" ? [ - { - name = "mcp-sidecar" - image = var.mcp_container_image - essential = false - cpu = floor(var.cpu * 0.25) - memory = floor(var.memory * 0.25) - - portMappings = [ - { - containerPort = var.mcp_port - protocol = "tcp" - } - ] - - environment = [ - { - name = "NODE_ENV" - value = "production" - }, - { - name = "MCP_PORT" - value = tostring(var.mcp_port) - } - ] - - logConfiguration = { - logDriver = "awslogs" - options = { - "awslogs-group" = aws_cloudwatch_log_group.app.name - "awslogs-region" = data.aws_region.current.name - "awslogs-stream-prefix" = "mcp" - } - } - } - ] : [] - )) - - volume { - name = "app-data" - - efs_volume_configuration { - file_system_id = var.storage_filesystem_id - root_directory = "/" - transit_encryption = "ENABLED" - - authorization_config { - iam = "ENABLED" - } - } - } - - tags = local.common_tags - - lifecycle { - create_before_destroy = true - } -} - -# ───────────────────────────────────────────────────────────────────────────── -# Target group (registered with LB by the loadbalancer module) -# ───────────────────────────────────────────────────────────────────────────── - -resource "aws_lb_target_group" "app" { - name_prefix = substr(var.deployment_slot, 0, 5) - port = var.app_port - protocol = "HTTP" - vpc_id = var.vpc_id - target_type = "ip" - - health_check { - enabled = true - path = var.health_check_path - port = "traffic-port" - protocol = "HTTP" - healthy_threshold = 2 - unhealthy_threshold = 3 - timeout = 5 - interval = 30 - matcher = "200" - } - - stickiness { - type = "lb_cookie" - cookie_duration = 86400 - enabled = true - } - - deregistration_delay = 60 - - tags = merge(local.common_tags, { - Name = "${local.slot_name}-tg" - }) - - lifecycle { - create_before_destroy = true - } -} - -# ───────────────────────────────────────────────────────────────────────────── -# ECS Service -# ───────────────────────────────────────────────────────────────────────────── - -resource "aws_ecs_service" "app" { - name = local.slot_name - cluster = aws_ecs_cluster.main.id - task_definition = aws_ecs_task_definition.app.arn - desired_count = var.desired_count - launch_type = "FARGATE" - platform_version = "LATEST" - health_check_grace_period_seconds = 120 - enable_execute_command = var.environment != "production" - - network_configuration { - subnets = var.private_subnet_ids - security_groups = var.security_group_ids - assign_public_ip = false - } - - load_balancer { - target_group_arn = aws_lb_target_group.app.arn - container_name = "app" - container_port = var.app_port - } - - deployment_configuration { - maximum_percent = 200 - minimum_healthy_percent = 100 - } - - deployment_circuit_breaker { - enable = true - rollback = true - } - - tags = local.common_tags - - lifecycle { - ignore_changes = [desired_count] - } -} - -# ───────────────────────────────────────────────────────────────────────────── -# Auto-scaling -# ───────────────────────────────────────────────────────────────────────────── - -resource "aws_appautoscaling_target" "ecs" { - count = var.max_count > 0 ? 1 : 0 - - max_capacity = var.max_count - min_capacity = var.min_count - resource_id = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.app.name}" - scalable_dimension = "ecs:service:DesiredCount" - service_namespace = "ecs" - - tags = local.common_tags -} - -resource "aws_appautoscaling_policy" "cpu" { - count = var.max_count > 0 ? 1 : 0 - - name = "${local.slot_name}-cpu-scaling" - policy_type = "TargetTrackingScaling" - resource_id = aws_appautoscaling_target.ecs[0].resource_id - scalable_dimension = aws_appautoscaling_target.ecs[0].scalable_dimension - service_namespace = aws_appautoscaling_target.ecs[0].service_namespace - - target_tracking_scaling_policy_configuration { - predefined_metric_specification { - predefined_metric_type = "ECSServiceAverageCPUUtilization" - } - target_value = var.autoscaling_cpu_target - scale_in_cooldown = 300 - scale_out_cooldown = 60 - } -} - -resource "aws_appautoscaling_policy" "memory" { - count = var.max_count > 0 ? 1 : 0 - - name = "${local.slot_name}-memory-scaling" - policy_type = "TargetTrackingScaling" - resource_id = aws_appautoscaling_target.ecs[0].resource_id - scalable_dimension = aws_appautoscaling_target.ecs[0].scalable_dimension - service_namespace = aws_appautoscaling_target.ecs[0].service_namespace - - target_tracking_scaling_policy_configuration { - predefined_metric_specification { - predefined_metric_type = "ECSServiceAverageMemoryUtilization" - } - target_value = var.autoscaling_memory_target - scale_in_cooldown = 300 - scale_out_cooldown = 60 - } -} diff --git a/deployments/terraform/modules/compute/outputs.tf b/deployments/terraform/modules/compute/outputs.tf deleted file mode 100644 index 269df04..0000000 --- a/deployments/terraform/modules/compute/outputs.tf +++ /dev/null @@ -1,53 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# Compute module outputs -# ───────────────────────────────────────────────────────────────────────────── - -output "cluster_name" { - description = "Name of the ECS cluster" - value = aws_ecs_cluster.main.name -} - -output "cluster_arn" { - description = "ARN of the ECS cluster" - value = aws_ecs_cluster.main.arn -} - -output "service_name" { - description = "Name of the ECS service for this slot" - value = aws_ecs_service.app.name -} - -output "service_arn" { - description = "ARN of the ECS service for this slot" - value = aws_ecs_service.app.id -} - -output "task_definition_arn" { - description = "ARN of the current task definition" - value = aws_ecs_task_definition.app.arn -} - -output "target_group_arn" { - description = "ARN of the target group for LB registration" - value = aws_lb_target_group.app.arn -} - -output "target_group_name" { - description = "Name of the target group" - value = aws_lb_target_group.app.name -} - -output "log_group_name" { - description = "CloudWatch log group name" - value = aws_cloudwatch_log_group.app.name -} - -output "task_execution_role_arn" { - description = "ARN of the task execution IAM role" - value = aws_iam_role.task_execution.arn -} - -output "task_role_arn" { - description = "ARN of the task IAM role" - value = aws_iam_role.task.arn -} diff --git a/deployments/terraform/modules/compute/variables.tf b/deployments/terraform/modules/compute/variables.tf deleted file mode 100644 index 986f127..0000000 --- a/deployments/terraform/modules/compute/variables.tf +++ /dev/null @@ -1,146 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# Compute module variables -# ───────────────────────────────────────────────────────────────────────────── - -variable "project_name" { - description = "Project identifier used in resource naming" - type = string -} - -variable "environment" { - description = "Deployment environment (dev, staging, production)" - type = string -} - -variable "cloud_provider" { - description = "Target cloud provider (aws, gcp, azure, oci)" - type = string -} - -variable "region" { - description = "Cloud region for deployment" - type = string -} - -variable "deployment_slot" { - description = "Deployment slot identifier for blue-green: blue or green" - type = string - default = "blue" - validation { - condition = contains(["blue", "green"], var.deployment_slot) - error_message = "deployment_slot must be blue or green." - } -} - -variable "container_image" { - description = "Container image URI for the main application" - type = string -} - -variable "mcp_container_image" { - description = "Container image URI for the MCP sidecar (empty to disable)" - type = string - default = "" -} - -variable "app_port" { - description = "Port the application container listens on" - type = number - default = 4820 -} - -variable "mcp_port" { - description = "Port the MCP sidecar container listens on" - type = number - default = 8819 -} - -variable "cpu" { - description = "CPU units for the task (256, 512, 1024, 2048, 4096)" - type = number - default = 512 - validation { - condition = contains([256, 512, 1024, 2048, 4096], var.cpu) - error_message = "cpu must be one of: 256, 512, 1024, 2048, 4096." - } -} - -variable "memory" { - description = "Memory in MiB for the task" - type = number - default = 1024 -} - -variable "desired_count" { - description = "Desired number of running task instances" - type = number - default = 1 -} - -variable "min_count" { - description = "Minimum number of task instances for auto-scaling" - type = number - default = 1 -} - -variable "max_count" { - description = "Maximum number of task instances for auto-scaling" - type = number - default = 3 -} - -variable "environment_variables" { - description = "Map of environment variables for the application container" - type = map(string) - default = {} -} - -variable "health_check_path" { - description = "HTTP path for container health checks" - type = string - default = "/api/health" -} - -variable "vpc_id" { - description = "VPC ID for target group and networking" - type = string -} - -variable "private_subnet_ids" { - description = "Subnet IDs where tasks will be placed" - type = list(string) -} - -variable "security_group_ids" { - description = "Security group IDs attached to task ENIs" - type = list(string) -} - -variable "storage_filesystem_id" { - description = "EFS file system ID for persistent SQLite storage" - type = string -} - -variable "storage_mount_targets" { - description = "EFS mount target IDs (ensures mount targets exist before service)" - type = list(string) - default = [] -} - -variable "autoscaling_cpu_target" { - description = "Target CPU utilization percentage for auto-scaling" - type = number - default = 70 -} - -variable "autoscaling_memory_target" { - description = "Target memory utilization percentage for auto-scaling" - type = number - default = 80 -} - -variable "tags" { - description = "Resource tags" - type = map(string) - default = {} -} diff --git a/deployments/terraform/modules/database/main.tf b/deployments/terraform/modules/database/main.tf deleted file mode 100644 index 99a2683..0000000 --- a/deployments/terraform/modules/database/main.tf +++ /dev/null @@ -1,142 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# Database module – Persistent storage for SQLite -# -# Creates a managed network file system (EFS on AWS) with: -# - Encryption at rest and in transit -# - Automated backup policy -# - Mount targets in each private subnet -# - Performance mode optimised for SQLite workloads -# ───────────────────────────────────────────────────────────────────────────── - -locals { - name_prefix = lower(replace("${var.project_name}-${var.environment}", "_", "-")) - - common_tags = merge( - { - module = "database" - }, - var.tags, - ) -} - -# ───────────────────────────────────────────────────────────────────────────── -# EFS file system -# ───────────────────────────────────────────────────────────────────────────── - -resource "aws_efs_file_system" "main" { - creation_token = "${local.name_prefix}-data" - encrypted = true - - # General Purpose is optimal for SQLite (latency-sensitive small I/O) - performance_mode = "generalPurpose" - throughput_mode = "elastic" - - lifecycle_policy { - transition_to_ia = "AFTER_30_DAYS" - } - - lifecycle_policy { - transition_to_primary_storage_class = "AFTER_1_ACCESS" - } - - tags = merge(local.common_tags, { - Name = "${local.name_prefix}-efs" - }) - - lifecycle { - prevent_destroy = true - } -} - -# ───────────────────────────────────────────────────────────────────────────── -# EFS backup policy -# ───────────────────────────────────────────────────────────────────────────── - -resource "aws_efs_backup_policy" "main" { - file_system_id = aws_efs_file_system.main.id - - backup_policy { - status = var.enable_backup ? "ENABLED" : "DISABLED" - } -} - -# ───────────────────────────────────────────────────────────────────────────── -# EFS mount targets (one per private subnet / AZ) -# ───────────────────────────────────────────────────────────────────────────── - -resource "aws_efs_mount_target" "main" { - count = length(var.private_subnet_ids) - - file_system_id = aws_efs_file_system.main.id - subnet_id = var.private_subnet_ids[count.index] - security_groups = var.allowed_security_group_ids -} - -# ───────────────────────────────────────────────────────────────────────────── -# EFS access point – scoped to /app/data for the container workload -# ───────────────────────────────────────────────────────────────────────────── - -resource "aws_efs_access_point" "app_data" { - file_system_id = aws_efs_file_system.main.id - - posix_user { - uid = 1000 - gid = 1000 - } - - root_directory { - path = "/app-data" - - creation_info { - owner_uid = 1000 - owner_gid = 1000 - permissions = "0755" - } - } - - tags = merge(local.common_tags, { - Name = "${local.name_prefix}-app-data-ap" - }) -} - -# ───────────────────────────────────────────────────────────────────────────── -# EFS file system policy – enforce encryption in transit -# ───────────────────────────────────────────────────────────────────────────── - -resource "aws_efs_file_system_policy" "main" { - file_system_id = aws_efs_file_system.main.id - - policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Sid = "EnforceEncryptInTransit" - Effect = "Deny" - Principal = { AWS = "*" } - Action = "*" - Resource = aws_efs_file_system.main.arn - Condition = { - Bool = { - "aws:SecureTransport" = "false" - } - } - }, - { - Sid = "AllowMountViaAccessPoint" - Effect = "Allow" - Principal = { AWS = "*" } - Action = [ - "elasticfilesystem:ClientMount", - "elasticfilesystem:ClientWrite", - "elasticfilesystem:ClientRootAccess", - ] - Resource = aws_efs_file_system.main.arn - Condition = { - Bool = { - "elasticfilesystem:AccessedViaMountTarget" = "true" - } - } - } - ] - }) -} diff --git a/deployments/terraform/modules/database/outputs.tf b/deployments/terraform/modules/database/outputs.tf deleted file mode 100644 index 64f6091..0000000 --- a/deployments/terraform/modules/database/outputs.tf +++ /dev/null @@ -1,38 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# Database module outputs -# ───────────────────────────────────────────────────────────────────────────── - -output "filesystem_id" { - description = "ID of the EFS file system" - value = aws_efs_file_system.main.id -} - -output "filesystem_arn" { - description = "ARN of the EFS file system" - value = aws_efs_file_system.main.arn -} - -output "filesystem_dns_name" { - description = "DNS name of the EFS file system" - value = aws_efs_file_system.main.dns_name -} - -output "mount_target_ids" { - description = "IDs of the EFS mount targets" - value = aws_efs_mount_target.main[*].id -} - -output "mount_target_ips" { - description = "IP addresses of the EFS mount targets" - value = aws_efs_mount_target.main[*].ip_address -} - -output "access_point_id" { - description = "ID of the EFS access point for /app/data" - value = aws_efs_access_point.app_data.id -} - -output "access_point_arn" { - description = "ARN of the EFS access point" - value = aws_efs_access_point.app_data.arn -} diff --git a/deployments/terraform/modules/database/variables.tf b/deployments/terraform/modules/database/variables.tf deleted file mode 100644 index 99b153e..0000000 --- a/deployments/terraform/modules/database/variables.tf +++ /dev/null @@ -1,60 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# Database module variables -# ───────────────────────────────────────────────────────────────────────────── - -variable "project_name" { - description = "Project identifier used in resource naming" - type = string -} - -variable "environment" { - description = "Deployment environment (dev, staging, production)" - type = string -} - -variable "cloud_provider" { - description = "Target cloud provider (aws, gcp, azure, oci)" - type = string -} - -variable "region" { - description = "Cloud region for deployment" - type = string -} - -variable "storage_size_gb" { - description = "Storage allocation in GiB (used by providers with provisioned capacity)" - type = number - default = 20 - validation { - condition = var.storage_size_gb >= 1 - error_message = "storage_size_gb must be at least 1 GiB." - } -} - -variable "enable_backup" { - description = "Enable automated backup of the file system" - type = bool - default = true -} - -variable "private_subnet_ids" { - description = "Private subnet IDs for mount targets" - type = list(string) -} - -variable "vpc_id" { - description = "VPC ID for security group association" - type = string -} - -variable "allowed_security_group_ids" { - description = "Security group IDs allowed to mount the file system" - type = list(string) -} - -variable "tags" { - description = "Resource tags" - type = map(string) - default = {} -} diff --git a/deployments/terraform/modules/loadbalancer/main.tf b/deployments/terraform/modules/loadbalancer/main.tf deleted file mode 100644 index 5b0036c..0000000 --- a/deployments/terraform/modules/loadbalancer/main.tf +++ /dev/null @@ -1,247 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# Load Balancer module – Application load balancer with WebSocket support -# -# Provisions: -# - ALB in public subnets -# - HTTPS listener with TLS termination -# - HTTP → HTTPS redirect -# - Weighted target groups for blue/green and canary deployments -# - Sticky sessions for WebSocket connections -# - Path-based routing for MCP sidecar (/mcp/*) -# - Health checks at /api/health -# ───────────────────────────────────────────────────────────────────────────── - -# Production TLS enforcement — prevents deploying production without encryption -check "production_tls_required" { - assert { - condition = var.environment != "production" || var.tls_certificate_arn != "" - error_message = "Production deployments require TLS. Set tls_certificate_arn and domain_name." - } -} - -check "production_domain_required" { - assert { - condition = var.environment != "production" || var.domain_name != "" - error_message = "Production deployments require a domain name. Set domain_name." - } -} - -locals { - name_prefix = lower(replace("${var.project_name}-${var.environment}", "_", "-")) - - # Determine whether TLS is configured - has_tls = var.tls_certificate_arn != "" - - common_tags = merge( - { - module = "loadbalancer" - }, - var.tags, - ) -} - -# ───────────────────────────────────────────────────────────────────────────── -# Application Load Balancer -# ───────────────────────────────────────────────────────────────────────────── - -resource "aws_lb" "main" { - name = "${local.name_prefix}-alb" - internal = false - load_balancer_type = "application" - security_groups = var.security_group_ids - subnets = var.public_subnet_ids - - enable_deletion_protection = var.environment == "production" ? true : var.enable_deletion_protection - enable_http2 = true - idle_timeout = 300 # WebSocket connections may be long-lived - - drop_invalid_header_fields = true - - tags = merge(local.common_tags, { - Name = "${local.name_prefix}-alb" - }) - - lifecycle { - prevent_destroy = false - # For production, set enable_deletion_protection = true above (enforced automatically) - } -} - -# ───────────────────────────────────────────────────────────────────────────── -# HTTPS listener (primary – with weighted target groups for blue/green) -# ───────────────────────────────────────────────────────────────────────────── - -resource "aws_lb_listener" "https" { - count = local.has_tls ? 1 : 0 - - load_balancer_arn = aws_lb.main.arn - port = 443 - protocol = "HTTPS" - ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06" - certificate_arn = var.tls_certificate_arn - - default_action { - type = "forward" - - forward { - target_group { - arn = var.blue_target_group_arn - weight = var.blue_weight - } - - target_group { - arn = var.green_target_group_arn - weight = var.green_weight - } - - stickiness { - enabled = true - duration = 86400 - } - } - } - - tags = local.common_tags -} - -# ───────────────────────────────────────────────────────────────────────────── -# HTTP listener – redirect to HTTPS when TLS is configured, else forward -# ───────────────────────────────────────────────────────────────────────────── - -resource "aws_lb_listener" "http_redirect" { - count = local.has_tls ? 1 : 0 - - load_balancer_arn = aws_lb.main.arn - port = 80 - protocol = "HTTP" - - default_action { - type = "redirect" - - redirect { - port = "443" - protocol = "HTTPS" - status_code = "HTTP_301" - } - } - - tags = local.common_tags -} - -resource "aws_lb_listener" "http_forward" { - count = local.has_tls ? 0 : 1 - - load_balancer_arn = aws_lb.main.arn - port = 80 - protocol = "HTTP" - - default_action { - type = "forward" - - forward { - target_group { - arn = var.blue_target_group_arn - weight = var.blue_weight - } - - target_group { - arn = var.green_target_group_arn - weight = var.green_weight - } - - stickiness { - enabled = true - duration = 86400 - } - } - } - - tags = local.common_tags -} - -# ───────────────────────────────────────────────────────────────────────────── -# MCP sidecar target group -# ───────────────────────────────────────────────────────────────────────────── - -resource "aws_lb_target_group" "mcp" { - name_prefix = "mcp-" - port = var.mcp_port - protocol = "HTTP" - vpc_id = var.vpc_id - target_type = "ip" - - health_check { - enabled = true - path = "/" - port = tostring(var.mcp_port) - protocol = "HTTP" - healthy_threshold = var.health_check_healthy_threshold - unhealthy_threshold = var.health_check_unhealthy_threshold - timeout = var.health_check_timeout - interval = var.health_check_interval - matcher = "200-404" - } - - tags = merge(local.common_tags, { - Name = "${local.name_prefix}-mcp-tg" - }) - - lifecycle { - create_before_destroy = true - } -} - -# ───────────────────────────────────────────────────────────────────────────── -# Path-based routing rule for MCP sidecar (/mcp/*) -# ───────────────────────────────────────────────────────────────────────────── - -resource "aws_lb_listener_rule" "mcp_https" { - count = local.has_tls ? 1 : 0 - - listener_arn = aws_lb_listener.https[0].arn - priority = 10 - - action { - type = "forward" - target_group_arn = aws_lb_target_group.mcp.arn - } - - condition { - path_pattern { - values = ["/mcp", "/mcp/*"] - } - } - - tags = local.common_tags -} - -resource "aws_lb_listener_rule" "mcp_http" { - count = local.has_tls ? 0 : 1 - - listener_arn = aws_lb_listener.http_forward[0].arn - priority = 10 - - action { - type = "forward" - target_group_arn = aws_lb_target_group.mcp.arn - } - - condition { - path_pattern { - values = ["/mcp", "/mcp/*"] - } - } - - tags = local.common_tags -} - -# ───────────────────────────────────────────────────────────────────────────── -# Weight sum validation -# ───────────────────────────────────────────────────────────────────────────── - -check "lb_weight_sum" { - assert { - condition = var.blue_weight + var.green_weight == 100 - error_message = "blue_weight (${var.blue_weight}) + green_weight (${var.green_weight}) must sum to 100 for correct traffic routing." - } -} diff --git a/deployments/terraform/modules/loadbalancer/outputs.tf b/deployments/terraform/modules/loadbalancer/outputs.tf deleted file mode 100644 index b16efdb..0000000 --- a/deployments/terraform/modules/loadbalancer/outputs.tf +++ /dev/null @@ -1,43 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# Load Balancer module outputs -# ───────────────────────────────────────────────────────────────────────────── - -output "loadbalancer_arn" { - description = "ARN of the application load balancer" - value = aws_lb.main.arn -} - -output "loadbalancer_id" { - description = "ID of the application load balancer" - value = aws_lb.main.id -} - -output "dns_name" { - description = "DNS name of the application load balancer" - value = aws_lb.main.dns_name -} - -output "zone_id" { - description = "Route53 zone ID for the load balancer (alias records)" - value = aws_lb.main.zone_id -} - -output "application_url" { - description = "Full URL to access the application" - value = local.has_tls ? "https://${var.domain_name != "" ? var.domain_name : aws_lb.main.dns_name}" : "http://${aws_lb.main.dns_name}" -} - -output "https_listener_arn" { - description = "ARN of the HTTPS listener (empty if TLS not configured)" - value = local.has_tls ? aws_lb_listener.https[0].arn : "" -} - -output "http_listener_arn" { - description = "ARN of the HTTP listener" - value = local.has_tls ? aws_lb_listener.http_redirect[0].arn : aws_lb_listener.http_forward[0].arn -} - -output "mcp_target_group_arn" { - description = "ARN of the MCP sidecar target group" - value = aws_lb_target_group.mcp.arn -} diff --git a/deployments/terraform/modules/loadbalancer/variables.tf b/deployments/terraform/modules/loadbalancer/variables.tf deleted file mode 100644 index 449fe23..0000000 --- a/deployments/terraform/modules/loadbalancer/variables.tf +++ /dev/null @@ -1,134 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# Load Balancer module variables -# ───────────────────────────────────────────────────────────────────────────── - -variable "project_name" { - description = "Project identifier used in resource naming" - type = string -} - -variable "environment" { - description = "Deployment environment (dev, staging, production)" - type = string -} - -variable "cloud_provider" { - description = "Target cloud provider (aws, gcp, azure, oci)" - type = string -} - -variable "region" { - description = "Cloud region for deployment" - type = string -} - -variable "vpc_id" { - description = "VPC ID for target group association" - type = string -} - -variable "public_subnet_ids" { - description = "Public subnet IDs for load balancer placement" - type = list(string) -} - -variable "security_group_ids" { - description = "Security group IDs attached to the load balancer" - type = list(string) -} - -variable "app_port" { - description = "Application container port" - type = number - default = 4820 -} - -variable "mcp_port" { - description = "MCP sidecar container port" - type = number - default = 8819 -} - -variable "tls_certificate_arn" { - description = "ARN of the TLS certificate for HTTPS (required for production)" - type = string - default = "" -} - -variable "domain_name" { - description = "Fully qualified domain name for the application (required for production)" - type = string - default = "" -} - -variable "blue_target_group_arn" { - description = "ARN of the blue deployment target group" - type = string -} - -variable "green_target_group_arn" { - description = "ARN of the green deployment target group" - type = string -} - -variable "blue_weight" { - description = "Traffic weight for blue target group (0-100)" - type = number - default = 100 - validation { - condition = var.blue_weight >= 0 && var.blue_weight <= 100 - error_message = "blue_weight must be between 0 and 100." - } -} - -variable "green_weight" { - description = "Traffic weight for green target group (0-100)" - type = number - default = 0 - validation { - condition = var.green_weight >= 0 && var.green_weight <= 100 - error_message = "green_weight must be between 0 and 100." - } -} - -variable "health_check_path" { - description = "HTTP path for health checks" - type = string - default = "/api/health" -} - -variable "health_check_interval" { - description = "Seconds between health checks" - type = number - default = 30 -} - -variable "health_check_timeout" { - description = "Seconds before a health check times out" - type = number - default = 5 -} - -variable "health_check_healthy_threshold" { - description = "Consecutive successes to mark healthy" - type = number - default = 2 -} - -variable "health_check_unhealthy_threshold" { - description = "Consecutive failures to mark unhealthy" - type = number - default = 3 -} - -variable "tags" { - description = "Resource tags" - type = map(string) - default = {} -} - -variable "enable_deletion_protection" { - description = "Enable deletion protection on the load balancer (recommended for production)" - type = bool - default = false -} diff --git a/deployments/terraform/modules/monitoring/main.tf b/deployments/terraform/modules/monitoring/main.tf deleted file mode 100644 index 2ccc9af..0000000 --- a/deployments/terraform/modules/monitoring/main.tf +++ /dev/null @@ -1,312 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# Monitoring module – Observability, alerting, and dashboards -# -# Provisions: -# - CloudWatch log groups for centralized log aggregation -# - Metric alarms for error rate, latency, disk, unhealthy hosts -# - SNS topic for alert notifications -# - CloudWatch dashboard with key operational metrics -# ───────────────────────────────────────────────────────────────────────────── - -locals { - name_prefix = lower(replace("${var.project_name}-${var.environment}", "_", "-")) - - common_tags = merge( - { - module = "monitoring" - }, - var.tags, - ) - - # Parse ALB ARN suffix for CloudWatch metric dimensions - alb_arn_suffix = try( - regex("app/.*$", var.loadbalancer_arn), - "" - ) -} - -# ───────────────────────────────────────────────────────────────────────────── -# SNS topic for alert notifications -# ───────────────────────────────────────────────────────────────────────────── - -resource "aws_sns_topic" "alerts" { - name = "${local.name_prefix}-alerts" - - tags = local.common_tags -} - -resource "aws_sns_topic_subscription" "email" { - count = var.alert_email != "" ? 1 : 0 - - topic_arn = aws_sns_topic.alerts.arn - protocol = "email" - endpoint = var.alert_email -} - -# ───────────────────────────────────────────────────────────────────────────── -# CloudWatch log group (application-level) -# ───────────────────────────────────────────────────────────────────────────── - -resource "aws_cloudwatch_log_group" "application" { - name = "/ccam/${local.name_prefix}" - retention_in_days = var.log_retention_days - - tags = local.common_tags -} - -# ───────────────────────────────────────────────────────────────────────────── -# Metric alarms -# ───────────────────────────────────────────────────────────────────────────── - -# High 5xx error rate from ALB -resource "aws_cloudwatch_metric_alarm" "high_5xx_rate" { - alarm_name = "${local.name_prefix}-high-5xx-error-rate" - comparison_operator = "GreaterThanThreshold" - evaluation_periods = 3 - metric_name = "HTTPCode_Target_5XX_Count" - namespace = "AWS/ApplicationELB" - period = 60 - statistic = "Sum" - threshold = 10 - alarm_description = "High 5XX error rate detected on ${local.name_prefix} ALB" - treat_missing_data = "notBreaching" - - dimensions = { - LoadBalancer = local.alb_arn_suffix - } - - alarm_actions = [aws_sns_topic.alerts.arn] - ok_actions = [aws_sns_topic.alerts.arn] - - tags = local.common_tags -} - -# High target response time (latency) -resource "aws_cloudwatch_metric_alarm" "high_latency" { - alarm_name = "${local.name_prefix}-high-latency" - comparison_operator = "GreaterThanThreshold" - evaluation_periods = 3 - metric_name = "TargetResponseTime" - namespace = "AWS/ApplicationELB" - period = 60 - statistic = "Average" - threshold = 2.0 # seconds - alarm_description = "High average latency (>2s) on ${local.name_prefix} ALB" - treat_missing_data = "notBreaching" - - dimensions = { - LoadBalancer = local.alb_arn_suffix - } - - alarm_actions = [aws_sns_topic.alerts.arn] - ok_actions = [aws_sns_topic.alerts.arn] - - tags = local.common_tags -} - -# Unhealthy host count -resource "aws_cloudwatch_metric_alarm" "unhealthy_hosts" { - count = length(var.target_group_arns) - - alarm_name = "${local.name_prefix}-unhealthy-hosts-${count.index}" - comparison_operator = "GreaterThanThreshold" - evaluation_periods = 2 - metric_name = "UnHealthyHostCount" - namespace = "AWS/ApplicationELB" - period = 60 - statistic = "Maximum" - threshold = 0 - alarm_description = "Unhealthy targets detected in target group ${count.index}" - treat_missing_data = "notBreaching" - - dimensions = { - LoadBalancer = local.alb_arn_suffix - TargetGroup = try(regex("targetgroup/.*$", var.target_group_arns[count.index]), "") - } - - alarm_actions = [aws_sns_topic.alerts.arn] - ok_actions = [aws_sns_topic.alerts.arn] - - tags = local.common_tags -} - -# EFS burst credit balance (low disk throughput) -resource "aws_cloudwatch_metric_alarm" "efs_burst_credits" { - alarm_name = "${local.name_prefix}-efs-low-burst-credits" - comparison_operator = "LessThanThreshold" - evaluation_periods = 3 - metric_name = "BurstCreditBalance" - namespace = "AWS/EFS" - period = 300 - statistic = "Average" - threshold = 1000000000 # 1 GiB in bytes - alarm_description = "EFS burst credits running low for ${local.name_prefix}" - treat_missing_data = "notBreaching" - - dimensions = { - FileSystemId = var.filesystem_id - } - - alarm_actions = [aws_sns_topic.alerts.arn] - - tags = local.common_tags -} - -# ECS CPU utilisation (cluster-level) -resource "aws_cloudwatch_metric_alarm" "ecs_high_cpu" { - alarm_name = "${local.name_prefix}-ecs-high-cpu" - comparison_operator = "GreaterThanThreshold" - evaluation_periods = 3 - metric_name = "CPUUtilization" - namespace = "AWS/ECS" - period = 300 - statistic = "Average" - threshold = 85 - alarm_description = "High ECS CPU utilisation (>85%) for cluster ${var.compute_cluster_name}" - treat_missing_data = "notBreaching" - - dimensions = { - ClusterName = var.compute_cluster_name - } - - alarm_actions = [aws_sns_topic.alerts.arn] - ok_actions = [aws_sns_topic.alerts.arn] - - tags = local.common_tags -} - -# ECS Memory utilisation -resource "aws_cloudwatch_metric_alarm" "ecs_high_memory" { - alarm_name = "${local.name_prefix}-ecs-high-memory" - comparison_operator = "GreaterThanThreshold" - evaluation_periods = 3 - metric_name = "MemoryUtilization" - namespace = "AWS/ECS" - period = 300 - statistic = "Average" - threshold = 85 - alarm_description = "High ECS memory utilisation (>85%) for cluster ${var.compute_cluster_name}" - treat_missing_data = "notBreaching" - - dimensions = { - ClusterName = var.compute_cluster_name - } - - alarm_actions = [aws_sns_topic.alerts.arn] - ok_actions = [aws_sns_topic.alerts.arn] - - tags = local.common_tags -} - -# ───────────────────────────────────────────────────────────────────────────── -# CloudWatch Dashboard -# ───────────────────────────────────────────────────────────────────────────── - -resource "aws_cloudwatch_dashboard" "main" { - dashboard_name = local.name_prefix - dashboard_body = jsonencode({ - widgets = [ - { - type = "metric" - x = 0 - y = 0 - width = 12 - height = 6 - properties = { - title = "ALB Request Count" - region = var.region - metrics = [ - ["AWS/ApplicationELB", "RequestCount", "LoadBalancer", local.alb_arn_suffix, { stat = "Sum", period = 60 }] - ] - view = "timeSeries" - stacked = false - } - }, - { - type = "metric" - x = 12 - y = 0 - width = 12 - height = 6 - properties = { - title = "ALB Response Time" - region = var.region - metrics = [ - ["AWS/ApplicationELB", "TargetResponseTime", "LoadBalancer", local.alb_arn_suffix, { stat = "Average", period = 60 }], - ["AWS/ApplicationELB", "TargetResponseTime", "LoadBalancer", local.alb_arn_suffix, { stat = "p99", period = 60 }], - ] - view = "timeSeries" - stacked = false - } - }, - { - type = "metric" - x = 0 - y = 6 - width = 12 - height = 6 - properties = { - title = "HTTP Error Rates" - region = var.region - metrics = [ - ["AWS/ApplicationELB", "HTTPCode_Target_4XX_Count", "LoadBalancer", local.alb_arn_suffix, { stat = "Sum", period = 60 }], - ["AWS/ApplicationELB", "HTTPCode_Target_5XX_Count", "LoadBalancer", local.alb_arn_suffix, { stat = "Sum", period = 60 }], - ] - view = "timeSeries" - stacked = false - } - }, - { - type = "metric" - x = 12 - y = 6 - width = 12 - height = 6 - properties = { - title = "ECS CPU & Memory" - region = var.region - metrics = [ - ["AWS/ECS", "CPUUtilization", "ClusterName", var.compute_cluster_name, { stat = "Average", period = 60 }], - ["AWS/ECS", "MemoryUtilization", "ClusterName", var.compute_cluster_name, { stat = "Average", period = 60 }], - ] - view = "timeSeries" - stacked = false - } - }, - { - type = "metric" - x = 0 - y = 12 - width = 12 - height = 6 - properties = { - title = "EFS I/O" - region = var.region - metrics = [ - ["AWS/EFS", "DataReadIOBytes", "FileSystemId", var.filesystem_id, { stat = "Sum", period = 60 }], - ["AWS/EFS", "DataWriteIOBytes", "FileSystemId", var.filesystem_id, { stat = "Sum", period = 60 }], - ] - view = "timeSeries" - stacked = false - } - }, - { - type = "metric" - x = 12 - y = 12 - width = 12 - height = 6 - properties = { - title = "Healthy vs Unhealthy Hosts" - region = var.region - metrics = [ - ["AWS/ApplicationELB", "HealthyHostCount", "LoadBalancer", local.alb_arn_suffix, { stat = "Average", period = 60 }], - ["AWS/ApplicationELB", "UnHealthyHostCount", "LoadBalancer", local.alb_arn_suffix, { stat = "Average", period = 60 }], - ] - view = "timeSeries" - stacked = false - } - }, - ] - }) -} diff --git a/deployments/terraform/modules/monitoring/outputs.tf b/deployments/terraform/modules/monitoring/outputs.tf deleted file mode 100644 index 7761a64..0000000 --- a/deployments/terraform/modules/monitoring/outputs.tf +++ /dev/null @@ -1,40 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# Monitoring module outputs -# ───────────────────────────────────────────────────────────────────────────── - -output "sns_topic_arn" { - description = "ARN of the SNS alert topic" - value = aws_sns_topic.alerts.arn -} - -output "log_group_name" { - description = "Name of the CloudWatch log group" - value = aws_cloudwatch_log_group.application.name -} - -output "log_group_arn" { - description = "ARN of the CloudWatch log group" - value = aws_cloudwatch_log_group.application.arn -} - -output "dashboard_name" { - description = "Name of the CloudWatch dashboard" - value = aws_cloudwatch_dashboard.main.dashboard_name -} - -output "dashboard_url" { - description = "URL to the CloudWatch dashboard in the AWS console" - value = "https://${var.region}.console.aws.amazon.com/cloudwatch/home?region=${var.region}#dashboards:name=${aws_cloudwatch_dashboard.main.dashboard_name}" -} - -output "alarm_arns" { - description = "ARNs of all configured CloudWatch alarms" - value = concat( - [aws_cloudwatch_metric_alarm.high_5xx_rate.arn], - [aws_cloudwatch_metric_alarm.high_latency.arn], - [aws_cloudwatch_metric_alarm.efs_burst_credits.arn], - [aws_cloudwatch_metric_alarm.ecs_high_cpu.arn], - [aws_cloudwatch_metric_alarm.ecs_high_memory.arn], - aws_cloudwatch_metric_alarm.unhealthy_hosts[*].arn, - ) -} diff --git a/deployments/terraform/modules/monitoring/variables.tf b/deployments/terraform/modules/monitoring/variables.tf deleted file mode 100644 index 1ef4a11..0000000 --- a/deployments/terraform/modules/monitoring/variables.tf +++ /dev/null @@ -1,66 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# Monitoring module variables -# ───────────────────────────────────────────────────────────────────────────── - -variable "project_name" { - description = "Project identifier used in resource naming" - type = string -} - -variable "environment" { - description = "Deployment environment (dev, staging, production)" - type = string -} - -variable "cloud_provider" { - description = "Target cloud provider (aws, gcp, azure, oci)" - type = string -} - -variable "region" { - description = "Cloud region for deployment" - type = string -} - -variable "alert_email" { - description = "Email address for alert notifications (empty to skip)" - type = string - default = "" -} - -variable "log_retention_days" { - description = "Number of days to retain application logs" - type = number - default = 30 - validation { - condition = contains([1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653], var.log_retention_days) - error_message = "log_retention_days must be a valid CloudWatch retention period." - } -} - -variable "loadbalancer_arn" { - description = "ARN of the application load balancer to monitor" - type = string -} - -variable "target_group_arns" { - description = "ARNs of target groups to monitor for unhealthy hosts" - type = list(string) - default = [] -} - -variable "compute_cluster_name" { - description = "Name of the ECS cluster for compute metrics" - type = string -} - -variable "filesystem_id" { - description = "EFS file system ID for storage metrics" - type = string -} - -variable "tags" { - description = "Resource tags" - type = map(string) - default = {} -} diff --git a/deployments/terraform/modules/networking/main.tf b/deployments/terraform/modules/networking/main.tf deleted file mode 100644 index 53b81b1..0000000 --- a/deployments/terraform/modules/networking/main.tf +++ /dev/null @@ -1,279 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# Networking module – Cloud-agnostic VPC / VNet / VCN abstraction -# -# Creates the foundational network topology: virtual network, public and -# private subnets across availability zones, NAT gateway, internet gateway, -# route tables, and security groups / firewall rules. -# ───────────────────────────────────────────────────────────────────────────── - -locals { - name_prefix = lower(replace("${var.project_name}-${var.environment}", "_", "-")) - - # Default AZs when none provided – derive from region - default_azs = [ - "${var.region}a", - "${var.region}b", - "${var.region}c", - ] - - availability_zones = length(var.availability_zones) > 0 ? var.availability_zones : local.default_azs - - # Number of AZs determines subnet count - az_count = min(length(local.availability_zones), length(var.public_subnet_cidrs), length(var.private_subnet_cidrs)) - - common_tags = merge( - { - module = "networking" - }, - var.tags, - ) -} - -# ───────────────────────────────────────────────────────────────────────────── -# VPC -# ───────────────────────────────────────────────────────────────────────────── - -resource "aws_vpc" "main" { - cidr_block = var.vpc_cidr - enable_dns_support = true - enable_dns_hostnames = true - - tags = merge(local.common_tags, { - Name = "${local.name_prefix}-vpc" - }) - - lifecycle { - prevent_destroy = false - } -} - -# ───────────────────────────────────────────────────────────────────────────── -# Internet gateway -# ───────────────────────────────────────────────────────────────────────────── - -resource "aws_internet_gateway" "main" { - vpc_id = aws_vpc.main.id - - tags = merge(local.common_tags, { - Name = "${local.name_prefix}-igw" - }) -} - -# ───────────────────────────────────────────────────────────────────────────── -# Public subnets -# ───────────────────────────────────────────────────────────────────────────── - -resource "aws_subnet" "public" { - count = local.az_count - - vpc_id = aws_vpc.main.id - cidr_block = var.public_subnet_cidrs[count.index] - availability_zone = local.availability_zones[count.index] - map_public_ip_on_launch = true - - tags = merge(local.common_tags, { - Name = "${local.name_prefix}-public-${local.availability_zones[count.index]}" - tier = "public" - }) -} - -resource "aws_route_table" "public" { - vpc_id = aws_vpc.main.id - - route { - cidr_block = "0.0.0.0/0" - gateway_id = aws_internet_gateway.main.id - } - - tags = merge(local.common_tags, { - Name = "${local.name_prefix}-public-rt" - }) -} - -resource "aws_route_table_association" "public" { - count = local.az_count - - subnet_id = aws_subnet.public[count.index].id - route_table_id = aws_route_table.public.id -} - -# ───────────────────────────────────────────────────────────────────────────── -# NAT gateway (single, in first public subnet – cost-conscious default) -# ───────────────────────────────────────────────────────────────────────────── - -resource "aws_eip" "nat" { - domain = "vpc" - - tags = merge(local.common_tags, { - Name = "${local.name_prefix}-nat-eip" - }) -} - -resource "aws_nat_gateway" "main" { - allocation_id = aws_eip.nat.id - subnet_id = aws_subnet.public[0].id - - tags = merge(local.common_tags, { - Name = "${local.name_prefix}-nat" - }) - - depends_on = [aws_internet_gateway.main] -} - -# ───────────────────────────────────────────────────────────────────────────── -# Private subnets -# ───────────────────────────────────────────────────────────────────────────── - -resource "aws_subnet" "private" { - count = local.az_count - - vpc_id = aws_vpc.main.id - cidr_block = var.private_subnet_cidrs[count.index] - availability_zone = local.availability_zones[count.index] - - tags = merge(local.common_tags, { - Name = "${local.name_prefix}-private-${local.availability_zones[count.index]}" - tier = "private" - }) -} - -resource "aws_route_table" "private" { - vpc_id = aws_vpc.main.id - - route { - cidr_block = "0.0.0.0/0" - nat_gateway_id = aws_nat_gateway.main.id - } - - tags = merge(local.common_tags, { - Name = "${local.name_prefix}-private-rt" - }) -} - -resource "aws_route_table_association" "private" { - count = local.az_count - - subnet_id = aws_subnet.private[count.index].id - route_table_id = aws_route_table.private.id -} - -# ───────────────────────────────────────────────────────────────────────────── -# Security groups -# ───────────────────────────────────────────────────────────────────────────── - -# Public (load-balancer-facing) -resource "aws_security_group" "public" { - name_prefix = "${local.name_prefix}-public-" - description = "Allow HTTPS/HTTP inbound and all outbound" - vpc_id = aws_vpc.main.id - - ingress { - description = "HTTPS" - from_port = 443 - to_port = 443 - protocol = "tcp" - cidr_blocks = ["0.0.0.0/0"] - } - - ingress { - description = "HTTP (redirect)" - from_port = 80 - to_port = 80 - protocol = "tcp" - cidr_blocks = ["0.0.0.0/0"] - } - - egress { - description = "All outbound" - from_port = 0 - to_port = 0 - protocol = "-1" - cidr_blocks = ["0.0.0.0/0"] - } - - tags = merge(local.common_tags, { - Name = "${local.name_prefix}-public-sg" - }) - - lifecycle { - create_before_destroy = true - } -} - -# Private (container-facing) -resource "aws_security_group" "private" { - name_prefix = "${local.name_prefix}-private-" - description = "Allow traffic from public SG to app and MCP ports" - vpc_id = aws_vpc.main.id - - ingress { - description = "Application port from LB" - from_port = var.app_port - to_port = var.app_port - protocol = "tcp" - security_groups = [aws_security_group.public.id] - } - - ingress { - description = "MCP sidecar port from LB" - from_port = var.mcp_port - to_port = var.mcp_port - protocol = "tcp" - security_groups = [aws_security_group.public.id] - } - - ingress { - description = "NFS (EFS) within VPC" - from_port = 2049 - to_port = 2049 - protocol = "tcp" - self = true - } - - egress { - description = "All outbound" - from_port = 0 - to_port = 0 - protocol = "-1" - cidr_blocks = ["0.0.0.0/0"] - } - - tags = merge(local.common_tags, { - Name = "${local.name_prefix}-private-sg" - }) - - lifecycle { - create_before_destroy = true - } -} - -# EFS security group -resource "aws_security_group" "storage" { - name_prefix = "${local.name_prefix}-storage-" - description = "Allow NFS access from private security group" - vpc_id = aws_vpc.main.id - - ingress { - description = "NFS from private subnets" - from_port = 2049 - to_port = 2049 - protocol = "tcp" - security_groups = [aws_security_group.private.id] - } - - egress { - description = "All outbound" - from_port = 0 - to_port = 0 - protocol = "-1" - cidr_blocks = ["0.0.0.0/0"] - } - - tags = merge(local.common_tags, { - Name = "${local.name_prefix}-storage-sg" - }) - - lifecycle { - create_before_destroy = true - } -} diff --git a/deployments/terraform/modules/networking/outputs.tf b/deployments/terraform/modules/networking/outputs.tf deleted file mode 100644 index e17841e..0000000 --- a/deployments/terraform/modules/networking/outputs.tf +++ /dev/null @@ -1,53 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# Networking module outputs -# ───────────────────────────────────────────────────────────────────────────── - -output "vpc_id" { - description = "ID of the provisioned VPC" - value = aws_vpc.main.id -} - -output "vpc_cidr" { - description = "CIDR block of the VPC" - value = aws_vpc.main.cidr_block -} - -output "public_subnet_ids" { - description = "IDs of the public subnets" - value = aws_subnet.public[*].id -} - -output "private_subnet_ids" { - description = "IDs of the private subnets" - value = aws_subnet.private[*].id -} - -output "public_security_group_ids" { - description = "Security group IDs for public-facing resources (LB)" - value = [aws_security_group.public.id] -} - -output "private_security_group_ids" { - description = "Security group IDs for private resources (containers)" - value = [aws_security_group.private.id] -} - -output "storage_security_group_ids" { - description = "Security group IDs for persistent storage" - value = [aws_security_group.storage.id] -} - -output "nat_gateway_ip" { - description = "Public IP of the NAT gateway" - value = aws_eip.nat.public_ip -} - -output "internet_gateway_id" { - description = "ID of the internet gateway" - value = aws_internet_gateway.main.id -} - -output "availability_zones" { - description = "Availability zones used for deployment" - value = local.availability_zones -} diff --git a/deployments/terraform/modules/networking/variables.tf b/deployments/terraform/modules/networking/variables.tf deleted file mode 100644 index 2c2af7a..0000000 --- a/deployments/terraform/modules/networking/variables.tf +++ /dev/null @@ -1,69 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# Networking module variables -# ───────────────────────────────────────────────────────────────────────────── - -variable "project_name" { - description = "Project identifier used in resource naming" - type = string -} - -variable "environment" { - description = "Deployment environment (dev, staging, production)" - type = string -} - -variable "cloud_provider" { - description = "Target cloud provider (aws, gcp, azure, oci)" - type = string -} - -variable "region" { - description = "Cloud region for deployment" - type = string -} - -variable "vpc_cidr" { - description = "CIDR block for the virtual network" - type = string - default = "10.0.0.0/16" - validation { - condition = can(cidrhost(var.vpc_cidr, 0)) - error_message = "vpc_cidr must be a valid CIDR block." - } -} - -variable "availability_zones" { - description = "List of availability zones for multi-AZ deployment" - type = list(string) - default = [] -} - -variable "public_subnet_cidrs" { - description = "CIDR blocks for public subnets (one per AZ)" - type = list(string) - default = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] -} - -variable "private_subnet_cidrs" { - description = "CIDR blocks for private subnets (one per AZ)" - type = list(string) - default = ["10.0.11.0/24", "10.0.12.0/24", "10.0.13.0/24"] -} - -variable "app_port" { - description = "Application container port" - type = number - default = 4820 -} - -variable "mcp_port" { - description = "MCP sidecar container port" - type = number - default = 8819 -} - -variable "tags" { - description = "Resource tags" - type = map(string) - default = {} -} diff --git a/deployments/terraform/outputs.tf b/deployments/terraform/outputs.tf deleted file mode 100644 index 14759a3..0000000 --- a/deployments/terraform/outputs.tf +++ /dev/null @@ -1,53 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# Root module outputs – Claude Code Agent Monitor -# ───────────────────────────────────────────────────────────────────────────── - -output "application_url" { - description = "Public URL of the Claude Code Agent Monitor dashboard" - value = module.loadbalancer.application_url -} - -output "loadbalancer_dns" { - description = "DNS name of the application load balancer" - value = module.loadbalancer.dns_name -} - -output "vpc_id" { - description = "ID of the provisioned VPC / VNet / VCN" - value = module.networking.vpc_id -} - -output "private_subnet_ids" { - description = "IDs of the private subnets hosting compute workloads" - value = module.networking.private_subnet_ids -} - -output "public_subnet_ids" { - description = "IDs of the public subnets hosting the load balancer" - value = module.networking.public_subnet_ids -} - -output "filesystem_id" { - description = "ID of the persistent file system for SQLite storage" - value = module.database.filesystem_id -} - -output "blue_service_name" { - description = "Name of the blue deployment compute service" - value = module.compute_blue.service_name -} - -output "green_service_name" { - description = "Name of the green deployment compute service" - value = module.compute_green.service_name -} - -output "active_slot" { - description = "Currently active deployment slot" - value = var.active_deployment_slot -} - -output "monitoring_dashboard_url" { - description = "URL of the monitoring dashboard (if enabled)" - value = var.enable_monitoring ? module.monitoring[0].dashboard_url : "monitoring disabled" -} diff --git a/deployments/terraform/providers/aws/main.tf b/deployments/terraform/providers/aws/main.tf deleted file mode 100644 index 7343103..0000000 --- a/deployments/terraform/providers/aws/main.tf +++ /dev/null @@ -1,264 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# AWS Provider – Full implementation for Claude Code Agent Monitor -# -# Composes the generic modules into a production-ready AWS stack: -# VPC → ECS Fargate → EFS → ALB → CloudWatch → ACM -# -# Features: -# - Multi-AZ deployment -# - Blue/green deployment slots -# - EFS for persistent SQLite storage -# - ALB with WebSocket support and sticky sessions -# - Auto-scaling with CPU/memory targets -# - CloudWatch monitoring, alarms, and dashboards -# - IAM least-privilege roles -# ───────────────────────────────────────────────────────────────────────────── - -provider "aws" { - region = var.region - - default_tags { - tags = local.common_tags - } -} - -# ── Data sources ──────────────────────────────────────────────────────────── - -data "aws_caller_identity" "current" {} -data "aws_region" "current" {} - -data "aws_availability_zones" "available" { - state = "available" -} - -# ── Locals ────────────────────────────────────────────────────────────────── - -locals { - name_prefix = lower(replace("${var.project_name}-${var.environment}", "_", "-")) - - common_tags = merge( - { - project = var.project_name - environment = var.environment - managed_by = "terraform" - cloud_provider = "aws" - repository = "Claude-Code-Agent-Monitor" - }, - var.tags, - ) - - # Use first 3 available AZs when none specified - availability_zones = length(var.availability_zones) > 0 ? var.availability_zones : slice(data.aws_availability_zones.available.names, 0, min(3, length(data.aws_availability_zones.available.names))) -} - -# ───────────────────────────────────────────────────────────────────────────── -# Networking -# ───────────────────────────────────────────────────────────────────────────── - -module "networking" { - source = "../../modules/networking" - - project_name = var.project_name - environment = var.environment - cloud_provider = "aws" - region = var.region - vpc_cidr = var.vpc_cidr - availability_zones = local.availability_zones - public_subnet_cidrs = var.public_subnet_cidrs - private_subnet_cidrs = var.private_subnet_cidrs - app_port = var.app_port - mcp_port = var.mcp_port - tags = local.common_tags -} - -# ───────────────────────────────────────────────────────────────────────────── -# Database (EFS for SQLite persistence) -# ───────────────────────────────────────────────────────────────────────────── - -module "database" { - source = "../../modules/database" - - project_name = var.project_name - environment = var.environment - cloud_provider = "aws" - region = var.region - storage_size_gb = var.storage_size_gb - enable_backup = var.enable_storage_backup - private_subnet_ids = module.networking.private_subnet_ids - vpc_id = module.networking.vpc_id - allowed_security_group_ids = module.networking.storage_security_group_ids - tags = local.common_tags -} - -# ───────────────────────────────────────────────────────────────────────────── -# Compute – Blue slot -# ───────────────────────────────────────────────────────────────────────────── - -module "compute_blue" { - source = "../../modules/compute" - - project_name = var.project_name - environment = var.environment - cloud_provider = "aws" - region = var.region - deployment_slot = "blue" - container_image = var.app_container_image - mcp_container_image = var.mcp_container_image - app_port = var.app_port - mcp_port = var.mcp_port - cpu = var.cpu - memory = var.memory - desired_count = var.active_deployment_slot == "blue" ? var.desired_replicas : 0 - min_count = var.active_deployment_slot == "blue" ? var.min_replicas : 0 - max_count = var.active_deployment_slot == "blue" ? var.max_replicas : 0 - environment_variables = var.environment_variables - health_check_path = var.health_check_path - vpc_id = module.networking.vpc_id - private_subnet_ids = module.networking.private_subnet_ids - security_group_ids = module.networking.private_security_group_ids - storage_filesystem_id = module.database.filesystem_id - storage_mount_targets = module.database.mount_target_ids - autoscaling_cpu_target = var.autoscaling_cpu_target - autoscaling_memory_target = var.autoscaling_memory_target - tags = local.common_tags -} - -# ───────────────────────────────────────────────────────────────────────────── -# Compute – Green slot -# ───────────────────────────────────────────────────────────────────────────── - -module "compute_green" { - source = "../../modules/compute" - - project_name = var.project_name - environment = var.environment - cloud_provider = "aws" - region = var.region - deployment_slot = "green" - container_image = var.app_container_image - mcp_container_image = var.mcp_container_image - app_port = var.app_port - mcp_port = var.mcp_port - cpu = var.cpu - memory = var.memory - desired_count = var.active_deployment_slot == "green" ? var.desired_replicas : 0 - min_count = var.active_deployment_slot == "green" ? var.min_replicas : 0 - max_count = var.active_deployment_slot == "green" ? var.max_replicas : 0 - environment_variables = var.environment_variables - health_check_path = var.health_check_path - vpc_id = module.networking.vpc_id - private_subnet_ids = module.networking.private_subnet_ids - security_group_ids = module.networking.private_security_group_ids - storage_filesystem_id = module.database.filesystem_id - storage_mount_targets = module.database.mount_target_ids - autoscaling_cpu_target = var.autoscaling_cpu_target - autoscaling_memory_target = var.autoscaling_memory_target - tags = local.common_tags -} - -# ───────────────────────────────────────────────────────────────────────────── -# ACM Certificate (optional – when domain_name is specified) -# ───────────────────────────────────────────────────────────────────────────── - -resource "aws_acm_certificate" "main" { - count = var.domain_name != "" && var.tls_certificate_arn == "" ? 1 : 0 - - domain_name = var.domain_name - validation_method = "DNS" - - tags = merge(local.common_tags, { - Name = "${local.name_prefix}-cert" - }) - - lifecycle { - create_before_destroy = true - } -} - -locals { - tls_cert_arn = var.tls_certificate_arn != "" ? var.tls_certificate_arn : ( - length(aws_acm_certificate.main) > 0 ? aws_acm_certificate.main[0].arn : "" - ) -} - -# ───────────────────────────────────────────────────────────────────────────── -# Load Balancer -# ───────────────────────────────────────────────────────────────────────────── - -module "loadbalancer" { - source = "../../modules/loadbalancer" - - project_name = var.project_name - environment = var.environment - cloud_provider = "aws" - region = var.region - vpc_id = module.networking.vpc_id - public_subnet_ids = module.networking.public_subnet_ids - security_group_ids = module.networking.public_security_group_ids - app_port = var.app_port - mcp_port = var.mcp_port - tls_certificate_arn = local.tls_cert_arn - domain_name = var.domain_name - - blue_target_group_arn = module.compute_blue.target_group_arn - green_target_group_arn = module.compute_green.target_group_arn - blue_weight = var.blue_weight - green_weight = var.green_weight - - health_check_path = var.health_check_path - health_check_interval = var.health_check_interval - health_check_timeout = var.health_check_timeout - health_check_healthy_threshold = var.health_check_healthy_threshold - health_check_unhealthy_threshold = var.health_check_unhealthy_threshold - - enable_deletion_protection = var.environment == "production" - - tags = local.common_tags -} - -# ───────────────────────────────────────────────────────────────────────────── -# Monitoring -# ───────────────────────────────────────────────────────────────────────────── - -module "monitoring" { - source = "../../modules/monitoring" - count = var.enable_monitoring ? 1 : 0 - - project_name = var.project_name - environment = var.environment - cloud_provider = "aws" - region = var.region - alert_email = var.alert_email - log_retention_days = var.log_retention_days - - loadbalancer_arn = module.loadbalancer.loadbalancer_arn - target_group_arns = [module.compute_blue.target_group_arn, module.compute_green.target_group_arn] - compute_cluster_name = module.compute_blue.cluster_name - filesystem_id = module.database.filesystem_id - - tags = local.common_tags -} - -# ───────────────────────────────────────────────────────────────────────────── -# Route53 DNS record (optional) -# ───────────────────────────────────────────────────────────────────────────── - -data "aws_route53_zone" "main" { - count = var.domain_name != "" && var.route53_zone_id != "" ? 1 : 0 - - zone_id = var.route53_zone_id -} - -resource "aws_route53_record" "app" { - count = var.domain_name != "" && var.route53_zone_id != "" ? 1 : 0 - - zone_id = data.aws_route53_zone.main[0].zone_id - name = var.domain_name - type = "A" - - alias { - name = module.loadbalancer.dns_name - zone_id = module.loadbalancer.zone_id - evaluate_target_health = true - } -} diff --git a/deployments/terraform/providers/aws/outputs.tf b/deployments/terraform/providers/aws/outputs.tf deleted file mode 100644 index 059feed..0000000 --- a/deployments/terraform/providers/aws/outputs.tf +++ /dev/null @@ -1,58 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# AWS provider outputs -# ───────────────────────────────────────────────────────────────────────────── - -output "application_url" { - description = "Public URL of the Claude Code Agent Monitor dashboard" - value = module.loadbalancer.application_url -} - -output "alb_dns_name" { - description = "DNS name of the Application Load Balancer" - value = module.loadbalancer.dns_name -} - -output "vpc_id" { - description = "ID of the VPC" - value = module.networking.vpc_id -} - -output "ecs_cluster_name" { - description = "Name of the ECS cluster" - value = module.compute_blue.cluster_name -} - -output "blue_service_name" { - description = "Name of the blue ECS service" - value = module.compute_blue.service_name -} - -output "green_service_name" { - description = "Name of the green ECS service" - value = module.compute_green.service_name -} - -output "efs_filesystem_id" { - description = "ID of the EFS file system" - value = module.database.filesystem_id -} - -output "acm_certificate_arn" { - description = "ARN of the ACM certificate (if auto-created)" - value = length(aws_acm_certificate.main) > 0 ? aws_acm_certificate.main[0].arn : var.tls_certificate_arn -} - -output "monitoring_dashboard_url" { - description = "CloudWatch dashboard URL" - value = var.enable_monitoring ? module.monitoring[0].dashboard_url : "monitoring disabled" -} - -output "account_id" { - description = "AWS account ID" - value = data.aws_caller_identity.current.account_id -} - -output "region" { - description = "AWS region" - value = data.aws_region.current.name -} diff --git a/deployments/terraform/providers/aws/terraform.tf b/deployments/terraform/providers/aws/terraform.tf deleted file mode 100644 index c850ca3..0000000 --- a/deployments/terraform/providers/aws/terraform.tf +++ /dev/null @@ -1,14 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# AWS provider – Terraform and provider constraints -# ───────────────────────────────────────────────────────────────────────────── - -terraform { - required_version = ">= 1.5.0" - - required_providers { - aws = { - source = "hashicorp/aws" - version = "~> 5.0" - } - } -} diff --git a/deployments/terraform/providers/aws/variables.tf b/deployments/terraform/providers/aws/variables.tf deleted file mode 100644 index 1f67ff1..0000000 --- a/deployments/terraform/providers/aws/variables.tf +++ /dev/null @@ -1,254 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# AWS provider variables -# ───────────────────────────────────────────────────────────────────────────── - -# ── Core ──────────────────────────────────────────────────────────────────── - -variable "project_name" { - description = "Project identifier used in resource naming and tagging" - type = string - default = "claude-agent-monitor" -} - -variable "environment" { - description = "Deployment environment: dev, staging, or production" - type = string - validation { - condition = contains(["dev", "staging", "production"], var.environment) - error_message = "environment must be one of: dev, staging, production." - } -} - -variable "region" { - description = "AWS region for resource deployment" - type = string - default = "us-east-1" -} - -variable "tags" { - description = "Additional tags to apply to all resources" - type = map(string) - default = {} -} - -# ── Networking ────────────────────────────────────────────────────────────── - -variable "vpc_cidr" { - description = "CIDR block for the VPC" - type = string - default = "10.0.0.0/16" -} - -variable "availability_zones" { - description = "List of AZs (auto-detected if empty)" - type = list(string) - default = [] -} - -variable "public_subnet_cidrs" { - description = "CIDR blocks for public subnets" - type = list(string) - default = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] -} - -variable "private_subnet_cidrs" { - description = "CIDR blocks for private subnets" - type = list(string) - default = ["10.0.11.0/24", "10.0.12.0/24", "10.0.13.0/24"] -} - -# ── Compute ───────────────────────────────────────────────────────────────── - -variable "app_container_image" { - description = "Docker image URI for the main application" - type = string -} - -variable "mcp_container_image" { - description = "Docker image URI for the MCP sidecar (empty to disable)" - type = string - default = "" -} - -variable "app_port" { - description = "Application container port" - type = number - default = 4820 -} - -variable "mcp_port" { - description = "MCP sidecar container port" - type = number - default = 8819 -} - -variable "cpu" { - description = "CPU units for Fargate tasks (256, 512, 1024, 2048, 4096)" - type = number - default = 512 -} - -variable "memory" { - description = "Memory in MiB for Fargate tasks" - type = number - default = 1024 -} - -variable "min_replicas" { - description = "Minimum number of ECS tasks" - type = number - default = 1 -} - -variable "max_replicas" { - description = "Maximum number of ECS tasks for auto-scaling" - type = number - default = 3 -} - -variable "desired_replicas" { - description = "Desired number of ECS tasks at steady state" - type = number - default = 1 -} - -variable "environment_variables" { - description = "Environment variables for the application container" - type = map(string) - default = { - NODE_ENV = "production" - DASHBOARD_PORT = "4820" - } -} - -# ── Deployment ────────────────────────────────────────────────────────────── - -variable "active_deployment_slot" { - description = "Active deployment slot: blue or green" - type = string - default = "blue" - validation { - condition = contains(["blue", "green"], var.active_deployment_slot) - error_message = "active_deployment_slot must be blue or green." - } -} - -variable "blue_weight" { - description = "Traffic weight for blue target group (0-100)" - type = number - default = 100 - validation { - condition = var.blue_weight >= 0 && var.blue_weight <= 100 - error_message = "blue_weight must be between 0 and 100." - } -} - -variable "green_weight" { - description = "Traffic weight for green target group (0-100)" - type = number - default = 0 - validation { - condition = var.green_weight >= 0 && var.green_weight <= 100 - error_message = "green_weight must be between 0 and 100." - } -} - -# ── TLS / Domain ──────────────────────────────────────────────────────────── - -variable "domain_name" { - description = "FQDN for the application (empty to skip DNS/TLS)" - type = string - default = "" -} - -variable "tls_certificate_arn" { - description = "ARN of an existing ACM certificate (auto-created if domain_name set)" - type = string - default = "" -} - -variable "route53_zone_id" { - description = "Route53 hosted zone ID for DNS records (empty to skip)" - type = string - default = "" -} - -# ── Storage ───────────────────────────────────────────────────────────────── - -variable "storage_size_gb" { - description = "EFS storage does not require pre-provisioning; kept for interface compatibility" - type = number - default = 20 -} - -variable "enable_storage_backup" { - description = "Enable AWS Backup for EFS" - type = bool - default = true -} - -# ── Health check ──────────────────────────────────────────────────────────── - -variable "health_check_path" { - description = "HTTP path for health checks" - type = string - default = "/api/health" -} - -variable "health_check_interval" { - description = "Seconds between health checks" - type = number - default = 30 -} - -variable "health_check_timeout" { - description = "Seconds before a health check request times out" - type = number - default = 5 -} - -variable "health_check_healthy_threshold" { - description = "Consecutive successes to mark target healthy" - type = number - default = 2 -} - -variable "health_check_unhealthy_threshold" { - description = "Consecutive failures to mark target unhealthy" - type = number - default = 3 -} - -# ── Auto-scaling ──────────────────────────────────────────────────────────── - -variable "autoscaling_cpu_target" { - description = "Target CPU utilization percentage for auto-scaling" - type = number - default = 70 -} - -variable "autoscaling_memory_target" { - description = "Target memory utilization percentage for auto-scaling" - type = number - default = 80 -} - -# ── Monitoring ────────────────────────────────────────────────────────────── - -variable "enable_monitoring" { - description = "Enable CloudWatch monitoring, alarms, and dashboards" - type = bool - default = true -} - -variable "alert_email" { - description = "Email address for SNS alert notifications" - type = string - default = "" -} - -variable "log_retention_days" { - description = "CloudWatch log retention in days" - type = number - default = 30 -} diff --git a/deployments/terraform/providers/azure/main.tf b/deployments/terraform/providers/azure/main.tf deleted file mode 100644 index 5952842..0000000 --- a/deployments/terraform/providers/azure/main.tf +++ /dev/null @@ -1,615 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# Azure Provider – Full implementation for Claude Code Agent Monitor -# -# Architecture: -# VNet → ACI (Container Instances) or AKS → Azure Files → Application -# Gateway → Azure Monitor → Key Vault -# -# Azure Container Instances is chosen for simplicity; for production at -# scale, AKS is recommended. Application Gateway provides L7 LB with -# WebSocket support and SSL termination. -# ───────────────────────────────────────────────────────────────────────────── - -provider "azurerm" { - features { - resource_group { - prevent_deletion_if_contains_resources = true - } - key_vault { - purge_soft_delete_on_destroy = false - } - } -} - -# ── Data sources ──────────────────────────────────────────────────────────── - -data "azurerm_client_config" "current" {} - -# ── Locals ────────────────────────────────────────────────────────────────── - -locals { - name_prefix = lower(replace("${var.project_name}-${var.environment}", "_", "-")) - - # Azure resource names (alphanumeric for storage accounts) - storage_account_name = lower(replace(substr("ccam${var.environment}${substr(md5(var.project_name), 0, 8)}", 0, 24), "-", "")) - - common_tags = merge( - { - project = var.project_name - environment = var.environment - managed_by = "terraform" - cloud_provider = "azure" - repository = "Claude-Code-Agent-Monitor" - }, - var.tags, - ) -} - -# ───────────────────────────────────────────────────────────────────────────── -# Resource Group -# ───────────────────────────────────────────────────────────────────────────── - -resource "azurerm_resource_group" "main" { - name = "${local.name_prefix}-rg" - location = var.region - - tags = local.common_tags - - lifecycle { - prevent_destroy = false - } -} - -# ───────────────────────────────────────────────────────────────────────────── -# Virtual Network -# ───────────────────────────────────────────────────────────────────────────── - -resource "azurerm_virtual_network" "main" { - name = "${local.name_prefix}-vnet" - resource_group_name = azurerm_resource_group.main.name - location = azurerm_resource_group.main.location - address_space = [var.vpc_cidr] - - tags = local.common_tags -} - -resource "azurerm_subnet" "public" { - name = "${local.name_prefix}-public" - resource_group_name = azurerm_resource_group.main.name - virtual_network_name = azurerm_virtual_network.main.name - address_prefixes = [var.public_subnet_cidrs[0]] -} - -resource "azurerm_subnet" "private" { - name = "${local.name_prefix}-private" - resource_group_name = azurerm_resource_group.main.name - virtual_network_name = azurerm_virtual_network.main.name - address_prefixes = [var.private_subnet_cidrs[0]] - - delegation { - name = "aci-delegation" - service_delegation { - name = "Microsoft.ContainerInstance/containerGroups" - actions = ["Microsoft.Network/virtualNetworks/subnets/action"] - } - } -} - -resource "azurerm_subnet" "appgw" { - name = "${local.name_prefix}-appgw" - resource_group_name = azurerm_resource_group.main.name - virtual_network_name = azurerm_virtual_network.main.name - address_prefixes = ["10.0.100.0/24"] -} - -# NSG for private subnet -resource "azurerm_network_security_group" "private" { - name = "${local.name_prefix}-private-nsg" - location = azurerm_resource_group.main.location - resource_group_name = azurerm_resource_group.main.name - - security_rule { - name = "allow-app-port" - priority = 100 - direction = "Inbound" - access = "Allow" - protocol = "Tcp" - source_port_range = "*" - destination_port_range = tostring(var.app_port) - source_address_prefix = var.vpc_cidr - destination_address_prefix = "*" - } - - security_rule { - name = "allow-mcp-port" - priority = 110 - direction = "Inbound" - access = "Allow" - protocol = "Tcp" - source_port_range = "*" - destination_port_range = tostring(var.mcp_port) - source_address_prefix = var.vpc_cidr - destination_address_prefix = "*" - } - - security_rule { - name = "allow-smb" - priority = 120 - direction = "Inbound" - access = "Allow" - protocol = "Tcp" - source_port_range = "*" - destination_port_range = "445" - source_address_prefix = var.vpc_cidr - destination_address_prefix = "*" - } - - tags = local.common_tags -} - -resource "azurerm_subnet_network_security_group_association" "private" { - subnet_id = azurerm_subnet.private.id - network_security_group_id = azurerm_network_security_group.private.id -} - -# ───────────────────────────────────────────────────────────────────────────── -# Azure Files (persistent storage for SQLite) -# ───────────────────────────────────────────────────────────────────────────── - -resource "azurerm_storage_account" "main" { - name = local.storage_account_name - resource_group_name = azurerm_resource_group.main.name - location = azurerm_resource_group.main.location - account_tier = var.environment == "production" ? "Premium" : "Standard" - account_replication_type = var.environment == "production" ? "ZRS" : "LRS" - account_kind = var.environment == "production" ? "FileStorage" : "StorageV2" - - min_tls_version = "TLS1_2" - - network_rules { - default_action = "Deny" - virtual_network_subnet_ids = [azurerm_subnet.private.id] - } - - tags = local.common_tags - - lifecycle { - prevent_destroy = true - } -} - -resource "azurerm_storage_share" "appdata" { - name = "appdata" - storage_account_name = azurerm_storage_account.main.name - quota = var.storage_size_gb - access_tier = var.environment == "production" ? "Premium" : "Hot" -} - -# ───────────────────────────────────────────────────────────────────────────── -# Key Vault (for secrets management) -# ───────────────────────────────────────────────────────────────────────────── - -resource "azurerm_key_vault" "main" { - name = substr("${local.name_prefix}-kv", 0, 24) - location = azurerm_resource_group.main.location - resource_group_name = azurerm_resource_group.main.name - tenant_id = data.azurerm_client_config.current.tenant_id - sku_name = "standard" - purge_protection_enabled = var.environment == "production" - - access_policy { - tenant_id = data.azurerm_client_config.current.tenant_id - object_id = data.azurerm_client_config.current.object_id - - secret_permissions = [ - "Get", "List", "Set", "Delete", "Purge", - ] - } - - tags = local.common_tags -} - -# ───────────────────────────────────────────────────────────────────────────── -# Container Instances (Blue / Green) -# ───────────────────────────────────────────────────────────────────────────── - -resource "azurerm_container_group" "blue" { - name = "${local.name_prefix}-blue" - location = azurerm_resource_group.main.location - resource_group_name = azurerm_resource_group.main.name - os_type = "Linux" - ip_address_type = "Private" - subnet_ids = [azurerm_subnet.private.id] - restart_policy = "Always" - - container { - name = "app" - image = var.app_container_image - cpu = var.cpu / 1000.0 - memory = var.memory / 1024.0 - - ports { - port = var.app_port - protocol = "TCP" - } - - dynamic "environment_variables" { - for_each = var.environment_variables - content { - name = environment_variables.key - value = environment_variables.value - } - } - - volume { - name = "app-data" - mount_path = "/app/data" - read_only = false - storage_account_name = azurerm_storage_account.main.name - storage_account_key = azurerm_storage_account.main.primary_access_key - share_name = azurerm_storage_share.appdata.name - } - - liveness_probe { - http_get { - path = var.health_check_path - port = var.app_port - scheme = "Http" - } - initial_delay_seconds = 30 - period_seconds = 30 - failure_threshold = 3 - } - - readiness_probe { - http_get { - path = var.health_check_path - port = var.app_port - scheme = "Http" - } - initial_delay_seconds = 10 - period_seconds = 10 - failure_threshold = 3 - } - } - - dynamic "container" { - for_each = var.mcp_container_image != "" ? [1] : [] - content { - name = "mcp-sidecar" - image = var.mcp_container_image - cpu = 0.25 - memory = 0.25 - - ports { - port = var.mcp_port - protocol = "TCP" - } - - environment_variables = { - NODE_ENV = "production" - MCP_PORT = tostring(var.mcp_port) - } - } - } - - tags = merge(local.common_tags, { - deployment_slot = "blue" - }) - - lifecycle { - ignore_changes = [ - tags["last_deployed"], - ] - } -} - -resource "azurerm_container_group" "green" { - count = var.active_deployment_slot == "green" || var.green_weight > 0 ? 1 : 0 - - name = "${local.name_prefix}-green" - location = azurerm_resource_group.main.location - resource_group_name = azurerm_resource_group.main.name - os_type = "Linux" - ip_address_type = "Private" - subnet_ids = [azurerm_subnet.private.id] - restart_policy = "Always" - - container { - name = "app" - image = var.app_container_image - cpu = var.cpu / 1000.0 - memory = var.memory / 1024.0 - - ports { - port = var.app_port - protocol = "TCP" - } - - dynamic "environment_variables" { - for_each = var.environment_variables - content { - name = environment_variables.key - value = environment_variables.value - } - } - - volume { - name = "app-data" - mount_path = "/app/data" - read_only = false - storage_account_name = azurerm_storage_account.main.name - storage_account_key = azurerm_storage_account.main.primary_access_key - share_name = azurerm_storage_share.appdata.name - } - - liveness_probe { - http_get { - path = var.health_check_path - port = var.app_port - scheme = "Http" - } - initial_delay_seconds = 30 - period_seconds = 30 - failure_threshold = 3 - } - } - - tags = merge(local.common_tags, { - deployment_slot = "green" - }) -} - -# ───────────────────────────────────────────────────────────────────────────── -# Application Gateway (L7 load balancer with WebSocket + SSL) -# ───────────────────────────────────────────────────────────────────────────── - -resource "azurerm_public_ip" "appgw" { - name = "${local.name_prefix}-appgw-pip" - location = azurerm_resource_group.main.location - resource_group_name = azurerm_resource_group.main.name - allocation_method = "Static" - sku = "Standard" - - tags = local.common_tags -} - -resource "azurerm_application_gateway" "main" { - name = "${local.name_prefix}-appgw" - resource_group_name = azurerm_resource_group.main.name - location = azurerm_resource_group.main.location - enable_http2 = true - - sku { - name = var.environment == "production" ? "WAF_v2" : "Standard_v2" - tier = var.environment == "production" ? "WAF_v2" : "Standard_v2" - capacity = var.environment == "production" ? 2 : 1 - } - - gateway_ip_configuration { - name = "gateway-ip" - subnet_id = azurerm_subnet.appgw.id - } - - frontend_ip_configuration { - name = "frontend-ip" - public_ip_address_id = azurerm_public_ip.appgw.id - } - - frontend_port { - name = "http" - port = 80 - } - - frontend_port { - name = "https" - port = 443 - } - - # Blue backend pool - backend_address_pool { - name = "blue-pool" - ip_addresses = [azurerm_container_group.blue.ip_address] - } - - # Green backend pool - dynamic "backend_address_pool" { - for_each = length(azurerm_container_group.green) > 0 ? [1] : [] - content { - name = "green-pool" - ip_addresses = [azurerm_container_group.green[0].ip_address] - } - } - - backend_http_settings { - name = "app-settings" - cookie_based_affinity = "Enabled" - port = var.app_port - protocol = "Http" - request_timeout = 300 # WebSocket support - pick_host_name_from_backend_address = false - - connection_draining { - enabled = true - drain_timeout_sec = 60 - } - - probe_name = "app-health" - } - - probe { - name = "app-health" - protocol = "Http" - path = var.health_check_path - host = "127.0.0.1" - interval = var.health_check_interval - timeout = var.health_check_timeout - unhealthy_threshold = var.health_check_unhealthy_threshold - - match { - status_code = ["200"] - } - } - - # HTTP listener - http_listener { - name = "http-listener" - frontend_ip_configuration_name = "frontend-ip" - frontend_port_name = "http" - protocol = "Http" - } - - # Routing rule – HTTP to blue pool - request_routing_rule { - name = "http-routing" - priority = 100 - rule_type = "Basic" - http_listener_name = "http-listener" - backend_address_pool_name = var.active_deployment_slot == "blue" ? "blue-pool" : "green-pool" - backend_http_settings_name = "app-settings" - } - - tags = local.common_tags - - lifecycle { - prevent_destroy = false - ignore_changes = [ - tags["last_deployed"], - ] - } -} - -# ───────────────────────────────────────────────────────────────────────────── -# Azure Monitor (alerts and diagnostics) -# ───────────────────────────────────────────────────────────────────────────── - -resource "azurerm_monitor_action_group" "main" { - count = var.enable_monitoring ? 1 : 0 - - name = "${local.name_prefix}-alerts" - resource_group_name = azurerm_resource_group.main.name - short_name = substr(local.name_prefix, 0, 12) - - dynamic "email_receiver" { - for_each = var.alert_email != "" ? [1] : [] - content { - name = "email-alert" - email_address = var.alert_email - } - } - - tags = local.common_tags -} - -resource "azurerm_monitor_metric_alert" "appgw_unhealthy" { - count = var.enable_monitoring ? 1 : 0 - - name = "${local.name_prefix}-unhealthy-backend" - resource_group_name = azurerm_resource_group.main.name - scopes = [azurerm_application_gateway.main.id] - description = "Alert when backend health drops below threshold" - severity = 1 - - criteria { - metric_namespace = "Microsoft.Network/applicationGateways" - metric_name = "UnhealthyHostCount" - aggregation = "Average" - operator = "GreaterThan" - threshold = 0 - } - - action { - action_group_id = azurerm_monitor_action_group.main[0].id - } - - tags = local.common_tags -} - -resource "azurerm_monitor_metric_alert" "appgw_5xx" { - count = var.enable_monitoring ? 1 : 0 - - name = "${local.name_prefix}-high-5xx" - resource_group_name = azurerm_resource_group.main.name - scopes = [azurerm_application_gateway.main.id] - description = "High 5xx error rate on Application Gateway" - severity = 2 - - criteria { - metric_namespace = "Microsoft.Network/applicationGateways" - metric_name = "ResponseStatus" - aggregation = "Count" - operator = "GreaterThan" - threshold = 10 - - dimension { - name = "HttpStatusGroup" - operator = "Include" - values = ["5xx"] - } - } - - action { - action_group_id = azurerm_monitor_action_group.main[0].id - } - - tags = local.common_tags -} - -resource "azurerm_monitor_metric_alert" "appgw_latency" { - count = var.enable_monitoring ? 1 : 0 - - name = "${local.name_prefix}-high-latency" - resource_group_name = azurerm_resource_group.main.name - scopes = [azurerm_application_gateway.main.id] - description = "High backend response latency" - severity = 2 - - criteria { - metric_namespace = "Microsoft.Network/applicationGateways" - metric_name = "BackendLastByteResponseTime" - aggregation = "Average" - operator = "GreaterThan" - threshold = 2000 # ms - } - - action { - action_group_id = azurerm_monitor_action_group.main[0].id - } - - tags = local.common_tags -} - -# Log Analytics Workspace -resource "azurerm_log_analytics_workspace" "main" { - count = var.enable_monitoring ? 1 : 0 - - name = "${local.name_prefix}-logs" - location = azurerm_resource_group.main.location - resource_group_name = azurerm_resource_group.main.name - sku = "PerGB2018" - retention_in_days = var.log_retention_days - - tags = local.common_tags -} - -# Diagnostic settings for App Gateway -resource "azurerm_monitor_diagnostic_setting" "appgw" { - count = var.enable_monitoring ? 1 : 0 - - name = "${local.name_prefix}-appgw-diag" - target_resource_id = azurerm_application_gateway.main.id - log_analytics_workspace_id = azurerm_log_analytics_workspace.main[0].id - - enabled_log { - category = "ApplicationGatewayAccessLog" - } - - enabled_log { - category = "ApplicationGatewayPerformanceLog" - } - - metric { - category = "AllMetrics" - enabled = true - } -} diff --git a/deployments/terraform/providers/azure/outputs.tf b/deployments/terraform/providers/azure/outputs.tf deleted file mode 100644 index 2f6a835..0000000 --- a/deployments/terraform/providers/azure/outputs.tf +++ /dev/null @@ -1,58 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# Azure provider outputs -# ───────────────────────────────────────────────────────────────────────────── - -output "application_url" { - description = "Public URL of the Claude Code Agent Monitor dashboard" - value = "http://${azurerm_public_ip.appgw.ip_address}" -} - -output "public_ip" { - description = "Public IP address of the Application Gateway" - value = azurerm_public_ip.appgw.ip_address -} - -output "resource_group_name" { - description = "Name of the Azure resource group" - value = azurerm_resource_group.main.name -} - -output "vnet_id" { - description = "ID of the Virtual Network" - value = azurerm_virtual_network.main.id -} - -output "blue_container_group_id" { - description = "ID of the blue container group" - value = azurerm_container_group.blue.id -} - -output "green_container_group_id" { - description = "ID of the green container group (if deployed)" - value = length(azurerm_container_group.green) > 0 ? azurerm_container_group.green[0].id : "" -} - -output "storage_account_name" { - description = "Name of the Azure Storage Account" - value = azurerm_storage_account.main.name -} - -output "key_vault_uri" { - description = "URI of the Azure Key Vault" - value = azurerm_key_vault.main.vault_uri -} - -output "app_gateway_id" { - description = "ID of the Application Gateway" - value = azurerm_application_gateway.main.id -} - -output "log_analytics_workspace_id" { - description = "ID of the Log Analytics workspace (if monitoring enabled)" - value = var.enable_monitoring ? azurerm_log_analytics_workspace.main[0].id : "" -} - -output "region" { - description = "Azure region" - value = var.region -} diff --git a/deployments/terraform/providers/azure/terraform.tf b/deployments/terraform/providers/azure/terraform.tf deleted file mode 100644 index 01f06b5..0000000 --- a/deployments/terraform/providers/azure/terraform.tf +++ /dev/null @@ -1,14 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# Azure provider – Terraform and provider constraints -# ───────────────────────────────────────────────────────────────────────────── - -terraform { - required_version = ">= 1.5.0" - - required_providers { - azurerm = { - source = "hashicorp/azurerm" - version = "~> 3.80" - } - } -} diff --git a/deployments/terraform/providers/azure/variables.tf b/deployments/terraform/providers/azure/variables.tf deleted file mode 100644 index 9bfe597..0000000 --- a/deployments/terraform/providers/azure/variables.tf +++ /dev/null @@ -1,190 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# Azure provider variables -# ───────────────────────────────────────────────────────────────────────────── - -variable "project_name" { - description = "Project identifier used in resource naming" - type = string - default = "claude-agent-monitor" -} - -variable "environment" { - description = "Deployment environment: dev, staging, or production" - type = string - validation { - condition = contains(["dev", "staging", "production"], var.environment) - error_message = "environment must be one of: dev, staging, production." - } -} - -variable "region" { - description = "Azure region for resource deployment" - type = string - default = "eastus" -} - -variable "tags" { - description = "Additional tags to apply to all resources" - type = map(string) - default = {} -} - -# ── Networking ────────────────────────────────────────────────────────────── - -variable "vpc_cidr" { - description = "Address space for the Virtual Network" - type = string - default = "10.0.0.0/16" -} - -variable "public_subnet_cidrs" { - description = "Address prefixes for the public subnet" - type = list(string) - default = ["10.0.1.0/24"] -} - -variable "private_subnet_cidrs" { - description = "Address prefixes for the private subnet" - type = list(string) - default = ["10.0.11.0/24"] -} - -# ── Compute ───────────────────────────────────────────────────────────────── - -variable "app_container_image" { - description = "Container image URI for the main application" - type = string -} - -variable "mcp_container_image" { - description = "Container image URI for the MCP sidecar (empty to disable)" - type = string - default = "" -} - -variable "app_port" { - description = "Application container port" - type = number - default = 4820 -} - -variable "mcp_port" { - description = "MCP sidecar container port" - type = number - default = 8819 -} - -variable "cpu" { - description = "CPU millicores for container instances" - type = number - default = 512 -} - -variable "memory" { - description = "Memory in MiB for container instances" - type = number - default = 1024 -} - -variable "environment_variables" { - description = "Environment variables for the application container" - type = map(string) - default = { - NODE_ENV = "production" - DASHBOARD_PORT = "4820" - } -} - -# ── Deployment ────────────────────────────────────────────────────────────── - -variable "active_deployment_slot" { - description = "Active deployment slot: blue or green" - type = string - default = "blue" - validation { - condition = contains(["blue", "green"], var.active_deployment_slot) - error_message = "active_deployment_slot must be blue or green." - } -} - -variable "blue_weight" { - description = "Traffic weight for blue backend (0-100)" - type = number - default = 100 - validation { - condition = var.blue_weight >= 0 && var.blue_weight <= 100 - error_message = "blue_weight must be between 0 and 100." - } -} - -variable "green_weight" { - description = "Traffic weight for green backend (0-100)" - type = number - default = 0 - validation { - condition = var.green_weight >= 0 && var.green_weight <= 100 - error_message = "green_weight must be between 0 and 100." - } -} - -# ── TLS / Domain ──────────────────────────────────────────────────────────── - -variable "domain_name" { - description = "FQDN for the application (empty to skip)" - type = string - default = "" -} - -# ── Storage ───────────────────────────────────────────────────────────────── - -variable "storage_size_gb" { - description = "Azure Files share quota in GiB" - type = number - default = 20 -} - -# ── Health check ──────────────────────────────────────────────────────────── - -variable "health_check_path" { - description = "HTTP path for health checks" - type = string - default = "/api/health" -} - -variable "health_check_interval" { - description = "Seconds between health checks" - type = number - default = 30 -} - -variable "health_check_timeout" { - description = "Seconds before a health check times out" - type = number - default = 5 -} - -variable "health_check_unhealthy_threshold" { - description = "Consecutive failures to mark unhealthy" - type = number - default = 3 -} - -# ── Monitoring ────────────────────────────────────────────────────────────── - -variable "enable_monitoring" { - description = "Enable Azure Monitor alerts and diagnostics" - type = bool - default = true -} - -variable "alert_email" { - description = "Email address for Azure Monitor alerts" - type = string - default = "" -} - -variable "log_retention_days" { - description = "Log Analytics workspace retention in days" - type = number - default = 30 -} diff --git a/deployments/terraform/providers/gcp/main.tf b/deployments/terraform/providers/gcp/main.tf deleted file mode 100644 index 12e4cd1..0000000 --- a/deployments/terraform/providers/gcp/main.tf +++ /dev/null @@ -1,678 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# GCP Provider – Full implementation for Claude Code Agent Monitor -# -# Architecture: -# VPC → Cloud Run (blue/green) → Filestore → Cloud Load Balancer -# → Cloud Monitoring → Managed SSL Certificate -# -# Cloud Run is chosen over GKE for cost efficiency and operational simplicity -# for this containerised workload. Filestore provides NFS for SQLite. -# ───────────────────────────────────────────────────────────────────────────── - -provider "google" { - project = var.gcp_project_id - region = var.region -} - -provider "google-beta" { - project = var.gcp_project_id - region = var.region -} - -# ── Locals ────────────────────────────────────────────────────────────────── - -locals { - name_prefix = lower(replace("${var.project_name}-${var.environment}", "_", "-")) - - common_labels = merge( - { - project = replace(var.project_name, "-", "_") - environment = var.environment - managed_by = "terraform" - cloud_provider = "gcp" - }, - { for k, v in var.tags : replace(k, "-", "_") => replace(v, "-", "_") }, - ) -} - -# ── Enable required APIs ─────────────────────────────────────────────────── - -resource "google_project_service" "apis" { - for_each = toset([ - "run.googleapis.com", - "compute.googleapis.com", - "file.googleapis.com", - "vpcaccess.googleapis.com", - "monitoring.googleapis.com", - "logging.googleapis.com", - "certificatemanager.googleapis.com", - ]) - - service = each.value - disable_on_destroy = false -} - -# ───────────────────────────────────────────────────────────────────────────── -# VPC Network -# ───────────────────────────────────────────────────────────────────────────── - -resource "google_compute_network" "main" { - name = "${local.name_prefix}-vpc" - auto_create_subnetworks = false - - depends_on = [google_project_service.apis] -} - -resource "google_compute_subnetwork" "private" { - name = "${local.name_prefix}-private" - ip_cidr_range = var.private_subnet_cidrs[0] - region = var.region - network = google_compute_network.main.id - - private_ip_google_access = true - - log_config { - aggregation_interval = "INTERVAL_5_SEC" - flow_sampling = 0.5 - } -} - -resource "google_compute_subnetwork" "proxy" { - name = "${local.name_prefix}-proxy" - ip_cidr_range = "10.0.100.0/24" - region = var.region - network = google_compute_network.main.id - purpose = "REGIONAL_MANAGED_PROXY" - role = "ACTIVE" -} - -# Cloud NAT for outbound internet -resource "google_compute_router" "main" { - name = "${local.name_prefix}-router" - region = var.region - network = google_compute_network.main.id -} - -resource "google_compute_router_nat" "main" { - name = "${local.name_prefix}-nat" - router = google_compute_router.main.name - region = var.region - nat_ip_allocate_option = "AUTO_ONLY" - source_subnetwork_ip_ranges_to_nat = "ALL_SUBNETWORKS_ALL_IP_RANGES" - - log_config { - enable = true - filter = "ERRORS_ONLY" - } -} - -# VPC Connector for Cloud Run → Filestore -resource "google_vpc_access_connector" "main" { - name = "${local.name_prefix}-conn" - region = var.region - network = google_compute_network.main.id - ip_cidr_range = "10.0.200.0/28" - min_instances = 2 - max_instances = var.environment == "production" ? 10 : 3 - - depends_on = [google_project_service.apis] -} - -# Firewall rules -resource "google_compute_firewall" "allow_health_checks" { - name = "${local.name_prefix}-allow-health-checks" - network = google_compute_network.main.id - - allow { - protocol = "tcp" - ports = [tostring(var.app_port), tostring(var.mcp_port)] - } - - source_ranges = ["130.211.0.0/22", "35.191.0.0/16"] # GCP health check ranges - target_tags = ["${local.name_prefix}-app"] -} - -resource "google_compute_firewall" "allow_internal" { - name = "${local.name_prefix}-allow-internal" - network = google_compute_network.main.id - - allow { - protocol = "tcp" - ports = [tostring(var.app_port), tostring(var.mcp_port), "2049"] - } - - source_ranges = [var.vpc_cidr] -} - -# ───────────────────────────────────────────────────────────────────────────── -# Filestore (NFS for SQLite persistence) -# ───────────────────────────────────────────────────────────────────────────── - -resource "google_filestore_instance" "main" { - name = "${local.name_prefix}-data" - location = "${var.region}-b" - tier = var.environment == "production" ? "BASIC_SSD" : "BASIC_HDD" - - file_shares { - name = "appdata" - capacity_gb = var.storage_size_gb - } - - networks { - network = google_compute_network.main.name - modes = ["MODE_IPV4"] - } - - labels = local.common_labels - - depends_on = [google_project_service.apis] - - lifecycle { - prevent_destroy = true - } -} - -# ───────────────────────────────────────────────────────────────────────────── -# Cloud Run services (Blue / Green) -# ───────────────────────────────────────────────────────────────────────────── - -resource "google_cloud_run_v2_service" "blue" { - name = "${local.name_prefix}-blue" - location = var.region - ingress = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER" - - template { - scaling { - min_instance_count = var.active_deployment_slot == "blue" ? var.min_replicas : 0 - max_instance_count = var.active_deployment_slot == "blue" ? var.max_replicas : 1 - } - - vpc_access { - connector = google_vpc_access_connector.main.id - egress = "ALL_TRAFFIC" - } - - containers { - image = var.app_container_image - name = "app" - - ports { - container_port = var.app_port - } - - resources { - limits = { - cpu = "${var.cpu}m" - memory = "${var.memory}Mi" - } - } - - dynamic "env" { - for_each = var.environment_variables - content { - name = env.key - value = env.value - } - } - - env { - name = "FILESTORE_IP" - value = google_filestore_instance.main.networks[0].ip_addresses[0] - } - - startup_probe { - http_get { - path = var.health_check_path - port = var.app_port - } - initial_delay_seconds = 10 - period_seconds = 10 - failure_threshold = 5 - } - - liveness_probe { - http_get { - path = var.health_check_path - port = var.app_port - } - period_seconds = 30 - failure_threshold = 3 - } - - volume_mounts { - name = "app-data" - mount_path = "/app/data" - } - } - - dynamic "containers" { - for_each = var.mcp_container_image != "" ? [1] : [] - content { - image = var.mcp_container_image - name = "mcp-sidecar" - - ports { - container_port = var.mcp_port - } - - resources { - limits = { - cpu = "250m" - memory = "256Mi" - } - } - - env { - name = "NODE_ENV" - value = "production" - } - } - } - - volumes { - name = "app-data" - nfs { - server = google_filestore_instance.main.networks[0].ip_addresses[0] - path = "/appdata" - read_only = false - } - } - - session_affinity = true - timeout = "300s" - } - - labels = local.common_labels - - depends_on = [google_project_service.apis] - - lifecycle { - ignore_changes = [ - client, - client_version, - ] - } -} - -resource "google_cloud_run_v2_service" "green" { - name = "${local.name_prefix}-green" - location = var.region - ingress = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER" - - template { - scaling { - min_instance_count = var.active_deployment_slot == "green" ? var.min_replicas : 0 - max_instance_count = var.active_deployment_slot == "green" ? var.max_replicas : 1 - } - - vpc_access { - connector = google_vpc_access_connector.main.id - egress = "ALL_TRAFFIC" - } - - containers { - image = var.app_container_image - name = "app" - - ports { - container_port = var.app_port - } - - resources { - limits = { - cpu = "${var.cpu}m" - memory = "${var.memory}Mi" - } - } - - dynamic "env" { - for_each = var.environment_variables - content { - name = env.key - value = env.value - } - } - - env { - name = "FILESTORE_IP" - value = google_filestore_instance.main.networks[0].ip_addresses[0] - } - - startup_probe { - http_get { - path = var.health_check_path - port = var.app_port - } - initial_delay_seconds = 10 - period_seconds = 10 - failure_threshold = 5 - } - - liveness_probe { - http_get { - path = var.health_check_path - port = var.app_port - } - period_seconds = 30 - failure_threshold = 3 - } - - volume_mounts { - name = "app-data" - mount_path = "/app/data" - } - } - - volumes { - name = "app-data" - nfs { - server = google_filestore_instance.main.networks[0].ip_addresses[0] - path = "/appdata" - read_only = false - } - } - - session_affinity = true - timeout = "300s" - } - - labels = local.common_labels - - depends_on = [google_project_service.apis] - - lifecycle { - ignore_changes = [ - client, - client_version, - ] - } -} - -# IAM – restrict access to load balancer service account only -# Cloud Run ingress is set to INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER, -# so public IAM bindings are not needed. The LB routes traffic internally. -# To grant specific service account access, replace with: -# member = "serviceAccount:@.iam.gserviceaccount.com" -# -# resource "google_cloud_run_v2_service_iam_member" "blue_invoker" { -# name = google_cloud_run_v2_service.blue.name -# location = var.region -# role = "roles/run.invoker" -# member = "serviceAccount:${var.project_id}-compute@developer.gserviceaccount.com" -# } -# -# resource "google_cloud_run_v2_service_iam_member" "green_invoker" { -# name = google_cloud_run_v2_service.green.name -# location = var.region -# role = "roles/run.invoker" -# member = "serviceAccount:${var.project_id}-compute@developer.gserviceaccount.com" -# } - -# ───────────────────────────────────────────────────────────────────────────── -# External Application Load Balancer -# ───────────────────────────────────────────────────────────────────────────── - -# Serverless NEGs for Cloud Run -resource "google_compute_region_network_endpoint_group" "blue" { - name = "${local.name_prefix}-blue-neg" - region = var.region - network_endpoint_type = "SERVERLESS" - - cloud_run { - service = google_cloud_run_v2_service.blue.name - } -} - -resource "google_compute_region_network_endpoint_group" "green" { - name = "${local.name_prefix}-green-neg" - region = var.region - network_endpoint_type = "SERVERLESS" - - cloud_run { - service = google_cloud_run_v2_service.green.name - } -} - -# Backend service with weighted backends for blue/green -resource "google_compute_backend_service" "main" { - name = "${local.name_prefix}-backend" - protocol = "HTTP" - load_balancing_scheme = "EXTERNAL_MANAGED" - timeout_sec = 300 # WebSocket support - - session_affinity = "GENERATED_COOKIE" - - backend { - group = google_compute_region_network_endpoint_group.blue.id - capacity_scaler = var.blue_weight / 100 - } - - backend { - group = google_compute_region_network_endpoint_group.green.id - capacity_scaler = var.green_weight / 100 - } - - health_checks = [google_compute_health_check.main.id] - - log_config { - enable = true - sample_rate = 1.0 - } -} - -resource "google_compute_health_check" "main" { - name = "${local.name_prefix}-hc" - - http_health_check { - port = var.app_port - request_path = var.health_check_path - } - - check_interval_sec = var.health_check_interval - timeout_sec = var.health_check_timeout - healthy_threshold = var.health_check_healthy_threshold - unhealthy_threshold = var.health_check_unhealthy_threshold -} - -# URL map -resource "google_compute_url_map" "main" { - name = "${local.name_prefix}-urlmap" - default_service = google_compute_backend_service.main.id -} - -# Managed SSL certificate (optional) -resource "google_compute_managed_ssl_certificate" "main" { - count = var.domain_name != "" ? 1 : 0 - - name = "${local.name_prefix}-cert" - - managed { - domains = [var.domain_name] - } -} - -# HTTPS proxy -resource "google_compute_target_https_proxy" "main" { - count = var.domain_name != "" ? 1 : 0 - - name = "${local.name_prefix}-https-proxy" - url_map = google_compute_url_map.main.id - ssl_certificates = [google_compute_managed_ssl_certificate.main[0].id] -} - -# HTTP proxy (for redirect or direct access) -resource "google_compute_target_http_proxy" "main" { - name = "${local.name_prefix}-http-proxy" - url_map = google_compute_url_map.main.id -} - -# Global forwarding rules -resource "google_compute_global_forwarding_rule" "https" { - count = var.domain_name != "" ? 1 : 0 - - name = "${local.name_prefix}-https" - target = google_compute_target_https_proxy.main[0].id - port_range = "443" - ip_protocol = "TCP" - load_balancing_scheme = "EXTERNAL_MANAGED" -} - -resource "google_compute_global_forwarding_rule" "http" { - name = "${local.name_prefix}-http" - target = google_compute_target_http_proxy.main.id - port_range = "80" - ip_protocol = "TCP" - load_balancing_scheme = "EXTERNAL_MANAGED" -} - -# ───────────────────────────────────────────────────────────────────────────── -# Cloud Monitoring – Alert policies -# ───────────────────────────────────────────────────────────────────────────── - -resource "google_monitoring_notification_channel" "email" { - count = var.alert_email != "" ? 1 : 0 - - display_name = "${local.name_prefix}-email" - type = "email" - - labels = { - email_address = var.alert_email - } -} - -resource "google_monitoring_alert_policy" "high_latency" { - count = var.enable_monitoring ? 1 : 0 - - display_name = "${local.name_prefix}-high-latency" - combiner = "OR" - - conditions { - display_name = "Cloud Run request latency > 2s" - - condition_threshold { - filter = "resource.type=\"cloud_run_revision\" AND metric.type=\"run.googleapis.com/request_latencies\"" - comparison = "COMPARISON_GT" - duration = "300s" - - threshold_value = 2000 # ms - - aggregations { - alignment_period = "60s" - per_series_aligner = "ALIGN_PERCENTILE_99" - } - } - } - - notification_channels = var.alert_email != "" ? [google_monitoring_notification_channel.email[0].id] : [] - - alert_strategy { - auto_close = "604800s" - } -} - -resource "google_monitoring_alert_policy" "high_error_rate" { - count = var.enable_monitoring ? 1 : 0 - - display_name = "${local.name_prefix}-high-error-rate" - combiner = "OR" - - conditions { - display_name = "Cloud Run 5xx error rate" - - condition_threshold { - filter = "resource.type=\"cloud_run_revision\" AND metric.type=\"run.googleapis.com/request_count\" AND metric.labels.response_code_class=\"5xx\"" - comparison = "COMPARISON_GT" - duration = "300s" - - threshold_value = 10 - - aggregations { - alignment_period = "60s" - per_series_aligner = "ALIGN_RATE" - } - } - } - - notification_channels = var.alert_email != "" ? [google_monitoring_notification_channel.email[0].id] : [] -} - -# ───────────────────────────────────────────────────────────────────────────── -# Cloud Monitoring Dashboard -# ───────────────────────────────────────────────────────────────────────────── - -resource "google_monitoring_dashboard" "main" { - count = var.enable_monitoring ? 1 : 0 - - dashboard_json = jsonencode({ - displayName = "${local.name_prefix} Dashboard" - gridLayout = { - columns = 2 - widgets = [ - { - title = "Request Count" - xyChart = { - dataSets = [{ - timeSeriesQuery = { - timeSeriesFilter = { - filter = "resource.type=\"cloud_run_revision\" AND metric.type=\"run.googleapis.com/request_count\"" - aggregation = { - alignmentPeriod = "60s" - perSeriesAligner = "ALIGN_RATE" - } - } - } - }] - } - }, - { - title = "Request Latency (p99)" - xyChart = { - dataSets = [{ - timeSeriesQuery = { - timeSeriesFilter = { - filter = "resource.type=\"cloud_run_revision\" AND metric.type=\"run.googleapis.com/request_latencies\"" - aggregation = { - alignmentPeriod = "60s" - perSeriesAligner = "ALIGN_PERCENTILE_99" - } - } - } - }] - } - }, - { - title = "Instance Count" - xyChart = { - dataSets = [{ - timeSeriesQuery = { - timeSeriesFilter = { - filter = "resource.type=\"cloud_run_revision\" AND metric.type=\"run.googleapis.com/container/instance_count\"" - aggregation = { - alignmentPeriod = "60s" - perSeriesAligner = "ALIGN_MEAN" - } - } - } - }] - } - }, - { - title = "CPU Utilization" - xyChart = { - dataSets = [{ - timeSeriesQuery = { - timeSeriesFilter = { - filter = "resource.type=\"cloud_run_revision\" AND metric.type=\"run.googleapis.com/container/cpu/utilizations\"" - aggregation = { - alignmentPeriod = "60s" - perSeriesAligner = "ALIGN_PERCENTILE_99" - } - } - } - }] - } - }, - ] - } - }) -} diff --git a/deployments/terraform/providers/gcp/outputs.tf b/deployments/terraform/providers/gcp/outputs.tf deleted file mode 100644 index b8a2b45..0000000 --- a/deployments/terraform/providers/gcp/outputs.tf +++ /dev/null @@ -1,53 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# GCP provider outputs -# ───────────────────────────────────────────────────────────────────────────── - -output "application_url" { - description = "Public URL of the Claude Code Agent Monitor dashboard" - value = var.domain_name != "" ? "https://${var.domain_name}" : "http://${google_compute_global_forwarding_rule.http.ip_address}" -} - -output "load_balancer_ip" { - description = "External IP address of the load balancer" - value = google_compute_global_forwarding_rule.http.ip_address -} - -output "blue_service_url" { - description = "URL of the blue Cloud Run service" - value = google_cloud_run_v2_service.blue.uri -} - -output "green_service_url" { - description = "URL of the green Cloud Run service" - value = google_cloud_run_v2_service.green.uri -} - -output "vpc_id" { - description = "Self-link of the VPC network" - value = google_compute_network.main.self_link -} - -output "filestore_ip" { - description = "IP address of the Filestore instance" - value = google_filestore_instance.main.networks[0].ip_addresses[0] -} - -output "filestore_share" { - description = "Filestore share name" - value = google_filestore_instance.main.file_shares[0].name -} - -output "monitoring_dashboard_url" { - description = "Cloud Monitoring dashboard URL" - value = var.enable_monitoring ? "https://console.cloud.google.com/monitoring/dashboards?project=${var.gcp_project_id}" : "monitoring disabled" -} - -output "project_id" { - description = "GCP project ID" - value = var.gcp_project_id -} - -output "region" { - description = "GCP region" - value = var.region -} diff --git a/deployments/terraform/providers/gcp/terraform.tf b/deployments/terraform/providers/gcp/terraform.tf deleted file mode 100644 index 0ed4b64..0000000 --- a/deployments/terraform/providers/gcp/terraform.tf +++ /dev/null @@ -1,18 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# GCP provider – Terraform and provider constraints -# ───────────────────────────────────────────────────────────────────────────── - -terraform { - required_version = ">= 1.5.0" - - required_providers { - google = { - source = "hashicorp/google" - version = "~> 5.0" - } - google-beta = { - source = "hashicorp/google-beta" - version = "~> 5.0" - } - } -} diff --git a/deployments/terraform/providers/gcp/variables.tf b/deployments/terraform/providers/gcp/variables.tf deleted file mode 100644 index 427b88e..0000000 --- a/deployments/terraform/providers/gcp/variables.tf +++ /dev/null @@ -1,201 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# GCP provider variables -# ───────────────────────────────────────────────────────────────────────────── - -variable "project_name" { - description = "Project identifier used in resource naming" - type = string - default = "claude-agent-monitor" -} - -variable "environment" { - description = "Deployment environment: dev, staging, or production" - type = string - validation { - condition = contains(["dev", "staging", "production"], var.environment) - error_message = "environment must be one of: dev, staging, production." - } -} - -variable "gcp_project_id" { - description = "GCP project ID for resource deployment" - type = string -} - -variable "region" { - description = "GCP region for resource deployment" - type = string - default = "us-central1" -} - -variable "tags" { - description = "Additional labels to apply to all resources" - type = map(string) - default = {} -} - -# ── Networking ────────────────────────────────────────────────────────────── - -variable "vpc_cidr" { - description = "CIDR block for the VPC (used for firewall rules)" - type = string - default = "10.0.0.0/16" -} - -variable "private_subnet_cidrs" { - description = "CIDR blocks for private subnets" - type = list(string) - default = ["10.0.11.0/24"] -} - -# ── Compute ───────────────────────────────────────────────────────────────── - -variable "app_container_image" { - description = "Container image URI for the main application" - type = string -} - -variable "mcp_container_image" { - description = "Container image URI for the MCP sidecar (empty to disable)" - type = string - default = "" -} - -variable "app_port" { - description = "Application container port" - type = number - default = 4820 -} - -variable "mcp_port" { - description = "MCP sidecar container port" - type = number - default = 8819 -} - -variable "cpu" { - description = "CPU millicores for each Cloud Run instance" - type = number - default = 512 -} - -variable "memory" { - description = "Memory in MiB for each Cloud Run instance" - type = number - default = 1024 -} - -variable "min_replicas" { - description = "Minimum number of Cloud Run instances" - type = number - default = 0 -} - -variable "max_replicas" { - description = "Maximum number of Cloud Run instances" - type = number - default = 3 -} - -variable "environment_variables" { - description = "Environment variables for the application container" - type = map(string) - default = { - NODE_ENV = "production" - DASHBOARD_PORT = "4820" - } -} - -# ── Deployment ────────────────────────────────────────────────────────────── - -variable "active_deployment_slot" { - description = "Active deployment slot: blue or green" - type = string - default = "blue" - validation { - condition = contains(["blue", "green"], var.active_deployment_slot) - error_message = "active_deployment_slot must be blue or green." - } -} - -variable "blue_weight" { - description = "Traffic weight for blue service (0-100)" - type = number - default = 100 - validation { - condition = var.blue_weight >= 0 && var.blue_weight <= 100 - error_message = "blue_weight must be between 0 and 100." - } -} - -variable "green_weight" { - description = "Traffic weight for green service (0-100)" - type = number - default = 0 - validation { - condition = var.green_weight >= 0 && var.green_weight <= 100 - error_message = "green_weight must be between 0 and 100." - } -} - -# ── TLS / Domain ──────────────────────────────────────────────────────────── - -variable "domain_name" { - description = "FQDN for managed SSL certificate (empty for HTTP only)" - type = string - default = "" -} - -# ── Storage ───────────────────────────────────────────────────────────────── - -variable "storage_size_gb" { - description = "Filestore capacity in GiB" - type = number - default = 1024 # Filestore minimum for BASIC_HDD -} - -# ── Health check ──────────────────────────────────────────────────────────── - -variable "health_check_path" { - description = "HTTP path for health checks" - type = string - default = "/api/health" -} - -variable "health_check_interval" { - description = "Seconds between health checks" - type = number - default = 30 -} - -variable "health_check_timeout" { - description = "Seconds before a health check times out" - type = number - default = 5 -} - -variable "health_check_healthy_threshold" { - description = "Consecutive successes to mark healthy" - type = number - default = 2 -} - -variable "health_check_unhealthy_threshold" { - description = "Consecutive failures to mark unhealthy" - type = number - default = 3 -} - -# ── Monitoring ────────────────────────────────────────────────────────────── - -variable "enable_monitoring" { - description = "Enable Cloud Monitoring alerts and dashboard" - type = bool - default = true -} - -variable "alert_email" { - description = "Email for monitoring notifications" - type = string - default = "" -} diff --git a/deployments/terraform/providers/oci/main.tf b/deployments/terraform/providers/oci/main.tf deleted file mode 100644 index 63f3bca..0000000 --- a/deployments/terraform/providers/oci/main.tf +++ /dev/null @@ -1,602 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# OCI Provider – Full implementation for Claude Code Agent Monitor -# -# Architecture: -# VCN → Container Instances (blue/green) → File Storage Service -# → Flexible Load Balancer → OCI Monitoring & Notifications -# -# OCI Container Instances provide a serverless container runtime. -# File Storage Service (FSS) delivers NFS for SQLite persistence. -# Flexible Load Balancer supports WebSocket, SSL, and weighted backends. -# ───────────────────────────────────────────────────────────────────────────── - -provider "oci" { - region = var.region -} - -# ── Data sources ──────────────────────────────────────────────────────────── - -data "oci_identity_availability_domains" "ads" { - compartment_id = var.compartment_id -} - -data "oci_identity_tenancy" "current" { - tenancy_id = var.tenancy_id -} - -# ── Locals ────────────────────────────────────────────────────────────────── - -locals { - name_prefix = lower(replace("${var.project_name}-${var.environment}", "_", "-")) - ad_name = data.oci_identity_availability_domains.ads.availability_domains[0].name - - common_tags = { - "project" = var.project_name - "environment" = var.environment - "managed_by" = "terraform" - "cloud_provider" = "oci" - "repository" = "Claude-Code-Agent-Monitor" - } -} - -# ───────────────────────────────────────────────────────────────────────────── -# VCN (Virtual Cloud Network) -# ───────────────────────────────────────────────────────────────────────────── - -resource "oci_core_vcn" "main" { - compartment_id = var.compartment_id - cidr_blocks = [var.vpc_cidr] - display_name = "${local.name_prefix}-vcn" - dns_label = replace(substr(local.name_prefix, 0, 15), "-", "") - - freeform_tags = local.common_tags - - lifecycle { - prevent_destroy = false - } -} - -# Internet Gateway -resource "oci_core_internet_gateway" "main" { - compartment_id = var.compartment_id - vcn_id = oci_core_vcn.main.id - display_name = "${local.name_prefix}-igw" - enabled = true - - freeform_tags = local.common_tags -} - -# NAT Gateway -resource "oci_core_nat_gateway" "main" { - compartment_id = var.compartment_id - vcn_id = oci_core_vcn.main.id - display_name = "${local.name_prefix}-nat" - - freeform_tags = local.common_tags -} - -# Service Gateway -resource "oci_core_service_gateway" "main" { - compartment_id = var.compartment_id - vcn_id = oci_core_vcn.main.id - display_name = "${local.name_prefix}-sgw" - - services { - service_id = data.oci_core_services.all.services[0].id - } - - freeform_tags = local.common_tags -} - -data "oci_core_services" "all" { - filter { - name = "name" - values = ["All .* Services In Oracle Services Network"] - regex = true - } -} - -# ── Route tables ──────────────────────────────────────────────────────────── - -resource "oci_core_route_table" "public" { - compartment_id = var.compartment_id - vcn_id = oci_core_vcn.main.id - display_name = "${local.name_prefix}-public-rt" - - route_rules { - network_entity_id = oci_core_internet_gateway.main.id - destination = "0.0.0.0/0" - destination_type = "CIDR_BLOCK" - } - - freeform_tags = local.common_tags -} - -resource "oci_core_route_table" "private" { - compartment_id = var.compartment_id - vcn_id = oci_core_vcn.main.id - display_name = "${local.name_prefix}-private-rt" - - route_rules { - network_entity_id = oci_core_nat_gateway.main.id - destination = "0.0.0.0/0" - destination_type = "CIDR_BLOCK" - } - - route_rules { - network_entity_id = oci_core_service_gateway.main.id - destination = data.oci_core_services.all.services[0].cidr_block - destination_type = "SERVICE_CIDR_BLOCK" - } - - freeform_tags = local.common_tags -} - -# ── Security lists ────────────────────────────────────────────────────────── - -resource "oci_core_security_list" "public" { - compartment_id = var.compartment_id - vcn_id = oci_core_vcn.main.id - display_name = "${local.name_prefix}-public-sl" - - ingress_security_rules { - protocol = "6" # TCP - source = "0.0.0.0/0" - source_type = "CIDR_BLOCK" - tcp_options { - min = 443 - max = 443 - } - } - - ingress_security_rules { - protocol = "6" - source = "0.0.0.0/0" - source_type = "CIDR_BLOCK" - tcp_options { - min = 80 - max = 80 - } - } - - egress_security_rules { - protocol = "all" - destination = "0.0.0.0/0" - destination_type = "CIDR_BLOCK" - } - - freeform_tags = local.common_tags -} - -resource "oci_core_security_list" "private" { - compartment_id = var.compartment_id - vcn_id = oci_core_vcn.main.id - display_name = "${local.name_prefix}-private-sl" - - ingress_security_rules { - protocol = "6" - source = var.vpc_cidr - source_type = "CIDR_BLOCK" - tcp_options { - min = var.app_port - max = var.app_port - } - } - - ingress_security_rules { - protocol = "6" - source = var.vpc_cidr - source_type = "CIDR_BLOCK" - tcp_options { - min = var.mcp_port - max = var.mcp_port - } - } - - # NFS (FSS) - ingress_security_rules { - protocol = "6" - source = var.vpc_cidr - source_type = "CIDR_BLOCK" - tcp_options { - min = 2048 - max = 2050 - } - } - - ingress_security_rules { - protocol = "6" - source = var.vpc_cidr - source_type = "CIDR_BLOCK" - tcp_options { - min = 111 - max = 111 - } - } - - ingress_security_rules { - protocol = "17" # UDP - source = var.vpc_cidr - source_type = "CIDR_BLOCK" - udp_options { - min = 111 - max = 111 - } - } - - egress_security_rules { - protocol = "all" - destination = "0.0.0.0/0" - destination_type = "CIDR_BLOCK" - } - - freeform_tags = local.common_tags -} - -# ── Subnets ───────────────────────────────────────────────────────────────── - -resource "oci_core_subnet" "public" { - compartment_id = var.compartment_id - vcn_id = oci_core_vcn.main.id - cidr_block = var.public_subnet_cidrs[0] - display_name = "${local.name_prefix}-public" - dns_label = "pub" - route_table_id = oci_core_route_table.public.id - security_list_ids = [oci_core_security_list.public.id] - - freeform_tags = local.common_tags -} - -resource "oci_core_subnet" "private" { - compartment_id = var.compartment_id - vcn_id = oci_core_vcn.main.id - cidr_block = var.private_subnet_cidrs[0] - display_name = "${local.name_prefix}-private" - dns_label = "priv" - route_table_id = oci_core_route_table.private.id - security_list_ids = [oci_core_security_list.private.id] - prohibit_public_ip_on_vnic = true - - freeform_tags = local.common_tags -} - -# ───────────────────────────────────────────────────────────────────────────── -# File Storage Service (FSS) – NFS for SQLite -# ───────────────────────────────────────────────────────────────────────────── - -resource "oci_file_storage_file_system" "main" { - compartment_id = var.compartment_id - availability_domain = local.ad_name - display_name = "${local.name_prefix}-data" - - freeform_tags = local.common_tags - - lifecycle { - prevent_destroy = true - } -} - -resource "oci_file_storage_mount_target" "main" { - compartment_id = var.compartment_id - availability_domain = local.ad_name - subnet_id = oci_core_subnet.private.id - display_name = "${local.name_prefix}-mt" - - freeform_tags = local.common_tags -} - -resource "oci_file_storage_export_set" "main" { - mount_target_id = oci_file_storage_mount_target.main.id - display_name = "${local.name_prefix}-exports" - max_fs_stat_bytes = var.storage_size_gb * 1073741824 # GiB → bytes -} - -resource "oci_file_storage_export" "main" { - export_set_id = oci_file_storage_export_set.main.id - file_system_id = oci_file_storage_file_system.main.id - path = "/appdata" - - export_options { - source = var.private_subnet_cidrs[0] - access = "READ_WRITE" - identity_squash = "NONE" - require_privileged_source_port = false - } -} - -# ───────────────────────────────────────────────────────────────────────────── -# Container Instances (Blue / Green) -# ───────────────────────────────────────────────────────────────────────────── - -resource "oci_container_instances_container_instance" "blue" { - compartment_id = var.compartment_id - availability_domain = local.ad_name - display_name = "${local.name_prefix}-blue" - - shape = "CI.Standard.E4.Flex" - shape_config { - ocpus = var.cpu / 1000.0 - memory_in_gbs = var.memory / 1024.0 - } - - vnics { - subnet_id = oci_core_subnet.private.id - is_public_ip_assigned = false - } - - containers { - display_name = "app" - image_url = var.app_container_image - - environment_variables = var.environment_variables - - health_checks { - health_check_type = "HTTP" - port = var.app_port - path = var.health_check_path - interval_in_seconds = 30 - timeout_in_seconds = 5 - } - - resource_config { - vcpus_limit = var.cpu / 1000.0 - memory_limit_in_gbs = var.memory / 1024.0 - } - - volume_mounts { - mount_path = "/app/data" - volume_name = "app-data" - is_read_only = false - } - } - - dynamic "containers" { - for_each = var.mcp_container_image != "" ? [1] : [] - content { - display_name = "mcp-sidecar" - image_url = var.mcp_container_image - - environment_variables = { - NODE_ENV = "production" - MCP_PORT = tostring(var.mcp_port) - } - - resource_config { - vcpus_limit = 0.25 - memory_limit_in_gbs = 0.25 - } - } - } - - # NOTE: OCI Container Instances only support EMPTYDIR and CONFIGFILE volume - # types. For persistent NFS (FSS) storage, mount via the container entrypoint - # using the mount target IP from oci_file_storage_mount_target.main, or - # migrate to OCI Kubernetes Engine (OKE) which supports NFS PersistentVolumes. - volumes { - name = "app-data" - volume_type = "EMPTYDIR" - backing_store = "EPHEMERAL_STORAGE" - } - - freeform_tags = merge(local.common_tags, { - deployment_slot = "blue" - }) - - lifecycle { - ignore_changes = [ - freeform_tags["last_deployed"], - ] - } -} - -resource "oci_container_instances_container_instance" "green" { - count = var.active_deployment_slot == "green" || var.green_weight > 0 ? 1 : 0 - - compartment_id = var.compartment_id - availability_domain = local.ad_name - display_name = "${local.name_prefix}-green" - - shape = "CI.Standard.E4.Flex" - shape_config { - ocpus = var.cpu / 1000.0 - memory_in_gbs = var.memory / 1024.0 - } - - vnics { - subnet_id = oci_core_subnet.private.id - is_public_ip_assigned = false - } - - containers { - display_name = "app" - image_url = var.app_container_image - - environment_variables = var.environment_variables - - health_checks { - health_check_type = "HTTP" - port = var.app_port - path = var.health_check_path - interval_in_seconds = 30 - timeout_in_seconds = 5 - } - - resource_config { - vcpus_limit = var.cpu / 1000.0 - memory_limit_in_gbs = var.memory / 1024.0 - } - - volume_mounts { - mount_path = "/app/data" - volume_name = "app-data" - is_read_only = false - } - } - - # NOTE: OCI Container Instances only support EMPTYDIR and CONFIGFILE volume - # types. See blue instance comment for FSS mounting guidance. - volumes { - name = "app-data" - volume_type = "EMPTYDIR" - backing_store = "EPHEMERAL_STORAGE" - } - - freeform_tags = merge(local.common_tags, { - deployment_slot = "green" - }) -} - -# ───────────────────────────────────────────────────────────────────────────── -# Flexible Load Balancer -# ───────────────────────────────────────────────────────────────────────────── - -resource "oci_load_balancer_load_balancer" "main" { - compartment_id = var.compartment_id - display_name = "${local.name_prefix}-lb" - shape = "flexible" - - shape_details { - minimum_bandwidth_in_mbps = var.environment == "production" ? 100 : 10 - maximum_bandwidth_in_mbps = var.environment == "production" ? 1000 : 100 - } - - subnet_ids = [oci_core_subnet.public.id] - - is_private = false - - freeform_tags = local.common_tags - - lifecycle { - prevent_destroy = false - } -} - -# Backend set with health check -resource "oci_load_balancer_backend_set" "app" { - load_balancer_id = oci_load_balancer_load_balancer.main.id - name = "${local.name_prefix}-app-bs" - policy = "ROUND_ROBIN" - - session_persistence_configuration { - cookie_name = "CCAM_SESSION" - is_secure = true - } - - health_checker { - protocol = "HTTP" - port = var.app_port - url_path = var.health_check_path - return_code = 200 - interval_ms = var.health_check_interval * 1000 - timeout_in_millis = var.health_check_timeout * 1000 - retries = var.health_check_unhealthy_threshold - } -} - -# Blue backend -resource "oci_load_balancer_backend" "blue" { - load_balancer_id = oci_load_balancer_load_balancer.main.id - backendset_name = oci_load_balancer_backend_set.app.name - ip_address = oci_container_instances_container_instance.blue.vnics[0].private_ip - port = var.app_port - weight = var.blue_weight -} - -# Green backend -resource "oci_load_balancer_backend" "green" { - count = length(oci_container_instances_container_instance.green) > 0 ? 1 : 0 - - load_balancer_id = oci_load_balancer_load_balancer.main.id - backendset_name = oci_load_balancer_backend_set.app.name - ip_address = oci_container_instances_container_instance.green[0].vnics[0].private_ip - port = var.app_port - weight = var.green_weight -} - -# HTTP listener -resource "oci_load_balancer_listener" "http" { - load_balancer_id = oci_load_balancer_load_balancer.main.id - name = "${local.name_prefix}-http" - default_backend_set_name = oci_load_balancer_backend_set.app.name - port = 80 - protocol = "HTTP" - - connection_configuration { - idle_timeout_in_seconds = 300 # WebSocket support - } -} - -# ───────────────────────────────────────────────────────────────────────────── -# OCI Monitoring – Alarms and Notifications -# ───────────────────────────────────────────────────────────────────────────── - -resource "oci_ons_notification_topic" "alerts" { - count = var.enable_monitoring ? 1 : 0 - - compartment_id = var.compartment_id - name = "${local.name_prefix}-alerts" - - freeform_tags = local.common_tags -} - -resource "oci_ons_subscription" "email" { - count = var.enable_monitoring && var.alert_email != "" ? 1 : 0 - - compartment_id = var.compartment_id - topic_id = oci_ons_notification_topic.alerts[0].id - protocol = "EMAIL" - endpoint = var.alert_email - - freeform_tags = local.common_tags -} - -resource "oci_monitoring_alarm" "lb_unhealthy" { - count = var.enable_monitoring ? 1 : 0 - - compartment_id = var.compartment_id - display_name = "${local.name_prefix}-unhealthy-backends" - namespace = "oci_lbaas" - query = "UnHealthyBackendCount[1m]{resourceId = \"${oci_load_balancer_load_balancer.main.id}\"}.max() > 0" - severity = "CRITICAL" - is_enabled = true - pending_duration = "PT5M" - - destinations = var.alert_email != "" ? [oci_ons_notification_topic.alerts[0].id] : [] - - message_format = "ONS_OPTIMIZED" - body = "Unhealthy backends detected for ${local.name_prefix} load balancer" - - freeform_tags = local.common_tags -} - -resource "oci_monitoring_alarm" "lb_5xx" { - count = var.enable_monitoring ? 1 : 0 - - compartment_id = var.compartment_id - display_name = "${local.name_prefix}-high-5xx" - namespace = "oci_lbaas" - query = "HttpResponses5xx[1m]{resourceId = \"${oci_load_balancer_load_balancer.main.id}\"}.sum() > 10" - severity = "WARNING" - is_enabled = true - pending_duration = "PT5M" - - destinations = var.alert_email != "" ? [oci_ons_notification_topic.alerts[0].id] : [] - - freeform_tags = local.common_tags -} - -resource "oci_monitoring_alarm" "high_latency" { - count = var.enable_monitoring ? 1 : 0 - - compartment_id = var.compartment_id - display_name = "${local.name_prefix}-high-latency" - namespace = "oci_lbaas" - query = "BackendTimeFirstByte[1m]{resourceId = \"${oci_load_balancer_load_balancer.main.id}\"}.percentile(0.99) > 2000" - severity = "WARNING" - is_enabled = true - pending_duration = "PT5M" - - destinations = var.alert_email != "" ? [oci_ons_notification_topic.alerts[0].id] : [] - - freeform_tags = local.common_tags -} diff --git a/deployments/terraform/providers/oci/outputs.tf b/deployments/terraform/providers/oci/outputs.tf deleted file mode 100644 index ebc954a..0000000 --- a/deployments/terraform/providers/oci/outputs.tf +++ /dev/null @@ -1,53 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# OCI provider outputs -# ───────────────────────────────────────────────────────────────────────────── - -output "application_url" { - description = "Public URL of the Claude Code Agent Monitor dashboard" - value = "http://${oci_load_balancer_load_balancer.main.ip_address_details[0].ip_address}" -} - -output "load_balancer_ip" { - description = "Public IP address of the load balancer" - value = oci_load_balancer_load_balancer.main.ip_address_details[0].ip_address -} - -output "vcn_id" { - description = "OCID of the VCN" - value = oci_core_vcn.main.id -} - -output "blue_instance_id" { - description = "OCID of the blue container instance" - value = oci_container_instances_container_instance.blue.id -} - -output "green_instance_id" { - description = "OCID of the green container instance (if deployed)" - value = length(oci_container_instances_container_instance.green) > 0 ? oci_container_instances_container_instance.green[0].id : "" -} - -output "file_system_id" { - description = "OCID of the File Storage file system" - value = oci_file_storage_file_system.main.id -} - -output "mount_target_ip" { - description = "IP address of the FSS mount target" - value = oci_file_storage_mount_target.main.ip_address -} - -output "load_balancer_id" { - description = "OCID of the load balancer" - value = oci_load_balancer_load_balancer.main.id -} - -output "compartment_id" { - description = "OCI compartment OCID" - value = var.compartment_id -} - -output "region" { - description = "OCI region" - value = var.region -} diff --git a/deployments/terraform/providers/oci/terraform.tf b/deployments/terraform/providers/oci/terraform.tf deleted file mode 100644 index 2d2db36..0000000 --- a/deployments/terraform/providers/oci/terraform.tf +++ /dev/null @@ -1,14 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# OCI provider – Terraform and provider constraints -# ───────────────────────────────────────────────────────────────────────────── - -terraform { - required_version = ">= 1.5.0" - - required_providers { - oci = { - source = "oracle/oci" - version = "~> 5.0" - } - } -} diff --git a/deployments/terraform/providers/oci/variables.tf b/deployments/terraform/providers/oci/variables.tf deleted file mode 100644 index ab25b42..0000000 --- a/deployments/terraform/providers/oci/variables.tf +++ /dev/null @@ -1,186 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# OCI provider variables -# ───────────────────────────────────────────────────────────────────────────── - -variable "project_name" { - description = "Project identifier used in resource naming" - type = string - default = "claude-agent-monitor" -} - -variable "environment" { - description = "Deployment environment: dev, staging, or production" - type = string - validation { - condition = contains(["dev", "staging", "production"], var.environment) - error_message = "environment must be one of: dev, staging, production." - } -} - -variable "region" { - description = "OCI region for resource deployment" - type = string - default = "us-ashburn-1" -} - -variable "tenancy_id" { - description = "OCI tenancy OCID" - type = string -} - -variable "compartment_id" { - description = "OCI compartment OCID for resource deployment" - type = string -} - -variable "tags" { - description = "Additional freeform tags to apply to all resources" - type = map(string) - default = {} -} - -# ── Networking ────────────────────────────────────────────────────────────── - -variable "vpc_cidr" { - description = "CIDR block for the VCN" - type = string - default = "10.0.0.0/16" -} - -variable "public_subnet_cidrs" { - description = "CIDR blocks for public subnets" - type = list(string) - default = ["10.0.1.0/24"] -} - -variable "private_subnet_cidrs" { - description = "CIDR blocks for private subnets" - type = list(string) - default = ["10.0.11.0/24"] -} - -# ── Compute ───────────────────────────────────────────────────────────────── - -variable "app_container_image" { - description = "Container image URI for the main application" - type = string -} - -variable "mcp_container_image" { - description = "Container image URI for the MCP sidecar (empty to disable)" - type = string - default = "" -} - -variable "app_port" { - description = "Application container port" - type = number - default = 4820 -} - -variable "mcp_port" { - description = "MCP sidecar container port" - type = number - default = 8819 -} - -variable "cpu" { - description = "CPU millicores (converted to OCPUs: 1000m = 1 OCPU)" - type = number - default = 512 -} - -variable "memory" { - description = "Memory in MiB (converted to GiB for OCI)" - type = number - default = 1024 -} - -variable "environment_variables" { - description = "Environment variables for the application container" - type = map(string) - default = { - NODE_ENV = "production" - DASHBOARD_PORT = "4820" - } -} - -# ── Deployment ────────────────────────────────────────────────────────────── - -variable "active_deployment_slot" { - description = "Active deployment slot: blue or green" - type = string - default = "blue" - validation { - condition = contains(["blue", "green"], var.active_deployment_slot) - error_message = "active_deployment_slot must be blue or green." - } -} - -variable "blue_weight" { - description = "Traffic weight for blue backend (0-100)" - type = number - default = 100 - validation { - condition = var.blue_weight >= 0 && var.blue_weight <= 100 - error_message = "blue_weight must be between 0 and 100." - } -} - -variable "green_weight" { - description = "Traffic weight for green backend (0-100)" - type = number - default = 0 - validation { - condition = var.green_weight >= 0 && var.green_weight <= 100 - error_message = "green_weight must be between 0 and 100." - } -} - -# ── Storage ───────────────────────────────────────────────────────────────── - -variable "storage_size_gb" { - description = "FSS export size limit in GiB" - type = number - default = 50 -} - -# ── Health check ──────────────────────────────────────────────────────────── - -variable "health_check_path" { - description = "HTTP path for health checks" - type = string - default = "/api/health" -} - -variable "health_check_interval" { - description = "Seconds between health checks" - type = number - default = 30 -} - -variable "health_check_timeout" { - description = "Seconds before a health check times out" - type = number - default = 5 -} - -variable "health_check_unhealthy_threshold" { - description = "Consecutive failures to mark unhealthy" - type = number - default = 3 -} - -# ── Monitoring ────────────────────────────────────────────────────────────── - -variable "enable_monitoring" { - description = "Enable OCI Monitoring alarms and notifications" - type = bool - default = true -} - -variable "alert_email" { - description = "Email address for alarm notifications" - type = string - default = "" -} diff --git a/deployments/terraform/variables.tf b/deployments/terraform/variables.tf deleted file mode 100644 index 4c4d141..0000000 --- a/deployments/terraform/variables.tf +++ /dev/null @@ -1,280 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# Root module variables – Claude Code Agent Monitor -# ───────────────────────────────────────────────────────────────────────────── - -# ── Provider selection ────────────────────────────────────────────────────── - -variable "cloud_provider" { - description = "Target cloud provider: aws, gcp, azure, or oci" - type = string - validation { - condition = contains(["aws", "gcp", "azure", "oci"], var.cloud_provider) - error_message = "cloud_provider must be one of: aws, gcp, azure, oci." - } -} - -variable "region" { - description = "Cloud provider region for resource deployment" - type = string -} - -# ── Project metadata ──────────────────────────────────────────────────────── - -variable "project_name" { - description = "Project identifier used in resource naming and tagging" - type = string - default = "claude-agent-monitor" -} - -variable "environment" { - description = "Deployment environment: dev, staging, or production" - type = string - validation { - condition = contains(["dev", "staging", "production"], var.environment) - error_message = "environment must be one of: dev, staging, production." - } -} - -variable "tags" { - description = "Additional tags to apply to all resources" - type = map(string) - default = {} -} - -# ── Networking ────────────────────────────────────────────────────────────── - -variable "vpc_cidr" { - description = "CIDR block for the VPC / VNet / VCN" - type = string - default = "10.0.0.0/16" - validation { - condition = can(cidrhost(var.vpc_cidr, 0)) - error_message = "vpc_cidr must be a valid CIDR block." - } -} - -variable "availability_zones" { - description = "List of availability zones for multi-AZ deployment" - type = list(string) - default = [] -} - -variable "public_subnet_cidrs" { - description = "CIDR blocks for public subnets (one per AZ)" - type = list(string) - default = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] -} - -variable "private_subnet_cidrs" { - description = "CIDR blocks for private subnets (one per AZ)" - type = list(string) - default = ["10.0.11.0/24", "10.0.12.0/24", "10.0.13.0/24"] -} - -# ── Compute ───────────────────────────────────────────────────────────────── - -variable "app_container_image" { - description = "Container image URI for the main application" - type = string -} - -variable "mcp_container_image" { - description = "Container image URI for the MCP sidecar" - type = string - default = "" -} - -variable "app_port" { - description = "Port the application container listens on" - type = number - default = 4820 -} - -variable "mcp_port" { - description = "Port the MCP sidecar container listens on" - type = number - default = 8819 -} - -variable "cpu" { - description = "CPU units for each container instance (e.g. 256, 512, 1024)" - type = number - default = 512 - validation { - condition = contains([256, 512, 1024, 2048, 4096], var.cpu) - error_message = "cpu must be one of: 256, 512, 1024, 2048, 4096 (valid Fargate CPU values)." - } -} - -variable "memory" { - description = "Memory in MiB for each container instance" - type = number - default = 1024 -} - -variable "min_replicas" { - description = "Minimum number of container replicas" - type = number - default = 1 -} - -variable "max_replicas" { - description = "Maximum number of container replicas for auto-scaling" - type = number - default = 3 -} - -variable "desired_replicas" { - description = "Desired number of container replicas at steady state" - type = number - default = 1 -} - -variable "environment_variables" { - description = "Environment variables passed to the application container" - type = map(string) - default = { - NODE_ENV = "production" - DASHBOARD_PORT = "4820" - } -} - -# ── Deployment strategy ───────────────────────────────────────────────────── - -variable "deployment_strategy" { - description = "Deployment strategy: rolling, blue-green, or canary" - type = string - default = "rolling" - validation { - condition = contains(["rolling", "blue-green", "canary"], var.deployment_strategy) - error_message = "deployment_strategy must be one of: rolling, blue-green, canary." - } -} - -variable "active_deployment_slot" { - description = "Active deployment slot for blue-green: blue or green" - type = string - default = "blue" - validation { - condition = contains(["blue", "green"], var.active_deployment_slot) - error_message = "active_deployment_slot must be blue or green." - } -} - -variable "blue_weight" { - description = "Traffic weight percentage for the blue deployment slot (0-100)" - type = number - default = 100 - validation { - condition = var.blue_weight >= 0 && var.blue_weight <= 100 - error_message = "blue_weight must be between 0 and 100." - } -} - -variable "green_weight" { - description = "Traffic weight percentage for the green deployment slot (0-100)" - type = number - default = 0 - validation { - condition = var.green_weight >= 0 && var.green_weight <= 100 - error_message = "green_weight must be between 0 and 100." - } -} - -# ── TLS / Domain ──────────────────────────────────────────────────────────── - -variable "domain_name" { - description = "Fully qualified domain name for the application" - type = string - default = "" -} - -variable "tls_certificate_arn" { - description = "ARN / ID of the TLS certificate for HTTPS termination" - type = string - default = "" -} - -# ── Monitoring ────────────────────────────────────────────────────────────── - -variable "enable_monitoring" { - description = "Enable monitoring, alerting, and log aggregation" - type = bool - default = true -} - -variable "alert_email" { - description = "Email address for monitoring alert notifications" - type = string - default = "" -} - -variable "log_retention_days" { - description = "Number of days to retain application logs" - type = number - default = 30 - validation { - condition = contains([1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653], var.log_retention_days) - error_message = "log_retention_days must be a valid CloudWatch retention period (1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, or 3653)." - } -} - -# ── Storage ───────────────────────────────────────────────────────────────── - -variable "storage_size_gb" { - description = "Persistent storage size in GiB for the SQLite database" - type = number - default = 20 -} - -variable "enable_storage_backup" { - description = "Enable automated backup of persistent storage" - type = bool - default = true -} - -# ── Auto-scaling ──────────────────────────────────────────────────────────── - -variable "autoscaling_cpu_target" { - description = "Target CPU utilization percentage for auto-scaling" - type = number - default = 70 -} - -variable "autoscaling_memory_target" { - description = "Target memory utilization percentage for auto-scaling" - type = number - default = 80 -} - -# ── Health check ──────────────────────────────────────────────────────────── - -variable "health_check_path" { - description = "HTTP path for application health checks" - type = string - default = "/api/health" -} - -variable "health_check_interval" { - description = "Interval in seconds between health checks" - type = number - default = 30 -} - -variable "health_check_timeout" { - description = "Timeout in seconds for each health check request" - type = number - default = 5 -} - -variable "health_check_healthy_threshold" { - description = "Consecutive successes required to mark target healthy" - type = number - default = 2 -} - -variable "health_check_unhealthy_threshold" { - description = "Consecutive failures required to mark target unhealthy" - type = number - default = 3 -} diff --git a/deployments/terraform/versions.tf b/deployments/terraform/versions.tf deleted file mode 100644 index 586bef6..0000000 --- a/deployments/terraform/versions.tf +++ /dev/null @@ -1,26 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# Claude Code Agent Monitor – Terraform version constraints -# ───────────────────────────────────────────────────────────────────────────── - -terraform { - required_version = ">= 1.5.0" - - required_providers { - aws = { - source = "hashicorp/aws" - version = "~> 5.0" - } - google = { - source = "hashicorp/google" - version = "~> 5.0" - } - azurerm = { - source = "hashicorp/azurerm" - version = "~> 3.80" - } - oci = { - source = "oracle/oci" - version = "~> 5.0" - } - } -} diff --git a/desktop/.gitignore b/desktop/.gitignore deleted file mode 100644 index edae83c..0000000 --- a/desktop/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -node_modules/ -out/ -release/ -*.log -.DS_Store - -# Generated icon binaries — regenerable from assets/icon.svg via scripts/build-icons.sh. -# Committed copies live alongside the SVG so users without iconutil can still build. -# Local rebuilds are ignored. -assets/icon.iconset/ diff --git a/desktop/README.md b/desktop/README.md deleted file mode 100644 index 9c2a4dd..0000000 --- a/desktop/README.md +++ /dev/null @@ -1,833 +0,0 @@ -# `desktop/` — Native macOS App (Electron) - -The **desktop workspace** ships the Claude Code Agent Monitor dashboard as a -native macOS `.app` (distributed as a `.dmg`). It is an Electron shell that -**embeds the existing Express server in-process** and renders the already-built -React client in a `BrowserWindow`. - -> **One-line mental model:** *Electron is a window onto the same code.* 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, tray menu, -auto-start) see [`../DESKTOP.md`](../DESKTOP.md). This file is the -**contributor / architecture** reference. - ---- - -## Table of contents - -- [TL;DR](#tldr) -- [Where the desktop app sits](#where-the-desktop-app-sits) -- [Process model](#process-model) -- [Boot lifecycle](#boot-lifecycle) -- [Server hosting & port discovery](#server-hosting--port-discovery) -- [`better-sqlite3` native-module handling](#better-sqlite3-native-module-handling) -- [Background services & hook bootstrap](#background-services--hook-bootstrap) -- [Window, tray & menu](#window-tray--menu) -- [Auto-start (Login Items)](#auto-start-login-items) -- [Source tree](#source-tree) -- [Packaged app layout](#packaged-app-layout) -- [Build pipeline](#build-pipeline) -- [Commands](#commands) -- [Build performance — read this](#build-performance--read-this) -- [Code signing & notarization](#code-signing--notarization) -- [Continuous integration](#continuous-integration) -- [Smoke test](#smoke-test) -- [Environment variables](#environment-variables) -- [Logs & troubleshooting](#logs--troubleshooting) -- [What this workspace does *not* touch](#what-this-workspace-does-not-touch) - ---- - -## TL;DR - -```bash -# From the repo root: -npm run setup # install root + client deps, build client, install hooks -npm run build # build client/dist (the SPA the Electron window loads) -npm run desktop:install # install Electron, electron-builder, types into desktop/ -npm run desktop:dev # tsc → launch Electron pointing at out/main.js -npm run desktop:test # smoke test (spawn Electron + probe /api/health) - -# Build a DMG (macOS): -npm run desktop:dmg # both per-arch DMGs (arm64 + x64) — correct for release, SLOWER -npm run desktop:dmg:arm64 # Apple Silicon only — fast, for your own machine -npm run desktop:dmg:x64 # Intel only — fast - -# Build a Windows .exe (run on Windows x64): -npm run desktop:win # NSIS installer → release/ClaudeCodeMonitor-Setup--x64.exe -npm run desktop:win:portable # no-install portable → release/ClaudeCodeMonitor--x64-portable.exe -``` - -> ⚠️ `desktop:dmg` is slower because it builds the app **twice** — once per -> architecture — and emits **two** per-arch DMGs (`arm64` + `x64`). It does not -> merge them into a universal binary. For running on your own Mac, use the -> arch-specific command. See [Build performance](#build-performance--read-this). - -> 🪟 **Windows builds run on Windows** (DMGs build on macOS). `desktop:win` -> produces an **unsigned** installer — fine to run; SmartScreen may show a -> "More info → Run anyway" prompt on first launch. The icon (`assets/icon.ico`) -> is generated from `assets/icon.png` by `npm run build:win-icon` -> (PowerShell + .NET, no extra tooling). `better-sqlite3` is fetched as a -> prebuilt Electron binary by `npm run desktop:install`, so no Visual Studio -> C++ toolchain is required for 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 prerequisite (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, then exits -> non-zero 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 binaries and avoids the compile entirely. - ---- - -## Where the desktop app sits - -`desktop/` is a **sibling workspace** — not a npm-workspaces conversion. It has -its own `package.json`, its own `node_modules`, and its own toolchain. It -consumes the rest of the repo as plain files. - -```mermaid -flowchart TD - subgraph repo["Claude-Code-Agent-Monitor (repo root)"] - server["server/
Express API · SQLite · WebSocket"] - client["client/
React + Vite SPA"] - scripts["scripts/
hook installer/handler, import, seed"] - mcp["mcp/
local MCP server"] - vscode["vscode-extension/"] - desktop["desktop/
★ Electron shell (this 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 desktop app touches **no other workspace's runtime behavior**. The only -change outside `desktop/` is a behavior-preserving refactor of -`server/index.js` (see [the last section](#what-this-workspace-does-not-touch)). - ---- - -## 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 just Chromium loading `http://127.0.0.1:` — exactly - the same origin a normal browser would. The `preload.ts` is intentionally - empty, so the renderer has zero privileged surface. - -```mermaid -flowchart LR - subgraph main["Electron Main Process (Node 22 / Electron 35)"] - boot["main.ts
lifecycle"] - host["server-host.ts
embedded server"] - express["server/index.js
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
React dashboard"] - preload["preload.ts
(empty — no bridge)"] - end - - express -- "http + ws on 127.0.0.1:port" --> win - win -.->|loads| preload - - hooks["Claude Code hooks
(separate node processes)"] -- "POST /api/hooks/event" --> express - - style main fill:#0d1117,stroke:#30363d,color:#e6edf3 - style renderer fill:#161b22,stroke:#30363d,color:#e6edf3 -``` - ---- - -## Boot lifecycle - -```mermaid -sequenceDiagram - autonumber - participant OS as macOS - 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: requestSingleInstanceLock() - alt lock not acquired - Main->>OS: exit(0) — focus existing instance - end - Main->>Host: startEmbeddedServer() - Host->>Host: probe port 4820 — adopt if a healthy server answers - alt no server to adopt - Host->>Host: pickFreePort() · patch better-sqlite3 ABI - Host->>Srv: require() · createApp() · startServer(port) - Host->>Srv: waitForHealthy() — poll /api/health - Host->>Srv: bootstrapOwnedServer() — schedulers, cc-watcher, install hooks - end - Host-->>Main: ServerHandle { url, port, ownedByUs, stop } - Main->>Main: installApplicationMenu() · createTray() - alt launched at login - Main->>OS: stay tray-only, hide dock - 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: before-quit → stop server + closeEmbeddedDatabase() -``` - -Key behaviors: - -| Event | Behavior | -|---|---| -| Second launch | `requestSingleInstanceLock()` fails → the new process exits and the existing window is focused. | -| Window close | Intercepted — the window **hides**, the server and tray keep running. | -| `window-all-closed` | App stays alive (tray-only mode). | -| Launched at login | The dashboard window is **not** shown — only the tray icon. | -| `before-quit` | If we own the server: stop the HTTP server, then `closeEmbeddedDatabase()` for a clean WAL checkpoint, then `app.exit(0)`. | - ---- - -## Server hosting & port discovery - -`server-host.ts` is the **only file** that imports `server/index.js`. It picks -a port, boots the server, and returns a `ServerHandle`. - -```mermaid -flowchart TD - start["startEmbeddedServer()"] --> forced{"CCAM_DESKTOP_BIND_PORT set?"} - forced -->|yes| bind["bind exactly that port
(no adoption, no fallback)"] - forced -->|no| adopt{"healthy server
already on :4820?"} - adopt -->|yes| reuse["adopt it
ownedByUs = false"] - adopt -->|no| pick["pickFreePort()"] - - pick --> p1{":4820 free?"} - p1 -->|yes| use4820["use 4820"] - p1 -->|no| p2{"any of
:4821–:4829 free?"} - p2 -->|yes| usefb["use that"] - p2 -->|no| p3{"any of
:49152–:49500 free?"} - p3 -->|yes| userand["use that"] - p3 -->|no| fail["throw — no free port"] - - bind --> boot["createApp() + startServer()"] - use4820 --> boot - usefb --> boot - userand --> boot - boot --> healthy["waitForHealthy()
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 -``` - -**Adoption** — `probePort()` connects, then checks that the listener answers -`GET /api/health` with `{ status: "ok" }`. If a healthy dashboard server is -already on `:4820` (e.g. you ran `npm start` in a terminal), the desktop app -**adopts** it rather than double-binding. An adopted server is *not* owned by -the app — quitting the app leaves it running. - -**`ServerHandle`:** - -```ts -interface ServerHandle { - url: string; // e.g. "http://127.0.0.1:4820" - port: number; - ownedByUs: boolean; // false when adopted - stop: () => Promise; -} -``` - -**Hook port discovery** — because the embedded server may bind a fallback port -(4821+) when 4820 is taken, the Claude Code hook handler must not assume 4820. -On startup the server writes its live port to `~/.claude/.agent-dashboard.json` -(`server/lib/server-info.js`); `scripts/hook-handler.js` reads that file to -target the running server. Without this, hook events would be POSTed to 4820 — -nothing would receive them and the dashboard would stay empty. - ---- - -## `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's copy is built for the **system Node** (so `npm run test:server` works); -Electron ships its **own Node ABI**. - -The desktop workspace solves this without disturbing the root install: - -```mermaid -flowchart TD - subgraph desk["desktop/node_modules"] - d1["better-sqlite3
rebuilt for Electron's ABI
(by electron-builder install-app-deps)"] - end - subgraph root["node_modules (repo root)"] - r1["better-sqlite3
built for system Node
(used by npm run test:server)"] - end - - patch["ensureNativeModulesPatched()
overrides Module._resolveFilename"] - srv["server/db.js
require('better-sqlite3')"] - - srv -->|"request intercepted"| patch - patch -->|"redirected to"| d1 - patch -.->|"everything else
passes through"| root - - style d1 fill:#238636,stroke:#196c2e,color:#fff - style patch fill:#1f6feb,stroke:#1158c7,color:#fff -``` - -- The patch is **process-local** and installed exactly once, before - `server/index.js` is `require()`d. -- It 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). -- PR #37's `compat-sqlite` (`node:sqlite`) fallback remains as a safety net — - one reason the desktop app pins **Electron 35** (its bundled Node 22.16 has - `node:sqlite`; Electron 31's Node 20 did not). -- **No toolchain needed in the common case** — `npm run desktop:install` runs - `scripts/install.js`, which wraps `npm install` (whose `postinstall` runs - `electron-builder install-app-deps`). On success it behaves like a bare - `npm install`. On failure — or if the native binary is missing afterward — it - prints actionable help (`scripts/preflight.js`'s `printNativeDepHelp()`) and - exits non-zero, never leaving a half-set-up `node_modules`. The help lists the - per-OS C++ prerequisite (Windows: Visual Studio Build Tools with the "Desktop - development with C++" workload; macOS: `xcode-select --install`; Linux: - build-essential + python3), notes that a Node LTS (20/22) ships prebuilt - binaries (avoiding the compile), and gives a no-toolchain alternative: - - ```bash - cd desktop - npm install --ignore-scripts - node node_modules/electron/install.js - npx electron-builder install-app-deps - ``` - - `prebuild.js` runs the same check (`hasBetterSqliteBinary()`) before **every** - `desktop:*` build/dev script and fails fast with the same help if the binary - is missing — turning what was a runtime fatal-dialog crash into a build-time, - copy-pasteable error. - ---- - -## 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. - -```mermaid -flowchart LR - subgraph standalone["node server/index.js"] - s1["require.main === module"] --> s2["startBackgroundServices()"] - end - subgraph desktopapp["desktop app"] - d1["server-host.ts
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/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 a **DMG-only user 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. - ---- - -## Window, tray & menu - -```mermaid -flowchart TD - tray["Menu-bar (tray) icon"] - tray -->|left-click| toggle["toggle dashboard window"] - tray -->|right-click| menu["context menu (built fresh)"] - - menu --> m1["Open Dashboard"] - menu --> m2["Open in Browser…"] - menu --> m3["Restart Server"] - menu --> m4["Show Logs"] - menu --> m5["Open at Login ☑"] - menu --> m6["Quit"] - - win["BrowserWindow"] - win -->|"close"| hide["hide() — server stays up"] - win -->|"resize / move"| persist["debounced save →
userData/window-state.json"] - win -->|"external link"| ext["shell.openExternal()"] - - style tray fill:#1f6feb,stroke:#1158c7,color:#fff -``` - -- **Tray** — the always-on surface. Left-click toggles the window; right-click - pops the context menu. The menu is rebuilt on each open so the port label and - *Open at Login* checkbox are always current. (The tray deliberately does - **not** use `setContextMenu`, which on macOS would make a left-click open the - menu and collide with the toggle behavior.) -- **Window** — `BrowserWindow` with `contextIsolation: true`, - `nodeIntegration: false`, an empty preload, and `webSecurity: true`. Geometry - is persisted to `window-state.json` under `app.getPath('userData')`. External - links open in the system browser, never inside Electron. Its `icon` is set to - the colored app logo via `appIconPath()` (`icon.ico` on Windows, `icon.png` - elsewhere — the same logo as the macOS Dock, rendered from `assets/icon.svg`), - resolving dev vs packaged asset paths, so an unpackaged `desktop:dev` run shows - the real logo in the title bar / taskbar instead of the generic Electron icon. - macOS ignores `BrowserWindow#icon` (the dev Dock icon is set separately in - `main.ts`; packaged apps get theirs from the bundle `.icns`/`.exe`). -- **Application menu** — standard menu (`About`, `Open at Login`, `File`, - `Edit`, `View`, `Window`, `Help`). `⌘R` / `Ctrl+R` is owned by `View ▸ reload`. - The `File ▸ Open Dashboard` item (`⌘1`) is gated behind `isMac`: macOS keeps a - global menu bar after the window hides so it can reopen it, but on - Windows/Linux the menu is attached to the window and a menu accelerator can't - fire while it's hidden — reopening there is the tray's *Open Dashboard*, and - `focusOrCreateWindow` calls `show()` unconditionally so it reliably raises a - backgrounded/minimized window (a bare `focus()` on Windows often only flashes - the taskbar button). - ---- - -## Auto-start (Login Items) - -Auto-start uses Electron's first-party `app.setLoginItemSettings` — which wraps -the modern macOS `SMAppService` / `ServiceManagement` framework — **not** a -`LaunchAgent` plist. The toggle therefore appears in -**System Settings → General → Login Items** where users expect to manage it. - -```mermaid -stateDiagram-v2 - [*] --> Disabled - Disabled --> Enabled: tray / menu "Open at Login" - Enabled --> Disabled: toggle again - Enabled --> LaunchedAtLogin: macOS login - LaunchedAtLogin --> TrayOnly: window hidden,
dock hidden - TrayOnly --> WindowShown: user clicks tray -``` - -When macOS launches the app at login (`wasOpenedAtLogin`), it starts -**tray-only** with `openAsHidden: true` — no window jumps into the user's face. - ---- - -## Source tree - -``` -desktop/ -├── src/ -│ ├── main.ts # main process entry — lifecycle, dialogs, wiring -│ ├── server-host.ts # ★ in-process Express boot, port discovery, adoption, -│ │ # better-sqlite3 ABI patch, DB + discovery-file close, -│ │ # /api/stats snapshot poller for the tray dropdown -│ ├── window.ts # BrowserWindow + persisted geometry; native macOS -│ │ # titleBarStyle: 'default' (clear traffic-light row) -│ ├── menu.ts # native application menu -│ ├── tray.ts # menu-bar icon + single-click dropdown w/ live -│ │ # {sessions, agents, events-today} snapshot -│ ├── login-item.ts # macOS Login Items (SMAppService) -│ ├── shell-path.ts # recover the user's shell PATH (so `claude` is found) -│ ├── logger.ts # file logger → app.getPath('logs')/desktop.log -│ ├── constants.ts # APP_NAME, ports, timeouts, window size -│ └── preload.ts # intentionally empty (zero renderer privilege) -├── scripts/ -│ ├── install.js # desktop:install wrapper: npm install + actionable -│ │ # native-dep help on failure (exits non-zero) -│ ├── preflight.js # shared hasBetterSqliteBinary() + printNativeDepHelp() -│ ├── prebuild.js # ensures client/dist + root node_modules exist; fails -│ │ # fast with setup help if better-sqlite3 binary missing -│ ├── notarize.js # electron-builder afterSign hook (opt-in) -│ └── build-icons.sh # regenerate icon.icns + tray PNGs from SVG -├── assets/ # icon.icns, icon.png, tray-icon-Template*.png, SVGs -├── tests/ -│ └── smoke.test.mjs # spawn Electron + probe /api/health -├── electron-builder.yml # DMG packaging config -├── tsconfig.json # strict; src/ → out/ -└── package.json -``` - -Compiled output lands in `desktop/out/` (git-ignored); packaged artifacts in -`desktop/release/` (git-ignored). - ---- - -## Packaged app layout - -`electron-builder` produces `Claude Code Monitor.app`. The Electron main -process code is packed into `app.asar`; the rest of the repo is shipped as -**`extraResources`** (plain files under `Resources/app/`): - -```mermaid -flowchart TD - app["Claude Code Monitor.app"] - app --> contents["Contents/"] - contents --> macos["MacOS/ — Electron binary"] - contents --> res["Resources/"] - res --> asar["app.asar
(compiled out/**, package.json)"] - res --> unpacked["app.asar.unpacked/
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: `process.resourcesPath/app` -when packaged, or the repo root in development. - ---- - -## Build pipeline - -```mermaid -flowchart LR - src["src/*.ts"] -->|prebuild guard| pre["scripts/prebuild.js
verify client/dist + node_modules"] - pre --> tsc["tsc → out/*.js"] - tsc --> eb["electron-builder"] - eb --> dl["download Electron runtime"] - eb --> rebuild["@electron/rebuild
better-sqlite3 per arch"] - eb --> asar["pack out/** → app.asar"] - eb --> extra["copy server/ client/dist/ scripts/ node_modules/
→ Resources/app/"] - asar --> appbundle[".app bundle"] - extra --> appbundle - rebuild --> appbundle - appbundle --> sign["codesign (ad-hoc by default)"] - sign --> notarize["notarize (opt-in, afterSign hook)"] - notarize --> dmg["hdiutil → .dmg"] - - style tsc fill:#1f6feb,stroke:#1158c7,color:#fff - style dmg fill:#238636,stroke:#196c2e,color:#fff -``` - -`desktop:dmg` runs the *packaging → rebuild → sign → DMG* steps **twice** -(once per architecture) and emits two separate DMGs (`…-arm64.dmg` + -`…-x64.dmg`). There is no `@electron/universal` merge step — the release ships -the two per-arch DMGs rather than one fat universal binary. - ---- - -## Commands - -All commands are runnable from the **repo root** (`desktop:*`) or from inside -`desktop/`. Every script that packages first runs `npm run build`, so you never -need to invoke `electron-builder` bare (doing so skips the TypeScript compile -and fails with *"entry file out/main.js does not exist"*). - -| Repo-root command | `desktop/` command | What it does | -|---|---|---| -| `npm run desktop:install` | `node scripts/install.js` | Install Electron, electron-builder, types; rebuild `better-sqlite3` for Electron's ABI (`postinstall`). Preflights native deps — on failure (or a missing binary) prints per-OS setup help + a no-toolchain alternative and exits non-zero. | -| `npm run desktop:build` | `npm run build` | Prebuild guard + `tsc` → `out/`. | -| `npm run desktop:dev` | `npm run dev` | Build, then launch Electron against `out/main.js`. | -| `npm run desktop:test` | `npm test` | Build, then run the smoke test. | -| `npm run desktop:dmg` | `npm run dmg` | **macOS:** both per-arch DMGs (arm64 + x64). Correct for release. **Slower.** | -| `npm run desktop:dmg:arm64` | `npm run dmg:arm64` | **macOS:** Apple-Silicon-only DMG. **Fast.** | -| `npm run desktop:dmg:x64` | `npm run dmg:x64` | **macOS:** Intel-only DMG. **Fast.** | -| `npm run desktop:dmg:universal` | `npm run dmg:universal` | **macOS:** one merged universal DMG (arm64 + x86_64 via `@electron/universal`). Optional — not what the release ships. **Slowest.** | -| `npm run desktop:win` | `npm run win` | **Windows:** NSIS installer `.exe` (x64). | -| `npm run desktop:win:portable` | `npm run win:portable` | **Windows:** no-install portable `.exe` (x64). | -| — | `npm run build:icons` | **macOS:** regenerate `icon.icns` + tray PNGs from the SVGs. | -| — | `npm run build:win-icon` | **Windows:** regenerate `icon.ico` from `icon.png` (PowerShell + .NET). | -| — | `npm run clean` | Remove `out/` and `release/`. | - -> **After `npm run clean`** you must `npm run build` again before packaging — -> `clean` deletes `out/`, and `electron-builder` only *packages*, it does not -> compile. The `dmg*` scripts chain the build for you; a bare -> `electron-builder` call does not. - ---- - -## Build performance — read this - -**`desktop:dmg` is slower than a single-arch build.** This is expected — it is -the standard Electron packaging cost, paid **once per architecture**: - -```mermaid -flowchart TD - u["npm run desktop:dmg (both arches)"] --> b1["build x64 app tree → sign → …-x64.dmg"] - u --> b2["build arm64 app tree → sign → …-arm64.dmg"] - - a["npm run desktop:dmg:arm64 (single arch)"] --> sb["build one app tree"] - sb --> ssign["sign"] - ssign --> sdmg["hdiutil → …-arm64.dmg"] - - style u fill:#9e6a03,stroke:#7d5300,color:#fff - style a fill:#238636,stroke:#196c2e,color:#fff -``` - -Why `desktop:dmg` is slow: - -1. **Everything happens twice** — electron-builder builds a full x64 app tree - *and* a full arm64 app tree, rebuilding `better-sqlite3` and packaging a DMG - for each. (There is no universal merge; each arch produces its own DMG.) -2. **The app tree is large** — the server's entire production dependency tree - (`express`, `swagger-ui-express`, `ws`, …) ships as `extraResources`; that's - tens of thousands of files, walked and copied for each architecture. -3. **Per-binary code signing** runs over each architecture's bundle. - -Net effect: a ~250 MB app is built, copied, and signed once per architecture — -gigabytes of disk I/O. The Electron runtime downloads (~110 MB each) are *not* -the bottleneck; packaging two architectures back-to-back is. - -**Guidance:** - -- Building for **your own Mac** → use `desktop:dmg:arm64` (Apple Silicon) or - `desktop:dmg:x64` (Intel). One architecture — finishes in roughly a minute - instead of two. -- Building the **release artifacts for everyone** → use `desktop:dmg` (builds - both arches) and expect it to take about twice as long. CI runs `desktop:dmg` - and uploads both DMGs as the `ClaudeCodeMonitor-dmg` artifact, so you rarely - need to build them locally. -- Each DMG is **~80 MB / ~250 MB on disk** — the standard Electron tax. - ---- - -## Code signing & notarization - -The DMG is **ad-hoc signed by default** so anyone can build a working `.dmg` -without a paid Apple Developer account. - -- The `package` script sets **`CSC_IDENTITY_AUTO_DISCOVERY=false`** so a - code-signing certificate already in the contributor's macOS keychain is - **never** picked up. (Without this, electron-builder auto-discovers such a - cert and attempts `type=distribution` signing, which fails on a non–Developer - ID cert with *"Application … could not be found"*.) -- **Real Developer ID signing** activates when `CSC_LINK` (a base64-encoded - `.p12`) and `CSC_KEY_PASSWORD` are provided — `CSC_LINK` is an *explicit* - certificate and is unaffected by the auto-discovery flag. -- **Notarization** is opt-in: `desktop/scripts/notarize.js` (an - `electron-builder` `afterSign` hook) runs only when `APPLE_ID`, - `APPLE_TEAM_ID`, and `APPLE_APP_SPECIFIC_PASSWORD` are all set. Otherwise it - is a no-op. - -```mermaid -flowchart TD - build["DMG build"] --> q{"CSC_LINK set?"} - q -->|yes| real["sign with Developer ID cert"] - q -->|no| adhoc["ad-hoc sign
(keychain scan disabled)"] - real --> n{"APPLE_ID + TEAM_ID + PASSWORD set?"} - adhoc --> n - n -->|yes| notar["notarize via notarytool"] - n -->|no| skip["skip notarization"] - notar --> out[".dmg"] - skip --> out - - style adhoc fill:#9e6a03,stroke:#7d5300,color:#fff - style real fill:#238636,stroke:#196c2e,color:#fff -``` - -An ad-hoc DMG triggers a Gatekeeper warning on first launch. The one-line -workaround is in [`../DESKTOP.md`](../DESKTOP.md): -`xattr -cr "/Applications/Claude Code Monitor.app"`. - ---- - -## Continuous integration - -The `🍎 macOS Desktop (DMG)` job in `.github/workflows/ci.yml`: - -```mermaid -flowchart LR - ch["changes job
dorny/paths-filter"] -->|"desktop/** changed?"| gate{run?} - push["push to any branch"] --> gate - label["PR has 'desktop' label"] --> gate - gate -->|yes| job["desktop job (macos-latest)"] - job --> j1["npm ci (root, client, desktop)"] - j1 --> j2["tsc build"] - j2 --> j3["smoke test"] - j3 --> j4["build both per-arch DMGs
(retry on flaky hdiutil detach)"] - j4 --> j5["upload ClaudeCodeMonitor-dmg artifact"] - j5 --> rel["release job (master only)
publish vX.Y.Z if new"] - - style job fill:#1f6feb,stroke:#1158c7,color:#fff - style rel fill:#238636,stroke:#1a6e2c,color:#fff -``` - -- The job is **path-filtered** — a `changes` job (`dorny/paths-filter`) - detects `desktop/**` edits; the desktop job also runs on any `push` or when a - PR carries the `desktop` label. -- **DMG build resilience** — `electron-builder` finalizes the DMG with - `hdiutil detach`, which is intermittently flaky on GitHub macOS runners. The - step disables Spotlight indexing and retries the build up to 3 times, - force-detaching any stale volume between attempts. -- The built DMG is uploaded as the **`ClaudeCodeMonitor-dmg`** artifact - (downloadable from the workflow run). -- On `master`, a follow-on **`release`** job reads the version from - `package.json` and publishes `vX.Y.Z` as a GitHub Release with the DMG - attached — but only when no release exists for that version yet, so bumping - the version is what cuts a release. The result is a permanent, anonymous - download URL at `releases/latest`. - ---- - -## Smoke test - -`tests/smoke.test.mjs` is intentionally minimal — it proves the embedded server -boots, without needing a display (so CI needs no `xvfb`). - -```mermaid -sequenceDiagram - autonumber - participant T as smoke.test.mjs - participant E as Electron (out/main.js) - participant S as embedded server - - T->>T: pick a unique high port - T->>E: spawn with CCAM_DESKTOP_BIND_PORT= - E->>S: startEmbeddedServer() — bind exactly - loop until healthy or 60s - T->>S: GET /api/health - end - T->>T: assert status == "ok" AND matched - T->>T: assert Electron process still alive - T->>E: SIGTERM -``` - -`CCAM_DESKTOP_BIND_PORT` forces the server onto an exact port (no adoption, no -fallback) so the test can be certain it probed *this* process and not an -unrelated server on `:4820`. - ---- - -## Environment variables - -| Variable | Used by | Effect | -|---|---|---| -| `CCAM_DESKTOP_BIND_PORT` | `server-host.ts` | Bind exactly this port — disables adoption and fallback. Used by the smoke test. | -| `CCAM_DESKTOP_NO_ADOPT` | `server-host.ts` | `=1` → never adopt an existing `:4820` server; always start our own. | -| `CCAM_DESKTOP_VERBOSE` | `logger.ts` | Mirror `info`/`warn` log lines to stdout (errors always go to stderr). | -| `DASHBOARD_DATA_DIR` | `server-host.ts` → server | Set automatically to `app.getPath('userData')/data` so the SQLite database and VAPID keys live in the per-user Application Support directory, never inside the (possibly read-only) `.app` bundle. | -| `CSC_IDENTITY_AUTO_DISCOVERY` | electron-builder | Set to `false` by the `package` script — forces ad-hoc signing. | -| `CSC_LINK` / `CSC_KEY_PASSWORD` | electron-builder | Explicit Developer ID `.p12` for real signing. | -| `APPLE_ID` / `APPLE_TEAM_ID` / `APPLE_APP_SPECIFIC_PASSWORD` | `notarize.js` | Enable Apple notarization when all three are set. | - -The embedded server also honors the dashboard's own env vars (`DASHBOARD_PORT` -and `DASHBOARD_DATA_DIR` are set automatically by `server-host.ts`; everything -else in [`../SETUP.md`](../SETUP.md) applies). - -> **Writable state never lives in the `.app` bundle.** A packaged, code-signed, -> or app-translocated bundle is read-only; a database written there would break -> History Import and event persistence. `server-host.ts` points -> `DASHBOARD_DATA_DIR` at `~/Library/Application Support/Claude Code Monitor/data/`, -> which is also why your imported history survives an app reinstall or update. - ---- - -## Logs & troubleshooting - -The Electron main process has no console when launched from Finder, so -`logger.ts` writes to a per-user file: - -``` -~/Library/Logs/Claude Code Monitor/desktop.log -``` - -Reach it from the tray menu → **Show Logs**. - -| Symptom | Cause / fix | -|---|---| -| `entry file out/main.js does not exist` | You ran `electron-builder` without building first. Run `npm run build` (or use a `dmg*` script). | -| Signing fails: `Application … could not be found` after retries | A keychain cert was auto-discovered. The `package` script now sets `CSC_IDENTITY_AUTO_DISCOVERY=false`; ensure you build via `npm run dmg*`, not bare `electron-builder`. | -| DMG build seems slow | Not hung — `desktop:dmg` packages two architectures back-to-back. See [Build performance](#build-performance--read-this). Use `dmg:arm64` / `dmg:x64` for a single arch. | -| `hdiutil detach … exit code 1` in CI | Flaky GitHub runner; the CI step already retries with Spotlight disabled. Re-run the job if it still fails. | -| Dashboard window is blank | The embedded server failed `/api/health` within 30 s — check `desktop.log`. | -| Gatekeeper blocks the app | Ad-hoc DMG. `xattr -cr "/Applications/Claude Code Monitor.app"`. | -| Hooks not firing | The app installs hooks on first owned-server boot; start a **new** Claude Code session afterwards. Verify entries in `~/.claude/settings.json`. | -| "Run Claude" says `claude` isn't on your PATH | `shell-path.ts` recovers the login-shell PATH at startup. If `claude` is a shell _alias_ or _function_ (not a real binary), it cannot be spawned — install the `claude` CLI as an executable. Check `desktop.log` for the `user PATH resolved` line. | -| `desktop:dev` / `desktop:test` fail with `ERR_DLOPEN_FAILED` | A prior DMG build left `better-sqlite3` built for the other CPU arch. `prebuild.js` auto-heals this on the next build; if needed, run `npm run desktop:install`. | -| Imported history disappeared after reinstall | Fixed — the database now lives in `~/Library/Application Support/Claude Code Monitor/data/`, outside the bundle. A one-time gap exists only across the upgrade from a build that predated this fix; re-run **Import History → Rescan**. | - ---- - -## What this workspace does *not* touch - -By design, changes outside `desktop/` are kept to a minimum: - -- **`server/index.js`** — its post-listen bootstrap was extracted into an - exported `startBackgroundServices()` so the embedded server boots the same - one-time legacy-session import, update scheduler, `cc-watcher`, and - orphaned-run reconciliation that `node server/index.js` does. A - **behavior-preserving refactor** — the standalone server path is functionally - unchanged. (The legacy-session import previously lived in the - `require.main === module` block, so the embedded server never ran it and the - desktop dashboard started empty; moving it into `startBackgroundServices()` - fixes that.) The server also publishes its live port on startup. -- **`server/lib/server-info.js`** *(new)* — multi-server discovery file at - `~/.claude/.agent-dashboard.json`. Every running dashboard appends its - `{port, pid, startedAt}` entry on startup, removes it on clean shutdown, - and stale entries are pruned by a `process.kill(pid, 0)` liveness check on - read. Exposes `writeServerInfo`, `removeServerInfo`, - `resolveAllDashboardPorts` (fan-out targets), and the legacy single-port - `resolveDashboardPort`. The file also carries legacy root-level - `port`/`pid`/`startedAt` fields populated from the most recently started - live server, so older hook handlers bundled inside an already-installed - `.app` still resolve to a reachable port. -- **`scripts/hook-handler.js`** — `Promise.all` fan-out of every hook - payload to every live server returned by `resolveAllDashboardPorts()` - (`CLAUDE_DASHBOARD_PORT` overrides to a single target). This is what lets - the desktop app coexist with `npm run dev` — both dashboards receive every - event and both stay real-time. -- **`server/lib/push.js`** — `sendPushToAll()` now also fires a **native - Electron notification** when `process.versions.electron` is set, so the - desktop app surfaces notifications via the OS API instead of relying on Web - Push (which fails inside Electron — no FCM credentials in the Chromium - build). The standalone server path is unchanged: the native leg is a no-op - there, and Web Push delivers as before. -- **`scripts/dev.js`** *(new)* — `npm run dev`'s entry point. Probes both - `127.0.0.1` and `::1` for a free port in `4820–4859` (so an SSH - `LocalForward` with loopback-specific binds can't shadow Node's wildcard - listen), exports `DASHBOARD_PORT`, then spawns the existing - `concurrently` server + client pipeline. `npm run dev:raw` bypasses it - for parity with the old behaviour. - -`client/`, `mcp/`, and `vscode-extension/` are **untouched**. If you find -yourself wanting to edit those, that belongs in a separate PR. - ---- - -*User-facing docs: [`../DESKTOP.md`](../DESKTOP.md) · Project architecture: -[`../ARCHITECTURE.md`](../ARCHITECTURE.md) · Setup: [`../SETUP.md`](../SETUP.md)* diff --git a/desktop/assets/icon.icns b/desktop/assets/icon.icns deleted file mode 100644 index a14853e..0000000 Binary files a/desktop/assets/icon.icns and /dev/null differ diff --git a/desktop/assets/icon.ico b/desktop/assets/icon.ico deleted file mode 100644 index 6816de6..0000000 Binary files a/desktop/assets/icon.ico and /dev/null differ diff --git a/desktop/assets/icon.png b/desktop/assets/icon.png deleted file mode 100644 index 4a42c74..0000000 Binary files a/desktop/assets/icon.png and /dev/null differ diff --git a/desktop/assets/icon.svg b/desktop/assets/icon.svg deleted file mode 100644 index db97682..0000000 --- a/desktop/assets/icon.svg +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/desktop/assets/tray-icon-Template.png b/desktop/assets/tray-icon-Template.png deleted file mode 100644 index d4126f0..0000000 Binary files a/desktop/assets/tray-icon-Template.png and /dev/null differ diff --git a/desktop/assets/tray-icon-Template@2x.png b/desktop/assets/tray-icon-Template@2x.png deleted file mode 100644 index 8d6219a..0000000 Binary files a/desktop/assets/tray-icon-Template@2x.png and /dev/null differ diff --git a/desktop/assets/tray-icon.svg b/desktop/assets/tray-icon.svg deleted file mode 100644 index ccc6e40..0000000 --- a/desktop/assets/tray-icon.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - diff --git a/desktop/electron-builder.yml b/desktop/electron-builder.yml deleted file mode 100644 index 73bce7a..0000000 --- a/desktop/electron-builder.yml +++ /dev/null @@ -1,167 +0,0 @@ -# electron-builder configuration for Claude Code Agent Monitor (macOS + Windows). -# -# Defaults to ad-hoc signing so the project can build distributable DMGs -# without a paid Apple Developer account. When the following environment -# variables are set in CI (typically as repository secrets), the same -# config produces a Developer ID–signed and Apple-notarized DMG: -# -# APPLE_ID Apple ID email -# APPLE_TEAM_ID Developer team identifier -# APPLE_APP_SPECIFIC_PASSWORD App-specific password for notarytool -# CSC_LINK Base64-encoded .p12 cert (optional) -# CSC_KEY_PASSWORD Password for the .p12 (optional) -# -# No code changes are required to enable notarization later. - -appId: com.vn.smartgift.ccam.desktop -productName: Claude Code Monitor -copyright: Copyright (c) 2026 SmartGift. All rights reserved. - -directories: - output: release - buildResources: assets - -# We pre-build with tsc into out/, and we ship the existing server/ and -# client/dist/ from the parent repo as extraResources. Keeping the bundle -# small: no source maps, no parent node_modules tree. -files: - - "out/**/*" - - "package.json" - -extraResources: - - from: "../server" - to: "app/server" - filter: - - "**/*" - - "!__tests__/**" - - "!**/*.test.js" - - from: "../client/dist" - to: "app/client/dist" - filter: ["**/*"] - - from: "../scripts" - to: "app/scripts" - filter: - - "**/*" - - "!**/*.test.js" - - from: "../package.json" - to: "app/package.json" - - from: "../node_modules" - to: "app/node_modules" - filter: - - "**/*" - - "!**/*.md" - - "!**/test/**" - - "!**/tests/**" - - "!**/*.d.ts" - # better-sqlite3 ships from desktop/node_modules (per-arch rebuilt by - # install-app-deps). The root copy is built for the system Node and - # would trip @electron/universal's identical-file detector. - - "!**/better-sqlite3/**" - # Tray icon images need to live OUTSIDE the asar archive: `nativeImage` can't - # always read them from an asar path, and `files` here (out/**, package.json) - # would otherwise leave them out of the bundle entirely. Ship them as plain - # files at `Resources/assets/`; `trayImagePath()` reads them via - # `process.resourcesPath` in production. macOS uses the template PNGs; Windows - # uses the colored `icon.ico` (a template/black glyph would vanish on the - # dark Windows taskbar). - - from: "assets" - to: "assets" - filter: - - "tray-icon-Template*.png" - - "icon.ico" - -asar: true -asarUnpack: - # better-sqlite3 native bindings cannot live inside asar. - - "**/node_modules/better-sqlite3/**" - -mac: - category: public.app-category.developer-tools - icon: assets/icon.icns - hardenedRuntime: true - gatekeeperAssess: false - # Ad-hoc signed by default: the `package` npm script sets - # CSC_IDENTITY_AUTO_DISCOVERY=false so a code-signing cert already in the - # contributor's keychain is never picked up (it would fail distribution - # signing). Real Developer ID signing activates only when CSC_LINK (an - # explicit .p12) is provided — that path is unaffected by the flag. - # No `arch:` here on purpose. Pinning the arch list in the config makes - # electron-builder build *every* listed architecture regardless of the CLI - # flag, so `electron-builder --mac --arm64` would still emit an x64 DMG too. - # With arch left unspecified, the `--arm64` / `--x64` / `--universal` flags - # the `dmg:*` npm scripts pass are what decide which single DMG is produced. - target: - - dmg - extendInfo: - LSUIElement: false - NSHighResolutionCapable: true - NSRequiresAquaSystemAppearance: false - -dmg: - artifactName: "ClaudeCodeMonitor-${version}-${arch}.dmg" - # The mounted-volume title carries the architecture so that, when a release/ - # directory holds more than one DMG, the Finder windows are distinguishable - # and nobody drags an x64 build onto Apple Silicon (which triggers a Rosetta - # prompt). The arch label is NOT set here: the `${arch}` macro expands - # inconsistently inside `title` — it yields `-arm64` / an empty string rather - # than `arm64` / `x64`. The per-arch `dmg:*` scripts in package.json override - # `dmg.title` with an explicit, clean label instead. - title: "Claude Code Monitor" - icon: assets/icon.icns - contents: - # Left slot: the .app bundle. Do NOT set `type: file` here — electron-builder - # auto-fills the app at this slot when type/path are omitted. With an - # explicit `type: file` and no `path`, recent electron-builder resolves - # `path: ""` against the project dir and stat's `/desktop`, then - # bombs with `/desktop not a file`. Omitting both is the canonical - # form documented in the electron-builder DMG layout examples. - - x: 130 - y: 220 - - x: 410 - y: 220 - type: link - path: /Applications - window: - width: 540 - height: 380 - -win: - # Multi-size BMP icon generated by `scripts/build-win-icon.ps1` from the same - # `icon.png` the macOS pipeline renders — see that script's header. Embedded - # in the .exe and reused for the installer + taskbar. - icon: assets/icon.ico - # Emit both an NSIS installer (.exe) and a no-install portable .exe. The - # `win` / `win:portable` npm scripts pass an explicit target so each produces - # exactly one artifact; a bare `electron-builder --win` builds both. No - # `arch:` is pinned here for the same reason as macOS above — the per-arch - # CLI flag (`--x64`) is what decides the architecture. - target: - - nsis - - portable - # Unsigned by default. Unlike macOS there is no keychain identity to suppress: - # Windows signing activates only when an explicit certificate is provided via - # CSC_LINK + CSC_KEY_PASSWORD (or win.certificateFile). Unsigned builds run - # fine; SmartScreen may show a "more info" prompt on first launch. - -nsis: - # A two-step installer (not oneClick) so the user can pick the install dir. - # Per-user install (perMachine: false) writes to %LOCALAPPDATA%\Programs and - # needs no administrator elevation. - oneClick: false - perMachine: false - allowToChangeInstallationDirectory: true - createDesktopShortcut: true - createStartMenuShortcut: true - shortcutName: "Claude Code Monitor" - uninstallDisplayName: "Claude Code Monitor ${version}" - artifactName: "ClaudeCodeMonitor-Setup-${version}-${arch}.${ext}" - # Keep the per-user SQLite database + settings (under userData) on uninstall, - # mirroring macOS where dragging the .app to Trash never touches user data. - deleteAppDataOnUninstall: false - -portable: - artifactName: "ClaudeCodeMonitor-${version}-${arch}-portable.${ext}" - -# Notarization runs only when the Apple credentials are present (macOS only; -# the hook is a no-op on Windows). -afterSign: scripts/notarize.js diff --git a/desktop/package-lock.json b/desktop/package-lock.json deleted file mode 100644 index 5c2a500..0000000 --- a/desktop/package-lock.json +++ /dev/null @@ -1,5176 +0,0 @@ -{ - "name": "agent-dashboard-desktop", - "version": "1.4.6", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "agent-dashboard-desktop", - "version": "1.4.6", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "better-sqlite3": "^11.7.0" - }, - "devDependencies": { - "@types/node": "^22.10.0", - "electron": "^35.7.0", - "electron-builder": "^25.1.8", - "typescript": "^5.5.4" - } - }, - "node_modules/@develar/schema-utils": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", - "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.0", - "ajv-keywords": "^3.4.1" - }, - "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/@electron/asar": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", - "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "commander": "^5.0.0", - "glob": "^7.1.6", - "minimatch": "^3.0.4" - }, - "bin": { - "asar": "bin/asar.js" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/@electron/asar/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@electron/asar/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@electron/asar/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@electron/asar/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@electron/get": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", - "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "env-paths": "^2.2.0", - "fs-extra": "^8.1.0", - "got": "^11.8.5", - "progress": "^2.0.3", - "semver": "^6.2.0", - "sumchecker": "^3.0.1" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "global-agent": "^3.0.0" - } - }, - "node_modules/@electron/get/node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/@electron/get/node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "license": "MIT", - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/get/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@electron/get/node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/@electron/notarize": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", - "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "fs-extra": "^9.0.1", - "promise-retry": "^2.0.1" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/notarize/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@electron/osx-sign": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.1.tgz", - "integrity": "sha512-BAfviURMHpmb1Yb50YbCxnOY0wfwaLXH5KJ4+80zS0gUkzDX3ec23naTlEqKsN+PwYn+a1cCzM7BJ4Wcd3sGzw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "compare-version": "^0.1.2", - "debug": "^4.3.4", - "fs-extra": "^10.0.0", - "isbinaryfile": "^4.0.8", - "minimist": "^1.2.6", - "plist": "^3.0.5" - }, - "bin": { - "electron-osx-flat": "bin/electron-osx-flat.js", - "electron-osx-sign": "bin/electron-osx-sign.js" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", - "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/gjtorikian/" - } - }, - "node_modules/@electron/universal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.1.tgz", - "integrity": "sha512-fKpv9kg4SPmt+hY7SVBnIYULE9QJl8L3sCfcBsnqbJwwBwAeTLokJ9TRt9y7bK0JAzIW2y78TVVjvnQEms/yyA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@electron/asar": "^3.2.7", - "@malept/cross-spawn-promise": "^2.0.0", - "debug": "^4.3.1", - "dir-compare": "^4.2.0", - "fs-extra": "^11.1.1", - "minimatch": "^9.0.3", - "plist": "^3.1.0" - }, - "engines": { - "node": ">=16.4" - } - }, - "node_modules/@electron/universal/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@electron/universal/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@electron/universal/node_modules/fs-extra": { - "version": "11.3.5", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", - "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@electron/universal/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@gar/promisify": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", - "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@malept/cross-spawn-promise": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", - "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/malept" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" - } - ], - "license": "Apache-2.0", - "dependencies": { - "cross-spawn": "^7.0.1" - }, - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/@malept/flatpak-bundler": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", - "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "fs-extra": "^9.0.0", - "lodash": "^4.17.15", - "tmp-promise": "^3.0.2" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@npmcli/fs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", - "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@gar/promisify": "^1.1.3", - "semver": "^7.3.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/@npmcli/move-file": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", - "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", - "deprecated": "This functionality has been moved to @npmcli/fs", - "dev": true, - "license": "MIT", - "dependencies": { - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "node_modules/@szmarczak/http-timer": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", - "dev": true, - "license": "MIT", - "dependencies": { - "defer-to-connect": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@tootallnate/once": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.1.tgz", - "integrity": "sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/@types/cacheable-request": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-cache-semantics": "*", - "@types/keyv": "^3.1.4", - "@types/node": "*", - "@types/responselike": "^1.0.0" - } - }, - "node_modules/@types/debug": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", - "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/fs-extra": { - "version": "9.0.13", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", - "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/keyv": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "22.19.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", - "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/plist": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", - "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*", - "xmlbuilder": ">=11.0.1" - } - }, - "node_modules/@types/responselike": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", - "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/verror": { - "version": "1.10.11", - "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", - "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@xmldom/xmldom": { - "version": "0.9.10", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz", - "integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.6" - } - }, - "node_modules/7zip-bin": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", - "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", - "dev": true, - "license": "MIT" - }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true, - "license": "ISC" - }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/agentkeepalive": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", - "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "humanize-ms": "^1.2.1" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/app-builder-bin": { - "version": "5.0.0-alpha.10", - "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.10.tgz", - "integrity": "sha512-Ev4jj3D7Bo+O0GPD2NMvJl+PGiBAfS7pUGawntBNpCbxtpncfUixqFj9z9Jme7V7s3LBGqsWZZP54fxBX3JKJw==", - "dev": true, - "license": "MIT" - }, - "node_modules/app-builder-lib": { - "version": "25.1.8", - "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-25.1.8.tgz", - "integrity": "sha512-pCqe7dfsQFBABC1jeKZXQWhGcCPF3rPCXDdfqVKjIeWBcXzyC1iOWZdfFhGl+S9MyE/k//DFmC6FzuGAUudNDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@develar/schema-utils": "~2.6.5", - "@electron/notarize": "2.5.0", - "@electron/osx-sign": "1.3.1", - "@electron/rebuild": "3.6.1", - "@electron/universal": "2.0.1", - "@malept/flatpak-bundler": "^0.4.0", - "@types/fs-extra": "9.0.13", - "async-exit-hook": "^2.0.1", - "bluebird-lst": "^1.0.9", - "builder-util": "25.1.7", - "builder-util-runtime": "9.2.10", - "chromium-pickle-js": "^0.2.0", - "config-file-ts": "0.2.8-rc1", - "debug": "^4.3.4", - "dotenv": "^16.4.5", - "dotenv-expand": "^11.0.6", - "ejs": "^3.1.8", - "electron-publish": "25.1.7", - "form-data": "^4.0.0", - "fs-extra": "^10.1.0", - "hosted-git-info": "^4.1.0", - "is-ci": "^3.0.0", - "isbinaryfile": "^5.0.0", - "js-yaml": "^4.1.0", - "json5": "^2.2.3", - "lazy-val": "^1.0.5", - "minimatch": "^10.0.0", - "resedit": "^1.7.0", - "sanitize-filename": "^1.6.3", - "semver": "^7.3.8", - "tar": "^6.1.12", - "temp-file": "^3.4.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "dmg-builder": "25.1.8", - "electron-builder-squirrel-windows": "25.1.8" - } - }, - "node_modules/app-builder-lib/node_modules/@electron/rebuild": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.6.1.tgz", - "integrity": "sha512-f6596ZHpEq/YskUd8emYvOUne89ij8mQgjYFA5ru25QwbrRO+t1SImofdDv7kKOuWCmVOuU5tvfkbgGxIl3E/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@malept/cross-spawn-promise": "^2.0.0", - "chalk": "^4.0.0", - "debug": "^4.1.1", - "detect-libc": "^2.0.1", - "fs-extra": "^10.0.0", - "got": "^11.7.0", - "node-abi": "^3.45.0", - "node-api-version": "^0.2.0", - "node-gyp": "^9.0.0", - "ora": "^5.1.0", - "read-binary-file-arch": "^1.0.6", - "semver": "^7.3.5", - "tar": "^6.0.5", - "yargs": "^17.0.1" - }, - "bin": { - "electron-rebuild": "lib/cli.js" - }, - "engines": { - "node": ">=12.13.0" - } - }, - "node_modules/aproba": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", - "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", - "dev": true, - "license": "ISC" - }, - "node_modules/archiver": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", - "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "archiver-utils": "^2.1.0", - "async": "^3.2.4", - "buffer-crc32": "^0.2.1", - "readable-stream": "^3.6.0", - "readdir-glob": "^1.1.2", - "tar-stream": "^2.2.0", - "zip-stream": "^4.1.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/archiver-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", - "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "glob": "^7.1.4", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^2.0.0" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/archiver-utils/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/archiver-utils/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/archiver-utils/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/archiver-utils/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/archiver-utils/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/archiver-utils/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/archiver-utils/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/are-we-there-yet": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", - "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", - "deprecated": "This package is no longer supported.", - "dev": true, - "license": "ISC", - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true, - "license": "MIT" - }, - "node_modules/async-exit-hook": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", - "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/better-sqlite3": { - "version": "11.10.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", - "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "bindings": "^1.5.0", - "prebuild-install": "^7.1.1" - } - }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "license": "MIT", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true, - "license": "MIT" - }, - "node_modules/bluebird-lst": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.9.tgz", - "integrity": "sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "bluebird": "^3.5.5" - } - }, - "node_modules/boolean": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", - "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/builder-util": { - "version": "25.1.7", - "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-25.1.7.tgz", - "integrity": "sha512-7jPjzBwEGRbwNcep0gGNpLXG9P94VA3CPAZQCzxkFXiV2GMQKlziMbY//rXPI7WKfhsvGgFXjTcXdBEwgXw9ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/debug": "^4.1.6", - "7zip-bin": "~5.2.0", - "app-builder-bin": "5.0.0-alpha.10", - "bluebird-lst": "^1.0.9", - "builder-util-runtime": "9.2.10", - "chalk": "^4.1.2", - "cross-spawn": "^7.0.3", - "debug": "^4.3.4", - "fs-extra": "^10.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.0", - "is-ci": "^3.0.0", - "js-yaml": "^4.1.0", - "source-map-support": "^0.5.19", - "stat-mode": "^1.0.0", - "temp-file": "^3.4.0" - } - }, - "node_modules/builder-util-runtime": { - "version": "9.2.10", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.10.tgz", - "integrity": "sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4", - "sax": "^1.2.4" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/cacache": { - "version": "16.1.3", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", - "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@npmcli/fs": "^2.1.0", - "@npmcli/move-file": "^2.0.0", - "chownr": "^2.0.0", - "fs-minipass": "^2.1.0", - "glob": "^8.0.1", - "infer-owner": "^1.0.4", - "lru-cache": "^7.7.1", - "minipass": "^3.1.6", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "mkdirp": "^1.0.4", - "p-map": "^4.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^3.0.2", - "ssri": "^9.0.0", - "tar": "^6.1.11", - "unique-filename": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/cacache/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/cacheable-lookup": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.6.0" - } - }, - "node_modules/cacheable-request": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", - "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^4.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^6.0.1", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/chromium-pickle-js": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", - "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", - "dev": true, - "license": "MIT" - }, - "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", - "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "slice-ansi": "^3.0.0", - "string-width": "^4.2.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/clone-response": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-response": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "dev": true, - "license": "ISC", - "bin": { - "color-support": "bin.js" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/compare-version": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", - "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/compress-commons": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", - "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "buffer-crc32": "^0.2.13", - "crc32-stream": "^4.0.2", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/config-file-ts": { - "version": "0.2.8-rc1", - "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.8-rc1.tgz", - "integrity": "sha512-GtNECbVI82bT4RiDIzBSVuTKoSHufnU7Ce7/42bkWZJZFLjmDF2WBpVsvRkhKCfKBnTBb3qZrBwPpFBU/Myvhg==", - "dev": true, - "license": "MIT", - "dependencies": { - "glob": "^10.3.12", - "typescript": "^5.4.3" - } - }, - "node_modules/config-file-ts/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/config-file-ts/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/config-file-ts/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/config-file-ts/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/config-file-ts/node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/crc": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", - "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "buffer": "^5.1.0" - } - }, - "node_modules/crc-32": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", - "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "bin": { - "crc32": "bin/crc32.njs" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/crc32-stream": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", - "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "crc-32": "^1.2.0", - "readable-stream": "^3.4.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decompress-response/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/defaults": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/dir-compare": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", - "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimatch": "^3.0.5", - "p-limit": "^3.1.0 " - } - }, - "node_modules/dir-compare/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/dir-compare/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/dir-compare/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/dmg-builder": { - "version": "25.1.8", - "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-25.1.8.tgz", - "integrity": "sha512-NoXo6Liy2heSklTI5OIZbCgXC1RzrDQsZkeEwXhdOro3FT1VBOvbubvscdPnjVuQ4AMwwv61oaH96AbiYg9EnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "app-builder-lib": "25.1.8", - "builder-util": "25.1.7", - "builder-util-runtime": "9.2.10", - "fs-extra": "^10.1.0", - "iconv-lite": "^0.6.2", - "js-yaml": "^4.1.0" - }, - "optionalDependencies": { - "dmg-license": "^1.0.11" - } - }, - "node_modules/dmg-license": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", - "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "@types/plist": "^3.0.1", - "@types/verror": "^1.10.3", - "ajv": "^6.10.0", - "crc": "^3.8.0", - "iconv-corefoundation": "^1.1.7", - "plist": "^3.0.4", - "smart-buffer": "^4.0.2", - "verror": "^1.10.0" - }, - "bin": { - "dmg-license": "bin/dmg-license.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dotenv-expand": { - "version": "11.0.7", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", - "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "dotenv": "^16.4.5" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/electron": { - "version": "35.7.5", - "resolved": "https://registry.npmjs.org/electron/-/electron-35.7.5.tgz", - "integrity": "sha512-dnL+JvLraKZl7iusXTVTGYs10TKfzUi30uEDTqsmTm0guN9V2tbOjTzyIZbh9n3ygUjgEYyo+igAwMRXIi3IPw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@electron/get": "^2.0.0", - "@types/node": "^22.7.7", - "extract-zip": "^2.0.1" - }, - "bin": { - "electron": "cli.js" - }, - "engines": { - "node": ">= 12.20.55" - } - }, - "node_modules/electron-builder": { - "version": "25.1.8", - "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-25.1.8.tgz", - "integrity": "sha512-poRgAtUHHOnlzZnc9PK4nzG53xh74wj2Jy7jkTrqZ0MWPoHGh1M2+C//hGeYdA+4K8w4yiVCNYoLXF7ySj2Wig==", - "dev": true, - "license": "MIT", - "dependencies": { - "app-builder-lib": "25.1.8", - "builder-util": "25.1.7", - "builder-util-runtime": "9.2.10", - "chalk": "^4.1.2", - "dmg-builder": "25.1.8", - "fs-extra": "^10.1.0", - "is-ci": "^3.0.0", - "lazy-val": "^1.0.5", - "simple-update-notifier": "2.0.0", - "yargs": "^17.6.2" - }, - "bin": { - "electron-builder": "cli.js", - "install-app-deps": "install-app-deps.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/electron-builder-squirrel-windows": { - "version": "25.1.8", - "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-25.1.8.tgz", - "integrity": "sha512-2ntkJ+9+0GFP6nAISiMabKt6eqBB0kX1QqHNWFWAXgi0VULKGisM46luRFpIBiU3u/TDmhZMM8tzvo2Abn3ayg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "app-builder-lib": "25.1.8", - "archiver": "^5.3.1", - "builder-util": "25.1.7", - "fs-extra": "^10.1.0" - } - }, - "node_modules/electron-publish": { - "version": "25.1.7", - "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-25.1.7.tgz", - "integrity": "sha512-+jbTkR9m39eDBMP4gfbqglDd6UvBC7RLh5Y0MhFSsc6UkGHj9Vj9TWobxevHYMMqmoujL11ZLjfPpMX+Pt6YEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/fs-extra": "^9.0.11", - "builder-util": "25.1.7", - "builder-util-runtime": "9.2.10", - "chalk": "^4.1.2", - "fs-extra": "^10.1.0", - "lazy-val": "^1.0.5", - "mime": "^2.5.2" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "dev": true, - "license": "MIT" - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, - "node_modules/exponential-backoff": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", - "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "node_modules/extsprintf": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", - "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT", - "optional": true - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "license": "MIT" - }, - "node_modules/filelist": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", - "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/filelist/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "license": "MIT" - }, - "node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gauge": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", - "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", - "deprecated": "This package is no longer supported.", - "dev": true, - "license": "ISC", - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.3", - "console-control-strings": "^1.1.0", - "has-unicode": "^2.0.1", - "signal-exit": "^3.0.7", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "license": "MIT" - }, - "node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/global-agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", - "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "boolean": "^3.0.1", - "es6-error": "^4.1.1", - "matcher": "^3.0.0", - "roarr": "^2.15.3", - "semver": "^7.3.2", - "serialize-error": "^7.0.1" - }, - "engines": { - "node": ">=10.0" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/got": { - "version": "11.8.6", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", - "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sindresorhus/is": "^4.0.0", - "@szmarczak/http-timer": "^4.0.5", - "@types/cacheable-request": "^6.0.1", - "@types/responselike": "^1.0.0", - "cacheable-lookup": "^5.0.3", - "cacheable-request": "^7.0.2", - "decompress-response": "^6.0.0", - "http2-wrapper": "^1.0.0-beta.5.2", - "lowercase-keys": "^2.0.0", - "p-cancelable": "^2.0.0", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=10.19.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/http2-wrapper": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.0.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.0.0" - } - }, - "node_modules/iconv-corefoundation": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", - "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "cli-truncate": "^2.1.0", - "node-addon-api": "^1.6.3" - }, - "engines": { - "node": "^8.11.2 || >=10" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/infer-owner": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", - "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", - "dev": true, - "license": "ISC" - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" - }, - "node_modules/ip-address": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", - "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/is-ci": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", - "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ci-info": "^3.2.0" - }, - "bin": { - "is-ci": "bin.js" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-lambda": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", - "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/isbinaryfile": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", - "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/gjtorikian/" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jake": { - "version": "10.9.4", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", - "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "async": "^3.2.6", - "filelist": "^1.0.4", - "picocolors": "^1.1.1" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/lazy-val": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", - "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/lazystream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", - "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "readable-stream": "^2.0.5" - }, - "engines": { - "node": ">= 0.6.3" - } - }, - "node_modules/lazystream/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/lazystream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/lazystream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/lodash.difference": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", - "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/lodash.union": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", - "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/make-fetch-happen": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", - "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", - "dev": true, - "license": "ISC", - "dependencies": { - "agentkeepalive": "^4.2.1", - "cacache": "^16.1.0", - "http-cache-semantics": "^4.1.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^7.7.1", - "minipass": "^3.1.6", - "minipass-collect": "^1.0.2", - "minipass-fetch": "^2.0.3", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^7.0.0", - "ssri": "^9.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/make-fetch-happen/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/make-fetch-happen/node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/make-fetch-happen/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/make-fetch-happen/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/matcher": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", - "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "escape-string-regexp": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-collect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", - "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-fetch": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", - "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^3.1.6", - "minipass-sized": "^1.0.3", - "minizlib": "^2.1.2" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" - } - }, - "node_modules/minipass-flush": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", - "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-pipeline": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-sized": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-abi": { - "version": "3.92.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", - "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-addon-api": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", - "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/node-api-version": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", - "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - } - }, - "node_modules/node-gyp": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.1.tgz", - "integrity": "sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.0", - "exponential-backoff": "^3.1.1", - "glob": "^7.1.4", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^10.0.3", - "nopt": "^6.0.0", - "npmlog": "^6.0.0", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.2", - "which": "^2.0.2" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": "^12.13 || ^14.13 || >=16" - } - }, - "node_modules/node-gyp/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-gyp/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/node-gyp/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/node-gyp/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/nopt": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", - "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", - "dev": true, - "license": "ISC", - "dependencies": { - "abbrev": "^1.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npmlog": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", - "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", - "deprecated": "This package is no longer supported.", - "dev": true, - "license": "ISC", - "dependencies": { - "are-we-there-yet": "^3.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^4.0.3", - "set-blocking": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-cancelable": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/path-scurry/node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/pe-library": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", - "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/jet2jet" - } - }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/plist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.1.tgz", - "integrity": "sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@xmldom/xmldom": "^0.9.10", - "base64-js": "^1.5.1", - "xmlbuilder": "^15.1.1" - }, - "engines": { - "node": ">=10.4.0" - } - }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/read-binary-file-arch": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", - "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4" - }, - "bin": { - "read-binary-file-arch": "cli.js" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdir-glob": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", - "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "minimatch": "^5.1.0" - } - }, - "node_modules/readdir-glob/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/readdir-glob/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/readdir-glob/node_modules/minimatch": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resedit": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", - "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pe-library": "^0.4.1" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/jet2jet" - } - }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/responselike": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", - "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "lowercase-keys": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/roarr": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", - "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "boolean": "^3.0.1", - "detect-node": "^2.0.4", - "globalthis": "^1.0.1", - "json-stringify-safe": "^5.0.1", - "semver-compare": "^1.0.0", - "sprintf-js": "^1.1.2" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT" - }, - "node_modules/sanitize-filename": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", - "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", - "dev": true, - "license": "WTFPL OR ISC", - "dependencies": { - "truncate-utf8-bytes": "^1.0.0" - } - }, - "node_modules/sax": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", - "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=11.0.0" - } - }, - "node_modules/semver": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", - "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/serialize-error": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", - "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "type-fest": "^0.13.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "dev": true, - "license": "ISC" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/slice-ansi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", - "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", - "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", - "dev": true, - "license": "MIT", - "dependencies": { - "ip-address": "^10.1.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", - "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^6.0.2", - "debug": "^4.3.3", - "socks": "^2.6.2" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/socks-proxy-agent/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/ssri": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", - "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.1.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/stat-mode": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", - "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sumchecker": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", - "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "debug": "^4.1.0" - }, - "engines": { - "node": ">= 8.0" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tar-fs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-fs/node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tar/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=8" - } - }, - "node_modules/temp-file": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", - "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-exit-hook": "^2.0.1", - "fs-extra": "^10.0.0" - } - }, - "node_modules/tmp": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", - "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.14" - } - }, - "node_modules/tmp-promise": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", - "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tmp": "^0.2.0" - } - }, - "node_modules/truncate-utf8-bytes": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", - "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", - "dev": true, - "license": "WTFPL", - "dependencies": { - "utf8-byte-length": "^1.0.1" - } - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/unique-filename": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", - "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", - "dev": true, - "license": "ISC", - "dependencies": { - "unique-slug": "^3.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/unique-slug": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", - "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/utf8-byte-length": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", - "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", - "dev": true, - "license": "(WTFPL OR MIT)" - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/verror": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", - "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "dev": true, - "license": "MIT", - "dependencies": { - "defaults": "^1.0.3" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/xmlbuilder": { - "version": "15.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", - "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zip-stream": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", - "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "archiver-utils": "^3.0.4", - "compress-commons": "^4.1.2", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/zip-stream/node_modules/archiver-utils": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", - "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "glob": "^7.2.3", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/zip-stream/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/zip-stream/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/zip-stream/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/zip-stream/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - } - } -} diff --git a/desktop/package.json b/desktop/package.json deleted file mode 100644 index 47ded70..0000000 --- a/desktop/package.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "name": "agent-dashboard-desktop", - "version": "1.4.6", - "private": true, - "description": "Native macOS and Windows desktop shell for Claude Code Agent Monitor.", - "author": "Nguyễn Ngọc Trí Vĩ ", - "license": "UNLICENSED", - "homepage": "https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor", - "repository": { - "type": "git", - "url": "https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor.git" - }, - "main": "out/main.js", - "scripts": { - "clean": "rm -rf out release", - "prebuild": "node scripts/prebuild.js", - "build": "npm run prebuild && tsc -p tsconfig.json", - "build:icons": "bash scripts/build-icons.sh", - "build:win-icon": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/build-win-icon.ps1", - "dev": "npm run build && electron out/main.js", - "package": "rm -rf release && npm run build && CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --mac --arm64 --x64 --publish never", - "dmg": "npm run package", - "dmg:arm64": "rm -rf release && npm run build && CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --mac --arm64 --publish never --config.dmg.title='Claude Code Monitor (Apple Silicon)'", - "dmg:x64": "rm -rf release && npm run build && CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --mac --x64 --publish never --config.dmg.title='Claude Code Monitor (Intel)'", - "dmg:universal": "rm -rf release && npm run build && CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --mac --universal --publish never --config.dmg.title='Claude Code Monitor (Universal)'", - "win": "npm run build && electron-builder --win nsis --x64 --publish never", - "win:portable": "npm run build && electron-builder --win portable --x64 --publish never", - "test": "npm run build && node --test --test-reporter=spec tests/smoke.test.mjs", - "postinstall": "electron-builder install-app-deps" - }, - "dependencies": { - "better-sqlite3": "^11.7.0" - }, - "devDependencies": { - "@types/node": "^22.10.0", - "electron": "^35.7.0", - "electron-builder": "^25.1.8", - "typescript": "^5.5.4" - } -} diff --git a/desktop/scripts/build-icons.sh b/desktop/scripts/build-icons.sh deleted file mode 100755 index 4151b94..0000000 --- a/desktop/scripts/build-icons.sh +++ /dev/null @@ -1,91 +0,0 @@ -#!/bin/bash -# Generate icon.icns + tray-icon-Template.png{,@2x.png} from the SVG sources. -# -# Uses macOS-built-in tools only — no Homebrew or npm dependencies: -# * qlmanage : SVG → PNG via Quick Look (always present on macOS) -# * sips : PNG resize/format -# * iconutil : .iconset directory → .icns -# -# This script is idempotent. Run from desktop/ or from anywhere. -# Author: Nguyễn Ngọc Trí Vĩ -# @author Nguyễn Ngọc Trí Vĩ -set -euo pipefail - -HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -ASSETS="$(cd "$HERE/../assets" && pwd)" - -require() { - command -v "$1" >/dev/null 2>&1 || { - echo "error: required tool '$1' not found. This script only runs on macOS." >&2 - exit 1 - } -} -require qlmanage -require sips -require iconutil - -cd "$ASSETS" - -echo ">>> rendering icon.svg → icon.png (1024)" -TMP="$(mktemp -d)" -trap 'rm -rf "$TMP"' EXIT -qlmanage -t -s 1024 -o "$TMP" icon.svg >/dev/null 2>&1 -mv "$TMP/icon.svg.png" icon.png - -echo ">>> building icon.iconset" -rm -rf icon.iconset -mkdir -p icon.iconset -for s in 16 32 64 128 256 512 1024; do - sips -z "$s" "$s" icon.png --out "icon.iconset/icon_${s}x${s}.png" >/dev/null -done -# Apple's @2x naming convention. -cp icon.iconset/icon_32x32.png icon.iconset/icon_16x16@2x.png -cp icon.iconset/icon_64x64.png icon.iconset/icon_32x32@2x.png -cp icon.iconset/icon_256x256.png icon.iconset/icon_128x128@2x.png -cp icon.iconset/icon_512x512.png icon.iconset/icon_256x256@2x.png -cp icon.iconset/icon_1024x1024.png icon.iconset/icon_512x512@2x.png -# Drop the 64-only file; iconutil dislikes unknown sizes. -rm -f icon.iconset/icon_64x64.png - -echo ">>> compiling icon.icns" -iconutil -c icns icon.iconset -o icon.icns -rm -rf icon.iconset - -echo ">>> rendering tray-icon-Template.png{,@2x.png} via Python" -# qlmanage flattens SVG against an opaque white background — the tray PNG -# ends up with alpha=255 everywhere and macOS template tinting turns the -# whole 22x22 bounding box white in the menu bar. Generate the RGBA PNG -# pixel-by-pixel instead. Geometry mirrors tray-icon.svg (22-unit viewBox). -require python3 -python3 - <<'PY' -import struct, zlib - -def make_png(width, height, pixels): - def chunk(tag, data): - return struct.pack('>I', len(data)) + tag + data + struct.pack('>I', zlib.crc32(tag + data)) - sig = b'\x89PNG\r\n\x1a\n' - ihdr = struct.pack('>IIBBBBB', width, height, 8, 6, 0, 0, 0) # 8-bit RGBA - raw = bytearray() - for y in range(height): - raw.append(0) - raw.extend(pixels[y*width*4:(y+1)*width*4]) - return sig + chunk(b'IHDR', ihdr) + chunk(b'IDAT', zlib.compress(bytes(raw), 9)) + chunk(b'IEND', b'') - -def draw(w, h, s): - px = bytearray(w * h * 4) # alpha=0 -> transparent - def rect(x, y, rw, rh): - for j in range(y, min(y+rh, h)): - for i in range(x, min(x+rw, w)): - o = (j*w + i) * 4 - px[o:o+4] = b'\x00\x00\x00\xff' # opaque black - rect(2*s, 14*s, 4*s, 7*s) - rect(9*s, 11*s, 4*s, 10*s) - rect(16*s, 5*s, 4*s, 16*s) - return px - -with open('tray-icon-Template.png', 'wb') as f: f.write(make_png(22, 22, draw(22, 22, 1))) -with open('tray-icon-Template@2x.png', 'wb') as f: f.write(make_png(44, 44, draw(44, 44, 2))) -PY - -echo ">>> done." -ls -la icon.icns tray-icon-Template.png tray-icon-Template@2x.png diff --git a/desktop/scripts/build-win-icon.ps1 b/desktop/scripts/build-win-icon.ps1 deleted file mode 100644 index f9294b2..0000000 --- a/desktop/scripts/build-win-icon.ps1 +++ /dev/null @@ -1,121 +0,0 @@ -<# -.SYNOPSIS - Generate assets/icon.ico from assets/icon.png — the Windows counterpart to - scripts/build-icons.sh (which produces icon.icns + the macOS tray PNGs). - -.DESCRIPTION - Uses only the .NET Framework's System.Drawing (always present on Windows) — - no ImageMagick, no npm dependency. icon.png is the 1024x1024 raster already - rendered from assets/icon.svg by the macOS icon pipeline; this script - downscales it to the standard Windows icon sizes and packs them into a - classic, maximally-compatible BMP-based .ico (32bpp BGRA + AND mask). That - format is what electron-builder embeds in the .exe and what NSIS uses for - the installer icon, and it renders correctly on Windows 7 through 11. - - Idempotent. Run from anywhere: - powershell -ExecutionPolicy Bypass -File desktop/scripts/build-win-icon.ps1 - -.NOTES - Author: Nguyễn Ngọc Trí Vĩ -#> -$ErrorActionPreference = 'Stop' -Add-Type -AssemblyName System.Drawing - -$here = Split-Path -Parent $MyInvocation.MyCommand.Path -$assets = Join-Path (Split-Path -Parent $here) 'assets' -$srcPng = Join-Path $assets 'icon.png' -$outIco = Join-Path $assets 'icon.ico' - -if (-not (Test-Path $srcPng)) { - throw "icon.png not found at $srcPng. Generate it first (scripts/build-icons.sh renders it from icon.svg)." -} - -# Standard Windows icon ladder. 256 is required by electron-builder; the small -# sizes keep the taskbar / Alt-Tab / tray crisp. -$sizes = 16, 24, 32, 48, 64, 128, 256 - -$src = [System.Drawing.Image]::FromFile($srcPng) -$entries = New-Object System.Collections.ArrayList - -try { - foreach ($s in $sizes) { - $bmp = New-Object System.Drawing.Bitmap($s, $s, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb) - $g = [System.Drawing.Graphics]::FromImage($bmp) - $g.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic - $g.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality - $g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality - $g.CompositingQuality = [System.Drawing.Drawing2D.CompositingQuality]::HighQuality - $g.Clear([System.Drawing.Color]::Transparent) - $g.DrawImage($src, 0, 0, $s, $s) - $g.Dispose() - - # Pull raw pixels: Format32bppArgb is stored little-endian as B,G,R,A — - # exactly the byte order a 32bpp DIB wants. Rows are top-down here. - $rect = New-Object System.Drawing.Rectangle(0, 0, $s, $s) - $data = $bmp.LockBits($rect, [System.Drawing.Imaging.ImageLockMode]::ReadOnly, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb) - $stride = $data.Stride - $buf = New-Object byte[] ($stride * $s) - [System.Runtime.InteropServices.Marshal]::Copy($data.Scan0, $buf, 0, $buf.Length) - $bmp.UnlockBits($data) - $bmp.Dispose() - - # Build the DIB: BITMAPINFOHEADER(40) + XOR bitmap (bottom-up BGRA) + - # 1bpp AND mask (bottom-up, all zeros — alpha channel does the masking). - $ms = New-Object System.IO.MemoryStream - $bw = New-Object System.IO.BinaryWriter($ms) - $bw.Write([int]40) # biSize - $bw.Write([int]$s) # biWidth - $bw.Write([int]($s * 2)) # biHeight = XOR height + AND height - $bw.Write([int16]1) # biPlanes - $bw.Write([int16]32) # biBitCount - $bw.Write([int]0) # biCompression = BI_RGB - $bw.Write([int]0) # biSizeImage - $bw.Write([int]0) # biXPelsPerMeter - $bw.Write([int]0) # biYPelsPerMeter - $bw.Write([int]0) # biClrUsed - $bw.Write([int]0) # biClrImportant - - # XOR pixels, bottom-up. - for ($y = $s - 1; $y -ge 0; $y--) { - $bw.Write($buf, $y * $stride, 4 * $s) - } - # AND mask: 1 bit/pixel, each row padded to a 4-byte boundary, all zero. - $maskRow = [int]([math]::Floor((($s + 31) / 32)) * 4) - $zeros = New-Object byte[] ($maskRow) - for ($y = 0; $y -lt $s; $y++) { $bw.Write($zeros, 0, $maskRow) } - - $bw.Flush() - [void]$entries.Add([pscustomobject]@{ Size = $s; Data = $ms.ToArray() }) - $bw.Dispose(); $ms.Dispose() - } -} -finally { - $src.Dispose() -} - -# Assemble the .ico: ICONDIR header, then one ICONDIRENTRY per image, then data. -$out = New-Object System.IO.MemoryStream -$w = New-Object System.IO.BinaryWriter($out) -$w.Write([int16]0) # reserved -$w.Write([int16]1) # type = icon -$w.Write([int16]$entries.Count) # image count - -$offset = 6 + 16 * $entries.Count -foreach ($e in $entries) { - $dim = if ($e.Size -ge 256) { 0 } else { $e.Size } # 0 means 256 in the dir - $w.Write([byte]$dim) # width - $w.Write([byte]$dim) # height - $w.Write([byte]0) # palette color count - $w.Write([byte]0) # reserved - $w.Write([int16]1) # color planes - $w.Write([int16]32) # bits per pixel - $w.Write([int]$e.Data.Length) # size of image data - $w.Write([int]$offset) # offset of image data - $offset += $e.Data.Length -} -foreach ($e in $entries) { $w.Write($e.Data, 0, $e.Data.Length) } -$w.Flush() -[System.IO.File]::WriteAllBytes($outIco, $out.ToArray()) -$w.Dispose(); $out.Dispose() - -Write-Output ("Wrote {0} ({1:N0} bytes, sizes: {2})" -f $outIco, (Get-Item $outIco).Length, ($sizes -join ', ')) diff --git a/desktop/scripts/install.js b/desktop/scripts/install.js deleted file mode 100644 index 462b23b..0000000 --- a/desktop/scripts/install.js +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env node -/** - * @file Desktop dependency installer with actionable failure help. - * - * Thin wrapper around `npm install` (which still runs the `postinstall` - * `electron-builder install-app-deps` to rebuild native modules for Electron). - * On success it behaves exactly like a bare `npm install`. On failure — almost - * always the `better-sqlite3` native build — it prints the prerequisite - * guidance + the no-toolchain alternative commands, then exits non-zero so the - * normal command still fails loudly rather than silently leaving a half-set-up - * `node_modules`. - * @author Nguyễn Ngọc Trí Vĩ - */ - -const { spawnSync } = require("node:child_process"); -const path = require("node:path"); -const { printNativeDepHelp, hasBetterSqliteBinary } = require("./preflight"); - -const desktopRoot = path.resolve(__dirname, ".."); - -// On Windows `npm` is a `.cmd` shim that `spawnSync` can only launch via a -// shell; without this it fails with ENOENT. POSIX is unaffected. -const result = spawnSync("npm", ["install"], { - cwd: desktopRoot, - stdio: "inherit", - shell: process.platform === "win32", -}); - -// `npm install` failed outright (e.g. node-gyp could not find a compiler), or -// it "succeeded" but the native binary never landed (a prebuilt download was -// skipped). Either way the desktop app cannot boot — surface the fix and fail. -if (result.status !== 0) { - printNativeDepHelp("`npm install` failed while building the native better-sqlite3 module."); - process.exit(result.status || 1); -} - -if (!hasBetterSqliteBinary()) { - printNativeDepHelp("Dependencies installed, but the better-sqlite3 native binary is missing."); - process.exit(1); -} diff --git a/desktop/scripts/notarize.js b/desktop/scripts/notarize.js deleted file mode 100755 index 0df936e..0000000 --- a/desktop/scripts/notarize.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * @file electron-builder afterSign hook for Apple notarization. - * - * This is opt-in: it only does anything when all three Apple credentials - * are present as environment variables. In every other case (local builds, - * fork CI without secrets) the hook is a no-op. That keeps the default - * `npm run dmg` working for contributors without an Apple Developer - * account while letting the project maintainer flip a switch later. - * @author Nguyễn Ngọc Trí Vĩ - */ - -exports.default = async function notarizeIfConfigured(context) { - const { electronPlatformName, appOutDir, packager } = context; - if (electronPlatformName !== "darwin") return; - - const { APPLE_ID, APPLE_TEAM_ID, APPLE_APP_SPECIFIC_PASSWORD } = process.env; - if (!APPLE_ID || !APPLE_TEAM_ID || !APPLE_APP_SPECIFIC_PASSWORD) { - console.log("[notarize] Apple credentials not set — skipping notarization (ad-hoc only)."); - return; - } - - // Lazy-require: @electron/notarize is only needed when we actually notarize, - // so contributors without Apple credentials don't have to install it. - let notarize; - try { - ({ notarize } = require("@electron/notarize")); - } catch { - console.log( - "[notarize] Apple credentials present but @electron/notarize is not installed. Run `npm install --save-dev @electron/notarize` in desktop/." - ); - return; - } - - const appName = packager.appInfo.productFilename; - const appPath = `${appOutDir}/${appName}.app`; - console.log(`[notarize] notarizing ${appPath}`); - - await notarize({ - tool: "notarytool", - appBundleId: packager.appInfo.id, - appPath, - appleId: APPLE_ID, - appleIdPassword: APPLE_APP_SPECIFIC_PASSWORD, - teamId: APPLE_TEAM_ID, - }); - - console.log("[notarize] done"); -}; diff --git a/desktop/scripts/prebuild.js b/desktop/scripts/prebuild.js deleted file mode 100755 index 4a82ea4..0000000 --- a/desktop/scripts/prebuild.js +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env node -/** - * @file Pre-build guard. - * - * Ensures the desktop bundle has everything it needs before TypeScript - * compiles. Specifically: - * 1. The root repo's node_modules exists (Express + friends). - * 2. The client has been built (client/dist exists). In production mode the - * Express server serves the SPA from client/dist; if it's missing the - * DMG would ship a 404-only dashboard. - * 3. Asset PNGs exist (or we leave a clear warning — icons can be - * regenerated via scripts/build-icons.sh). - * 4. The desktop-local better-sqlite3 native binary matches this machine's - * CPU architecture. A prior `electron-builder --mac --x64/--arm64` build - * rebuilds it for the target arch; left mismatched it breaks `desktop:dev` - * and `desktop:test` with ERR_DLOPEN_FAILED. We rebuild it if so. - * @author Nguyễn Ngọc Trí Vĩ - */ - -const fs = require("node:fs"); -const path = require("node:path"); -const { spawnSync } = require("node:child_process"); -const { hasBetterSqliteBinary, printNativeDepHelp } = require("./preflight"); - -const desktopRoot = path.resolve(__dirname, ".."); -const repoRoot = path.resolve(__dirname, "..", ".."); -const clientDist = path.join(repoRoot, "client", "dist"); -const rootNodeModules = path.join(repoRoot, "node_modules"); -const assets = path.join(desktopRoot, "assets"); - -function run(cmd, args, opts = {}) { - // On Windows `npm`/`npx` are `.cmd` shims that `spawnSync` can only launch - // through a shell; without this it fails with ENOENT. POSIX is unaffected. - const result = spawnSync(cmd, args, { - stdio: "inherit", - shell: process.platform === "win32", - ...opts, - }); - if (result.status !== 0) { - throw new Error(`${cmd} ${args.join(" ")} failed with exit ${result.status}`); - } -} - -if (!fs.existsSync(rootNodeModules)) { - console.log("[prebuild] installing root dependencies…"); - run("npm", ["ci"], { cwd: repoRoot }); -} - -if (!fs.existsSync(clientDist) || !fs.existsSync(path.join(clientDist, "index.html"))) { - console.log("[prebuild] building client (client/dist missing)…"); - run("npm", ["ci"], { cwd: path.join(repoRoot, "client") }); - run("npm", ["run", "build"], { cwd: repoRoot }); -} - -const trayIcon = path.join(assets, "tray-icon-Template.png"); -if (!fs.existsSync(trayIcon)) { - console.warn( - "[prebuild] WARN: tray-icon-Template.png missing. Run `npm run build:icons` to regenerate from assets/icon.svg." - ); -} - -// Heal a better-sqlite3 native binary left built for the wrong CPU arch by a -// prior `electron-builder --mac --x64/--arm64` run. Without this, `desktop:dev` -// and `desktop:test` fail to load the module (ERR_DLOPEN_FAILED) until the -// contributor manually re-runs `npm run desktop:install`. -if (process.platform === "darwin") { - const bsNode = path.join( - desktopRoot, - "node_modules", - "better-sqlite3", - "build", - "Release", - "better_sqlite3.node" - ); - if (fs.existsSync(bsNode)) { - const desc = spawnSync("file", ["-b", bsNode], { encoding: "utf8" }).stdout || ""; - // A universal binary works on both arches; only act on a clear mismatch. - const universal = /universal/i.test(desc); - const wrongArch = - !universal && - ((process.arch === "arm64" && !/arm64/.test(desc)) || - (process.arch === "x64" && !/x86_64/.test(desc))); - if (wrongArch) { - console.log( - "[prebuild] better-sqlite3 is built for the wrong CPU arch (a prior DMG build left it that way) — rebuilding for this machine…" - ); - run("npx", ["electron-builder", "install-app-deps"], { cwd: desktopRoot }); - } - } -} - -// The embedded server `require`s better-sqlite3 at boot; without its native -// binary the desktop app dies with a fatal dialog after compiling cleanly. -// Catch it here (a build-time, copy-pasteable failure) rather than at runtime. -if (!hasBetterSqliteBinary()) { - printNativeDepHelp("The desktop-local better-sqlite3 native binary is missing."); - process.exit(1); -} - -console.log("[prebuild] ok"); diff --git a/desktop/scripts/preflight.js b/desktop/scripts/preflight.js deleted file mode 100644 index f95cbc6..0000000 --- a/desktop/scripts/preflight.js +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env node -/** - * @file Shared native-dependency preflight checks + actionable failure help. - * - * The desktop shell embeds the dashboard server in-process, which `require`s - * the native `better-sqlite3` module rebuilt against Electron's Node ABI. That - * build is the single most common setup failure: it needs either a C++ toolchain - * (to compile from source) or a Node version new enough to have a prebuilt - * binary. When it's missing we want a clear, copy-pasteable message instead of a - * raw node-gyp stack trace or a runtime "Cannot find module" deep inside boot. - * - * This module is shared by `install.js` (wraps the dependency install) and - * `prebuild.js` (gates every `desktop:*` build/dev script) so both surfaces - * print the same guidance. - * @author Nguyễn Ngọc Trí Vĩ - */ - -const fs = require("node:fs"); -const path = require("node:path"); - -const desktopRoot = path.resolve(__dirname, ".."); - -/** Absolute path to the compiled/prebuilt better-sqlite3 native binary. */ -function betterSqliteBinary() { - return path.join( - desktopRoot, - "node_modules", - "better-sqlite3", - "build", - "Release", - "better_sqlite3.node" - ); -} - -/** True when the Electron-ABI better-sqlite3 binary is present on disk. */ -function hasBetterSqliteBinary() { - return fs.existsSync(betterSqliteBinary()); -} - -/** - * Print prerequisite guidance and the no-toolchain alternative commands to - * stderr. Callers should `process.exit(1)` after this so the failing npm - * command exits non-zero (never leave the user thinking setup succeeded). - */ -function printNativeDepHelp(reason) { - const line = "─".repeat(74); - const out = (s) => process.stderr.write(s + "\n"); - out(""); - out(line); - out(" Claude Code Monitor — desktop native dependency setup did not complete"); - out(line); - if (reason) { - out(` ${reason}`); - out(""); - } - out(" The desktop app embeds the dashboard server, which needs the native"); - out(" 'better-sqlite3' module built for Electron's Node ABI. This typically"); - out(" fails for one of two reasons:"); - out(""); - out(" 1. No C++ build toolchain, so the module can't compile from source:"); - out(' • Windows: install "Visual Studio Build Tools" with the'); - out(' "Desktop development with C++" workload.'); - out(" • macOS: xcode-select --install"); - out(" • Linux: install build-essential + python3."); - out(""); - out(" 2. Your Node.js is newer than any published better-sqlite3 prebuilt"); - out(` binary (you are on Node ${process.version}). A Node LTS (20 or 22)`); - out(" ships prebuilt binaries and avoids the compile entirely."); - out(""); - out(" Or skip the source build and fetch Electron's prebuilt binary directly"); - out(" (no C++ toolchain needed):"); - out(""); - out(" cd desktop"); - out(" npm install --ignore-scripts"); - out(" node node_modules/electron/install.js"); - out(" npx electron-builder install-app-deps"); - out(""); - out(line); - out(""); -} - -module.exports = { betterSqliteBinary, hasBetterSqliteBinary, printNativeDepHelp }; diff --git a/desktop/src/constants.ts b/desktop/src/constants.ts deleted file mode 100644 index 5897110..0000000 --- a/desktop/src/constants.ts +++ /dev/null @@ -1,126 +0,0 @@ -/** - * @file constants.ts - * @description Shared compile-time constants for the Electron desktop shell. - * Values here must stay aligned with `electron-builder.yml` (app ID), the - * documented default dashboard port, and the embedded server health probe in - * `server-host.ts`. - * - * @author Nguyễn Ngọc Trí Vĩ - */ -/* ============================================================================= - * MODULE_GUIDE — extended in-file reference (comments only; safe to read, never executed) - * ============================================================================= - * **Purpose:** Dashboard module consumed by the React client, MCP tools, or desktop shell depending on deployment mode. - * - * ## Design constraints - * - Local-first: no telemetry leaves the machine unless the user configures webhooks. - * - Fail-safe hooks path on the server must never block Claude Code; UI mirrors that - * philosophy by degrading gracefully (empty states, stale badges, reconnect loops). - * - Destructive flows stay behind explicit confirmation modals and server-side gates. - * - Internationalization: user-visible strings belong in i18n JSON, not literals here. - * - * ## Remote data & SSH - * Remote Data Sources let operators aggregate multiple machines. SSH entries describe - * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every - * scoped GET via `?sources=`. Health checks and import history surface in Settings. - * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * - * ## Public surface - * - `APP_NAME` — exported API; see TSDoc on the symbol for behavior. - * - `APP_ID` — exported API; see TSDoc on the symbol for behavior. - * - `PREFERRED_PORT` — exported API; see TSDoc on the symbol for behavior. - * - `FALLBACK_PORT_RANGE` — exported API; see TSDoc on the symbol for behavior. - * - `HEALTH_TIMEOUT_MS` — exported API; see TSDoc on the symbol for behavior. - * - `DEFAULT_WINDOW` — exported API; see TSDoc on the symbol for behavior. - * - * ## Testing pointers - * - Prefer colocated `__tests__` with Vitest + Testing Library for UI. - * - Server contract changes require `npm run test:server` and OpenAPI sync. - * - MCP edits: `npm run mcp:typecheck` and `npm run mcp:build`. - * - * ## Related docs - * - `ARCHITECTURE.md` — hooks → API → SQLite → WebSocket → UI pipeline. - * - `docs/API.md` — REST reference. - * - `.claude/skills/file-headers/` — mandatory `@author` header policy. - * ============================================================================= */ -/* ----------------------------------------------------------------------------- - * EXPORT CATALOG — quick index of symbols defined below (documentation only). - * ----------------------------------------------------------------------------- - * **APP_NAME** - * Part of this module's public contract. Downstream imports should treat - * the signature and return type as stable unless release notes say otherwise. - * When behavior changes, update the `@file` overview and relevant tests. - * - * **APP_ID** - * Part of this module's public contract. Downstream imports should treat - * the signature and return type as stable unless release notes say otherwise. - * When behavior changes, update the `@file` overview and relevant tests. - * - * **PREFERRED_PORT** - * Part of this module's public contract. Downstream imports should treat - * the signature and return type as stable unless release notes say otherwise. - * When behavior changes, update the `@file` overview and relevant tests. - * - * **FALLBACK_PORT_RANGE** - * Part of this module's public contract. Downstream imports should treat - * the signature and return type as stable unless release notes say otherwise. - * When behavior changes, update the `@file` overview and relevant tests. - * - * **HEALTH_TIMEOUT_MS** - * Part of this module's public contract. Downstream imports should treat - * the signature and return type as stable unless release notes say otherwise. - * When behavior changes, update the `@file` overview and relevant tests. - * - * **DEFAULT_WINDOW** - * Part of this module's public contract. Downstream imports should treat - * the signature and return type as stable unless release notes say otherwise. - * When behavior changes, update the `@file` overview and relevant tests. - * - * ----------------------------------------------------------------------------- */ - -/** Human-readable product name shown in window title and About menu. */ -export const APP_NAME = "Claude Code Monitor"; - -/** - * Application identifier. Must match `appId` in electron-builder.yml: on Windows - * we hand it to `app.setAppUserModelId()` so toast notifications attribute to - * the installed Start-Menu shortcut (NSIS writes the same AUMID there) instead - * of appearing as a generic "electron.app" toast — and so taskbar windows group - * under one icon. Ignored on macOS/Linux. - */ -export const APP_ID = "com.vn.smartgift.ccam.desktop"; - -/** - * Preferred dashboard port — matches the project's documented default. Also - * the only port `server-host.ts`'s `startEmbeddedServer` will *adopt* an - * already-healthy server on; a server found on any other port is never - * treated as "ours" to reuse. - */ -export const PREFERRED_PORT = 4820; - -/** - * Last-resort port scan range when `PREFERRED_PORT` and its nine immediate - * fallbacks (4821–4829) are all taken. Set to the IANA-registered - * dynamic/private port range (49152–65535, truncated here to 49500 — far more - * headroom than `pickFreePort()` should ever need) so we never guess at a - * port some other, unrelated service might be registered on. - */ -export const FALLBACK_PORT_RANGE = { min: 49152, max: 49500 } as const; - -/** - * How long `server-host.ts`'s `waitForHealthy()` polls a freshly bound port - * for `/api/health` before giving up and surfacing an error dialog to the - * user. 30s comfortably covers a cold start on a slow disk (SQLite file - * creation, migrations) without leaving the user staring at a spinner - * indefinitely if something is actually broken. - */ -export const HEALTH_TIMEOUT_MS = 30_000; - -/** Default window size, used only when no `window-state.json` exists yet - * (first launch). Persisted to `app.getPath('userData')` after that — see - * `window.ts`'s `loadState`/`saveState`. */ -export const DEFAULT_WINDOW = { width: 1280, height: 800 } as const; diff --git a/desktop/src/logger.ts b/desktop/src/logger.ts deleted file mode 100644 index 7aa978c..0000000 --- a/desktop/src/logger.ts +++ /dev/null @@ -1,125 +0,0 @@ -/** - * @file Lightweight file logger for the desktop shell. - * - * Electron's main process has no console attached when launched from Finder, - * so all diagnostics go to a per-user log file under app.getPath('logs'). - * We deliberately avoid the `electron-log` dependency — the project keeps a - * small dependency tree and this file does the only three things we need. - * @author Nguyễn Ngọc Trí Vĩ - */ -/* ============================================================================= - * MODULE_GUIDE — extended in-file reference (comments only; safe to read, never executed) - * ============================================================================= - * **Purpose:** Dashboard module consumed by the React client, MCP tools, or desktop shell depending on deployment mode. - * - * ## Design constraints - * - Local-first: no telemetry leaves the machine unless the user configures webhooks. - * - Fail-safe hooks path on the server must never block Claude Code; UI mirrors that - * philosophy by degrading gracefully (empty states, stale badges, reconnect loops). - * - Destructive flows stay behind explicit confirmation modals and server-side gates. - * - Internationalization: user-visible strings belong in i18n JSON, not literals here. - * - * ## Remote data & SSH - * Remote Data Sources let operators aggregate multiple machines. SSH entries describe - * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every - * scoped GET via `?sources=`. Health checks and import history surface in Settings. - * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * - * ## Public surface - * - `log` — exported API; see TSDoc on the symbol for behavior. - * - * ## Testing pointers - * - Prefer colocated `__tests__` with Vitest + Testing Library for UI. - * - Server contract changes require `npm run test:server` and OpenAPI sync. - * - MCP edits: `npm run mcp:typecheck` and `npm run mcp:build`. - * - * ## Related docs - * - `ARCHITECTURE.md` — hooks → API → SQLite → WebSocket → UI pipeline. - * - `docs/API.md` — REST reference. - * - `.claude/skills/file-headers/` — mandatory `@author` header policy. - * ============================================================================= */ -/* ----------------------------------------------------------------------------- - * EXPORT CATALOG — quick index of symbols defined below (documentation only). - * ----------------------------------------------------------------------------- - * **log** - * Part of this module's public contract. Downstream imports should treat - * the signature and return type as stable unless release notes say otherwise. - * When behavior changes, update the `@file` overview and relevant tests. - * - * ----------------------------------------------------------------------------- */ - -import { app } from "electron"; -import * as fs from "node:fs"; -import * as path from "node:path"; - -let stream: fs.WriteStream | null = null; -let logPath = ""; - -/** - * Lazily open the append-mode write stream to `desktop.log`, creating the - * `app.getPath('logs')` directory if this is the first write of the process. - * Cached in the module-level `stream` so every subsequent `write()` call - * reuses the same file descriptor instead of re-opening the file. - */ -function ensureStream(): fs.WriteStream { - if (stream) return stream; - const dir = app.getPath("logs"); - fs.mkdirSync(dir, { recursive: true }); - logPath = path.join(dir, "desktop.log"); - stream = fs.createWriteStream(logPath, { flags: "a" }); - return stream; -} - -/** - * Format one log line (ISO timestamp + level + space-joined parts) and fan it - * out to the log file and, conditionally, to the process streams: - * - `error` always echoes to `stderr`, so a crash is visible even without - * `CCAM_DESKTOP_VERBOSE` (e.g. when Electron is launched from a terminal). - * - `info`/`warn` only echo to `stdout` when `CCAM_DESKTOP_VERBOSE` is set, - * keeping a normal launch quiet. - * The file write is wrapped in try/catch — a logging failure (e.g. a full - * disk) must never take down the app. - */ -function write(level: "info" | "warn" | "error", parts: unknown[]): void { - const line = `${new Date().toISOString()} [${level}] ${parts - .map((p) => (typeof p === "string" ? p : safeStringify(p))) - .join(" ")}\n`; - try { - ensureStream().write(line); - } catch { - // Logging must never crash the app. - } - if (level === "error") { - process.stderr.write(line); - } else if (process.env.CCAM_DESKTOP_VERBOSE) { - process.stdout.write(line); - } -} - -/** `JSON.stringify` a non-string log argument, falling back to `String()` for - * values it can't serialize (e.g. circular objects or `BigInt`). */ -function safeStringify(value: unknown): string { - try { - return JSON.stringify(value); - } catch { - return String(value); - } -} - -/** - * The desktop shell's only logging surface. Electron's main process has no - * attached console when launched from Finder/Dock, so every call here is - * durably persisted to `desktop.log` (see `ensureStream`) in addition to the - * conditional stdout/stderr echo described in `write`. - */ -export const log = { - info: (...parts: unknown[]) => write("info", parts), - warn: (...parts: unknown[]) => write("warn", parts), - error: (...parts: unknown[]) => write("error", parts), - /** Absolute path to the active log file (populated after first write). */ - path: () => logPath, -}; diff --git a/desktop/src/login-item.ts b/desktop/src/login-item.ts deleted file mode 100644 index 8ee0d96..0000000 --- a/desktop/src/login-item.ts +++ /dev/null @@ -1,163 +0,0 @@ -/** - * @file Open-at-login integration (macOS Login Items + Windows startup). - * - * Both platforms go through Electron's first-party `app.*LoginItemSettings` - * API — no third-party deps, no hand-rolled plist or registry edits: - * - macOS: wraps the modern `SMAppService` / `ServiceManagement` framework - * (macOS 13+), so the toggle appears in System Settings → General → - * Login Items where users expect to manage it. - * - Windows: writes an entry under the per-user - * `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` registry key (the - * standard startup location), which shows up in Task Manager → Startup. - * - * Linux has no Electron-supported equivalent, so the toggle is a no-op there. - * @author Nguyễn Ngọc Trí Vĩ - */ -/* ============================================================================= - * MODULE_GUIDE — extended in-file reference (comments only; safe to read, never executed) - * ============================================================================= - * **Purpose:** Dashboard module consumed by the React client, MCP tools, or desktop shell depending on deployment mode. - * - * ## Design constraints - * - Local-first: no telemetry leaves the machine unless the user configures webhooks. - * - Fail-safe hooks path on the server must never block Claude Code; UI mirrors that - * philosophy by degrading gracefully (empty states, stale badges, reconnect loops). - * - Destructive flows stay behind explicit confirmation modals and server-side gates. - * - Internationalization: user-visible strings belong in i18n JSON, not literals here. - * - * ## Remote data & SSH - * Remote Data Sources let operators aggregate multiple machines. SSH entries describe - * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every - * scoped GET via `?sources=`. Health checks and import history surface in Settings. - * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * - * ## Public surface - * - `isOpenAtLogin` — exported API; see TSDoc on the symbol for behavior. - * - `setOpenAtLogin` — exported API; see TSDoc on the symbol for behavior. - * - `toggleOpenAtLogin` — exported API; see TSDoc on the symbol for behavior. - * - `launchedAtLogin` — exported API; see TSDoc on the symbol for behavior. - * - * ## Testing pointers - * - Prefer colocated `__tests__` with Vitest + Testing Library for UI. - * - Server contract changes require `npm run test:server` and OpenAPI sync. - * - MCP edits: `npm run mcp:typecheck` and `npm run mcp:build`. - * - * ## Related docs - * - `ARCHITECTURE.md` — hooks → API → SQLite → WebSocket → UI pipeline. - * - `docs/API.md` — REST reference. - * - `.claude/skills/file-headers/` — mandatory `@author` header policy. - * ============================================================================= */ -/* ----------------------------------------------------------------------------- - * EXPORT CATALOG — quick index of symbols defined below (documentation only). - * ----------------------------------------------------------------------------- - * **isOpenAtLogin** - * Part of this module's public contract. Downstream imports should treat - * the signature and return type as stable unless release notes say otherwise. - * When behavior changes, update the `@file` overview and relevant tests. - * - * **setOpenAtLogin** - * Part of this module's public contract. Downstream imports should treat - * the signature and return type as stable unless release notes say otherwise. - * When behavior changes, update the `@file` overview and relevant tests. - * - * **toggleOpenAtLogin** - * Part of this module's public contract. Downstream imports should treat - * the signature and return type as stable unless release notes say otherwise. - * When behavior changes, update the `@file` overview and relevant tests. - * - * **launchedAtLogin** - * Part of this module's public contract. Downstream imports should treat - * the signature and return type as stable unless release notes say otherwise. - * When behavior changes, update the `@file` overview and relevant tests. - * - * ----------------------------------------------------------------------------- */ - -import { app } from "electron"; - -/** - * CLI flag we register the Windows startup entry with, then look for in - * `process.argv` to recognise a login-triggered launch (Windows has no - * `wasOpenedAtLogin`). Harmless if it ever reaches another code path. - */ -const WIN_LAUNCH_FLAG = "--ccam-hidden"; - -/** True on macOS and Windows — the only platforms Electron can register an - * auto-start entry for. Every exported function below is a no-op on Linux. */ -function supported(): boolean { - return process.platform === "darwin" || process.platform === "win32"; -} - -/** - * Read the current auto-start state directly from the OS (macOS Login Items - * or the Windows `Run` key), not from any value cached by this module — so it - * stays correct even if the user disables the entry from outside the app - * (e.g. macOS System Settings, or Windows Task Manager → Startup). - */ -export function isOpenAtLogin(): boolean { - if (!supported()) return false; - return app.getLoginItemSettings().openAtLogin; -} - -/** - * Enable or disable launching the app at login. Delegates entirely to - * `app.setLoginItemSettings`, which picks the platform mechanism: - * - **Windows** — writes/removes the per-user - * `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` entry, tagged with - * `WIN_LAUNCH_FLAG` so a subsequent launch can be recognised as - * login-triggered (see `launchedAtLogin`). - * - **macOS** — registers via the modern `SMAppService` API and starts the - * app hidden (see the `openAsHidden` comment below). - * No-op on Linux, where Electron has no supported mechanism. - */ -export function setOpenAtLogin(enabled: boolean): void { - if (!supported()) return; - if (process.platform === "win32") { - app.setLoginItemSettings({ - openAtLogin: enabled, - // Tag the registry Run entry so launchedAtLogin() can tell a login-time - // start apart from the user double-clicking the app. - args: [WIN_LAUNCH_FLAG], - }); - return; - } - app.setLoginItemSettings({ - openAtLogin: enabled, - // Start hidden — the user just logged in, they didn't ask for a window - // to appear. The tray icon makes the app's presence obvious. (macOS only; - // `openAsHidden` is ignored on other platforms.) - openAsHidden: true, - }); -} - -/** - * Flip the auto-start setting and return the new state. Used by both the - * tray "Open at Login" checkbox and the application menu item — each reads - * `isOpenAtLogin()` to render its own checked state, then calls this on click. - */ -export function toggleOpenAtLogin(): boolean { - const next = !isOpenAtLogin(); - setOpenAtLogin(next); - return next; -} - -/** - * Returns true if the current process was launched at login (as opposed to the - * user double-clicking the app). When true, we keep the window hidden and only - * show the tray icon. - * - * macOS reports this directly via `wasOpenedAtLogin`. Windows has no such flag, - * so we detect the marker argument we registered the startup entry with. - */ -export function launchedAtLogin(): boolean { - if (process.platform === "darwin") { - return app.getLoginItemSettings().wasOpenedAtLogin; - } - if (process.platform === "win32") { - return process.argv.includes(WIN_LAUNCH_FLAG); - } - return false; -} diff --git a/desktop/src/main.ts b/desktop/src/main.ts deleted file mode 100644 index d148cc9..0000000 --- a/desktop/src/main.ts +++ /dev/null @@ -1,377 +0,0 @@ -/** - * @file Electron main process entry point. - * - * Lifecycle: - * 1. App ready → start (or adopt) the embedded Express server. - * 2. Build the application menu + system tray. - * 3. Open the dashboard window (skipped when launched at login). - * 4. On `window-all-closed`: keep the app running (tray-only mode). - * 5. On `before-quit`: gracefully stop the server if we own it. - * - * Single-instance is enforced on every platform via `requestSingleInstanceLock` - * so double-launching (a second Dock click, or the Windows Start-Menu shortcut) - * just focuses the existing window instead of spawning a second tray + server. - * @author Nguyễn Ngọc Trí Vĩ - */ -/* ============================================================================= - * MODULE_GUIDE — extended in-file reference (comments only; safe to read, never executed) - * ============================================================================= - * **Purpose:** Dashboard module consumed by the React client, MCP tools, or desktop shell depending on deployment mode. - * - * ## Design constraints - * - Local-first: no telemetry leaves the machine unless the user configures webhooks. - * - Fail-safe hooks path on the server must never block Claude Code; UI mirrors that - * philosophy by degrading gracefully (empty states, stale badges, reconnect loops). - * - Destructive flows stay behind explicit confirmation modals and server-side gates. - * - Internationalization: user-visible strings belong in i18n JSON, not literals here. - * - * ## Remote data & SSH - * Remote Data Sources let operators aggregate multiple machines. SSH entries describe - * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every - * scoped GET via `?sources=`. Health checks and import history surface in Settings. - * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * - * ## Internal dependencies - * - `./constants` - * - `./login-item` - * - `./logger` - * - `./menu` - * - `./server-host` - * - `./shell-path` - * - `./tray` - * - `./window` - * - * ## Testing pointers - * - Prefer colocated `__tests__` with Vitest + Testing Library for UI. - * - Server contract changes require `npm run test:server` and OpenAPI sync. - * - MCP edits: `npm run mcp:typecheck` and `npm run mcp:build`. - * - * ## Related docs - * - `ARCHITECTURE.md` — hooks → API → SQLite → WebSocket → UI pipeline. - * - `docs/API.md` — REST reference. - * - `.claude/skills/file-headers/` — mandatory `@author` header policy. - * ============================================================================= */ - -import { BrowserWindow, Notification, app, dialog, shell } from "electron"; - -import { APP_ID, APP_NAME } from "./constants"; -import { isOpenAtLogin, launchedAtLogin, toggleOpenAtLogin } from "./login-item"; -import { log } from "./logger"; -import { focusOrCreateWindow, installApplicationMenu } from "./menu"; -import { - closeEmbeddedDatabase, - getServerSnapshot, - refreshServerSnapshot, - startEmbeddedServer, - startSnapshotPolling, - type ServerHandle, -} from "./server-host"; -import { ensureUserPath } from "./shell-path"; -import { createTray } from "./tray"; -import { appIconPath, createDashboardWindow } from "./window"; - -/** Single mutable record of process-wide state, held in the module-level - * `state` singleton below rather than passed around — this main-process - * entry point has exactly one window, one tray, and one server, so a class - * or a dependency-injected context would add indirection without benefit. */ -interface AppState { - /** `null` until `startEmbeddedServer()` resolves during `boot()`. */ - serverHandle: ServerHandle | null; - /** `null` when hidden/not-yet-created; a live window still counts even - * while hidden by a `close` — see the `win.on("close", ...)` handler. */ - win: BrowserWindow | null; - // Hold a reference to the tray so the GC doesn't collect it (electron quirk). - tray: Electron.Tray | null; - /** Set once teardown has begun (inside `requestQuit`'s confirm callback or - * the bypass path in `before-quit`); gates re-entrant quit handling. */ - quitting: boolean; - /** True while the quit-confirmation dialog is open; a second ⌘Q in this - * window bypasses the dialog and lets macOS quit immediately. */ - confirmingQuit: boolean; -} - -const state: AppState = { - serverHandle: null, - win: null, - tray: null, - quitting: false, - confirmingQuit: false, -}; - -/** - * Show the "Quit Claude Code Monitor?" confirmation dialog. Clicking Quit - * runs the synchronous teardown and exits. Pressing ⌘Q again while the - * dialog is open is caught by `before-quit` below and skips this prompt. - */ -function requestQuit(): void { - if (state.quitting || state.confirmingQuit) return; - state.confirmingQuit = true; - // On macOS a second ⌘Q while this dialog is open bypasses it (handled in - // `before-quit`); mention that shortcut only where it applies. - const quitAccel = process.platform === "darwin" ? "⌘Q" : "Ctrl+Q"; - const opts: Electron.MessageBoxOptions = { - type: "question", - buttons: ["Quit", "Cancel"], - defaultId: 0, - cancelId: 1, - title: APP_NAME, - message: "Quit Claude Code Monitor?", - detail: - "The embedded server will stop and your dashboard window will close. " + - `Press ${quitAccel} again to skip this prompt and quit immediately.`, - noLink: true, - }; - const parent = state.win && !state.win.isDestroyed() ? state.win : undefined; - const promise = parent ? dialog.showMessageBox(parent, opts) : dialog.showMessageBox(opts); - void promise - .then((result) => { - state.confirmingQuit = false; - if (result.response === 0) { - state.quitting = true; - if (state.serverHandle?.ownedByUs) closeEmbeddedDatabase(); - app.exit(0); - } - }) - .catch(() => { - state.confirmingQuit = false; - }); -} - -/** - * The single entry point every "open the dashboard" action goes through - * (dock/tray click, menu item, `second-instance`, macOS `activate`). Delegates - * to `focusOrCreateWindow` to reuse an existing window when possible, and - * otherwise builds one with `createDashboardWindow` and wires its `close` - * handler to hide-not-destroy (see the inline comment below). - * - * @throws If called before `startEmbeddedServer()` has resolved — there is no - * URL to point the window at yet. `boot()` guarantees this can't happen on - * the normal startup path. - */ -function ensureWindow(): BrowserWindow { - if (!state.serverHandle) { - throw new Error("Cannot create window before the server is up."); - } - return focusOrCreateWindow(state.win, () => { - const win = createDashboardWindow(state.serverHandle!.url); - state.win = win; - win.on("close", (event) => { - if (state.quitting) return; - // On macOS, "close" means "hide" — the tray stays, the server stays. - // We deliberately do NOT call `app.dock.hide()` here. With the red - // close button leaving the app running, the user needs a visible - // indication that it is still alive. The dock icon (clickable to - // re-open the window) is exactly that signal; the menu-bar tray - // icon backs it up. Login-launched startup is the only path that - // hides the dock, since that user explicitly asked for unobtrusive - // background behaviour. - event.preventDefault(); - win.hide(); - }); - return win; - }); -} - -/** - * Handler for the "Restart Server" menu/tray action. Stops the current - * server only if we own it (an adopted external server is left untouched — - * we have no business killing a process we didn't start), starts a fresh - * one via `startEmbeddedServer()` (which re-runs port adoption/selection - * from scratch), reloads the dashboard window at the new URL if one is - * open, and surfaces a native notification so the user has confirmation the - * click did something. - */ -async function restartServer(): Promise { - log.info("restarting server"); - if (state.serverHandle?.ownedByUs) { - await state.serverHandle.stop(); - } - state.serverHandle = await startEmbeddedServer(); - if (state.win && !state.win.isDestroyed()) { - state.win - .loadURL(state.serverHandle.url) - .catch((err) => log.error("reload after restart failed", err)); - } - new Notification({ title: APP_NAME, body: "Server restarted." }).show(); -} - -/** Reveal `desktop.log` in the OS file browser (Finder/Explorer), or log a - * no-op note if no line has been written yet (so `log.path()` is empty). */ -function openLogs(): void { - const p = log.path(); - if (p) { - void shell.showItemInFolder(p); - } else { - log.info("(no log file yet)"); - } -} - -/** Open the dashboard's URL in the user's default system browser. A no-op - * before the server has started, since there is no URL yet. */ -function openInBrowser(): void { - if (state.serverHandle) void shell.openExternal(state.serverHandle.url); -} - -/** Show a blocking native error dialog. Used only for conditions the user - * must see immediately and cannot recover from without restarting the app - * (e.g. the embedded server failing to boot at all). */ -function showFatalDialog(message: string, detail?: string): void { - dialog.showErrorBox(`${APP_NAME} — Error`, detail ? `${message}\n\n${detail}` : message); -} - -/** - * Runs once, after Electron fires `app.whenReady()`. Performs the full - * startup sequence documented in the file header: recover the shell `PATH`, - * boot (or adopt) the embedded server, install the application menu and - * tray, start the tray's snapshot poller, then open the dashboard window — - * unless this launch was triggered by the OS at login, in which case the app - * stays tray-only. A server-boot failure here is fatal: it shows a blocking - * error dialog and exits the process, since there is nothing useful the app - * can do without its server. - */ -async function boot(): Promise { - // macOS only shows the bundle's .icns in the Dock; an unpackaged `desktop:dev` - // run otherwise displays the generic Electron icon. Set it explicitly so the - // dev Dock matches the packaged app (Windows/Linux get theirs via the - // BrowserWindow `icon`). Wrapped in try/catch — purely cosmetic. - if (process.platform === "darwin" && !app.isPackaged) { - const icon = appIconPath(); - if (icon) { - try { - app.dock?.setIcon(icon); - } catch (err) { - log.warn("could not set dev dock icon", err); - } - } - } - - // Recover the user's shell PATH before the server boots — a Finder/Dock or - // login-launched app only inherits launchd's minimal PATH, which makes the - // "Run Claude" feature unable to find the `claude` CLI. - ensureUserPath(); - - try { - state.serverHandle = await startEmbeddedServer(); - } catch (err) { - log.error("server failed to start", err); - showFatalDialog( - "The dashboard server failed to start.", - err instanceof Error ? err.message : String(err) - ); - app.exit(1); - return; - } - - installApplicationMenu({ - showDashboard: () => ensureWindow(), - reloadDashboard: () => state.win?.webContents.reload(), - restartServer: () => { - void restartServer().catch((err) => - showFatalDialog("Could not restart the server.", String(err)) - ); - }, - openLogs, - toggleOpenAtLogin: () => { - const next = toggleOpenAtLogin(); - log.info("open-at-login set to", next); - }, - isOpenAtLogin, - }); - - state.tray = createTray({ - showDashboard: () => ensureWindow(), - restartServer: () => { - void restartServer().catch((err) => - showFatalDialog("Could not restart the server.", String(err)) - ); - }, - openLogs, - openInBrowser, - toggleOpenAtLogin: () => toggleOpenAtLogin(), - isOpenAtLogin, - serverPort: () => state.serverHandle?.port ?? null, - getSnapshot: () => getServerSnapshot(), - refreshSnapshot: () => void refreshServerSnapshot(state.serverHandle?.port ?? null), - requestQuit, - }); - - // Keep the tray's live counts fresh by polling the running server's stats - // API on an interval (and on each menu open via refreshSnapshot above). - startSnapshotPolling(() => state.serverHandle?.port ?? null); - - // Skip the dashboard window when macOS launched us at login — the user just - // logged in, they don't want a window jumping in their face. Tray only. - if (!launchedAtLogin()) { - ensureWindow(); - } else { - log.info("launched at login — staying tray-only"); - if (process.platform === "darwin") app.dock?.hide(); - } -} - -/** - * Register the app-level lifecycle handlers. Called synchronously before - * `app.whenReady()` so the single-instance lock and `before-quit` interception - * are in place from the very first tick — there is no window yet to race - * against. - * - * `requestSingleInstanceLock()` is what makes a second launch (a second Dock - * click, or double-clicking the Start-Menu shortcut again) just focus the - * existing window instead of spawning a second tray + embedded server, which - * would otherwise fight over the same port and SQLite file. - */ -function wireLifecycle(): void { - // Single-instance lock: second launches just focus the first window. - const gotLock = app.requestSingleInstanceLock(); - if (!gotLock) { - app.exit(0); - return; - } - app.on("second-instance", () => { - if (state.serverHandle) ensureWindow(); - }); - - app.on("activate", () => { - if (state.serverHandle) ensureWindow(); - }); - - app.on("window-all-closed", () => { - // Stay alive: tray + server keep running on every platform. - }); - - app.on("before-quit", (event) => { - // Second ⌘Q while the confirm dialog is up — bypass the prompt and let - // macOS quit. We still close the SQLite handle on the way out so WAL is - // checkpointed cleanly. - if (state.confirmingQuit) { - state.quitting = true; - if (state.serverHandle?.ownedByUs) closeEmbeddedDatabase(); - return; - } - if (state.quitting) return; - if (state.serverHandle?.ownedByUs) { - event.preventDefault(); - requestQuit(); - } - }); -} - -app.setName(APP_NAME); -// Windows: associate this process with the installed app's AppUserModelID so -// `new Notification()` toasts (e.g. "Server restarted") render under the app's -// name/icon and taskbar windows group correctly. Must be set before any window -// or notification is created. No-op on macOS/Linux. -if (process.platform === "win32") app.setAppUserModelId(APP_ID); -wireLifecycle(); -app - .whenReady() - .then(boot) - .catch((err) => { - log.error("fatal during boot", err); - showFatalDialog("Fatal error during startup.", String(err)); - app.exit(1); - }); diff --git a/desktop/src/menu.ts b/desktop/src/menu.ts deleted file mode 100644 index ef006be..0000000 --- a/desktop/src/menu.ts +++ /dev/null @@ -1,258 +0,0 @@ -/** - * @file Native application menu (the macOS top-bar menu). - * @author Nguyễn Ngọc Trí Vĩ - */ -/* ============================================================================= - * MODULE_GUIDE — extended in-file reference (comments only; safe to read, never executed) - * ============================================================================= - * **Purpose:** Dashboard module consumed by the React client, MCP tools, or desktop shell depending on deployment mode. - * - * ## Design constraints - * - Local-first: no telemetry leaves the machine unless the user configures webhooks. - * - Fail-safe hooks path on the server must never block Claude Code; UI mirrors that - * philosophy by degrading gracefully (empty states, stale badges, reconnect loops). - * - Destructive flows stay behind explicit confirmation modals and server-side gates. - * - Internationalization: user-visible strings belong in i18n JSON, not literals here. - * - * ## Remote data & SSH - * Remote Data Sources let operators aggregate multiple machines. SSH entries describe - * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every - * scoped GET via `?sources=`. Health checks and import history surface in Settings. - * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * - * ## Internal dependencies - * - `./constants` - * - * ## Public surface - * - `MenuActions` — exported API; see TSDoc on the symbol for behavior. - * - `installApplicationMenu` — exported API; see TSDoc on the symbol for behavior. - * - `focusOrCreateWindow` — exported API; see TSDoc on the symbol for behavior. - * - * ## Testing pointers - * - Prefer colocated `__tests__` with Vitest + Testing Library for UI. - * - Server contract changes require `npm run test:server` and OpenAPI sync. - * - MCP edits: `npm run mcp:typecheck` and `npm run mcp:build`. - * - * ## Related docs - * - `ARCHITECTURE.md` — hooks → API → SQLite → WebSocket → UI pipeline. - * - `docs/API.md` — REST reference. - * - `.claude/skills/file-headers/` — mandatory `@author` header policy. - * ============================================================================= */ -/* ----------------------------------------------------------------------------- - * EXPORT CATALOG — quick index of symbols defined below (documentation only). - * ----------------------------------------------------------------------------- - * **MenuActions** - * Part of this module's public contract. Downstream imports should treat - * the signature and return type as stable unless release notes say otherwise. - * When behavior changes, update the `@file` overview and relevant tests. - * - * **installApplicationMenu** - * Part of this module's public contract. Downstream imports should treat - * the signature and return type as stable unless release notes say otherwise. - * When behavior changes, update the `@file` overview and relevant tests. - * - * **focusOrCreateWindow** - * Part of this module's public contract. Downstream imports should treat - * the signature and return type as stable unless release notes say otherwise. - * When behavior changes, update the `@file` overview and relevant tests. - * - * ----------------------------------------------------------------------------- */ - -import { BrowserWindow, Menu, app, shell, type MenuItemConstructorOptions } from "electron"; - -import { APP_NAME } from "./constants"; - -/** Callbacks the menu wires to its items. `main.ts` supplies these, sharing - * the same handlers passed to `createTray` so both surfaces stay consistent. */ -export interface MenuActions { - /** Bring the dashboard window to front, creating it if it doesn't exist. */ - showDashboard: () => void; - /** Reload the currently loaded dashboard page (`webContents.reload()`). */ - reloadDashboard: () => void; - /** Stop and re-launch the embedded server, then reload the window. */ - restartServer: () => void; - /** Reveal `desktop.log` in the OS file browser. */ - openLogs: () => void; - /** Flip the OS auto-start-at-login registration. */ - toggleOpenAtLogin: () => void; - /** Read the current auto-start state, used to render the checkbox. */ - isOpenAtLogin: () => boolean; -} - -/** - * Build and install the native application menu (the macOS global menu bar; - * the per-window menu on Windows/Linux) and return it. - * - * Structure: an macOS-only app submenu (About, Open at Login, Services, - * Hide/Quit) prepended to standard File / Edit / View / Window / Help menus. - * Item visibility and roles branch on `process.platform === "darwin"` in a - * handful of places — see the inline comments on the `File ▸ Open Dashboard` - * item and the `Window` submenu for why those specific items are macOS-only. - */ -export function installApplicationMenu(actions: MenuActions): Menu { - const isMac = process.platform === "darwin"; - - const template: MenuItemConstructorOptions[] = [ - ...(isMac - ? ([ - { - label: APP_NAME, - submenu: [ - { role: "about" }, - { type: "separator" }, - { - label: "Open at Login", - type: "checkbox", - checked: actions.isOpenAtLogin(), - click: () => actions.toggleOpenAtLogin(), - }, - { type: "separator" }, - { role: "services" }, - { type: "separator" }, - { role: "hide" }, - { role: "hideOthers" }, - { role: "unhide" }, - { type: "separator" }, - { role: "quit" }, - ], - }, - ] satisfies MenuItemConstructorOptions[]) - : []), - { - label: "File", - submenu: [ - // "Open Dashboard" is macOS-only. On macOS the menu bar is global and - // persists after the window is closed/hidden, so this item (and Cmd+1) - // genuinely reopens it. On Windows/Linux the menu is attached to the - // window itself and a menu accelerator only fires while that window is - // already focused/foreground — so the item could only ever run when the - // window is already up, making it a confusing no-op. Reopening from a - // hidden/tray state is handled by the tray's own "Open Dashboard" there. - ...(isMac - ? ([ - { - label: "Open Dashboard", - accelerator: "CmdOrCtrl+1", - click: () => actions.showDashboard(), - }, - ] satisfies MenuItemConstructorOptions[]) - : []), - { - // No accelerator here: the View menu's `reload` role already owns - // CmdOrCtrl+R. Two menu items sharing one accelerator triggers an - // Electron duplicate-accelerator warning at startup. - label: "Reload Dashboard", - click: () => actions.reloadDashboard(), - }, - { type: "separator" }, - { - label: "Restart Server", - click: () => actions.restartServer(), - }, - { - label: "Show Logs", - click: () => actions.openLogs(), - }, - { type: "separator" }, - isMac ? { role: "close" } : { role: "quit" }, - ], - }, - { - label: "Edit", - submenu: [ - { role: "undo" }, - { role: "redo" }, - { type: "separator" }, - { role: "cut" }, - { role: "copy" }, - { role: "paste" }, - { role: "selectAll" }, - ], - }, - { - label: "View", - submenu: [ - { role: "reload" }, - { role: "forceReload" }, - { role: "toggleDevTools" }, - { type: "separator" }, - { role: "resetZoom" }, - { role: "zoomIn" }, - { role: "zoomOut" }, - { type: "separator" }, - { role: "togglefullscreen" }, - ], - }, - { - label: "Window", - submenu: [ - { role: "minimize" }, - { role: "zoom" }, - ...(isMac - ? ([ - { type: "separator" }, - { role: "front" }, - { type: "separator" }, - { role: "window" }, - ] satisfies MenuItemConstructorOptions[]) - : ([{ role: "close" }] satisfies MenuItemConstructorOptions[])), - ], - }, - { - role: "help", - submenu: [ - { - label: "Project on GitHub", - click: () => - void shell.openExternal("https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor"), - }, - { - label: "Report an Issue", - click: () => - void shell.openExternal( - "https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor/issues/new/choose" - ), - }, - { - label: `${APP_NAME} v${app.getVersion()}`, - enabled: false, - }, - ], - }, - ]; - - const menu = Menu.buildFromTemplate(template); - Menu.setApplicationMenu(menu); - return menu; -} - -/** - * Bring the dashboard window to focus, creating one via the supplied factory - * if needed. Shared by `main.ts`'s `ensureWindow` for every "open the - * dashboard" entry point (dock click, tray click, menu item, second-instance - * relaunch) so they all get the same restore/show/focus sequence. - * - * @param existing The current window reference, or `null`/destroyed if none. - * @param create Factory invoked only when `existing` is missing or destroyed. - * @returns The existing (now focused) window, or the newly created one. - */ -export function focusOrCreateWindow( - existing: BrowserWindow | null, - create: () => BrowserWindow -): BrowserWindow { - if (existing && !existing.isDestroyed()) { - if (existing.isMinimized()) existing.restore(); - // Call show() unconditionally (not just when hidden): on Windows a bare - // focus() on a visible-but-background window often only flashes the taskbar - // button instead of raising it, whereas show() reliably activates and - // brings it to the foreground. Harmless when the window is already frontmost. - existing.show(); - existing.focus(); - return existing; - } - return create(); -} diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts deleted file mode 100644 index fce92f4..0000000 --- a/desktop/src/preload.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * @file Preload script. - * - * The dashboard runs as standard web content loaded from - * `http://127.0.0.1:`. It does not need privileged APIs to function; - * keeping this preload empty is intentional and keeps the attack surface - * minimal. Renderer-side desktop helpers (e.g. native notification routing) - * can be added here later via `contextBridge.exposeInMainWorld` if the - * dashboard ever wants to call them. - * @author Nguyễn Ngọc Trí Vĩ - */ -/* ============================================================================= - * MODULE_GUIDE — extended in-file reference (comments only; safe to read, never executed) - * ============================================================================= - * **Purpose:** Dashboard module consumed by the React client, MCP tools, or desktop shell depending on deployment mode. - * - * ## Design constraints - * - Local-first: no telemetry leaves the machine unless the user configures webhooks. - * - Fail-safe hooks path on the server must never block Claude Code; UI mirrors that - * philosophy by degrading gracefully (empty states, stale badges, reconnect loops). - * - Destructive flows stay behind explicit confirmation modals and server-side gates. - * - Internationalization: user-visible strings belong in i18n JSON, not literals here. - * - * ## Remote data & SSH - * Remote Data Sources let operators aggregate multiple machines. SSH entries describe - * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every - * scoped GET via `?sources=`. Health checks and import history surface in Settings. - * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * - * ## Testing pointers - * - Prefer colocated `__tests__` with Vitest + Testing Library for UI. - * - Server contract changes require `npm run test:server` and OpenAPI sync. - * - MCP edits: `npm run mcp:typecheck` and `npm run mcp:build`. - * - * ## Related docs - * - `ARCHITECTURE.md` — hooks → API → SQLite → WebSocket → UI pipeline. - * - `docs/API.md` — REST reference. - * - `.claude/skills/file-headers/` — mandatory `@author` header policy. - * ============================================================================= */ - -export {}; diff --git a/desktop/src/server-host.ts b/desktop/src/server-host.ts deleted file mode 100644 index 0adf16a..0000000 --- a/desktop/src/server-host.ts +++ /dev/null @@ -1,556 +0,0 @@ -/** - * @file Hosts the existing Express server in-process. - * - * The dashboard's `server/index.js` already exports `{ createApp, startServer }` - * and serves the built React client (`client/dist`) as static assets in - * production. We import that module directly — no child process, no IPC, no - * port marshalling — and start it on a free port. The whole thing keeps the - * desktop shell to "Electron is a window onto the same code." - * - * If another process is already listening on the preferred port and that - * process answers `/api/health` with `{ status: "ok" }`, we adopt it instead - * of starting a second server. This covers the case where the user already - * runs `npm start` in a terminal — we should not double-bind. - * @author Nguyễn Ngọc Trí Vĩ - */ -/* ============================================================================= - * MODULE_GUIDE — extended in-file reference (comments only; safe to read, never executed) - * ============================================================================= - * **Purpose:** Dashboard module consumed by the React client, MCP tools, or desktop shell depending on deployment mode. - * - * ## Design constraints - * - Local-first: no telemetry leaves the machine unless the user configures webhooks. - * - Fail-safe hooks path on the server must never block Claude Code; UI mirrors that - * philosophy by degrading gracefully (empty states, stale badges, reconnect loops). - * - Destructive flows stay behind explicit confirmation modals and server-side gates. - * - Internationalization: user-visible strings belong in i18n JSON, not literals here. - * - * ## Remote data & SSH - * Remote Data Sources let operators aggregate multiple machines. SSH entries describe - * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every - * scoped GET via `?sources=`. Health checks and import history surface in Settings. - * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * - * ## Internal dependencies - * - `./constants` - * - `./logger` - * - * ## Public surface - * - `ServerHandle` — exported API; see TSDoc on the symbol for behavior. - * - `ServerSnapshot` — exported API; see TSDoc on the symbol for behavior. - * - `getServerSnapshot` — exported API; see TSDoc on the symbol for behavior. - * - `refreshServerSnapshot` — exported API; see TSDoc on the symbol for behavior. - * - `startSnapshotPolling` — exported API; see TSDoc on the symbol for behavior. - * - `closeEmbeddedDatabase` — exported API; see TSDoc on the symbol for behavior. - * - `startEmbeddedServer` — exported API; see TSDoc on the symbol for behavior. - * - * ## Testing pointers - * - Prefer colocated `__tests__` with Vitest + Testing Library for UI. - * - Server contract changes require `npm run test:server` and OpenAPI sync. - * - MCP edits: `npm run mcp:typecheck` and `npm run mcp:build`. - * - * ## Related docs - * - `ARCHITECTURE.md` — hooks → API → SQLite → WebSocket → UI pipeline. - * - `docs/API.md` — REST reference. - * - `.claude/skills/file-headers/` — mandatory `@author` header policy. - * ============================================================================= */ -/* ----------------------------------------------------------------------------- - * EXPORT CATALOG — quick index of symbols defined below (documentation only). - * ----------------------------------------------------------------------------- - * **ServerHandle** - * Part of this module's public contract. Downstream imports should treat - * the signature and return type as stable unless release notes say otherwise. - * When behavior changes, update the `@file` overview and relevant tests. - * - * **ServerSnapshot** - * Part of this module's public contract. Downstream imports should treat - * the signature and return type as stable unless release notes say otherwise. - * When behavior changes, update the `@file` overview and relevant tests. - * - * **getServerSnapshot** - * Part of this module's public contract. Downstream imports should treat - * the signature and return type as stable unless release notes say otherwise. - * When behavior changes, update the `@file` overview and relevant tests. - * - * **refreshServerSnapshot** - * Part of this module's public contract. Downstream imports should treat - * the signature and return type as stable unless release notes say otherwise. - * When behavior changes, update the `@file` overview and relevant tests. - * - * **startSnapshotPolling** - * Part of this module's public contract. Downstream imports should treat - * the signature and return type as stable unless release notes say otherwise. - * When behavior changes, update the `@file` overview and relevant tests. - * - * **closeEmbeddedDatabase** - * Part of this module's public contract. Downstream imports should treat - * the signature and return type as stable unless release notes say otherwise. - * When behavior changes, update the `@file` overview and relevant tests. - * - * **startEmbeddedServer** - * Part of this module's public contract. Downstream imports should treat - * the signature and return type as stable unless release notes say otherwise. - * When behavior changes, update the `@file` overview and relevant tests. - * - * ----------------------------------------------------------------------------- */ - -import * as fs from "node:fs"; -import * as http from "node:http"; -import Module from "node:module"; -import * as net from "node:net"; -import * as path from "node:path"; -import { app } from "electron"; - -import { FALLBACK_PORT_RANGE, HEALTH_TIMEOUT_MS, PREFERRED_PORT } from "./constants"; -import { log } from "./logger"; - -/** - * Redirect `require("better-sqlite3")` from anywhere in the embedded server - * to the copy in `desktop/node_modules`, which has been rebuilt against - * Electron's Node ABI by `electron-builder install-app-deps`. The repo-root - * copy is intentionally left built for the system Node so `npm run test:server` - * continues to work for contributors. This patch is process-local — it does - * not affect any other Node process. - * - * The patch is installed exactly once before we require the server module. - */ -let nativeModulesPatched = false; -function ensureNativeModulesPatched(): void { - if (nativeModulesPatched) return; - nativeModulesPatched = true; - - // Resolve the desktop-local better-sqlite3 from this file's location so we - // get the ABI-correct binary regardless of where the require originates. - let desktopBetterSqlite: string; - try { - desktopBetterSqlite = require.resolve("better-sqlite3"); - } catch (err) { - log.warn("could not pre-resolve desktop better-sqlite3; server may fall back", err); - return; - } - - // Module._resolveFilename is Node's internal lookup. We override it to - // short-circuit "better-sqlite3" requests; everything else passes through. - // Using a typed shim instead of `any` to keep strict mode honest. - type ResolveFn = ( - request: string, - parent: NodeJS.Module | null | undefined, - isMain: boolean, - options?: { paths?: string[] } - ) => string; - const mod = Module as unknown as { _resolveFilename: ResolveFn }; - const original = mod._resolveFilename.bind(Module); - mod._resolveFilename = function (request, parent, isMain, options) { - if (request === "better-sqlite3") return desktopBetterSqlite; - return original(request, parent, isMain, options); - }; - log.info("native module redirect installed", { betterSqlite3: desktopBetterSqlite }); -} - -export interface ServerHandle { - /** Origin (e.g. `http://127.0.0.1:4820`) used by the window. */ - url: string; - port: number; - /** True when the server is owned by us (and we should stop it on quit). */ - ownedByUs: boolean; - /** Gracefully close the HTTP server. A no-op when `ownedByUs` is false — - * an adopted server belongs to whatever process started it, and this app - * must never shut it down out from under that process. */ - stop: () => Promise; -} - -/** - * The subset of `server/index.js`'s exports this file calls. Kept as an - * `unknown`-typed shim (rather than importing the JS module's real types) - * because `server/` is plain JavaScript with no `.d.ts`, and the desktop - * workspace's `tsconfig.json` builds in `strict` mode — this interface is the - * hand-written contract between the two. - */ -interface ServerModule { - createApp: () => unknown; - startServer: (app: unknown, port: number) => Promise; - startBackgroundServices: () => void; -} - -/** - * One-time bootstrap of the services that the standalone `node server/index.js` - * path runs from its `require.main === module` block — the update scheduler, - * the Claude Code config watcher, orphaned-run reconciliation, and Claude Code - * hook installation. The desktop shell `require()`s the server module, so that - * block never fires; without this the embedded server is a degraded copy. - * - * Guarded so a "Restart Server" does not double-register schedulers/watchers. - */ -let backgroundServicesStarted = false; -function bootstrapOwnedServer(appRoot: string, serverModule: ServerModule): void { - if (backgroundServicesStarted) return; - backgroundServicesStarted = true; - - try { - serverModule.startBackgroundServices(); - log.info("background services started"); - } catch (err) { - log.warn("startBackgroundServices failed", err); - } - - // Auto-install Claude Code hooks so a DMG-only user gets events flowing - // without having to run `npm run install-hooks` from a checkout. - try { - const hooks = require(path.join(appRoot, "scripts", "install-hooks.js")) as { - installHooks: (silent?: boolean) => boolean; - }; - hooks.installHooks(true); - log.info("Claude Code hooks ensured"); - } catch (err) { - log.warn("hook auto-install failed", err); - } -} - -/** - * Status snapshot for the tray menu. Sourced from the live server's - * `/api/stats` endpoint rather than a direct SQLite read, so the numbers stay - * correct whether we started the server in-process or adopted an external one - * already listening on the port. (A second SQLite handle opened from the - * desktop process can point at a different/empty database file — or fail - * against the read-only `.app` bundle path — which previously pinned the menu - * at 0/0/0.) - * - * The HTTP fetch is asynchronous but the tray menu is built synchronously on - * click, so we poll on an interval and serve the last cached value. Returns - * `null` until the first successful poll completes. - */ -export interface ServerSnapshot { - /** Count of sessions the dashboard currently considers active. */ - activeSessions: number; - /** Count of agents specifically in the `working` status (not idle/waiting). */ - workingAgents: number; - /** Hook events received since the user's local midnight. */ - eventsToday: number; -} - -let lastSnapshot: ServerSnapshot | null = null; -let snapshotTimer: ReturnType | null = null; - -/** Synchronous accessor for the tray menu's build step — always returns the - * last value `refreshServerSnapshot` cached, never blocks on a network call. */ -export function getServerSnapshot(): ServerSnapshot | null { - return lastSnapshot; -} - -/** - * Fetch a fresh snapshot from the running server's stats API. Resolves to - * `null` on any error (server not up yet, non-200, malformed JSON) so the - * poller can simply keep the previous cached value. - */ -function fetchSnapshotOverHttp(port: number, timeoutMs = 2500): Promise { - // Server expects tz_offset in minutes (Date#getTimezoneOffset) to compute - // "events today" against the user's local midnight. - const tzOffset = new Date().getTimezoneOffset(); - return new Promise((resolve) => { - const req = http.get( - { - host: "127.0.0.1", - port, - path: `/api/stats?tz_offset=${tzOffset}`, - timeout: timeoutMs, - }, - (res) => { - if (res.statusCode !== 200) { - res.resume(); - resolve(null); - return; - } - let buf = ""; - res.setEncoding("utf8"); - res.on("data", (chunk) => (buf += chunk)); - res.on("end", () => { - try { - const j = JSON.parse(buf) as { - active_sessions?: number; - events_today?: number; - agents_by_status?: Record; - }; - resolve({ - activeSessions: Number(j.active_sessions) || 0, - // "working" specifically — waiting/idle agents are not working. - workingAgents: Number(j.agents_by_status?.working) || 0, - eventsToday: Number(j.events_today) || 0, - }); - } catch { - resolve(null); - } - }); - } - ); - req.on("error", () => resolve(null)); - req.on("timeout", () => { - req.destroy(); - resolve(null); - }); - }); -} - -/** Poll once now and update the cache. Safe to call on demand (e.g. menu open). */ -export async function refreshServerSnapshot(port: number | null): Promise { - if (!port) return; - const snap = await fetchSnapshotOverHttp(port); - if (snap) lastSnapshot = snap; -} - -/** - * Begin polling the server's stats endpoint so the tray menu always reflects - * recent state. Idempotent — a second call (e.g. after "Restart Server") is a - * no-op. The timer is unref'd so it never keeps the event loop alive on quit. - */ -export function startSnapshotPolling(getPort: () => number | null, intervalMs = 4000): void { - if (snapshotTimer) return; - const tick = (): void => { - void refreshServerSnapshot(getPort()); - }; - tick(); - snapshotTimer = setInterval(tick, intervalMs); - snapshotTimer.unref?.(); -} - -/** - * Close the embedded SQLite handle so WAL is checkpointed cleanly. Call once on - * application quit — never between restarts, since `server/db.js` is a cached - * singleton and a closed handle would break a subsequent server start. - */ -export function closeEmbeddedDatabase(): void { - try { - const dbModule = require(path.join(resolveAppRoot(), "server", "db.js")) as { - db?: { open?: boolean; close: () => void }; - }; - if (dbModule.db && dbModule.db.open !== false) { - dbModule.db.close(); - log.info("embedded database closed"); - } - } catch (err) { - log.warn("failed to close embedded database", err); - } - // Remove our entry from the multi-server discovery file so the hook - // handler doesn't try to POST to this PID after the process is gone. - // (Stale entries also self-prune via the liveness check on read, but the - // explicit removal closes the window between quit and the next reader.) - try { - const serverInfo = require(path.join(resolveAppRoot(), "server", "lib", "server-info.js")) as { - removeServerInfo: () => void; - }; - serverInfo.removeServerInfo(); - } catch (err) { - log.warn("failed to remove discovery file entry", err); - } -} - -/** - * Resolve the directory that contains the bundled `server/` and `client/dist/`. - * In the packaged DMG these live under `Resources/app/`. In `npm run dev` they - * live at the repo root (one directory up from `desktop/`). - */ -function resolveAppRoot(): string { - if (app.isPackaged) { - return path.join(process.resourcesPath, "app"); - } - // Dev: desktop/out/main.js → ../.. = repo root. - return path.resolve(__dirname, "..", ".."); -} - -/** - * Classify a TCP port on `127.0.0.1` in two steps: - * 1. Attempt a raw socket connection — if nothing answers, the port is - * `"free"`. - * 2. If something is listening, `GET /api/health` and check for - * `{ status: "ok" }` — a match means it is *our* kind of server - * (`"healthy"`, safe to adopt); anything else (wrong app, wrong - * response, timeout) means the port is occupied by something unrelated - * (`"busy"`, must be avoided). - * - * Used both for startup port selection (`pickFreePort`) and for deciding - * whether to adopt an already-running server (`startEmbeddedServer`). - */ -async function probePort(port: number, timeoutMs = 1500): Promise<"healthy" | "busy" | "free"> { - // 1. Is anything listening? Try to connect. - const reachable = await new Promise((resolve) => { - const socket = net.createConnection({ host: "127.0.0.1", port }); - const done = (v: boolean) => { - socket.destroy(); - resolve(v); - }; - socket.setTimeout(timeoutMs); - socket.once("connect", () => done(true)); - socket.once("error", () => done(false)); - socket.once("timeout", () => done(false)); - }); - - if (!reachable) return "free"; - - // 2. Does it answer /api/health like our server would? - const healthy = await new Promise((resolve) => { - const req = http.get( - { host: "127.0.0.1", port, path: "/api/health", timeout: timeoutMs }, - (res) => { - let buf = ""; - res.setEncoding("utf8"); - res.on("data", (chunk) => (buf += chunk)); - res.on("end", () => { - try { - const parsed = JSON.parse(buf); - resolve(parsed?.status === "ok"); - } catch { - resolve(false); - } - }); - } - ); - req.on("error", () => resolve(false)); - req.on("timeout", () => { - req.destroy(); - resolve(false); - }); - }); - - return healthy ? "healthy" : "busy"; -} - -/** - * Choose a port for a server we are about to start ourselves (i.e. we already - * know `PREFERRED_PORT` has nothing healthy to adopt). Tries, in order: - * 1. `PREFERRED_PORT` (4820) — the project's documented default. - * 2. The next nine ports (4821–4829) — small, predictable fallbacks that - * are still easy for a user to guess/bookmark. - * 3. The full `FALLBACK_PORT_RANGE` (49152–49500, the IANA dynamic/private - * range) — scanned sequentially as a last resort. - * - * @throws If every port in both ranges is occupied (practically never). - */ -async function pickFreePort(): Promise { - // Prefer the project's documented port. Otherwise scan a private range. - const initial = await probePort(PREFERRED_PORT); - if (initial === "free") return PREFERRED_PORT; - - // Try the next 9 well-known fallbacks first (4821..4829) before going random. - for (let p = PREFERRED_PORT + 1; p < PREFERRED_PORT + 10; p++) { - if ((await probePort(p)) === "free") return p; - } - for (let p = FALLBACK_PORT_RANGE.min; p <= FALLBACK_PORT_RANGE.max; p++) { - if ((await probePort(p)) === "free") return p; - } - throw new Error("Could not find a free TCP port for the dashboard server."); -} - -/** - * Block until `probePort` reports `"healthy"` for the port we just bound, or - * throw once `timeoutMs` (default `HEALTH_TIMEOUT_MS`, 30s) elapses. Called - * right after `startServer()` returns, before the caller treats the server as - * usable — Express's `listen()` callback fires as soon as the socket is - * bound, which can be before the app has finished any async initialization - * that gates `/api/health`. - */ -async function waitForHealthy(port: number, timeoutMs = HEALTH_TIMEOUT_MS): Promise { - const deadline = Date.now() + timeoutMs; - while (Date.now() < deadline) { - if ((await probePort(port, 500)) === "healthy") return; - await new Promise((r) => setTimeout(r, 250)); - } - throw new Error(`Server on port ${port} did not become healthy within ${timeoutMs}ms.`); -} - -/** - * Bring up the dashboard server. Returns a handle the caller uses to point - * the BrowserWindow and to shut down cleanly on quit. - * - * Two environment overrides exist primarily for testing: - * - `CCAM_DESKTOP_BIND_PORT`: bind exactly this port (no adoption, no fallback). - * Used by the smoke test to verify the spawned process actually started a - * server rather than finding an unrelated one. - * - `CCAM_DESKTOP_NO_ADOPT=1`: skip the "is there already a healthy server - * on 4820?" check and always start our own. - */ -export async function startEmbeddedServer(): Promise { - const forcedPort = process.env.CCAM_DESKTOP_BIND_PORT - ? parseInt(process.env.CCAM_DESKTOP_BIND_PORT, 10) - : null; - const noAdopt = process.env.CCAM_DESKTOP_NO_ADOPT === "1" || forcedPort !== null; - - if (!noAdopt) { - // Adopt an already-running healthy server (e.g. user has `npm start` open). - const adopt = await probePort(PREFERRED_PORT); - if (adopt === "healthy") { - log.info("adopting existing healthy server on port", PREFERRED_PORT); - return { - url: `http://127.0.0.1:${PREFERRED_PORT}`, - port: PREFERRED_PORT, - ownedByUs: false, - stop: async () => { - /* not ours to stop */ - }, - }; - } - } - - const port = forcedPort ?? (await pickFreePort()); - const appRoot = resolveAppRoot(); - const serverEntry = path.join(appRoot, "server", "index.js"); - - // The server reads from process.env. Set everything up before require()ing. - process.env.NODE_ENV = "production"; - process.env.DASHBOARD_PORT = String(port); - - // The server now defaults its writable state (SQLite DB, VAPID keys, - // transcript snapshots) to the shared user-global `~/.claude/agent-dashboard/` - // — outside the read-only `.app`/installed bundle AND identical to what - // `npm start`/`npm run dev` use, so the desktop app and the web app share ONE - // database. We therefore no longer override DASHBOARD_DATA_DIR to this app's - // private `userData/data`. - // - // Earlier desktop builds DID write there, so point the server's one-time - // migration at that old per-user DB: on first launch with no shared DB yet, - // it copies this app's accumulated history into the canonical location - // (non-destructively — the old file is left untouched as a backup). - if (!process.env.DASHBOARD_DATA_DIR && !process.env.DASHBOARD_LEGACY_DB_PATH) { - const legacyDbPath = path.join(app.getPath("userData"), "data", "dashboard.db"); - if (fs.existsSync(legacyDbPath)) { - process.env.DASHBOARD_LEGACY_DB_PATH = legacyDbPath; - log.info("legacy desktop database available for migration", { legacyDbPath }); - } - } - - // Make sure server's `require("better-sqlite3")` finds the ABI-correct copy. - ensureNativeModulesPatched(); - - log.info("starting embedded server", { port, serverEntry, appRoot }); - - // eslint-disable-next-line @typescript-eslint/no-var-requires - const serverModule = require(serverEntry) as ServerModule; - - const expressApp = serverModule.createApp(); - const httpServer = await serverModule.startServer(expressApp, port); - - await waitForHealthy(port); - log.info("embedded server healthy", { port }); - - // Bring up the same background services the standalone server path runs. - // Skipped automatically on a "Restart Server" via the one-time guard. - bootstrapOwnedServer(appRoot, serverModule); - - return { - url: `http://127.0.0.1:${port}`, - port, - ownedByUs: true, - stop: () => - new Promise((resolve) => { - try { - httpServer.close(() => resolve()); - // Force-close lingering websocket connections after a short grace. - setTimeout(() => resolve(), 2000).unref(); - } catch { - resolve(); - } - }), - }; -} diff --git a/desktop/src/shell-path.ts b/desktop/src/shell-path.ts deleted file mode 100644 index 2e31772..0000000 --- a/desktop/src/shell-path.ts +++ /dev/null @@ -1,152 +0,0 @@ -/** - * @file Recover the user's real shell `PATH`. - * - * A macOS app launched from Finder/Dock (or the Login Items auto-start) is - * spawned by `launchd`, which gives it a minimal `PATH` — roughly - * `/usr/bin:/bin:/usr/sbin:/sbin`. It does **not** source the user's shell - * profile (`.zshrc` / `.zprofile` / `.bash_profile`). - * - * The dashboard's "Run Claude" feature 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, etc. Under the minimal `launchd` `PATH`, - * `which claude` fails and the dashboard reports *"the `claude` CLI isn't on - * your PATH"* — even though the exact same server works when started from a - * terminal, because a terminal hands down the full shell `PATH`. - * - * We run the user's login shell once at startup, capture its `PATH`, and merge - * it into `process.env.PATH`. The embedded server runs in this same process, - * so it (and every `claude` it spawns) inherits the corrected `PATH`. - * @author Nguyễn Ngọc Trí Vĩ - */ -/* ============================================================================= - * MODULE_GUIDE — extended in-file reference (comments only; safe to read, never executed) - * ============================================================================= - * **Purpose:** Dashboard module consumed by the React client, MCP tools, or desktop shell depending on deployment mode. - * - * ## Design constraints - * - Local-first: no telemetry leaves the machine unless the user configures webhooks. - * - Fail-safe hooks path on the server must never block Claude Code; UI mirrors that - * philosophy by degrading gracefully (empty states, stale badges, reconnect loops). - * - Destructive flows stay behind explicit confirmation modals and server-side gates. - * - Internationalization: user-visible strings belong in i18n JSON, not literals here. - * - * ## Remote data & SSH - * Remote Data Sources let operators aggregate multiple machines. SSH entries describe - * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every - * scoped GET via `?sources=`. Health checks and import history surface in Settings. - * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * - * ## Internal dependencies - * - `./logger` - * - * ## Public surface - * - `ensureUserPath` — exported API; see TSDoc on the symbol for behavior. - * - * ## Testing pointers - * - Prefer colocated `__tests__` with Vitest + Testing Library for UI. - * - Server contract changes require `npm run test:server` and OpenAPI sync. - * - MCP edits: `npm run mcp:typecheck` and `npm run mcp:build`. - * - * ## Related docs - * - `ARCHITECTURE.md` — hooks → API → SQLite → WebSocket → UI pipeline. - * - `docs/API.md` — REST reference. - * - `.claude/skills/file-headers/` — mandatory `@author` header policy. - * ============================================================================= */ -/* ----------------------------------------------------------------------------- - * EXPORT CATALOG — quick index of symbols defined below (documentation only). - * ----------------------------------------------------------------------------- - * **ensureUserPath** - * Part of this module's public contract. Downstream imports should treat - * the signature and return type as stable unless release notes say otherwise. - * When behavior changes, update the `@file` overview and relevant tests. - * - * ----------------------------------------------------------------------------- */ - -import { spawnSync } from "node:child_process"; -import * as os from "node:os"; -import * as path from "node:path"; - -import { log } from "./logger"; - -// Markers fence the PATH off from any shell-startup noise (banners, MOTD, …). -// An interactive login shell may print arbitrary text before running our -// `-c` command (e.g. a `.zshrc` `neofetch` call); scanning for this sentinel -// pair — rather than trusting the last line of stdout — makes extraction -// robust to whatever the user's shell profile prints. -const DELIM = "__CCAM_SHELL_PATH__"; - -/** - * Run the user's login+interactive shell and capture its `PATH`. Returns null - * on any failure (timeout, missing shell, unparseable output). - */ -function loginShellPath(): string | null { - if (process.platform === "win32") return null; - const shell = process.env.SHELL || "/bin/zsh"; - try { - // -i interactive (sources .zshrc/.bashrc), -l login (sources .zprofile), - // -c command. printf avoids the trailing newline `echo` would add. - const res = spawnSync(shell, ["-ilc", `printf '%s' "${DELIM}$PATH${DELIM}"`], { - encoding: "utf8", - timeout: 5000, - }); - const out = `${res.stdout || ""}`; - const start = out.indexOf(DELIM); - const end = out.indexOf(DELIM, start + DELIM.length); - if (start === -1 || end === -1) return null; - const captured = out.slice(start + DELIM.length, end).trim(); - return captured || null; - } catch (err) { - log.warn("could not capture login-shell PATH", err); - return null; - } -} - -/** - * Merge the login-shell `PATH` — plus the common directories CLIs install - * into — onto `process.env.PATH`. Idempotent: deduplicates entries, so it is - * safe even if called more than once. No-op on Windows. - */ -export function ensureUserPath(): void { - if (process.platform === "win32") return; - - const ordered: string[] = []; - const seen = new Set(); - const add = (value?: string | null): void => { - if (!value) return; - for (const seg of value.split(path.delimiter)) { - if (seg && !seen.has(seg)) { - seen.add(seg); - ordered.push(seg); - } - } - }; - - // 1. The user's real shell PATH — the authoritative source. - add(loginShellPath()); - - // 2. Common install locations, as a fallback if the shell capture missed - // them (or failed entirely). - const home = os.homedir(); - add( - [ - "/opt/homebrew/bin", - "/usr/local/bin", - path.join(home, ".local", "bin"), - path.join(home, ".claude", "local"), - path.join(home, ".bun", "bin"), - path.join(home, ".deno", "bin"), - path.join(home, ".npm-global", "bin"), - ].join(path.delimiter) - ); - - // 3. Whatever launchd already gave us, last. - add(process.env.PATH); - - process.env.PATH = ordered.join(path.delimiter); - log.info("user PATH resolved for spawned CLIs", { entries: ordered.length }); -} diff --git a/desktop/src/tray.ts b/desktop/src/tray.ts deleted file mode 100644 index fb07ee9..0000000 --- a/desktop/src/tray.ts +++ /dev/null @@ -1,242 +0,0 @@ -/** - * @file Menu-bar / notification-area (system tray) icon and its context menu. - * - * The tray is the "always-on" surface of the app. A single click opens the - * menu showing live status snapshots from the embedded server plus an Open - * Dashboard action. - * - * The image is platform-specific: macOS uses a black "template" PNG so the OS - * tints it for light/dark menu bars; Windows uses the colored `icon.ico`, - * because a black template glyph would be invisible on the (usually dark) - * Windows taskbar notification area. - * @author Nguyễn Ngọc Trí Vĩ - */ -/* ============================================================================= - * MODULE_GUIDE — extended in-file reference (comments only; safe to read, never executed) - * ============================================================================= - * **Purpose:** Dashboard module consumed by the React client, MCP tools, or desktop shell depending on deployment mode. - * - * ## Design constraints - * - Local-first: no telemetry leaves the machine unless the user configures webhooks. - * - Fail-safe hooks path on the server must never block Claude Code; UI mirrors that - * philosophy by degrading gracefully (empty states, stale badges, reconnect loops). - * - Destructive flows stay behind explicit confirmation modals and server-side gates. - * - Internationalization: user-visible strings belong in i18n JSON, not literals here. - * - * ## Remote data & SSH - * Remote Data Sources let operators aggregate multiple machines. SSH entries describe - * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every - * scoped GET via `?sources=`. Health checks and import history surface in Settings. - * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * - * ## Internal dependencies - * - `./constants` - * - `./logger` - * - * ## Public surface - * - `TrayActions` — exported API; see TSDoc on the symbol for behavior. - * - `ServerSnapshot` — exported API; see TSDoc on the symbol for behavior. - * - `createTray` — exported API; see TSDoc on the symbol for behavior. - * - * ## Testing pointers - * - Prefer colocated `__tests__` with Vitest + Testing Library for UI. - * - Server contract changes require `npm run test:server` and OpenAPI sync. - * - MCP edits: `npm run mcp:typecheck` and `npm run mcp:build`. - * - * ## Related docs - * - `ARCHITECTURE.md` — hooks → API → SQLite → WebSocket → UI pipeline. - * - `docs/API.md` — REST reference. - * - `.claude/skills/file-headers/` — mandatory `@author` header policy. - * ============================================================================= */ -/* ----------------------------------------------------------------------------- - * EXPORT CATALOG — quick index of symbols defined below (documentation only). - * ----------------------------------------------------------------------------- - * **TrayActions** - * Part of this module's public contract. Downstream imports should treat - * the signature and return type as stable unless release notes say otherwise. - * When behavior changes, update the `@file` overview and relevant tests. - * - * **ServerSnapshot** - * Part of this module's public contract. Downstream imports should treat - * the signature and return type as stable unless release notes say otherwise. - * When behavior changes, update the `@file` overview and relevant tests. - * - * **createTray** - * Part of this module's public contract. Downstream imports should treat - * the signature and return type as stable unless release notes say otherwise. - * When behavior changes, update the `@file` overview and relevant tests. - * - * ----------------------------------------------------------------------------- */ - -import { Menu, Tray, app, nativeImage } from "electron"; -import * as path from "node:path"; - -import { APP_NAME } from "./constants"; -import { log } from "./logger"; - -/** Callbacks the tray menu wires to its rows. `main.ts` supplies these — - * several are shared verbatim with `installApplicationMenu`'s `MenuActions` - * so the tray and the application menu never disagree about behavior. */ -export interface TrayActions { - /** Bring the dashboard window to front, creating it if it doesn't exist. */ - showDashboard: () => void; - /** Stop and re-launch the embedded server, then reload the window. */ - restartServer: () => void; - /** Reveal `desktop.log` in the OS file browser. */ - openLogs: () => void; - /** Open the dashboard URL in the user's default system browser. */ - openInBrowser: () => void; - /** Flip the OS auto-start-at-login registration. */ - toggleOpenAtLogin: () => void; - /** Read the current auto-start state, used to render the checkbox. */ - isOpenAtLogin: () => boolean; - /** The embedded server's live port, or `null` before it has started. */ - serverPort: () => number | null; - /** Last cached status snapshot (refreshed by the background poller). */ - getSnapshot: () => ServerSnapshot | null; - /** Kick an immediate async snapshot refresh (fire-and-forget on menu open). */ - refreshSnapshot: () => void; - /** Prompt the same quit-confirmation dialog ⌘Q triggers. */ - requestQuit: () => void; -} - -/** - * Structurally identical to `server-host.ts`'s `ServerSnapshot` — redeclared - * here so this module has no compile-time dependency on `server-host.ts`, - * only on the `TrayActions` callbacks `main.ts` wires between them. `main.ts` - * passes `getServerSnapshot`/`refreshServerSnapshot` straight through, so the - * two types must stay in sync by hand if the stats API response shape changes. - */ -export interface ServerSnapshot { - activeSessions: number; - workingAgents: number; - eventsToday: number; -} - -/** - * Tray icon image location. In dev `__dirname` is `desktop/out/`, so `../assets` - * resolves to `desktop/assets/`. In the packaged app the images ship outside - * the asar archive via `extraResources` (see electron-builder.yml), so we - * read them from `process.resourcesPath/assets/`. Loading these from inside - * asar can yield empty `nativeImage` results, which is why we keep them - * unpacked. - * - * Windows gets the colored `icon.ico`; macOS gets the black template PNG that - * the menu bar tints automatically. - */ -/** Pick the platform-appropriate tray image filename — a colored `.ico` on - * Windows (a black glyph would vanish on the usually-dark taskbar), or the - * black "template" PNG on macOS (the menu bar auto-tints it for light/dark). */ -function trayImageFile(): string { - return process.platform === "win32" ? "icon.ico" : "tray-icon-Template.png"; -} - -/** Resolve `trayImageFile()` to an absolute path, branching on dev vs - * packaged layout — see the file-level doc comment for why these assets are - * read from disk (`extraResources`) rather than bundled inside the asar. */ -function trayImagePath(): string { - const file = trayImageFile(); - if (app.isPackaged) { - return path.join(process.resourcesPath, "assets", file); - } - return path.join(__dirname, "..", "assets", file); -} - -/** - * Create the menu-bar / notification-area tray icon and wire its dropdown - * menu. The menu is deliberately rebuilt from `actions` on every open (see - * `showMenu` below) rather than mutated in place, so the port label, the - * live `{sessions, agents, events-today}` snapshot, and the "Open at Login" - * checkbox are always current — Electron menus have no live-binding, so a - * cached template would show stale values until the app happened to rebuild - * it for an unrelated reason. - * - * Left- and right-click both pop the same dropdown via `popUpContextMenu` - * (`tray.on('click', ...)` and `tray.on('right-click', ...)`) instead of - * `Tray#setContextMenu` — a static, pre-assigned menu that Electron shows - * automatically on click, with no hook for the `refreshSnapshot()` call that - * needs to run first so the dropdown reflects the very latest counts. - */ -export function createTray(actions: TrayActions): Tray { - const imagePath = trayImagePath(); - const image = nativeImage.createFromPath(imagePath); - if (image.isEmpty()) { - log.warn("tray image is empty; falling back to in-memory placeholder", imagePath); - } else if (process.platform === "darwin") { - // Template tinting is a macOS concept; on Windows the icon is colored and - // must be shown as-is. - image.setTemplateImage(true); - } - - const tray = new Tray(image.isEmpty() ? nativeImage.createEmpty() : image); - tray.setToolTip(APP_NAME); - - // Singular/plural helper so "1 active session" doesn't read as "1 active sessions". - const plural = (n: number, singular: string, pluralForm?: string): string => - `${n.toLocaleString()} ${n === 1 ? singular : (pluralForm ?? singular + "s")}`; - - // Built fresh on each click so the port, status snapshot, and the - // "Open at Login" checkbox always reflect current state. Snapshot rows - // are intentionally `enabled` (with a click handler that opens the - // dashboard) instead of `enabled: false` — disabled menu items get - // dimmed by macOS, which looked sickly next to the actionable rows - // below them. Clicking any row now lands on the dashboard where the - // user can see the same numbers in context. - const buildMenu = (): Menu => { - const port = actions.serverPort(); - const portLabel = port ? `🟢 Listening on :${port}` : "🔴 Server not running"; - const snap = actions.getSnapshot(); - const open = (): void => actions.showDashboard(); - const snapshotItems: Electron.MenuItemConstructorOptions[] = snap - ? [ - { type: "separator" }, - { label: `📊 ${plural(snap.activeSessions, "active session")}`, click: open }, - { label: `🤖 ${plural(snap.workingAgents, "working agent")}`, click: open }, - { label: `📥 ${plural(snap.eventsToday, "event")} today`, click: open }, - ] - : [{ type: "separator" }, { label: "Snapshot unavailable", enabled: false }]; - - return Menu.buildFromTemplate([ - { label: APP_NAME, enabled: false }, - { label: portLabel, enabled: false }, - ...snapshotItems, - { type: "separator" }, - { label: "Open Dashboard", accelerator: "CmdOrCtrl+O", click: open }, - { label: "Open in Browser…", click: () => actions.openInBrowser() }, - { type: "separator" }, - { label: "Restart Server", click: () => actions.restartServer() }, - { label: "Show Logs", click: () => actions.openLogs() }, - { type: "separator" }, - { - label: "Open at Login", - type: "checkbox", - checked: actions.isOpenAtLogin(), - click: () => actions.toggleOpenAtLogin(), - }, - { type: "separator" }, - { label: `Version ${app.getVersion()}`, enabled: false }, - { - label: "Quit Claude Code Monitor", - accelerator: "CmdOrCtrl+Q", - click: () => actions.requestQuit(), - }, - ]); - }; - - // Single click (left or right) opens the menu — the conventional macOS - // menu-bar utility pattern. Opening the dashboard is the first action in - // the menu, so it's still one click + Enter to surface the window. - // We kick an async refresh on open so the next interaction reflects the - // very latest counts; this open renders the most recent cached snapshot. - const showMenu = (): void => { - actions.refreshSnapshot(); - tray.popUpContextMenu(buildMenu()); - }; - tray.on("click", showMenu); - tray.on("right-click", showMenu); - return tray; -} diff --git a/desktop/src/window.ts b/desktop/src/window.ts deleted file mode 100644 index 86bbb13..0000000 --- a/desktop/src/window.ts +++ /dev/null @@ -1,219 +0,0 @@ -/** - * @file Dashboard window creation + state persistence. - * - * We persist size/position to a JSON file under `app.getPath('userData')`. - * Avoids the `electron-window-state` dependency for ~30 lines of code. - * @author Nguyễn Ngọc Trí Vĩ - */ -/* ============================================================================= - * MODULE_GUIDE — extended in-file reference (comments only; safe to read, never executed) - * ============================================================================= - * **Purpose:** Dashboard module consumed by the React client, MCP tools, or desktop shell depending on deployment mode. - * - * ## Design constraints - * - Local-first: no telemetry leaves the machine unless the user configures webhooks. - * - Fail-safe hooks path on the server must never block Claude Code; UI mirrors that - * philosophy by degrading gracefully (empty states, stale badges, reconnect loops). - * - Destructive flows stay behind explicit confirmation modals and server-side gates. - * - Internationalization: user-visible strings belong in i18n JSON, not literals here. - * - * ## Remote data & SSH - * Remote Data Sources let operators aggregate multiple machines. SSH entries describe - * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every - * scoped GET via `?sources=`. Health checks and import history surface in Settings. - * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * - * ## Internal dependencies - * - `./constants` - * - `./logger` - * - * ## Public surface - * - `appIconPath` — exported API; see TSDoc on the symbol for behavior. - * - `createDashboardWindow` — exported API; see TSDoc on the symbol for behavior. - * - * ## Testing pointers - * - Prefer colocated `__tests__` with Vitest + Testing Library for UI. - * - Server contract changes require `npm run test:server` and OpenAPI sync. - * - MCP edits: `npm run mcp:typecheck` and `npm run mcp:build`. - * - * ## Related docs - * - `ARCHITECTURE.md` — hooks → API → SQLite → WebSocket → UI pipeline. - * - `docs/API.md` — REST reference. - * - `.claude/skills/file-headers/` — mandatory `@author` header policy. - * ============================================================================= */ -/* ----------------------------------------------------------------------------- - * EXPORT CATALOG — quick index of symbols defined below (documentation only). - * ----------------------------------------------------------------------------- - * **appIconPath** - * Part of this module's public contract. Downstream imports should treat - * the signature and return type as stable unless release notes say otherwise. - * When behavior changes, update the `@file` overview and relevant tests. - * - * **createDashboardWindow** - * Part of this module's public contract. Downstream imports should treat - * the signature and return type as stable unless release notes say otherwise. - * When behavior changes, update the `@file` overview and relevant tests. - * - * ----------------------------------------------------------------------------- */ - -import { BrowserWindow, app, shell } from "electron"; -import * as fs from "node:fs"; -import * as path from "node:path"; - -import { APP_NAME, DEFAULT_WINDOW } from "./constants"; -import { log } from "./logger"; - -/** Persisted window geometry. `x`/`y` are omitted until the window has been - * moved at least once — a fresh install lets Electron pick the OS default - * placement rather than forcing `(0, 0)`. */ -interface WindowState { - width: number; - height: number; - x?: number; - y?: number; -} - -/** Absolute path to the JSON file geometry is persisted to, under this - * platform's `userData` directory (e.g. `~/Library/Application Support/…` - * on macOS, `%APPDATA%` on Windows). */ -function statePath(): string { - return path.join(app.getPath("userData"), "window-state.json"); -} - -/** - * Absolute path to the colored application icon used for the window title bar - * and the Windows taskbar / Linux launcher — the same logo the macOS app shows - * in its Dock (rendered from `assets/icon.svg`). Without this, an unpackaged - * `electron out/main.js` run falls back to the generic Electron icon. - * - * Windows wants the multi-size `.ico` (crisp at every taskbar scale); other - * platforms take the `.png`. macOS ignores `BrowserWindow#icon` entirely (its - * window has no icon and the Dock uses the bundle's `.icns`), so the value is - * harmless there. Resolves dev (`desktop/assets`) vs packaged - * (`Resources/assets`, shipped via `extraResources`); returns `undefined` if - * the file is absent so we cleanly fall back instead of throwing. - */ -export function appIconPath(): string | undefined { - const file = process.platform === "win32" ? "icon.ico" : "icon.png"; - const base = app.isPackaged - ? path.join(process.resourcesPath, "assets") - : path.join(__dirname, "..", "assets"); - const p = path.join(base, file); - return fs.existsSync(p) ? p : undefined; -} - -/** - * Read the persisted window geometry, falling back field-by-field to - * `DEFAULT_WINDOW` (and to `undefined` for position) whenever the file is - * missing, unreadable, or contains a field of the wrong type — so a - * corrupted or partially-written state file degrades gracefully instead of - * preventing the window from opening at all. - */ -function loadState(): WindowState { - try { - const raw = fs.readFileSync(statePath(), "utf8"); - const parsed = JSON.parse(raw) as Partial; - return { - width: typeof parsed.width === "number" ? parsed.width : DEFAULT_WINDOW.width, - height: typeof parsed.height === "number" ? parsed.height : DEFAULT_WINDOW.height, - x: typeof parsed.x === "number" ? parsed.x : undefined, - y: typeof parsed.y === "number" ? parsed.y : undefined, - }; - } catch { - return { width: DEFAULT_WINDOW.width, height: DEFAULT_WINDOW.height }; - } -} - -/** - * Write the window's current bounds to `statePath()`. Skipped while the - * window is destroyed or minimized, since `getBounds()` on a minimized - * window reports the pre-minimize size on some platforms — persisting it - * would silently discard the user's last real resize/move. Failures (e.g. - * a read-only `userData` dir) are logged, not thrown — losing the saved - * geometry is cosmetic, not fatal. - */ -function saveState(win: BrowserWindow): void { - if (win.isDestroyed() || win.isMinimized()) return; - const { width, height, x, y } = win.getBounds(); - try { - fs.writeFileSync(statePath(), JSON.stringify({ width, height, x, y })); - } catch (err) { - log.warn("could not persist window state", err); - } -} - -/** - * Create the single dashboard `BrowserWindow` and point it at the embedded - * server's origin. Restores the last persisted size/position (see - * `loadState`), re-saves it (debounced) on every resize/move/close, routes - * all external navigation to the system browser instead of inside Electron, - * and defers `show()` until `ready-to-show` so the window never flashes an - * unstyled blank frame while the page loads. - * - * @param targetUrl The embedded server's origin, e.g. `http://127.0.0.1:4820`. - * @returns The newly created, not-yet-visible `BrowserWindow`. - */ -export function createDashboardWindow(targetUrl: string): BrowserWindow { - const state = loadState(); - - const win = new BrowserWindow({ - width: state.width, - height: state.height, - x: state.x, - y: state.y, - minWidth: 720, - minHeight: 480, - show: false, - title: APP_NAME, - // Colored app logo for the title bar + taskbar (matches the macOS Dock - // icon). No-op on macOS; falls through to the Electron default if missing. - icon: appIconPath(), - // Use the standard macOS title bar rather than `hiddenInset`. With a hidden - // title bar the traffic-light buttons float directly over the React app's - // top edge and visually blend into the dashboard chrome; a native title bar - // gives them their own clearly-separated row, shows the app name, and - // restores the conventional double-click-to-maximize / drag-from-anywhere - // behaviour without needing custom drag regions in the renderer. - titleBarStyle: "default", - backgroundColor: "#0b0f1a", - webPreferences: { - preload: path.join(__dirname, "preload.js"), - contextIsolation: true, - nodeIntegration: false, - sandbox: false, - // We're loading our own localhost-only origin, never remote content. - webSecurity: true, - }, - }); - - win.once("ready-to-show", () => win.show()); - - // Persist size/position on resize/move (debounced via the close handler too). - let saveTimer: NodeJS.Timeout | null = null; - const debounced = () => { - if (saveTimer) clearTimeout(saveTimer); - saveTimer = setTimeout(() => saveState(win), 400); - }; - win.on("resize", debounced); - win.on("move", debounced); - win.on("close", () => saveState(win)); - - // External links open in the user's browser, not inside Electron. - win.webContents.setWindowOpenHandler(({ url }) => { - void shell.openExternal(url); - return { action: "deny" }; - }); - win.webContents.on("will-navigate", (event, url) => { - if (!url.startsWith(targetUrl)) { - event.preventDefault(); - void shell.openExternal(url); - } - }); - - win.loadURL(targetUrl).catch((err) => log.error("failed to load dashboard URL", err)); - return win; -} diff --git a/desktop/tests/smoke.test.mjs b/desktop/tests/smoke.test.mjs deleted file mode 100644 index a42fa49..0000000 --- a/desktop/tests/smoke.test.mjs +++ /dev/null @@ -1,150 +0,0 @@ -/** - * @file Desktop smoke test. - * - * Boots the compiled main process under Electron, then probes the embedded - * dashboard server's /api/health endpoint. This is intentionally minimal: - * it does not exercise the BrowserWindow (which requires a display) so it - * runs on headless CI without xvfb. The window itself is covered by manual - * QA in the PR description. - * @author Nguyễn Ngọc Trí Vĩ - */ - -import { describe, it, before, after } from "node:test"; -import assert from "node:assert/strict"; -import { spawn } from "node:child_process"; -import { once } from "node:events"; -import http from "node:http"; -import { createRequire } from "node:module"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const DESKTOP_ROOT = path.resolve(__dirname, ".."); -const MAIN_JS = path.join(DESKTOP_ROOT, "out", "main.js"); -// Resolve the actual Electron executable (electron.exe on Windows, the binary -// under Electron.app on macOS). The `.bin/electron` shim is extension-less and -// cannot be spawned without a shell on Windows; `require("electron")` returns -// the real binary path on every platform. -const ELECTRON_BIN = createRequire(import.meta.url)("electron"); - -const HEALTH_TIMEOUT_MS = 60_000; -const POLL_INTERVAL_MS = 500; - -/** Resolve when GET /api/health on any of these ports answers ok. */ -async function waitForHealth(ports, deadline) { - while (Date.now() < deadline) { - for (const port of ports) { - const ok = await probeHealth(port); - if (ok) return port; - } - await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS)); - } - throw new Error(`No port answered /api/health within ${HEALTH_TIMEOUT_MS}ms (tried ${ports})`); -} - -function probeHealth(port) { - return new Promise((resolve) => { - const req = http.get({ host: "127.0.0.1", port, path: "/api/health", timeout: 1500 }, (res) => { - let buf = ""; - res.setEncoding("utf8"); - res.on("data", (chunk) => (buf += chunk)); - res.on("end", () => { - try { - resolve(JSON.parse(buf)?.status === "ok"); - } catch { - resolve(false); - } - }); - }); - req.on("error", () => resolve(false)); - req.on("timeout", () => { - req.destroy(); - resolve(false); - }); - }); -} - -let electronProc; -// Pick a unique high port for each test run so we never accidentally probe an -// unrelated server (e.g. the user's own `npm start` on 4820). The env var -// `CCAM_DESKTOP_BIND_PORT` tells the desktop process to bind exactly this port, -// skipping the "adopt an existing healthy server" code path. -const TEST_PORT = 50000 + Math.floor(Math.random() * 5000); - -// On POSIX, spawn the Electron parent as a process-group leader so we can -// signal the whole tree (helpers, embedded server) with one kill(-pid). -// Without this, SIGTERM only hits the parent and leaves helpers alive, -// keeping the stdio pipes open and hanging `node --test` indefinitely. -const IS_POSIX = process.platform !== "win32"; - -/** Kill the Electron process tree and resolve when it's actually gone. */ -async function killElectronTree(proc, { timeoutMs = 5_000 } = {}) { - if (!proc || proc.exitCode !== null || proc.signalCode !== null) return; - proc.killedByTest = true; - - const signalGroup = (sig) => { - try { - if (IS_POSIX && proc.pid) process.kill(-proc.pid, sig); - else proc.kill(sig); - } catch { - /* group may already be gone */ - } - }; - - const exited = once(proc, "exit"); - signalGroup("SIGTERM"); - - const timer = new Promise((resolve) => setTimeout(resolve, timeoutMs, "timeout")); - const winner = await Promise.race([exited.then(() => "exit"), timer]); - if (winner === "timeout") { - signalGroup("SIGKILL"); - await Promise.race([exited, new Promise((r) => setTimeout(r, 2_000))]); - } -} - -describe("desktop smoke", () => { - before(async () => { - electronProc = spawn(ELECTRON_BIN, [MAIN_JS], { - cwd: DESKTOP_ROOT, - detached: IS_POSIX, - env: { - ...process.env, - // Suppress the BrowserWindow on the test runner; we only care that - // the server boots cleanly. - ELECTRON_DISABLE_GPU: "1", - ELECTRON_ENABLE_LOGGING: "1", - CCAM_DESKTOP_VERBOSE: "1", - CCAM_DESKTOP_BIND_PORT: String(TEST_PORT), - }, - stdio: ["ignore", "pipe", "pipe"], - }); - - electronProc.stdout.on("data", (b) => process.stdout.write(`[electron] ${b}`)); - electronProc.stderr.on("data", (b) => process.stderr.write(`[electron] ${b}`)); - - electronProc.on("exit", (code, signal) => { - if (!electronProc.killedByTest) { - // eslint-disable-next-line no-console - console.error(`electron exited unexpectedly: code=${code} signal=${signal}`); - } - }); - }); - - after(async () => { - await killElectronTree(electronProc); - }); - - it("brings up the embedded server and serves /api/health on the bound port", async () => { - const deadline = Date.now() + HEALTH_TIMEOUT_MS; - const port = await waitForHealth([TEST_PORT], deadline); - assert.equal( - port, - TEST_PORT, - `desktop process should have bound CCAM_DESKTOP_BIND_PORT=${TEST_PORT}` - ); - assert.ok( - electronProc && !electronProc.killed && electronProc.exitCode === null, - "electron process should still be alive when /api/health answers" - ); - }); -}); diff --git a/desktop/tsconfig.json b/desktop/tsconfig.json deleted file mode 100644 index c032052..0000000 --- a/desktop/tsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "Node16", - "moduleResolution": "Node16", - "lib": ["ES2022"], - "outDir": "out", - "rootDir": "src", - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true, - "forceConsistentCasingInFileNames": true, - "esModuleInterop": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "sourceMap": true, - "declaration": false - }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules", "out", "release", "tests"] -} diff --git a/docker-compose.full.yml b/docker-compose.full.yml deleted file mode 100644 index 225fa70..0000000 --- a/docker-compose.full.yml +++ /dev/null @@ -1,92 +0,0 @@ -# CCAM dashboard + Prometheus + Grafana on one Docker network. -# -# npm run docker:full:up -# open http://localhost:4820 (dashboard) -# open http://localhost:3000 (Grafana — admin / admin) -# -# Hooks still run on the HOST — run `npm run install-hooks` on the host and point -# them at http://localhost:4820 (same as docker-compose.yml alone). -services: - agent-monitor: - build: . - container_name: agent-monitor - ports: - - "127.0.0.1:${DASHBOARD_PORT:-4820}:4820" - volumes: - - ${CLAUDE_HOME:-~/.claude}/agent-dashboard:/app/data - - ${CLAUDE_HOME:-~/.claude}:/root/.claude:ro - environment: - NODE_ENV: production - DASHBOARD_PORT: 4820 - # Lets in-network Prometheus scrape /api/metrics (Host: agent-monitor:4820). - DASHBOARD_ALLOWED_HOSTS: agent-monitor - restart: unless-stopped - networks: - - ccam - healthcheck: - test: - [ - "CMD", - "node", - "-e", - "fetch('http://127.0.0.1:4820/api/health').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))", - ] - interval: 10s - timeout: 5s - retries: 6 - start_period: 20s - - prometheus: - image: prom/prometheus:latest - container_name: ccam-prometheus - restart: unless-stopped - depends_on: - agent-monitor: - condition: service_healthy - command: - - --config.file=/etc/prometheus/prometheus-docker.yml - - --storage.tsdb.path=/prometheus - - --web.enable-lifecycle - - --web.console.templates=/etc/prometheus/consoles - volumes: - - ./monitoring/prometheus:/etc/prometheus:ro - - prometheus-data:/prometheus - ports: - - "127.0.0.1:9090:9090" - networks: - - ccam - healthcheck: - test: ["CMD", "wget", "-qO-", "http://127.0.0.1:9090/-/ready"] - interval: 10s - timeout: 5s - retries: 6 - - grafana: - image: grafana/grafana:latest - container_name: ccam-grafana - restart: unless-stopped - depends_on: - prometheus: - condition: service_healthy - env_file: - - ./monitoring/grafana.defaults.env - volumes: - - ./monitoring/grafana/provisioning:/etc/grafana/provisioning:ro - - ./monitoring/grafana/dashboards:/var/lib/grafana/dashboards:ro - - grafana-data:/var/lib/grafana - ports: - - "127.0.0.1:3000:3000" - networks: - - ccam - healthcheck: - test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/api/health"] - interval: 10s - timeout: 5s - retries: 6 - -networks: - ccam: - -volumes: - prometheus-data: - grafana-data: diff --git a/docker-compose.yml b/docker-compose.yml index 7cf1c22..8fd8e83 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,8 +15,8 @@ services: environment: - NODE_ENV=production - DASHBOARD_PORT=4820 - # Optional — required when Prometheus-in-Docker scrapes this container - # (host.docker.internal) or the full stack (agent-monitor). See monitoring/README.md. + # Optional — required when a Prometheus-in-Docker scraper reaches this + # container as anything other than loopback (e.g. host.docker.internal). - DASHBOARD_ALLOWED_HOSTS=${DASHBOARD_ALLOWED_HOSTS:-} # DASHBOARD_HOST=0.0.0.0 and DASHBOARD_DATA_DIR=/app/data are baked into # the image (see Dockerfile) so both `docker run` and Compose work as-is. diff --git a/docs/API.md b/docs/API.md index 5466f7a..14b61e5 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1058,8 +1058,6 @@ scrape_configs: # credentials: "" ``` -A ready-to-run Prometheus + Grafana stack (four auto-provisioned dashboards; default home **CCAM — Overview**) lives in [`monitoring/`](../monitoring/README.md). **npm path (no Docker):** `npm run monitoring:install` then `npm run monitoring:up` (binaries are pulled via the monitoring package's `postinstall` — there is no official `grafana`/`prometheus` server package on npm). **Docker path:** `npm run monitoring:docker:up` or `npm run docker:full:up` (set `DASHBOARD_ALLOWED_HOSTS=host.docker.internal` on the dashboard when Prometheus runs in a container). Pre-built Prometheus console: `http://localhost:9090/consoles/index.html`. - --- ### Pricing diff --git a/docs/README.md b/docs/README.md index 004542a..d2b21c5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -31,8 +31,6 @@ graph TB Architecture --> Hooks[HOOKS.md
Hook system integration] Architecture --> MCP[MCP.md
MCP server integration] - Setup --> Deploy[DEPLOYMENT.md
Production deployment] - style Start fill:#3B82F6 style Setup fill:#10B981 style Architecture fill:#F59E0B @@ -50,10 +48,8 @@ graph TB | [DATABASE.md](./DATABASE.md) | SQLite schema, queries, performance | Database administrators | | [HOOKS.md](./HOOKS.md) | Claude Code hook system integration | Hook developers | | [MCP.md](./MCP.md) | MCP server setup and tool reference | MCP integrators | -| [DEPLOYMENT.md](./DEPLOYMENT.md) | Production deployment strategies | DevOps engineers | | [I18N.md](./I18N.md) | Language architecture, locale strategy, and rollout checklist | Frontend and product teams | | [CLI.md](./CLI.md) | `ccam` command reference — monitoring, browsing, insights, alerts, pricing, import, administration | Terminal users and CI scripting | -| [monitoring/README.md](../monitoring/README.md) | Prometheus + Grafana stack (`npm run monitoring:up` or Docker) | DevOps / observability | --- @@ -122,27 +118,6 @@ graph TB --- -### For DevOps Engineers - -```mermaid -graph TB - DevOps[DevOps Engineer] --> Deploy[DEPLOYMENT.md] - Deploy --> Docker[Docker Setup] - Deploy --> PM2[PM2 Process Manager] - Deploy --> Cloud[Cloud Deployment] - Deploy --> Monitoring[Monitoring & Logging] - - style DevOps fill:#F59E0B -``` - -**Key Documents:** - -- [DEPLOYMENT.md](./DEPLOYMENT.md) - Complete deployment guide -- [DATABASE.md](./DATABASE.md#backup-strategies) - Backup strategies -- [server/README.md](../server/README.md#performance) - Performance tuning - ---- - ### For Integration Developers ```mermaid @@ -378,37 +353,6 @@ erDiagram --- -## Deployment Options - -```mermaid -graph TB - subgraph "Development" - Dev[npm run dev
Hot reload] - end - - subgraph "Production" - Docker[Docker Compose
Containerized] - PM2[PM2
Process manager] - Systemd[Systemd Service
Linux systems] - Cloud[Cloud Platform
AWS, Azure, GCP] - end - - Dev -.->|Build| Docker - Dev -.->|Build| PM2 - Dev -.->|Build| Systemd - Dev -.->|Build| Cloud - - style Dev fill:#3B82F6 - style Docker fill:#2496ED - style PM2 fill:#10B981 - style Systemd fill:#F59E0B - style Cloud fill:#8B5CF6 -``` - -**Full Reference:** [DEPLOYMENT.md](./DEPLOYMENT.md) - ---- - ## Performance Metrics ### Benchmarks diff --git a/mcp/build/.srchash b/mcp/build/.srchash index 5a0a20c..feaa6c8 100644 --- a/mcp/build/.srchash +++ b/mcp/build/.srchash @@ -1 +1 @@ -2e9282b25c6d10d236ad0fd91bceeebf9a5c08cdbea74f1c780c1bae5303ca81 +4a478bdd2063e99f6bc7cf62ec091b03757fd54fd99fba4b662d02b1f51543bc diff --git a/mcp/build/clients/dashboard-api-client.js b/mcp/build/clients/dashboard-api-client.js index a68624a..e9e27eb 100644 --- a/mcp/build/clients/dashboard-api-client.js +++ b/mcp/build/clients/dashboard-api-client.js @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../config/app-config.js` * - `../core/logger.js` diff --git a/mcp/build/config/app-config.js b/mcp/build/config/app-config.js index 8fd3084..f39b63c 100644 --- a/mcp/build/config/app-config.js +++ b/mcp/build/config/app-config.js @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Public surface * - `LogLevel` — exported API; see TSDoc on the symbol for behavior. * - `TransportMode` — exported API; see TSDoc on the symbol for behavior. diff --git a/mcp/build/core/logger.js b/mcp/build/core/logger.js index f6d9eee..3ef03a7 100644 --- a/mcp/build/core/logger.js +++ b/mcp/build/core/logger.js @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../config/app-config.js` * diff --git a/mcp/build/core/tool-registry.js b/mcp/build/core/tool-registry.js index f22cf03..9fe9adf 100644 --- a/mcp/build/core/tool-registry.js +++ b/mcp/build/core/tool-registry.js @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `./logger.js` * - `./tool-result.js` diff --git a/mcp/build/core/tool-result.js b/mcp/build/core/tool-result.js index 81d7168..34a53a8 100644 --- a/mcp/build/core/tool-result.js +++ b/mcp/build/core/tool-result.js @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../clients/dashboard-api-client.js` * diff --git a/mcp/build/index.js b/mcp/build/index.js index c322a1f..c2eedd2 100644 --- a/mcp/build/index.js +++ b/mcp/build/index.js @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `./clients/dashboard-api-client.js` * - `./config/app-config.js` diff --git a/mcp/build/policy/tool-guards.js b/mcp/build/policy/tool-guards.js index a4549fe..03f2636 100644 --- a/mcp/build/policy/tool-guards.js +++ b/mcp/build/policy/tool-guards.js @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../config/app-config.js` * diff --git a/mcp/build/server.js b/mcp/build/server.js index 94d7b70..034325c 100644 --- a/mcp/build/server.js +++ b/mcp/build/server.js @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `./config/app-config.js` * - `./clients/dashboard-api-client.js` diff --git a/mcp/build/tools/domains/agent-tools.js b/mcp/build/tools/domains/agent-tools.js index 6fb91b4..b18db2d 100644 --- a/mcp/build/tools/domains/agent-tools.js +++ b/mcp/build/tools/domains/agent-tools.js @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../../core/tool-registry.js` * - `../../policy/tool-guards.js` diff --git a/mcp/build/tools/domains/event-tools.js b/mcp/build/tools/domains/event-tools.js index 802c408..c4a1199 100644 --- a/mcp/build/tools/domains/event-tools.js +++ b/mcp/build/tools/domains/event-tools.js @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../../core/tool-registry.js` * - `../../policy/tool-guards.js` diff --git a/mcp/build/tools/domains/maintenance-tools.js b/mcp/build/tools/domains/maintenance-tools.js index 351a098..75407ca 100644 --- a/mcp/build/tools/domains/maintenance-tools.js +++ b/mcp/build/tools/domains/maintenance-tools.js @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../../core/tool-registry.js` * - `../../policy/tool-guards.js` diff --git a/mcp/build/tools/domains/observability-tools.js b/mcp/build/tools/domains/observability-tools.js index 3a411ef..ace5bfb 100644 --- a/mcp/build/tools/domains/observability-tools.js +++ b/mcp/build/tools/domains/observability-tools.js @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../../types/tool-context.js` * - `../../core/tool-registry.js` diff --git a/mcp/build/tools/domains/pricing-tools.js b/mcp/build/tools/domains/pricing-tools.js index 8a940a7..fc4b72a 100644 --- a/mcp/build/tools/domains/pricing-tools.js +++ b/mcp/build/tools/domains/pricing-tools.js @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../../core/tool-registry.js` * - `../../policy/tool-guards.js` diff --git a/mcp/build/tools/domains/session-tools.js b/mcp/build/tools/domains/session-tools.js index 8de20fa..954f233 100644 --- a/mcp/build/tools/domains/session-tools.js +++ b/mcp/build/tools/domains/session-tools.js @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../../core/tool-registry.js` * - `../../policy/tool-guards.js` diff --git a/mcp/build/tools/index.js b/mcp/build/tools/index.js index d6f9fcc..7c27445 100644 --- a/mcp/build/tools/index.js +++ b/mcp/build/tools/index.js @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../types/tool-context.js` * - `./domains/observability-tools.js` diff --git a/mcp/build/tools/schemas.js b/mcp/build/tools/schemas.js index 5892a18..d8966f9 100644 --- a/mcp/build/tools/schemas.js +++ b/mcp/build/tools/schemas.js @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Public surface * - `SessionStatusSchema` — exported API; see TSDoc on the symbol for behavior. * - `AgentStatusSchema` — exported API; see TSDoc on the symbol for behavior. diff --git a/mcp/build/transports/http-server.js b/mcp/build/transports/http-server.js index c5dc415..60ecf24 100644 --- a/mcp/build/transports/http-server.js +++ b/mcp/build/transports/http-server.js @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../config/app-config.js` * - `../core/logger.js` diff --git a/mcp/build/transports/repl.js b/mcp/build/transports/repl.js index 0a25724..4d6caf5 100644 --- a/mcp/build/transports/repl.js +++ b/mcp/build/transports/repl.js @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../config/app-config.js` * - `../clients/dashboard-api-client.js` diff --git a/mcp/build/transports/tool-collector.js b/mcp/build/transports/tool-collector.js index d594492..2ea165b 100644 --- a/mcp/build/transports/tool-collector.js +++ b/mcp/build/transports/tool-collector.js @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../config/app-config.js` * - `../clients/dashboard-api-client.js` diff --git a/mcp/build/types/tool-context.js b/mcp/build/types/tool-context.js index 5452e8b..86b11e6 100644 --- a/mcp/build/types/tool-context.js +++ b/mcp/build/types/tool-context.js @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../config/app-config.js` * - `../clients/dashboard-api-client.js` diff --git a/mcp/build/ui/banner.js b/mcp/build/ui/banner.js index 7b4f4a4..7de9dd0 100644 --- a/mcp/build/ui/banner.js +++ b/mcp/build/ui/banner.js @@ -23,11 +23,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Public surface * - `printBanner` — exported API; see TSDoc on the symbol for behavior. * - `printServerInfo` — exported API; see TSDoc on the symbol for behavior. diff --git a/mcp/build/ui/colors.js b/mcp/build/ui/colors.js index 4d00dd8..cabe975 100644 --- a/mcp/build/ui/colors.js +++ b/mcp/build/ui/colors.js @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Public surface * - `bold` — exported API; see TSDoc on the symbol for behavior. * - `dim` — exported API; see TSDoc on the symbol for behavior. diff --git a/mcp/build/ui/formatter.js b/mcp/build/ui/formatter.js index 70d54eb..1b71ac8 100644 --- a/mcp/build/ui/formatter.js +++ b/mcp/build/ui/formatter.js @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Public surface * - `box` — exported API; see TSDoc on the symbol for behavior. * - `divider` — exported API; see TSDoc on the symbol for behavior. diff --git a/mcp/src/clients/dashboard-api-client.ts b/mcp/src/clients/dashboard-api-client.ts index 4f6e271..3189c77 100644 --- a/mcp/src/clients/dashboard-api-client.ts +++ b/mcp/src/clients/dashboard-api-client.ts @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../config/app-config.js` * - `../core/logger.js` diff --git a/mcp/src/config/app-config.ts b/mcp/src/config/app-config.ts index 9ccc6ee..2d5e393 100644 --- a/mcp/src/config/app-config.ts +++ b/mcp/src/config/app-config.ts @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Public surface * - `LogLevel` — exported API; see TSDoc on the symbol for behavior. * - `TransportMode` — exported API; see TSDoc on the symbol for behavior. diff --git a/mcp/src/core/logger.ts b/mcp/src/core/logger.ts index 0f7c3c6..c7552d9 100644 --- a/mcp/src/core/logger.ts +++ b/mcp/src/core/logger.ts @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../config/app-config.js` * diff --git a/mcp/src/core/tool-registry.ts b/mcp/src/core/tool-registry.ts index fdc3a6b..d2823d9 100644 --- a/mcp/src/core/tool-registry.ts +++ b/mcp/src/core/tool-registry.ts @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `./logger.js` * - `./tool-result.js` diff --git a/mcp/src/core/tool-result.ts b/mcp/src/core/tool-result.ts index 6e87972..8a27332 100644 --- a/mcp/src/core/tool-result.ts +++ b/mcp/src/core/tool-result.ts @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../clients/dashboard-api-client.js` * diff --git a/mcp/src/index.ts b/mcp/src/index.ts index f9f8742..7d06823 100644 --- a/mcp/src/index.ts +++ b/mcp/src/index.ts @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `./clients/dashboard-api-client.js` * - `./config/app-config.js` diff --git a/mcp/src/policy/tool-guards.ts b/mcp/src/policy/tool-guards.ts index ba7eec2..f296a92 100644 --- a/mcp/src/policy/tool-guards.ts +++ b/mcp/src/policy/tool-guards.ts @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../config/app-config.js` * diff --git a/mcp/src/server.ts b/mcp/src/server.ts index f03a4b7..98c8708 100644 --- a/mcp/src/server.ts +++ b/mcp/src/server.ts @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `./config/app-config.js` * - `./clients/dashboard-api-client.js` diff --git a/mcp/src/tools/domains/agent-tools.ts b/mcp/src/tools/domains/agent-tools.ts index 1d11312..2952dd2 100644 --- a/mcp/src/tools/domains/agent-tools.ts +++ b/mcp/src/tools/domains/agent-tools.ts @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../../core/tool-registry.js` * - `../../policy/tool-guards.js` diff --git a/mcp/src/tools/domains/event-tools.ts b/mcp/src/tools/domains/event-tools.ts index bc896cc..2eac2ab 100644 --- a/mcp/src/tools/domains/event-tools.ts +++ b/mcp/src/tools/domains/event-tools.ts @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../../core/tool-registry.js` * - `../../policy/tool-guards.js` diff --git a/mcp/src/tools/domains/maintenance-tools.ts b/mcp/src/tools/domains/maintenance-tools.ts index f2008d7..80cd875 100644 --- a/mcp/src/tools/domains/maintenance-tools.ts +++ b/mcp/src/tools/domains/maintenance-tools.ts @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../../core/tool-registry.js` * - `../../policy/tool-guards.js` diff --git a/mcp/src/tools/domains/observability-tools.ts b/mcp/src/tools/domains/observability-tools.ts index bc21a3a..f3fc9c0 100644 --- a/mcp/src/tools/domains/observability-tools.ts +++ b/mcp/src/tools/domains/observability-tools.ts @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../../types/tool-context.js` * - `../../core/tool-registry.js` diff --git a/mcp/src/tools/domains/pricing-tools.ts b/mcp/src/tools/domains/pricing-tools.ts index 20429ee..cef5938 100644 --- a/mcp/src/tools/domains/pricing-tools.ts +++ b/mcp/src/tools/domains/pricing-tools.ts @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../../core/tool-registry.js` * - `../../policy/tool-guards.js` diff --git a/mcp/src/tools/domains/session-tools.ts b/mcp/src/tools/domains/session-tools.ts index 42291ba..8afaac7 100644 --- a/mcp/src/tools/domains/session-tools.ts +++ b/mcp/src/tools/domains/session-tools.ts @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../../core/tool-registry.js` * - `../../policy/tool-guards.js` diff --git a/mcp/src/tools/index.ts b/mcp/src/tools/index.ts index 8be49d4..1cfbad2 100644 --- a/mcp/src/tools/index.ts +++ b/mcp/src/tools/index.ts @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../types/tool-context.js` * - `./domains/observability-tools.js` diff --git a/mcp/src/tools/schemas.ts b/mcp/src/tools/schemas.ts index d8b063c..8eaa09f 100644 --- a/mcp/src/tools/schemas.ts +++ b/mcp/src/tools/schemas.ts @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Public surface * - `SessionStatusSchema` — exported API; see TSDoc on the symbol for behavior. * - `AgentStatusSchema` — exported API; see TSDoc on the symbol for behavior. diff --git a/mcp/src/transports/http-server.ts b/mcp/src/transports/http-server.ts index 00da65e..7c0a26c 100644 --- a/mcp/src/transports/http-server.ts +++ b/mcp/src/transports/http-server.ts @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../config/app-config.js` * - `../core/logger.js` diff --git a/mcp/src/transports/repl.ts b/mcp/src/transports/repl.ts index 50d3517..cfdacf7 100644 --- a/mcp/src/transports/repl.ts +++ b/mcp/src/transports/repl.ts @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../config/app-config.js` * - `../clients/dashboard-api-client.js` diff --git a/mcp/src/transports/tool-collector.ts b/mcp/src/transports/tool-collector.ts index 8773465..5e95843 100644 --- a/mcp/src/transports/tool-collector.ts +++ b/mcp/src/transports/tool-collector.ts @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../config/app-config.js` * - `../clients/dashboard-api-client.js` diff --git a/mcp/src/types/tool-context.ts b/mcp/src/types/tool-context.ts index e2b7e04..38e8882 100644 --- a/mcp/src/types/tool-context.ts +++ b/mcp/src/types/tool-context.ts @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Internal dependencies * - `../config/app-config.js` * - `../clients/dashboard-api-client.js` diff --git a/mcp/src/ui/banner.ts b/mcp/src/ui/banner.ts index f5ccd9a..35af21b 100644 --- a/mcp/src/ui/banner.ts +++ b/mcp/src/ui/banner.ts @@ -23,11 +23,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Public surface * - `printBanner` — exported API; see TSDoc on the symbol for behavior. * - `printServerInfo` — exported API; see TSDoc on the symbol for behavior. diff --git a/mcp/src/ui/colors.ts b/mcp/src/ui/colors.ts index 412f02c..44e8f20 100644 --- a/mcp/src/ui/colors.ts +++ b/mcp/src/ui/colors.ts @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Public surface * - `bold` — exported API; see TSDoc on the symbol for behavior. * - `dim` — exported API; see TSDoc on the symbol for behavior. diff --git a/mcp/src/ui/formatter.ts b/mcp/src/ui/formatter.ts index ebbff64..a90def6 100644 --- a/mcp/src/ui/formatter.ts +++ b/mcp/src/ui/formatter.ts @@ -20,11 +20,6 @@ * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * - * ## Observability - * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four - * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and - * Docker Compose profiles are documented in `monitoring/README.md`. - * * ## Public surface * - `box` — exported API; see TSDoc on the symbol for behavior. * - `divider` — exported API; see TSDoc on the symbol for behavior. diff --git a/monitoring/.gitignore b/monitoring/.gitignore deleted file mode 100644 index f348210..0000000 --- a/monitoring/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -.data/ -.bin/ -node_modules/ -package-lock.json diff --git a/monitoring/README.md b/monitoring/README.md deleted file mode 100644 index 1b9fa3c..0000000 --- a/monitoring/README.md +++ /dev/null @@ -1,272 +0,0 @@ -# CCAM monitoring stack (Prometheus + Grafana) - -![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) -![Docker](https://img.shields.io/badge/Docker-optional-2496ED?style=flat-square&logo=docker&logoColor=white) -![Node.js](https://img.shields.io/badge/npm-managed-339933?style=flat-square&logo=nodedotjs&logoColor=white) - -A turnkey [Prometheus](https://prometheus.io/) + [Grafana](https://grafana.com/) -stack that scrapes the dashboard's [`GET /api/metrics`](../docs/API.md#metrics) -endpoint and renders **four auto-provisioned Grafana dashboards** (default home: -**CCAM — Overview**). Use it to watch live -sessions, agent states, event throughput, and token burn from the same -observability stack as the rest of your infra. - -**No Homebrew, apt, or global installs required.** There is no official `prometheus` or -`grafana` server package on npm (only client libraries and UI components) — the -stack uses npm's own install lifecycle instead: `npm run monitoring:install` -runs `postinstall` in this folder and pulls official release binaries into -`monitoring/.bin/` (same pattern as Playwright browsers or Electron). - -### Supported platforms (npm path) - -| OS | Architectures | -| --- | --- | -| macOS | Apple Silicon (`arm64`), Intel (`x64`) | -| Linux | `arm64`, `amd64` (`x64`) | -| Windows | `x64` | - -Node.js 20+ is the only prerequisite. On Windows, run the npm commands from -PowerShell or Command Prompt in the repo root (same as the rest of CCAM). - -## Grafana login (local default) - -Open after `monitoring:up` or `monitoring:docker:up`. -The admin account is **auto-created on first start** — no manual signup: - -| Field | Value | -| --- | --- | -| Username | `admin` | -| Password | `admin` | - -Credentials are defined in [`grafana.defaults.env`](./grafana.defaults.env) and -applied by both the npm and Docker paths. **CCAM — Overview** is the default -home dashboard (`GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_UID=ccam-overview`) with -PromQL queries against your live `/api/metrics` scrape — no sample or synthetic -data. The Prometheus datasource is pre-provisioned, so you land straight in the -UI after login. - -> If login fails after an earlier Grafana run, reset local state: -> `rm -rf monitoring/.data/grafana` (npm) or recreate the `grafana-data` Docker -> volume, then start the stack again. - -``` -monitoring/ -├── package.json # npm install downloads binaries (postinstall) -├── grafana.defaults.env # default admin / admin credentials -├── scripts/ # lifecycle helpers (setup / up / down) -├── prometheus/ -│ ├── prometheus-native.yml # scrape config for npm-managed stack -│ ├── prometheus.yml # scrape config for Docker stack -│ ├── prometheus-docker.yml # all-Docker stack (docker-compose.full.yml) -│ ├── ccam-rules.yml # recording rules (derived from live scrapes) -│ └── consoles/index.html # Prometheus CCAM console (pre-built graphs) -├── grafana/ -│ ├── provisioning/… # Docker datasource + dashboard provider -│ ├── provisioning-native/… # npm-managed datasource template -│ └── dashboards/ # ccam-overview + 3 focused boards -└── docker-compose.yml # optional Docker path -``` - -## Quick start (npm — no Docker) - -1. **Start the dashboard** on loopback (default `npm start` on port 4820). No - `DASHBOARD_ALLOWED_HOSTS` override is needed — Prometheus scrapes - `127.0.0.1:4820`, which the server's Host guard already accepts. - - ```bash - npm start - ``` - -2. **One-time install** — uses npm's `postinstall` to download official - Prometheus + Grafana OSS binaries into `monitoring/.bin/` (~150 MB total): - - ```bash - npm run monitoring:install - ``` - - (`monitoring:setup` is an alias for the same command.) - -3. **Bring up the stack:** - - ```bash - npm run monitoring:up - ``` - -4. **Open Grafana** at (login `admin` / `admin`). The - **CCAM — Overview** dashboard is already there — no import step. For - Prometheus, open the pre-built **CCAM console** at - (live graphs + tables), or - → **Consoles** → `index.html` (`Status → Targets`: - `ccam` should be **UP**). - -Stop with `npm run monitoring:down`. For a foreground session with logs on the -terminal, use `npm run monitoring:start` instead (Ctrl+C stops both). - -## Quick start (Docker / Podman) - -Use this when you prefer containers or when the dashboard itself runs in Docker. - -1. **Start the dashboard** so the container can scrape it. The server's - DNS-rebinding guard only accepts loopback `Host` headers, and Prometheus (in - Docker) reaches the host as `host.docker.internal` — so allow that Host: - - ```bash - DASHBOARD_ALLOWED_HOSTS=host.docker.internal npm start - ``` - - When the dashboard runs **inside Docker** (`docker compose up` at the repo - root), set the same variable on the `agent-monitor` service: - - ```bash - DASHBOARD_ALLOWED_HOSTS=host.docker.internal docker compose up -d - ``` - - > Without this you'll see the Prometheus target stuck **DOWN** with - > `403 EBADHOST`. If you also set `DASHBOARD_TOKEN`, see [Auth](#auth) below. - -2. **Bring up the monitoring stack:** - - ```bash - npm run monitoring:docker:up - ``` - -3. **Open Grafana** at (login `admin` / `admin`). - -Tear down with `npm run monitoring:docker:down`. - -### All-in-one Docker (app + monitoring) - -Runs the dashboard, Prometheus, and Grafana on one network — no -`host.docker.internal` wiring required: - -```bash -npm run docker:full:up -npm run monitoring:verify -``` - -Stop with `npm run docker:full:down`. - -## Deployment options - -| Dashboard | Monitoring | Commands | -| --- | --- | --- | -| **npm** (`npm start`) | **npm** | `monitoring:install` → `monitoring:up` | -| **npm** | **Docker** | `DASHBOARD_ALLOWED_HOSTS=host.docker.internal npm start` → `monitoring:docker:up` | -| **Docker** (`docker:up`) | **Docker** | `DASHBOARD_ALLOWED_HOSTS=host.docker.internal docker compose up -d` → `monitoring:docker:up` | -| **Docker** | **npm** | `docker:up` + `monitoring:up` (scrapes host `127.0.0.1:4820`) | -| **Docker full stack** | **included** | `docker:full:up` | - -Verify any running stack: `npm run monitoring:verify`. - -## npm scripts - -| Command | Description | -| --- | --- | -| `npm run monitoring:install` | `npm install` in `monitoring/` — downloads binaries via `postinstall` | -| `npm run monitoring:setup` | Alias for `monitoring:install` | -| `npm run monitoring:up` | Start Prometheus (:9090) + Grafana (:3000) in the background | -| `npm run monitoring:down` | Stop the npm-managed stack | -| `npm run monitoring:start` | Foreground start (Ctrl+C stops both) | -| `npm run monitoring:docker:up` | `docker compose` Prometheus + Grafana | -| `npm run monitoring:docker:down` | Tear down the Docker monitoring stack | -| `npm run monitoring:verify` | Health-check dashboard + Prometheus + Grafana + scrape target | -| `npm run docker:up` | Start the dashboard container only | -| `npm run docker:down` | Stop the dashboard container | -| `npm run docker:full:up` | Dashboard + Prometheus + Grafana (all Docker) | -| `npm run docker:full:down` | Tear down the full Docker stack | - -## Bundled Grafana dashboards - -Four dashboards are auto-provisioned from `grafana/dashboards/` — all query live -`/api/metrics` data (no sample or synthetic series): - -| Dashboard | UID | Focus | -| --- | --- | --- | -| **CCAM — Overview** | `ccam-overview` | Default home — fleet snapshot, totals, breakdowns, rates | -| **CCAM — Sessions & Agents** | `ccam-sessions-agents` | Session lifecycle, agent states, WebSocket clients | -| **CCAM — Tokens & Events** | `ccam-tokens-events` | Cumulative tokens/events, throughput rates, cache efficiency | -| **CCAM — Platform Health** | `ccam-platform` | Scrape/API uptime, process memory, remote sources, build info | - -Each board links to the others in the header. After `monitoring:up`, open -http://localhost:3000/dashboards or land on **Overview** as the home dashboard. - -## What's on the Overview dashboard - -The bundled **CCAM — Overview** board uses only metrics from your database. -**Cumulative stat panels** (total sessions, events, tokens) show substantial -numbers on the first scrape; **rate panels** need a few minutes of scrape -history before lines appear. - -| Section | Example queries | -| --- | --- | -| Live fleet | `ccam_sessions{status="active"}`, `ccam_agents{status="working"}`, `ccam_websocket_clients`, `sum(ccam_sessions)` | -| Database totals | `ccam_sessions{status="completed"}`, `ccam_events_total`, `ccam_tokens_total`, `sum(ccam_tokens_total)` | -| Breakdown | `ccam_sessions` (pie), `ccam_tokens_total` (bar gauge) | -| Over time | `ccam_sessions`, `ccam_events_total`, `ccam_tokens_total` | -| Rates | `rate(ccam_events_total[5m])`, `rate(ccam_tokens_total[5m])` | -| Process | `ccam_process_uptime_seconds`, `ccam_process_resident_memory_bytes`, `ccam_build_info` | - -See [`docs/API.md` → Metrics](../docs/API.md#metrics) for the full metric list. - -## Prometheus quick start - -**Open the CCAM console first** — static HTML that queries Prometheus directly -(Prometheus 3.x compatible; no deprecated console template libraries): - -**http://localhost:9090/consoles/index.html** - -Also reachable from the Prometheus UI menu: **Consoles → index.html**. - -The console runs real PromQL against your scraped metrics: session totals, -cumulative events/tokens, working agents, and drill-down links into the Graph UI. -If the page warns about a missing scrape target, start CCAM on port 4820 and -wait ~15s for the first poll. - -### Graph tab bookmarks - -Open [http://localhost:9090](http://localhost:9090) and paste any of these into -the **Graph** tab (all read live scraped data — never seeded): - -| Query | What it shows | -| --- | --- | -| `sum(ccam_sessions)` | Total sessions across all statuses | -| `ccam_events_total` | Cumulative hook events in the database | -| `sum(ccam_tokens_total)` | All token kinds combined | -| `ccam:sessions:total` | Same as above via [recording rule](./prometheus/ccam-rules.yml) | -| `ccam:events:rate5m` | Events per second (5m window) | - -Pre-filled graph links (bookmark these): - -- [Total sessions](http://localhost:9090/graph?g0.expr=sum(ccam_sessions)&g0.tab=0) -- [Total events](http://localhost:9090/graph?g0.expr=ccam_events_total&g0.tab=0) -- [Token totals by kind](http://localhost:9090/graph?g0.expr=ccam_tokens_total&g0.tab=0) - -## Configuration - -- **npm-managed stack.** Scrape target lives in - [`prometheus/prometheus-native.yml`](./prometheus/prometheus-native.yml) - (default `127.0.0.1:4820`). Binary versions are pinned in - [`scripts/paths.js`](./scripts/paths.js). -- **Docker stack.** Host-native dashboard: [`prometheus/prometheus.yml`](./prometheus/prometheus.yml) - (`host.docker.internal:4820`). All-Docker stack: [`prometheus/prometheus-docker.yml`](./prometheus/prometheus-docker.yml) - (`agent-monitor:4820`, used by `docker-compose.full.yml`). -- **Auth (`DASHBOARD_TOKEN`).** If the server requires a token, - uncomment the `authorization` block in the relevant prometheus config and set - `credentials` to your `DASHBOARD_TOKEN`. (The `DASHBOARD_ALLOWED_HOSTS` step is - still required for Docker scrapes — the Host guard runs independently of the - token.) -- **Scrape interval** lives in the prometheus config (`global.scrape_interval`). -- **Grafana admin password** — defaults to `admin` / `admin` via - [`grafana.defaults.env`](./grafana.defaults.env) (Docker) and `grafanaAdminEnv()` - in [`scripts/paths.js`](./scripts/paths.js) (npm). Applied on **first start** - only; wipe `monitoring/.data/grafana` or the `grafana-data` volume to re-seed. - -## Security note - -`/api/metrics` exposes aggregate operational counts (session/agent tallies, event -and token totals, uptime) — no prompts, transcripts, costs, or secrets. It sits -behind the same loopback/Host guard and optional `DASHBOARD_TOKEN` as the rest of -the API; scraping is only possible once you explicitly allow the scraper's Host -(and token, if set). Keep Grafana/Prometheus on a trusted network or behind your -own reverse proxy. diff --git a/monitoring/docker-compose.yml b/monitoring/docker-compose.yml deleted file mode 100644 index 5552d62..0000000 --- a/monitoring/docker-compose.yml +++ /dev/null @@ -1,48 +0,0 @@ -# Turnkey Prometheus + Grafana stack for the CCAM `/api/metrics` endpoint. -# -# cd monitoring && docker compose up -d -# open http://localhost:3000 (admin / admin) -# -# The "CCAM — Overview" dashboard and the Prometheus datasource are -# pre-provisioned, so there is nothing to click after start. Prometheus scrapes -# the dashboard running on the HOST (a plain `npm start` on :4820) via -# host.docker.internal — see ./prometheus/prometheus.yml to change the target -# or add a bearer token when DASHBOARD_TOKEN is set. See ./README.md. -services: - prometheus: - image: prom/prometheus:latest - container_name: ccam-prometheus - restart: unless-stopped - command: - - --config.file=/etc/prometheus/prometheus.yml - - --storage.tsdb.path=/prometheus - - --web.enable-lifecycle - - --web.console.templates=/etc/prometheus/consoles - volumes: - - ./prometheus:/etc/prometheus:ro - - prometheus-data:/prometheus - ports: - - "127.0.0.1:9090:9090" - # Lets the container reach a CCAM server running on the host (Linux needs the - # explicit host-gateway mapping; Docker Desktop resolves it natively). - extra_hosts: - - "host.docker.internal:host-gateway" - - grafana: - image: grafana/grafana:latest - container_name: ccam-grafana - restart: unless-stopped - depends_on: - - prometheus - env_file: - - ./grafana.defaults.env - volumes: - - ./grafana/provisioning:/etc/grafana/provisioning:ro - - ./grafana/dashboards:/var/lib/grafana/dashboards:ro - - grafana-data:/var/lib/grafana - ports: - - "127.0.0.1:3000:3000" - -volumes: - prometheus-data: - grafana-data: diff --git a/monitoring/grafana.defaults.env b/monitoring/grafana.defaults.env deleted file mode 100644 index 9c2aa01..0000000 --- a/monitoring/grafana.defaults.env +++ /dev/null @@ -1,6 +0,0 @@ -# Default local Grafana credentials — applied on first start (npm or Docker). -# Shared by monitoring/docker-compose.yml, docker-compose.full.yml, and start.js. -GF_SECURITY_ADMIN_USER=admin -GF_SECURITY_ADMIN_PASSWORD=admin -GF_USERS_ALLOW_SIGN_UP=false -GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_UID=ccam-overview diff --git a/monitoring/grafana/dashboards/ccam-overview.json b/monitoring/grafana/dashboards/ccam-overview.json deleted file mode 100644 index d27042e..0000000 --- a/monitoring/grafana/dashboards/ccam-overview.json +++ /dev/null @@ -1,855 +0,0 @@ -{ - "uid": "ccam-overview", - "title": "CCAM — Overview", - "description": "Live metrics scraped from GET /api/metrics on each 15s interval. Every value is read from your SQLite database at scrape time — no sample or synthetic data.", - "tags": ["ccam", "claude-code"], - "timezone": "browser", - "schemaVersion": 39, - "version": 2, - "editable": true, - "refresh": "15s", - "time": { "from": "now-24h", "to": "now" }, - "links": [ - { - "title": "Sessions & Agents", - "type": "link", - "url": "/d/ccam-sessions-agents/ccam-sessions-agents", - "keepTime": true - }, - { - "title": "Tokens & Events", - "type": "link", - "url": "/d/ccam-tokens-events/ccam-tokens-events", - "keepTime": true - }, - { - "title": "Platform Health", - "type": "link", - "url": "/d/ccam-platform/ccam-platform", - "keepTime": true - }, - { - "title": "Prometheus — total sessions", - "type": "link", - "icon": "external link", - "url": "http://localhost:9090/graph?g0.expr=sum(ccam_sessions)&g0.tab=0", - "targetBlank": true, - "keepTime": true - }, - { - "title": "Prometheus — event total", - "type": "link", - "icon": "external link", - "url": "http://localhost:9090/graph?g0.expr=ccam_events_total&g0.tab=0", - "targetBlank": true, - "keepTime": true - } - ], - "templating": { "list": [] }, - "annotations": { "list": [] }, - "panels": [ - { - "id": 100, - "type": "row", - "title": "Live fleet", - "collapsed": false, - "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 } - }, - { - "id": 1, - "type": "stat", - "title": "Active sessions", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 4, "w": 4, "x": 0, "y": 1 }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, - "colorMode": "value", - "graphMode": "area", - "textMode": "auto", - "justifyMode": "auto" - }, - "fieldConfig": { - "defaults": { - "unit": "none", - "color": { "mode": "thresholds" }, - "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] } - }, - "overrides": [] - }, - "targets": [ - { - "refId": "A", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "expr": "ccam_sessions{status=\"active\"}", - "legendFormat": "active" - } - ] - }, - { - "id": 2, - "type": "stat", - "title": "Working agents", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 4, "w": 4, "x": 4, "y": 1 }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, - "colorMode": "value", - "graphMode": "area", - "textMode": "auto", - "justifyMode": "auto" - }, - "fieldConfig": { - "defaults": { - "unit": "none", - "color": { "mode": "thresholds" }, - "thresholds": { "mode": "absolute", "steps": [{ "color": "blue", "value": null }] } - }, - "overrides": [] - }, - "targets": [ - { - "refId": "A", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "expr": "ccam_agents{status=\"working\"}", - "legendFormat": "working" - } - ] - }, - { - "id": 3, - "type": "stat", - "title": "Realtime clients", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 4, "w": 4, "x": 8, "y": 1 }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, - "colorMode": "value", - "graphMode": "area", - "textMode": "auto", - "justifyMode": "auto" - }, - "fieldConfig": { - "defaults": { - "unit": "none", - "color": { "mode": "thresholds" }, - "thresholds": { "mode": "absolute", "steps": [{ "color": "purple", "value": null }] } - }, - "overrides": [] - }, - "targets": [ - { - "refId": "A", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "expr": "ccam_websocket_clients", - "legendFormat": "clients" - } - ] - }, - { - "id": 4, - "type": "stat", - "title": "Enabled remote sources", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 4, "w": 4, "x": 12, "y": 1 }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, - "colorMode": "value", - "graphMode": "none", - "textMode": "auto", - "justifyMode": "auto" - }, - "fieldConfig": { - "defaults": { - "unit": "none", - "color": { "mode": "thresholds" }, - "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] } - }, - "overrides": [] - }, - "targets": [ - { - "refId": "A", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "expr": "ccam_remote_sources{enabled=\"true\"}", - "legendFormat": "enabled" - } - ] - }, - { - "id": 12, - "type": "stat", - "title": "Scrape healthy", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 4, "w": 4, "x": 16, "y": 1 }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, - "colorMode": "background", - "graphMode": "none", - "textMode": "value", - "justifyMode": "center" - }, - "fieldConfig": { - "defaults": { - "mappings": [ - { - "type": "value", - "options": { - "0": { "text": "DOWN", "color": "red" }, - "1": { "text": "UP", "color": "green" } - } - } - ], - "thresholds": { - "mode": "absolute", - "steps": [ - { "color": "red", "value": null }, - { "color": "green", "value": 1 } - ] - } - }, - "overrides": [] - }, - "targets": [ - { - "refId": "A", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "expr": "ccam_up", - "legendFormat": "up" - } - ] - }, - { - "id": 13, - "type": "stat", - "title": "Total sessions", - "description": "Sum of all session statuses in the database (active + completed + error + abandoned).", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 4, "w": 4, "x": 20, "y": 1 }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, - "colorMode": "value", - "graphMode": "area", - "textMode": "auto", - "justifyMode": "auto" - }, - "fieldConfig": { - "defaults": { - "unit": "none", - "decimals": 0, - "color": { "mode": "thresholds" }, - "thresholds": { "mode": "absolute", "steps": [{ "color": "orange", "value": null }] } - }, - "overrides": [] - }, - "targets": [ - { - "refId": "A", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "expr": "sum(ccam_sessions)", - "legendFormat": "total" - } - ] - }, - { - "id": 101, - "type": "row", - "title": "Database totals (cumulative — from your SQLite history)", - "collapsed": false, - "gridPos": { "h": 1, "w": 24, "x": 0, "y": 5 } - }, - { - "id": 14, - "type": "stat", - "title": "Completed sessions", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 4, "w": 4, "x": 0, "y": 6 }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, - "colorMode": "value", - "graphMode": "area", - "textMode": "auto", - "justifyMode": "auto" - }, - "fieldConfig": { - "defaults": { - "unit": "none", - "decimals": 0, - "color": { "mode": "thresholds" }, - "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] } - }, - "overrides": [] - }, - "targets": [ - { - "refId": "A", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "expr": "ccam_sessions{status=\"completed\"}", - "legendFormat": "completed" - } - ] - }, - { - "id": 15, - "type": "stat", - "title": "Error sessions", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 4, "w": 4, "x": 4, "y": 6 }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, - "colorMode": "value", - "graphMode": "area", - "textMode": "auto", - "justifyMode": "auto" - }, - "fieldConfig": { - "defaults": { - "unit": "none", - "decimals": 0, - "color": { "mode": "thresholds" }, - "thresholds": { "mode": "absolute", "steps": [{ "color": "red", "value": null }] } - }, - "overrides": [] - }, - "targets": [ - { - "refId": "A", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "expr": "ccam_sessions{status=\"error\"}", - "legendFormat": "error" - } - ] - }, - { - "id": 16, - "type": "stat", - "title": "Total events", - "description": "Monotonic counter since the database was created.", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 4, "w": 4, "x": 8, "y": 6 }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, - "colorMode": "value", - "graphMode": "area", - "textMode": "auto", - "justifyMode": "auto" - }, - "fieldConfig": { - "defaults": { - "unit": "short", - "decimals": 0, - "color": { "mode": "thresholds" }, - "thresholds": { "mode": "absolute", "steps": [{ "color": "blue", "value": null }] } - }, - "overrides": [] - }, - "targets": [ - { - "refId": "A", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "expr": "ccam_events_total", - "legendFormat": "events" - } - ] - }, - { - "id": 17, - "type": "stat", - "title": "Input tokens", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 4, "w": 4, "x": 12, "y": 6 }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, - "colorMode": "value", - "graphMode": "area", - "textMode": "auto", - "justifyMode": "auto" - }, - "fieldConfig": { - "defaults": { - "unit": "short", - "decimals": 1, - "color": { "mode": "thresholds" }, - "thresholds": { "mode": "absolute", "steps": [{ "color": "yellow", "value": null }] } - }, - "overrides": [] - }, - "targets": [ - { - "refId": "A", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "expr": "ccam_tokens_total{kind=\"input\"}", - "legendFormat": "input" - } - ] - }, - { - "id": 18, - "type": "stat", - "title": "Output tokens", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 4, "w": 4, "x": 16, "y": 6 }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, - "colorMode": "value", - "graphMode": "area", - "textMode": "auto", - "justifyMode": "auto" - }, - "fieldConfig": { - "defaults": { - "unit": "short", - "decimals": 1, - "color": { "mode": "thresholds" }, - "thresholds": { "mode": "absolute", "steps": [{ "color": "orange", "value": null }] } - }, - "overrides": [] - }, - "targets": [ - { - "refId": "A", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "expr": "ccam_tokens_total{kind=\"output\"}", - "legendFormat": "output" - } - ] - }, - { - "id": 19, - "type": "stat", - "title": "All tokens (sum)", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 4, "w": 4, "x": 20, "y": 6 }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, - "colorMode": "value", - "graphMode": "area", - "textMode": "auto", - "justifyMode": "auto" - }, - "fieldConfig": { - "defaults": { - "unit": "short", - "decimals": 1, - "color": { "mode": "thresholds" }, - "thresholds": { "mode": "absolute", "steps": [{ "color": "purple", "value": null }] } - }, - "overrides": [] - }, - "targets": [ - { - "refId": "A", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "expr": "sum(ccam_tokens_total)", - "legendFormat": "tokens" - } - ] - }, - { - "id": 102, - "type": "row", - "title": "Breakdown", - "collapsed": false, - "gridPos": { "h": 1, "w": 24, "x": 0, "y": 10 } - }, - { - "id": 20, - "type": "piechart", - "title": "Sessions by status", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 8, "w": 8, "x": 0, "y": 11 }, - "options": { - "legend": { "displayMode": "table", "placement": "right", "values": ["value", "percent"] }, - "pieType": "donut", - "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, - "tooltip": { "mode": "single" } - }, - "fieldConfig": { - "defaults": { "unit": "none", "decimals": 0 }, - "overrides": [] - }, - "targets": [ - { - "refId": "A", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "expr": "ccam_sessions", - "legendFormat": "{{status}}", - "instant": true - } - ] - }, - { - "id": 21, - "type": "bargauge", - "title": "Token totals by kind", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 8, "w": 8, "x": 8, "y": 11 }, - "options": { - "orientation": "horizontal", - "displayMode": "gradient", - "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, - "showUnfilled": true - }, - "fieldConfig": { - "defaults": { - "unit": "short", - "decimals": 1, - "color": { "mode": "palette-classic" }, - "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] } - }, - "overrides": [] - }, - "targets": [ - { - "refId": "A", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "expr": "ccam_tokens_total", - "legendFormat": "{{kind}}", - "instant": true - } - ] - }, - { - "id": 22, - "type": "bargauge", - "title": "Agents by status", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 8, "w": 8, "x": 16, "y": 11 }, - "options": { - "orientation": "horizontal", - "displayMode": "gradient", - "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, - "showUnfilled": true - }, - "fieldConfig": { - "defaults": { - "unit": "none", - "decimals": 0, - "color": { "mode": "palette-classic" }, - "thresholds": { "mode": "absolute", "steps": [{ "color": "blue", "value": null }] } - }, - "overrides": [] - }, - "targets": [ - { - "refId": "A", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "expr": "ccam_agents", - "legendFormat": "{{status}}", - "instant": true - } - ] - }, - { - "id": 103, - "type": "row", - "title": "Over time", - "collapsed": false, - "gridPos": { "h": 1, "w": 24, "x": 0, "y": 19 } - }, - { - "id": 5, - "type": "timeseries", - "title": "Sessions by status", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 8, "w": 12, "x": 0, "y": 20 }, - "options": { - "legend": { "displayMode": "list", "placement": "bottom", "calcs": [] }, - "tooltip": { "mode": "multi", "sort": "desc" } - }, - "fieldConfig": { - "defaults": { - "unit": "none", - "color": { "mode": "palette-classic" }, - "custom": { - "drawStyle": "line", - "lineWidth": 2, - "fillOpacity": 10, - "showPoints": "never", - "stacking": { "mode": "none", "group": "A" } - } - }, - "overrides": [] - }, - "targets": [ - { - "refId": "A", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "expr": "ccam_sessions", - "legendFormat": "{{status}}" - } - ] - }, - { - "id": 6, - "type": "timeseries", - "title": "Agents by status", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 8, "w": 12, "x": 12, "y": 20 }, - "options": { - "legend": { "displayMode": "list", "placement": "bottom", "calcs": [] }, - "tooltip": { "mode": "multi", "sort": "desc" } - }, - "fieldConfig": { - "defaults": { - "unit": "none", - "color": { "mode": "palette-classic" }, - "custom": { - "drawStyle": "line", - "lineWidth": 2, - "fillOpacity": 10, - "showPoints": "never", - "stacking": { "mode": "none", "group": "A" } - } - }, - "overrides": [] - }, - "targets": [ - { - "refId": "A", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "expr": "ccam_agents", - "legendFormat": "{{status}}" - } - ] - }, - { - "id": 23, - "type": "timeseries", - "title": "Cumulative events (database total)", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 8, "w": 12, "x": 0, "y": 28 }, - "options": { - "legend": { "displayMode": "list", "placement": "bottom", "calcs": ["lastNotNull"] }, - "tooltip": { "mode": "single", "sort": "none" } - }, - "fieldConfig": { - "defaults": { - "unit": "short", - "decimals": 0, - "color": { "mode": "palette-classic" }, - "custom": { - "drawStyle": "line", - "lineWidth": 2, - "fillOpacity": 20, - "showPoints": "never", - "stacking": { "mode": "none", "group": "A" } - } - }, - "overrides": [] - }, - "targets": [ - { - "refId": "A", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "expr": "ccam_events_total", - "legendFormat": "events" - } - ] - }, - { - "id": 24, - "type": "timeseries", - "title": "Cumulative tokens by kind", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 8, "w": 12, "x": 12, "y": 28 }, - "options": { - "legend": { "displayMode": "list", "placement": "bottom", "calcs": ["lastNotNull"] }, - "tooltip": { "mode": "multi", "sort": "desc" } - }, - "fieldConfig": { - "defaults": { - "unit": "short", - "color": { "mode": "palette-classic" }, - "custom": { - "drawStyle": "line", - "lineWidth": 2, - "fillOpacity": 10, - "showPoints": "never", - "stacking": { "mode": "none", "group": "A" } - } - }, - "overrides": [] - }, - "targets": [ - { - "refId": "A", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "expr": "ccam_tokens_total", - "legendFormat": "{{kind}}" - } - ] - }, - { - "id": 104, - "type": "row", - "title": "Rates (need a few scrape intervals)", - "collapsed": false, - "gridPos": { "h": 1, "w": 24, "x": 0, "y": 36 } - }, - { - "id": 7, - "type": "timeseries", - "title": "Event throughput (events/sec)", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 8, "w": 12, "x": 0, "y": 37 }, - "options": { - "legend": { "displayMode": "list", "placement": "bottom", "calcs": [] }, - "tooltip": { "mode": "single", "sort": "none" } - }, - "fieldConfig": { - "defaults": { - "unit": "ops", - "color": { "mode": "palette-classic" }, - "custom": { - "drawStyle": "line", - "lineWidth": 2, - "fillOpacity": 15, - "showPoints": "never" - } - }, - "overrides": [] - }, - "targets": [ - { - "refId": "A", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "expr": "rate(ccam_events_total[5m])", - "legendFormat": "events/s" - } - ] - }, - { - "id": 8, - "type": "timeseries", - "title": "Token usage rate by kind (tokens/sec)", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 8, "w": 12, "x": 12, "y": 37 }, - "options": { - "legend": { "displayMode": "list", "placement": "bottom", "calcs": [] }, - "tooltip": { "mode": "multi", "sort": "desc" } - }, - "fieldConfig": { - "defaults": { - "unit": "none", - "color": { "mode": "palette-classic" }, - "custom": { - "drawStyle": "line", - "lineWidth": 2, - "fillOpacity": 10, - "showPoints": "never" - } - }, - "overrides": [] - }, - "targets": [ - { - "refId": "A", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "expr": "rate(ccam_tokens_total[5m])", - "legendFormat": "{{kind}}" - } - ] - }, - { - "id": 105, - "type": "row", - "title": "Process", - "collapsed": false, - "gridPos": { "h": 1, "w": 24, "x": 0, "y": 45 } - }, - { - "id": 9, - "type": "stat", - "title": "Server uptime", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 6, "w": 6, "x": 0, "y": 46 }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, - "colorMode": "value", - "graphMode": "none", - "textMode": "auto", - "justifyMode": "auto" - }, - "fieldConfig": { - "defaults": { - "unit": "s", - "color": { "mode": "thresholds" }, - "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] } - }, - "overrides": [] - }, - "targets": [ - { - "refId": "A", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "expr": "ccam_process_uptime_seconds", - "legendFormat": "uptime" - } - ] - }, - { - "id": 10, - "type": "stat", - "title": "Server memory (RSS)", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 6, "w": 6, "x": 6, "y": 46 }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, - "colorMode": "value", - "graphMode": "area", - "textMode": "auto", - "justifyMode": "auto" - }, - "fieldConfig": { - "defaults": { - "unit": "bytes", - "color": { "mode": "thresholds" }, - "thresholds": { "mode": "absolute", "steps": [{ "color": "blue", "value": null }] } - }, - "overrides": [] - }, - "targets": [ - { - "refId": "A", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "expr": "ccam_process_resident_memory_bytes", - "legendFormat": "rss" - } - ] - }, - { - "id": 11, - "type": "table", - "title": "Build info", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 6, "w": 12, "x": 12, "y": 46 }, - "options": { "showHeader": true }, - "fieldConfig": { - "defaults": {}, - "overrides": [ - { - "matcher": { "id": "byName", "options": "Value" }, - "properties": [{ "id": "custom.hidden", "value": true }] - }, - { - "matcher": { "id": "byName", "options": "Time" }, - "properties": [{ "id": "custom.hidden", "value": true }] - }, - { - "matcher": { "id": "byName", "options": "job" }, - "properties": [{ "id": "custom.hidden", "value": true }] - } - ] - }, - "targets": [ - { - "refId": "A", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "expr": "ccam_build_info", - "format": "table", - "instant": true - } - ] - } - ] -} diff --git a/monitoring/grafana/dashboards/ccam-platform.json b/monitoring/grafana/dashboards/ccam-platform.json deleted file mode 100644 index 6f20b6b..0000000 --- a/monitoring/grafana/dashboards/ccam-platform.json +++ /dev/null @@ -1,401 +0,0 @@ -{ - "uid": "ccam-platform", - "title": "CCAM — Platform Health", - "description": "Scrape health, process resources, remote sources, and build info from live metrics.", - "tags": ["ccam", "claude-code", "platform", "health"], - "timezone": "browser", - "schemaVersion": 39, - "version": 1, - "editable": true, - "refresh": "15s", - "time": { "from": "now-24h", "to": "now" }, - "links": [ - { - "title": "Overview", - "type": "link", - "url": "/d/ccam-overview/ccam-overview", - "keepTime": true - }, - { - "title": "Sessions & Agents", - "type": "link", - "url": "/d/ccam-sessions-agents/ccam-sessions-agents", - "keepTime": true - }, - { - "title": "Tokens & Events", - "type": "link", - "url": "/d/ccam-tokens-events/ccam-tokens-events", - "keepTime": true - } - ], - "templating": { "list": [] }, - "annotations": { "list": [] }, - "panels": [ - { - "id": 100, - "type": "row", - "title": "Availability", - "collapsed": false, - "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 } - }, - { - "id": 1, - "type": "stat", - "title": "Prometheus scrape", - "description": "up{job=\"ccam\"} — 1 when Prometheus successfully scrapes /api/metrics.", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 5, "w": 6, "x": 0, "y": 1 }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"] }, - "colorMode": "background", - "graphMode": "none", - "textMode": "value" - }, - "fieldConfig": { - "defaults": { - "mappings": [ - { - "type": "value", - "options": { - "0": { "text": "DOWN", "color": "red" }, - "1": { "text": "UP", "color": "green" } - } - } - ], - "thresholds": { - "steps": [ - { "color": "red", "value": null }, - { "color": "green", "value": 1 } - ] - } - } - }, - "targets": [{ "refId": "A", "expr": "up{job=\"ccam\"}", "legendFormat": "scrape" }] - }, - { - "id": 2, - "type": "stat", - "title": "CCAM API", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 5, "w": 6, "x": 6, "y": 1 }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"] }, - "colorMode": "background", - "graphMode": "none", - "textMode": "value" - }, - "fieldConfig": { - "defaults": { - "mappings": [ - { - "type": "value", - "options": { - "0": { "text": "DOWN", "color": "red" }, - "1": { "text": "UP", "color": "green" } - } - } - ], - "thresholds": { - "steps": [ - { "color": "red", "value": null }, - { "color": "green", "value": 1 } - ] - } - } - }, - "targets": [{ "refId": "A", "expr": "ccam_up", "legendFormat": "api" }] - }, - { - "id": 3, - "type": "stat", - "title": "Server uptime", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 5, "w": 6, "x": 12, "y": 1 }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"] }, - "colorMode": "value", - "graphMode": "area" - }, - "fieldConfig": { - "defaults": { - "unit": "s", - "thresholds": { "steps": [{ "color": "green", "value": null }] } - } - }, - "targets": [{ "refId": "A", "expr": "ccam_process_uptime_seconds", "legendFormat": "uptime" }] - }, - { - "id": 4, - "type": "stat", - "title": "Memory (RSS)", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 5, "w": 6, "x": 18, "y": 1 }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"] }, - "colorMode": "value", - "graphMode": "area" - }, - "fieldConfig": { - "defaults": { - "unit": "bytes", - "thresholds": { "steps": [{ "color": "blue", "value": null }] } - } - }, - "targets": [ - { "refId": "A", "expr": "ccam_process_resident_memory_bytes", "legendFormat": "rss" } - ] - }, - { - "id": 5, - "type": "timeseries", - "title": "Scrape health over time", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 7, "w": 12, "x": 0, "y": 6 }, - "options": { "legend": { "displayMode": "list", "placement": "bottom" } }, - "fieldConfig": { - "defaults": { - "min": 0, - "max": 1, - "custom": { - "drawStyle": "line", - "lineWidth": 2, - "fillOpacity": 30, - "showPoints": "never", - "spanNulls": false - } - }, - "overrides": [ - { - "matcher": { "id": "byName", "options": "scrape" }, - "properties": [{ "id": "color", "value": { "fixedColor": "green", "mode": "fixed" } }] - }, - { - "matcher": { "id": "byName", "options": "api" }, - "properties": [{ "id": "color", "value": { "fixedColor": "blue", "mode": "fixed" } }] - } - ] - }, - "targets": [ - { "refId": "A", "expr": "up{job=\"ccam\"}", "legendFormat": "scrape" }, - { "refId": "B", "expr": "ccam_up", "legendFormat": "api" } - ] - }, - { - "id": 6, - "type": "timeseries", - "title": "Process memory (RSS)", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 7, "w": 12, "x": 12, "y": 6 }, - "options": { "legend": { "displayMode": "list", "placement": "bottom" } }, - "fieldConfig": { - "defaults": { - "unit": "bytes", - "custom": { - "drawStyle": "line", - "lineWidth": 2, - "fillOpacity": 20, - "showPoints": "never" - }, - "color": { "mode": "fixed", "fixedColor": "blue" } - } - }, - "targets": [ - { "refId": "A", "expr": "ccam_process_resident_memory_bytes", "legendFormat": "rss" } - ] - }, - { - "id": 101, - "type": "row", - "title": "Realtime & federation", - "collapsed": false, - "gridPos": { "h": 1, "w": 24, "x": 0, "y": 13 } - }, - { - "id": 7, - "type": "stat", - "title": "WebSocket clients", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 4, "w": 6, "x": 0, "y": 14 }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"] }, - "colorMode": "value", - "graphMode": "area" - }, - "fieldConfig": { - "defaults": { - "decimals": 0, - "thresholds": { "steps": [{ "color": "purple", "value": null }] } - } - }, - "targets": [{ "refId": "A", "expr": "ccam_websocket_clients", "legendFormat": "clients" }] - }, - { - "id": 8, - "type": "stat", - "title": "Remote sources (enabled)", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 4, "w": 6, "x": 6, "y": 14 }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"] }, - "colorMode": "value", - "graphMode": "none" - }, - "fieldConfig": { - "defaults": { - "decimals": 0, - "thresholds": { "steps": [{ "color": "green", "value": null }] } - } - }, - "targets": [ - { "refId": "A", "expr": "ccam_remote_sources{enabled=\"true\"}", "legendFormat": "enabled" } - ] - }, - { - "id": 9, - "type": "stat", - "title": "Remote sources (disabled)", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 4, "w": 6, "x": 12, "y": 14 }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"] }, - "colorMode": "value", - "graphMode": "none" - }, - "fieldConfig": { - "defaults": { - "decimals": 0, - "thresholds": { "steps": [{ "color": "orange", "value": null }] } - } - }, - "targets": [ - { - "refId": "A", - "expr": "ccam_remote_sources{enabled=\"false\"}", - "legendFormat": "disabled" - } - ] - }, - { - "id": 10, - "type": "stat", - "title": "Hook activity (events/s)", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 4, "w": 6, "x": 18, "y": 14 }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"] }, - "colorMode": "value", - "graphMode": "area" - }, - "fieldConfig": { - "defaults": { - "unit": "ops", - "decimals": 3, - "thresholds": { "steps": [{ "color": "blue", "value": null }] } - } - }, - "targets": [ - { "refId": "A", "expr": "rate(ccam_events_total[5m])", "legendFormat": "events/s" } - ] - }, - { - "id": 11, - "type": "timeseries", - "title": "WebSocket clients over time", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 7, "w": 12, "x": 0, "y": 18 }, - "options": { "legend": { "displayMode": "list", "placement": "bottom" } }, - "fieldConfig": { - "defaults": { - "custom": { - "drawStyle": "line", - "lineWidth": 2, - "fillOpacity": 25, - "showPoints": "never" - }, - "color": { "mode": "fixed", "fixedColor": "purple" } - } - }, - "targets": [{ "refId": "A", "expr": "ccam_websocket_clients", "legendFormat": "clients" }] - }, - { - "id": 12, - "type": "timeseries", - "title": "Remote data sources", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 7, "w": 12, "x": 12, "y": 18 }, - "options": { "legend": { "displayMode": "list", "placement": "bottom" } }, - "fieldConfig": { - "defaults": { - "custom": { - "drawStyle": "line", - "lineWidth": 2, - "fillOpacity": 15, - "showPoints": "never" - }, - "color": { "mode": "palette-classic" } - } - }, - "targets": [ - { "refId": "A", "expr": "ccam_remote_sources", "legendFormat": "enabled={{enabled}}" } - ] - }, - { - "id": 102, - "type": "row", - "title": "Build", - "collapsed": false, - "gridPos": { "h": 1, "w": 24, "x": 0, "y": 25 } - }, - { - "id": 13, - "type": "table", - "title": "CCAM build info", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 6, "w": 12, "x": 0, "y": 26 }, - "options": { "showHeader": true }, - "fieldConfig": { - "defaults": {}, - "overrides": [ - { - "matcher": { "id": "byName", "options": "Value" }, - "properties": [{ "id": "custom.hidden", "value": true }] - }, - { - "matcher": { "id": "byName", "options": "Time" }, - "properties": [{ "id": "custom.hidden", "value": true }] - }, - { - "matcher": { "id": "byName", "options": "job" }, - "properties": [{ "id": "custom.hidden", "value": true }] - }, - { - "matcher": { "id": "byName", "options": "instance" }, - "properties": [{ "id": "custom.hidden", "value": true }] - } - ] - }, - "targets": [{ "refId": "A", "expr": "ccam_build_info", "format": "table", "instant": true }] - }, - { - "id": 14, - "type": "timeseries", - "title": "Server uptime", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 6, "w": 12, "x": 12, "y": 26 }, - "options": { "legend": { "displayMode": "list", "placement": "bottom" } }, - "fieldConfig": { - "defaults": { - "unit": "s", - "custom": { - "drawStyle": "line", - "lineWidth": 2, - "fillOpacity": 10, - "showPoints": "never" - } - } - }, - "targets": [{ "refId": "A", "expr": "ccam_process_uptime_seconds", "legendFormat": "uptime" }] - } - ] -} diff --git a/monitoring/grafana/dashboards/ccam-sessions-agents.json b/monitoring/grafana/dashboards/ccam-sessions-agents.json deleted file mode 100644 index 962a6f3..0000000 --- a/monitoring/grafana/dashboards/ccam-sessions-agents.json +++ /dev/null @@ -1,371 +0,0 @@ -{ - "uid": "ccam-sessions-agents", - "title": "CCAM — Sessions & Agents", - "description": "Session lifecycle and agent fleet metrics from live /api/metrics scrapes.", - "tags": ["ccam", "claude-code", "sessions", "agents"], - "timezone": "browser", - "schemaVersion": 39, - "version": 1, - "editable": true, - "refresh": "15s", - "time": { "from": "now-24h", "to": "now" }, - "links": [ - { - "title": "Overview", - "type": "link", - "url": "/d/ccam-overview/ccam-overview", - "keepTime": true - }, - { - "title": "Tokens & Events", - "type": "link", - "url": "/d/ccam-tokens-events/ccam-tokens-events", - "keepTime": true - }, - { - "title": "Platform Health", - "type": "link", - "url": "/d/ccam-platform/ccam-platform", - "keepTime": true - } - ], - "templating": { "list": [] }, - "annotations": { "list": [] }, - "panels": [ - { - "id": 100, - "type": "row", - "title": "Sessions", - "collapsed": false, - "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 } - }, - { - "id": 1, - "type": "stat", - "title": "Active", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 4, "w": 4, "x": 0, "y": 1 }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"] }, - "colorMode": "value", - "graphMode": "area" - }, - "fieldConfig": { - "defaults": { - "decimals": 0, - "color": { "mode": "thresholds" }, - "thresholds": { "steps": [{ "color": "green", "value": null }] } - } - }, - "targets": [ - { "refId": "A", "expr": "ccam_sessions{status=\"active\"}", "legendFormat": "active" } - ] - }, - { - "id": 2, - "type": "stat", - "title": "Completed", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 4, "w": 4, "x": 4, "y": 1 }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"] }, - "colorMode": "value", - "graphMode": "area" - }, - "fieldConfig": { - "defaults": { - "decimals": 0, - "color": { "mode": "thresholds" }, - "thresholds": { "steps": [{ "color": "blue", "value": null }] } - } - }, - "targets": [ - { "refId": "A", "expr": "ccam_sessions{status=\"completed\"}", "legendFormat": "completed" } - ] - }, - { - "id": 3, - "type": "stat", - "title": "Error", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 4, "w": 4, "x": 8, "y": 1 }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"] }, - "colorMode": "value", - "graphMode": "area" - }, - "fieldConfig": { - "defaults": { - "decimals": 0, - "color": { "mode": "thresholds" }, - "thresholds": { "steps": [{ "color": "red", "value": null }] } - } - }, - "targets": [ - { "refId": "A", "expr": "ccam_sessions{status=\"error\"}", "legendFormat": "error" } - ] - }, - { - "id": 4, - "type": "stat", - "title": "Abandoned", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 4, "w": 4, "x": 12, "y": 1 }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"] }, - "colorMode": "value", - "graphMode": "area" - }, - "fieldConfig": { - "defaults": { - "decimals": 0, - "color": { "mode": "thresholds" }, - "thresholds": { "steps": [{ "color": "orange", "value": null }] } - } - }, - "targets": [ - { "refId": "A", "expr": "ccam_sessions{status=\"abandoned\"}", "legendFormat": "abandoned" } - ] - }, - { - "id": 5, - "type": "stat", - "title": "Total sessions", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 4, "w": 4, "x": 16, "y": 1 }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"] }, - "colorMode": "value", - "graphMode": "area" - }, - "fieldConfig": { - "defaults": { - "decimals": 0, - "unit": "none", - "color": { "mode": "thresholds" }, - "thresholds": { "steps": [{ "color": "purple", "value": null }] } - } - }, - "targets": [{ "refId": "A", "expr": "sum(ccam_sessions)", "legendFormat": "total" }] - }, - { - "id": 6, - "type": "stat", - "title": "Active % of total", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 4, "w": 4, "x": 20, "y": 1 }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"] }, - "colorMode": "value", - "graphMode": "none" - }, - "fieldConfig": { - "defaults": { - "unit": "percentunit", - "decimals": 1, - "min": 0, - "max": 1, - "color": { "mode": "thresholds" }, - "thresholds": { "steps": [{ "color": "green", "value": null }] } - } - }, - "targets": [ - { - "refId": "A", - "expr": "ccam_sessions{status=\"active\"} / clamp_min(sum(ccam_sessions), 1)", - "legendFormat": "active ratio" - } - ] - }, - { - "id": 7, - "type": "piechart", - "title": "Sessions by status", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 9, "w": 8, "x": 0, "y": 5 }, - "options": { - "legend": { "displayMode": "table", "placement": "right", "values": ["value", "percent"] }, - "pieType": "donut", - "reduceOptions": { "calcs": ["lastNotNull"] } - }, - "fieldConfig": { "defaults": { "decimals": 0 } }, - "targets": [ - { "refId": "A", "expr": "ccam_sessions", "legendFormat": "{{status}}", "instant": true } - ] - }, - { - "id": 8, - "type": "timeseries", - "title": "Sessions over time", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 9, "w": 16, "x": 8, "y": 5 }, - "options": { - "legend": { "displayMode": "list", "placement": "bottom" }, - "tooltip": { "mode": "multi" } - }, - "fieldConfig": { - "defaults": { - "custom": { - "drawStyle": "line", - "lineWidth": 2, - "fillOpacity": 12, - "showPoints": "never" - }, - "color": { "mode": "palette-classic" } - } - }, - "targets": [{ "refId": "A", "expr": "ccam_sessions", "legendFormat": "{{status}}" }] - }, - { - "id": 101, - "type": "row", - "title": "Agents", - "collapsed": false, - "gridPos": { "h": 1, "w": 24, "x": 0, "y": 14 } - }, - { - "id": 9, - "type": "stat", - "title": "Working", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 4, "w": 6, "x": 0, "y": 15 }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"] }, - "colorMode": "value", - "graphMode": "area" - }, - "fieldConfig": { - "defaults": { - "decimals": 0, - "thresholds": { "steps": [{ "color": "green", "value": null }] } - } - }, - "targets": [ - { "refId": "A", "expr": "ccam_agents{status=\"working\"}", "legendFormat": "working" } - ] - }, - { - "id": 10, - "type": "stat", - "title": "Waiting", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 4, "w": 6, "x": 6, "y": 15 }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"] }, - "colorMode": "value", - "graphMode": "area" - }, - "fieldConfig": { - "defaults": { - "decimals": 0, - "thresholds": { "steps": [{ "color": "yellow", "value": null }] } - } - }, - "targets": [ - { "refId": "A", "expr": "ccam_agents{status=\"waiting\"}", "legendFormat": "waiting" } - ] - }, - { - "id": 11, - "type": "stat", - "title": "Completed", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 4, "w": 6, "x": 12, "y": 15 }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"] }, - "colorMode": "value", - "graphMode": "area" - }, - "fieldConfig": { - "defaults": { - "decimals": 0, - "thresholds": { "steps": [{ "color": "blue", "value": null }] } - } - }, - "targets": [ - { "refId": "A", "expr": "ccam_agents{status=\"completed\"}", "legendFormat": "completed" } - ] - }, - { - "id": 12, - "type": "stat", - "title": "Error", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 4, "w": 6, "x": 18, "y": 15 }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"] }, - "colorMode": "value", - "graphMode": "area" - }, - "fieldConfig": { - "defaults": { - "decimals": 0, - "thresholds": { "steps": [{ "color": "red", "value": null }] } - } - }, - "targets": [ - { "refId": "A", "expr": "ccam_agents{status=\"error\"}", "legendFormat": "error" } - ] - }, - { - "id": 13, - "type": "bargauge", - "title": "Agents by status", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 8, "w": 8, "x": 0, "y": 19 }, - "options": { - "orientation": "horizontal", - "displayMode": "gradient", - "reduceOptions": { "calcs": ["lastNotNull"] } - }, - "fieldConfig": { "defaults": { "decimals": 0, "color": { "mode": "palette-classic" } } }, - "targets": [ - { "refId": "A", "expr": "ccam_agents", "legendFormat": "{{status}}", "instant": true } - ] - }, - { - "id": 14, - "type": "timeseries", - "title": "Agents over time", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 8, "w": 16, "x": 8, "y": 19 }, - "options": { - "legend": { "displayMode": "list", "placement": "bottom" }, - "tooltip": { "mode": "multi" } - }, - "fieldConfig": { - "defaults": { - "custom": { - "drawStyle": "line", - "lineWidth": 2, - "fillOpacity": 10, - "showPoints": "never" - }, - "color": { "mode": "palette-classic" } - } - }, - "targets": [{ "refId": "A", "expr": "ccam_agents", "legendFormat": "{{status}}" }] - }, - { - "id": 15, - "type": "timeseries", - "title": "Realtime dashboard clients", - "description": "WebSocket clients connected to the CCAM UI.", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 7, "w": 24, "x": 0, "y": 27 }, - "options": { "legend": { "displayMode": "list", "placement": "bottom" } }, - "fieldConfig": { - "defaults": { - "custom": { - "drawStyle": "line", - "lineWidth": 2, - "fillOpacity": 20, - "showPoints": "never" - }, - "color": { "mode": "fixed", "fixedColor": "purple" } - } - }, - "targets": [{ "refId": "A", "expr": "ccam_websocket_clients", "legendFormat": "clients" }] - } - ] -} diff --git a/monitoring/grafana/dashboards/ccam-tokens-events.json b/monitoring/grafana/dashboards/ccam-tokens-events.json deleted file mode 100644 index 6003505..0000000 --- a/monitoring/grafana/dashboards/ccam-tokens-events.json +++ /dev/null @@ -1,350 +0,0 @@ -{ - "uid": "ccam-tokens-events", - "title": "CCAM — Tokens & Events", - "description": "Cumulative and rate views of hook events and token usage from live /api/metrics scrapes.", - "tags": ["ccam", "claude-code", "tokens", "events"], - "timezone": "browser", - "schemaVersion": 39, - "version": 1, - "editable": true, - "refresh": "15s", - "time": { "from": "now-24h", "to": "now" }, - "links": [ - { - "title": "Overview", - "type": "link", - "url": "/d/ccam-overview/ccam-overview", - "keepTime": true - }, - { - "title": "Sessions & Agents", - "type": "link", - "url": "/d/ccam-sessions-agents/ccam-sessions-agents", - "keepTime": true - }, - { - "title": "Platform Health", - "type": "link", - "url": "/d/ccam-platform/ccam-platform", - "keepTime": true - } - ], - "templating": { "list": [] }, - "annotations": { "list": [] }, - "panels": [ - { - "id": 100, - "type": "row", - "title": "Cumulative totals", - "collapsed": false, - "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 } - }, - { - "id": 1, - "type": "stat", - "title": "Total events", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 4, "w": 4, "x": 0, "y": 1 }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"] }, - "colorMode": "value", - "graphMode": "area" - }, - "fieldConfig": { - "defaults": { - "unit": "short", - "decimals": 0, - "thresholds": { "steps": [{ "color": "blue", "value": null }] } - } - }, - "targets": [{ "refId": "A", "expr": "ccam_events_total", "legendFormat": "events" }] - }, - { - "id": 2, - "type": "stat", - "title": "Input tokens", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 4, "w": 5, "x": 4, "y": 1 }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"] }, - "colorMode": "value", - "graphMode": "area" - }, - "fieldConfig": { - "defaults": { - "unit": "short", - "decimals": 1, - "thresholds": { "steps": [{ "color": "yellow", "value": null }] } - } - }, - "targets": [ - { "refId": "A", "expr": "ccam_tokens_total{kind=\"input\"}", "legendFormat": "input" } - ] - }, - { - "id": 3, - "type": "stat", - "title": "Output tokens", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 4, "w": 5, "x": 9, "y": 1 }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"] }, - "colorMode": "value", - "graphMode": "area" - }, - "fieldConfig": { - "defaults": { - "unit": "short", - "decimals": 1, - "thresholds": { "steps": [{ "color": "orange", "value": null }] } - } - }, - "targets": [ - { "refId": "A", "expr": "ccam_tokens_total{kind=\"output\"}", "legendFormat": "output" } - ] - }, - { - "id": 4, - "type": "stat", - "title": "Cache read", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 4, "w": 5, "x": 14, "y": 1 }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"] }, - "colorMode": "value", - "graphMode": "area" - }, - "fieldConfig": { - "defaults": { - "unit": "short", - "decimals": 1, - "thresholds": { "steps": [{ "color": "green", "value": null }] } - } - }, - "targets": [ - { - "refId": "A", - "expr": "ccam_tokens_total{kind=\"cache_read\"}", - "legendFormat": "cache_read" - } - ] - }, - { - "id": 5, - "type": "stat", - "title": "Cache write", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 4, "w": 5, "x": 19, "y": 1 }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"] }, - "colorMode": "value", - "graphMode": "area" - }, - "fieldConfig": { - "defaults": { - "unit": "short", - "decimals": 1, - "thresholds": { "steps": [{ "color": "teal", "value": null }] } - } - }, - "targets": [ - { - "refId": "A", - "expr": "ccam_tokens_total{kind=\"cache_write\"}", - "legendFormat": "cache_write" - } - ] - }, - { - "id": 6, - "type": "piechart", - "title": "Token share by kind", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 9, "w": 8, "x": 0, "y": 5 }, - "options": { - "legend": { "displayMode": "table", "placement": "right", "values": ["value", "percent"] }, - "pieType": "pie", - "reduceOptions": { "calcs": ["lastNotNull"] } - }, - "fieldConfig": { "defaults": { "unit": "short" } }, - "targets": [ - { "refId": "A", "expr": "ccam_tokens_total", "legendFormat": "{{kind}}", "instant": true } - ] - }, - { - "id": 7, - "type": "timeseries", - "title": "Cumulative tokens by kind", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 9, "w": 16, "x": 8, "y": 5 }, - "options": { - "legend": { "displayMode": "list", "placement": "bottom" }, - "tooltip": { "mode": "multi" } - }, - "fieldConfig": { - "defaults": { - "unit": "short", - "custom": { - "drawStyle": "line", - "lineWidth": 2, - "fillOpacity": 15, - "showPoints": "never" - }, - "color": { "mode": "palette-classic" } - } - }, - "targets": [{ "refId": "A", "expr": "ccam_tokens_total", "legendFormat": "{{kind}}" }] - }, - { - "id": 8, - "type": "timeseries", - "title": "Cumulative events", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 8, "w": 24, "x": 0, "y": 14 }, - "options": { - "legend": { "displayMode": "list", "placement": "bottom", "calcs": ["lastNotNull"] } - }, - "fieldConfig": { - "defaults": { - "unit": "short", - "decimals": 0, - "custom": { - "drawStyle": "line", - "lineWidth": 2, - "fillOpacity": 25, - "showPoints": "never" - }, - "color": { "mode": "fixed", "fixedColor": "blue" } - } - }, - "targets": [{ "refId": "A", "expr": "ccam_events_total", "legendFormat": "events" }] - }, - { - "id": 101, - "type": "row", - "title": "Throughput rates", - "collapsed": false, - "gridPos": { "h": 1, "w": 24, "x": 0, "y": 22 } - }, - { - "id": 9, - "type": "timeseries", - "title": "Events per second (5m rate)", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 8, "w": 12, "x": 0, "y": 23 }, - "options": { "legend": { "displayMode": "list", "placement": "bottom" } }, - "fieldConfig": { - "defaults": { - "unit": "ops", - "custom": { - "drawStyle": "line", - "lineWidth": 2, - "fillOpacity": 20, - "showPoints": "never" - }, - "color": { "mode": "fixed", "fixedColor": "blue" } - } - }, - "targets": [ - { "refId": "A", "expr": "rate(ccam_events_total[5m])", "legendFormat": "events/s" }, - { "refId": "B", "expr": "ccam:events:rate5m", "legendFormat": "recording rule" } - ] - }, - { - "id": 10, - "type": "timeseries", - "title": "Tokens per second by kind (5m rate)", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 8, "w": 12, "x": 12, "y": 23 }, - "options": { - "legend": { "displayMode": "list", "placement": "bottom" }, - "tooltip": { "mode": "multi" } - }, - "fieldConfig": { - "defaults": { - "unit": "ops", - "custom": { - "drawStyle": "line", - "lineWidth": 2, - "fillOpacity": 12, - "showPoints": "never" - }, - "color": { "mode": "palette-classic" } - } - }, - "targets": [ - { "refId": "A", "expr": "rate(ccam_tokens_total[5m])", "legendFormat": "{{kind}}" } - ] - }, - { - "id": 102, - "type": "row", - "title": "Cache efficiency", - "collapsed": false, - "gridPos": { "h": 1, "w": 24, "x": 0, "y": 31 } - }, - { - "id": 11, - "type": "stat", - "title": "Cache read share of prompt tokens", - "description": "cache_read / (input + cache_read) — higher means more prompt served from cache.", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 5, "w": 8, "x": 0, "y": 32 }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"] }, - "colorMode": "value", - "graphMode": "area" - }, - "fieldConfig": { - "defaults": { - "unit": "percentunit", - "decimals": 1, - "min": 0, - "max": 1, - "thresholds": { - "mode": "absolute", - "steps": [ - { "color": "red", "value": null }, - { "color": "yellow", "value": 0.2 }, - { "color": "green", "value": 0.5 } - ] - } - } - }, - "targets": [ - { - "refId": "A", - "expr": "ccam_tokens_total{kind=\"cache_read\"} / clamp_min(ccam_tokens_total{kind=\"input\"} + ccam_tokens_total{kind=\"cache_read\"}, 1)", - "legendFormat": "cache hit ratio" - } - ] - }, - { - "id": 12, - "type": "timeseries", - "title": "Input vs cache read tokens (cumulative)", - "datasource": { "type": "prometheus", "uid": "ccam-prometheus" }, - "gridPos": { "h": 5, "w": 16, "x": 8, "y": 32 }, - "options": { "legend": { "displayMode": "list", "placement": "bottom" } }, - "fieldConfig": { - "defaults": { - "unit": "short", - "custom": { - "drawStyle": "line", - "lineWidth": 2, - "fillOpacity": 10, - "showPoints": "never" - } - } - }, - "targets": [ - { "refId": "A", "expr": "ccam_tokens_total{kind=\"input\"}", "legendFormat": "input" }, - { - "refId": "B", - "expr": "ccam_tokens_total{kind=\"cache_read\"}", - "legendFormat": "cache_read" - } - ] - } - ] -} diff --git a/monitoring/grafana/provisioning-native/dashboards/provider.yml b/monitoring/grafana/provisioning-native/dashboards/provider.yml deleted file mode 100644 index c3627ad..0000000 --- a/monitoring/grafana/provisioning-native/dashboards/provider.yml +++ /dev/null @@ -1,15 +0,0 @@ -# Dashboard provider for the native stack. start-native.sh writes the absolute -# dashboards path into .data/grafana-provisioning/dashboards/provider.runtime.yml -# because Grafana requires an absolute path in file providers. -apiVersion: 1 - -providers: - - name: CCAM - orgId: 1 - type: file - disableDeletion: false - updateIntervalSeconds: 30 - allowUiUpdates: true - options: - path: /var/lib/grafana/dashboards - foldersFromFilesStructure: false diff --git a/monitoring/grafana/provisioning-native/datasources/datasource.yml b/monitoring/grafana/provisioning-native/datasources/datasource.yml deleted file mode 100644 index 2dabc17..0000000 --- a/monitoring/grafana/provisioning-native/datasources/datasource.yml +++ /dev/null @@ -1,12 +0,0 @@ -# Auto-provisions the Prometheus datasource for a NATIVE (non-Docker) stack. -# Prometheus is expected on the host loopback interface (see start-native.sh). -apiVersion: 1 - -datasources: - - name: Prometheus - type: prometheus - uid: ccam-prometheus - access: proxy - url: http://127.0.0.1:9090 - isDefault: true - editable: true diff --git a/monitoring/grafana/provisioning/dashboards/provider.yml b/monitoring/grafana/provisioning/dashboards/provider.yml deleted file mode 100644 index 9df5510..0000000 --- a/monitoring/grafana/provisioning/dashboards/provider.yml +++ /dev/null @@ -1,14 +0,0 @@ -# Loads every dashboard JSON under /var/lib/grafana/dashboards on startup — four -# bundled CCAM boards (Overview, Sessions & Agents, Tokens & Events, Platform Health). -apiVersion: 1 - -providers: - - name: CCAM - orgId: 1 - type: file - disableDeletion: false - updateIntervalSeconds: 30 - allowUiUpdates: true - options: - path: /var/lib/grafana/dashboards - foldersFromFilesStructure: false diff --git a/monitoring/grafana/provisioning/datasources/datasource.yml b/monitoring/grafana/provisioning/datasources/datasource.yml deleted file mode 100644 index 3fa9d16..0000000 --- a/monitoring/grafana/provisioning/datasources/datasource.yml +++ /dev/null @@ -1,12 +0,0 @@ -# Auto-provisions the Prometheus datasource the CCAM dashboard queries. The uid -# is fixed so the bundled dashboard JSON can reference it deterministically. -apiVersion: 1 - -datasources: - - name: Prometheus - type: prometheus - uid: ccam-prometheus - access: proxy - url: http://prometheus:9090 - isDefault: true - editable: true diff --git a/monitoring/package.json b/monitoring/package.json deleted file mode 100644 index c522850..0000000 --- a/monitoring/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "@ccam/monitoring", - "version": "1.0.0", - "private": true, - "description": "CCAM Prometheus + Grafana stack (npm-managed binaries, no Docker required)", - "scripts": { - "postinstall": "node scripts/ensure-binaries.js", - "start": "node scripts/start.js --detach", - "start:foreground": "node scripts/start.js --foreground", - "stop": "node scripts/stop.js", - "verify": "node scripts/verify.js" - }, - "dependencies": { - "adm-zip": "^0.5.16", - "tar": "^7.4.3" - }, - "engines": { - "node": ">=20.0.0" - } -} diff --git a/monitoring/prometheus/ccam-rules.yml b/monitoring/prometheus/ccam-rules.yml deleted file mode 100644 index 103eeda..0000000 --- a/monitoring/prometheus/ccam-rules.yml +++ /dev/null @@ -1,21 +0,0 @@ -# Recording rules derived from live CCAM scrapes — no synthetic data. -# Pre-aggregates common queries so Prometheus Graph/Explore autocomplete -# surfaces useful series immediately after the ccam target comes up. -groups: - - name: ccam_recording - interval: 15s - rules: - - record: ccam:sessions:total - expr: sum(ccam_sessions) - - record: ccam:sessions:active - expr: ccam_sessions{status="active"} - - record: ccam:sessions:completed - expr: ccam_sessions{status="completed"} - - record: ccam:agents:working - expr: ccam_agents{status="working"} - - record: ccam:events:rate5m - expr: rate(ccam_events_total[5m]) - - record: ccam:tokens:rate5m - expr: sum by (kind) (rate(ccam_tokens_total[5m])) - - record: ccam:tokens:total - expr: sum(ccam_tokens_total) diff --git a/monitoring/prometheus/consoles/index.html b/monitoring/prometheus/consoles/index.html deleted file mode 100644 index 4232593..0000000 --- a/monitoring/prometheus/consoles/index.html +++ /dev/null @@ -1,294 +0,0 @@ - - - - - - CCAM — Prometheus Console - - - -
-

CCAM — Claude Code Agent Monitor

-

- Live metrics from GET /api/metrics via Prometheus — your SQLite - database on each scrape. No synthetic data. -

- - - -
- -

Sessions by status

- - - -
StatusCountGraph
Loading…
- -

Tokens by kind

- - - -
KindTotalGraph
Loading…
- -

Starter expressions

-
    - - -
    - - - - diff --git a/monitoring/prometheus/prometheus-docker.yml b/monitoring/prometheus/prometheus-docker.yml deleted file mode 100644 index 46eecdd..0000000 --- a/monitoring/prometheus/prometheus-docker.yml +++ /dev/null @@ -1,19 +0,0 @@ -# Prometheus scrape config when BOTH CCAM and Prometheus run in Docker on the -# shared `ccam` network (docker-compose.full.yml). The Host guard on the -# dashboard must allow `agent-monitor` — set via DASHBOARD_ALLOWED_HOSTS there. -global: - scrape_interval: 15s - scrape_timeout: 10s - -rule_files: - - ccam-rules.yml - -scrape_configs: - - job_name: ccam - metrics_path: /api/metrics - static_configs: - - targets: ["agent-monitor:4820"] - # If the server sets DASHBOARD_TOKEN (LAN deployments), uncomment and fill in: - # authorization: - # type: Bearer - # credentials: "" diff --git a/monitoring/prometheus/prometheus-native.yml b/monitoring/prometheus/prometheus-native.yml deleted file mode 100644 index b078823..0000000 --- a/monitoring/prometheus/prometheus-native.yml +++ /dev/null @@ -1,26 +0,0 @@ -# Prometheus scrape config for a NATIVE (non-Docker) CCAM + monitoring stack. -# -# Use this when Prometheus runs directly on the host (Homebrew, apt, a release -# tarball, etc.) and the dashboard is a plain `npm start` on loopback :4820. -# No DASHBOARD_ALLOWED_HOSTS override is required — 127.0.0.1 is already -# accepted by the server's Host guard. -# -# Started by: monitoring/scripts/start-native.sh (or npm run monitoring:native) -# See ../README.md. -global: - scrape_interval: 15s - scrape_timeout: 10s - -rule_files: - - ccam-rules.yml - -scrape_configs: - - job_name: ccam - metrics_path: /api/metrics - static_configs: - - targets: ["127.0.0.1:4820"] - # If the server sets DASHBOARD_TOKEN (LAN deployments), uncomment and fill in - # so the scrape passes the token guard: - # authorization: - # type: Bearer - # credentials: "" diff --git a/monitoring/prometheus/prometheus.yml b/monitoring/prometheus/prometheus.yml deleted file mode 100644 index 8b76db9..0000000 --- a/monitoring/prometheus/prometheus.yml +++ /dev/null @@ -1,29 +0,0 @@ -# Prometheus scrape config for the CCAM dashboard's /api/metrics endpoint. -# -# CCAM normally runs on the HOST (a plain `npm start` on port 4820), so from -# inside the Prometheus container it is reached via host.docker.internal. If the -# dashboard runs elsewhere or on a different port, edit the `targets` below. -# -# IMPORTANT: the CCAM server's DNS-rebinding guard rejects any Host header that -# is not loopback, so a container scrape gets 403 EBADHOST until you start the -# server allowing this Host: -# DASHBOARD_ALLOWED_HOSTS=host.docker.internal npm start -# (and, if DASHBOARD_TOKEN is set, also uncomment the authorization block below). -# See ../README.md. -global: - scrape_interval: 15s - scrape_timeout: 10s - -rule_files: - - ccam-rules.yml - -scrape_configs: - - job_name: ccam - metrics_path: /api/metrics - static_configs: - - targets: ["host.docker.internal:4820"] - # If the server sets DASHBOARD_TOKEN (LAN deployments), uncomment and fill in - # so the scrape passes the same-origin/token guard: - # authorization: - # type: Bearer - # credentials: "" diff --git a/monitoring/scripts/ensure-binaries.js b/monitoring/scripts/ensure-binaries.js deleted file mode 100644 index edf7f7c..0000000 --- a/monitoring/scripts/ensure-binaries.js +++ /dev/null @@ -1,101 +0,0 @@ -#!/usr/bin/env node -/** - * @file Downloads official Prometheus + Grafana OSS binaries into monitoring/.bin/. - * Supports macOS (arm64/Intel), Linux (arm64/amd64), and Windows (x64). - * Run via `npm run monitoring:setup` — no Homebrew, apt, or global install needed. - * @author Nguyễn Ngọc Trí Vĩ - */ -const fs = require("node:fs"); -const path = require("node:path"); -const { - BIN_ROOT, - DATA_ROOT, - VERSIONS, - SUPPORTED_TARGETS, - prometheusUrl, - grafanaUrl, - prometheusArchiveName, - grafanaArchiveName, - prometheusArchiveExt, - grafanaArchiveExt, - prometheusHome, - grafanaHome, - prometheusBinary, - grafanaBinary, - prometheusPlatform, - binariesReady, -} = require("./paths"); -const { - downloadFile, - extractArchive, - findExtractedRoot, - replaceDir, - prometheusContainsFile, - grafanaContainsFile, -} = require("./install-utils"); - -async function installPrometheus(tmpDir) { - const archive = path.join(tmpDir, `${prometheusArchiveName()}.${prometheusArchiveExt()}`); - const extractRoot = path.join(tmpDir, "prometheus-extract"); - console.log(`→ Prometheus ${VERSIONS.prometheus} (${prometheusPlatform()})`); - await downloadFile(prometheusUrl(), archive); - await fs.promises.rm(extractRoot, { recursive: true, force: true }); - await extractArchive(archive, extractRoot); - const extracted = await findExtractedRoot( - extractRoot, - [prometheusArchiveName(), `prometheus-${VERSIONS.prometheus}`], - prometheusContainsFile - ); - await replaceDir(prometheusHome(), extracted); -} - -async function installGrafana(tmpDir) { - const archive = path.join(tmpDir, `${grafanaArchiveName()}.${grafanaArchiveExt()}`); - const extractRoot = path.join(tmpDir, "grafana-extract"); - console.log(`→ Grafana OSS ${VERSIONS.grafana} (${prometheusPlatform()})`); - await downloadFile(grafanaUrl(), archive); - await fs.promises.rm(extractRoot, { recursive: true, force: true }); - await extractArchive(archive, extractRoot); - const extracted = await findExtractedRoot( - extractRoot, - [grafanaArchiveName(), `grafana-${VERSIONS.grafana}`, `grafana-v${VERSIONS.grafana}`], - grafanaContainsFile - ); - await replaceDir(grafanaHome(), extracted); -} - -async function main() { - if (binariesReady()) { - console.log("Monitoring binaries already present:"); - console.log(` Prometheus: ${prometheusBinary()}`); - console.log(` Grafana: ${grafanaBinary()}`); - return; - } - - console.log(`Platform: ${process.platform} ${process.arch}`); - console.log(`Supported: ${SUPPORTED_TARGETS.join(" · ")}`); - - await fs.promises.mkdir(BIN_ROOT, { recursive: true }); - await fs.promises.mkdir(DATA_ROOT, { recursive: true }); - - const tmpDir = path.join(DATA_ROOT, "downloads"); - await fs.promises.mkdir(tmpDir, { recursive: true }); - - console.log("Downloading CCAM monitoring binaries (one-time setup)…"); - await installPrometheus(tmpDir); - await installGrafana(tmpDir); - await fs.promises.rm(tmpDir, { recursive: true, force: true }); - - if (!binariesReady()) { - throw new Error("Binary install finished but executables were not found."); - } - - console.log("Done. Binaries installed to monitoring/.bin/"); - console.log(` Prometheus: ${prometheusBinary()}`); - console.log(` Grafana: ${grafanaBinary()}`); -} - -main().catch((err) => { - console.error(`monitoring:setup failed: ${err.message}`); - process.exit(1); -}); diff --git a/monitoring/scripts/install-utils.js b/monitoring/scripts/install-utils.js deleted file mode 100644 index d11a965..0000000 --- a/monitoring/scripts/install-utils.js +++ /dev/null @@ -1,88 +0,0 @@ -/** - * @file Archive extraction and install-dir discovery for cross-platform binaries. - * @author Nguyễn Ngọc Trí Vĩ - */ -const fs = require("node:fs"); -const path = require("node:path"); -const { pipeline } = require("node:stream/promises"); -const { Readable } = require("node:stream"); -const tar = require("tar"); -const AdmZip = require("adm-zip"); -const { isWindows } = require("./paths"); - -async function downloadFile(url, dest) { - const response = await fetch(url, { redirect: "follow" }); - if (!response.ok) { - throw new Error(`Download failed (${response.status}): ${url}`); - } - if (!response.body) { - throw new Error(`Empty response body: ${url}`); - } - await fs.promises.mkdir(path.dirname(dest), { recursive: true }); - await pipeline(Readable.fromWeb(response.body), fs.createWriteStream(dest)); -} - -async function extractTar(archivePath, destDir) { - await fs.promises.mkdir(destDir, { recursive: true }); - await tar.x({ file: archivePath, cwd: destDir }); -} - -async function extractZip(archivePath, destDir) { - await fs.promises.mkdir(destDir, { recursive: true }); - const zip = new AdmZip(archivePath); - zip.extractAllTo(destDir, true); -} - -async function extractArchive(archivePath, destDir) { - if (archivePath.endsWith(".zip")) { - await extractZip(archivePath, destDir); - return; - } - await extractTar(archivePath, destDir); -} - -async function listChildDirs(parent) { - const entries = await fs.promises.readdir(parent, { withFileTypes: true }); - return entries.filter((e) => e.isDirectory()).map((e) => path.join(parent, e.name)); -} - -/** - * Locate a single top-level directory after extraction. Official archives ship - * one root folder; this also tolerates minor naming differences across OS builds. - */ -async function findExtractedRoot(extractRoot, preferredNames, containsFile) { - for (const name of preferredNames) { - const candidate = path.join(extractRoot, name); - if (fs.existsSync(path.join(candidate, containsFile))) return candidate; - } - - const dirs = await listChildDirs(extractRoot); - const matches = []; - for (const dir of dirs) { - if (fs.existsSync(path.join(dir, containsFile))) matches.push(dir); - } - if (matches.length === 1) return matches[0]; - if (matches.length > 1) { - throw new Error( - `Multiple install directories matched in ${extractRoot}: ${matches.map(path.basename).join(", ")}` - ); - } - throw new Error( - `Could not find install directory under ${extractRoot} (expected ${containsFile}).` - ); -} - -async function replaceDir(target, source) { - await fs.promises.rm(target, { recursive: true, force: true }); - await fs.promises.mkdir(path.dirname(target), { recursive: true }); - await fs.promises.cp(source, target, { recursive: true }); -} - -module.exports = { - downloadFile, - extractArchive, - findExtractedRoot, - replaceDir, - prometheusContainsFile: isWindows() ? "prometheus.exe" : "prometheus", - grafanaContainsFile: path.join("bin", isWindows() ? "grafana.exe" : "grafana"), -}; diff --git a/monitoring/scripts/lib.js b/monitoring/scripts/lib.js deleted file mode 100644 index da2918a..0000000 --- a/monitoring/scripts/lib.js +++ /dev/null @@ -1,158 +0,0 @@ -/** - * @file Shared helpers for starting and stopping the npm-managed monitoring stack. - * @author Nguyễn Ngọc Trí Vĩ - */ -const fs = require("node:fs"); -const path = require("node:path"); -const { spawn, spawnSync } = require("node:child_process"); -const { - DATA_ROOT, - RUNTIME_PROVISIONING, - GRAFANA_DASHBOARDS, - GRAFANA_DATASOURCE_TEMPLATE, - PROMETHEUS_PID, - GRAFANA_PID, - PROMETHEUS_LOG, - GRAFANA_LOG, - isWindows, - toGrafanaPath, -} = require("./paths"); - -function readPid(file) { - try { - const raw = fs.readFileSync(file, "utf8").trim(); - const pid = Number(raw); - return Number.isInteger(pid) && pid > 0 ? pid : null; - } catch { - return null; - } -} - -function isRunning(pid) { - if (!pid) return false; - try { - process.kill(pid, 0); - return true; - } catch { - return false; - } -} - -function writePid(file, pid) { - fs.mkdirSync(path.dirname(file), { recursive: true }); - fs.writeFileSync(file, String(pid)); -} - -function removePid(file) { - try { - fs.unlinkSync(file); - } catch { - /* already gone */ - } -} - -function killProcess(pid) { - if (!pid) return; - if (isWindows()) { - spawnSync("taskkill", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore" }); - return; - } - process.kill(pid, "SIGTERM"); -} - -function stopPid(file, label) { - const pid = readPid(file); - if (!pid) return false; - if (!isRunning(pid)) { - removePid(file); - return false; - } - try { - killProcess(pid); - console.log(`Stopped ${label} (pid ${pid})`); - } catch (err) { - console.warn(`Could not stop ${label} (pid ${pid}): ${err.message}`); - } - removePid(file); - return true; -} - -function writeGrafanaProvisioning() { - const dashboardsDir = path.join(RUNTIME_PROVISIONING, "dashboards"); - const datasourcesDir = path.join(RUNTIME_PROVISIONING, "datasources"); - fs.mkdirSync(dashboardsDir, { recursive: true }); - fs.mkdirSync(datasourcesDir, { recursive: true }); - - fs.copyFileSync(GRAFANA_DATASOURCE_TEMPLATE, path.join(datasourcesDir, "datasource.yml")); - - const provider = [ - "apiVersion: 1", - "", - "providers:", - " - name: CCAM", - " orgId: 1", - " type: file", - " disableDeletion: false", - " updateIntervalSeconds: 30", - " allowUiUpdates: true", - " options:", - ` path: ${toGrafanaPath(GRAFANA_DASHBOARDS)}`, - " foldersFromFilesStructure: false", - "", - ].join("\n"); - fs.writeFileSync(path.join(dashboardsDir, "provider.yml"), provider); -} - -function spawnDetached(binary, args, env, logFile) { - fs.mkdirSync(path.dirname(logFile), { recursive: true }); - const logFd = fs.openSync(logFile, "a"); - const child = spawn(binary, args, { - detached: !isWindows(), - stdio: ["ignore", logFd, logFd], - env: { ...process.env, ...env }, - windowsHide: true, - }); - if (!isWindows()) child.unref(); - fs.closeSync(logFd); - return child.pid; -} - -function spawnForeground(binary, args, env) { - return spawn(binary, args, { - stdio: "inherit", - env: { ...process.env, ...env }, - windowsHide: true, - }); -} - -async function waitForHttp(url, attempts = 30, intervalMs = 500) { - for (let i = 0; i < attempts; i += 1) { - try { - const res = await fetch(url); - if (res.ok) return true; - } catch { - /* retry */ - } - await new Promise((r) => setTimeout(r, intervalMs)); - } - return false; -} - -module.exports = { - readPid, - isRunning, - writePid, - removePid, - killProcess, - stopPid, - writeGrafanaProvisioning, - spawnDetached, - spawnForeground, - waitForHttp, - PROMETHEUS_PID, - GRAFANA_PID, - PROMETHEUS_LOG, - GRAFANA_LOG, - DATA_ROOT, - RUNTIME_PROVISIONING, -}; diff --git a/monitoring/scripts/paths.js b/monitoring/scripts/paths.js deleted file mode 100644 index 7cdc1dc..0000000 --- a/monitoring/scripts/paths.js +++ /dev/null @@ -1,212 +0,0 @@ -/** - * @file Shared paths and version pins for the CCAM monitoring stack. - * Binaries are downloaded into monitoring/.bin/ by ensure-binaries.js so the - * stack runs with plain npm on macOS, Linux, and Windows — no Homebrew, apt, - * or global installs required. - * @author Nguyễn Ngọc Trí Vĩ - */ -const path = require("node:path"); - -const MONITORING_ROOT = path.resolve(__dirname, ".."); -const REPO_ROOT = path.resolve(MONITORING_ROOT, ".."); - -const VERSIONS = { - prometheus: "3.2.1", - grafana: "11.6.1", -}; - -/** Human-readable list for error messages. */ -const SUPPORTED_TARGETS = [ - "macOS (Apple Silicon / Intel)", - "Linux (arm64 / amd64)", - "Windows (x64)", -]; - -const BIN_ROOT = path.join(MONITORING_ROOT, ".bin"); -const DATA_ROOT = path.join(MONITORING_ROOT, ".data"); -const PROMETHEUS_DATA = path.join(DATA_ROOT, "prometheus"); -const GRAFANA_DATA = path.join(DATA_ROOT, "grafana"); -const RUNTIME_PROVISIONING = path.join(DATA_ROOT, "grafana-provisioning"); - -const PROMETHEUS_CONFIG = path.join(MONITORING_ROOT, "prometheus", "prometheus-native.yml"); -const PROMETHEUS_CONSOLES = path.join(MONITORING_ROOT, "prometheus", "consoles"); -const GRAFANA_DASHBOARDS = path.join(MONITORING_ROOT, "grafana", "dashboards"); -const GRAFANA_DATASOURCE_TEMPLATE = path.join( - MONITORING_ROOT, - "grafana", - "provisioning-native", - "datasources", - "datasource.yml" -); - -const PROMETHEUS_PID = path.join(DATA_ROOT, "prometheus.pid"); -const GRAFANA_PID = path.join(DATA_ROOT, "grafana.pid"); -const PROMETHEUS_LOG = path.join(DATA_ROOT, "prometheus.log"); -const GRAFANA_LOG = path.join(DATA_ROOT, "grafana.log"); - -function isWindows() { - return process.platform === "win32"; -} - -/** - * Maps Node's `process.arch` to Prometheus/Grafana release asset arch slugs. - * @returns {"arm64"|"amd64"} - */ -function releaseArch() { - if (process.arch === "arm64") return "arm64"; - if (process.arch === "x64") return "amd64"; - throw new Error( - `Unsupported CPU architecture "${process.arch}". Supported: ${SUPPORTED_TARGETS.join(", ")}.` - ); -} - -/** @returns {string} Prometheus platform slug used in release asset names. */ -function prometheusPlatform() { - const arch = releaseArch(); - if (process.platform === "darwin") return arch === "arm64" ? "darwin-arm64" : "darwin-amd64"; - if (process.platform === "linux") return arch === "arm64" ? "linux-arm64" : "linux-amd64"; - if (process.platform === "win32") return "windows-amd64"; - throw new Error( - `Unsupported OS "${process.platform}". Supported: ${SUPPORTED_TARGETS.join(", ")}.` - ); -} - -/** @returns {string} Grafana platform slug used in release asset names. */ -function grafanaPlatform() { - return prometheusPlatform(); -} - -function prometheusArchiveName() { - return `prometheus-${VERSIONS.prometheus}.${prometheusPlatform()}`; -} - -function grafanaArchiveName() { - return `grafana-${VERSIONS.grafana}.${grafanaPlatform()}`; -} - -function prometheusArchiveExt() { - return isWindows() ? "zip" : "tar.gz"; -} - -function grafanaArchiveExt() { - return isWindows() ? "zip" : "tar.gz"; -} - -function prometheusUrl() { - const name = prometheusArchiveName(); - return `https://github.com/prometheus/prometheus/releases/download/v${VERSIONS.prometheus}/${name}.${prometheusArchiveExt()}`; -} - -function grafanaUrl() { - const plat = grafanaPlatform(); - return `https://dl.grafana.com/oss/release/grafana-${VERSIONS.grafana}.${plat}.${grafanaArchiveExt()}`; -} - -function prometheusHome() { - return path.join(BIN_ROOT, "prometheus"); -} - -function grafanaHome() { - return path.join(BIN_ROOT, "grafana"); -} - -function prometheusBinary() { - const name = isWindows() ? "prometheus.exe" : "prometheus"; - return path.join(prometheusHome(), name); -} - -function grafanaBinary() { - const name = isWindows() ? "grafana.exe" : "grafana"; - return path.join(grafanaHome(), "bin", name); -} - -const GRAFANA_ADMIN_USER = "admin"; -const GRAFANA_ADMIN_PASSWORD = "admin"; - -/** Grafana env vars that seed the default admin account on first start. */ -function grafanaAdminEnv() { - return { - GF_SECURITY_ADMIN_USER: GRAFANA_ADMIN_USER, - GF_SECURITY_ADMIN_PASSWORD: GRAFANA_ADMIN_PASSWORD, - GF_USERS_ALLOW_SIGN_UP: "false", - GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_UID: "ccam-overview", - }; -} - -function grafanaLoginLabel() { - return `${GRAFANA_ADMIN_USER} / ${GRAFANA_ADMIN_PASSWORD}`; -} - -/** Grafana file providers require forward slashes even on Windows. */ -function toGrafanaPath(filePath) { - return filePath.replace(/\\/g, "/"); -} - -/** CLI args for Prometheus 3.x (console libraries removed upstream). */ -function prometheusServerArgs(configFile, storagePath = PROMETHEUS_DATA) { - return [ - `--config.file=${configFile}`, - `--storage.tsdb.path=${storagePath}`, - "--web.enable-lifecycle", - `--web.console.templates=${PROMETHEUS_CONSOLES}`, - ]; -} - -function prometheusDockerServerArgs(configFile) { - return [ - `--config.file=${configFile}`, - `--storage.tsdb.path=/prometheus`, - "--web.enable-lifecycle", - `--web.console.templates=/etc/prometheus/consoles`, - ]; -} - -const PROMETHEUS_CONSOLES_URL = "http://localhost:9090/consoles/index.html"; - -function binariesReady() { - const fs = require("node:fs"); - return fs.existsSync(prometheusBinary()) && fs.existsSync(grafanaBinary()); -} - -module.exports = { - MONITORING_ROOT, - REPO_ROOT, - VERSIONS, - SUPPORTED_TARGETS, - BIN_ROOT, - DATA_ROOT, - PROMETHEUS_DATA, - GRAFANA_DATA, - RUNTIME_PROVISIONING, - PROMETHEUS_CONFIG, - PROMETHEUS_CONSOLES, - PROMETHEUS_CONSOLES_URL, - GRAFANA_DASHBOARDS, - GRAFANA_DATASOURCE_TEMPLATE, - PROMETHEUS_PID, - GRAFANA_PID, - PROMETHEUS_LOG, - GRAFANA_LOG, - isWindows, - releaseArch, - prometheusPlatform, - grafanaPlatform, - prometheusArchiveName, - grafanaArchiveName, - prometheusArchiveExt, - grafanaArchiveExt, - prometheusUrl, - grafanaUrl, - prometheusHome, - grafanaHome, - prometheusBinary, - grafanaBinary, - prometheusServerArgs, - prometheusDockerServerArgs, - GRAFANA_ADMIN_USER, - GRAFANA_ADMIN_PASSWORD, - grafanaAdminEnv, - grafanaLoginLabel, - toGrafanaPath, - binariesReady, -}; diff --git a/monitoring/scripts/start.js b/monitoring/scripts/start.js deleted file mode 100644 index a40058f..0000000 --- a/monitoring/scripts/start.js +++ /dev/null @@ -1,154 +0,0 @@ -#!/usr/bin/env node -/** - * @file Starts the npm-managed Prometheus + Grafana stack (no Docker / no Brew). - * Downloads binaries on first run if monitoring:setup has not been run yet. - * @author Nguyễn Ngọc Trí Vĩ - */ -const fs = require("node:fs"); -const { spawnSync } = require("node:child_process"); -const path = require("node:path"); -const { - PROMETHEUS_CONFIG, - PROMETHEUS_DATA, - GRAFANA_DATA, - RUNTIME_PROVISIONING, - prometheusBinary, - prometheusServerArgs, - PROMETHEUS_CONSOLES_URL, - grafanaBinary, - grafanaHome, - grafanaAdminEnv, - grafanaLoginLabel, - binariesReady, -} = require("./paths"); -const { - readPid, - isRunning, - writePid, - writeGrafanaProvisioning, - spawnDetached, - spawnForeground, - waitForHttp, - killProcess, - PROMETHEUS_PID, - GRAFANA_PID, - PROMETHEUS_LOG, - GRAFANA_LOG, -} = require("./lib"); - -const detached = process.argv.includes("--detach") || process.argv.includes("-d"); -const foreground = process.argv.includes("--foreground") || process.argv.includes("-f"); - -async function ensureBinaries() { - if (binariesReady()) return; - console.log("Monitoring binaries not found — running setup…"); - const script = path.join(__dirname, "ensure-binaries.js"); - const result = spawnSync(process.execPath, [script], { stdio: "inherit" }); - if (result.status !== 0) process.exit(result.status || 1); -} - -function assertNotRunning() { - for (const [file, label] of [ - [PROMETHEUS_PID, "Prometheus"], - [GRAFANA_PID, "Grafana"], - ]) { - const pid = readPid(file); - if (isRunning(pid)) { - console.error(`${label} is already running (pid ${pid}). Run: npm run monitoring:down`); - process.exit(1); - } - } -} - -function startPrometheus() { - fs.mkdirSync(PROMETHEUS_DATA, { recursive: true }); - const args = prometheusServerArgs(PROMETHEUS_CONFIG); - const binary = prometheusBinary(); - if (detached || (!foreground && !process.stdout.isTTY)) { - const pid = spawnDetached(binary, args, {}, PROMETHEUS_LOG); - writePid(PROMETHEUS_PID, pid); - return pid; - } - return spawnForeground(binary, args, {}); -} - -function startGrafana() { - fs.mkdirSync(GRAFANA_DATA, { recursive: true }); - writeGrafanaProvisioning(); - const env = { - GF_PATHS_HOME: grafanaHome(), - GF_PATHS_DATA: GRAFANA_DATA, - GF_PATHS_PROVISIONING: RUNTIME_PROVISIONING, - ...grafanaAdminEnv(), - }; - const args = ["server", "--homepath", grafanaHome()]; - const binary = grafanaBinary(); - if (detached || (!foreground && !process.stdout.isTTY)) { - const pid = spawnDetached(binary, args, env, GRAFANA_LOG); - writePid(GRAFANA_PID, pid); - return pid; - } - return spawnForeground(binary, args, env); -} - -async function main() { - await ensureBinaries(); - assertNotRunning(); - - console.log("Starting CCAM monitoring stack…"); - console.log(` Prometheus config: ${PROMETHEUS_CONFIG}`); - console.log(` Grafana home: ${grafanaHome()}`); - - if (detached || (!foreground && !process.stdout.isTTY)) { - const promPid = startPrometheus(); - const grafPid = startGrafana(); - const promOk = await waitForHttp("http://127.0.0.1:9090/-/ready"); - const grafOk = await waitForHttp("http://127.0.0.1:3000/api/health"); - console.log(""); - console.log( - `Prometheus http://localhost:9090 (pid ${promPid})${promOk ? "" : " [still starting]"}` - ); - console.log(` CCAM console: ${PROMETHEUS_CONSOLES_URL}`); - console.log( - `Grafana http://localhost:3000 (pid ${grafPid}) ${grafanaLoginLabel()}${grafOk ? "" : " [still starting]"}` - ); - console.log( - "CCAM dashboards auto-provisioned (Overview, Sessions & Agents, Tokens & Events, Platform Health)." - ); - console.log("Stop with: npm run monitoring:down"); - if (!promOk || !grafOk) { - console.log(`Logs: ${PROMETHEUS_LOG} ${GRAFANA_LOG}`); - } - return; - } - - // Foreground: Prometheus in background, Grafana in foreground (Grafana owns the tty). - const promPid = spawnDetached( - prometheusBinary(), - prometheusServerArgs(PROMETHEUS_CONFIG), - {}, - PROMETHEUS_LOG - ); - writePid(PROMETHEUS_PID, promPid); - console.log(`Prometheus running at http://localhost:9090 (pid ${promPid})`); - console.log(` CCAM console: ${PROMETHEUS_CONSOLES_URL}`); - console.log( - `Grafana starting at http://localhost:3000 (${grafanaLoginLabel()}) — Ctrl+C stops both` - ); - const graf = startGrafana(); - const shutdown = () => { - killProcess(promPid); - process.exit(0); - }; - process.on("SIGINT", shutdown); - process.on("SIGTERM", shutdown); - graf.on("exit", (code) => { - shutdown(); - process.exit(code ?? 0); - }); -} - -main().catch((err) => { - console.error(`monitoring:start failed: ${err.message}`); - process.exit(1); -}); diff --git a/monitoring/scripts/stop.js b/monitoring/scripts/stop.js deleted file mode 100644 index 8a06a83..0000000 --- a/monitoring/scripts/stop.js +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env node -/** - * @file Stops the npm-managed Prometheus + Grafana stack started by start.js. - * @author Nguyễn Ngọc Trí Vĩ - */ -const { stopPid, PROMETHEUS_PID, GRAFANA_PID } = require("./lib"); - -const stoppedGrafana = stopPid(GRAFANA_PID, "Grafana"); -const stoppedPrometheus = stopPid(PROMETHEUS_PID, "Prometheus"); - -if (!stoppedGrafana && !stoppedPrometheus) { - console.log("Monitoring stack is not running."); -} else { - console.log("Monitoring stack stopped."); -} diff --git a/monitoring/scripts/verify.js b/monitoring/scripts/verify.js deleted file mode 100644 index 7eb2643..0000000 --- a/monitoring/scripts/verify.js +++ /dev/null @@ -1,199 +0,0 @@ -#!/usr/bin/env node -/** - * @file Verifies the CCAM dashboard and optional Prometheus/Grafana stack are up. - * Used after `monitoring:up` or `monitoring:docker:up` to confirm scrape health. - * @author Nguyễn Ngọc Trí Vĩ - */ -const { GRAFANA_ADMIN_USER, GRAFANA_ADMIN_PASSWORD } = require("./paths"); - -const DASHBOARD_URL = process.env.CCAM_DASHBOARD_URL || "http://127.0.0.1:4820"; -const PROMETHEUS_URL = process.env.CCAM_PROMETHEUS_URL || "http://127.0.0.1:9090"; -const GRAFANA_URL = process.env.CCAM_GRAFANA_URL || "http://127.0.0.1:3000"; -const JSON_MODE = process.argv.includes("--json"); - -async function checkDashboardHealth() { - try { - const res = await fetch(`${DASHBOARD_URL}/api/health`); - if (!res.ok) { - return { name: "Dashboard /api/health", ok: false, detail: `HTTP ${res.status}` }; - } - const body = await res.json(); - const detail = body.version ? `v${body.version}` : undefined; - return { name: "Dashboard /api/health", ok: true, detail, version: body.version }; - } catch (err) { - return { name: "Dashboard /api/health", ok: false, detail: err.message }; - } -} - -async function check(name, url, ok = (res) => res.ok) { - try { - const res = await fetch(url); - if (!ok(res)) { - return { name, ok: false, detail: `HTTP ${res.status}` }; - } - return { name, ok: true }; - } catch (err) { - return { name, ok: false, detail: err.message }; - } -} - -async function checkPrometheusTarget(attempts = 12, intervalMs = 2500) { - for (let i = 0; i < attempts; i += 1) { - const result = await checkPrometheusTargetOnce(); - if (result.ok) return result; - if (i < attempts - 1) await new Promise((r) => setTimeout(r, intervalMs)); - if (i === attempts - 1) return result; - } - return { name: "Prometheus ccam target", ok: false, detail: "timeout" }; -} - -async function checkPrometheusTargetOnce() { - try { - const res = await fetch(`${PROMETHEUS_URL}/api/v1/targets`); - if (!res.ok) return { name: "Prometheus ccam target", ok: false, detail: `HTTP ${res.status}` }; - const body = await res.json(); - const target = body?.data?.activeTargets?.find((t) => t.labels?.job === "ccam"); - if (!target) return { name: "Prometheus ccam target", ok: false, detail: "job not found" }; - if (target.health !== "up") { - return { - name: "Prometheus ccam target", - ok: false, - detail: target.lastError || `health=${target.health}`, - }; - } - return { name: "Prometheus ccam target", ok: true, detail: target.scrapeUrl }; - } catch (err) { - return { name: "Prometheus ccam target", ok: false, detail: err.message }; - } -} - -async function checkGrafanaLogin() { - try { - const res = await fetch(`${GRAFANA_URL}/login`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - user: GRAFANA_ADMIN_USER, - password: GRAFANA_ADMIN_PASSWORD, - }), - }); - if (!res.ok) { - return { name: "Grafana admin login", ok: false, detail: `HTTP ${res.status}` }; - } - const body = await res.json(); - if (body?.message !== "Logged in") { - return { name: "Grafana admin login", ok: false, detail: body?.message || "login rejected" }; - } - return { name: "Grafana admin login", ok: true, detail: grafanaLoginDetail() }; - } catch (err) { - return { name: "Grafana admin login", ok: false, detail: err.message }; - } -} - -function grafanaLoginDetail() { - return `${GRAFANA_ADMIN_USER} / ${GRAFANA_ADMIN_PASSWORD}`; -} - -async function checkPrometheusMetrics() { - try { - const queries = [ - { name: "Prometheus ccam_up", q: "ccam_up" }, - { name: "Prometheus total sessions", q: "sum(ccam_sessions)" }, - ]; - const results = []; - for (const { name, q } of queries) { - const res = await fetch( - `${PROMETHEUS_URL}/api/v1/query?${new URLSearchParams({ query: q })}` - ); - if (!res.ok) { - results.push({ name, ok: false, detail: `HTTP ${res.status}` }); - continue; - } - const body = await res.json(); - const series = body?.data?.result; - if (!Array.isArray(series) || series.length === 0) { - results.push({ name, ok: false, detail: "no series (is CCAM scraping?)" }); - continue; - } - const sample = series[0]?.value?.[1]; - results.push({ name, ok: true, detail: `${q} = ${sample}` }); - } - return results; - } catch (err) { - return [{ name: "Prometheus ccam metrics", ok: false, detail: err.message }]; - } -} - -async function checkPrometheusConsole() { - try { - const res = await fetch(`${PROMETHEUS_URL}/consoles/index.html`); - if (!res.ok) { - return { name: "Prometheus CCAM console", ok: false, detail: `HTTP ${res.status}` }; - } - const html = await res.text(); - if (!html.includes("CCAM")) { - return { name: "Prometheus CCAM console", ok: false, detail: "unexpected page body" }; - } - return { name: "Prometheus CCAM console", ok: true, detail: "/consoles/index.html" }; - } catch (err) { - return { name: "Prometheus CCAM console", ok: false, detail: err.message }; - } -} - -async function main() { - const checks = [ - await checkDashboardHealth(), - await check( - "Dashboard /api/metrics", - `${DASHBOARD_URL}/api/metrics`, - (res) => res.ok && res.headers.get("content-type")?.includes("text/plain") - ), - await check("Prometheus /-/ready", `${PROMETHEUS_URL}/-/ready`), - await checkPrometheusConsole(), - await check("Grafana /api/health", `${GRAFANA_URL}/api/health`), - await checkGrafanaLogin(), - await checkPrometheusTarget(), - ...(await checkPrometheusMetrics()), - ]; - - let failed = 0; - for (const c of checks) { - if (c.ok) { - if (!JSON_MODE) { - console.log(`✔ ${c.name}${c.detail ? ` (${c.detail})` : ""}`); - } - } else { - failed += 1; - if (!JSON_MODE) { - console.error(`✖ ${c.name}: ${c.detail || "failed"}`); - } - } - } - - if (JSON_MODE) { - const payload = { - ok: failed === 0, - checks, - urls: { - dashboard: DASHBOARD_URL, - prometheus: PROMETHEUS_URL, - grafana: GRAFANA_URL, - metrics: `${DASHBOARD_URL}/api/metrics`, - }, - }; - console.log(JSON.stringify(payload, null, 2)); - process.exit(failed > 0 ? 1 : 0); - } - - if (failed > 0) { - console.error(`\n${failed} check(s) failed.`); - process.exit(1); - } - console.log("\nMonitoring stack OK."); - console.log(` Grafana: ${GRAFANA_URL} (${grafanaLoginDetail()})`); - console.log(` Console: ${PROMETHEUS_URL}/consoles/index.html`); - console.log(` Graph: ${PROMETHEUS_URL}/graph`); - console.log(` Metrics: ${DASHBOARD_URL}/api/metrics`); -} - -main(); diff --git a/package.json b/package.json index 46c8e09..f5d86e7 100644 --- a/package.json +++ b/package.json @@ -72,16 +72,6 @@ "mcp:typecheck": "npm --prefix mcp run typecheck", "mcp:docker:build": "docker build -f mcp/Dockerfile -t agent-dashboard-mcp:local .", "mcp:podman:build": "podman build -f mcp/Dockerfile -t localhost/agent-dashboard-mcp:local .", - "desktop:install": "cd desktop && node scripts/install.js", - "desktop:dev": "npm run build && cd desktop && npm run dev", - "desktop:build": "cd desktop && npm run build", - "desktop:dmg": "npm run build && cd desktop && npm run dmg", - "desktop:dmg:arm64": "npm run build && cd desktop && npm run dmg:arm64", - "desktop:dmg:x64": "npm run build && cd desktop && npm run dmg:x64", - "desktop:dmg:universal": "npm run build && cd desktop && npm run dmg:universal", - "desktop:win": "npm run build && cd desktop && npm run win", - "desktop:win:portable": "npm run build && cd desktop && npm run win:portable", - "desktop:test": "cd desktop && npm test", "install-hooks": "node scripts/install-hooks.js", "seed": "node scripts/seed.js", "import-history": "node scripts/import-history.js", @@ -95,18 +85,8 @@ "format:check": "prettier --check .", "openapi:yaml": "node scripts/generate-openapi-yaml.js", "link-cli": "npm link || node -e \"console.warn('ccam CLI not linked globally (no permission?). Link manually with: npm link')\"", - "monitoring:install": "npm install --prefix monitoring", - "monitoring:setup": "npm run monitoring:install", - "monitoring:up": "npm --prefix monitoring run start", - "monitoring:down": "npm --prefix monitoring run stop", - "monitoring:start": "npm --prefix monitoring run start:foreground", - "monitoring:docker:up": "docker compose -f monitoring/docker-compose.yml up -d", - "monitoring:docker:down": "docker compose -f monitoring/docker-compose.yml down", - "monitoring:verify": "node monitoring/scripts/verify.js", "docker:up": "docker compose up -d --build", - "docker:down": "docker compose down", - "docker:full:up": "docker compose -f docker-compose.full.yml up -d --build", - "docker:full:down": "docker compose -f docker-compose.full.yml down" + "docker:down": "docker compose down" }, "dependencies": { "adm-zip": "^0.5.16", diff --git a/scripts/import-history.js b/scripts/import-history.js index 452c673..342b0bc 100644 --- a/scripts/import-history.js +++ b/scripts/import-history.js @@ -34,14 +34,11 @@ const CLAUDE_DIR = getClaudeHome(); const PROJECTS_DIR = getProjectsDir(); // Max session files a directory sweep scans synchronously before it yields to -// the event loop. The desktop app hosts this Express server IN the Electron main -// process (see desktop/src/server-host.ts), so a long synchronous scan of a -// large ~/.claude/projects tree — one statSync + one getSession query per file — -// would freeze the whole app window, not just delay an API response. Yielding -// every N files keeps each synchronous burst short so a multi-thousand-session -// history never monopolizes the loop. Under `npm start` the server is its own -// process, so the same scan can't freeze the UI there — which is exactly why -// the issue only reproduces in the packaged app (#223). +// the event loop. A long synchronous scan of a large ~/.claude/projects tree — +// one statSync + one getSession query per file — would otherwise block every +// other request (including hook ingestion) on this single Node process. +// Yielding every N files keeps each synchronous burst short so a +// multi-thousand-session history never monopolizes the loop. const SWEEP_YIELD_EVERY_FILES = 100; /** @@ -1900,10 +1897,10 @@ async function syncDefaultProjects(dbModule, options = {}) { // Cooperative yield BEFORE the per-file work, so it covers the unchanged // -file fast paths below (statSync + a getSession query) that otherwise // never await. Without this, a cold-cache sweep of a large projects tree - // runs thousands of files back-to-back with no yield and freezes the - // desktop app's window (its server shares the Electron main event loop — - // see SWEEP_YIELD_EVERY_FILES). The heavy-parse path keeps its own yield - // further down; this one is what makes the common skip path cooperative. + // runs thousands of files back-to-back with no yield and blocks every + // other request on this process (see SWEEP_YIELD_EVERY_FILES). The + // heavy-parse path keeps its own yield further down; this one is what + // makes the common skip path cooperative. if (scanned > 0 && scanned % SWEEP_YIELD_EVERY_FILES === 0) { await new Promise((resolve) => setImmediate(resolve)); } diff --git a/server/README.md b/server/README.md index 42d1dc9..7d0c86c 100644 --- a/server/README.md +++ b/server/README.md @@ -467,7 +467,7 @@ The OpenAPI spec is generated from `server/openapi.js` (`createOpenApiSpec()`), | `GET` | `/api/analytics` | Analytics aggregates for charts/trends | | `GET` | `/api/metrics` | Prometheus / OpenMetrics exposition (text; v0.0.4) | -**Prometheus metrics (`GET /api/metrics`).** Exposes the dashboard's live counters — `ccam_sessions`/`ccam_agents` by status, `ccam_events_total`, `ccam_tokens_total` by kind, `ccam_websocket_clients`, `ccam_remote_sources` by enabled state, `ccam_process_uptime_seconds`/`ccam_process_resident_memory_bytes`, and `ccam_build_info{version}` — in the Prometheus v0.0.4 text-exposition format for scraping into Prometheus / Grafana (`server/routes/metrics.js`). Values come from the same `server/db.js` prepared statements the REST API uses, so they match the UI; status series are enumerated so a gauge never drops out of the exposition at zero. The route is read-only and, being under `/api`, sits behind both the Host-header (DNS-rebinding) guard and the optional `DASHBOARD_TOKEN` guard: a non-loopback scraper (e.g. Prometheus in Docker via `host.docker.internal`) must be allowlisted with `DASHBOARD_ALLOWED_HOSTS` or it gets `403 EBADHOST`, and must send the token when one is set. A ready-to-run Prometheus + Grafana stack with four auto-provisioned dashboards (default home **CCAM — Overview**) lives in [`monitoring/`](../monitoring/README.md). +**Prometheus metrics (`GET /api/metrics`).** Exposes the dashboard's live counters — `ccam_sessions`/`ccam_agents` by status, `ccam_events_total`, `ccam_tokens_total` by kind, `ccam_websocket_clients`, `ccam_remote_sources` by enabled state, `ccam_process_uptime_seconds`/`ccam_process_resident_memory_bytes`, and `ccam_build_info{version}` — in the Prometheus v0.0.4 text-exposition format for scraping into Prometheus / Grafana (`server/routes/metrics.js`). Values come from the same `server/db.js` prepared statements the REST API uses, so they match the UI; status series are enumerated so a gauge never drops out of the exposition at zero. The route is read-only and, being under `/api`, sits behind both the Host-header (DNS-rebinding) guard and the optional `DASHBOARD_TOKEN` guard: a non-loopback scraper (e.g. Prometheus in Docker via `host.docker.internal`) must be allowlisted with `DASHBOARD_ALLOWED_HOSTS` or it gets `403 EBADHOST`, and must send the token when one is set. **Data scope (`?sources=`).** `GET /api/sessions`, `/api/events`, `/api/agents`, `/api/stats`, and `/api/analytics` all accept an optional `sources` query param — a comma-separated list of source ids (`local` plus any remote source id, see [Remote Data Sources](#remote-data-sources)) — that narrows the result to sessions with a matching `sessions.source`. It is parsed by `server/lib/source-filter.js` into SQL predicates; `/api/stats` and `/api/analytics` route to the source-scoped aggregates in `server/lib/scoped-stats.js` only when a scope is present, leaving the unscoped fast paths unchanged. `GET /api/sessions/facets` additionally returns a `sources` facet enumerating the known source ids. diff --git a/server/__tests__/monitoring-paths.test.js b/server/__tests__/monitoring-paths.test.js deleted file mode 100644 index dfbd856..0000000 --- a/server/__tests__/monitoring-paths.test.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * @file Unit tests for cross-platform monitoring binary URL/path resolution. - * @author Nguyễn Ngọc Trí Vĩ - */ -const { describe, it } = require("node:test"); -const assert = require("node:assert/strict"); -const { - prometheusUrl, - grafanaUrl, - prometheusArchiveExt, - grafanaArchiveExt, - prometheusPlatform, - prometheusArchiveName, - grafanaArchiveName, - toGrafanaPath, -} = require("../../monitoring/scripts/paths"); - -describe("monitoring paths", () => { - it("builds official download URLs for the current platform", () => { - const plat = prometheusPlatform(); - const promExt = prometheusArchiveExt(); - const grafExt = grafanaArchiveExt(); - - assert.match( - prometheusUrl(), - new RegExp(`${prometheusArchiveName()}\\.${promExt.replace(".", "\\.")}$`) - ); - assert.match( - grafanaUrl(), - new RegExp(`grafana-[0-9.]+\\.${plat}\\.${grafExt.replace(".", "\\.")}$`) - ); - - if (process.platform === "win32") { - assert.equal(promExt, "zip"); - assert.equal(grafExt, "zip"); - } else { - assert.equal(promExt, "tar.gz"); - assert.equal(grafExt, "tar.gz"); - } - }); - - it("normalizes Windows paths for Grafana YAML", () => { - assert.equal( - toGrafanaPath("C:\\ccam\\monitoring\\grafana\\dashboards"), - "C:/ccam/monitoring/grafana/dashboards" - ); - }); - - it("uses consistent archive naming", () => { - assert.match(prometheusArchiveName(), /^prometheus-[0-9.]+\./); - assert.match(grafanaArchiveName(), /^grafana-[0-9.]+\./); - }); -}); diff --git a/server/routes/metrics.js b/server/routes/metrics.js index f88cb88..49b2dc4 100644 --- a/server/routes/metrics.js +++ b/server/routes/metrics.js @@ -17,8 +17,7 @@ * server as anything other than loopback (e.g. Prometheus in Docker via * `host.docker.internal`) must therefore be allowlisted with * `DASHBOARD_ALLOWED_HOSTS` (and send the token when one is set), so an instance - * never leaks operational data to an unexpected origin. The turnkey Prometheus + - * Grafana bundle in `monitoring/` documents the exact setup. + * never leaks operational data to an unexpected origin. * * @author Nguyễn Ngọc Trí Vĩ */