diff --git a/backlog/docs/doc-17 - Implementation-Plan-ML-176.2-—-Presto-splash-screen-search-views-and-on-screen-keyboard.md b/backlog/docs/doc-17 - Implementation-Plan-ML-176.2-—-Presto-splash-screen-search-views-and-on-screen-keyboard.md new file mode 100644 index 00000000..145b02bd --- /dev/null +++ b/backlog/docs/doc-17 - Implementation-Plan-ML-176.2-—-Presto-splash-screen-search-views-and-on-screen-keyboard.md @@ -0,0 +1,655 @@ +--- +id: doc-17 +title: >- + Implementation Plan: ML-176.2 — Presto splash screen, search views, and + on-screen keyboard +type: specification +created_date: "2026-05-10 13:48" +updated_date: "2026-05-10 13:56" +tags: + - presto + - search + - micropython + - implementation-plan +--- + +# Implementation Plan — ML-176.2: Presto splash screen, search views, and on-screen keyboard + +## 0. Dependencies + +- **ML-176.1** (In Progress): API accepts `q` parameter on `GET /api/v1/collection`. Must be completed before `fetch_search_results()` works end-to-end, but Presto-side implementation can proceed in parallel — the `q` parameter is additive (absent → same as current behavior). Presto code can be written and syntax-checked independently. + +## 1. Objective Alignment + +The current `records_on_the_day.py` is a single-purpose calendar app that boots directly into today's records. The objective is to refactor it into a multi-screen app: a **splash screen** (home) with two entry points ("Search Collection" and "Today's Records"), a **search input view** with a custom on-screen QWERTY/numbers keyboard, and a **search results view** that reuses the existing record-list rendering infrastructure. The app must also navigate correctly (back from detail returns to the origin screen) and always return to the splash screen on display wake. + +Each section below maps a concrete implementation step to a verifiable outcome. + +## 2. Simplicity and Alternatives Considered + +**Chosen approach:** Everything stays in the single `records_on_the_day.py` file. No new modules, no class refactor. Use flat state constants and new drawing/touch-handler functions. Reuse existing `_draw_record_list()`, `_draw_record_row()`, `prepare_records_for_display()`, `draw_record_detail()`, and scroll infrastructure verbatim — search results is just `STATE_DAY` with a different header and data source. + +**Alternatives evaluated and rejected:** + +- _Separate files per screen_ — adds import complexity on MicroPython, increases flash fragmentation risk. Rejected. +- _Class-based screen hierarchy_ — class allocation and method dispatch add memory overhead. Flat function dispatch keeps GC pressure low and is consistent with existing code. Rejected. +- _Virtual keyboard as a component module_ — same fragmentation argument. Inline in the main file keeps everything in one compilation unit. Rejected. +- _Shift/caps support on keyboard_ — task explicitly says lowercase only. Deferred. +- _Infinite scroll for search results_ — task explicitly says 20 per page, no infinite scroll. Rejected. + +## 3. Completeness and Sequencing + +### Step 1: Add new state constants and global variables + +Add at the top of the state globals section (after `STATE_RECORD = 3`): + +```python +STATE_HOME = 4 +STATE_SEARCH_INPUT = 5 +STATE_SEARCH_RESULTS = 6 +``` + +Change default `state = STATE_HOME` (was `STATE_STARTUP`). + +Add new globals: + +```python +search_query = "" # Current search buffer (max ~50 chars) +search_results = [] # Records from search API +search_results_error = False +search_scroll_offset = 0 # Separate scroll offset for search results +_search_content_height = 0 # Separate content height cache for search results +keyboard_mode = "alpha" # "alpha" or "numbers" +previous_state = None # Track where we came from for detail view back +``` + +Remove `STATE_STARTUP = 0`. Replace all references to `STATE_STARTUP` with `STATE_HOME`. + +**Verification:** `python3 -c "import py_compile; py_compile.compile('records_on_the_day.py', cfile='/tmp/records_on_the_day.pyc', doraise=True)"` passes. + +### Step 2: Add `fetch_search_results()` API client function + +**Depends on: ML-176.1** (backend `q` parameter support). + +Add after `fetch_records()` in the API section: + +```python +def fetch_search_results(query): + """Fetch search results from the API. + Returns (records_list, error_flag). Same shape as fetch_records(). + """ + # urequests doesn't have url-encode built-in; hand-encode common chars + encoded = query.replace(" ", "+") + url = API_BASE + "/api/v1/collection?q=" + encoded + "&limit=20" + + try: + resp = urequests.get(url, headers=_auth_header()) + if resp.status_code == 200: + data = resp.json() + recs = data.get("records", []) + resp.close() + gc.collect() + return recs, False + else: + resp.close() + gc.collect() + return [], True + except Exception: + gc.collect() + return [], True +``` + +**Verification:** After ML-176.1 is deployed, tapping OK on keyboard with a valid query shows results. Tapping OK with no network shows "Could not reach server". + +### Step 3: Implement STATE_HOME (splash screen) + +#### 3a. Layout constants + +```python +HOME_BUTTON_W = WIDTH - 80 +HOME_BUTTON_H = 90 +HOME_BUTTON_X = 40 +HOME_BUTTON1_Y = 220 +HOME_BUTTON2_Y = HOME_BUTTON1_Y + HOME_BUTTON_H + 30 +HOME_TITLE_Y = 100 +HOME_TITLE = "Music Library" +``` + +#### 3b. Drawing function `draw_home_screen()` + +```python +def draw_home_screen(): + display.set_pen(_pen_bg) + display.clear() + + display.set_pen(_pen_title) + display.set_font("bitmap14_outline") + tw = display.measure_text(HOME_TITLE, scale=1) + display.text(HOME_TITLE, (WIDTH - tw) // 2, HOME_TITLE_Y, scale=1) + + _draw_home_button(HOME_BUTTON1_Y, "Search Collection") + _draw_home_button(HOME_BUTTON2_Y, "Today's Records") + + presto.update() + +def _draw_home_button(y, label): + display.set_pen(_pen_cell_bg) + display.rectangle(HOME_BUTTON_X, y, HOME_BUTTON_W, HOME_BUTTON_H) + display.set_pen(_pen_header_text) + display.set_font("bitmap8") + tw = display.measure_text(label, scale=1) + tx = HOME_BUTTON_X + (HOME_BUTTON_W - tw) // 2 + ty = y + (HOME_BUTTON_H - 8) // 2 + display.text(label, tx, ty, scale=1) +``` + +**Verification:** On boot, splash screen renders with two buttons. Tapping "Today's Records" transitions to STATE_MONTH. Tapping "Search Collection" transitions to STATE_SEARCH_INPUT. + +### Step 4: Implement on-screen keyboard (STATE_SEARCH_INPUT) + +#### 4a. Keyboard layout constants + +```python +KB_MARGIN = 20 +KB_KEY_GAP = 4 +KB_KEY_H = 48 +KB_KEY_W = (WIDTH - 2 * KB_MARGIN - 9 * KB_KEY_GAP) // 10 # computed from 10-key alpha row + +KB_INPUT_Y = 30 +KB_INPUT_H = 40 +KB_INPUT_MARGIN = 40 +KB_ROWS_START_Y = KB_INPUT_Y + KB_INPUT_H + 16 + +ALPHA_KEYS = [ + list("qwertyuiop"), + list("asdfghjkl"), + list("zxcvbnm"), +] + +NUM_KEYS = [ + list("1234567890"), + list("!@#$%^&*()_"), +] +``` + +#### 4b. Drawing function `draw_search_input()` + +```python +def draw_search_input(): + display.set_pen(_pen_bg) + display.clear() + + # Header with "Search Collection" title and Back (→ Home) button + _draw_search_input_header() + + # Query text field + display.set_pen(_pen_cell_bg) + display.rectangle(KB_INPUT_MARGIN, KB_INPUT_Y, WIDTH - 2 * KB_INPUT_MARGIN, KB_INPUT_H) + display.set_pen(_pen_header_text) + display.set_font("bitmap8") + display.text(search_query, KB_INPUT_MARGIN + 8, KB_INPUT_Y + 12, scale=1) + + display.set_font("bitmap8") + keys_matrix = ALPHA_KEYS if keyboard_mode == "alpha" else NUM_KEYS + toggle_label = "123" if keyboard_mode == "alpha" else "ABC" + + cy = KB_ROWS_START_Y + for row_keys in keys_matrix: + num_keys = len(row_keys) + total_gap = (num_keys - 1) * KB_KEY_GAP + total_key_w = num_keys * KB_KEY_W + row_start_x = (WIDTH - total_key_w - total_gap) // 2 + for i, char in enumerate(row_keys): + kx = row_start_x + i * (KB_KEY_W + KB_KEY_GAP) + _draw_key(kx, cy, char, KB_KEY_W, KB_KEY_H) + cy += KB_KEY_H + KB_KEY_GAP + + # Bottom row: toggle | space | backspace | OK + bottom_w = (WIDTH - 2 * KB_MARGIN - 3 * KB_KEY_GAP) // 4 + bx = KB_MARGIN + _draw_key(bx, cy, toggle_label, bottom_w, KB_KEY_H) + bx += bottom_w + KB_KEY_GAP + _draw_key(bx, cy, "Space", bottom_w, KB_KEY_H) + bx += bottom_w + KB_KEY_GAP + _draw_key(bx, cy, "<-", bottom_w, KB_KEY_H) + bx += bottom_w + KB_KEY_GAP + ok_pen = _pen_dim_text if search_query == "" else _pen_header_text + _draw_key(bx, cy, "OK", bottom_w, KB_KEY_H, text_pen=ok_pen) + + presto.update() + +def _draw_key(x, y, label, w, h, text_pen=None): + display.set_pen(_pen_cell_bg) + display.rectangle(x, y, w, h) + pen = text_pen if text_pen is not None else _pen_normal_text + display.set_pen(pen) + tw = display.measure_text(label, scale=1) + tx = x + (w - tw) // 2 + ty = y + (h - 8) // 2 + display.text(label, tx, ty, scale=1) +``` + +#### 4c. Keyboard touch handler `handle_search_input_touch(x, y)` + +```python +def handle_search_input_touch(x, y): + global search_query, keyboard_mode, state, search_results, search_results_error + global search_scroll_offset, _search_content_height, previous_state + + # Header Back button → Home + if BACK_X <= x <= BACK_X + BACK_W and BACK_Y <= y <= BACK_Y + BACK_H: + state = STATE_HOME + draw_home_screen() + return + + action = _keyboard_hit_test(x, y) + if action is None: + return + + if action == "toggle": + keyboard_mode = "numbers" if keyboard_mode == "alpha" else "alpha" + draw_search_input() + return + + if action == "space": + if len(search_query) < 50: + search_query += " " + draw_search_input() + return + + if action == "backspace": + search_query = search_query[:-1] + draw_search_input() + return + + if action == "ok": + if not search_query.strip(): + return + draw_status("Searching for:\n" + search_query) + if not ensure_wifi_connected(): + search_results_error = True + search_results = [] + state = STATE_SEARCH_RESULTS + draw_search_results() + return + recs, had_error = fetch_search_results(search_query) + search_results = recs + search_results_error = had_error + search_scroll_offset = 0 + if not had_error and recs: + prepare_records_for_display(recs) + previous_state = STATE_SEARCH_INPUT + state = STATE_SEARCH_RESULTS + draw_search_results() + return + + # Character key + if len(search_query) < 50 and len(action) == 1: + search_query += action + draw_search_input() +``` + +#### 4d. Hit-testing function `_keyboard_hit_test(x, y)` + +```python +def _keyboard_hit_test(x, y): + if y < KB_ROWS_START_Y: + return None + + keys_matrix = ALPHA_KEYS if keyboard_mode == "alpha" else NUM_KEYS + + cy = KB_ROWS_START_Y + for row_keys in keys_matrix: + num_keys = len(row_keys) + total_gap = (num_keys - 1) * KB_KEY_GAP + total_key_w = num_keys * KB_KEY_W + row_start_x = (WIDTH - total_key_w - total_gap) // 2 + for i, char in enumerate(row_keys): + kx = row_start_x + i * (KB_KEY_W + KB_KEY_GAP) + if kx <= x < kx + KB_KEY_W and cy <= y < cy + KB_KEY_H: + return char + cy += KB_KEY_H + KB_KEY_GAP + + # Bottom row + bottom_w = (WIDTH - 2 * KB_MARGIN - 3 * KB_KEY_GAP) // 4 + bx = KB_MARGIN + if bx <= x < bx + bottom_w and cy <= y < cy + KB_KEY_H: + return "toggle" + bx += bottom_w + KB_KEY_GAP + if bx <= x < bx + bottom_w and cy <= y < cy + KB_KEY_H: + return "space" + bx += bottom_w + KB_KEY_GAP + if bx <= x < bx + bottom_w and cy <= y < cy + KB_KEY_H: + return "backspace" + bx += bottom_w + KB_KEY_GAP + if bx <= x < bx + bottom_w and cy <= y < cy + KB_KEY_H: + return "ok" + + return None +``` + +#### 4e. Key press visual feedback + +Brief highlight flash on key press. Call `_flash_key()` before the action is applied; the subsequent `draw_search_input()` overwrites it: + +```python +def _flash_key(kx, ky, kw, kh, label): + _draw_key(kx, ky, label, kw, kh, text_pen=_pen_today_text) + display.set_pen(_pen_arrow) + display.rectangle(kx, ky, kw, kh) + presto.update() + time.sleep(0.05) +``` + +**Verification:** All keys appear on a 720×720 display. Tapping letter keys appends to the query field. Backspace removes last char. 123/ABC toggles layouts. OK dim when empty and no-ops. OK with text triggers API call. + +### Step 5: Implement STATE_SEARCH_RESULTS + +#### 5a. Drawing function `draw_search_results()` + +Reuses `_draw_record_row()` from the day view. Unique elements: search-specific header, own scroll state, empty/error states. + +```python +def draw_search_results(): + global search_scroll_offset + display.set_pen(_pen_bg) + display.clear() + + _draw_search_results_header() + + if search_results_error: + _draw_search_error() + presto.update() + return + + if not search_results: + _draw_search_empty() + presto.update() + return + + _draw_search_record_list() + presto.update() + +def _draw_search_results_header(): + display.set_pen(_pen_cell_bg) + display.rectangle(0, DAY_HEADER_Y, WIDTH, DAY_HEADER_H) + + # Back button + bx, by = BACK_X, BACK_Y + display.set_pen(_pen_placeholder) + display.rectangle(bx, by, BACK_W, BACK_H) + display.set_pen(_pen_back) + display.set_font("bitmap8") + _draw_centered_text("<", bx, by, BACK_W, BACK_H, DAY_COUNT_TEXT_H) + + # Title "Search: " — truncate if needed + display.set_pen(_pen_header_text) + display.set_font("bitmap14_outline") + label = "Search: " + search_query + max_w = WIDTH - (BACK_X + BACK_W + 8) - 8 + if display.measure_text(label, scale=1) > max_w: + display_ellipsis = "..." + while (display.measure_text("Search: " + search_query + display_ellipsis, scale=1) > max_w + and len(search_query) > 0): + search_query = search_query[:-1] + label = "Search: " + search_query + display_ellipsis if search_query else "Search: ..." + tw = display.measure_text(label, scale=1) + display.text(label, (WIDTH - tw)//2, + DAY_HEADER_Y + (DAY_HEADER_H - DAY_HEADER_TEXT_H)//2, scale=1) +``` + +#### 5b. Search record list scroll (own state) + +```python +def _draw_search_record_list(): + global search_scroll_offset + max_offset = _search_max_scroll_offset() + search_scroll_offset = min(max(0, search_scroll_offset), max_offset) + + if search_scroll_offset > 0: + display.set_pen(_pen_arrow) + display.set_font("bitmap14_outline") + display.text("^", WIDTH // 2 - 8, RECORD_START_Y - 28, scale=1) + + cy = RECORD_START_Y - search_scroll_offset + for rec in search_results: + row_h = rec.get("_row_height", THUMB_SIZE + 8) + row_bottom = cy + row_h + if row_bottom <= RECORD_START_Y: + cy = row_bottom + continue + if cy >= HEIGHT - 28: + break + _draw_record_row(rec, cy) + cy = row_bottom + + if search_scroll_offset < max_offset: + display.set_pen(_pen_arrow) + display.set_font("bitmap14_outline") + display.text("v", WIDTH // 2 - 8, HEIGHT - 24, scale=1) + +def _search_max_scroll_offset(): + viewport_h = HEIGHT - RECORD_START_Y - 28 + return max(0, _search_content_height - viewport_h) +``` + +#### 5c. Generic `prepare_records_for_display(recs=None)` + +Make the existing preparation pipeline accept an optional list so both `records` and `search_results` can use it: + +```python +def prepare_records_for_display(recs=None): + global _content_height, _search_content_height, records, search_results + if recs is None: + recs = records + display.set_font("bitmap8") + if recs is search_results: + _search_content_height = 0 + else: + _content_height = 0 + for rec in recs: + _prepare_record_for_display(rec) + if recs is search_results: + _search_content_height += rec.get("_row_height", THUMB_SIZE + 8) + else: + _content_height += rec.get("_row_height", THUMB_SIZE + 8) + preload_record_thumbnails(recs) + gc.collect() +``` + +Similarly parameterize `preload_record_thumbnails(recs=None)`. + +#### 5d. Empty and error states + +```python +def _draw_search_empty(): + display.set_pen(_pen_dim_text) + display.set_font("bitmap8") + msg = "No records found" + mw = display.measure_text(msg, scale=1) + display.text(msg, (WIDTH - mw) // 2, HEIGHT // 2 - 10, scale=1) + +def _draw_search_error(): + display.set_pen(_pen_error) + display.set_font("bitmap8") + msg = "Could not reach server" + mw = display.measure_text(msg, scale=1) + display.text(msg, (WIDTH - mw) // 2, HEIGHT // 2 - 20, scale=1) + display.set_pen(_pen_dim_text) + hint = "Tap to retry" + hw = display.measure_text(hint, scale=1) + display.text(hint, (WIDTH - hw) // 2, HEIGHT // 2 + 10, scale=1) +``` + +**Verification:** Search results display with cover art, titles, artists, format, year — identical layout to day view. Back returns to STATE_SEARCH_INPUT with query preserved. Tapping a record opens detail. Empty/error states work. Scroll works identically to day view. + +### Step 6: Navigation state tracking for Back from detail + +Track `previous_state` on transitions to `STATE_RECORD`: + +- From `STATE_DAY` → `previous_state = STATE_DAY` +- From `STATE_SEARCH_RESULTS` → `previous_state = STATE_SEARCH_RESULTS` + +On Back in `STATE_RECORD`, navigate to `previous_state` instead of always `STATE_DAY`. + +**Verification:** Detail view Back → search results when entered from search. Back → day when entered from day. + +### Step 7: Update existing views for Back-to-Home + +#### 7a. Month view Home button + +Add a "Home" button in the month header, leftmost position. Month header layout becomes: `[Home] [<] [Month Year] [>]`. + +Update `_draw_month_header()` to draw the home button and shift the left arrow right. Update `handle_month_touch()` to check the home button first. + +#### 7b. Display wake returns to home + +In `wake_display()`, set `state = STATE_HOME` before calling `redraw_current_view()`. + +#### 7c. `redraw_current_view()` updated + +Add cases for `STATE_HOME`, `STATE_SEARCH_INPUT`, `STATE_SEARCH_RESULTS`. + +**Verification:** From any view, after display sleep + wake, splash screen appears. Month view has Home button. Search input Back → Home. + +### Step 8: Update `main()` globals and boot flow + +Change boot flow: + +1. `state = STATE_HOME` +2. After WiFi + NTP sync + `set_today()`, call `draw_home_screen()` — no auto-fetch of today's records +3. User must tap "Today's Records" to trigger the calendar flow + +**Verification:** On cold boot, splash screen is the first thing shown. No API call until user taps a button. + +### Step 9: Update main loop dispatch + +Add handlers for `STATE_HOME`, `STATE_SEARCH_INPUT`, `STATE_SEARCH_RESULTS` in the main loop. Implement `handle_home_touch(x, y)` for "Search Collection" and "Today's Records" buttons. Implement drag-scroll for `STATE_SEARCH_RESULTS` mirroring day-view scroll. Wire `previous_state` tracking for detail navigation. + +```python +def handle_home_touch(x, y): + global state + + # "Search Collection" + if (HOME_BUTTON_X <= x <= HOME_BUTTON_X + HOME_BUTTON_W and + HOME_BUTTON1_Y <= y <= HOME_BUTTON1_Y + HOME_BUTTON_H): + state = STATE_SEARCH_INPUT + search_query = "" + keyboard_mode = "alpha" + draw_search_input() + return + + # "Today's Records" → month view with today highlighted + if (HOME_BUTTON_X <= x <= HOME_BUTTON_X + HOME_BUTTON_W and + HOME_BUTTON2_Y <= y <= HOME_BUTTON2_Y + HOME_BUTTON_H): + view_year = today_year + view_month = today_month + selected_day = today_day + state = STATE_MONTH + draw_month_view() + return +``` + +**Design decision:** "Today's Records" goes to month view (with today highlighted) rather than directly to day view. This gives the user full calendar context — they can tap today or any other day. Consistent with the existing month-first navigation pattern. + +**Verification:** All touch handlers dispatch correctly. No crashes on state transitions. Scroll works in search results. Record taps in search results open detail. + +### Step 10: Delete unused `STATE_STARTUP` references + +Remove `STATE_STARTUP = 0`. Replace remaining references with `STATE_HOME`. + +**Verification:** `grep -n "STATE_STARTUP" records_on_the_day.py` returns nothing. + +### Step 11: Test and deploy + +1. Syntax check: `python3 -c "import py_compile; py_compile.compile('records_on_the_day.py', cfile='/tmp/records_on_the_day.pyc', doraise=True)"` +2. Verify ML-176.1 is deployed to production +3. Deploy to Presto: `mise run presto` +4. Manual verification on device for all 19 acceptance criteria + +## 4. Verifiability + +| Step | Verification | +| ---- | ---------------------------------------------------------------------------------------- | +| 1 | Syntax check passes | +| 2 | Functional after ML-176.1 deployed; OK triggers API call, network error handled | +| 3 | Presto boots to splash screen; two buttons visible and tappable | +| 4 | All keys visible; typing works; backspace works; 123/ABC toggles; OK dim when empty | +| 5 | Search results render with cover art; scroll works; empty/error states display correctly | +| 6 | Back from detail → correct origin (search results or day view) | +| 7 | Home button on month view works; wake always returns to home | +| 8 | On cold boot, splash screen shows first | +| 9 | All touch handlers dispatch correctly; no crashes | +| 10 | No STATE_STARTUP references remain | +| 11 | All 19 ACs pass on physical device | + +## 5. Architecture Impact Analysis + +| Touchpoint | Impact | +| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `records_on_the_day.py` | Primary change. ~250–350 new lines for keyboard, splash, search views. Existing drawing functions reused. `prepare_records_for_display()` and `preload_record_thumbnails()` made generic (accept optional list param). | +| `collection_controller.ex` | **No change** — handled by ML-176.1 | +| `collection_json.ex` | **No change** — `index/1` already returns correct shape | +| `Collection.search_records/2` | **No change** — already accepts query string | +| Routes | **No change** — existing `GET /api/v1/collection` reused | +| PubSub | No impact | +| Supervision tree | No impact | +| External APIs | Search API call added to Presto. Same auth header, same error handling pattern as `fetch_records()` | +| Month view UI | Header gains Home button; left/right arrow positions shift right | + +## 6. Performance Profile + +- **Keyboard drawing:** O(1) — fixed ~34 keys drawn per keystroke. Each key is a rectangle + text call. No network calls during typing. Estimated <5ms per redraw on RP2350B at 150 MHz. +- **Search results rendering:** O(n) where n ≤ 20 records per page. Same scroll performance rules as day view: layout/text pre-computed in `prepare_records_for_display()`, drag scroll uses placeholders for thumbnails. No measurable difference from day view. +- **Memory:** Search query capped at 50 chars. Keyboard layout arrays ~200 bytes total (module-level constants). Search results reuse `search_results` list (same structure as `records`). GC pressure identical to current app. +- **Network:** One additional API call per search (same `urequests` pattern). No network calls during scroll or typing. +- **N+1 risks:** None. Both API endpoints return full record data in a single response. Thumbnails preloaded in batch. +- **Latency:** Same 1–3 second API call latency as day-view fetch. Keyboard is instant (local only). + +## 7. Benchmarking Requirements + +No formal benchmarks needed. The keyboard is a fixed-size static layout with O(1) redraw. Search results use the same rendering path as day view, already validated on hardware. The only quantitative check: + +- **Keyboard tap-to-redraw latency:** Verify that tapping a key and seeing the character appear feels instant (<100ms). If noticeable lag occurs, profile with `time.ticks_ms()` around `draw_search_input()`. + +Manual verification on the Presto device is sufficient. + +## 8. Cost Profile + +No additional paid resources: + +- **API calls:** Search uses the same `GET /api/v1/collection` endpoint (ML-176.1). No third-party API calls. Incremental server load is one extra request per search, same magnitude as a day-view request. +- **Storage:** No new storage requirements. Keyboard layouts are constant strings in the MicroPython module. +- **Compute:** No additional server-side processing beyond what `search_records` already handles. + +## 9. Production Infrastructure Steps + +### Production Changes + +**1. Deploy ML-176.1 backend change first** + +```bash +git push origin main # triggers Coolify deploy (includes ML-176.1 commit) +``` + +Verify: `curl -H "Authorization: Bearer " "https://music-library.claudio-ortolina.org/api/v1/collection?q=test&limit=1"` returns results. + +**2. Deploy updated Presto firmware** + +```bash +mise run presto +``` + +Rollback: copy the previous version of `records_on_the_day.py` to the device. + +No environment variable changes, no service provisioning, no DNS changes, no firewall rules. + +## 10. Documentation Updates + +| File | Change needed | +| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `presto/README.md` | Add sections: "Splash Screen", "Search", "On-Screen Keyboard". Update "Features" list. Add API endpoint `GET /api/v1/collection?q=&limit=20` to "API Endpoint" section with response format. | +| `presto/AGENTS.md` | Update "State Management" section: add `STATE_HOME`, `STATE_SEARCH_INPUT`, `STATE_SEARCH_RESULTS`. Update "API Contract" section with search endpoint. Update "Scroll Performance" to mention per-view offset/cache discipline for search results. Add keyboard constraints: fixed layouts, no shift, max 50 chars. | +| `docs/architecture.md` | No change needed. | +| `docs/project-conventions.md` | No change needed. | +| `docs/production-infrastructure.md` | No change needed. | diff --git a/backlog/tasks/ml-176.2 - Presto-add-splash-screen-search-views-and-on-screen-keyboard.md b/backlog/tasks/ml-176.2 - Presto-add-splash-screen-search-views-and-on-screen-keyboard.md index e0c2a7ce..02dc0e70 100644 --- a/backlog/tasks/ml-176.2 - Presto-add-splash-screen-search-views-and-on-screen-keyboard.md +++ b/backlog/tasks/ml-176.2 - Presto-add-splash-screen-search-views-and-on-screen-keyboard.md @@ -4,15 +4,20 @@ title: "Presto: add splash screen, search views, and on-screen keyboard" status: To Do assignee: [] created_date: "2026-05-10 13:35" +updated_date: "2026-05-10 13:56" labels: - presto - search - micropython milestone: m-0 -dependencies: [] +dependencies: + - ML-176.1 references: - - presto/AGENTS.md (constraints and patterns) - - presto/README.md (API contract) + - presto/AGENTS.md + - presto/README.md + - >- + doc-17 - + Implementation-Plan-ML-176.2-—-Presto-splash-screen-search-views-and-on-screen-keyboard modified_files: - presto/records_on_the_day.py parent_task_id: ML-176 @@ -113,3 +118,35 @@ Refactor `presto/records_on_the_day.py` into a unified app with a splash screen, - [ ] #18 Keyboard renders correctly at 720×720: all keys visible and tappable - [ ] #19 Back-to-Home navigation available from month view and search views + +## Implementation Notes + + + +## Implementation Plan + +Full plan: `doc-17 - Implementation Plan: ML-176.2 — Presto splash screen, search views, and on-screen keyboard` + +### Summary + +11-step plan, all within `records_on_the_day.py`: + +1. **State constants + globals** — Add `STATE_HOME` (4), `STATE_SEARCH_INPUT` (5), `STATE_SEARCH_RESULTS` (6). Remove `STATE_STARTUP`. Add `search_query`, `search_results`, `keyboard_mode`, `previous_state` globals. +2. **`fetch_search_results()`** — New API client calling `GET /api/v1/collection?q=&limit=20`. Returns `(records_list, error_flag)`. Depends on ML-176.1 for the backend `q` parameter. +3. **Splash screen (`STATE_HOME`)** — `draw_home_screen()` with two large buttons: "Search Collection" and "Today's Records". +4. **On-screen keyboard (`STATE_SEARCH_INPUT`)** — `draw_search_input()` with 3-row QWERTY + numbers keyboard. Hit-test via `_keyboard_hit_test()`. Touch handler `handle_search_input_touch()`. Visual key-press feedback. OK button dimmed when query empty. +5. **Search results (`STATE_SEARCH_RESULTS`)** — Reuses `_draw_record_row()`. Own scroll state (`search_scroll_offset`, `_search_content_height`). `prepare_records_for_display(recs=None)` made generic to support both `records` and `search_results`. Empty/error states. +6. **Navigation tracking** — `previous_state` tracks whether detail was entered from day or search. Back from detail returns to correct origin. +7. **Back-to-Home** — Home button added to month header. `wake_display()` sets `state = STATE_HOME`. `redraw_current_view()` updated for new states. +8. **Boot flow** — Starts at `STATE_HOME`, no auto-fetch. User taps "Today's Records" → month view. +9. **Main loop dispatch** — Added handlers for all new states. `handle_home_touch()`. Search results drag-scroll mirrors day view. +10. **Cleanup** — Remove all `STATE_STARTUP` references. +11. **Deploy** — Verify ML-176.1 deployed → `mise run presto` → manual verification on device. + +### Key design decisions + +- "Today's Records" navigates to month view (with today highlighted), not directly to day — preserves calendar context +- Keyboard: fixed layout, lowercase only, no shift/caps, max 50 chars +- Search results: 20 per page, no infinite scroll. Own scroll state per AGENTS.md rules. +- `prepare_records_for_display()` parameterized to accept optional list — avoids duplicating the entire display-prep pipeline +