Skip to content

feat(boards): drive every configured board from its own schedule (#1243 first slice)#1395

Open
jeffredodd wants to merge 7 commits into
nextfrom
feat-1243-per-board-driving-slice
Open

feat(boards): drive every configured board from its own schedule (#1243 first slice)#1395
jeffredodd wants to merge 7 commits into
nextfrom
feat-1243-per-board-driving-slice

Conversation

@jeffredodd

Copy link
Copy Markdown
Member

Summary

Closes the two product gaps pinned as test.fixme in #1386: the display engine now drives every configured board, not just boards[0], and /settings/board mutations rebuild the board clients so output never targets a removed board's connection.

  • _build_board_clients builds one client per board with credentials (board_clients map); vb_client stays the primary's client for all single-board code paths.
  • _update_secondary_boards: each enabled, unpaused, schedule-enabled secondary board resolves its own active page (schedules → per-board default, collections included) and receives it via its own client, with per-board content caching, per-page transitions, and failure isolation (one board erroring never blocks the others). Global silence mode freezes secondaries.
  • /settings/board PUT/add/DELETE now reinitialize the clients (the former stale-client bug: after deleting board 1, content kept flowing to the removed board's hardware until restart).
  • /force-refresh clears every board client's cache.
  • Page-driven sends use resolve_dimensions() so note_array pages no longer raise in the send path (issue Per-board #2: per-board display engine (DisplayService → runtimes) #1243 item 9).
  • /pages/{id}/send returns a structured error body when the board is unreachable (nginx swallows 502/503/504 on /api/).

Scope note: this is the first slice of #1243, not the full BoardRuntime refactor — triggers, temporary overrides, the manual active page, and board-read polling remain primary-only; the issue stays open for that work.

Tests

  • tests/test_multi_board_driving.py — 13 unit tests: client-per-board building, reinit pruning, per-board routing, pause/schedule/enabled gates, silence, unchanged-content skip, send-failure no-cache.
  • The two test.fixme pins in web/tests/multi-board-output.spec.ts are now active tests, plus three new mixed-mode scenarios: both boards showing their own scheduled content from one refresh; board 2's schedule mode off silencing only board 2; board 1 in manual mode showing its manual page (its own schedule ignored) while board 2 delivers scheduled content.
  • Verified against a fresh production-image container built from this branch: multi-board-output 13/13 passed (0 skipped — the suite has no remaining fixmes), neighboring suites (multi-board, multi-board-schedule, mock-board, note-multiboard-extended, note-pages) 59/59 passed, full platform pytest 4162 passed in Docker.

🤖 Generated with Claude Code

jeffredodd and others added 2 commits July 18, 2026 14:09
… first slice)

The display engine now builds one client per configured board and, on each
update cycle, resolves and delivers every enabled, unpaused,
schedule-enabled board's own active page via its own client — where
previously only boards[0] was ever driven.

- _build_board_clients: one client per board with credentials (board_clients
  map); vb_client remains the primary board's client for the single-board
  code paths. Reinit no longer blocks on board I/O inside request handlers.
- _update_secondary_boards: schedule + per-board default page, per-board
  pause/schedule_enabled/enabled gates, collections, per-page transitions,
  per-board content cache, failure isolation (one board's error never
  blocks another). Silence mode freezes secondaries; triggers, temporary
  overrides and the manual active page remain primary-only for now.
- /settings/board PUT/add/DELETE now rebuild the board clients, fixing the
  stale-client bug where output kept targeting a removed board's
  connection until restart.
- /force-refresh clears every board client's cache, not just the primary.
- Page-driven sends use resolve_dimensions() so note_array pages no longer
  raise in the send path.
- /pages/{id}/send returns a structured error body when the board is
  unreachable (nginx swallows 502/503/504 on /api/).
- E2E: the two test.fixme gap pins flip to active tests, plus new
  mixed-mode coverage: both boards showing their own content from one
  refresh, and schedule mode off for one board silencing only that board.

Remaining #1243 scope (BoardRuntime refactor, per-board read polling,
per-board triggers/overrides) is unchanged and tracked on the issue.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…enario

With schedule mode off, the primary board falls back to its manual active
page (and defaults to the first page rather than going dark), so "board 1
receives nothing" was never the contract. Assert what users actually get:
board 1 shows its manual page and ignores its own schedule while board 2
delivers scheduled content, with no cross-board bleed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The board selector is the app-wide context switcher — it scopes every
page below it — so it now leads the menu instead of sitting below the
primary navigation:

- Desktop: directly under the logo in its own zone, restyled as a
  context chip on the sidebar surface (translucent accent background,
  sidebar foreground text) instead of a bare form control. Collapsed
  mode keeps the icon-only trigger with the current-board tooltip.
- Mobile: promoted out of the drawer into the always-visible header
  bar, so the current board is always on screen and switching takes
  one tap without opening the menu (it also stays reachable while the
  drawer is open). The drawer no longer duplicates it.

Renders only for multi-board installs, as before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added minor and removed minor labels Jul 19, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: no a11y concerns.

The only web change is web/src/components/navigation-sidebar.tsx, which relocates the multi-board BoardSelector (top of the desktop sidebar + a new mobileHeader variant in the always-visible header) and adds a variant prop. No new interactive elements, strings, headings, or landmarks are introduced — it re-parametrizes styling on the existing Radix SelectTrigger.

A11y checks against FiestaBoard conventions / WCAG 2.2 AA:

  • Accessible name (4.1.2/1.3.1): ✅ The SelectTrigger keeps aria-label={t("boardSelector")} (navigation-sidebar.tsx:708), so the control is named in both variants even when the visible label is truncated (max-w-[170px]) or hidden in the collapsed sidebar (max-w-0 opacity-0).
  • Strings via next-intl (i18n): ✅ No hardcoded literals added. boardSelector and selectBoard already exist under the navigation namespace in web/messages/en.json (lines 81–82); no web/messages/** changes needed.
  • Semantics (2.1.1/4.1.2): ✅ Still a Radix Select trigger — no new div role=button / unlabeled onClick.
  • No duplicate control: ✅ The old bottom-of-nav and mobile-menu instances are removed; mobile now has exactly one selector (header), desktop one (top of sidebar), each in its own responsive branch.
  • Headings/landmarks (1.3.1): ✅ Unchanged; existing <nav aria-label> landmarks preserved.

No blockers, no suggested fixes.

(Note, low priority / out of scope: the decorative Monitor icon has no aria-hidden, but this is pre-existing and harmless here since the aria-label sets the trigger's accessible name.)

jeffredodd and others added 3 commits July 18, 2026 18:06
The multi-board specs all ran flagship+flagship; this closes the mixed-
device gap: one refresh delivers correctly-sized content to each device
(6×22 grid to the flagship, 3×15 to the note board, dimension + token
asserts, no cross-device leakage in either direction), and the board
selector switches cleanly between a flagship and a note board on
/schedule. Uses the previously-unused board2DeviceType helper support.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A new page's device type was locked to the Pages tab the user came from
(/pages/new?device=…) — starting on the wrong tab meant recreating the
page. The editor now shows a board-size switcher (Flagship/Note) next
to the size indicator for NEW pages; the grid resizes live and the
chosen type is what gets saved. Existing pages stay locked: converting
saved 6×22 content to 3×15 is lossy, so that remains out of scope.

TDD: unit specs (switcher resizes to 3×15, absent for existing pages)
and an e2e (flagship-entry page saved as a note page) were written and
verified failing before the implementation. The aria-label is
translated in all 14 locales.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
"removing Note board hides Note tab" failed deterministically whenever
an earlier suite's cleanup left a note-typed page behind: deleting the
last page auto-creates a Welcome typed to the deleted page's device,
and pages keep their device tab alive by design (#943). Harden the test
to clear lingering note pages (anchored so the store never empties),
add a deletePagesByDevice helper, and make the suites that create note
pages clean them up note-first so the regenerated Welcome is flagship.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added minor and removed minor labels Jul 19, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: no a11y concerns. The web UI changes are accessible and i18n-clean.

Reviewed the two TSX files (web/src/components/page-builder.tsx, web/src/components/navigation-sidebar.tsx) and all 14 web/messages/*.json changes on the a11y dimension only.

What I checked

  • New board-size switcher (web/src/components/page-builder.tsx:1481) — implemented as a shadcn Select with a proper accessible name via aria-label={t("deviceTypeSwitcherAriaLabel")} on the SelectTrigger. Native semantics, keyboard-operable, no icon-only-button gap. (WCAG 4.1.2 / 2.1.1)
  • Strings via next-intl — the new aria-label resolves through the pageBuilder namespace, and deviceTypeSwitcherAriaLabel is present in en.json (under pageBuilder) and translated in all 13 other locales — none left as raw English. The option labels reuse existing displaySettings.flagshipLabel / noteLabel keys. No hardcoded literals. (WCAG 1.3.1 / 3.1.2)
  • BoardSelector mobileHeader variant (web/src/components/navigation-sidebar.tsx) — the refactor only changes placement/styling; the trigger keeps its aria-label={t("boardSelector")}, so the accessible name is preserved across both variants. Responsive duplication (sidebar vs. header) is standard and each instance is viewport-scoped. (WCAG 4.1.2)
  • Headings / landmarks / images — no changes; the header and nav aria-label landmarks and heading structure are untouched.

No blockers, no fixes needed. Sign-off stays human.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docs review

Verdict: one small accuracy fix - otherwise clean.

The two new coverage rows (multi-board-mixed.spec.ts, page-device-switch.spec.ts) are accurate: both specs exist at head and the concrete claims check out (Flagship 6x22, Note 3x15, the /schedule device-type selector, and device_type=note persistence). Examples-first voice, no marketing fluff, no branding/privacy/link issues.

One staleness finding is inline below - click Apply suggestion to commit it.

🤖 Auto-review by claude-docs-review.yml.

Comment thread web/tests/COVERAGE.md
@@ -36,6 +36,8 @@ Playwright browser → Next.js UI → FastAPI backend → Mock Vestaboard API
| `multi-board.spec.ts` | Multi-board | Board card display, board CRUD (add/rename/type/color/toggle/delete), wizard board type/color picker, cross-feature config |
| `multi-board-schedule.spec.ts` | Multi-board scheduling | Single-board schedule page, two-board board selector, schedule mode toggle, per-board CRUD, filtering by board_id |
| `multi-board-output.spec.ts` | Multi-board output | Two boards with real mock connections (ports 7000/7001): manual send + scheduled send land on the right board's hardware, cross-board leak checks, per-board pause isolation, per-board schedule list consistency, board-deletion consistency, board-selector UI clarity (name shown, view fully swaps, per-board toggle). Known gaps as fixme: board 2 driving (epic #1241), client reinit on board mutations |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This row still lists board 2 driving (epic #1241) and client reinit on board mutations as fixme gaps - but the headline change of this PR is that those two test.fixme pins are now active tests. The Known gaps as fixme clause is now stale; fold those items into the covered list instead:

Suggested change
| `multi-board-output.spec.ts` | Multi-board output | Two boards with real mock connections (ports 7000/7001): manual send + scheduled send land on the right board's hardware, cross-board leak checks, per-board pause isolation, per-board schedule list consistency, board-deletion consistency, board-selector UI clarity (name shown, view fully swaps, per-board toggle). Known gaps as fixme: board 2 driving (epic #1241), client reinit on board mutations |
| `multi-board-output.spec.ts` | Multi-board output | Two boards with real mock connections (ports 7000/7001): manual send + scheduled send land on the correct board hardware, cross-board leak checks, per-board pause isolation, per-board schedule list consistency, board-deletion consistency, board-selector UI clarity (name shown, view fully swaps, per-board toggle), board 2 driving, and client reinit on board mutations |

The client-map build pre-filtered boards on local_api_key/cloud_key, so
note-array boards (which authenticate with note_array_token) were
silently dropped: a solo array install fell back to the legacy Config
client and secondary arrays were never driven at all (issue #1243
item 3). Drop the pre-filter — board_client_from_board_dict already
returns None for any board without a usable connection.

TDD: a unit test (note-array board with only a token gets a client) and
a 14-test e2e suite were written and verified failing first. The new
note-array-output.spec.ts proves, against the size-strict Cloud API
mock:
- solo array installs deliver scheduled AND manual content at the
  array's exact geometry
- every geometry class: all five named presets, 1×1, the 8-wide /
  8-tall / 8×8 maximums, and a non-preset size (3×15 up to 24×120)
- a flagship + array fleet is driven independently in one refresh with
  no cross-device leakage, and pausing the array blocks only the array
- per-test unique tokens respect the module-level ≥15s note-array send
  throttle

CI: the e2e job now runs a mock-cloud container per worker and passes
VESTABOARD_CLOUD_API_URL to the app containers (integration-tests.yml
started the mock but never told the backend where it was); helpers gain
worker-aware MOCK_CLOUD_URL + state/configure accessors. Also restores
prettier formatting of COVERAGE.md (lint-web failure on the previous
push).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added minor and removed minor labels Jul 19, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: no a11y concerns. The web UI changes are accessible as written — nothing to fix.

Reviewed only the accessibility dimension of the two changed TSX files (web/src/components/navigation-sidebar.tsx, web/src/components/page-builder.tsx) plus the web/messages/** additions.

What I checked and confirmed:

  • page-builder.tsx:1252 — new device-type Select. The icon-free trigger carries an accessible name via aria-label={t("deviceTypeSwitcherAriaLabel")} (WCAG 4.1.2), and the SelectItems use displaySettings.flagshipLabel / displaySettings.noteLabel rather than hardcoded English. Semantics/keyboard come from Radix Select. ✅
  • next-intl coverage. pageBuilder.deviceTypeSwitcherAriaLabel is added to all 14 locale files with real translations (not left as raw English in the non-English files — e.g. de "Boardgröße ändern", ja "ボードサイズを変更"), and flagshipLabel/noteLabel already exist in displaySettings. No hardcoded user-facing strings introduced. ✅
  • navigation-sidebar.tsx — new mobileHeader variant + relocated sidebar BoardSelector. Both instances keep the existing aria-label={t("boardSelector")} on the SelectTrigger, so the control stays named in every layout including the collapsed icon-only desktop state. The Monitor icon sits inside a trigger whose aria-label supplies the name, so it isn't separately announced. Only Tailwind/layout classes and placement changed. ✅
  • No heading/landmark changes, no new images, no focus-management or live-region surfaces added.

Nothing here trips the axe 2.2 AA gate in web/tests/a11y.spec.ts. Sign-off left to a human.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docs review

Verdict: one stale-accuracy fix; otherwise clean.

Only web/tests/COVERAGE.md is a doc change. The three new rows (multi-board-mixed, note-array-output, page-device-switch) all check out against the actual specs — geometry list, dimension asserts, device-switch behavior, and the /schedule selector test all match. Voice, branding (FiestaBoard vs. Vestaboard), and privacy are fine.

One substantive finding inline: this PR's own code changes make the existing multi-board-output.spec.ts row inaccurate — it still lists the two test.fixme gaps (board 2 driving, client reinit) that this PR converts into passing tests. Suggestion below updates that row.

🤖 Auto-review by claude-docs-review.yml.

Comment thread web/tests/COVERAGE.md
@@ -36,6 +36,9 @@ Playwright browser → Next.js UI → FastAPI backend → Mock Vestaboard API
| `multi-board.spec.ts` | Multi-board | Board card display, board CRUD (add/rename/type/color/toggle/delete), wizard board type/color picker, cross-feature config |
| `multi-board-schedule.spec.ts` | Multi-board scheduling | Single-board schedule page, two-board board selector, schedule mode toggle, per-board CRUD, filtering by board_id |
| `multi-board-output.spec.ts` | Multi-board output | Two boards with real mock connections (ports 7000/7001): manual send + scheduled send land on the right board's hardware, cross-board leak checks, per-board pause isolation, per-board schedule list consistency, board-deletion consistency, board-selector UI clarity (name shown, view fully swaps, per-board toggle). Known gaps as fixme: board 2 driving (epic #1241), client reinit on board mutations |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This row is now stale: the same PR removes both test.fixme blocks in multi-board-output.spec.ts and turns them into passing tests (board 2 per-board driving via #1243, and the primary-board-removal redirect), plus adds simultaneous-schedule, schedule-mode-off, and mixed manual/scheduled cases. "Known gaps as fixme: board 2 driving (epic #1241)…" no longer describes the file.

Suggested change
| `multi-board-output.spec.ts` | Multi-board output | Two boards with real mock connections (ports 7000/7001): manual send + scheduled send land on the right board's hardware, cross-board leak checks, per-board pause isolation, per-board schedule list consistency, board-deletion consistency, board-selector UI clarity (name shown, view fully swaps, per-board toggle). Known gaps as fixme: board 2 driving (epic #1241), client reinit on board mutations |
| `multi-board-output.spec.ts` | Multi-board output | Two boards with real mock connections (ports 7000/7001): manual send + scheduled send land on the right board's hardware, cross-board leak checks, per-board pause isolation, per-board schedule list consistency, board-deletion consistency, board-selector UI clarity (name shown, view fully swaps, per-board toggle). Per-board driving (#1243): both boards in schedule mode update simultaneously, board 2 receives its own scheduled content, per-board schedule-mode gating (mixed manual/scheduled), removing the primary board redirects output to the promoted board |

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant