From b0078a4fb261f63d5bd31fe99ee86147263763bd Mon Sep 17 00:00:00 2001 From: Kawa Date: Wed, 15 Apr 2026 15:08:53 +0200 Subject: [PATCH] docs(phase-11): complete phase execution All 4 plans complete. UIE-01..05 verified (15/15 must-haves). Human-approved. Co-Authored-By: Claude Sonnet 4.6 --- .planning/ROADMAP.md | 2 +- .planning/STATE.md | 2 +- .../11-ui-enhancements/11-02-SUMMARY.md | 95 +++++++++++ .../11-ui-enhancements/11-03-SUMMARY.md | 127 +++++++++++++++ .../11-ui-enhancements/11-VERIFICATION.md | 148 ++++++++++++++++++ 5 files changed, 372 insertions(+), 2 deletions(-) create mode 100644 .planning/phases/11-ui-enhancements/11-02-SUMMARY.md create mode 100644 .planning/phases/11-ui-enhancements/11-03-SUMMARY.md create mode 100644 .planning/phases/11-ui-enhancements/11-VERIFICATION.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 632f2bd..1d8dce1 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -108,4 +108,4 @@ Full details: [`milestones/v1.0-ROADMAP.md`](milestones/v1.0-ROADMAP.md) | 8. Nyquist Validation Track | v1.1 | 8/8 | Complete | 2026-04-13 | | 9. UX Tech Debt Closure | 3/3 | Complete | 2026-04-13 | 2026-04-13 | | 10. Real-World Runtime Validation | v1.1 | 3/3 | Complete | 2026-04-13 | -| 11. UI Enhancements | 4/4 | Complete | 2026-04-15 | | +| 11. UI Enhancements | 4/4 | Complete | 2026-04-15 | | diff --git a/.planning/STATE.md b/.planning/STATE.md index 31702cc..20ed6d0 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -5,7 +5,7 @@ milestone_name: Hardening & Validation current_plan: 3 status: verifying stopped_at: Completed 11-ui-enhancements/11-04-PLAN.md -last_updated: "2026-04-15T12:54:00.372Z" +last_updated: "2026-04-15T13:08:42.940Z" last_activity: 2026-04-15 progress: total_phases: 4 diff --git a/.planning/phases/11-ui-enhancements/11-02-SUMMARY.md b/.planning/phases/11-ui-enhancements/11-02-SUMMARY.md new file mode 100644 index 0000000..f167b5a --- /dev/null +++ b/.planning/phases/11-ui-enhancements/11-02-SUMMARY.md @@ -0,0 +1,95 @@ +--- +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 diff --git a/.planning/phases/11-ui-enhancements/11-03-SUMMARY.md b/.planning/phases/11-ui-enhancements/11-03-SUMMARY.md new file mode 100644 index 0000000..85e0360 --- /dev/null +++ b/.planning/phases/11-ui-enhancements/11-03-SUMMARY.md @@ -0,0 +1,127 @@ +--- +phase: 11-ui-enhancements +plan: "03" +subsystem: ui +tags: [alpine.js, i18n, theme, localStorage, pico-css, e2e, playwright] + +# Dependency graph +requires: + - phase: 11-01 + provides: base.html layout foundation with sidebar nav and Alpine.js loaded + +provides: + - Alpine.store('theme') cycling Light/Dark/System with localStorage persistence + - Alpine.store('i18n') FR/EN toggle with full static UI translation dictionary + - Top-right topbar with theme and language toggle buttons in base.html + - 4 E2E Playwright tests covering both toggles and localStorage persistence + +affects: + - Any future plan modifying base.html or adding new static UI strings + +# Tech tracking +tech-stack: + added: [] + patterns: + - alpine:init script placed before defer alpine.min.js for store registration timing + - Alpine.store() for global reactive state shared across all pages + - localStorage keys imptune_theme and imptune_lang for cross-reload persistence + - x-data on individual elements to scope Alpine binding where needed + - :aria-label binding used as Playwright selector anchor for theme button state + +key-files: + created: + - tests/e2e/test_theme_toggle.py + - tests/e2e/test_i18n_toggle.py + modified: + - imptune/templates/base.html + - imptune/static/app.css + - tests/test_static.py + +key-decisions: + - "Alpine stores registered via alpine:init event before defer script runs — ensures stores available at hydration" + - "x-data on topbar-controls div (not individual buttons) to scope Alpine scope once for both controls" + - ":aria-label bound to $store.theme.current to track current state — doubles as Playwright E2E selector" + - "Test assertion fixed: class-scoped check for 'class=empty-state>No printers configured' instead of raw string (which now also appears in i18n JS)" + +patterns-established: + - "i18n pattern: Alpine.store('i18n').t('key') via x-text binding on any element needing translation" + - "Theme pattern: data-theme on driven by Alpine.store('theme').cycle() on button click" + +requirements-completed: + - UIE-04 + - UIE-05 + +# Metrics +duration: 4min +completed: 2026-04-15 +--- + +# Phase 11 Plan 03: Theme + Language Toggle Summary + +**Alpine.js stores for Light/Dark/System theme cycling and FR/EN i18n toggle in base.html, both persisted via localStorage, with 4 passing Playwright E2E tests** + +## Performance + +- **Duration:** 4 min +- **Started:** 2026-04-15T09:05:49Z +- **Completed:** 2026-04-15T09:10:08Z +- **Tasks:** 2 +- **Files modified:** 4 (base.html, app.css, test_static.py, + 2 created E2E test files) + +## Accomplishments +- Alpine.store('theme') registered via alpine:init with Light/Dark/System cycling, localStorage persistence, and :aria-label binding for state tracking +- Alpine.store('i18n') with complete FR/EN translation dictionary covering all static UI strings (30+ keys per language) +- Top-right topbar added to base.html layout with theme toggle and lang toggle buttons, styled via new .main-wrapper + .topbar CSS classes +- 4 E2E Playwright tests: theme cycles, theme persists, lang switches nav label, lang persists — all GREEN + +## Task Commits + +Each task was committed atomically: + +1. **Task 1: Alpine.js stores + top-right controls in base.html** - `3353f45` (feat) +2. **Task 2: E2E tests for theme toggle and language toggle** - `4db15d6` (test) + +## Files Created/Modified +- `imptune/templates/base.html` - Alpine.js stores script (alpine:init), topbar with toggle buttons, x-text nav bindings +- `imptune/static/app.css` - Added .main-wrapper, .topbar, .topbar-controls styles +- `tests/test_static.py` - Added test_theme_toggle_present; fixed test_dashboard_shows_recent_printers assertion +- `tests/e2e/test_theme_toggle.py` - 2 E2E tests: theme cycles on click, theme persists across reload +- `tests/e2e/test_i18n_toggle.py` - 2 E2E tests: lang toggle switches nav label, lang persists across reload + +## Decisions Made +- Alpine stores registered via alpine:init event before the defer alpine.min.js script — inline scripts run synchronously before any deferred scripts, guaranteeing stores are defined before Alpine initializes +- x-data placed on the .topbar-controls div wrapper instead of individual buttons — scopes Alpine once for both controls +- :aria-label bound to $store.theme.current — provides a reactive state indicator that doubles as a stable Playwright selector (button[aria-label='auto'], button[aria-label='light'], etc.) +- Test assertion in test_dashboard_shows_recent_printers updated: raw string "No printers configured yet" now appears in the inline i18n JS, so assertion narrowed to class-qualified check + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 1 - Bug] Fixed test_dashboard_shows_recent_printers false failure due to i18n string** +- **Found during:** Task 1 (base.html stores + controls) +- **Issue:** Adding the i18n translation dictionary inline in base.html embeds the string `'No printers configured yet.'` verbatim in the JS. The existing test asserted this string was absent from the response, which now always fails regardless of DB state. +- **Fix:** Narrowed assertion to `'class="empty-state">No printers configured yet'` — this checks for the server-rendered HTML element rather than the raw string, which correctly distinguishes actual empty-state rendering from JS dictionary content. +- **Files modified:** tests/test_static.py +- **Verification:** test_dashboard_shows_recent_printers passes; all 6 test_static.py tests GREEN +- **Committed in:** 3353f45 (Task 1 commit) + +--- + +**Total deviations:** 1 auto-fixed (Rule 1 - bug in test assertion caused by i18n strings in HTML) +**Impact on plan:** Necessary correctness fix. No scope creep. + +## Issues Encountered +- Two pre-existing failures in tests/test_printer_crud.py (test_client_detail_returns_200, test_client_links_in_printer_list) confirmed pre-existing by git stash check — out of scope, logged for deferred triage. + +## User Setup Required +None - no external service configuration required. + +## Next Phase Readiness +- UIE-04 (theme toggle) and UIE-05 (i18n FR/EN) complete and verified +- base.html now has Alpine.js stores available globally — future plans can use $store.i18n.t() for any new static UI strings +- To add new translation keys: extend translations.fr and translations.en objects in the inline script in base.html + +--- +*Phase: 11-ui-enhancements* +*Completed: 2026-04-15* diff --git a/.planning/phases/11-ui-enhancements/11-VERIFICATION.md b/.planning/phases/11-ui-enhancements/11-VERIFICATION.md new file mode 100644 index 0000000..d6c024d --- /dev/null +++ b/.planning/phases/11-ui-enhancements/11-VERIFICATION.md @@ -0,0 +1,148 @@ +--- +phase: 11-ui-enhancements +verified: 2026-04-15T12:00:00Z +status: passed +score: 15/15 must-haves verified +gaps: [] +human_verification: + - test: "Open /printers/new and visually confirm the form is clearly separated and easy to find" + expected: "A clean standalone Add Printer form page with all fields visible" + why_human: "Visual layout quality and discoverability cannot be verified with grep or test output" + - test: "Toggle FR→EN and EN→FR on any page; confirm all nav labels, buttons, and headings switch instantly with no page reload" + expected: "Full-page language switch with no stale hardcoded text visible" + why_human: "Full-page visual scan needed to catch any untranslated strings that tests don't cover" + - test: "Click the Edit button on a printer, then visually verify that ALL fields (name, IP, port, driver, duplex, color, paper, collate, client) are pre-filled with that printer's data" + expected: "Every field shows the correct current value before any editing" + why_human: "E2E test only checks the name field; full pre-fill coverage requires visual inspection" +--- + +# Phase 11: UI Enhancements Verification Report + +**Phase Goal:** Improve the daily usability of ImpTune with printer editing, better form/list layout, client-scoped navigation, dark/light theme toggle, and bilingual (FR/EN) support. +**Verified:** 2026-04-15 +**Status:** passed +**Re-verification:** No — initial verification + +## Requirements Traceability Note + +UIE-01 through UIE-05 are defined in ROADMAP.md (Phase 11 section) and in the PLAN frontmatter for plans 11-01 through 11-04. They are **not** present in `.planning/REQUIREMENTS.md`, which covers only v1.1 Hardening requirements (RTVAL, UX, NYQ, RWR). The UIE IDs form a separate requirements namespace declared at phase definition time. No orphaned requirements were found — all five UIE IDs are claimed by plans within this phase. + +| Requirement | Source Plan | Description | Status | +| ----------- | ----------- | ----------- | ------ | +| UIE-01 | 11-02 | Printer edit modal with PATCH route | Satisfied | +| UIE-02 | 11-01 | Dedicated /printers/new page with 303 redirect | Satisfied | +| UIE-03 | 11-04 | Client detail page + clickable client names | Satisfied | +| UIE-04 | 11-03 | Theme toggle (Light/Dark/System) with localStorage | Satisfied | +| UIE-05 | 11-03 | FR/EN language toggle with localStorage | Satisfied | + +--- + +## Goal Achievement + +### Observable Truths + +| # | Truth | Status | Evidence | +| --- | --- | --- | --- | +| 1 | Every printer in the list has an Edit button opening a pre-filled form that saves in-place | VERIFIED | printer_edit_modal.html (102 lines): Edit button + hx-patch form with all fields; PATCH /printers/{id} in printers.py returns _render_printer_list; test_patch_printer GREEN | +| 2 | The new-printer form is visually separated from the printer list on its own page | VERIFIED | printers_new.html exists (100 lines); printers.html contains only a link to /printers/new with no inline form; test_printers_new_returns_200 GREEN | +| 3 | Every client name is a clickable link navigating to a filtered per-client page | VERIFIED | client_list.html wraps name in anchor to /clients/{c.id}; printer_list.html group headers link to /clients/{group_client_id} for assigned clients; client_detail.html + GET /clients/{id} route exist; test_client_detail_returns_200 + test_client_links_in_printer_list GREEN | +| 4 | A toggle lets the user switch Dark/Light/System theme with persistence | VERIFIED | base.html: Alpine.store('theme') with cycle() + localStorage; theme button with @click="$store.theme.cycle()"; test_theme_cycles_on_click + test_theme_persists_across_reload E2E GREEN | +| 5 | A toggle switches the UI between French and English with persistence | VERIFIED | base.html: Alpine.store('i18n') with 30+ keys per language; nav links use x-text="$store.i18n.t(...)"; lang toggle button present; test_language_toggle_switches_nav_label + test_language_persists_across_reload E2E GREEN | + +**Score:** 5/5 truths verified + +--- + +## Required Artifacts + +| Artifact | Expected | Lines | Status | Details | +| --- | --- | --- | --- | --- | +| `imptune/templates/printers_new.html` | Dedicated Add Printer page (GET /printers/new) | 100 | VERIFIED | Plain `
` — no hx-post, browser follows 303 naturally; contains all printer fields | +| `imptune/templates/printers.html` | Printer Library only, no inline form | 13 | VERIFIED | Contains link to /printers/new; zero printer form markup | +| `imptune/api/pages.py` (printers_new_page) | GET /printers/new route | — | VERIFIED | Route at line 88; passes clients + driver_data context | +| `imptune/api/printers.py` (PATCH route) | PATCH /printers/{id} handler | — | VERIFIED | Route at line 126; full validation; sets updated_at; returns _render_printer_list | +| `imptune/api/printers.py` (RedirectResponse) | POST /printers returns 303 | — | VERIFIED | Line 113: `return RedirectResponse(url="/printers", status_code=303)` | +| `imptune/templates/partials/printer_edit_modal.html` | Edit modal with pre-filled PATCH form | 102 | VERIFIED | hx-patch, hx-target="#printer-list", hx-on::after-request close; all printer fields pre-filled | +| `imptune/templates/partials/printer_list.html` | Edit button + client name links in group headers | 56 | VERIFIED | Includes printer_edit_modal.html per row; client link logic via group_client_id | +| `imptune/templates/partials/client_list.html` | Client names wrapped in anchor tags | 23 | VERIFIED | `{{ c.name }}` | +| `imptune/templates/client_detail.html` | Per-client filtered printer page | 12 | VERIFIED | Extends base.html; renders client.name as h1; includes printer_list.html partial | +| `imptune/api/pages.py` (client_detail) | GET /clients/{client_id} route | — | VERIFIED | Route at line 159; 404 on missing client; grouped dict for filtered printer list | +| `imptune/templates/base.html` | Alpine.js stores + theme/lang toggle buttons | 158 | VERIFIED | alpine:init script before defer; Alpine.store('theme') + Alpine.store('i18n'); topbar buttons wired | +| `tests/e2e/test_printer_edit.py` | E2E: modal open, pre-fill, submit, list update | — | VERIFIED | 2 tests, both GREEN | +| `tests/e2e/test_theme_toggle.py` | E2E: theme cycles, localStorage persists | — | VERIFIED | 2 tests, both GREEN | +| `tests/e2e/test_i18n_toggle.py` | E2E: lang toggle switches nav labels, persists | — | VERIFIED | 2 tests, both GREEN | + +--- + +## Key Link Verification + +| From | To | Via | Status | Details | +| --- | --- | --- | --- | --- | +| printers_new.html | POST /printers | Plain `` (no hx-post) | WIRED | Line 9 of printers_new.html; no HTMX on main form — browser follows 303 | +| printers.py create_printer | /printers | `RedirectResponse(url="/printers", status_code=303)` | WIRED | Line 113; test_create_printer_redirects asserts 303 | +| printer_list.html | printer_edit_modal.html | `{% include "partials/printer_edit_modal.html" %}` inside `{% for p in printers %}` | WIRED | Line 39 of printer_list.html; p is in scope for modal | +| printer_edit_modal.html | PATCH /printers/{id} | `hx-patch="/printers/{{ p.id }}"` on form element | WIRED | Line 16 of printer_edit_modal.html | +| printers.py update_printer | _render_printer_list | `return _render_printer_list(request)` on success | WIRED | Line 174 of printers.py | +| client_list.html | /clients/{c.id} | `{{ c.name }}` | WIRED | Line 15 of client_list.html | +| printer_list.html | /clients/{group_client_id} | Conditional `

` | WIRED | Lines 7-12 of printer_list.html; Unassigned renders as plain text | +| base.html alpine:init | Alpine.store('theme') + Alpine.store('i18n') | `document.addEventListener('alpine:init', ...)` before `