---
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: <div id='printer-list' class='error'><p>{msg}</p></div> 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*
