Skip to content

feat(cockpit): operator surfaces — Approvals + Calibration + Activity + cockpit chrome#1

Merged
samtuckerdavis merged 14 commits into
mainfrom
feat/cockpit-inbox-density
May 8, 2026
Merged

feat(cockpit): operator surfaces — Approvals + Calibration + Activity + cockpit chrome#1
samtuckerdavis merged 14 commits into
mainfrom
feat/cockpit-inbox-density

Conversation

@samtuckerdavis
Copy link
Copy Markdown
Contributor

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

  • Approvals — reads gates from gary-ui sidecar, full gate detail, gate-answer write path, #gate=<id> deep-link, default-submit-recommended, keyboard cockpit.
  • Calibration — match-rate hero + per-window stats.
  • Activity — decisions log + override + outputs feed.
  • Cockpit chrome — operator layout (slim sticky header, nav, frequency telemetry strip), drops Graze dashboard chrome on operator routes.
  • Auth pattern/api/operator/* classified PUBLIC under OPERATOR_BUILD (zod-validated bodies, lint-clean, npm audit fix).

Commits (chronological)

  • 8a31359 scaffold operator approvals route reading gary-ui
  • 8056f99 tailwind pass + gate-answer write path on operator approvals
  • 075d2a9 operator layout chrome — nav + frequency telemetry strip
  • e4d83ec calibration surface — match-rate hero + per-window stats
  • ab44d80 activity surface — decisions log + override + outputs feed
  • 082a867 (authz) /api/operator/* PUBLIC under OPERATOR_BUILD
  • 40b0f68 full gate detail in Approvals + #gate= deep-link
  • 9ee63ac merge feat/cockpit-calibration-telemetry
  • cb83498 cockpit chrome — drop Graze layout, slim sticky header
  • 7bcd597 inbox 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.ts operator carve-out.

Test plan

  • Lint/typecheck clean
  • Unit tests pass (the calibration-telemetry branch added authz tests)
  • Manual: run gary-ui sidecar locally, navigate to /operator/approvals, verify gate list renders
  • Manual: answer a gate via cockpit, verify decisions.jsonl gets the entry
  • Manual: deep-link #gate=<id> opens the right item

Adversarial considerations

  • /api/operator/* PUBLIC carve-out — only active under OPERATOR_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.
  • Cockpit reads via gary-ui sidecar use the legacy unversioned /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.
  • Drops Graze dashboard chrome on operator routes — verify this doesn't accidentally drop on non-operator routes.

Out of scope

  • Sources surface (next PR)
  • Migration to /api/v1/* tenant-scoped routes (tracked under garyos #137 epic)

samtuckerdavis and others added 10 commits May 8, 2026 06:27
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.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 8, 2026

Warning

Rate limit exceeded

@samtuckerdavis has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 54 seconds before requesting another review.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Repository: Open-Paws/coderabbit/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d8df6e0a-5f84-4b12-873b-480782680f1c

📥 Commits

Reviewing files that changed from the base of the PR and between c77b999 and 0126d71.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (17)
  • src/app/(operator)/CockpitHeader.tsx
  • src/app/(operator)/Sparkline.tsx
  • src/app/(operator)/activity/ActivityClient.tsx
  • src/app/(operator)/activity/page.tsx
  • src/app/(operator)/approvals/ApprovalsClient.tsx
  • src/app/(operator)/approvals/page.tsx
  • src/app/(operator)/calibration/CalibrationWindowToggle.tsx
  • src/app/(operator)/calibration/page.tsx
  • src/app/(operator)/layout.tsx
  • src/app/api/operator/decide/[id]/route.ts
  • src/app/api/operator/gate-detail/[id]/route.ts
  • src/app/api/operator/quality-events/route.ts
  • src/app/api/operator/status/route.ts
  • src/app/globals.css
  • src/lib/gary-ui/client.ts
  • src/server/authz/classify.ts
  • tests/unit/gary-ui-client.test.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cockpit-inbox-density
  • 🛠️ fix NAV violations: Commit on current branch
  • 🛠️ fix NAV violations: Create PR

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread src/lib/gary-ui/client.ts
async function get<T>(path: string): Promise<T> {
const token = await readToken();
const res = await fetch(`${baseUrl()}${path}`, {
headers: { Authorization: `Bearer ${token}` },
Comment thread src/lib/gary-ui/client.ts
Comment on lines +140 to +143
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
Comment thread src/lib/gary-ui/client.ts
Comment on lines +173 to +176
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 8, 2026

CI Coverage Report

  • Coverage job: success
  • PR test policy: success

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.
@samtuckerdavis samtuckerdavis merged commit a6fd909 into main May 8, 2026
67 checks passed
@samtuckerdavis samtuckerdavis deleted the feat/cockpit-inbox-density branch May 8, 2026 18:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants