Skip to content

Web console UI polish: dialog primitives, table truncation + mobile cards - #33

Merged
yashau merged 5 commits into
mainfrom
claude/festive-tharp-4fe01e
Jul 5, 2026
Merged

Web console UI polish: dialog primitives, table truncation + mobile cards#33
yashau merged 5 commits into
mainfrom
claude/festive-tharp-4fe01e

Conversation

@yashau

@yashau yashau commented Jul 5, 2026

Copy link
Copy Markdown
Owner

A focused polishing pass over the operator console: consistent dialogs, tables that behave on every screen size, and better shared-component/token conformance. Web-only, no API/schema changes.

What changed

Dialogs — consistent form primitives

  • Settings/policy edit dialogs had drifted because Field/Toggle/NumberField were copy-pasted per file (one variant even used <span>+gap-1 instead of <Label>). Consolidated onto the shared settings-fields.tsx Field/Toggle, anchored the Save action in DialogFooter, and fixed the upload dialog's column misalignment (the empty-destinations note now spans full width). Net removal of duplication across the upload/retention/watchdog/recording-profile editors + node inventory.

Tables — truncate, tooltip, and mobile cards

  • Cells no longer wrap on desktop; long values truncate and the table scrolls horizontally. Action button rows stay on one line (dropped flex-wrap).
  • New TruncateCell (components/ui): truncates one line, measures its own overflow, and mounts a shadcn tooltip with the full text only when clipped. Used per-line in multi-line name + id cells and, via meta.truncateClassName, on single-value columns — each column capped at a width sized to its data (canonical max-w-*).
  • DataTable renders a card per row below md (full table at md+). One shared change gives every list — nodes, schedules, jobs, health, audit, access, rooms, and all settings sections — a readable mobile layout: first column as title, rest as labelled fields, select/expander top-right, actions in a footer.

Conformance + mobile tidy

  • Added toneTextClass() and routed the dashboard status icons + settings warning chip through it, replacing hardcoded text-emerald-*/amber-* ladders.
  • Stat-tile grids (dashboard/jobs/health) go 2-up on mobile; fixed-width selects (upload policy, channel mode) become full-width on mobile.

Verification

  • tsc --noEmit, oxlint --deny-warnings, oxfmt --check, and the 144 web unit tests all pass.
  • Mobile cards, single/multi-line truncation, and the overflow tooltip were verified deterministically in a Playwright harness at 390px and desktop widths (name-first and badge-first table shapes).

Deliberately left out (rationale)

  • Data-viz colors (quality-timeline, node-health bars, meter-bank glow) — intentional color scales/effects, not badge tones.
  • NumberField consolidation — the nullable vs required variants differ enough that a merged type would churn ~18 call sites for little gain (both already use the shared Field+Input).
  • bg-black/10 overlays, one-off StatCard, minor p-2/p-4 spacing — low-value or intentional.

Not yet driven through the full authenticated app on mobile (needs the Postgres/API stack); the mobile-card mechanism is validated in isolation and inherited by every page through the shared component.

🤖 Generated with Claude Code

yashau and others added 5 commits July 5, 2026 20:07
Settings/policy edit dialogs had drifted because Field/Toggle were
copy-pasted per file: 5 local Field copies (one using <span>+gap-1
instead of <Label>+gap-1.5), two divergent checkbox-row styles, and
bare floating Save buttons. The upload dialog's empty-destinations hint
also lived inside a grid cell, inflating one column and breaking the
two-column vertical rhythm.

Make apps/web/src/components/settings-fields.tsx the single source of
truth (Field with optional hint/className at gap-2; polished Toggle with
a disabled state) and migrate every consumer:

- upload/retention/watchdog policy editors and the recording-profile
  editor now share Field/Toggle and anchor Save in DialogFooter.
- Upload: destinations note moved to a full-width strip, Subfolder spans
  full width, fixing column alignment.
- Recording profile: items-end so toggles bottom-align with inputs in
  the mixed Field/Toggle grid.
- node-inventory-editors: dropped its duplicate Field.

Net removal of duplication (-161/+76). Web tsc, oxlint (--deny-warnings),
oxfmt, and the 144 web unit tests all pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On narrow desktop widths, list-table cells wrapped their contents —
most visibly the action buttons (e.g. Delete dropping to a second line
on Schedules), which looked broken.

Make DataTable header and body cells whitespace-nowrap so the table
grows to its natural width and the existing overflow-auto wrapper scrolls
horizontally instead of wrapping. Drop flex-wrap from the five DataTable
action columns (schedules, jobs, health, access users, access groups) so
their button rows stay on a single line. Card action rows that use
responsive justify (schedule-detail, node-health-events,
channel-map-template-card) are left as-is — they are not tables.

Web tsc, oxlint (--deny-warnings), oxfmt, and the 144 web unit tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Long single-value cells (names, ids, hostnames, emails, recurrence,
audit action/reason, etc.) previously stretched their column and forced
excessive horizontal scroll. Cap each such column at a width chosen for
its likely data and truncate with an ellipsis; when a value is actually
clipped, hovering (or focusing) reveals the full text in a tooltip.

- New TruncateCell (components/ui): a single truncating line that
  measures its own overflow and only mounts a shadcn tooltip when the
  text is clipped. Used for multi-line name+id cells so each line clamps
  independently, and by DataTable for single-value columns.
- DataTable gains an opt-in meta.truncateClassName (a max-w-* utility).
  Columns set it per their data; short columns (badges, dates, counts),
  action, checkbox and expander columns stay unset.
- Applied across schedules, jobs, health, nodes, access, audit, rooms,
  access groups, and the settings sections (retention, watchdog,
  recording profiles, upload policies/destinations, channel maps,
  switchers).

Web tsc, oxlint (--deny-warnings), oxfmt, and the 144 web unit tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Dense multi-column tables only scrolled horizontally on phones. The
shared DataTable now renders a stacked card per row below the md
breakpoint (full table at md+), so every list in the console — nodes,
schedules, jobs, health, audit, access, rooms, and all settings
sections — gets a readable mobile layout from one change.

Each card reuses the column `cell` renderers (badges, links, truncation
preserved): the first data column is the title, the rest become labelled
fields, select/expander chrome sits top-right, and the actions column
becomes a footer. Long single-value fields wrap in the card rather than
truncate, since there is no hover to reveal a tooltip on touch.

Web tsc, oxlint (--deny-warnings), oxfmt, and 144 web tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Conformance + mobile polish surfaced by the UI audit:
- Add toneTextClass() to status-colors and use it for the dashboard
  status icons and the settings access-state chip, replacing hardcoded
  text-emerald-*/text-amber-* ladders duplicated from the tone system.
- Stat-tile grids (dashboard, jobs, health) go 2-up on mobile
  (grid-cols-2) instead of a single stacked column.
- Fixed-width upload-policy / channel-mode selects become full-width on
  mobile (w-full sm:w-44) so they no longer overflow narrow rows.

Web tsc, oxlint (--deny-warnings), oxfmt, and 144 web tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@yashau
yashau merged commit 619b6f1 into main Jul 5, 2026
1 check passed
@yashau
yashau deleted the claude/festive-tharp-4fe01e branch July 5, 2026 15:23
yashau added a commit that referenced this pull request Jul 8, 2026
…cleanup (#34)

* Fix agent-release route shadowed by /nodes/:nodeId (audit G-CRIT)

GET /api/v1/nodes/agent-release was registered after GET
/api/v1/nodes/:nodeId. The static+param collision forces Hono onto the
registration-order-sensitive TrieRouter, so the release route was
swallowed by the detail handler (404), silently killing the whole PR #32
update-available feature in production while isolated unit tests stayed
green. Register the static route first (mirroring /export); make the
release service injectable through registerNodeRoutes; add a
full-surface route test (red: 404, green: 200). Extract large node-route
test fakes to node-routes-helpers.ts to stay under the LOC guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Exclude provisioning nodes from reachable counts (audit N1/N2)

A never-contacted provisioning node was reported as reachable by both
the rakkr_node_online metric gauge and the dashboard Active Nodes count,
because each used a naive status !== "offline" that predates the new
provisioning state — inflating the reachable count and masking the
RakkrNodeOffline alert. Add a shared isNodeReachable predicate (online/
recording/degraded/alerting) and use it in both sites so they cannot
diverge again. Red->green via a metrics test (provisioning -> 0) and a
dashboardReportingNodes helper test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Prefill scheduling defaults on calendar create (audit S1)

The calendar day-cell create path called defaultDraft(firstNode) without
the controller settings, so it always fell back to the built-in profile/
policies and ignored the operator's configured scheduling defaults (the
schedules list page passes them). Thread the already-fetched controller
settings into the create draft, mirroring the list page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Cover controller-settings default merge/clear on the in-memory store (audit S4)

The keep-vs-?? merge (omitted field keeps, explicit null clears a
default) was only exercised by a DB-gated test, so a regression of keep()
to ?? would clear-silently-broken with the whole in-memory suite still
green. Add an in-memory route test that sets two defaults independently,
confirms an unrelated PATCH preserves them, and confirms explicit null
clears exactly one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Lock provisioning/offline-liveness invariant in node-lifecycle baseline (audit N3)

The node-lifecycle baseline verifier (shipped to promote node lifecycle
to done) never asserted the provisioning-gating invariant PR #31
introduced: its source list omitted node-liveness.ts and
watchdog-node-liveness.ts and no phrase/snippet mentioned provisioning or
the offline gate. Document the provisioning/offline-liveness contract and
extend the verifier to assert the gate source, the isNodeReachable
predicate, and the liveness/watchdog test titles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Add 2026-07-08 gap-hunt audit ledger (Run 1)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Re-clamp paginated lists when the total shrinks (audit H4-1)

useServerPagination only reset offset on filter/page-size change, never
when the row total shrank below the current offset. Deleting the last
page's rows (bulk delete / retention sweep) stranded the operator on a
blank page past the end with only Previous to escape. Add a pure
clampedOffset helper (red->green) + a clampToTotal hook method the pages
call during render with the server meta.total, mirroring the filter-reset
pattern. Wired into all paginated pages.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Finish stub-upload-policy removal from the console (audit H3-1, H3-2)

PR #31 filtered the test-only stub upload policy from settings/scheduling
but missed two paths: (H3-1) the recordings page fed the unfiltered list
to the per-recording and bulk Queue Upload dropdowns, so the stub was a
selectable — and default-first — real upload target; (H3-2) editing a
legacy schedule persisted with the stub silently re-saved it (the form's
filtered toggles left no way to clear it). Add a selectableUploadPolicies
helper for the recordings action dropdowns (labeling keeps the full list),
and strip the stub in scheduleToDraft. Red->green helper + draft tests.
(recordings.tsx also carries its pagination clampToTotal wiring, H4-1.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Stop agent :jobId route from shadowing /recording-jobs/export (audit H1-1)

The residual instance of the G1 route-shadow class: the node-auth
GET /recording-jobs/:jobId (registered before the operator export route)
swallowed GET /recording-jobs/export under TrieRouter, answering it with a
node-credential 401. The agent handler must stay registered first (so
agent job-reads work), so defer the reserved 'export' segment to the
downstream operator handler — a job id is never literally 'export'.
Red->green in the agent-job-read harness (production registration order).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Promote provisioning nodes on heartbeat; block self-un-promotion (audit N4)

The heartbeat write persisted the agent-reported status verbatim and the
schema accepted 'provisioning'/'offline', so a first heartbeat only
promoted a provisioning node because the shipped agent hardcodes 'online'
— and any node-credential holder could POST status:'provisioning' to
un-promote a live node, which deriveNodeStatus short-circuits, suppressing
its offline alert forever. Coerce a heartbeat's provisioning/offline to
online in both stores (heartbeatStatus); the controller owns lifecycle
state. Red->green transform tests (also closes the H2-2 coverage gap).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Require a real destination when creating an upload policy (audit H3-3)

PR #31 intended every upload policy to target a real destination, but the
create input left destinationId optional and the console eager-created a
destination-less policy on New — assignable, and reconciling its
recordings to partial (provider_not_configured). Enforce destinationId at
the operator create route (the store stays lenient for seeds/tests);
client seeds the first destination and disables New until one exists.
Red->green route test; fixture updates for the stricter route.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Offer provisioning in the node status filter (audit H1-2)

The node inventory status filter omitted 'provisioning', so operators
could not filter to the enrolled-but-never-contacted cohort the API fully
supports. Add it and lock the dropdown against the full NodeStatus enum.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Update gap-hunt ledger: Run 2 (7 fixes, streak 0)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Cap and dedup schedule uploadPolicyIds (audit R3-4)

Each schedule upload policy id fans a recording out to its own upload
queue item, but the list was uncapped and un-deduped — a schedule:manage
holder could multiply queue work per recording with a long/duplicate list
(the sibling switcher-mappings list already caps at 256). Cap the schedule
create/update schemas at 32 and dedup the client draft before submit.
Red->green dedup test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Lock upload-policy update against clearing the destination (audit H3-3 coverage)

The create route rejects a destination-less policy; add the matching
coverage on the update path — an empty destinationId is schema-rejected
(400) and an omitted one is preserved — so a future nullable/empty-allowed
change to the update schema can't silently reopen the H3-3 hole.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Update gap-hunt ledger: Run 3 (2 changes, streak 0)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Update gap-hunt ledger: Run 4 clean (streak 1/5)

Run 4 convergence attempt: adversary-on-Run-3 + completeness critic (all
85 changed files), data-integrity/concurrency, permission-boundary sweeps
— zero code changes; db:verify migration replay green. R4-1/R4-2 logged
as suspected/mitigated. First clean run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Update gap-hunt ledger: Run 5 clean (streak 2/5)

Run 5 convergence (fresh angles): end-to-end feature-trace, deploy/ops +
docs/baseline consistency, observability/error-surface — all clean, zero
code changes. node:test-db 22/22 green. Second consecutive clean run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Stop cleared watchdog numeric fields from persisting 0 (audit H4-2 re-triage)

Run-6 re-triage found H4-2 was mis-classified as cosmetic for the watchdog
editor: clearing a numeric field yields Number("")===0, and thresholdDbfs
(dbfsSchema [-160,24]) and the score thresholds ([0,1]) ACCEPT 0 — so a
cleared threshold silently persists 0 and arms an always-fire alert
(watchdog-signal fires low-signal for all healthy audio), flooding the
operator with spurious critical health events. Add a shared numericInputCommit
helper (empty/invalid -> no commit, never 0) + a local text buffer in the
shared NumberField (keeps the field editable while typing) so all 16 watchdog
numeric fields are fixed at one point. Red->green helper test. Other editors'
clear->0 is server-rejected (no persist) and stays the catalogued cosmetic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Update gap-hunt ledger: Run 6 dirty, H4-2 re-triaged + fixed (streak reset 2->0)

Catalogue re-triage found H4-2 mis-classified (watchdog thresholdDbfs/score
thresholds accept 0 -> persisted always-fire alert); fixed as H4-2W. Other
19 catalogued items confirmed correctly triaged. New suspected: R6-ENROLL-DUP,
R6-INSTALL-URL-QUOTE.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Truncate over-cap heartbeat ipAddresses instead of failing closed (audit R7-IPCAP)

The agent's collect_ip_addresses() reports every address from 'hostname -I'
uncapped, but the heartbeat schema caps ipAddresses at .max(16). A multi-homed
node (>16 IPs: IPv6 SLAAC/privacy + Docker/libvirt/VLAN bridges) had every
heartbeat rejected 400 — and since the agent freezes the IP list at startup,
that desynced the node forever, freezing lastSeenAt and flipping the live,
recording node offline. A liveness heartbeat must not fail closed over a
cosmetic field: preprocess ipAddresses to truncate to the documented cap and
accept the heartbeat (keeping the first 16, the primary addresses). Red->green
route test (20 IPs -> 202 + truncated, was 400). Agent-side .take(16) noted as
defensive follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Update gap-hunt ledger: Run 7 dirty, R7-IPCAP heartbeat desync fixed (streak 0)

Rust agent-contract angle found the agent's uncapped ipAddresses vs the
heartbeat .max(16) reject -> permanent desync for multi-homed nodes; fixed
controller-side (truncate). H4-2W verified sound. New catalogue: R7-IP-AGENT-CAP,
R7-NUMCOMMIT-HEX, R7-SEED-LIVENESS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Keep a stale retention selection visible in the schedule form (audit R8-RETENTION-SELECT)

The schedule form wrapped its recording-profile and watchdog Selects in
withSelectedOption (which prepends a synthetic option so a current-but-absent
id stays visible), but the retention Select rendered the raw list. A stale/
deleted retention id (a prefilled controller-settings default, or an edited
schedule whose policy was removed) made the controlled Select fall back to its
placeholder — reading as 'unselected' while the draft silently kept and
re-saved the id. Route retention through the same helper (moved to schedule-draft
as a shared, now-tested export). Red->green helper test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Update gap-hunt ledger: Run 8 dirty, R8-RETENTION-SELECT fixed (streak 0)

Broad web sweep found the schedule-form retention Select not wrapped in
withSelectedOption (stale id renders blank but re-saves); fixed. R7-IPCAP
verified sound+complete. Runs 6-8 each closed one marginal PR-surface defect.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Update gap-hunt ledger: Run 9 clean (streak 1/5)

Adversary verified R8 sound; completeness critic over all 85 changed files
found no new real defect. Zero code changes. R9-NODEIFACE-SELECT catalogued
(pre-existing/bounded). First clean run after the 6-8 dirty streak.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Update gap-hunt ledger: Run 10 clean (streak 2/5)

Security-lens + production-scenario edge passes both clean; zero code changes.
Cross-replica schedule double-fire investigated -> documented single-writer
constraint, rejected. Two consecutive clean runs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Update gap-hunt ledger: Run 11 clean (streak 3/5)

Fresh-eyes dense-logic re-read + test-durability critic (all 18 fixes'
tests re-derived to catch a revert) both clean; zero code changes. Three
consecutive clean runs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Update gap-hunt ledger: Run 12 clean (streak 4/5)

Diff-as-reviewer (raw PR hunks) + React effect/query-timing passes, both
clean (run inline after session limits killed the subagents; diff-reviewer
rerun independently confirmed). Zero code changes. Four consecutive clean runs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Gap-hunt audit converged: 5 consecutive clean runs (Runs 9-13)

Run 13 final convergence: last-chance broad sweep + agent-contract re-confirm
both clean; full 'mise run check' (incl. rust+miri+fake-controller-smoke+
db:verify+all baselines) and 'mise run build' green. 18 findings fixed across
13 runs against static base 619b6f1; remaining opens all catalogued cosmetic/
suspected/pre-existing/by-design.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Handle catalogued audit entries: API hardening

Post-convergence cleanup of the 2026-07-08 gap-hunt catalogue (API items):

- R4-1: dedup uploadPolicyIds server-side in buildSchedule +
  sanitizeScheduleUpdate (mirrors uniqueTags) so the server is authoritative.
- R4-2: isolate each node's reconcile in reconcileNodeLivenessEvents with a
  per-node try/catch -> {skipped, reconcile_failed} so one failing node no
  longer aborts the whole tick.
- H1-3: treat a provisioning node as unavailable in node-action-routes with an
  accurate node_provisioning reason (not node_offline).
- N-3A/3B: agent-release fetch now follows Link rel=next (bounded to 5 pages,
  same-origin) and reads the body through a bounded reader (Content-Length
  pre-check + streaming byte cap); an over-cap body is rejected, last-good kept.
- N-COV-FETCH + R11: lock the GitHub request contract and the uploadPolicyIds
  >32 rejection.

Each with a red->green/coverage test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Handle catalogued audit entries: web console fixes

Post-convergence cleanup of the 2026-07-08 gap-hunt catalogue (web items):

- R3-1 (operator decision: Critical): offline node status now reads critical
  (red); nodeStatusBadgeClass rewritten as an exhaustive switch.
- R3-2: toneFillClass gets an explicit neutral (muted) branch.
- H4-2/H4-3: extract the buffered NumberField into shared settings-fields and
  wire it into the recording-profile, upload-policy, and watchdog cards;
  retention's optionalNumber routes through numericInputCommit.
- R7-NUMCOMMIT-HEX: numericInputCommit gates on a decimal-shape regex.
- R9: node/interface selects show a synthetic "(unavailable)" option for a
  stale id (mirrors withSelectedOption).
- R6-INSTALL-URL-QUOTE: shell-quote every interpolated install-command value.
- H2: nodeStatusLabel (Title Case) replaces raw status on dashboard,
  room-detail, and the nodes table.
- S2: remove the dead schedulingDefaultsFrom export.
- S3: defaultDraft validates operator defaults against available lists
  (threaded from schedules list + calendar via cached query data).
- W2: TruncateCell effect depends on [children] (no per-render observer churn).
- W3: nodes.tsx omits the Actions column entirely for read-only users.
- W4A: withWatchdogDisplayDefaults folds display fallbacks into the draft so the
  watchdog form is what-you-see-is-what-you-save.
- R6-ENROLL-DUP: invalidate ["nodes"] on enroll error too.

Unit-tested where a seam exists (node-status, settings-updates, schedule-draft,
node-page-helpers); render-only items verified via tsc + oxlint + build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Cap recorder-agent ipAddresses at the documented heartbeat limit

R7-IP-AGENT-CAP: refactor collect_ip_addresses to a pure parse_ip_addresses
helper bounded to MAX_IP_ADDRESSES (16, matching nodeHeartbeatSchema), so a
well-behaved agent never emits a payload the controller must truncate. Pure
Rust unit test (cap at 16, normal parse, empty).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Record post-convergence catalogued-entry cleanup in the gap-hunt ledger

Document the disposition of the 2026-07-08 gap-hunt catalogue after convergence:
21 items fixed (each with a test where a unit seam exists), 6 deferred with
rationale (render-harness-gated or product decisions, plus R7-SEED-LIVENESS as
won't-fix). Records the two operator product decisions (offline tone -> Critical;
switcher password-clear -> defer) and the green full-check gate at close.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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.

1 participant