--- phase: 03-printer-configuration plan: "01" subsystem: api tags: [fastapi, peewee, htmx, alpinejs, jinja2, sqlite, forms] requires: - phase: 02-driver-management provides: Driver ORM model, HTMX partial rendering pattern, error response pattern, test fixtures with tmp_data_dir provides: - POST /printers endpoint with form parsing, validation, checkbox-to-bool conversion, FK resolution - DELETE /printers/{id} endpoint - POST /clients endpoint with duplicate-name handling - GET /printers page grouped by client with LEFT OUTER JOIN (no N+1) - GET /clients page with creation form - Alpine.js port auto-derivation (IP -> port name, preserves manual edits) - HTMX-powered form submission with outerHTML swap on #printer-list and #client-list - Integration test suite covering PRNT-01 through PRNT-09 affects: - 03-02 (next plan in printer configuration phase) - Any phase using Printer or Client ORM models - Test isolation pattern now fixed in conftest.py (affects all future test suites) tech-stack: added: [] patterns: - "Printer/Client CRUD via FastAPI Form() parameters with sync def handlers" - "Checkbox boolean convention: 'on'=True, absent/empty=False" - "Grouped list via defaultdict + LEFT_OUTER JOIN — no N+1 queries" - "HTMX partial swap: success returns partial, failure returns error div with same id" - "Alpine.js x-data for reactive port derivation with portEdited guard" - "Peewee test isolation: conftest.py fixture teardown closes test-thread DB connection" key-files: created: - imptune/api/printers.py - imptune/api/clients.py - imptune/templates/printers.html - imptune/templates/clients.html - imptune/templates/partials/printer_form.html - imptune/templates/partials/printer_list.html - imptune/templates/partials/client_list.html - tests/test_printer_crud.py modified: - imptune/api/pages.py - imptune/main.py - imptune/db/database.py - tests/conftest.py key-decisions: - "Use list(Printer.select().where(...)) in tests instead of Printer.get() — Peewee's get() uses paginate+cursor caching that fails across DB re-inits in the same process" - "Close test-thread DB connection in conftest.py fixture teardown — thread-local Peewee connections persist across tests and read from stale DB" - "Close db in lifespan shutdown — enables clean re-init when TestClient is restarted in the same process" - "Alpine.js portEdited guard prevents port overwrite after manual edit (PRNT-03 requirement)" patterns-established: - "HTMX error fragment:

{msg}

with matching id for outerHTML swap" - "Grouped list query: LEFT_OUTER JOIN with defaultdict grouping, 'Unassigned' fallback for null FK" - "Form checkbox handling: Form('') default, 'on' == True conversion" requirements-completed: - PRNT-01 - PRNT-02 - PRNT-03 - PRNT-04 - PRNT-05 - PRNT-06 - PRNT-07 - PRNT-08 - PRNT-09 duration: 7min completed: "2026-04-10" --- # Phase 03 Plan 01: Printer and Client CRUD Summary **FastAPI printer CRUD with Alpine.js IP-to-port derivation, HTMX form submission, LEFT JOIN grouped list by client, and 10-test integration suite covering PRNT-01 through PRNT-09** ## Performance - **Duration:** ~7 min - **Started:** 2026-04-10T10:49:28Z - **Completed:** 2026-04-10T10:56:22Z - **Tasks:** 2 of 3 (Task 3 is checkpoint:human-verify — pending) - **Files modified:** 12 ## Accomplishments - Printer CRUD: POST /printers (all 9 fields, checkbox bool conversion, optional FK), DELETE /printers/{id} - Client CRUD: POST /clients (duplicate handling), GET /clients page - Alpine.js port auto-derivation: fills `IP_x_x_x_x` from IP, preserves manual edits via `portEdited` guard - Grouped list: LEFT_OUTER JOIN query, defaultdict grouping with "Unassigned" fallback, no N+1 - 10 integration tests pass (GREEN), full 58-test suite passes ## Task Commits Each task was committed atomically: 1. **Task 1: Failing integration tests (RED)** - `9bc26e3` (test) 2. **Task 2: Full CRUD implementation + GREEN tests** - `356c2ee` (feat) 3. **Task 3: Browser verification** - pending (checkpoint:human-verify) ## Files Created/Modified - `imptune/api/printers.py` - POST /printers, DELETE /printers/{id}, _render_printer_list helper - `imptune/api/clients.py` - POST /clients, _render_client_list helper - `imptune/api/pages.py` - Added GET /printers and GET /clients page routes - `imptune/main.py` - Registered printers + clients routers; db.close() in lifespan shutdown - `imptune/db/database.py` - Close existing connection before re-init in init_db() - `imptune/templates/printers.html` - Printer page (form + list sections) - `imptune/templates/clients.html` - Clients page (add form + list) - `imptune/templates/partials/printer_form.html` - All 9 fields, Alpine.js x-data reactivity - `imptune/templates/partials/printer_list.html` - Grouped by client with h3 headers, delete buttons - `imptune/templates/partials/client_list.html` - Client table partial - `tests/conftest.py` - Added db.close() teardown in tmp_data_dir fixture - `tests/test_printer_crud.py` - 10 integration tests for all PRNT requirements ## Decisions Made - Use `list(Model.select().where(...))` in tests instead of `Model.get()` — Peewee's `get()` uses `paginate(1,1)` with cursor caching that hits the wrong database when the deferred db is re-initialized between tests in the same process. - Close db connection in conftest.py fixture teardown — thread-local Peewee connections persist across tests and read from stale DB path even after `db.init()` updates the path. - Alpine.js `portEdited` boolean guard preserves manually edited port names when user changes IP (PRNT-03). ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 1 - Bug] Peewee thread-local DB connection leaks across test boundaries** - **Found during:** Task 2 (GREEN phase verification) - **Issue:** After TestClient exits and a new test begins with a fresh tmp DB, the test thread's Peewee connection still pointed at the previous test's DB file. `Printer.get()` would query the wrong database (empty or stale data). - **Fix:** 1. Added `db.close()` in lifespan shutdown (main.py) so each TestClient teardown closes the ASGI-thread connection. 2. Added `if not db.is_closed(): db.close()` before `db.init()` in `init_db()` (database.py) so re-init always starts fresh. 3. Added db connection teardown in `conftest.py` `tmp_data_dir` fixture to close the test-thread's connection after each test. 4. Updated test DB queries from `Model.get()` to `list(Model.select().where(...))` to avoid Peewee paginate cursor caching issue. - **Files modified:** imptune/main.py, imptune/db/database.py, tests/conftest.py, tests/test_printer_crud.py - **Verification:** All 58 tests pass including cross-test ordering - **Committed in:** `356c2ee` (Task 2 commit) --- **Total deviations:** 1 auto-fixed (Rule 1 - Bug) **Impact on plan:** Fix was necessary for test correctness. The underlying isolation pattern now benefits all future test suites in this project. No scope creep. ## Issues Encountered - Peewee `Model.get()` uses `paginate(1,1)` which clears `_cursor_wrapper` cache and re-executes — but after db re-init, the cursor wrapper was returning empty even though `count()` and direct SQL showed the record existed. Root cause: thread-local SQLite connection not updated by `db.init()`. Resolved by proper connection lifecycle management. ## User Setup Required None — no external service configuration required. ## Next Phase Readiness - /printers and /clients pages functional with full CRUD - Alpine.js port auto-derivation implemented (PRNT-03) — browser verification still pending (Task 3 checkpoint) - Printer form supports driver dropdown from uploaded drivers - Grouped printer list ready for 03-02 (script generation) - Test isolation pattern fixed — future test suites can safely use `list(Model.select().where(...))` for DB assertions --- *Phase: 03-printer-configuration* *Completed: 2026-04-10*