feat(cockpit): operator surfaces — Approvals + Calibration + Activity + cockpit chrome#1
Conversation
First step toward folding the GaryOS cockpit into Graze. Adds an (operator) route group gated by OPERATOR_BUILD env so the public OSS build 404s these paths, and an /approvals landing page that reads gates+sends from a running gary-ui sidecar. Read-only — write paths and Tailwind pass land next. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…pprovals Replaces inline styles with graze's Tailwind v4 + shared component library (Button/Card/Badge/EmptyState). Adds the gate-answer write path: an internal /api/operator/decide/:id proxy that forwards to gary-ui's POST /api/decide/:id with the operator's bearer token kept server-side. Approvals page becomes a two-pane layout — queue left, inline expansion right (M-modal per cockpit spec, not a modal route). Recommended option pre-selected; window-level keydown listener for ⌘↵ submit. Sends still render but write-execute is pass 2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…trip
Adds the foundation the cockpit surfaces hang off:
- OperatorNav: tab nav across the four operator surfaces
- FrequencyStrip: server-rendered top-of-page card showing 30-day
gates-per-day sparkline + recommended-match aggregate ratio. Fails
silently to placeholder copy when /api/metrics returns empty (fresh
install) or /api/decisions errors. Threshold-crossing alert banner
surfaces when /api/metrics carries a recent crossing.
- Sparkline: dependency-free SVG bar chart, also exported as a
server-side helper (buildDailyBuckets) so other server components
can reuse the daily-bucket logic.
Extends src/lib/gary-ui/client.ts with typed clients for /api/decisions,
/api/metrics, /api/quality-metrics, /api/gates/:id, /api/outputs, plus
a POST helper for /api/quality-events.
Patches /api/operator/decide to compute and forward learning_tag on
every gate answer (confirmed_recommendation / operator_override /
no_recommendation), so the calibration corpus carries a reason label
on every record. Manual learning_tag from the client wins when
present.
Adds /api/operator/quality-events POST proxy so client components can
record override / sample_flag / customer_correction events.
…tats
The M-modal cockpit surface. Single huge match-rate number leads, with
the per-window aggregate ('X of Y aligned with Gary'). Below that:
decisions-per-day sparkline with a week-over-week trend label, then a
row of supporting stats (decisions in window, marked overrides,
auto-approved %, customer corrections).
Window selector is server-side via URL search param (?window=7|30|90).
Reload-safe, deep-linkable, no client state to invalidate. The selector
is the only client component on the page; everything else is server-
rendered from /api/decisions and /api/quality-metrics.
Renders gracefully empty when /api/quality-metrics returns nulls (fresh
install) or when no decisions in the window have a recommendation
(match rate shows '—' with copy explaining the loop).
Pulls from existing /api/decisions for the per-day decision sparkline
since /api/quality-metrics doesn't expose daily breakdown.
…s feed
The Activity surface. Two-column on wide viewports, stacked on narrow:
left = recent decisions, right = stage outputs.
Each decision row carries the full enrichment the gap analysis flagged:
- title + slug
- answer alongside recommended (when divergent) with 'rec' label
- aligned/overrode badge, playbook, risk pill (low/medium/high/
irreversible), outcome (auto-approved / executed / deferred),
sensitivity tier (Tier-2/3 only — Tier-1 stays uncluttered)
- Override button with optimistic update; marked state cached in
localStorage so the button stays disabled across page reloads
Override clicks hit POST /api/operator/quality-events which proxies to
gary-ui's /api/quality-events with event_kind: 'override' (the closest
backend kind to 'I would now decide differently'). Backend has no
'good_decision' kind, so positive signal is read off the existing
aligned indicator on each row.
Stage outputs feed shows the latest 30 autonomous outputs with a
collapsible content viewer per row.
The proxy.ts middleware runs runAuthzPipeline over /api/* and routes any non-v1, non-public path into the MANAGEMENT class, which 401s without a management bearer. The cockpit's /api/operator/* proxies need to be reachable from the browser without a graze management token because the upstream auth (against gary-ui) happens server-side using a token read from disk; the request from the browser carries no credentials. OPERATOR_BUILD is the existing gate that 404s the operator route group in public OSS builds; piggy-backing on it here keeps the public exposure matched to the build mode. Discovered when Ctrl+Enter from /approvals returned 401 from the proxy before reaching the route handler. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ApprovalsClient now auto-fetches /api/operator/gate-detail/[id] when a gate row is selected. The detail panel below the decide controls surfaces the sam-gate decision card markdown and stage outputs the inline /api/gates summary doesn't carry. Decide buttons stay on the main card — one canonical decision surface per gate. URL hash sync: /approvals#gate=<action-id> auto-selects that row on load (deep-link from Slack/email). Selecting a row updates the hash via replaceState (no history pollution). Submit posts now include the gate's recommended value so the server can derive learning_tag without re-fetching the gate detail. Adds /api/operator/gate-detail/[id] proxy to gary-ui's /api/gates/:id. Stale-request guard via incrementing request id keeps a slow earlier fetch from clobbering a newer selection.
…metry feat: cockpit calibration + frequency telemetry + activity (Graze pivot)
The (operator) route group no longer wraps in DashboardLayout. The Graze admin sidebar (Home / Endpoints / API Manager / Providers / etc.) and the top bar (lang switcher, theme toggle, breadcrumbs) are irrelevant to running the operator's day and were stealing attention from the actual decision surface. Replaces all of that with one sticky 56px row: - status dot + 'gary cockpit' wordmark - tabs: Inbox / Calibration / Activity, with active background fill - Inbox tab badge showing combined gate + send count - match-rate pill (color-toned at 75% / 50% thresholds) - last-refresh indicator that ticks each second Status pulled from new /api/operator/status — one round-trip per 5s poll instead of three separate calls. Header tints amber and shows 'offline' label if the upstream gary-ui is unreachable. Drops the redundant page-level <header> + <h1> on each operator route since the active tab already names the surface. Renames Approvals to Inbox in the cockpit (more cockpit-y; Calibration and Activity stay). Removes FrequencyStrip and OperatorNav components (replaced by the header). The frequency telemetry data lives entirely on /calibration where it's the page subject — the strip was duplicating the match-rate chip on every other surface. Effect: gate cards visible above the fold went from 5 to 9. Cockpit no longer reads as a guest page in someone else's app.
…commended
Three changes that turn the Inbox from a form into a cockpit.
Density:
- Compact two-line gate row (title row + slug/risk/playbook row)
- Risk pill is now color-coded — green low, amber medium, red high,
saturated red irreversible. Text-only 'risk: medium' was invisible
at scan.
- Priority rail on the left of each row (red/amber/muted by P0..P3)
- 12 cards above the fold on 1440x900 (was 5 before chrome change,
9 after chrome change)
Keyboard cockpit:
- j / arrow-down: next gate
- k / arrow-up: previous gate
- 1-9: pick option N
- Enter: submit selected option
- Cmd/Ctrl+Enter: submit (works inside form fields too)
- ?: toggle help overlay
- Esc: close help / drawer
- Form fields (input/textarea/contenteditable) opt out of letter
shortcuts — j/k/1/? inside a form field type normally.
Default-submit-recommended:
- When the gate carries a recommended option, the decide block is a
single primary button labelled with that option, plus a 'change…'
affordance that expands the full radio group.
- One click + Enter ships the recommended path. The radio fieldset
only opens for actual overrides, with numbered keys (1, 2, ...)
matching the listed options.
Inbox is sorted by priority then most-recent gate-entry, so P0 work
bubbles to the top and the keyboard 'first item' is always the most
critical gate.
Adds priority/updated/due to the GateRow contract so the page server
component can sort and surface the priority rail.
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Repository: Open-Paws/coderabbit/.coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (17)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
| async function get<T>(path: string): Promise<T> { | ||
| const token = await readToken(); | ||
| const res = await fetch(`${baseUrl()}${path}`, { | ||
| headers: { Authorization: `Bearer ${token}` }, |
| headers: { | ||
| Authorization: `Bearer ${token}`, | ||
| "Content-Type": "application/json", | ||
| }, |
| headers: { | ||
| Authorization: `Bearer ${token}`, | ||
| "Content-Type": "application/json", | ||
| }, |
CI Coverage Report
Coverage artifact was not available for this run. |
- CockpitHeader: lift Date.now() into useState, update via interval - ApprovalsClient: derive expanded during render instead of setState in effect - calibration/page: eslint-disable for RSC Date.now() (rule false-positives on async server render) - Add tests/unit/gary-ui-client.test.ts — covers gates(), qualityMetrics(), gateDetail() URL encoding, and HTTP error propagation
…POST bodies Replaces hand-rolled type guards with Zod schemas on the two operator endpoints that accept JSON bodies. The check:route-validation:t06 lint gate requires either validateBody() or .safeParse(); equivalent runtime behavior preserved.
Summary
Lands the operator-cockpit surfaces in graze, per the 2026-05-08 decision (cockpit lives in Graze; gary-ui Express stays as kernel-reader sidecar). Three of four surfaces shipped here: Approvals, Calibration, Activity. (Sources is the next PR.)
The branch was sitting unmerged while the kernel-reader API wedge (Open-Paws/garyos#164 + #166) was in flight. Both wedges are now merged, so this is unblocked.
Surfaces in this PR
#gate=<id>deep-link, default-submit-recommended, keyboard cockpit./api/operator/*classified PUBLIC underOPERATOR_BUILD(zod-validated bodies, lint-clean, npm audit fix).Commits (chronological)
8a31359scaffold operator approvals route reading gary-ui8056f99tailwind pass + gate-answer write path on operator approvals075d2a9operator layout chrome — nav + frequency telemetry stripe4d83eccalibration surface — match-rate hero + per-window statsab44d80activity surface — decisions log + override + outputs feed082a867(authz) /api/operator/* PUBLIC under OPERATOR_BUILD40b0f68full gate detail in Approvals + #gate= deep-link9ee63acmerge feat/cockpit-calibration-telemetrycb83498cockpit chrome — drop Graze layout, slim sticky header7bcd597inbox density + keyboard cockpit + default-submit-recommended+2037 lines across 16 files. New:
src/lib/gary-ui/client.ts(kernel-reader client),src/app/api/operator/status/route.ts,src/server/authz/classify.tsoperator carve-out.Test plan
#gate=<id>opens the right itemAdversarial considerations
/api/operator/*PUBLIC carve-out — only active underOPERATOR_BUILD. Confirm this env var is unset in the SaaS prod build and only set for self-hosted operator builds. Otherwise operator routes would be unauthenticated in prod./api/*routes (per ADR-036 §1, those keep their unversioned paths until consumers migrate). Will need to migrate to/api/v1/orgs/.../garys/.../...when the SaaS-tenant version of cockpit is built.Out of scope
/api/v1/*tenant-scoped routes (tracked under garyos #137 epic)