32 KiB
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>
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
<dialog>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:
/printersbecomes the Printer Library only — the Add Printer form is removed from this page- A dedicated
/printers/newpage holds the Add Printer form - An "Add Printer" button (or link) on
/printersnavigates 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-themeattribute on<html>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
$storeor 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
<dialog>close is triggered (close button, backdrop click, or both)
Deferred Ideas (OUT OF SCOPE)
None — discussion stayed within phase scope. </user_constraints>
<phase_requirements>
Phase Requirements
| ID | Description | Research Support |
|---|---|---|
| UIE-01 | Printer edit modal via native <dialog>, 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 <html>; 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 |
| </phase_requirements> |
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 <dialog> 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 <dialog> + 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: <dialog> with edit form
│ └── client_list.html # CHANGE: wrap client name in <a href="/clients/{id}">
└── 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 <dialog>, Alpine.js initialises normally because <dialog> 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.
# 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 <dialog> uses:
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 <dialog> Modal (UIE-01)
What: Pico CSS 2.1.1 styles <dialog> natively. Open with dialog.showModal(), close with dialog.close() or a <form method="dialog"> cancel button.
When to use: Any lightweight overlay that does not need a third-party modal library.
<!-- partials/printer_edit_modal.html — included once per printer row -->
<dialog id="edit-modal-{{ p.id }}">
<article>
<header>
<button aria-label="Close" rel="prev"
onclick="document.getElementById('edit-modal-{{ p.id }}').close()"></button>
<h3>Edit Printer</h3>
</header>
<div x-data="{ ip: '{{ p.ip_address }}', port: '{{ p.port_name }}', portEdited: true }">
<form hx-patch="/printers/{{ p.id }}"
hx-target="#printer-list"
hx-swap="outerHTML"
hx-on::after-request="document.getElementById('edit-modal-{{ p.id }}').close()">
<!-- same fields as printer_form.html, pre-filled via Jinja2 p.* values -->
</form>
</div>
</article>
</dialog>
<!-- Trigger button in the Actions column -->
<button class="secondary"
onclick="document.getElementById('edit-modal-{{ p.id }}').showModal()">
Edit
</button>
Pico CSS dialog header close button: Pico CSS 2.x styles <button rel="prev"> inside <header> as a close × icon automatically. This is the idiomatic Pico close pattern.
HTMX after-request close: hx-on::after-request fires after a successful HTMX request. Use it to close the modal programmatically. For error cases the modal stays open, which is correct — the error fragment replaces #printer-list but the modal remains for correction.
Pattern 3: POST → Redirect for /printers/new (UIE-02)
What: After the add form is submitted, the server should redirect to /printers instead of returning a partial. This requires a change to the existing POST /printers handler or detecting the request origin.
How to implement cleanly: The simplest approach is to detect the HX-Request header. If it is present (HTMX request from the library page or any HTMX consumer), return the partial as today. If absent (full-page form submit from /printers/new), return a RedirectResponse to /printers.
from fastapi.responses import RedirectResponse
@router.post("", response_class=HTMLResponse)
def create_printer(request: Request, ...) -> HTMLResponse:
# ... validate + create ...
if request.headers.get("HX-Request"):
return _render_printer_list(request)
return RedirectResponse(url="/printers", status_code=303)
Why 303 not 302: RFC 7231 requires 303 See Other for POST→GET redirect to ensure the browser does a GET on the redirect target.
Pattern 4: Alpine.js $store for Global Reactive State (UIE-04 + UIE-05)
What: Alpine.js 3.x Alpine.store(name, initialState) creates a globally accessible reactive object. Any element with x-data can read it via $store.name.
Placement: Define stores in a <script> block in base.html using the alpine:init event, which fires before Alpine initialises the DOM:
<!-- In base.html <head> or just before </body> -->
<script>
document.addEventListener('alpine:init', () => {
Alpine.store('theme', {
current: localStorage.getItem('theme') || 'auto',
cycle() {
const order = ['light', 'dark', 'auto'];
const next = order[(order.indexOf(this.current) + 1) % 3];
this.current = next;
localStorage.setItem('theme', next);
document.documentElement.setAttribute('data-theme', next);
},
init() {
document.documentElement.setAttribute('data-theme', this.current);
}
});
Alpine.store('i18n', {
lang: localStorage.getItem('lang') || 'fr',
translations: {
fr: {
printers: 'Imprimantes',
clients: 'Clients',
drivers: 'Pilotes',
packages: 'Paquets',
dashboard: 'Tableau de bord',
add_printer: 'Ajouter une imprimante',
edit: 'Modifier',
delete: 'Supprimer',
save: 'Enregistrer',
cancel: 'Annuler',
// ... all UI strings
},
en: {
printers: 'Printers',
clients: 'Clients',
drivers: 'Drivers',
packages: 'Packages',
dashboard: 'Dashboard',
add_printer: 'Add Printer',
edit: 'Edit',
delete: 'Delete',
save: 'Save',
cancel: 'Cancel',
// ... all UI strings
}
},
t(key) {
return this.translations[this.lang][key] || key;
},
toggle() {
this.lang = this.lang === 'fr' ? 'en' : 'fr';
localStorage.setItem('lang', this.lang);
}
});
});
</script>
Usage in templates: Any element can bind text via x-text="$store.i18n.t('edit')" or use it inline with :value. Buttons and labels need x-text; placeholder and aria-label attributes need :placeholder and :aria-label.
defer script tag ordering: alpine.min.js is loaded with defer. The alpine:init event fires when Alpine is ready but before it scans the DOM. The store definition script MUST either (a) be placed before the Alpine defer load and listen to alpine:init, or (b) be a defer script placed after the Alpine script tag. Option (a) is the safe pattern used by Alpine.js documentation.
Pattern 5: GET /clients/{id} Client Detail Page (UIE-03)
What: New page route in pages.py. Queries printers filtered by client_id. Renders new client_detail.html template that reuses partials/printer_list.html.
# In api/pages.py
@router.get("/clients/{client_id}", response_class=HTMLResponse)
def client_detail(request: Request, client_id: int):
from imptune.db.models import Client, Driver, Printer
client = Client.get_or_none(Client.id == client_id)
if client is None:
return HTMLResponse(content="<h1>404</h1>", status_code=404)
query = (
Printer.select(Printer, Client)
.join(Client, JOIN.LEFT_OUTER)
.where(Printer.client == client_id)
.order_by(Printer.name)
)
grouped = {client.name: list(query)} # single-client grouped dict
all_drivers = list(Driver.select().order_by(Driver.uploaded_at.desc()))
driver_data = [
{"driver": d, "names": json.loads(d.driver_desc) if d.driver_desc else []}
for d in all_drivers
]
return templates.TemplateResponse(
request=request,
name="client_detail.html",
context={"client": client, "grouped": grouped, "driver_data": driver_data},
)
Reuse: client_detail.html includes partials/printer_list.html directly. Since printer_list.html already renders from a grouped dict, the same partial works for both the full printer library and the per-client view without modification — as long as the modal partial is also included.
Anti-Patterns to Avoid
- Separate Alpine component per printer row for the modal: Do not use
x-dataon each<tr>to manage modal open state. Usedocument.getElementById(...).showModal()directly on the buttononclick. Alpine is only needed inside the dialog for the IP/port reactive fields. - Putting
<dialog>outsideprinter_list.html: The modal must be co-located with the printer row data (inside the Jinja2{% for p in printers %}loop) so it can be pre-filled withp.*values. Do not try to fill it via JavaScript after open — it will not work with HTMX-swapped content. - Using Alpine
$storefor modal open/close state: Native<dialog>.showModal()/.close()is simpler and does not require Alpine state. Reserve$storefor cross-page state (theme, language). - Translating Jinja2 server-rendered strings with Alpine i18n: Server-rendered strings (e.g., dynamic data like printer names, error messages from the server) cannot be translated by Alpine. Only static UI chrome (labels, buttons, headings, nav items) should use
x-text="$store.i18n.t()". Server error messages must be translated server-side if needed — but since they are out of scope for this phase, leave them in English. - Using
hx-method="PATCH"instead ofhx-patch: HTMX 2.x useshx-patchdirectly.hx-methodis not a real HTMX attribute. Also do NOT add a hidden_methodfield — that is a Rails/Laravel pattern HTMX does not use.
Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---|---|---|---|
| Modal overlay | Custom CSS + JS show/hide | Native <dialog> via Pico CSS |
Browser-native, accessible, keyboard-focustrapped, Pico already styles it |
| Theme persistence | Custom CSS class toggling | data-theme attr on <html> + Pico CSS |
Pico 2.x natively supports light/dark/auto on this attribute |
| Global reactive state | Custom event bus or window globals | Alpine.js Alpine.store() |
Built into Alpine 3.x, reactive, no extra libraries |
| i18n library | Vue-i18n, i18next | Alpine $store with translation dict |
Stack is Alpine; no build step; total string count is ~30 keys |
| HTTP method override | Hidden _method field |
hx-patch attribute (HTMX native) |
HTMX 2.x sends real PATCH method natively |
| Client-side routing | SPA router | Standard <a href="/clients/{id}"> links |
Server-rendered pages; no SPA needed; simpler |
Key insight: Every custom solution adds maintenance burden without solving problems the existing stack doesn't already handle. The entire phase is achievable with zero new dependencies.
Common Pitfalls
Pitfall 1: Alpine.js store not available when templates render
What goes wrong: $store.i18n.t('edit') in a template returns undefined or throws because the store was not defined before Alpine scanned that element.
Why it happens: If the store-definition <script> runs after Alpine has already initialised the DOM (e.g., wrong script tag placement or missing alpine:init listener), stores are undefined.
How to avoid: Always define stores inside a document.addEventListener('alpine:init', ...) handler. The alpine:init event is emitted by Alpine before it walks the DOM, ensuring stores exist when directives are evaluated.
Warning signs: Console error "Alpine: Cannot read properties of undefined (reading 't')" on page load.
Pitfall 2: Modal pre-fill not updating when HTMX swaps the list
What goes wrong: After editing a printer, the HTMX swap replaces #printer-list. The new HTML includes fresh modals with correct Jinja2-rendered values. BUT if the user opens a modal again for the same printer, they may see stale values if the modal is opened before the swap completes.
Why it happens: The hx-on::after-request closes the modal after the swap. If the swap and close happen in the wrong order, the old modal DOM is gone before close() is called.
How to avoid: Use hx-on::after-request on the form (fires after response is processed and swap is done). The dialog referenced by ID will be in the new DOM by then — close it via document.getElementById(...) which re-queries the DOM at call time.
Pitfall 3: HTMX PATCH returns partial but POST redirect was changed
What goes wrong: After UIE-02 adds the HX-Request detection, a future refactor or test sends a POST without HX-Request header and unexpectedly gets a 303 redirect instead of an HTML partial.
Why it happens: The HX-Request detection branch changes the POST contract. Tests that use TestClient.post(...) without setting the HX-Request header will now get 303.
How to avoid: Update test_printer_crud.py tests that POST to /printers to add headers={"HX-Request": "true"} when they expect the partial response. Or — alternative — use the redirect for ALL POST /printers responses and have /printers page rebuild the list from scratch on GET (simpler for the new page flow, slightly less seamless if accessed from the library page via HTMX).
Decision needed for planner: The CONTEXT.md says the add form on /printers/new redirects after submit. But the existing HTMX tests on /printers POST expect a partial response. The safest approach is: keep HTMX POST returning partial (for future HTMX consumers), and on /printers/new use a plain <form> (no HTMX) so the browser follows the redirect. This avoids branching logic in the handler.
Pitfall 4: Pico CSS <dialog> close button visual
What goes wrong: The <button rel="prev"> close button only renders as an × if it is inside a <header> element within the <article> wrapper inside <dialog>. Wrong nesting produces an unstyled button.
Why it happens: Pico CSS 2.x <dialog> styling relies on the article > header > button[rel="prev"] selector pattern.
How to avoid: Always nest: <dialog> > <article> > <header> > <button rel="prev">.
Pitfall 5: x-text vs server-rendered text in translated templates
What goes wrong: A <button>Delete</button> with x-text="$store.i18n.t('delete')" will show "Delete" initially (server-rendered text node), then flicker to the translated value when Alpine initialises.
Why it happens: Alpine.js initialises asynchronously after DOM parse. The initial text content is visible briefly before Alpine overwrites it.
How to avoid: Use empty text content in translated elements: <button x-text="$store.i18n.t('delete')"></button>. Alpine fills it on init. For SSR fallback, this is acceptable for a tool used by technicians (no progressive enhancement requirement stated).
Pitfall 6: Printer.updated_at field exists but is never set on edit
What goes wrong: PATCH updates printer fields but updated_at stays at creation time.
Why it happens: Peewee .save() without specifying fields updates all columns, but updated_at has a default=_utcnow which only fires on .create(), not .save().
How to avoid: Explicitly set printer.updated_at = _utcnow() before printer.save() in the PATCH handler.
Code Examples
Verified patterns from live codebase inspection:
Existing Delete button pattern (printer_list.html — lines 34-40)
<button
hx-delete="/printers/{{ p.id }}"
hx-target="#printer-list"
hx-swap="outerHTML"
hx-confirm="Delete '{{ p.name }}'?">
Delete
</button>
Edit button slots in beside this — same column, same pattern, different HTTP method and trigger mechanism.
Existing Alpine.js x-data + x-model pattern (printer_form.html — line 1)
<div x-data="{ ip: '{{ printer.ip_address if printer else '' }}',
port: '{{ printer.port_name if printer else '' }}',
portEdited: {{ 'true' if printer else 'false' }} }">
Edit modal pre-fill follows the exact same pattern, with portEdited: true hardcoded (edit mode always treats port as user-set).
Existing grouped-by-client query (api/printers.py — lines 33-48)
query = (
Printer.select(Printer, Client)
.join(Client, JOIN.LEFT_OUTER)
.order_by(Client.name, Printer.name)
)
grouped: dict[str, list[Printer]] = defaultdict(list)
for p in query:
client_name = p.client.name if p.client_id else "Unassigned"
grouped[client_name].append(p)
Client detail page uses the same pattern filtered by Printer.client == client_id.
Pico CSS data-theme toggle (already wired in base.html — line 2)
<html lang="en" data-theme="auto">
Alpine.js theme store only needs to call document.documentElement.setAttribute('data-theme', value). No CSS changes required.
State of the Art
| Old Approach | Current Approach | Impact |
|---|---|---|
| Custom modal JS libraries | Native <dialog> + Pico CSS |
Zero extra JS, accessible by default |
hx-method attribute override |
hx-patch / hx-delete native HTMX 2.x |
Cleaner, no hidden fields |
| Alpine.js component-scoped data only | Alpine.js $store (v3.x) |
True global reactive state across partials |
Page-level x-data for global state |
Alpine.store() + alpine:init event |
Reliable init order, accessible anywhere |
Open Questions
-
POST /printers response strategy for UIE-02
- What we know: Current tests expect a partial HTML response from
POST /printers. CONTEXT.md says/printers/newshould redirect after submit. - What's unclear: Should the PATCH handler detect HX-Request, or should
/printers/newuse a plain form (non-HTMX) so the server always redirects? - Recommendation: Use a plain
<form>(nohx-post) on/printers/new. The server always redirects onPOST /printers. Update existing tests to usefollow_redirects=Falseand assert 303, or update them to follow the redirect and check the resulting page. This avoids branching logic in the handler and is consistent with the "redirect after form submit" web convention.
- What we know: Current tests expect a partial HTML response from
-
Where to include
printer_edit_modal.html- What we know: The modal must be inside the
{% for p in printers %}loop to accessp.*values. - What's unclear: Should the modal be inside
printer_list.htmlor a separate include per page that uses the list? - Recommendation: Include the modal directly inside
printer_list.html's loop, as a Jinja2{% include %}or inline block. This keeps it co-located with the trigger button and avoids duplication.
- What we know: The modal must be inside the
-
Translation string completeness for UIE-05
- What we know: All static UI strings must be translated. Exact count unknown until templates are audited.
- What's unclear: Are error messages from the server (e.g., "Printer name is required.") in scope?
- Recommendation: Server-side error messages are out of scope for this phase (they appear as HTMX-swapped fragments). Only translate static template strings (nav labels, buttons, headings, form labels).
Validation Architecture
Test Framework
| Property | Value |
|---|---|
| Framework | pytest 8.x + httpx 0.27 + pytest-playwright |
| Config file | none — pytest.ini or pyproject.toml not detected; pytest auto-discovers tests/ |
| Quick run command | pytest tests/ -x -q --ignore=tests/e2e |
| Full suite command | pytest tests/ -q |
| E2E run command | pytest tests/e2e/ -q (requires live server + playwright browsers) |
Phase Requirements → Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|---|---|---|---|---|
| UIE-01 | PATCH /printers/{id} updates DB and returns printer list partial |
unit/integration | pytest tests/test_printer_crud.py -x -q -k "patch or edit" |
❌ Wave 0 |
| UIE-01 | Edit modal triggers showModal() and pre-fills values |
E2E (Alpine.js) | pytest tests/e2e/test_printer_edit.py -x -q |
❌ Wave 0 |
| UIE-02 | GET /printers/new returns 200 with add form |
integration | pytest tests/test_printer_crud.py -x -q -k "printers_new" |
❌ Wave 0 |
| UIE-02 | POST /printers returns 303 redirect to /printers |
integration | pytest tests/test_printer_crud.py -x -q -k "redirect" |
❌ Wave 0 |
| UIE-02 | GET /printers no longer contains the add form |
integration | pytest tests/test_printer_crud.py -x -q -k "library_no_form" |
❌ Wave 0 |
| UIE-03 | GET /clients/{id} returns 200 with filtered printer list |
integration | pytest tests/test_printer_crud.py -x -q -k "client_detail" |
❌ Wave 0 |
| UIE-03 | GET /clients/9999 returns 404 |
integration | pytest tests/test_printer_crud.py -x -q -k "client_not_found" |
❌ Wave 0 |
| UIE-03 | Client names appear as <a href="/clients/{id}"> in printer list |
integration | pytest tests/test_printer_crud.py -x -q -k "client_links" |
❌ Wave 0 |
| UIE-04 | Theme toggle button present in base layout | integration | pytest tests/test_static.py -x -q -k "theme_toggle" |
❌ Wave 0 |
| UIE-04 | Alpine.js theme store persists to localStorage and sets data-theme | E2E | pytest tests/e2e/test_theme_toggle.py -x -q |
❌ Wave 0 |
| UIE-05 | Alpine.js i18n store switches all labels between FR/EN | E2E | pytest tests/e2e/test_i18n_toggle.py -x -q |
❌ Wave 0 |
Sampling Rate
- Per task commit:
pytest tests/ -x -q --ignore=tests/e2e - Per wave merge:
pytest tests/ -q - Phase gate: Full suite green (including E2E) before
/gsd:verify-work
Wave 0 Gaps
tests/test_printer_crud.py— add test functions for UIE-01 (PATCH), UIE-02 (redirect, new page), UIE-03 (client detail, 404, links)tests/e2e/test_printer_edit.py— E2E: open modal, check pre-fill, submit, confirm list updatetests/e2e/test_theme_toggle.py— E2E: click theme button, verifydata-themeattribute cycles, verify localStoragetests/e2e/test_i18n_toggle.py— E2E: click lang toggle, verify nav label text changes, verify localStorage
Existing test files (test_printer_crud.py, conftest.py, tests/e2e/conftest.py) are already in place and working — new test functions are added to existing files, no new infrastructure needed.
Sources
Primary (HIGH confidence — live codebase inspection)
imptune/static/pico.min.cssv2.1.1 — confirmeddata-themelight/dark/auto support,<dialog>native styling,button[rel="prev"]close patternimptune/static/alpine.min.jsv3.15.11 — confirmedAlpine.store()API,alpine:initevent,$storemagic propertyimptune/static/htmx.min.jsv2.0.8 — confirmedhx-patchattribute support (no method override needed)imptune/api/printers.py— confirmed_render_printer_list()helper, form field names,_error_response()patternimptune/api/pages.py— confirmed route structure, template context patterns, grouped query patternimptune/templates/partials/printer_list.html— confirmed Actions column, grouped dict rendering,#printer-listtarget IDimptune/templates/partials/printer_form.html— confirmed Alpine.js x-data fields, form field names, driver upload sub-form locationimptune/db/models.py— confirmedPrinter.updated_atfield exists, all field names for PATCH form
Secondary (MEDIUM confidence — Alpine.js 3.x documentation patterns)
- Alpine.js
$storeandalpine:initpattern: confirmed in Alpine.js v3 source code (version string "3.15.11" found in static file;Jrfunction =Alpine.store,alpine:initdispatch confirmed inOr=Alpine.start)
Tertiary (LOW confidence)
- None — all critical claims verified from live files.
Metadata
Confidence breakdown:
- Standard stack: HIGH — all versions confirmed from bundled static files and requirements.txt
- Architecture: HIGH — all patterns grounded in existing code; no speculation
- Pitfalls: HIGH — derived from direct code inspection (e.g.,
portEditedflag,updated_atdefault, Alpine init order) - Test map: HIGH — existing test infrastructure fully inspected; gaps identified precisely
Research date: 2026-04-15 Valid until: 2026-05-15 (stable stack; only changes if Alpine/HTMX/Pico are upgraded)