diff --git a/.planning/phases/03-printer-configuration/03-RESEARCH.md b/.planning/phases/03-printer-configuration/03-RESEARCH.md new file mode 100644 index 0000000..e5e29a8 --- /dev/null +++ b/.planning/phases/03-printer-configuration/03-RESEARCH.md @@ -0,0 +1,416 @@ +# Phase 3: Printer Configuration - Research + +**Researched:** 2026-04-10 +**Domain:** FastAPI + Peewee ORM + HTMX + Alpine.js — form CRUD, client grouping, saved config retrieval +**Confidence:** HIGH + +## Summary + +Phase 3 builds on a fully functional Phase 2 stack: FastAPI 0.115, Peewee 3.17, Jinja2 3.1, HTMX, Alpine.js, Pico CSS. The Printer and Client ORM models are already created (from the Phase 1 schema spike) with every field the requirements specify: `name`, `ip_address`, `port_name`, `client` (FK), `driver` (FK), `duplex_mode`, `color_mode`, `paper_size`, `collate`. No schema changes are needed in Phase 3 — it is purely route + template + service work. + +The key interaction patterns are already proven in Phase 2: HTMX `hx-post` / `hx-get` with `outerHTML` swaps for partial re-renders, Jinja2 partials for list fragments, Peewee sync queries in sync FastAPI route handlers (`def`, not `async def`), and the `client` fixture + `monkeypatch` pattern for integration tests. + +The three planned sub-plans map cleanly to separable concerns: (1) printer CRUD with form validation, (2) Client CRUD and grouped display, (3) regeneration flow that re-uses the saved `driver` FK. No new libraries are required. + +**Primary recommendation:** Re-use every established Phase 2 pattern exactly — HTMX partial swaps, Peewee `get_or_create`/`save()`, `json.dumps` for multi-value fields, sync route handlers, and the conftest `client` fixture. + + +## Phase Requirements + +| ID | Description | Research Support | +|----|-------------|-----------------| +| PRNT-01 | User can set printer display name | `Printer.name = CharField()` already in models.py. Route validates non-empty. | +| PRNT-02 | User can set printer IP address or hostname | `Printer.ip_address = CharField()` already in models.py. Server-side regex validates format. | +| PRNT-03 | System auto-suggests port name from IP (user can override) | Alpine.js `x-model` / `@input` on IP field drives port field in-browser; field remains editable. | +| PRNT-04 | User can set duplex mode (one-sided, long-edge, short-edge) | `Printer.duplex_mode = CharField(default="OneSided")` already in models.py. `` with fixed options. | +| PRNT-07 | User can set collate on/off | `Printer.collate = BooleanField(default=True)` already in models.py. Checkbox. | +| PRNT-08 | User can assign printer to a client/tenant label | `Printer.client = ForeignKeyField(Client, null=True)` already in models.py. ` + + +``` +Note: The exact derivation logic should be `IP_` + IP with dots replaced by underscores — this matches the Windows standard `pnputil` port name format used in Phase 4 scripts. + +### Pattern 5: Peewee FK Population for Dropdown (new in Phase 3) +**What:** Pass a list of all Client records to the printer form template. Render as `` on printer form shows no options because drivers list was not passed to template context. +**Why it happens:** GET /printers/new handler omits `drivers` from context. +**How to avoid:** Always pass `drivers` list to both create and edit form contexts. Build `driver_data` the same way as in `drivers.py` (parse `driver_desc` JSON into a list per driver). +**Warning signs:** Driver dropdown empty on new/edit printer form. + +### Pitfall 5: PRNT-10 Scope Creep into Phase 4 +**What goes wrong:** Implementing actual script/package regeneration (PowerShell generation) in Phase 3. +**Why it happens:** PRNT-10 says "regenerate its package" — but Phase 4 is the script generation phase. +**How to avoid:** Phase 3's scope for PRNT-10 is: (a) display saved printer config with all fields pre-populated, (b) show associated driver info, (c) provide a "Regenerate" button that will call the Phase 4 endpoint. The button can be disabled/placeholder in Phase 3. The plan 03-03 "Saved config retrieval and regeneration flow" is about navigation and form pre-population, not script generation. +**Warning signs:** Trying to write PowerShell templates in Phase 3. + +### Pitfall 6: TestClient Lifespan Not Triggered +**What goes wrong:** Tests fail with "table does not exist" errors. +**Why it happens:** TestClient must be used as a context manager to trigger the FastAPI `lifespan` (which calls `init_db()`). Using `TestClient(app)` without `with` does not trigger lifespan. +**How to avoid:** Always use `with TestClient(app) as c:` — established and enforced in conftest.py's `client` fixture. All new tests should use the `client` fixture, not create their own TestClient. +**Warning signs:** `OperationalError: no such table: printer` in test output. + +## Code Examples + +### Create Printer (Peewee) +```python +# Pattern: use keyword args matching Printer model field names +# Source: Peewee 3.17 docs + established Phase 2 Driver.get_or_create pattern +from imptune.db.models import Printer, Client, Driver + +printer = Printer.create( + name=name, + ip_address=ip, + port_name=port, + duplex_mode=duplex, # "OneSided" | "LongEdge" | "ShortEdge" + color_mode=color_mode, # bool + paper_size=paper_size, # "A4" | "Letter" | "Legal" + collate=collate, # bool + client=client_obj_or_none, + driver=driver_obj_or_none, +) +``` + +### Update Printer (Peewee) +```python +# Source: Peewee 3.17 — Model.save() with only_fields for efficiency +printer = Printer.get_by_id(printer_id) +printer.name = new_name +printer.ip_address = new_ip +# ... set other fields ... +printer.updated_at = datetime.utcnow() +printer.save() +``` + +### Query Printers Grouped by Client (Peewee) +```python +# Source: Peewee 3.17 JOIN pattern — avoids N+1 +from collections import defaultdict +from peewee import JOIN + +printers = list( + Printer.select(Printer, Client) + .join(Client, JOIN.LEFT_OUTER) + .order_by(Client.name.nulls_last(), Printer.name) +) +grouped: dict[str, list] = defaultdict(list) +for p in printers: + label = p.client.name if p.client_id else "Unassigned" + grouped[label].append(p) +``` + +### FastAPI Form Parsing (python-multipart) +```python +# Source: FastAPI docs — Form parameters +from fastapi import Form + +@router.post("/printers", response_class=HTMLResponse) +def create_printer( + request: Request, + name: str = Form(...), + ip_address: str = Form(...), + port_name: str = Form(...), + duplex_mode: str = Form("OneSided"), + color_mode: bool = Form(True), + paper_size: str = Form("A4"), + collate: bool = Form(True), + client_id: int | None = Form(None), + driver_id: int | None = Form(None), +) -> HTMLResponse: + ... +``` + +### Alpine.js Port Auto-Derivation (PRNT-03) +```html + +
+ + + + + +
+``` + +### HTMX Delete with Confirmation (optional, for plan 03-01) +```html + + +``` + +### Retrieve Driver Path for Regeneration (PRNT-10) +```python +# Source: imptune/storage/driver_store.py (Phase 2) +from imptune.storage.driver_store import DriverStore +import imptune.config as cfg + +printer = Printer.get_by_id(printer_id) +if printer.driver_id: + store = DriverStore(cfg.DRIVERS_DIR) + driver_zip_path = store.get_path(printer.driver.sha256) + # driver_zip_path is the Path to the stored ZIP, available for Phase 4 script generation +``` + +## State of the Art + +| Old Approach | Current Approach | When Changed | Impact | +|--------------|------------------|--------------|--------| +| `@app.on_event("startup")` | `asynccontextmanager lifespan` | FastAPI 0.93+ / Starlette 0.40+ | Use lifespan pattern, never on_event | +| `TemplateResponse("name", {"request": req})` positional dict | `TemplateResponse(request=req, name="name", context={})` kwargs | Starlette 0.40+ | Must use kwargs form | +| `async def` with Peewee | `def` (sync) handlers | Phase 2 decision | Peewee is sync; async would block event loop | + +**Deprecated/outdated:** +- `@app.on_event`: Replaced by lifespan context manager (established in Plan 01-01). +- Schema changes in Phase 3: Full schema was created in Phase 1. No `CREATE TABLE` calls needed. + +## Open Questions + +1. **How many paper sizes beyond A4/Letter/Legal?** + - What we know: PRNT-06 says "A4, Letter, Legal at minimum" + - What's unclear: Should the `