# Phase 9: UX Tech Debt Closure - Context

**Gathered:** 2026-04-13
**Status:** Ready for planning

<domain>
## Phase Boundary

Close the three carried-over UX defects from v1.0 (UX-01, UX-02, UX-03) so the build rolled out in Phase 11 is the polished one technicians actually use. Also fix the `POST /drivers/upload` HTTP 500 surfaced during Phase 8 kickoff — it's a hard blocker for UX-01 and is bundled into that work. No new product capabilities, no refactors outside the touched files.

</domain>

<decisions>
## Implementation Decisions

### Plan structure (3 plans, sequential)
- **09-01 — UX-01 + driver upload 500 fix (bundled).** Reproduce the 500, write a failing pytest first (TDD), fix root cause, then add inline driver upload input to the printer form template, wire HTMX OOB swap so the printer form's `<select name="driver_id">` refreshes after upload, and auto-select the newly uploaded driver. Single plan because the 500 fix and the inline-upload work touch the same endpoint/template pair — splitting them would mean touching `imptune/api/drivers.py` twice with an intermediate broken state.
- **09-02 — UX-02 live verification via Playwright headless test.** Add Playwright as a dev dependency, write a headless test that loads the printer form, types an IP address, and asserts the `port_name` input auto-fills with `IP_<dotted_underscore>`. The test itself IS the evidence cited in `09-VALIDATION.md` for UX-02.
- **09-03 — UX-03 per-script download links.** Add 3 new routes `GET /printers/{id}/scripts/install.ps1`, `.../uninstall.ps1`, `.../detect.ps1` that regenerate the .ps1 text on the fly from the saved printer config (same generators used by package export) and return it with appropriate `Content-Disposition`. Wire 3 direct-download links into `printer_detail.html` next to the existing package export buttons.
- Total plans: **3** (not 4). Phase 8 flagged the 500 as blocking UX-01, so it rides the same plan as UX-01 rather than a standalone bugfix plan.

### UX-01: Driver upload flow (inline in printer form)
- **Requirement re-read:** UX-01 says "After a new driver is uploaded *on the printer form*, the DriverDesc dropdown refreshes automatically." The v1.0 printer form has NO inline upload — that's the tech debt. Adding it IS in scope; it's what the requirement asks for.
- **Current v1.0 flow (broken/missing):** Technician visits `/drivers` separately, uploads driver, navigates back to the printer form, then sees the driver only if the page is reloaded. This is what we're replacing.
- **New flow:** Inline driver upload control inside `imptune/templates/partials/printer_form.html`, next to or above the `<select name="driver_id">`.
- **Refresh mechanism:** HTMX Out-of-Band (OOB) swap. `POST /drivers/upload` response is extended so that when called from the printer form (detected via `HX-Target` header or a posted sentinel field), it returns BOTH the existing `#driver-list` fragment AND an OOB-swap fragment replacing the printer form's driver `<select>`. Keeps the existing `/drivers` page behavior untouched.
- **Auto-select behavior:** Newly uploaded driver becomes the `selected` option in the refreshed `<select>`. Saves a click in the common case ("I'm uploading this driver *for this printer*"). No confirmation prompt; keep it frictionless.
- **Existing selection preservation:** Not applicable — if the technician was mid-form and had already picked a different driver, auto-select overrides it. This is the user-requested behavior.

### UX-01: 500 bug fix
- **Reported:** 2026-04-13 during Phase 8 kickoff. Repro: upload a driver from the `/drivers` page, server returns HTTP 500.
- **Blocker status:** Confirmed blocker for UX-01 regardless of which page triggers upload — any 500 on the upload path fails Phase 9 acceptance. Non-optional.
- **TDD flow (mandatory):** (1) Reproduce against the running app, capture the traceback. (2) Write a failing `pytest` case in `tests/test_driver_upload.py` matching the repro. (3) Confirm red. (4) Fix the root cause in `imptune/api/drivers.py` (or deeper — INF parser, DriverStore, Peewee layer). (5) Confirm green. (6) Commit the failing test and the fix atomically per GSD conventions.
- **Error-handling scope:** Fix the specific root cause of THIS 500. Do NOT rewrite the handler to swallow all exceptions into 400 responses — that would mask future bugs. Existing `_error_response()` helper returns 400 for validated failures; unhandled exceptions should remain loud but the reported repro must not be one of them.

### UX-02: PRNT-03 live verification
- **Mechanism:** **Playwright headless test**, committed to `tests/`. Not a manual screenshot.
- **Why:** Permanent regression guard costs one-time setup, then self-maintains. A screenshot decays the moment the template changes; a Playwright test fails loudly in CI.
- **Scope of the test:** Load `/printers/new` (or equivalent printer-form route), type an IP into `input[name="ip_address"]`, assert `input[name="port_name"]` now contains `IP_<dotted_underscore>` matching the Alpine.js handler in `printer_form.html:12-17`.
- **Evidence for VALIDATION.md:** The Playwright test file path + pytest command. The recorded-green test run at the commit that closes UX-02 is the evidence. No screenshot needed in the validation record — the test IS the record.
- **Dev dependency addition:** Playwright is a new dev dep. Add to `requirements-dev.txt`. No Playwright in production image (headless browsers violate the "single container, minimal deps" constraint). Test-only.

### UX-03: Per-script download links
- **Route shape:** **Three distinct GET routes**, one per script kind.
  - `GET /printers/{id}/scripts/install.ps1`
  - `GET /printers/{id}/scripts/uninstall.ps1`
  - `GET /printers/{id}/scripts/detect.ps1`
- **Why 3 routes over `?kind=` param:** Discoverable URLs (a technician can share `/printers/42/scripts/install.ps1` directly), trivial to bookmark, maps naturally to `Content-Disposition: attachment; filename=install.ps1`. Route sprawl is minimal (3 lines in the router).
- **Content generation:** Reuse the existing script generators (`imptune/services/` — the ones that feed package export). Plain-string generator contract, no new ORM access pattern needed.
- **Response headers:** `Content-Type: text/plain; charset=utf-8`, `Content-Disposition: attachment; filename="<kind>.ps1"`. Download, not inline-view (avoids browsers rendering `.ps1` as text and copy-paste losing CRLFs).
- **Template placement:** In [imptune/templates/printer_detail.html:48-51](imptune/templates/printer_detail.html#L48-L51), add a new `<h2>Scripts</h2>` block above or below the existing Export section with 3 `<a role="button">` links. Existing package export buttons stay untouched (UX-03 says "in addition to the existing package export buttons").

### Claude's Discretion
- Exact Alpine.js / HTMX OOB wiring syntax for the driver refresh.
- Whether to inline the upload control inside the printer form or stack it above — visual judgment during implementation.
- Playwright config file location and browser choice (chromium is the default, fine).
- Exact wording of the 3 download link labels ("Install script", "Install (.ps1)", etc.).
- Whether the 3 new script routes live in `imptune/api/printers.py` or a new `imptune/api/scripts.py` — router organization call.
- Test fixture format for the 500 repro (real driver ZIP vs. synthetic ZIP) — whichever reproduces fastest.

</decisions>

<code_context>
## Existing Code Insights

### Reusable Assets
- **[imptune/api/drivers.py:35-115](imptune/api/drivers.py#L35-L115)** — `upload_driver` handler. Already returns HTMX partials and uses `_error_response()` for validated 400s. Extend to emit OOB fragment for printer-form callers; find and fix the uncaught exception causing the 500.
- **[imptune/templates/partials/driver_list.html](imptune/templates/partials/driver_list.html)** — existing partial returned by upload. Inspect to understand current structure before layering OOB output.
- **[imptune/templates/partials/printer_form.html:29-39](imptune/templates/partials/printer_form.html#L29-L39)** — driver `<select>`. Target for OOB swap. The whole label block (or just the `<select>`) becomes the OOB-swap root.
- **[imptune/templates/partials/printer_form.html:1](imptune/templates/partials/printer_form.html#L1)** — existing Alpine.js `x-data` block. Add upload control inside the same form scope to share Alpine state if needed.
- **[imptune/templates/printer_detail.html:48-51](imptune/templates/printer_detail.html#L48-L51)** — Export section where script download links will be added.
- **Script generators under `imptune/services/`** — plain-string contract per PROJECT.md Key Decisions; safe to call from new routes without DB coupling.
- **`tests/test_driver_upload.py`** — existing test file, extend with the 500 regression case.
- **`imptune/api/printers.py`** — existing printer router, candidate home for the 3 new script download routes.

### Established Patterns
- **HTMX partial responses with HTTP 4xx for validated failures** (`_error_response()`) — reuse pattern for any new validation in the inline-upload path.
- **Jinja2 templates served via `fastapi.templating.Jinja2Templates(directory=templates_dir)`** — add new blocks, no router-level template refactor needed.
- **Content-addressed storage (SHA256)** for drivers — no change; the 500 bugfix should not alter this.
- **Alpine.js `x-data` inline state** in printer_form.html — pattern for new inline-upload local state (e.g., `uploading: false`).
- **Existing pytest + httpx test pattern** per PROJECT.md — 500 repro test follows same structure.

### Integration Points
- **OOB swap contract:** The HTMX OOB fragment must use `<select name="driver_id" hx-swap-oob="true" id="printer-form-driver-select">` (or equivalent); requires adding a stable `id` to the `<select>` in printer_form.html if it doesn't have one.
- **Playwright integration:** New `tests/e2e/` (or similar) subdir holding Playwright specs. Pytest invokes them via `pytest-playwright` plugin. App must be reachable on a test port during the run — fixture starts FastAPI via uvicorn in a subprocess or thread.
- **Script route dependency:** New download routes need to load `Printer` by id and call the same generator functions package export uses. No new business logic, pure re-use.
- **No source files outside `imptune/api/`, `imptune/templates/`, and `tests/` should need editing** for this phase (plus `requirements-dev.txt` for Playwright).

</code_context>

<specifics>
## Specific Ideas

- User re-framed UX-01 mid-discussion: adding an inline upload to the printer form was initially flagged as scope creep, but re-reading REQUIREMENTS.md UX-01 confirmed the requirement literally says "uploaded *on the printer form*" — so the inline upload IS the ask, not an add-on.
- User explicitly chose auto-select-new-driver behavior after upload ("Dropdown shows + auto-selects new") — the common case is "I uploaded this driver *for this printer*", so save the click.
- User treats the 500 as non-negotiable ("Yes — must be fixed") regardless of whether it's edge-case or happy-path. No conditional shipping.
- TDD is required for the 500 fix — failing test first, red-green-commit, not fix-then-test.
- Playwright chosen over manual screenshot because a committed test survives template edits; a screenshot doesn't.
- Three distinct GET routes preferred over `?kind=` for discoverability and clean `Content-Disposition` filenames.

</specifics>

<deferred>
## Deferred Ideas

- **Hardening `/drivers/upload` against unknown future exceptions** (global try/except, structured error pages) — out of scope. Fix the reported 500 only; broader resilience is v1.2 territory.
- **A "Download all scripts as ZIP" button** alongside the 3 individual links — tempting but not asked for by UX-03. Log for v1.2 if technicians request it after rollout.
- **Playwright tests for anything beyond UX-02** (e.g., full printer CRUD flow, driver upload E2E) — Phase 9 adds Playwright only for UX-02; expanding coverage is a Phase 11 feedback-driven decision.
- **Refactoring `upload_driver` into a service layer** to separate validation from HTTP concerns — not needed for this fix; avoid scope creep.
- **Fixing `/drivers/upload` to return JSON for programmatic clients** — v1.0 is HTMX-only; no programmatic consumers exist.

</deferred>

---

*Phase: 09-ux-tech-debt-closure*
*Context gathered: 2026-04-13*
