--- phase: 11-ui-enhancements plan: "02" type: execute wave: 2 depends_on: - "11-01" files_modified: - imptune/api/printers.py - imptune/templates/partials/printer_list.html - imptune/templates/partials/printer_edit_modal.html - tests/e2e/test_printer_edit.py autonomous: true requirements: - UIE-01 must_haves: truths: - "Every printer row in the list has an Edit button next to the Delete button" - "Clicking Edit opens a pre-filled native modal for that printer" - "Submitting the edit form sends HTMX PATCH to /printers/{id} and refreshes the printer list in-place" - "PATCH /printers/{id} returns 200 with the updated printer list partial" - "PATCH /printers/9999 returns 404" artifacts: - path: "imptune/templates/partials/printer_edit_modal.html" provides: "Edit modal template with pre-filled fields and PATCH form" min_lines: 40 - path: "imptune/api/printers.py" provides: "PATCH /printers/{id} route handler" contains: "@router.patch" - path: "tests/e2e/test_printer_edit.py" provides: "E2E test: modal open, pre-fill verification, submit, list update" min_lines: 20 key_links: - from: "imptune/templates/partials/printer_list.html" to: "printer_edit_modal.html" via: "{% include %} inside {% for p in printers %} loop" pattern: "include.*printer_edit_modal" - from: "printer_edit_modal.html" to: "PATCH /printers/{id}" via: "hx-patch attribute on the edit form" pattern: "hx-patch" - from: "imptune/api/printers.py update_printer" to: "_render_printer_list" via: "return _render_printer_list(request) on success" pattern: "_render_printer_list" --- Add the printer edit modal — Edit button per row, native dialog, HTMX PATCH handler, in-place list refresh. Purpose: UIE-01 — Users need to fix printer details (wrong IP, changed driver) without deleting and recreating. A lightweight in-place edit flow covers the daily need. Output: PATCH /printers/{id} route, printer_edit_modal.html partial, updated printer_list.html with Edit button, E2E test. @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/phases/11-ui-enhancements/11-CONTEXT.md @.planning/phases/11-ui-enhancements/11-RESEARCH.md @.planning/phases/11-ui-enhancements/11-01-SUMMARY.md From imptune/api/printers.py (existing helpers — reuse unchanged): ```python _VALID_DUPLEX = {"OneSided", "LongEdge", "ShortEdge"} _VALID_PAPER = {"A4", "Letter", "Legal"} def _error_response(message: str, status_code: int = 400) -> HTMLResponse: ... def _render_printer_list(request: Request) -> HTMLResponse: ... # existing routes: POST "", DELETE "/{printer_id}" # ADD: PATCH "/{printer_id}" ``` From imptune/db/models.py — Printer fields for pre-fill: ```python class Printer(BaseModel): name = CharField() ip_address = CharField() port_name = CharField() client = ForeignKeyField(Client, null=True) driver = ForeignKeyField(Driver, null=True) duplex_mode = CharField(default="OneSided") # "OneSided" | "LongEdge" | "ShortEdge" color_mode = BooleanField(default=True) paper_size = CharField(default="A4") # "A4" | "Letter" | "Legal" collate = BooleanField(default=True) updated_at = DateTimeField(default=_utcnow) # MUST be set explicitly on update ``` From imptune/templates/partials/printer_list.html — current Actions cell (to be updated): ```html ``` Pico CSS dialog pattern (from RESEARCH.md): ```html

Edit Printer

``` HTMX PATCH + close after success (from RESEARCH.md): ```html
``` Alpine.js portEdited in edit mode — must be TRUE (not false) so editing IP does not overwrite a manually set port: ```html
``` From tests/e2e/conftest.py — live_server fixture already available (session-scoped). Task 1: PATCH /printers/{id} route handler imptune/api/printers.py - PATCH /printers/{id} with valid fields returns 200 and HTML containing the updated printer name - DB record is updated: Printer.get_by_id(id).name == new_name - PATCH /printers/{id} with updated_at is set (not creation time) after update - PATCH /printers/9999 returns 404 - PATCH /printers/{id} with empty name returns 400 These tests already exist as RED scaffolds from Plan 01 (test_patch_printer, test_patch_printer_not_found) Add a PATCH route to imptune/api/printers.py immediately after the DELETE route. Import addition at top of file: ```python from imptune.db.models import Client, Driver, Printer ``` (Client and Driver may need to be added if not already imported — check existing imports first) Add this handler: ```python @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: """Update an existing printer configuration in-place.""" from imptune.db.models import Printer from datetime import UTC from datetime import datetime 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) name = name.strip() ip_address = ip_address.strip() port_name = port_name.strip() if not name: return _error_response("Printer name is required.") if not ip_address: return _error_response("IP address is required.") if not port_name: return _error_response("Port name is required.") if duplex_mode not in _VALID_DUPLEX: return _error_response(f"Invalid duplex mode: {duplex_mode}.") if paper_size not in _VALID_PAPER: return _error_response(f"Invalid paper size: {paper_size}.") printer.name = name printer.ip_address = ip_address printer.port_name = port_name printer.duplex_mode = duplex_mode printer.color_mode = color_mode == "on" printer.paper_size = paper_size printer.collate = collate == "on" printer.client = int(client_id) if client_id.strip() else None printer.driver = int(driver_id) if driver_id.strip() else None printer.updated_at = datetime.now(UTC).replace(tzinfo=None) printer.save() return _render_printer_list(request) ``` Do NOT import datetime at module level if it conflicts with existing imports — use local import inside the function as shown. cd C:/Users/SebastienQUEROL/Documents/projets/ImpTune && pytest tests/test_printer_crud.py -x -q -k "patch_printer" 2>&1 | tail -10 test_patch_printer and test_patch_printer_not_found both GREEN; existing DELETE tests still pass. Task 2: Edit button, edit modal partial, and E2E test imptune/templates/partials/printer_list.html, imptune/templates/partials/printer_edit_modal.html, tests/e2e/test_printer_edit.py **Step 1 — Create imptune/templates/partials/printer_edit_modal.html:** This partial is included once per printer row (inside the {% for p in printers %} loop in printer_list.html). It renders the edit dialog AND the Edit trigger button. Structure (follow the Pico CSS dialog pattern from RESEARCH.md): ```html

Edit Printer

``` Note: `clients` and `driver_data` context variables are already passed to printer_list.html via _render_printer_list — verify this. If _render_printer_list does NOT pass clients, update it to include `clients = list(Client.select().order_by(Client.name))` in the context. Check imptune/api/printers.py _render_printer_list to confirm. **Step 2 — Update imptune/templates/partials/printer_list.html:** In the Actions ``, include the edit modal partial: ```html {% include "partials/printer_edit_modal.html" %} ``` The {% include %} is INSIDE the {% for p in printers %} loop — it inherits the `p` variable directly. **Step 3 — Update _render_printer_list in api/printers.py if needed:** Check if `clients` is in the context passed to printer_list.html. The current _render_printer_list only passes `grouped`. Add clients to the context: ```python def _render_printer_list(request: Request) -> HTMLResponse: from imptune.db.models import Client, Driver import json # existing grouped query... clients = list(Client.select().order_by(Client.name)) 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="partials/printer_list.html", context={"grouped": grouped, "clients": clients, "driver_data": driver_data}, ) ``` **Step 4 — Create tests/e2e/test_printer_edit.py:** ```python """UIE-01: E2E test for printer edit modal — open, pre-fill, submit, list update.""" from __future__ import annotations import pytest def test_printer_edit_modal_open_and_prefill(page, live_server: str) -> None: """Edit button opens modal with printer's current name pre-filled.""" import httpx # Create a printer via API with httpx.Client(base_url=live_server, follow_redirects=True) as api: api.post("/printers", data={ "name": "EditTest Printer", "ip_address": "10.0.5.1", "port_name": "IP_10_0_5_1", }) page.goto(f"{live_server}/printers", wait_until="domcontentloaded") page.wait_for_selector("button:has-text('Edit')") page.click("button:has-text('Edit')") # Dialog should be open page.wait_for_selector("dialog[open]") # Name input should be pre-filled name_val = page.input_value("dialog[open] input[name='name']") assert name_val == "EditTest Printer" def test_printer_edit_submit_updates_list(page, live_server: str) -> None: """Submitting the edit form updates the printer name in the list (no page reload).""" import httpx with httpx.Client(base_url=live_server, follow_redirects=True) as api: api.post("/printers", data={ "name": "OriginalName", "ip_address": "10.0.5.2", "port_name": "IP_10_0_5_2", }) page.goto(f"{live_server}/printers", wait_until="domcontentloaded") page.wait_for_selector("button:has-text('Edit')") page.click("button:has-text('Edit')") page.wait_for_selector("dialog[open]") # Clear and update the name field page.fill("dialog[open] input[name='name']", "UpdatedName") page.click("dialog[open] button[type='submit']") # Modal should close and list should update page.wait_for_selector("#printer-list") assert "UpdatedName" in page.text_content("#printer-list") assert "OriginalName" not in page.text_content("#printer-list") ```
cd C:/Users/SebastienQUEROL/Documents/projets/ImpTune && pytest tests/test_printer_crud.py -x -q -k "patch_printer" && pytest tests/ -x -q --ignore=tests/e2e 2>&1 | tail -10 E2E separately: pytest tests/e2e/test_printer_edit.py -x -q (requires live server + playwright) - Edit button appears in every printer row - Clicking Edit opens a pre-filled dialog - Submitting saves changes and refreshes the list - test_patch_printer and test_patch_printer_not_found GREEN - test_printer_edit_modal_open_and_prefill and test_printer_edit_submit_updates_list pass (E2E) - Full integration suite (non-E2E) GREEN
Run full non-E2E suite: ``` cd C:/Users/SebastienQUEROL/Documents/projets/ImpTune && pytest tests/ -x -q --ignore=tests/e2e ``` Expected: all GREEN. Run E2E: ``` cd C:/Users/SebastienQUEROL/Documents/projets/ImpTune && pytest tests/e2e/ -q ``` Expected: test_printer_edit.py passes (2 tests GREEN). Spot-check: PATCH /printers/{id} with valid data returns 200, response HTML contains updated name. - Every printer row has an Edit button - Clicking Edit opens a native dialog pre-filled with that printer's current data - Submitting the edit form sends HTMX PATCH, closes the modal, and updates the list - PATCH /printers/{id} validated via test_patch_printer (GREEN) - PATCH /printers/9999 returns 404 (confirmed by test_patch_printer_not_found) - E2E tests pass: modal opens, name is pre-filled, submit updates list After completion, create `.planning/phases/11-ui-enhancements/11-02-SUMMARY.md`