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