--- phase: 09-ux-tech-debt-closure plan: 01 type: execute wave: 1 depends_on: [] files_modified: - tests/test_driver_upload.py - imptune/api/drivers.py - imptune/templates/partials/driver_list.html - imptune/templates/partials/driver_upload_with_oob.html - imptune/templates/partials/printer_form.html autonomous: true requirements: [UX-01] must_haves: truths: - "POST /drivers/upload never returns HTTP 500 for a valid driver ZIP" - "Uploading a driver from the printer form refreshes the driver " - "Uploading from the standalone /drivers page still returns only the #driver-list fragment (no OOB noise)" artifacts: - path: "tests/test_driver_upload.py" provides: "Regression test for the 500 + OOB contract tests" contains: "test_upload_500_regression" - path: "imptune/api/drivers.py" provides: "Fixed upload_driver handler with caller-aware OOB branch" contains: "caller" - path: "imptune/templates/partials/driver_upload_with_oob.html" provides: "Template emitting primary driver_list fragment + OOB has stable id + separate inline upload form" contains: "printer-form-driver-select" key_links: - from: "imptune/templates/partials/printer_form.html" to: "POST /drivers/upload" via: "separate
with hidden caller=printer_form field" pattern: 'name="caller"\s+value="printer_form"' - from: "imptune/api/drivers.py (upload_driver)" to: "partials/driver_upload_with_oob.html" via: "TemplateResponse when caller == 'printer_form'" pattern: 'driver_upload_with_oob\.html' - from: "partials/driver_upload_with_oob.html" to: "printer_form.html #printer-form-driver-select" via: 'hx-swap-oob="true" on ` via HTMX Out-of-Band swap and auto-selects the newly uploaded driver. Purpose: Closes UX-01 — technicians uploading a driver while creating/editing a printer see it appear in the dropdown and get it auto-selected, no manual reload. Output: Green regression + OOB tests, a working inline upload control, and an OOB-capable upload handler. @C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md @C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/09-ux-tech-debt-closure/09-CONTEXT.md @.planning/phases/09-ux-tech-debt-closure/09-RESEARCH.md @.planning/phases/09-ux-tech-debt-closure/09-VALIDATION.md @imptune/api/drivers.py @imptune/templates/partials/printer_form.html @imptune/templates/partials/driver_list.html @tests/test_driver_upload.py @tests/conftest.py From imptune/api/drivers.py: ```python router = APIRouter(prefix="/drivers") # mounted at /drivers in main.py MAX_UPLOAD_BYTES = 100 * 1024 * 1024 def _error_response(message: str, status_code: int = 400) -> HTMLResponse: ... @router.post("/upload", response_class=HTMLResponse) def upload_driver(request: Request, file: UploadFile) -> HTMLResponse: ... # Validates zip, parses INF via parse_inf, persists via DriverStore(_cfg.DRIVERS_DIR), # upserts Driver via Driver.get_or_create(sha256=..., defaults={...}), # returns TemplateResponse("partials/driver_list.html", {driver_data, parsed}). # There is NO try/except around parse_inf / DriverStore.save / Driver.get_or_create — # any of these can bubble into a FastAPI 500. ``` From imptune/db/models.Driver: fields include id, sha256, original_filename, driver_desc (JSON list), inf_filename, architecture, has_cat_file, uploaded_at. From imptune/services/inf_parser: `parse_inf(inf_text, inf_filename, zip_names) -> ParsedInf` with attributes `driver_names: list[str]`, `inf_filename: str`, `architecture: str | None`, `has_cat_file: bool`. Current printer_form.html driver select (lines 28-39): ```html ``` HTMX OOB contract: response body contains the primary swap fragment (targets `#driver-list`) PLUS one or more sibling elements with `hx-swap-oob="true"` whose `id` matches an element in the current page DOM. Out-of-band elements MUST be top-level in the response body (not nested inside the primary fragment). Task 1: Wave 0 — write failing driver-upload regression + OOB contract tests tests/test_driver_upload.py - test_upload_500_regression: POST /drivers/upload with a valid driver ZIP fixture MUST return status_code != 500. Use the existing test fixture pattern from tests/conftest.py (tmp_data_dir) and the same synthetic driver ZIP builder already used in this test module if present; otherwise create `_make_driver_zip()` helper that writes a minimal valid INF + `.cat` file into a ZIP. Assertion: `assert resp.status_code == 200, resp.text`. - test_upload_returns_oob_when_called_from_form: POST /drivers/upload with multipart fields `{file: valid_zip, caller: "printer_form"}` MUST return 200 AND the response body MUST contain `hx-swap-oob="true"` AND `id="printer-form-driver-select"`. - test_upload_oob_autoselects_new_driver: Same call as above — response body MUST contain the newly created driver's ` {% for item in driver_data %} {% endfor %} ``` Step 6 — Inspect `partials/driver_list.html`. Confirm its root element has `id="driver-list"` (hx-target from the inline upload form will swap it). If the partial currently wraps itself differently, leave as-is; the OOB template simply includes it. Do NOT refactor driver_list.html unless necessary. Step 7 — Run the failing tests. Iterate until GREEN. Then run full non-e2e suite. Commit: `fix(09-01): resolve driver upload 500 and add HTMX OOB refresh path` pytest tests/test_driver_upload.py -x -v && pytest tests/ -x -q --ignore=tests/e2e All Task 1 tests green. Full non-e2e suite green. `upload_driver` accepts a `caller` form field and returns OOB-enabled response only when caller=="printer_form". New partial file exists. Task 3: Wire inline driver upload form into printer_form.html imptune/templates/partials/printer_form.html, tests/test_printer_form.py - printer_form.html renders a separate inline `` OUTSIDE the main printer `` but inside the Alpine x-data wrapper div. - The driver `` (line 30). - AFTER the closing `` of the printer form (line 85) but BEFORE the closing `` of the x-data wrapper (line 86), add a separate inline upload form: ```html
``` - CRITICAL: Do NOT nest this form inside the printer `
` — HTML forbids nested forms and browsers silently drop the inner one. Place it as a sibling, still within the outer `
` so visual grouping and Alpine scope are preserved. - Also ensure `partials/driver_list.html` (or wherever the `#driver-list` anchor lives) is reachable from the page that renders printer_form.html. If the printer form page doesn't currently include a `
` anchor, add a hidden one next to the upload form: `` so the primary HTMX swap target exists even on the printer form page. Alternatively render the full driver_list partial for visibility (preferred if space allows — shows technician the uploaded driver landed). Step 2 — Extend `tests/test_printer_form.py` with `test_printer_form_has_inline_driver_upload`: - GET the printer form route (`/printers/new` or the HTMX partial route used by the existing tests — match the existing pattern in this test file). - Assert response body contains `id="printer-form-driver-select"`. - Assert response body contains `name="caller"` with value `printer_form`. - Assert response body contains `hx-post="/drivers/upload"`. - Assert nested form check: the substring between `` does NOT contain `hx-post="/drivers/upload"` (naive check is fine: split on `` and verify the printer form chunk is clean). Step 3 — Run the test. GREEN. Commit: `feat(09-01): add inline driver upload to printer form with OOB refresh` pytest tests/test_printer_form.py -x -v && pytest tests/ -x -q --ignore=tests/e2e Printer form template contains stable-id driver select + sibling inline upload form with caller sentinel. test_printer_form.py guards the wiring. Full non-e2e suite green. - `pytest tests/ -x -q --ignore=tests/e2e` passes - Manual eye check (captured in 09-VALIDATION.md manual section): start app, open printer form, upload a real driver ZIP, confirm driver list refreshes AND the new driver becomes the selected option in the dropdown without a page reload - UX-01 observable truth #1 achieved: technician uploading a driver on the printer form sees new DriverDesc in the dropdown and auto-selected, no reload - No HTTP 500 from `POST /drivers/upload` for the captured repro case - All 4 new tests (500 regression, OOB contract, auto-select, no-oob-on-standalone) green After completion, create `.planning/phases/09-ux-tech-debt-closure/09-01-SUMMARY.md` documenting: actual root cause of the 500, files changed, test results, and link to commits.