# 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 `