diff --git a/.planning/phases/11-ui-enhancements/11-RESEARCH.md b/.planning/phases/11-ui-enhancements/11-RESEARCH.md new file mode 100644 index 0000000..dcc452b --- /dev/null +++ b/.planning/phases/11-ui-enhancements/11-RESEARCH.md @@ -0,0 +1,568 @@ +# Phase 11: UI Enhancements - Research + +**Researched:** 2026-04-15 +**Domain:** FastAPI + Jinja2 + HTMX + Alpine.js + Pico CSS — frontend UX improvements +**Confidence:** HIGH (all findings grounded in live codebase inspection) + +--- + + +## User Constraints (from CONTEXT.md) + +### Locked Decisions + +**UIE-01 — Printer Edit Modal:** +- Edit button added to each row in the Actions column, next to the existing Delete button (small button, labelled "Edit") +- Clicking Edit opens a native HTML `` modal pre-filled with that printer's fields +- Modal contains printer fields only (name, IP, port, driver, duplex, color, paper, collate, client) — no embedded driver upload form +- Submitting the edit form sends HTMX PATCH to `/printers/{id}`, closes the modal, and refreshes the printer list in-place (no page reload) + +**UIE-02 — Add-Printer Form Separation:** +- `/printers` becomes the Printer Library only — the Add Printer form is removed from this page +- A dedicated `/printers/new` page holds the Add Printer form +- An "Add Printer" button (or link) on `/printers` navigates to `/printers/new` +- After submitting the add form, auto-redirect back to `/printers` +- The driver upload form embedded in the printer form stays on `/printers/new` + +**UIE-03 — Client-Filtered Navigation:** +- Client names become clickable links everywhere they appear (printer list group headers, client list table) +- Links navigate to `/clients/{id}` — a dedicated page per client +- `/clients/{id}` shows: client name as page title + filtered printer list for that client only +- Edit and Delete actions on `/clients/{id}` work identically to `/printers` (same modal, same HTMX routes) + +**UIE-04 — Theme Toggle:** +- Icon button in the top-right corner of the page (in the main layout, visible on all pages) +- Cycles through: Light → Dark → System on each click; shows current mode icon (sun / moon / auto) +- Alpine.js sets `data-theme` attribute on `` element + persists choice in localStorage +- Pico CSS already supports `data-theme="light"`, `data-theme="dark"`, `data-theme="auto"` natively — no CSS changes needed for basic theming + +**UIE-05 — Language Toggle:** +- FR/EN toggle in the top-right corner alongside the theme icon +- Implemented with Alpine.js translation object: all UI strings stored in a JS translation dictionary keyed by `fr` / `en` +- Switching language updates instantly with no page reload; choice persists in localStorage +- Alpine.js `$store` or top-level x-data used to make the language reactive across all components +- All labels, buttons, headings, and messages must be translated (no hardcoded English strings left in templates) + +### Claude's Discretion +- Exact icon choices for the theme cycle button (emoji vs SVG vs Unicode symbols) +- Exact layout/styling of the top-right controls area (spacing, grouping of theme + language) +- Translation string file organisation (inline in base.html JS block vs separate translations.js file) +- How `` close is triggered (close button, backdrop click, or both) + +### Deferred Ideas (OUT OF SCOPE) +None — discussion stayed within phase scope. + + +--- + + +## Phase Requirements + +| ID | Description | Research Support | +|----|-------------|-----------------| +| UIE-01 | Printer edit modal via native ``, pre-filled, HTMX PATCH `/printers/{id}`, in-place list refresh | PATCH route pattern, dialog open/close, reuse printer_form partial minus upload form | +| UIE-02 | Separate `/printers/new` page for add form; `/printers` shows library + "Add Printer" link; POST redirects back | New GET route `/printers/new` in pages.py, redirect on POST, `/printers` strips form | +| UIE-03 | Client names as links to `/clients/{id}`; per-client page with filtered printer list | New GET route `/clients/{id}` in pages.py, filtered Printer query, client_id lookup | +| UIE-04 | Theme toggle (Light/Dark/System) in top-right; Alpine.js sets `data-theme` on ``; persisted in localStorage | Pico CSS 2.1.1 native data-theme support confirmed, Alpine.js store pattern | +| UIE-05 | FR/EN language toggle; Alpine.js `$store` with translation dictionary; instant update, localStorage persist | Alpine.js `$store` global reactive store, x-text binding pattern | + + +--- + +## Summary + +Phase 11 is a pure frontend sprint on top of a fully operational v1.0 backend. No database schema changes, no new backend business logic. The stack is locked: **FastAPI 0.115, Jinja2 3.1, HTMX 2.0.8, Alpine.js 3.15.11, Pico CSS 2.1.1** — all bundled statically, no CDN dependency, no npm build step. + +The five requirements decompose into three backend touches (one new PATCH route, two new GET page routes) and two purely frontend changes (theme toggle + i18n store, both live entirely in `base.html`). The heaviest integration work is UIE-01 (edit modal): it reuses the existing `printer_form.html` partial with a changed form action and HTMX method, wrapped in a `` element that Pico CSS already styles natively. + +The test infrastructure is pytest (unit/integration via `TestClient`) plus pytest-playwright for E2E. All new routes have clear HTTP-level contracts testable without a browser. Alpine.js-driven behaviours (theme persistence, language switching) require either E2E or visual-manual verification; unit tests cannot observe DOM state mutations from `x-data`. + +**Primary recommendation:** Implement in 3 plans: (1) backend routes + modal PATCH + form separation, (2) client detail page, (3) base.html theme + i18n controls. + +--- + +## Standard Stack + +### Core (all versions confirmed from static files and requirements.txt) + +| Library | Version | Purpose | Why Standard | +|---------|---------|---------|--------------| +| FastAPI | 0.115.x | HTTP routing, form parsing, template rendering | Already the app framework | +| Jinja2 | 3.1.x | Server-side HTML templating with `{% include %}` | Already used for all pages and partials | +| HTMX | 2.0.8 | Declarative AJAX — POST/DELETE/PATCH with HTML swap | Already used for all list mutations | +| Alpine.js | 3.15.11 | Client-side reactivity, `x-data`, `$store` | Already used for port auto-fill; `defer` loaded | +| Pico CSS | 2.1.1 | Semantic CSS framework with `` + `data-theme` support | Already loaded; native dialog + theme support confirmed | +| Peewee | 3.17.x | ORM for SQLite — Printer/Client/Driver models | Already the ORM; no schema changes needed | +| pytest + TestClient | 8.x / httpx 0.27 | HTTP-level integration tests | Existing test harness used across 14 plans | +| pytest-playwright | current | E2E Alpine.js verification | Established in Phase 9 UX-02 | + +### No New Dependencies +This phase requires zero new pip installs and zero new JS libraries. Everything needed is already bundled in `imptune/static/`. + +--- + +## Architecture Patterns + +### Recommended Project Structure Changes + +``` +imptune/ +├── api/ +│ ├── printers.py # ADD: PATCH /printers/{id} handler +│ └── clients.py # (no changes needed — POST /clients unchanged) +├── templates/ +│ ├── base.html # ADD: top-right controls (theme + lang toggle) +│ ├── printers.html # CHANGE: remove form include, add "Add Printer" link +│ ├── printers_new.html # NEW: /printers/new page +│ ├── client_detail.html # NEW: /clients/{id} page +│ └── partials/ +│ ├── printer_list.html # CHANGE: add Edit button + client links +│ ├── printer_edit_modal.html # NEW: with edit form +│ └── client_list.html # CHANGE: wrap client name in +└── api/pages.py # ADD: GET /printers/new, GET /clients/{id} +``` + +### Pattern 1: HTMX PATCH Route (UIE-01) + +**What:** Add `PATCH /printers/{id}` to `api/printers.py`. Accepts same form fields as POST. Updates the DB record. Returns `_render_printer_list(request)` — identical to DELETE response. + +**When to use:** Any in-place mutation that follows the existing delete pattern. + +**Key implementation note:** `printer_form.html` has Alpine.js `x-data` with `ip` and `port` bound to `x-model`. Inside a ``, Alpine.js initialises normally because `` is a standard DOM element — no special wiring needed. The `portEdited` flag should be initialised to `true` in the edit modal (unlike the add form where it starts `false`) so that editing IP does not clobber a manually-set port. + +```python +# In api/printers.py — add alongside existing POST and DELETE +@router.patch("/{printer_id}", response_class=HTMLResponse) +def update_printer( + request: Request, + printer_id: int, + name: str = Form(...), + ip_address: str = Form(...), + port_name: str = Form(...), + duplex_mode: str = Form("OneSided"), + color_mode: str = Form(""), + paper_size: str = Form("A4"), + collate: str = Form(""), + client_id: str = Form(""), + driver_id: str = Form(""), +) -> HTMLResponse: + printer = Printer.get_or_none(Printer.id == printer_id) + if printer is None: + return _error_response(f"Printer {printer_id} not found.", status_code=404) + # ... validate + update fields ... + printer.updated_at = _utcnow() # models.py already has updated_at field + printer.save() + return _render_printer_list(request) +``` + +**HTMX PATCH caveat:** HTMX 2.x sends PATCH natively via `hx-patch`. FastAPI 0.115 registers `@router.patch(...)` without issue. The form inside the `` uses: +```html +hx-patch="/printers/{{ p.id }}" +hx-target="#printer-list" +hx-swap="outerHTML" +``` +No HTMX method override (`X-HTTP-Method-Override`) is needed — HTMX sends the real HTTP method. + +### Pattern 2: Native `` Modal (UIE-01) + +**What:** Pico CSS 2.1.1 styles `` natively. Open with `dialog.showModal()`, close with `dialog.close()` or a `
` cancel button. + +**When to use:** Any lightweight overlay that does not need a third-party modal library. + +```html + + +
+
+ +

Edit Printer

+
+
+ + + +
+
+
+ + + +``` + +**Pico CSS dialog header close button:** Pico CSS 2.x styles `