Skip to content

feat(devices): note-array support [WIP]#1185

Draft
jeffredodd wants to merge 17 commits into
mainfrom
next
Draft

feat(devices): note-array support [WIP]#1185
jeffredodd wants to merge 17 commits into
mainfrom
next

Conversation

@jeffredodd

Copy link
Copy Markdown
Member

This branch contains an active PR showing the progression of Note Array support

…1184)

* feat(devices): add note-array dimension model (generalized W×H)

Implements #1183 (part of epic #1167). Adds note_array device type,
NOTE_ROWS/NOTE_COLS/MAX_NOTES_PER_AXIS, NOTE_ARRAY_PRESETS,
note_array_dimensions/is_note_array/is_valid_note_array_grid/resolve_dimensions,
and notes_wide/notes_tall on BoardInstance. No pages.json migration needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* style(devices): apply ruff format to note-array model

Matches CI ruff format --check (dev container lacks ruff). No logic change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(devices): reject bool for notes_wide/notes_tall

bool is a subclass of int, so True/False slipped through the int guard in
BoardInstance.__post_init__. Reject bools explicitly and default to 1.
Adds regression tests. (Task review finding on #1183.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(devices): clarify get_dimensions error for note_array callers

get_dimensions() only resolves static DEVICE_DIMENSIONS entries;
after DEVICE_TYPES was expanded to include 'note_array', the old
error message ('Must be one of {DEVICE_TYPES}') became misleading
because it implied note_array was a valid argument.

The message now names get_dimensions() supported types explicitly
and points callers to resolve_dimensions(). A new test covers the
note_array ValueError path and anchors on the hint text.

Co-authored-by: Jeffrey D Johnson <jeffredodd@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
* feat(devices): note-array credentials & board config [#1170]

Add `note_array_token: str = ""` to `BoardInstance` for the
X-Vestaboard-Token credential used by Note Array hardware.

Changes:
- `src/devices.py`: new `note_array_token` field; `is_connection_configured`
  now short-circuits on `is_note_array(device_type)` and requires the token
  (plus notes_wide/notes_tall ≥ 1); `from_dict` extracts the field with ""
  default (backward-compat); `to_dict` via `asdict` serialises it for free.
- `src/settings/service.py`: add `note_array_token` to
  `BOARD_SENSITIVE_FIELDS` so it is masked in API responses and preserved
  when the UI echoes back "***".
- `src/config_manager.py`: add `note_array_token` to `SENSITIVE_FIELDS`
  (masked in `get_all_masked`) and `DEFAULT_CONFIG["board"]` (allows
  `set_board` guard to accept the field); add env-var override
  `BOARD_NOTE_ARRAY_TOKEN` in `_apply_env_overrides`.
- `env.example`: document `BOARD_NOTE_ARRAY_TOKEN` with a placeholder.
- `tests/test_devices.py`: new `TestNoteArrayToken` class (5 tests);
  extended `TestBoardInstanceConnectionConfigured` (6 new tests);
  `test_valid_api_modes_unchanged` in `TestDeviceConstants`.
- `tests/test_config_manager.py`: 4 new round-trip/masking/configured tests
  via `SettingsService`.

api_mode decision: `VALID_API_MODES` stays `("local", "cloud")`. A
note-array board uses `api_mode="cloud"` + the new `note_array_token`
field. Routing in #1168 branches on `is_note_array(device_type)`, not
`api_mode`, to pick the correct endpoint and credential.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(devices): address review findings on note-array credentials

- Drop dead W×H tautology in is_connection_configured (notes_wide/tall are
  always >=1 after __post_init__ clamping); rely on token presence.
- Strip whitespace on note_array_token in from_dict (consistent with other
  credential fields); a whitespace-only token now reads as unconfigured.
- Add tests for the BOARD_NOTE_ARRAY_TOKEN env-override path (apply-when-empty
  and do-not-clobber-existing) and for token whitespace stripping.
- Dedupe redundant VALID_API_MODES test.

Addresses task-review + @claude review findings on #1170.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added minor and removed minor labels Jun 18, 2026
* feat(api): de-hardcode board geometry for note arrays [#1171]

Replace hardcoded 6×22 literals in api_server.py (/debug/blank, /debug/fill,
/debug/info comment, _build_welcome_template, send_welcome_message) and
MessageFormatter (MAX_ROWS/MAX_COLS class constants used at 26 sites) with
resolved dimensions from resolve_dimensions(). Adds _get_first_board_dims()
helper and per-instance rows/cols to MessageFormatter. Flagship/note output
is byte-identical; note_array boards now produce correctly-sized grids.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(api): address review findings on de-hardcoded geometry

- /debug/info: size text_to_board_array to the active board's dims (closes the
  brief AC for /info); add tests asserting 3x30 for a note array and 6x22 for
  flagship.
- Remove dead DisplayService.formatter attribute (+ now-unused import); it was
  never read and would have produced flagship-sized output if wired up.
- Simplify the no-op truncation loop in MessageFormatter (byte-identical) and
  fix stale "22 characters" comments/docstring to reference self._cols.
- Make test_blank_board_success / test_fill_board_success environment-
  independent by mocking get_settings_service to flagship.
- Clarify the /info slice-cap comment (caps are flagship-oriented; final grid
  is board-sized).

Addresses task-review + @claude review findings on #1171.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added minor and removed minor labels Jun 18, 2026
…1188)

* feat(pages): render correctly at any note-array dimensions [#1173]

Add notes_wide/notes_tall fields to Page/PageCreate/PageUpdate models and
wire them through validate_config, _render_composite, and _render_template
so all centering, wrapping, truncation, and vertical-fill rendering uses the
correct dimensions for any valid note-array size (up to 12 rows × 60 cols).

Key changes:
- src/pages/storage.py: v3→v4 migration adds notes_wide=1/notes_tall=1 to
  existing pages (required by CLAUDE.md schema-versioning rules); bumps
  CURRENT_SCHEMA_VERSION from 3 to 4.
- src/pages/models.py: add notes_wide/notes_tall fields (default 1, 1–8);
  validate_config now calls resolve_dimensions(device_type, notes_wide,
  notes_tall) instead of get_dimensions() so note_array row-index bounds
  are computed correctly (e.g., 12-row array allows rows 0–11).
- src/pages/service.py: _render_composite and _render_template forward
  notes_wide/notes_tall to resolve_dimensions / render_lines.
- src/templates/engine.py: render_lines accepts notes_wide/notes_tall and
  calls resolve_dimensions instead of DEVICE_DIMENSIONS.get() (which
  silently fell back to flagship for note_array); validate_template and
  _calculate_max_line_length accept optional cols param (forward-compat).
- Tests: 54 new tests covering preset shapes (3×30, 3×60, 6×15, 12×15,
  6×30), centering/wrapping at wide cols, 12-row vertical fill, row-index
  bounds for note_array pages, v3→v4 migration, and backward-compat proofs
  that flagship/note rendering is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(pages): address review findings on note-array rendering

- Use MAX_NOTES_PER_AXIS for the Page/PageCreate/PageUpdate W×H Field bounds
  instead of hardcoded le=8 (single source of truth with devices.py).
- Add migration test for a mixed page list (some pages have the fields, some
  don't) — only the missing ones are migrated.

Deferred (with rationale, see PR comment): TS Page-type fields → #1174 (its
scope); /templates/validate cols forwarding → web-wiring follow-up (pre-existing
limitation, would conflict with in-flight #1171's api_server.py changes).

Addresses task-review + @claude review findings on #1173.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(pages): repair note-array fallout in AI generator + create path

Full-suite run (not the narrow subset) surfaced regressions from adding
notes_wide/notes_tall to Page:

- AI generator: Page(**PageCreate(...).model_dump()) passed notes_wide=None
  (PageCreate leaves W×H optional), failing Page's int validation. Bridge now
  uses exclude_none so int defaults apply, and the returned draft carries
  concrete W×H ints. Fixes ~19 tests/ai failures.
- PageService.create_page now threads notes_wide/notes_tall (defaulting to 1)
  so created note-array pages persist their size; adds tests.
- test_demo_pages: assert CURRENT_SCHEMA_VERSION == 4 (was 3) after the v3->v4
  migration bump.

Verified: tests/ all green except async tests/ai (need pytest-asyncio, absent
in dev container); with pytest-asyncio installed, tests/ai is 189 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added minor and removed minor labels Jun 18, 2026
] (#1189)

* feat(web): note-array TypeScript types + shared dimension helpers [#1174]

Mirror the Python dimension model from src/devices.py into the web app.
Single source of truth for board geometry; removes duplicated DEVICE_DIMS
literals from board-display.tsx, static-board-display.tsx, page-builder.tsx,
and src/board_html_renderer.py. Closes the DeviceType drift flagged on #1173.

- Add web/src/lib/board-dimensions.ts: NOTE_ROWS/NOTE_COLS/MAX_NOTES_PER_AXIS
  constants, 5 NOTE_ARRAY_PRESETS (exact ids/labels/values matching Python),
  noteArrayDimensions(), isNoteArray(), resolveDimensions()
- Extend DeviceType in api.ts to include "note_array"; add notes_wide/notes_tall
  to BoardInstance, Page, PageCreate, PageUpdate interfaces
- Refactor board-display.tsx and static-board-display.tsx to import
  resolveDimensions instead of maintaining local DEVICE_DIMS maps; add
  notesWide/notesTall props for future note_array rendering (#1176)
- Refactor page-builder.tsx to use resolveDimensions instead of tiptap
  constants DEVICE_DIMENSIONS (which lacked note_array)
- Update page-grid-selector.tsx deviceTypeFilter prop to accept full DeviceType
- Update src/board_html_renderer.py to use resolve_dimensions() from
  src/devices.py; render_board_html() now accepts notes_wide/notes_tall;
  render_page_preview_html() reads them from the page object
- Add 28-test vitest suite covering all 5 presets, custom W×H,
  resolveDimensions for all device types, isNoteArray

Part of #1167

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(web): address review findings on note-array TS helpers

- BoardDisplay memo comparator now includes notesWide/notesTall, so the board
  re-renders when a note_array's dimensions change (was silently swallowed).
- Fix tests/test_board_html_renderer.py: import was renamed away (DEVICE_DIMS);
  use resolve_dimensions instead. (Full `pytest tests/` caught this — the web-only
  check had missed it.)
- Remove dead _STATIC_DEVICE_DIMS dict in board_html_renderer.py; inline the
  unknown-device flagship fallback.
- page-grid-selector: `as DeviceType` instead of stale `as "flagship" | "note"`.
- board-dimensions.ts JSDoc: clarify the unknown-type fallback divergence.
- page-builder: comment the note_array W×H gap (threaded in #1178).

Verified: full pytest tests/ = 3899 passed; web vitest = 1093 passed; ruff +
prettier clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…1216)

* feat(board): note-array Cloud API send/read in BoardClient [#1168]

Add send/read paths for the Vestaboard note-array Cloud API
(https://cloud.vestaboard.com/) in BoardClient. Routing is determined
by device type via is_note_array(), not api_mode. Widen
_is_valid_character_grid to accept variable note-array grid sizes
(rows × cols, multiples of 3 × 15). Add board_client_from_board_dict
factory support for note_array device_type. Flagship and note local/RW
cloud paths are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(board): address review findings on note-array Cloud API

- send_text now guards note-array boards: Cloud API is characters-only, so it
  fails clearly (False, False) instead of POSTing to the wrong RW URL/auth.
  Adds a test asserting it never POSTs.
- Extract _note_array_headers property (was duplicated in send_characters and
  read_current_message).
- Clarify __init__ comments: note arrays override base_url/headers in send/read;
  notes_wide/tall carried for future grid-size enforcement.
- Update read_current_message docstring to mention note-array variable sizes.
- Comment the intentional size-mismatch acceptance in the 6x30 test.

Addresses task-review + @claude review findings on #1168.
Full pytest tests/ = 3930 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added minor and removed minor labels Jun 18, 2026
…1217)

* feat(web): variable-size note-array board preview with seams [#1176]

- Import isNoteArray, NOTE_COLS, NOTE_ROWS from @/lib/board-dimensions in
  both board-display.tsx and static-board-display.tsx
- Remove stale default params (rows=6, cols=22) from messageToGrid signature
- Replace hardcoded-column comment block with a generic width comment
- Add showSeams / seamGap computation in BoardDisplay and StaticBoardDisplay
- Apply data-note-row on every row wrapper; data-note-row-seam on NOTE_ROWS
  boundaries when showSeams is true
- Apply data-note-tile on every tile; data-note-col-seam on NOTE_COLS
  boundaries when showSeams is true; marginLeft/marginTop seam gap inline
- Pass showSeams / isRowSeam / seamGap through StaticGridRow and GridRow
- Add new test file board-display-variable-size.test.tsx (12 test cases)
  covering 3×30, 3×60, 6×15, 12×15, 6×30, 6×45 sizes; seam presence for
  note_array; and seam absence for flagship/note

Flagship/note rendering is visually unchanged: isNoteArray returns false for
those device types so no extra margins or seam DOM attributes are applied.

Part of #1167 · Implements #1176

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(web): cover animated-path seams; document tile-wrapper rationale

Addresses @claude review on #1176:
- Important 2 (test gap): add seam tests for the animated BoardDisplay path
  (isStatic=false) — note_array col/row seams present, flagship none.
- Important 1 (extra wrapper): keep the per-tile wrapper — it is structurally
  required because CharTile returns a React fragment (flap-animation layers)
  that needs a single containing flex item; it also hosts the data-note-tile
  hook + seam margin. Added comments explaining this. StaticGridRow mirrors it
  for DOM consistency (tests 11/12 rely on data-note-tile on flagship tiles).

vitest: 1108 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added minor and removed minor labels Jun 18, 2026
@jeffredodd jeffredodd changed the title feat(devices): note support [WIP] feat(devices): note-carry support [WIP] Jun 19, 2026
@jeffredodd jeffredodd changed the title feat(devices): note-carry support [WIP] feat(devices): note-array support [WIP] Jun 19, 2026
@github-actions github-actions Bot added minor and removed minor labels Jun 19, 2026
* feat(web): board-size indicator for note arrays [#1175]

Add reusable BoardSizeIndicator component showing rows×cols for flagship/note
boards and rows×cols·preset-label (or "Custom") for note_array layouts. Place
it adjacent to the preview label in page-builder and inside board settings in
display-settings, replacing the hardcoded "22×6"/"15×3" strings.

- New component: web/src/components/board-size-indicator.tsx
- New tests: web/src/__tests__/board-size-indicator.test.tsx (15 cases covering
  flagship, note, all 5 presets, custom note_array, i18n aria-labels, className)
- Add boardSizeIndicator i18n namespace (custom / ariaLabel / ariaLabelWithLayout)
  to en.json and all 13 other locale files
- Wire BoardSizeIndicator into display-settings.tsx and page-builder.tsx
- Add board-size-indicator.tsx to vitest coverage include list

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(web): board-size indicator uses width×height; fix lint + E2E [#1175]

Align BoardSizeIndicator with the rest of the app, which displays board
dimensions as width × height (cols × rows): the setup wizard shows
"22 × 6 characters" and board cards historically showed "22×6". The
indicator previously rendered rows × cols ("6 × 22"), which contradicted
the wizard for the same board on adjacent screens.

- Render {cols} × {rows} ("22 × 6", "15 × 3", "60 × 3"); aria-label keeps
  explicit "{rows} rows by {cols} columns" wording (order-independent, no
  locale churn) and now documents the visual-order rationale.
- Lock visual order with toHaveTextContent assertions in the unit test.
- Fix Lint UI failure: sort the BoardSizeIndicator import in
  display-settings.tsx ahead of @/components/ui/* (simple-import-sort).
- Update E2E board-card assertions "22×6"/"15×3" → "22 × 6"/"15 × 3"
  (multi-board.spec.ts, settings-hardware-network.spec.ts). Wizard
  "… characters" assertions are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot removed the minor label Jun 19, 2026
@github-actions github-actions Bot added the minor label Jun 19, 2026
)

Add classify_dimensions(rows, cols) to src/devices.py and a
POST /settings/board/{board_id}/detect-size endpoint that reads the
board's current layout over its own transport (local / cloud / note-array
via board_client_from_board_dict) and classifies the grid shape.

- classify_dimensions: 6×22 → flagship, 3×15 → note (checked first), valid
  note-array grids → note_array with notes_wide/notes_tall + matched_preset
  (one of the 5 presets, else None). Raises ValueError ("unclassifiable")
  for anything else.
- Endpoint errors: 404 unknown board, 400 not configured, 422 no layout /
  unclassifiable grid.
- Tests: TestClassifyDimensions (14 cases inc. presets, no-preset, cap,
  unclassifiable, zero dims) + test_api_server_detect_size.py (8 cases,
  one per transport + every error path).

Purely additive — no existing call sites, models, or schema change.
Verified: ruff check/format clean; targeted tests 112 passed; broader
suite (minus tests/ai) 3832 passed, 0 failures.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added minor and removed minor labels Jun 19, 2026
…rottle [#1169] (#1223)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added minor and removed minor labels Jun 19, 2026
…ts [#1181] (#1224)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added minor and removed minor labels Jun 19, 2026
#1177] (#1225)

Default the board preview to fit-to-width and add an Actual size toggle
that renders note-array boards at natural tile size inside a horizontally
scrollable container. The toggle only renders for note_array devices, so
flagship/note previews are visually unchanged. Mode is persisted per tab
in sessionStorage (fiestaboard:boardPreviewMode). Tall configs flow with
no vertical clipping in actual mode.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added minor and removed minor labels Jun 19, 2026
…#1179] (#1226)

Add a cohesive end-to-end note-array test and fill remaining note-array
coverage gaps without duplicating the existing focused suites:

- tests/test_note_array_e2e.py: a single deterministic send -> read round
  trip through BoardClient using a stateful in-memory HTTP mock. Asserts the
  POST hits cloud.vestaboard.com (not the RW Cloud URL) with the
  X-Vestaboard-Token header and a {"characters": grid} body, transitions
  are stripped, a subsequent read parses the layout back to the same grid,
  and a second immediate send is throttled by the 15s limit (injected clock).
- tests/test_board_html_renderer.py: render + validate all 5 note-array
  presets (and a custom 3x3) through render_board_html (#1173 gap).
- tests/test_debug_endpoints.py: direct unit tests for _get_first_board_dims
  covering the object-attribute path, empty-boards fallback, and error
  fallback (previously-uncovered note-array geometry branches).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added minor and removed minor labels Jun 19, 2026
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…1228)

Addresses @claude review findings on the merged Wave-3 PRs, plus one real
integration bug that only surfaced once #1169 + #1181 were both on `next`.

- **[integration bug] integration-tests/test_cloud_mock.py:** reset the
  module-level `_note_array_last_send` throttle in the `board_client_module`
  fixture. #1169's 15s throttle (merged separately from #1181) throttled the
  second send in `test_board_client_send_then_read_roundtrip`, so the read saw
  a stale grid → the test failed on `next` (each PR passed its own CI under
  strict=false; the conflict was semantic, not textual). Now green alone and
  alongside tests/test_board_client.py.
- **[#1172] src/devices.py:** make `classify_dimensions`' unclassifiable error
  internally consistent — the comparison sizes were cols×rows ("22×6") while
  the grid is described rows×cols ("5×15"); both are rows×cols now.
- **[#1169] src/board_client.py:** broaden the transition-strip guard to fire on
  any of strategy/step_interval_ms/step_size (not just strategy) so a lone step
  param still logs why it was dropped; add a comment noting the throttle
  check+update is single-threaded-main-loop safe (TOCTOU unreachable in practice).
- **[#1181] integration-tests/mock-cloud/server.py:** give the jagged/non-
  rectangular grid 400 a distinct message instead of reusing the missing-key one.

Verified: ruff clean; tests/ (minus tests/ai) + integration-tests/test_cloud_mock.py = 3858 passed, 0 failures.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added minor and removed minor labels Jun 19, 2026
…detect [#1178] (#1229)

Replace the per-board flagship/note pills with a grouped Radix Select
(Devices: Flagship/Note; Note arrays: 5 presets + Custom…). Add Custom
W×H inputs (validated 1..MAX_NOTES_PER_AXIS), a masked note_array_token
field gated by isNoteArray, and an "Auto-detect from board" button that
calls the detect-size endpoint and populates type/dims (matching presets
by dimensions, not the matched_preset label). Adds note_array_token to
BoardInstance, DetectBoardSizeResponse, and api.detectBoardSize(). New
i18n keys mirrored across all 14 locales. New vitest suite + MSW
detect-size handler; migrated the multi-board E2E to drive the Select.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added minor and removed minor labels Jun 19, 2026
@DarthXoc DarthXoc mentioned this pull request Jun 19, 2026
9 tasks
…cator, auto-detect [#1180] (#1233)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added minor and removed minor labels Jun 19, 2026
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