From a46f235720ed75bd276745b297012e9d3f52b782 Mon Sep 17 00:00:00 2001 From: I4cDeath Date: Sat, 25 Apr 2026 15:27:18 +0900 Subject: [PATCH 1/3] feat(web): full HeroUI v3 + HeroUI Pro overhaul of the marketing site Replaces the hand-rolled landing/docs/changelog pages with a HeroUI v3 + HeroUI Pro implementation backed by Tailwind v4 and lucide-react, and adds the customer-facing surfaces that were missing from the previous build. Site - Hero rebuilt with social-card art, install-command picker (Tabs over pnpm/npm/yarn/bun/brew), TrustStrip KPIs, and locale-stable number formatting to eliminate hydration warnings. - New sections: WhyQRing comparison table, IntegrationsCarousel, LiveDemo terminal driving the new global CommandPalette, AgentMode, CursorPlugin showcase, FAQ, FreeCallout, FinalCta. - Floating documentation TOC: pinned-right sidebar with manual scroll-spy that survives dynamic content shifts and an accurate active-section highlight. - MCP prompt cookbook gains a per-row copy button with success/failure toasts. - Navbar contrast lifted (text-secondary baseline, primary + bold for the current page); Search and GitHub item brightened to match. - Custom navigate() in Nav handles same-page hash anchors and scroll-to-top when reclicking the home link in App Router. - Terminal cards (TerminalCard, LiveDemo) replaced HeroUI Card with styled divs to keep the macOS traffic-light header inline (HeroUI's Card.Header BEM defaults forced a column layout). - Tabs across Hero, Features, McpSection, ChangelogList get !w-auto to override HeroUI's default w-full and stop vertical stacking. - Background tokens darkened and #webgl-bg::after opacity bumped for better text contrast over the animated WebGL background. - New /lib data + helpers: cli-commands, mcp-tools, command-palette context, plus changelog/docs/icons component bundles. Dashboard (npm-shipping changes) - Status dashboard rebuilt: KPI strip, sortable + searchable secrets table, manifest/policy/approvals/hooks/agent-memory panels, audit filters with action and source chips, top-bar pause/refresh/JSON controls, and keyboard shortcuts (/, P, R, Esc). - /api/status + SSE payload extended with version, projectPath, scopes, protectedCount, manifest, policy, approvals, hooks, memoryKeys, and auditMetrics. - Search input survives the 5s SSE tick via a focus-preserving DOM swap that keeps caret position, selection, and scroll offset. Made-with: Cursor --- CHANGELOG.md | 19 + README.md | 20 +- cursor-plugin/commands/dashboard.md | 28 +- src/core/dashboard-html.ts | 951 ++++++++++-- src/core/dashboard.ts | 294 +++- src/mcp/tools/tooling.ts | 2 +- web/app/changelog/page.tsx | 326 +--- web/app/docs/page.tsx | 1088 +++++--------- web/app/globals.css | 36 +- web/app/layout.tsx | 40 +- web/app/page.tsx | 16 +- web/app/providers.tsx | 20 + web/components/AgentMode.tsx | 184 ++- web/components/Architecture.tsx | 265 ++-- web/components/CommandPalette.tsx | 288 ++++ web/components/CopyableTerminal.tsx | 78 - web/components/CursorPlugin.tsx | 275 ++-- web/components/Dashboard.tsx | 451 ++++-- web/components/Faq.tsx | 119 ++ web/components/Features.tsx | 541 +++---- web/components/FinalCta.tsx | 64 + web/components/Footer.tsx | 169 ++- web/components/FreeCallout.tsx | 111 ++ web/components/Hero.tsx | 230 +-- web/components/IntegrationsCarousel.tsx | 162 ++ web/components/LiveDemo.tsx | 165 ++ web/components/McpSection.tsx | 388 ++--- web/components/Nav.tsx | 302 ++-- web/components/QuickStart.tsx | 6 +- web/components/Stats.tsx | 81 - web/components/TerminalCard.tsx | 91 ++ web/components/TrustStrip.tsx | 98 ++ web/components/WebGLBackground.tsx | 9 +- web/components/WhyQRing.tsx | 232 +++ web/components/changelog/ChangelogList.tsx | 290 ++++ web/components/docs/CliReferenceList.tsx | 75 + web/components/docs/CommandCard.tsx | 76 + web/components/docs/DocsToc.tsx | 93 ++ web/components/docs/PromptCard.tsx | 52 + web/components/icons/BrandIcons.tsx | 18 + web/lib/command-palette-context.tsx | 33 + web/lib/data/changelog.ts | 226 +++ web/lib/data/cli-commands.ts | 211 +++ web/lib/data/cli-reference.ts | 546 +++++++ web/lib/data/features.ts | 328 ++++ web/lib/data/install.ts | 11 + web/lib/data/mcp-tools.ts | 147 ++ web/lib/data/plugin.ts | 148 ++ web/lib/data/version.ts | 1 + web/package.json | 22 +- web/pnpm-lock.yaml | 1338 ++++++++++++++++- web/vendor/gravity-ui-icons-shim/generate.mjs | 35 + web/vendor/gravity-ui-icons-shim/index.js | 787 ++++++++++ web/vendor/gravity-ui-icons-shim/package.json | 14 + 54 files changed, 9005 insertions(+), 2595 deletions(-) create mode 100644 web/app/providers.tsx create mode 100644 web/components/CommandPalette.tsx delete mode 100644 web/components/CopyableTerminal.tsx create mode 100644 web/components/Faq.tsx create mode 100644 web/components/FinalCta.tsx create mode 100644 web/components/FreeCallout.tsx create mode 100644 web/components/IntegrationsCarousel.tsx create mode 100644 web/components/LiveDemo.tsx delete mode 100644 web/components/Stats.tsx create mode 100644 web/components/TerminalCard.tsx create mode 100644 web/components/TrustStrip.tsx create mode 100644 web/components/WhyQRing.tsx create mode 100644 web/components/changelog/ChangelogList.tsx create mode 100644 web/components/docs/CliReferenceList.tsx create mode 100644 web/components/docs/CommandCard.tsx create mode 100644 web/components/docs/DocsToc.tsx create mode 100644 web/components/docs/PromptCard.tsx create mode 100644 web/components/icons/BrandIcons.tsx create mode 100644 web/lib/command-palette-context.tsx create mode 100644 web/lib/data/changelog.ts create mode 100644 web/lib/data/cli-commands.ts create mode 100644 web/lib/data/cli-reference.ts create mode 100644 web/lib/data/features.ts create mode 100644 web/lib/data/install.ts create mode 100644 web/lib/data/mcp-tools.ts create mode 100644 web/lib/data/plugin.ts create mode 100644 web/lib/data/version.ts create mode 100644 web/vendor/gravity-ui-icons-shim/generate.mjs create mode 100644 web/vendor/gravity-ui-icons-shim/index.js create mode 100644 web/vendor/gravity-ui-icons-shim/package.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 28f3a0d..cf9841d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ All notable changes to this project will be documented in this file. +## [Unreleased] + +### Changed +- **Status dashboard rebuilt** — `qring status` (and the `status_dashboard` MCP tool) now serves a denser, more useful live page. New surfaces: + - **KPI strip** — total secrets, detected env, protected count, active approvals, hooks (enabled / total), 24-hour read & write counts, denied-action count, and live anomaly count. + - **Manifest panel** — declared / required / missing / expired / stale keys from `.q-ring.json`. + - **Policy panel** — at-a-glance MCP / exec / secret-policy presence with allow-deny / approval / rotation counts. + - **Approvals panel** — every grant with scope, reason, time-remaining, and tamper / expiry state. + - **Hooks panel** — every registered shell / HTTP / signal hook with its `key` / `tag` / `event` match summary. + - **Agent memory panel** — count of encrypted memory keys. + - **Sortable + searchable secrets table** — key / scope / env / type / decay / tags / last-read columns with quick chips (`expired`, `stale`, `protected`) and a `/`-focusable search box. + - **Audit log filters** — action chips (`read` / `write` / `delete` / `export`), source chips (`cli` / `mcp` / `hook` / `agent`), and a free-text filter. + - **Top-bar controls** — pause SSE updates, force refresh, jump to raw JSON, and a relative "updated Ns ago" timestamp that keeps ticking while paused. + - **Keyboard shortcuts** — `/` focus secrets search, `P` pause, `R` refresh, `Esc` blur. +- **Snapshot payload expanded** — `/api/status` and the SSE stream now also include `version`, `projectPath`, `scopes`, `protectedCount`, `manifest`, `policy`, `approvals`, `hooks`, `memoryKeys`, and `auditMetrics` (action / source counts, top read keys, total events in the last 24h). + +### Fixed +- **Dashboard search input no longer loses focus** — the SSE re-render now uses a focus-preserving DOM swap that retains caret position, selection, and scroll offset across the 5-second tick (previously, typing a search term would be wiped out on every snapshot). + ## [0.10.1] — 2026-04-24 ### Fixed diff --git a/README.md b/README.md index 05a233f..06848c3 100644 --- a/README.md +++ b/README.md @@ -626,9 +626,23 @@ qring agent --once ### Quantum Status Dashboard — Live Monitoring -Launch a real-time dashboard in your browser that visualizes every quantum subsystem at a glance: health summary, decay timers, superposition states, entanglement pairs, active tunnels, anomaly alerts, audit log, and environment detection. +Launch a real-time dashboard in your browser that turns the entire quantum subsystem into one glanceable page. The dashboard is a single self-contained HTML page served locally — no dependencies, no cloud, no config — and streams updates every 5 seconds via Server-Sent Events while preserving search input and scroll position across ticks. -The dashboard is a self-contained HTML page served locally. Data streams in via Server-Sent Events and updates every 5 seconds — no dependencies, no cloud, no config. +What you get: + +- **KPI strip** — total secrets, detected environment, protected count, active approvals, hooks, 24-hour reads, and live anomaly count. +- **Health summary** — donut chart of healthy / stale / expired / no-decay secrets plus per-scope counts (global / project / team / org). +- **Environment** — wavefunction collapse details: detected env, source, branch, and any project context. +- **Manifest** — `.q-ring.json` summary with declared / required / missing / expired / stale keys. +- **Policy** — at-a-glance view of MCP, exec, and secret policies (allow/deny tools, deny keys/tags, allow/deny commands, approval & rotation requirements). +- **Secrets table** — searchable, sortable view of every secret (key, scope, env, type, decay, tags, last read), with quick chips for `expired`, `stale`, and `protected` filters. Press `/` to focus the search box. +- **Quantum cards** — decay timers, superposition states, entanglement pairs, and active quantum tunnels. +- **Approvals & hooks** — live list of valid (and tampered) approval grants and every registered hook with its match summary. +- **Agent memory** — count of encrypted memory keys persisted at `~/.config/q-ring/agent-memory.enc`. +- **Anomaly alerts** — burst reads, off-hours access, tampered audit chain, and other suspicious patterns. +- **Audit log (24h)** — filterable feed with action chips (`read`/`write`/`delete`/`export`), source chips (`cli`/`mcp`/`hook`/`agent`), and a free-text filter. + +Top-bar controls let you **pause** SSE updates (handy while reading the audit feed), **refresh** on demand, or jump to the raw JSON snapshot at `/api/status`. Keyboard shortcuts: `/` focus secrets search · `P` pause · `R` refresh. ```bash # Open the dashboard (auto-launches your browser) @@ -727,7 +741,7 @@ q-ring includes a full MCP server with 44 tools for AI agent integration. | `verify_audit_chain` | Verify tamper-evident hash chain integrity | | `export_audit` | Export audit events in jsonl, json, or csv format | | `health_check` | Full health report | -| `status_dashboard` | Launch the quantum status dashboard via MCP | +| `status_dashboard` | Launch the quantum status dashboard (SSE) — live KPIs, health, secrets table, manifest, policy, approvals, hooks, agent memory, anomalies, and audit feed | | `agent_scan` | Run autonomous agent scan | ### Governance & Policy Tools diff --git a/cursor-plugin/commands/dashboard.md b/cursor-plugin/commands/dashboard.md index e6ce955..ac337af 100644 --- a/cursor-plugin/commands/dashboard.md +++ b/cursor-plugin/commands/dashboard.md @@ -1,11 +1,33 @@ --- name: qring:dashboard -description: Start the q-ring local status dashboard (SSE + browser UI). Use when the user wants a live view of secret health without exposing values. +description: Start the q-ring local status dashboard (SSE + browser UI). Use when the user wants a live view of secret health, manifest gaps, policy posture, approvals, hooks, and audit traffic without exposing secret values. --- # /qring:dashboard 1. Call MCP tool **`status_dashboard`**. -2. Open the returned `http://127.0.0.1:…` URL in a browser (local only). +2. Open the returned `http://127.0.0.1:…` URL in a browser (local only — bound to loopback). -If the dashboard is already running, the tool reports the existing URL. +If the dashboard is already running, the tool reports the existing URL instead of starting a second listener. + +## What the page shows + +- **KPI strip** — total secrets, detected env, protected count, active approvals, hooks, 24h reads/writes, denied actions, and live anomaly count. +- **Health summary** — donut of healthy / stale / expired / no-decay secrets with per-scope breakdown. +- **Environment** — wavefunction collapse details (env, source, branch). +- **Manifest** — declared / required / missing / expired / stale keys from `.q-ring.json`. +- **Policy** — MCP / exec / secret policy presence with allow-deny / approval / rotation counts. +- **Secrets table** — sortable, searchable view of every secret with quick chips for `expired`, `stale`, `protected`. +- **Quantum cards** — decay timers, superposition states, entanglement pairs, active tunnels. +- **Approvals & hooks** — live grants and registered hooks with tamper / expiry state and match summaries. +- **Agent memory** — count of encrypted memory keys. +- **Anomaly alerts + audit feed** — filterable by action and source with free-text search. + +## Controls + +- Top bar: pause SSE updates, force refresh, jump to raw JSON (`/api/status`). +- Keyboard: `/` focus secrets search · `P` pause · `R` refresh · `Esc` blur input. + +## Important + +The dashboard never renders secret **values** — only metadata. It is safe to share a screenshot for debugging, but the bound URL stays on `127.0.0.1` and should not be exposed beyond the local machine. diff --git a/src/core/dashboard-html.ts b/src/core/dashboard-html.ts index b46d2a4..b934a9a 100644 --- a/src/core/dashboard-html.ts +++ b/src/core/dashboard-html.ts @@ -1,9 +1,22 @@ /** * Self-contained HTML dashboard for q-ring quantum status. - * Matches the gh-pages site design system: Outfit + JetBrains Mono fonts, - * deep navy palette, neon cyan→violet SVG icons, glassmorphism cards. * - * Zero dependencies — inline CSS + vanilla JS with EventSource for SSE. + * Matches the gh-pages site design system: deep-navy palette, neon cyan→violet + * SVG icons, glassmorphism cards. Zero dependencies — inline CSS + vanilla JS + * with EventSource for SSE. + * + * Layout: + * 1. Sticky header with brand, version, project path, live/paused state, + * pause+refresh controls, last-update timestamp. + * 2. KPI strip — at-a-glance counters (total, healthy, stale/expired, + * protected, hooks, approvals, anomalies, audit-24h). + * 3. Health donut + Environment + Manifest + Policy summary cards. + * 4. Searchable + sortable secrets table (key, scope, env, type, decay, + * tags, last accessed). Click a row to inspect details. + * 5. Quantum side panels — Decay, Superposition, Entanglement, Tunnels. + * 6. Governance — Approvals, Hooks, Memory. + * 7. Anomalies (full-width). + * 8. Audit log with action+source+text filters and an action-count strip. */ export function getDashboardHtml(): string { @@ -65,30 +78,54 @@ body{min-height:100vh;overflow-x:hidden;position:relative} @keyframes drift2{0%,100%{transform:translate(0,0) scale(1)}33%{transform:translate(-50px,30px) scale(1.05)}66%{transform:translate(40px,-60px) scale(.92)}} @keyframes drift3{0%,100%{transform:translate(-50%,-50%) scale(1)}50%{transform:translate(-40%,-60%) scale(1.1)}} -.container{position:relative;z-index:1;max-width:1280px;margin:0 auto;padding:24px 20px 48px} +.container{position:relative;z-index:1;max-width:1320px;margin:0 auto;padding:20px 20px 48px} /* Header */ -.header{display:flex;align-items:center;justify-content:space-between;margin-bottom:28px;padding:16px 20px;background:rgba(4,8,15,0.75);backdrop-filter:blur(16px) saturate(1.2);-webkit-backdrop-filter:blur(16px) saturate(1.2);border:1px solid var(--border);border-radius:var(--radius)} -.header h1{font-family:var(--font-display);font-size:1.65rem;font-weight:700;letter-spacing:-.02em;display:flex;align-items:center;gap:10px} +.header{position:sticky;top:0;z-index:5;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:14px;margin-bottom:20px;padding:14px 18px;background:rgba(4,8,15,0.85);backdrop-filter:blur(16px) saturate(1.2);-webkit-backdrop-filter:blur(16px) saturate(1.2);border:1px solid var(--border);border-radius:var(--radius)} +.header-left{display:flex;align-items:center;gap:14px;flex-wrap:wrap;min-width:0} +.header h1{font-family:var(--font-display);font-size:1.45rem;font-weight:700;letter-spacing:-.02em;display:flex;align-items:center;gap:10px} .header h1 .q-icon{display:flex;filter:drop-shadow(0 0 6px rgba(14,165,233,0.6))} .header h1 .brand{background:linear-gradient(135deg,#00D1FF,var(--violet));-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent} -.header h1 .sub{color:var(--text-dim);font-weight:400;font-size:1.1rem;-webkit-text-fill-color:var(--text-dim)} +.header h1 .sub{color:var(--text-dim);font-weight:400;font-size:1rem;-webkit-text-fill-color:var(--text-dim)} +.meta-chip{font-family:var(--font-mono);font-size:.72rem;color:var(--text-dim);background:rgba(255,255,255,.04);border:1px solid var(--border);padding:3px 8px;border-radius:99px;display:inline-flex;align-items:center;gap:6px;max-width:340px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.meta-chip svg{width:11px;height:11px;flex-shrink:0;opacity:.7} +.header-right{display:flex;align-items:center;gap:10px;flex-wrap:wrap} .status-dot{width:8px;height:8px;border-radius:50%;background:var(--green);box-shadow:0 0 8px var(--green);display:inline-block;animation:pulse 2s ease-in-out infinite} -.status-dot.disconnected{background:var(--danger);box-shadow:0 0 8px var(--danger)} +.status-dot.disconnected{background:var(--danger);box-shadow:0 0 8px var(--danger);animation:none} +.status-dot.paused{background:var(--warning);box-shadow:0 0 8px var(--warning);animation:none} @keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}} -.conn-label{font-size:.85rem;color:var(--text-dim);display:flex;align-items:center;gap:6px;font-family:var(--font-mono)} +.conn-label{font-size:.78rem;color:var(--text-dim);display:flex;align-items:center;gap:6px;font-family:var(--font-mono)} +.btn{font-family:var(--font-display);font-size:.78rem;font-weight:500;color:var(--text-secondary);background:rgba(255,255,255,.04);border:1px solid var(--border);padding:5px 11px;border-radius:var(--radius-sm);cursor:pointer;display:inline-flex;align-items:center;gap:5px;transition:background .2s,border-color .2s,color .2s} +.btn:hover{background:rgba(14,165,233,.08);border-color:var(--accent);color:var(--text-primary)} +.btn svg{width:13px;height:13px} +.btn.active{background:var(--accent-dim);color:var(--accent-bright);border-color:rgba(14,165,233,.4)} +.kbd{font-family:var(--font-mono);font-size:.65rem;color:var(--text-dim);background:rgba(255,255,255,.06);border:1px solid var(--border);border-bottom-width:2px;padding:1px 5px;border-radius:4px;margin-left:4px} + +/* KPI strip */ +.kpi-strip{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:10px;margin-bottom:20px} +.kpi{background:var(--glass-bg);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid var(--glass-border);border-radius:var(--radius);padding:12px 14px;display:flex;flex-direction:column;gap:2px;transition:border-color .25s,transform .25s} +.kpi:hover{border-color:var(--border-glow);transform:translateY(-2px)} +.kpi-label{font-size:.66rem;text-transform:uppercase;letter-spacing:.1em;color:var(--text-dim);font-weight:600;display:flex;align-items:center;gap:6px} +.kpi-label svg{width:12px;height:12px;flex-shrink:0} +.kpi-value{font-family:var(--font-display);font-size:1.5rem;font-weight:700;line-height:1.1;color:var(--text-primary)} +.kpi-value.warning{color:var(--warning)} +.kpi-value.danger{color:var(--danger)} +.kpi-value.green{color:var(--green)} +.kpi-value.dim{color:var(--text-dim)} +.kpi-sub{font-size:.7rem;color:var(--text-dim);font-family:var(--font-mono)} /* Grid */ -.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));gap:16px} +.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));gap:16px;margin-bottom:16px} .grid-wide{grid-column:1/-1} /* Cards — reveal animation */ .card{background:var(--glass-bg);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid var(--glass-border);border-radius:var(--radius);padding:18px 20px;box-shadow:var(--shadow-card);transition:border-color .3s,box-shadow .3s,transform .3s;opacity:0;transform:translateY(16px);animation:cardReveal .5s cubic-bezier(0.16,1,0.3,1) forwards} -.card:hover{border-color:var(--border-glow);box-shadow:var(--shadow-card),var(--shadow-glow);transform:translateY(-2px)} +.card:hover{border-color:var(--border-glow);box-shadow:var(--shadow-card),var(--shadow-glow)} @keyframes cardReveal{to{opacity:1;transform:translateY(0)}} -.card-title{font-family:var(--font-display);font-size:.8rem;text-transform:uppercase;letter-spacing:.1em;color:var(--text-dim);margin-bottom:14px;display:flex;align-items:center;gap:8px;font-weight:600} +.card-title{font-family:var(--font-display);font-size:.78rem;text-transform:uppercase;letter-spacing:.1em;color:var(--text-dim);margin-bottom:14px;display:flex;align-items:center;gap:8px;font-weight:600} .card-title svg{width:16px;height:16px;flex-shrink:0;filter:drop-shadow(0 0 4px rgba(14,165,233,0.4))} +.card-title .title-aside{margin-left:auto;font-size:.7rem;color:var(--text-dim);font-weight:400;letter-spacing:.04em;text-transform:none;display:flex;align-items:center;gap:6px} /* Health donut */ .health-row{display:flex;align-items:center;gap:24px} @@ -96,9 +133,12 @@ body{min-height:100vh;overflow-x:hidden;position:relative} .donut-wrap svg{transform:rotate(-90deg)} .donut-wrap .donut-label{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;flex-direction:column;font-size:1.5rem;font-weight:700;line-height:1;font-family:var(--font-display)} .donut-wrap .donut-label small{font-size:.7rem;color:var(--text-dim);font-weight:400;margin-top:2px;letter-spacing:.04em} -.health-legend{display:flex;flex-direction:column;gap:6px} -.legend-item{display:flex;align-items:center;gap:8px;font-size:.88rem} +.health-legend{display:flex;flex-direction:column;gap:6px;font-family:var(--font-mono)} +.legend-item{display:flex;align-items:center;gap:8px;font-size:.82rem} .legend-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0} +.scope-row{display:flex;gap:6px;margin-top:14px;flex-wrap:wrap;font-family:var(--font-mono);font-size:.72rem;color:var(--text-dim)} +.scope-pill{padding:2px 8px;border-radius:99px;background:rgba(255,255,255,.04);border:1px solid var(--border)} +.scope-pill strong{color:var(--text-primary);margin-right:4px;font-weight:600} /* Decay bars */ .decay-list{display:flex;flex-direction:column;gap:8px;max-height:280px;overflow-y:auto} @@ -106,57 +146,153 @@ body{min-height:100vh;overflow-x:hidden;position:relative} .decay-key{font-family:var(--font-mono);font-size:.85rem;min-width:120px;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .decay-bar{flex:1;height:6px;border-radius:3px;background:rgba(255,255,255,0.06);overflow:hidden;position:relative} .decay-fill{height:100%;border-radius:3px;transition:width .6s ease} -.decay-time{font-size:.8rem;color:var(--text-dim);min-width:56px;text-align:right;font-family:var(--font-mono)} +.decay-time{font-size:.8rem;color:var(--text-dim);min-width:60px;text-align:right;font-family:var(--font-mono)} /* Superposition pills */ .super-list{display:flex;flex-direction:column;gap:8px;max-height:280px;overflow-y:auto} .super-item{display:flex;align-items:center;gap:8px;flex-wrap:wrap} .super-key{font-family:var(--font-mono);font-size:.85rem;min-width:100px} -.env-pill{font-size:.75rem;padding:2px 8px;border-radius:99px;font-weight:600;letter-spacing:.03em;font-family:var(--font-mono)} +.env-pill{font-size:.72rem;padding:2px 8px;border-radius:99px;font-weight:600;letter-spacing:.03em;font-family:var(--font-mono)} .env-prod{background:rgba(255,0,85,0.2);color:var(--pink);border:1px solid rgba(255,0,85,0.3)} .env-staging{background:rgba(251,191,36,0.15);color:var(--warning);border:1px solid rgba(251,191,36,0.25)} .env-dev{background:rgba(34,197,94,0.15);color:var(--green);border:1px solid rgba(34,197,94,0.25)} .env-default{background:rgba(168,85,247,0.15);color:var(--violet);border:1px solid rgba(168,85,247,0.25)} +.env-test{background:rgba(14,165,233,0.15);color:var(--accent-bright);border:1px solid rgba(14,165,233,0.25)} /* Entanglement */ .entangle-list{display:flex;flex-direction:column;gap:6px;max-height:240px;overflow-y:auto} -.entangle-pair{display:flex;align-items:center;gap:8px;font-family:var(--font-mono);font-size:.85rem} +.entangle-pair{display:flex;align-items:center;gap:8px;font-family:var(--font-mono);font-size:.83rem} .entangle-arrow{color:var(--accent-bright)} /* Tunnels */ .tunnel-list{display:flex;flex-direction:column;gap:8px;max-height:240px;overflow-y:auto} -.tunnel-card{background:rgba(168,85,247,0.06);border:1px solid rgba(168,85,247,0.15);border-radius:var(--radius-sm);padding:10px 12px;font-size:.85rem;font-family:var(--font-mono)} -.tunnel-meta{display:flex;gap:12px;margin-top:4px;font-size:.8rem;color:var(--text-dim)} +.tunnel-card{background:rgba(168,85,247,0.06);border:1px solid rgba(168,85,247,0.15);border-radius:var(--radius-sm);padding:10px 12px;font-size:.83rem;font-family:var(--font-mono)} +.tunnel-meta{display:flex;gap:12px;margin-top:4px;font-size:.78rem;color:var(--text-dim)} /* Audit feed */ -.audit-feed{display:flex;flex-direction:column;gap:4px;max-height:300px;overflow-y:auto;font-size:.85rem;font-family:var(--font-mono)} -.audit-row{display:flex;gap:8px;padding:3px 0;border-bottom:1px solid rgba(255,255,255,0.03)} -.audit-ts{color:var(--text-dim);min-width:70px;flex-shrink:0} -.audit-action{min-width:64px;font-weight:600} +.audit-toolbar{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px;align-items:center} +.audit-actions-strip{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px} +.audit-chip{font-family:var(--font-mono);font-size:.7rem;padding:2px 9px;border-radius:99px;background:rgba(255,255,255,.04);border:1px solid var(--border);color:var(--text-secondary);cursor:pointer;transition:all .2s} +.audit-chip:hover{border-color:var(--accent);color:var(--text-primary)} +.audit-chip.active{background:var(--accent-dim);color:var(--accent-bright);border-color:rgba(14,165,233,.4)} +.audit-chip strong{color:var(--text-primary);margin-left:5px;font-weight:600} +.audit-feed{display:flex;flex-direction:column;gap:2px;max-height:340px;overflow-y:auto;font-size:.82rem;font-family:var(--font-mono)} +.audit-row{display:grid;grid-template-columns:78px 76px 56px 1fr;gap:8px;padding:4px 0;border-bottom:1px solid rgba(255,255,255,0.03);align-items:baseline} +.audit-ts{color:var(--text-dim)} +.audit-action{font-weight:600} .audit-action.read{color:var(--accent)}.audit-action.write{color:var(--green)}.audit-action.delete{color:var(--danger)} .audit-action.entangle{color:var(--violet)}.audit-action.tunnel{color:var(--violet)}.audit-action.teleport{color:var(--warning)} .audit-action.generate{color:var(--warning)}.audit-action.list{color:var(--text-dim)}.audit-action.export{color:var(--text-dim)} -.audit-action.collapse{color:var(--accent)} +.audit-action.collapse{color:var(--accent)}.audit-action.approve{color:var(--green)}.audit-action.revoke{color:var(--danger)} +.audit-action.policy_deny{color:var(--danger)}.audit-action.rotate{color:var(--violet)} +.audit-source{color:var(--text-dim);font-size:.72rem;text-transform:uppercase;letter-spacing:.04em} +.audit-detail{color:var(--text-dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.audit-key{color:var(--text-secondary)} /* Anomalies */ +.anomaly-list{display:flex;flex-direction:column;gap:8px} .anomaly-card{background:rgba(255,94,91,0.06);border:1px solid rgba(255,94,91,0.15);border-radius:var(--radius-sm);padding:10px 14px;animation:anomaly-pulse 3s ease-in-out infinite} @keyframes anomaly-pulse{0%,100%{border-color:rgba(255,94,91,0.15)}50%{border-color:rgba(255,94,91,0.4)}} -.anomaly-type{font-size:.8rem;text-transform:uppercase;letter-spacing:.06em;color:var(--danger);font-weight:700;margin-bottom:2px;font-family:var(--font-display)} -.anomaly-desc{font-size:.88rem;color:var(--text-primary)} +.anomaly-type{font-size:.7rem;text-transform:uppercase;letter-spacing:.06em;color:var(--danger);font-weight:700;margin-bottom:2px;font-family:var(--font-display)} +.anomaly-desc{font-size:.86rem;color:var(--text-primary)} +.anomaly-hint{font-size:.74rem;color:var(--text-dim);margin-top:4px;font-family:var(--font-mono)} /* Environment badge */ -.env-status{display:flex;align-items:center;gap:12px} -.env-big{font-size:1.1rem;font-weight:700;padding:4px 14px;border-radius:var(--radius-sm)} -.env-source{font-size:.85rem;color:var(--text-dim)} +.env-status{display:flex;align-items:center;gap:12px;flex-wrap:wrap} +.env-big{font-size:1rem;font-weight:700;padding:4px 14px;border-radius:var(--radius-sm)} +.env-source{font-size:.78rem;color:var(--text-dim);font-family:var(--font-mono)} + +/* Manifest */ +.manifest-bars{display:flex;flex-direction:column;gap:8px} +.manifest-row{display:flex;align-items:center;gap:8px;font-family:var(--font-mono);font-size:.78rem} +.manifest-bar{flex:1;height:6px;border-radius:3px;background:rgba(255,255,255,.06);overflow:hidden} +.manifest-fill{height:100%;border-radius:3px} +.manifest-list{margin-top:10px;display:flex;flex-direction:column;gap:3px;font-family:var(--font-mono);font-size:.78rem;color:var(--text-secondary)} +.manifest-list .label{color:var(--text-dim);text-transform:uppercase;font-size:.66rem;letter-spacing:.08em;margin-top:6px} +.manifest-list .key-pill{display:inline-block;padding:1px 6px;background:rgba(255,255,255,.04);border:1px solid var(--border);border-radius:4px;margin:1px 3px 1px 0;font-size:.74rem} +.manifest-list .key-pill.miss{color:var(--danger);border-color:rgba(255,94,91,.3)} +.manifest-list .key-pill.exp{color:var(--warning);border-color:rgba(251,191,36,.3)} + +/* Policy */ +.policy-rows{display:flex;flex-direction:column;gap:6px;font-family:var(--font-mono);font-size:.78rem;color:var(--text-secondary)} +.policy-row{display:flex;justify-content:space-between;padding:2px 0;border-bottom:1px solid rgba(255,255,255,.04)} +.policy-label{color:var(--text-dim)} +.policy-value{color:var(--text-primary)} +.policy-value.zero{color:var(--text-dim)} + +/* Approvals */ +.approval-list{display:flex;flex-direction:column;gap:8px;max-height:240px;overflow-y:auto} +.approval-card{background:rgba(34,197,94,.05);border:1px solid rgba(34,197,94,.2);border-radius:var(--radius-sm);padding:10px 12px;font-size:.82rem} +.approval-card.tampered{background:rgba(255,94,91,.06);border-color:rgba(255,94,91,.4)} +.approval-card.expiring{background:rgba(251,191,36,.06);border-color:rgba(251,191,36,.3)} +.approval-head{display:flex;justify-content:space-between;gap:8px;font-family:var(--font-mono);font-weight:600} +.approval-reason{color:var(--text-dim);font-size:.76rem;margin-top:3px} +.approval-meta{display:flex;gap:12px;margin-top:4px;font-size:.72rem;color:var(--text-dim);font-family:var(--font-mono)} + +/* Hooks */ +.hooks-list{display:flex;flex-direction:column;gap:6px;max-height:240px;overflow-y:auto} +.hook-row{display:flex;align-items:center;gap:8px;font-family:var(--font-mono);font-size:.8rem;padding:6px 10px;background:rgba(255,255,255,.03);border:1px solid var(--border);border-radius:var(--radius-sm)} +.hook-row.disabled{opacity:.5} +.hook-type{font-size:.7rem;text-transform:uppercase;font-weight:600;padding:1px 6px;border-radius:99px;letter-spacing:.04em} +.hook-type.shell{background:rgba(168,85,247,.15);color:var(--violet)} +.hook-type.http{background:rgba(14,165,233,.15);color:var(--accent-bright)} +.hook-type.signal{background:rgba(251,191,36,.15);color:var(--warning)} +.hook-summary{flex:1;color:var(--text-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.hook-id{color:var(--text-dim);font-size:.72rem} + +/* Secrets table */ +.secrets-toolbar{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px;align-items:center} +.search{flex:1;min-width:180px;display:flex;align-items:center;gap:6px;background:rgba(255,255,255,.04);border:1px solid var(--border);border-radius:var(--radius-sm);padding:5px 10px;transition:border-color .2s} +.search:focus-within{border-color:var(--accent);box-shadow:0 0 0 3px rgba(14,165,233,.12)} +.search svg{width:13px;height:13px;color:var(--text-dim);flex-shrink:0} +.search input{flex:1;background:transparent;border:none;outline:none;color:var(--text-primary);font-family:var(--font-mono);font-size:.82rem} +.search input::placeholder{color:var(--text-dim)} +.secrets-table-wrap{max-height:380px;overflow:auto;border:1px solid var(--border);border-radius:var(--radius-sm);background:rgba(4,8,15,.4)} +.secrets-table{width:100%;border-collapse:collapse;font-family:var(--font-mono);font-size:.78rem} +.secrets-table thead th{position:sticky;top:0;background:rgba(8,14,24,.95);backdrop-filter:blur(8px);text-align:left;padding:8px 10px;color:var(--text-dim);font-weight:600;font-size:.7rem;text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid var(--border);cursor:pointer;user-select:none;white-space:nowrap} +.secrets-table thead th:hover{color:var(--text-primary)} +.secrets-table thead th .sort-arrow{display:inline-block;margin-left:4px;color:var(--accent-bright);opacity:0;font-size:.7rem} +.secrets-table thead th.sorted .sort-arrow{opacity:1} +.secrets-table tbody td{padding:6px 10px;border-bottom:1px solid rgba(255,255,255,.03);color:var(--text-secondary);vertical-align:middle} +.secrets-table tbody tr:hover td{background:rgba(14,165,233,.05);color:var(--text-primary)} +.secrets-table .col-key{color:var(--text-primary);font-weight:500;max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.secrets-table .col-decay{min-width:90px} +.secrets-table .col-tags{max-width:200px;overflow:hidden} +.tag-pill{display:inline-block;font-size:.68rem;padding:1px 6px;background:rgba(168,85,247,.1);color:var(--violet);border:1px solid rgba(168,85,247,.2);border-radius:99px;margin-right:3px;font-family:var(--font-mono)} +.scope-tag{display:inline-block;font-size:.68rem;padding:1px 6px;border-radius:4px;letter-spacing:.04em;font-weight:600;text-transform:uppercase} +.scope-tag.global{background:rgba(14,165,233,.15);color:var(--accent-bright)} +.scope-tag.project{background:rgba(34,197,94,.15);color:var(--green)} +.scope-tag.team{background:rgba(168,85,247,.15);color:var(--violet)} +.scope-tag.org{background:rgba(251,191,36,.15);color:var(--warning)} +.type-tag{display:inline-block;font-size:.68rem;padding:1px 6px;border-radius:4px;letter-spacing:.04em;text-transform:uppercase;background:rgba(255,255,255,.05);color:var(--text-dim)} +.type-tag.superposition{background:rgba(168,85,247,.12);color:var(--violet)} +.protected-icon{color:var(--warning);margin-left:4px;display:inline-flex;vertical-align:middle} +.mini-decay{display:inline-flex;align-items:center;gap:6px;width:100%} +.mini-decay-bar{flex:1;height:4px;border-radius:2px;background:rgba(255,255,255,.06);overflow:hidden;min-width:30px} +.mini-decay-fill{height:100%;border-radius:2px} +.mini-decay-time{color:var(--text-dim);font-size:.7rem;min-width:38px;text-align:right} /* Empty states */ -.empty{color:var(--text-dim);font-size:.88rem;font-style:italic;padding:8px 0} +.empty{color:var(--text-dim);font-size:.86rem;font-style:italic;padding:8px 0} +.empty-cta{margin-top:6px;display:block;font-style:normal;font-family:var(--font-mono);font-size:.78rem;color:var(--accent-bright)} + +/* Footer */ +.foot{margin-top:24px;text-align:center;color:var(--text-dim);font-size:.72rem;font-family:var(--font-mono)} +.foot a{color:var(--accent-bright);text-decoration:none} +.foot a:hover{text-decoration:underline} /* Scrollbar */ -::-webkit-scrollbar{width:4px;height:4px} +::-webkit-scrollbar{width:6px;height:6px} ::-webkit-scrollbar-track{background:transparent} -::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.08);border-radius:2px} +::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.08);border-radius:3px} ::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,0.15)} + +/* Print */ +@media print{ + .blob,.header-right{display:none} + .header{position:static} + body{background:#fff;color:#000} +} @@ -174,45 +310,91 @@ body{min-height:100vh;overflow-x:hidden;position:relative}
+
-
-

- - q-ring - quantum status -

-
connecting\u2026
-
-
-
Connecting to q-ring\u2026
-
+
+
+

+ + q-ring + quantum status +

+ + +
+
+ + + + + + JSON + + connecting… +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +

+ + · keyboard: / search · P pause · R refresh +

diff --git a/src/core/dashboard.ts b/src/core/dashboard.ts index 1cc79b3..029ecbb 100644 --- a/src/core/dashboard.ts +++ b/src/core/dashboard.ts @@ -10,9 +10,14 @@ import { listSecrets } from "./keyring.js"; import { checkDecay, type DecayStatus, type QuantumEnvelope } from "./envelope.js"; import { listEntanglements, type EntanglementPair } from "./entanglement.js"; import { tunnelList } from "./tunnel.js"; -import { queryAudit, detectAnomalies, type AuditEvent, type AccessAnomaly } from "./observer.js"; -import { collapseEnvironment, type CollapseResult } from "./collapse.js"; +import { queryAudit, detectAnomalies, type AuditEvent, type AccessAnomaly, type AuditAction } from "./observer.js"; +import { collapseEnvironment, readProjectConfig, type CollapseResult } from "./collapse.js"; +import { listHooks, type HookEntry, type HookType } from "./hooks.js"; +import { listApprovals } from "./approval.js"; +import { listMemory } from "./memory.js"; +import { getPolicySummary } from "./policy.js"; import { getDashboardHtml } from "./dashboard-html.js"; +import { PACKAGE_VERSION } from "../version.js"; export interface SecretSnapshot { key: string; @@ -28,6 +33,14 @@ export interface SecretSnapshot { description?: string; tags?: string[]; entangled?: { service: string; key: string }[]; + /** Validation provider name (e.g. "openai", "stripe") */ + provider?: string; + /** Whether reads require explicit approval via MCP */ + requiresApproval?: boolean; + /** Just-In-Time provisioner name, if any */ + jitProvider?: string; + /** Whether the rotation format is declared on this envelope */ + hasRotationFormat: boolean; } export interface TunnelSnapshot { @@ -38,17 +51,125 @@ export interface TunnelSnapshot { maxReads?: number; } +export interface ApprovalSnapshot { + id: string; + key: string; + scope: string; + reason: string; + grantedBy: string; + grantedAt: string; + expiresAt: string; + /** Seconds remaining until expiry; negative if expired */ + secondsRemaining: number; + /** True if HMAC verifies and not expired */ + valid: boolean; + /** True if HMAC failed (forged/tampered) */ + tampered: boolean; +} + +export interface HookSnapshot { + id: string; + type: HookType; + description?: string; + enabled: boolean; + match: HookEntry["match"]; + /** A short, human-readable summary of the match criteria */ + matchSummary: string; +} + +export interface ManifestSnapshot { + /** Total declared keys in `.q-ring.json` `secrets` */ + declared: number; + /** Number marked `required: true` */ + required: number; + /** Required keys that are missing from project scope */ + missing: string[]; + /** Required keys that are present but expired */ + expired: string[]; + /** Required keys that are present but stale */ + stale: string[]; +} + +export interface PolicySnapshot { + hasMcpPolicy: boolean; + hasExecPolicy: boolean; + hasSecretPolicy: boolean; + /** Counts per restriction so the UI can render at-a-glance dots */ + counts: { + allowTools: number; + denyTools: number; + deniedKeys: number; + deniedTags: number; + allowCommands: number; + denyCommands: number; + requireApprovalForTags: number; + requireRotationFormatForTags: number; + }; + maxTtlSeconds?: number; + maxRuntimeSeconds?: number; +} + +export interface AuditMetrics { + /** Counts grouped by action over the audited window */ + byAction: Record; + /** Counts by source (cli, mcp, agent, etc.) */ + bySource: Record; + /** Top accessed keys (read action) over the window */ + topRead: Array<{ key: string; reads: number }>; + /** Total events read for the metrics window */ + total: number; + /** Window covered by the metrics, in seconds */ + windowSeconds: number; +} + +export interface ScopeBreakdown { + global: number; + project: number; + team: number; + org: number; +} + export interface DashboardSnapshot { + /** Snapshot generation timestamp (ISO) */ timestamp: string; + /** q-ring version that produced the snapshot */ + version: string; + /** Project path in use (for manifest/policy resolution) */ + projectPath: string; + /** All visible secrets (no values) */ secrets: SecretSnapshot[]; + /** Headline counts of secret health */ health: { healthy: number; stale: number; expired: number; noDecay: number; total: number }; + /** Counts per scope */ + scopes: ScopeBreakdown; + /** Secrets that require approval before MCP read */ + protectedCount: number; + /** Active entanglement links */ entanglements: EntanglementPair[]; + /** In-memory tunnels */ tunnels: TunnelSnapshot[]; + /** Recent audit events (most recent first; "list" filtered out) */ audit: AuditEvent[]; + /** Aggregate audit metrics (read counts, top keys, source breakdown) */ + auditMetrics: AuditMetrics; + /** Detected access anomalies */ anomalies: AccessAnomaly[]; + /** Auto-detected environment & detection source */ environment: CollapseResult | null; + /** `.q-ring.json` manifest analysis (declared vs missing) */ + manifest: ManifestSnapshot | null; + /** Governance policy summary */ + policy: PolicySnapshot; + /** Active approval tokens (HMAC-verified) */ + approvals: ApprovalSnapshot[]; + /** Registered hooks (shell/http/signal) */ + hooks: HookSnapshot[]; + /** Agent memory key count */ + memoryKeys: number; } +const AUDIT_WINDOW_SECONDS = 24 * 60 * 60; // 24h + function toSecretSnapshot(entry: { key: string; scope: string; @@ -78,11 +199,114 @@ function toSecretSnapshot(entry: { description: envelope?.meta.description, tags: envelope?.meta.tags, entangled: envelope?.meta.entangled, + provider: envelope?.meta.provider, + requiresApproval: envelope?.meta.requiresApproval, + jitProvider: envelope?.meta.jitProvider, + hasRotationFormat: !!envelope?.meta.rotationFormat, + }; +} + +function summariseHookMatch(match: HookEntry["match"]): string { + const parts: string[] = []; + if (match.key) parts.push(`key=${match.key}`); + if (match.keyPattern) parts.push(`pattern=${match.keyPattern}`); + if (match.tag) parts.push(`tag=${match.tag}`); + if (match.scope) parts.push(`scope=${match.scope}`); + if (match.action?.length) parts.push(`on=${match.action.join("|")}`); + return parts.length ? parts.join(" · ") : "any change"; +} + +function buildManifest( + projectPath: string, + secrets: SecretSnapshot[], +): ManifestSnapshot | null { + const config = readProjectConfig(projectPath); + if (!config?.secrets) return null; + const declared = Object.keys(config.secrets); + const projectSecrets = new Map( + secrets.filter((s) => s.scope === "project").map((s) => [s.key, s]), + ); + + const required: string[] = []; + const missing: string[] = []; + const expired: string[] = []; + const stale: string[] = []; + + for (const key of declared) { + const entry = config.secrets[key]; + if (entry.required) required.push(key); + const present = projectSecrets.get(key); + if (!present) { + if (entry.required) missing.push(key); + continue; + } + if (present.decay.isExpired) expired.push(key); + else if (present.decay.isStale) stale.push(key); + } + + return { + declared: declared.length, + required: required.length, + missing, + expired, + stale, + }; +} + +function buildPolicySnapshot(projectPath: string): PolicySnapshot { + const summary = getPolicySummary(projectPath); + const mcp = summary.details.mcp ?? {}; + const exec = summary.details.exec ?? {}; + const sec = summary.details.secrets ?? {}; + return { + hasMcpPolicy: summary.hasMcpPolicy, + hasExecPolicy: summary.hasExecPolicy, + hasSecretPolicy: summary.hasSecretPolicy, + counts: { + allowTools: mcp.allowTools?.length ?? 0, + denyTools: mcp.denyTools?.length ?? 0, + deniedKeys: mcp.deniedKeys?.length ?? 0, + deniedTags: mcp.deniedTags?.length ?? 0, + allowCommands: exec.allowCommands?.length ?? 0, + denyCommands: exec.denyCommands?.length ?? 0, + requireApprovalForTags: sec.requireApprovalForTags?.length ?? 0, + requireRotationFormatForTags: sec.requireRotationFormatForTags?.length ?? 0, + }, + maxTtlSeconds: sec.maxTtlSeconds, + maxRuntimeSeconds: exec.maxRuntimeSeconds, + }; +} + +function buildAuditMetrics(events: AuditEvent[]): AuditMetrics { + const byAction = {} as Record; + const bySource: Record = {}; + const readCounts = new Map(); + + for (const e of events) { + byAction[e.action] = (byAction[e.action] ?? 0) + 1; + bySource[e.source] = (bySource[e.source] ?? 0) + 1; + if (e.action === "read" && e.key) { + readCounts.set(e.key, (readCounts.get(e.key) ?? 0) + 1); + } + } + + const topRead = [...readCounts.entries()] + .map(([key, reads]) => ({ key, reads })) + .sort((a, b) => b.reads - a.reads) + .slice(0, 10); + + return { + byAction, + bySource, + topRead, + total: events.length, + windowSeconds: AUDIT_WINDOW_SECONDS, }; } export function collectSnapshot(): DashboardSnapshot { - const entries = listSecrets({ source: "api", silent: true }); + const projectPath = process.cwd(); + const entries = listSecrets({ source: "api", silent: true, projectPath }); const secrets = entries.map(toSecretSnapshot); @@ -90,28 +314,70 @@ export function collectSnapshot(): DashboardSnapshot { let stale = 0; let expired = 0; let noDecay = 0; + let protectedCount = 0; + const scopes: ScopeBreakdown = { global: 0, project: 0, team: 0, org: 0 }; for (const s of secrets) { - if (!s.decay.timeRemaining) { - noDecay++; - } else if (s.decay.isExpired) { - expired++; - } else if (s.decay.isStale) { - stale++; - } else { - healthy++; - } + if (!s.decay.timeRemaining) noDecay++; + else if (s.decay.isExpired) expired++; + else if (s.decay.isStale) stale++; + else healthy++; + if (s.requiresApproval) protectedCount++; + if (s.scope === "global") scopes.global++; + else if (s.scope === "project") scopes.project++; + else if (s.scope === "team") scopes.team++; + else if (s.scope === "org") scopes.org++; } + const auditWindow = queryAudit({ + since: new Date(Date.now() - AUDIT_WINDOW_SECONDS * 1000).toISOString(), + }).filter((e) => e.action !== "list"); + + const recent = auditWindow.slice(0, 80); + + const approvals = listApprovals().map((a) => ({ + id: a.id, + key: a.key, + scope: a.scope, + reason: a.reason, + grantedBy: a.grantedBy, + grantedAt: a.grantedAt, + expiresAt: a.expiresAt, + secondsRemaining: Math.floor( + (new Date(a.expiresAt).getTime() - Date.now()) / 1000, + ), + valid: a.valid && !a.tampered, + tampered: a.tampered, + })); + + const hookEntries = listHooks().map((h) => ({ + id: h.id, + type: h.type, + description: h.description, + enabled: h.enabled, + match: h.match, + matchSummary: summariseHookMatch(h.match), + })); + return { timestamp: new Date().toISOString(), + version: PACKAGE_VERSION, + projectPath, secrets, health: { healthy, stale, expired, noDecay, total: secrets.length }, + scopes, + protectedCount, entanglements: listEntanglements(), tunnels: tunnelList(), - audit: queryAudit({ limit: 50 }).filter(e => e.action !== "list"), + audit: recent, + auditMetrics: buildAuditMetrics(auditWindow), anomalies: detectAnomalies(), - environment: collapseEnvironment(), + environment: collapseEnvironment({ projectPath }), + manifest: buildManifest(projectPath, secrets), + policy: buildPolicySnapshot(projectPath), + approvals, + hooks: hookEntries, + memoryKeys: listMemory().length, }; } diff --git a/src/mcp/tools/tooling.ts b/src/mcp/tools/tooling.ts index e31d383..0ac4435 100644 --- a/src/mcp/tools/tooling.ts +++ b/src/mcp/tools/tooling.ts @@ -189,7 +189,7 @@ export function registerToolingTools(server: McpServer): void { server.tool( "status_dashboard", - "[dashboard] Launch the quantum status dashboard — a local web page showing live health, decay timers, superposition states, entanglement graph, tunnels, audit log, and anomaly alerts. Returns the URL to open in a browser.", + "[dashboard] Launch the quantum status dashboard — a local SSE-driven web page showing live KPIs (secrets, env, protected, approvals, hooks, 24h reads, anomalies), health summary, environment, .q-ring.json manifest gaps, governance policy summary, sortable searchable secrets table, decay/superposition/entanglement/tunnel cards, active approvals & hooks, agent memory, anomaly alerts, and a filterable 24h audit feed. Returns the URL to open in a browser. Never exposes secret values.", { port: z.number().optional().default(9876).describe("Port to serve on"), }, diff --git a/web/app/changelog/page.tsx b/web/app/changelog/page.tsx index 09e31f1..9b310fc 100644 --- a/web/app/changelog/page.tsx +++ b/web/app/changelog/page.tsx @@ -1,288 +1,86 @@ import type { Metadata } from "next"; +import Link from "next/link"; +import { Breadcrumbs, Chip } from "@heroui/react"; +import { ChevronLeft, ScrollText } from "lucide-react"; + import Nav from "@/components/Nav"; import Footer from "@/components/Footer"; import FadeIn from "@/components/motion/FadeIn"; -import Link from "next/link"; +import ChangelogList from "@/components/changelog/ChangelogList"; +import { CHANGELOG, CHANGELOG_RELEASE_COUNT } from "@/lib/data/changelog"; export const metadata: Metadata = { title: "Changelog — q-ring", description: "Version history and release notes for q-ring.", }; -interface ChangelogEntry { - version: string; - date: string; - highlights: { type: "added" | "changed" | "fixed" | "security"; text: string }[]; -} - -const changelog: ChangelogEntry[] = [ - { - version: "0.10.1", - date: "2026-04-24", - highlights: [ - { type: "fixed", text: "Publish workflow — removed the broken `npm install -g npm@latest` bootstrap step that had silently blocked v0.9.9 and v0.10.0 from reaching npm (Cannot find module 'promise-retry' on Node 22 runner)" }, - { type: "added", text: "publish.yml gained a workflow_dispatch trigger with optional `ref` input so a stuck release can be re-published manually from the Actions tab" }, - { type: "changed", text: "Functional code unchanged from 0.10.0 — patch exists solely to ship a new artifact through the now-fixed pipeline" }, - ], - }, - { - version: "0.10.0", - date: "2026-04-24", - highlights: [ - { type: "added", text: "CLI decomposed by category — nine themed modules under src/cli/commands/ (secrets, project, quantum, validation, tooling, audit, hooks, agent, security) replacing the monolithic register-cli-part{1,2,3}" }, - { type: "added", text: "MCP tools decomposed by category — ten focused modules under src/mcp/tools/ plus a shared _shared.ts, replacing the 1.5k-line tool-registration.ts" }, - { type: "added", text: "Grouped CLI help — `qring --help` now renders commands under nine glyph-prefixed sections with dimmed fallback for ungrouped commands" }, - { type: "added", text: "docs/cli-mcp-parity.md — full CLI ↔ MCP mapping, shared behavior notes, and remaining CLI-only / MCP-only gaps" }, - { type: "added", text: "New tests — keyring-lifecycle, ssrf-jit, and an approval-tamper test for workspace/sessionId HMAC coverage (164 tests across 24 files)" }, - { type: "security", text: "Approval HMAC widened to cover `workspace` and `sessionId`; forged or tampered bindings are now rejected and marked `tampered`" }, - { type: "security", text: "Approval HMAC verification uses `crypto.timingSafeEqual` on fixed-length hex digests to reduce timing leakage" }, - { type: "security", text: "~/.config/q-ring/ is created with explicit mode 0o700 for the HMAC secret and approvals registry" }, - { type: "security", text: "JIT HTTP SSRF fails closed on DNS errors and blocks non-http(s) URLs; private-IP resolution check hardened" }, - { type: "security", text: "Teleport AES-GCM now uses the recommended 12-byte IV for new bundles (unpacking unchanged)" }, - { type: "security", text: "entanglement.json and hooks.json writes use mode 0o600; shell hooks switched from exec() to execFile() with bounded stdout buffer" }, - { type: "changed", text: "Renamed httpRequest_ → httpRequest across utils and call sites; node:http request imported as httpRequestPlain to clear the shadowing" }, - { type: "changed", text: "`qring get` default output is JSON; `--raw` restores legacy stdout-only value. MCP get_secret / list_secrets / tunnel_* / agent_recall now return structured JSON text" }, - { type: "fixed", text: "policy.secrets enforced on setSecret; JIT envelope refresh uses a cross-process file lock under ~/.config/q-ring/jit-locks/; queryAudit capped to last 12MB" }, - { type: "fixed", text: "Removed dead src/services/types.ts and the `void listSecrets;` tree-shake workaround; cleaned up unused parameters to keep --noUnusedParameters green" }, - ], - }, - { - version: "0.9.9", - date: "2026-03-25", - highlights: [ - { type: "security", text: "hono >=4.12.12 — resolves 5 medium vulnerabilities in transitive dep via @modelcontextprotocol/sdk" }, - { type: "security", text: "@hono/node-server >=1.19.13 — resolves serveStatic bypass via @modelcontextprotocol/sdk" }, - { type: "security", text: "vite >=8.0.5 — resolves 2 high + 1 medium vulnerability in dev dep via vitest" }, - ], - }, - { - version: "0.9.8", - date: "2026-03-25", - highlights: [ - { type: "security", text: "SSRF protection expanded — shared guard applied to validate.ts and provision.ts; blocks private/loopback addresses" }, - { type: "security", text: "Shell injection fix — pgrep in hooks.ts now uses spawn() instead of exec() to prevent metacharacter injection" }, - { type: "security", text: "Dashboard XSS fix — audit action field escaped in renderAudit to prevent script injection" }, - { type: "security", text: "MCP policy enforcement — listSecrets, exportSecrets, hasSecret, deleteSecret, getEnvelope now enforce key read policy" }, - { type: "security", text: "Crypto hardening — tunnel IDs use crypto.randomBytes; memory encryption key stored in OS keyring with migration" }, - { type: "security", text: "Glob-to-regex escaping — metacharacters escaped in MCP list filter and hook keyPattern matching" }, - { type: "security", text: "Exec profile hardening — denyCommands uses word-boundary regex instead of substring matching" }, - { type: "security", text: "Dependency overrides — path-to-regexp >=8.4.0 resolves known vulnerability" }, - { type: "added", text: "src/core/ssrf.ts — shared SSRF guard with isPrivateIP, checkSSRF (async), and checkSSRFSync" }, - { type: "added", text: "CSP meta tag added to web layout for defense-in-depth on GitHub Pages" }, - { type: "added", text: "12 SSRF tests + tunnel ID uniqueness test (150 total tests)" }, - ], - }, - { - version: "0.9.7", - date: "2026-03-26", - highlights: [ - { type: "fixed", text: "Nav anchor links on subpages — clicking section links from /docs or /changelog now routes back to the homepage via Next.js Link instead of broken same-page anchors" }, - ], - }, - { - version: "0.9.6", - date: "2026-03-26", - highlights: [ - { type: "security", text: "Double-escaping fix — `parseDotenv()` escape chain replaced with single-pass regex to prevent double-unescape (CodeQL js/double-escaping, high severity)" }, - { type: "security", text: "picomatch >=4.0.4 override added to web lockfile — resolves ReDoS and method injection vulnerabilities" }, - { type: "security", text: "Stale `package-lock.json` removed — eliminated false-positive Dependabot alerts; added to .gitignore" }, - { type: "added", text: "8 new `parseDotenv` unit tests covering escape sequences, double-backslash handling, and edge cases (133 total)" }, - ], - }, - { - version: "0.9.5", - date: "2026-03-26", - highlights: [ - { type: "added", text: "Cursor marketplace plugin — 3 rules, 4 skills, 2 agents, 5 commands, 2 hooks, MCP connector. All 44 tools surfaced through IDE-native components" }, - { type: "added", text: "Marketplace discovery — `.cursor-plugin/marketplace.json` for monorepo-based plugin resolution" }, - { type: "added", text: "Web: Cursor Plugin homepage section, plugin nav link, Homebrew install tabs in Hero and docs" }, - { type: "added", text: "README: Cursor Plugin section and Homebrew install option" }, - { type: "fixed", text: "Removed beforeShellExecution hook — caused circular block with Cursor metadata injection" }, - { type: "security", text: "picomatch >=4.0.4 override — resolves ReDoS and method injection vulnerabilities in tsup > tinyglobby > picomatch" }, - ], - }, - { - version: "0.9.4", - date: "2026-03-25", - highlights: [ - { type: "added", text: "Vitest test suite — 125 tests across 17 files covering core, CLI, and MCP (all 44 tools verified)" }, - { type: "added", text: "CI test step — `pnpm run test:ci` added to ci.yml workflow" }, - { type: "added", text: "Homebrew tap — `brew install i4ctime/tap/qring` with auto-update workflow on release" }, - ], - }, - { - version: "0.9.3", - date: "2026-03-25", - highlights: [ - { type: "changed", text: "Custom domain — site now served at `qring.i4c.studio`" }, - { type: "changed", text: "Funding — Ko-fi slug updated; favicon metadata added" }, - { type: "changed", text: "Deploy workflow — CNAME file persists across gh-pages deploys" }, - ], - }, - { - version: "0.9.2", - date: "2026-03-24", - highlights: [ - { type: "changed", text: "README — improved intro, fixed badges, corrected MCP tool count from 31 to 44" }, - { type: "changed", text: "Docs page — added descriptions to every CLI command" }, - { type: "changed", text: "Repo settings — CODEOWNERS, branch rulesets, disabled default CodeQL" }, - ], - }, - { - version: "0.9.1", - date: "2026-03-24", - highlights: [ - { type: "changed", text: "CHANGELOG — added missing Tier 4–6 feature entries to the v0.9.0 record" }, - { type: "changed", text: "Web landing site — added 11 feature cards, 3 MCP tool groups (15 tools), 8 architecture modules; updated counts" }, - { type: "changed", text: "Web changelog — synced with CHANGELOG.md" }, - { type: "changed", text: "Stats — removed Tiers and Platforms cards; kept MCP Tools and Quantum Features" }, - ], - }, - { - version: "0.9.0", - date: "2026-03-22", - highlights: [ - { type: "added", text: "Composite / Templated Secrets — `{{OTHER_KEY}}` placeholders in connection strings resolve dynamically on read" }, - { type: "added", text: "User Approvals (Zero-Trust Agent) — HMAC-verified, scoped, time-limited approval tokens for sensitive MCP reads" }, - { type: "added", text: "JIT Provisioning — dynamically generate short-lived tokens on read (AWS STS, Generic HTTP)" }, - { type: "added", text: "Secure Execution & Auto-Redaction — `qring exec` injects secrets with automatic stdout/stderr redaction" }, - { type: "added", text: "Exec Profiles — `unrestricted`, `restricted`, and `ci` profiles for command execution policy" }, - { type: "added", text: "Codebase Secret Scanner — `qring scan` detects hardcoded credentials via regex + Shannon entropy" }, - { type: "added", text: "Secret-Aware Linter — `qring lint --fix` auto-replaces hardcoded values with `process.env.KEY` references" }, - { type: "added", text: "Agent Memory — encrypted persistent key-value store across AI agent sessions" }, - { type: "added", text: "Project Context — safe, redacted project overview for agent system prompts" }, - { type: "added", text: "Pre-Commit Secret Scanning — `qring hook:install` blocks commits containing hardcoded secrets" }, - { type: "added", text: "Secret Analytics — `qring analyze` reports usage patterns and rotation recommendations" }, - { type: "added", text: "Service Setup Wizard — `qring wizard` scaffolds service integrations in one command" }, - { type: "added", text: "Governance Policy — `.q-ring.json` policy engine for MCP tool gating, key access, and exec restrictions" }, - { type: "added", text: "Team & Org Scopes — `--team` and `--org` flags with cascade resolution (project → team → org → global)" }, - { type: "added", text: "Issuer-Native Rotation — `qring rotate` attempts provider-native rotation before falling back to local generation" }, - { type: "added", text: "CI Secret Validation — `qring ci:validate` batch-validates all secrets with structured pass/fail output" }, - { type: "added", text: "Tamper-Evident Audit — `qring audit:verify` checks SHA-256 chain integrity; `qring audit:export` outputs jsonl/json/csv" }, - { type: "added", text: "Shared HTTP client for validation/hooks; SSRF mitigation for HTTP hooks (private IP block; Q_RING_ALLOW_PRIVATE_HOOKS override)" }, - { type: "added", text: "Next.js site refresh — Tailwind v4, Motion, /docs, /changelog, mobile nav, copyable terminals, stats, interactive architecture" }, - { type: "changed", text: "MCP server tool count increased from 31 to 44" }, - { type: "changed", text: "Dashboard routing, SSE backpressure, CORS, offline-safe HTML" }, - ], - }, - { - version: "0.4.0", - date: "2026-03-22", - highlights: [ - { type: "added", text: "Secret Liveness Validation — `qring validate` tests secrets against live services (OpenAI, Stripe, GitHub, AWS, Generic HTTP)" }, - { type: "added", text: "Hooks on Secret Change — shell commands, HTTP webhooks, or process signals on write/delete/rotate" }, - { type: "added", text: "`.env` file import — `qring import .env` parses dotenv syntax and bulk-stores secrets" }, - { type: "added", text: "Project Secret Manifest — `.q-ring.json` for declaring required secrets" }, - { type: "added", text: "Env File Sync — `qring env:generate` produces .env from manifest" }, - { type: "added", text: "Disentangle command, Selective export, branchMap globs, Configurable rotation, Secret search/filtering" }, - { type: "changed", text: "MCP tool count increased from 20 to 31" }, - ], - }, - { - version: "0.3.2", - date: "2026-03-21", - highlights: [ - { type: "security", text: "Resolved 8 CodeQL `js/clear-text-logging` alerts by sanitizing keyring metadata" }, - ], - }, - { - version: "0.3.1", - date: "2026-03-21", - highlights: [ - { type: "fixed", text: "Dashboard re-rendering — cards now update in-place via differential DOM patching" }, - { type: "fixed", text: "Audit log noise — `qring list` from dashboard no longer generates audit entries" }, - { type: "changed", text: "Increased dashboard font sizes for readability" }, - { type: "changed", text: "Replaced dashboard header icon with project icon" }, - ], - }, - { - version: "0.3.0", - date: "2026-03-21", - highlights: [ - { type: "added", text: "Quantum Status Dashboard — live SSE dashboard with 8 panels at `qring status`" }, - { type: "added", text: "MCP `status_dashboard` tool for AI agents" }, - { type: "added", text: "MCP Registry support, GitHub issue templates, SECURITY.md" }, - { type: "fixed", text: "Shebang and bin paths corrected" }, - ], - }, - { - version: "0.2.7", - date: "2026-03-20", - highlights: [ - { type: "added", text: "Initial public release with quantum keyring core, MCP server (20 tools), and CLI" }, - { type: "added", text: "Superposition, entanglement, tunneling, teleportation, decay, observer, and agent features" }, - { type: "added", text: "GitHub Pages landing site, Glama integration" }, - ], - }, -]; - -const typeColors: Record = { - added: "bg-green/20 text-green border-green/30", - changed: "bg-accent/20 text-accent border-accent/30", - fixed: "bg-warning/20 text-warning border-warning/30", - security: "bg-danger/20 text-danger border-danger/30", -}; - export default function ChangelogPage() { + const latest = CHANGELOG[0]; return ( <>