feat(bin): serve the Action Deck as a private /deck page on the bridge - #122
Merged
Merged
Conversation
Add `fm-deck.sh --json`, which emits the pane's payload as one structured `fm-deck.v1` model, and split fm-deck-render.py's section builders into shared row/group helpers so the terminal frame and the model apply the same reading rules. The terminal rendering is unchanged. Add `GET /deck` and `GET /api/deck` to fm-bridge-view.py behind the existing session cookie, Host check, CSP and security headers. The deck read runs as the same scrubbed bounded child as the bearings snapshot, is cached, and returns 503 with the collector's reason on failure. Live asks are the headline; held backlog decisions, finished workers, completions and loose ends are folded away from the actionable lists. Only ordinary https URLs become links; tray targets and local paths render as text. The page carries no approve, run or merge control and says why: the gateway has no executor and approval needs the captain secret on the desk. Tests cover the JSON model, the route, the auth boundary, link vetting, the read-only guarantee, the 503 path and the page rendering. Docs point at the new page from bridge-view.md, ops-command-center.md and scripts.md.
… overlay On a first-load failure the deck raised the full-page 'Cannot reach the desk.' overlay and wrote the collector's reason underneath it, so the captain never saw the reason. The header and every region already say the desk is unreachable and #deck-error carries the reason; the overlay is now reserved for a tab whose refreshes stop after a successful load. The rendering test drives refresh() against a fake 503 and asserts the overlay stays off while the reason is shown.
…tranks /usr/bin Adding python3 ahead of jq in CHILD_PATH_TOOLS let /usr/bin lead the child PATH on Linux, where python3 lives there, so the real /usr/bin/tmux outranked the test's fake tmux and the live observation read a paused worker as Stuck.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Build the captain's private web Action Deck as a real implementation on the existing Firstmate bridge (bin/fm-bridge-view.py + .sh), reusing the existing fm-deck.sh collector and fm-deck-render.py as the single source of the deck payload: expose the current payload as one structured JSON model (fm-deck.sh --json) and serve GET /deck (page) and GET /api/deck (model) behind the bridge's existing session cookie, Host check, CSP and security headers, plus a nav entry from the glance page. No second database, no new public service, no new UI framework, no generic shell endpoint or unconstrained command runner, no automatic FOTA sending, order booking or new privileged service. The Action Deck is for automatable actions the captain can review and fire off quickly; daily to-dos and the broad fleet backlog are separate, so held backlog decisions must not be rendered as the captain's obligations or as 'needs you' counts. The UI must clearly distinguish ready-to-run actions, staged actions needing inputs or approval, running work, and outcomes; hide completed work from the actionable default view; show honest updated-at (taken from the collector's own sample time, not browser receipt time), source, error, empty and unconfirmed states, identifiers and relevant source links. Use only real existing staged action records and never invent ready actions or executable controls; because the existing action gateway's execute is unwired and approval needs the captain secret on the desk, the page carries no Run/Approve/Merge control and says exactly what is missing. Only ordinary https URLs become clickable links; tray targets and local paths render as text, never arbitrary filesystem exposure. Compact, responsive, keyboard-accessible, mobile-friendly layout; no wall of terminal output. Terminal deck rendering stays byte-identical. Tests cover the JSON model, the route, the auth boundary, link vetting, the read-only guarantee, the 503 error path and page rendering; docs point at the new page. Accepted review decisions already carried on the branch: the deck header reports the collector sample age from read_at/server_unix rather than client time, and the duplicate setCount helper is dropped in favour of setText. No agent co-author attribution in commits. No production deployment.
What Changed
bin/fm-deck.shgains a--jsonmode that emits the collector payload as one structuredfm-deck.v1model (needs_you / staged / under_way / outcomes) viabin/fm-deck-render.py, which also grows the JSON serializer and https-only link vetting; terminal rendering with--oncestays byte-identical to base.bin/fm-bridge-view.pyservesGET /deck(compact, responsive, keyboard-accessible page) andGET /api/deck(model) behind the existing session cookie, Host check, nonce CSP and security headers, plus a nav entry from the glance page. The page carries no Run/Approve/Merge control and states what is missing, renders tray targets and local paths as text, hides finished work by default, reports the collector sample age fromread_at/server_unix, and on a first-load 503 shows the reason inline instead of the full-screen overlay.tests/fm-deck.test.shandtests/fm-bridge-view.test.shcover the JSON model, the route and auth boundary, link vetting, the read-only guarantee, the 503 path and page states;docs/bridge-view.md,docs/architecture.md,docs/ops-command-center.mdanddocs/scripts.mdpoint at the new page. The pipeline Review left two informational notes (duplicate inline https rule inpinned_links_html, shared 20s collector timeout) that are not addressed here.🤖 Generated with Claude Code
Risk Assessment
✅ Low: The terminal renderer refactor is output-preserving (verified branch by branch against the old code), the new routes sit behind the existing Host check, session cookie and CSP, links are vetted server-side and again client-side with only https allowed, no run/approve control exists, the child runs in the same scrubbed bounded environment as the bearings snapshot, and tests cover the JSON model, route, auth boundary, link vetting, read-only fingerprint, 503 path and page rendering as the intent requires.
Testing
Ran the deck and bridge-view suites (all passing), then stood up a real bridge with the deck fixture plus a second bridge with a failing collector and drove both through headless Chromium as the https tailnet origin, capturing the login gate, glance nav, desktop and mobile deck renders, expanded folds, keyboard focus, and the 503 state with the reason visible and no overlay; also recorded curl transcripts for the auth boundary, security headers, JSON model and 503 body, and proved the terminal pane is byte-identical between base and target with a frozen clock. Everything matched the stated intent.
/var/folders/2c/9sf1hvpn23q336b08gs1np0c0000gn/T/no-mistakes-evidence/01M290766C8B9TVTNTK4CQVE31/03-deck-desktop-1280.png)/var/folders/2c/9sf1hvpn23q336b08gs1np0c0000gn/T/no-mistakes-evidence/01M290766C8B9TVTNTK4CQVE31/06-deck-mobile-390.png)/var/folders/2c/9sf1hvpn23q336b08gs1np0c0000gn/T/no-mistakes-evidence/01M290766C8B9TVTNTK4CQVE31/04-deck-desktop-folds-expanded.png)/var/folders/2c/9sf1hvpn23q336b08gs1np0c0000gn/T/no-mistakes-evidence/01M290766C8B9TVTNTK4CQVE31/05-deck-asks-keyboard-focus.png)/var/folders/2c/9sf1hvpn23q336b08gs1np0c0000gn/T/no-mistakes-evidence/01M290766C8B9TVTNTK4CQVE31/07-deck-503-desk-unreachable-desktop.png)/var/folders/2c/9sf1hvpn23q336b08gs1np0c0000gn/T/no-mistakes-evidence/01M290766C8B9TVTNTK4CQVE31/08-deck-503-desk-unreachable-mobile.png)/var/folders/2c/9sf1hvpn23q336b08gs1np0c0000gn/T/no-mistakes-evidence/01M290766C8B9TVTNTK4CQVE31/02-glance-with-action-deck-nav.png)/var/folders/2c/9sf1hvpn23q336b08gs1np0c0000gn/T/no-mistakes-evidence/01M290766C8B9TVTNTK4CQVE31/01-deck-unauthenticated-login-gate.png)Evidence: HTTP transcript: auth boundary, Host check, headers, /api/deck model, nav, 503 body
Evidence: Terminal pane byte-identical between base and target
fm-deck.sh --once, same fixture home, FM_DECK_NOW=1789157400 fixture home base sha256: 0540d919...c557b8 fixture home target sha256: 0540d919...c557b8 -> cmp: byte-identical empty home base sha256: 8c3041d1...52eb2 empty home target sha256: 8c3041d1...52eb2 -> cmp: byte-identicalEvidence: Browser observations: header text, counts, buttons, links with rel/target, tab order, mobile scrollWidth, 503 overlay state
buttons on deck page: 1 ['Log out'] links: ('/', None, None), ('kunchenguid#9;, '_blank', 'noopener noreferrer'), ('https://gitlab.example/artevo/site/-/merge_requests/3', '_blank', 'noopener noreferrer') mobile scrollWidth at 390 viewport: 390 503: header 'Cannot reach the desk.', error 'deck collector exploded: tray log unreadable', overlay visible FalsePipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
bin/fm-bridge-view.py:2024- pinned_links_html still carries its own inline copy of the https-only link rule (scheme == https, netloc present, no whitespace) while the new safe_https() docstring says it is 'the same rule the pinned links use'. Calling safe_https(url) here would make the two link vetting paths one function so they cannot drift (safe_https additionally caps length at 2048, which is harmless for pinned links).bin/fm-bridge-view.py:1204- run_deck reuses SNAPSHOT_TIMEOUT_SECONDS (20s) via _bounded_process_output. fm-deck.sh --json shells out to fm-tray.sh json (full audit-log fold), fm-order.sh list and tasks-axi list; on a large audit log a slow but healthy read becomes a 503 'action deck timed out' rather than a deck. The page copes honestly (503 path and 25s client abort are aligned), so this is a tuning note only.🔧 **Test** - 1 issue found → auto-fixed ✅
bin/fm-bridge-view.py:2326- When /api/deck answers 503 on first load, the deck JS sets the full-screen #stale overlay ("Cannot reach the desk.") and also writes the reason into #deck-error, but the overlay (position: fixed; inset: 0; z-index 9) covers the whole page, so the captain never sees the reason. Screenshot deck-collector-error-503-mobile.png shows only the overlay; deck-collector-error-503-underlying-page-overlay-removed.png shows the hidden reason. The glance uses the same overlay on first failure, so this matches the existing pattern, but the intent and docs/bridge-view.md say the page shows "Cannot reach the desk" and the reason. Decide whether the deck should skip the full-screen overlay on first failure (or render the reason inside it).bash tests/fm-deck.test.sh(35 ok, incl.test_json_mode_emits_the_pane_as_one_model,test_json_mode_degrades_honestly_and_scrubs)bash tests/fm-bridge-view.test.sh(37 ok, incl.test_deck_page_and_api,test_deck_api_reports_desk_unreachable,test_deck_page_renders_states_without_run_controls)Byte-identity: captured the exact sentinel payloadbin/fm-deck.sh --oncepipes to the renderer over the deck test's full fixture home and an empty home, rendered it withgit archive 41e98a0 binfm-deck-render.py and the branch renderer,cmpat widths 80/100/120/160 → identical; same payload through--jsongives schema fm-deck.v1 with needs_you=4 staged=3 under_way=3; base--jsonexits 1Live bridge via curl:GET /api/deckwithout session → 401;GET /deckwithout session → login page, no deck markup;GET /deckwith Host 192.168.1.20 → 403; authenticated/deck→ 200 with nonce CSP, Referrer-Policy, X-Frame-Options, nosniff, single<button>(Log out), only/logoutform action, no Run/Approve/Merge control; authenticated/api/deck→ 200 JSON, Cache-Control no-store, home fingerprint unchanged; brokenfm-deck.shroot → 503{"error":"desk unreachable","detail":"deck collector exploded: tray log unreadable"}Playwright (chromium) against the live bridge with Host+cookie injected via route: screenshots at 390px and 1100px, unauthenticated /deck, glance nav → click Action Deck lands on /deck, held-decisions/finished/landed/loose-ends folds opened via keyboard Enter, Tab order recorded (Log out → Fleet glance → https links → fold summaries), device:// targets and data/ report path rendered as text with zero links, all https links target=_blank rel=noopener, header "Read just now" → "Read 7 seconds ago" ticking from read_at, api read_at == server_unix, ready == []Verified no Co-Authored-By lines in the two branch commits;git statusclean after cleanup of temp scripts🔧 Fix: deck first-load 503 shows reason, no overlay
✅ Re-checked - no issues remain.
bash tests/fm-deck.test.sh(35 ok, including the two new --json model tests)bash tests/fm-bridge-view.test.sh(37 ok, including test_deck_page_and_api, test_deck_api_reports_desk_unreachable, test_deck_page_renders_states_without_run_controls)curl transcript against a live bridge: anonymous GET /deck (login page, no deck markup), GET /api/deck 401, LAN Host 403, POST /login then GET /api/deck 200 fm-deck.v1 with security headers, GET /deck headers and single Log out button, glance nav entry, broken-collector GET /api/deck 503 with reasonPlaywright headless Chromium, requests relayed to the loopback bridge as https://bridge.test.example: login through the real form, glance nav click-through to /deck, desktop 1280 and mobile 390 screenshots, folds expanded, Tab-order walk and focus ring on an https ask link, 503 desktop and mobile screenshotsfm-deck.sh --onceat base 41e98a0 vs target HEAD on the same fixture home and an empty home with FM_DECK_NOW frozen: sha256 and cmp byte-identicalTwo /api/deck reads 3s apart confirm read_at is the cached collector sample used for the header age✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.