--- phase: 11-ui-enhancements plan: "02" subsystem: printer-ui tags: [uie-01, htmx, patch, modal, pico-css, alpine-js, e2e, playwright] dependency_graph: requires: [11-01] provides: [PATCH /printers/{id}, printer_edit_modal.html, Edit button per row] affects: [imptune/api/printers.py, imptune/templates/partials/printer_list.html, imptune/templates/partials/printer_edit_modal.html, tests/e2e/test_printer_edit.py] tech_stack: added: [] patterns: [HTMX PATCH in-place update, Pico CSS native dialog, Alpine.js portEdited guard, session-scoped E2E row targeting] key_files: created: - imptune/templates/partials/printer_edit_modal.html - tests/e2e/test_printer_edit.py modified: - imptune/api/printers.py - imptune/templates/partials/printer_list.html decisions: - "PATCH ip_address and port_name are optional Form fields (default empty string) that fall back to existing printer values — matches Wave 0 test scaffold that only sends name" - "E2E row targeting uses locator(tr, has=locator(a, has_text)) to handle session-scoped live_server accumulating multiple printers across tests" - "updated_at set explicitly via datetime.now(UTC).replace(tzinfo=None) inside PATCH handler" - "clients and driver_data added to _render_printer_list context for edit modal pre-population" metrics: duration: "~20 minutes" completed: "2026-04-15" tasks_completed: 2 tasks_total: 2 files_modified: 4 --- # Phase 11 Plan 02: Printer Edit Modal (UIE-01) Summary **One-liner:** HTMX PATCH route + Pico CSS native dialog edit modal with Alpine.js port guard and Playwright E2E coverage. ## What Was Built UIE-01 is now complete: every printer row in the library has an Edit button that opens a pre-filled native `` modal. Submitting the form sends a HTMX PATCH to `/printers/{id}`, closes the modal, and refreshes the printer list in-place without a page reload. ### Key Changes - **`imptune/api/printers.py`** — Added `PATCH /{printer_id}` route handler with full validation (name/ip/port required, duplex/paper enum checks). Optional `ip_address` and `port_name` fall back to existing values when not submitted. Updated `_render_printer_list` to pass `clients` and `driver_data` in the template context for modal pre-population. Imported `Driver` at module level. - **`imptune/templates/partials/printer_edit_modal.html`** (new, 98 lines) — Pico CSS native `` with Edit trigger button and HTMX PATCH form. Uses `hx-on::after-request` to close the modal on success. Alpine.js `x-data` sets `portEdited: true` so editing IP does not overwrite a manually-set port. Pre-fills all printer fields including driver/client selects with `selected` conditional. - **`imptune/templates/partials/printer_list.html`** — Actions `` updated: `{% include "partials/printer_edit_modal.html" %}` inserted before the Delete button, inside the `{% for p in printers %}` loop so `p` is in scope. - **`tests/e2e/test_printer_edit.py`** (new) — Two Playwright E2E tests: modal open and pre-fill verification; submit updates list via HTMX PATCH. Row targeting uses `page.locator("tr", has=page.locator("a", has_text="OriginalName"))` to handle session-scoped live_server accumulating data across tests. ## Test Results | Suite | Status | |-------|--------| | test_patch_printer | GREEN | | test_patch_printer_not_found | GREEN | | test_printer_edit_modal_open_and_prefill | GREEN (E2E) | | test_printer_edit_submit_updates_list | GREEN (E2E) | | Full non-E2E suite | 120 passed, 2 expected RED (UIE-03 Wave 0) | ## Commits | Hash | Message | |------|---------| | `4b212b6` | feat(11-02): PATCH /printers/{id} route handler and updated _render_printer_list | | `7b948b6` | feat(11-02): UIE-01 edit modal — Edit button per row, Pico dialog, E2E tests | ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 2 - Missing functionality] ip_address and port_name made optional in PATCH handler** - **Found during:** Task 1 — reviewing Wave 0 test scaffold `test_patch_printer` - **Issue:** The plan specified `ip_address: str = Form(...)` and `port_name: str = Form(...)` as required, but the existing RED scaffold test only sends `{"name": "Updated Name"}`. The handler would have returned 422 Unprocessable Entity. - **Fix:** Changed `ip_address` and `port_name` to `Form("")` with fallback to `printer.ip_address` / `printer.port_name` when empty, preserving validation logic while passing the test. - **Files modified:** `imptune/api/printers.py` - **Commit:** `4b212b6` **2. [Rule 1 - Bug] E2E test_printer_edit_submit_updates_list used wrong selector for session-scope isolation** - **Found during:** Task 2 — E2E test run - **Issue:** `page.click("button:has-text('Edit')")` clicked the first Edit button in the list, which belonged to a printer from a previous test (session-scoped live_server). The targeted printer ("OriginalName") was not updated. - **Fix:** Changed to `page.locator("tr", has=page.locator("a", has_text="OriginalName")).locator("button:has-text('Edit')").click()` to target the specific row. Also updated assertion to check anchor text (`a:has-text`) rather than `td:first-child` inner text, and used `page.wait_for_selector("a:has-text('UpdatedName')")` for HTMX swap completion. - **Files modified:** `tests/e2e/test_printer_edit.py` - **Commit:** `7b948b6` ## Success Criteria Check - [x] Every printer row has an Edit button - [x] Clicking Edit opens a native dialog pre-filled with that printer's current data - [x] Submitting the edit form sends HTMX PATCH, closes the modal, and updates the list - [x] PATCH /printers/{id} validated via test_patch_printer (GREEN) - [x] PATCH /printers/9999 returns 404 (confirmed by test_patch_printer_not_found) - [x] E2E tests pass: modal opens, name is pre-filled, submit updates list ## Self-Check: PASSED