417 lines
24 KiB
Markdown
417 lines
24 KiB
Markdown
# 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>
|
|
## 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. `<select>` with three fixed options. |
|
|
| PRNT-05 | User can set color vs. grayscale default | `Printer.color_mode = BooleanField(default=True)` already in models.py. Radio buttons or checkbox. |
|
|
| PRNT-06 | User can set paper size (A4, Letter, Legal at minimum) | `Printer.paper_size = CharField(default="A4")` already in models.py. `<select>` 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. `<select>` of existing clients or inline creation. |
|
|
| PRNT-09 | Printer configurations are persisted in SQLite across sessions | Peewee `Printer.create()` / `Printer.save()` to existing DB. init_db() already creates table. |
|
|
| PRNT-10 | User can regenerate a package from saved config without re-uploading drivers | `Printer.driver` FK stores the original `Driver` record. Regeneration reads `DriverStore(sha256)` path. (Actual script generation is Phase 4; Phase 3 only ensures the association is persisted and displayable.) |
|
|
</phase_requirements>
|
|
|
|
## Standard Stack
|
|
|
|
### Core
|
|
| Library | Version | Purpose | Why Standard |
|
|
|---------|---------|---------|--------------|
|
|
| FastAPI | 0.115.* | HTTP routing, request parsing, response | Already in use; same patterns as Phase 2 |
|
|
| Peewee | 3.17.* | ORM for SQLite CRUD | Already in use; Printer/Client models pre-created |
|
|
| Jinja2 | 3.1.* | Server-side HTML templating | Already in use; base.html + partials pattern established |
|
|
| HTMX | baked-in static | Async partial HTML swaps without JS | Already in use; proven with driver upload flow |
|
|
| Alpine.js | baked-in static | Reactive in-browser logic (auto-port derivation) | Already in use; needed for PRNT-03 live field derivation |
|
|
| Pico CSS | baked-in static | Semantic HTML styling | Already in use |
|
|
|
|
### Supporting
|
|
| Library | Version | Purpose | When to Use |
|
|
|---------|---------|---------|-------------|
|
|
| python-multipart | 0.0.9 | Form data parsing for FastAPI | Required for `Form(...)` parameters (already in requirements.txt) |
|
|
|
|
### Alternatives Considered
|
|
None — all decisions are locked by prior phases. Do not introduce new libraries.
|
|
|
|
**Installation:** No new packages required.
|
|
|
|
## Architecture Patterns
|
|
|
|
### Project Structure (additions only)
|
|
```
|
|
imptune/
|
|
├── api/
|
|
│ ├── drivers.py # existing
|
|
│ ├── pages.py # extend with /printers, /clients routes
|
|
│ └── printers.py # NEW — POST /printers, PUT /printers/{id}, DELETE /printers/{id}
|
|
│ └── clients.py # NEW — POST /clients (inline create for PRNT-08)
|
|
├── templates/
|
|
│ ├── printers.html # NEW — printer list page grouped by client
|
|
│ ├── partials/
|
|
│ │ ├── driver_list.html # existing
|
|
│ │ ├── printer_form.html # NEW — create/edit form fragment
|
|
│ │ ├── printer_list.html # NEW — grouped printer list fragment (HTMX target)
|
|
│ │ └── printer_row.html # NEW — single printer row (optional, for inline edit)
|
|
tests/
|
|
└── test_printer_crud.py # NEW — integration tests for printer CRUD
|
|
└── test_client_crud.py # NEW (or merged into test_printer_crud) — client creation tests
|
|
```
|
|
|
|
### Pattern 1: Sync Peewee Route Handler (established in Phase 2)
|
|
**What:** All Peewee queries run in `def` (sync) route handlers, never `async def`. FastAPI runs sync handlers in a threadpool automatically.
|
|
**When to use:** Every route that touches the DB.
|
|
**Example:**
|
|
```python
|
|
# Source: imptune/api/drivers.py (Phase 2 established pattern)
|
|
@router.post("/upload", response_class=HTMLResponse)
|
|
def upload_driver(request: Request, file: UploadFile) -> HTMLResponse:
|
|
# Peewee calls here — all sync
|
|
Driver.get_or_create(sha256=sha256, defaults={...})
|
|
```
|
|
|
|
### Pattern 2: HTMX Partial Swap (established in Phase 2)
|
|
**What:** Form submits to an API endpoint via `hx-post`; endpoint returns an HTML fragment that HTMX swaps into the target div using `outerHTML`.
|
|
**When to use:** Printer form submission (PRNT-09), client assignment, list refresh.
|
|
**Example:**
|
|
```html
|
|
<!-- Source: imptune/templates/drivers.html (Phase 2 established) -->
|
|
<form
|
|
hx-post="/printers"
|
|
hx-target="#printer-list"
|
|
hx-swap="outerHTML"
|
|
>
|
|
...
|
|
</form>
|
|
```
|
|
|
|
### Pattern 3: Error Fragment Response (established in Phase 2)
|
|
**What:** Validation errors return an `HTMLResponse` whose content is a div with `id="[target-id]"` so HTMX replaces the target area with the error message.
|
|
**When to use:** Any form validation failure in printer or client routes.
|
|
**Example:**
|
|
```python
|
|
# Source: imptune/api/drivers.py (Phase 2 established)
|
|
def _error_response(message: str, status_code: int = 400) -> HTMLResponse:
|
|
return HTMLResponse(
|
|
content=f"<div id='printer-list' class='error'><p>{message}</p></div>",
|
|
status_code=status_code,
|
|
)
|
|
```
|
|
|
|
### Pattern 4: Alpine.js Reactive Field (new in Phase 3, PRNT-03)
|
|
**What:** Alpine.js `x-data` component on the printer form watches the IP address field and derives a default port name. The port field remains user-editable (not disabled).
|
|
**When to use:** PRNT-03 auto-suggest port name from IP.
|
|
**Example:**
|
|
```html
|
|
<!-- Alpine.js already available in base.html as /static/alpine.min.js -->
|
|
<div x-data="{ ip: '', port: '' }" @input.debounce="port = port || ('IP_' + ip.replaceAll('.', '_'))">
|
|
<input type="text" name="ip_address" x-model="ip" placeholder="192.168.1.100" required>
|
|
<input type="text" name="port_name" x-model="port" placeholder="IP_192_168_1_100">
|
|
</div>
|
|
```
|
|
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 `<select name="client_id">` with an "Unassigned" option (value="").
|
|
**When to use:** PRNT-08 client assignment dropdown.
|
|
**Example:**
|
|
```python
|
|
# In GET /printers/new or GET /printers/{id}/edit route
|
|
clients = list(Client.select().order_by(Client.name))
|
|
drivers = list(Driver.select().order_by(Driver.uploaded_at.desc()))
|
|
return templates.TemplateResponse(
|
|
request=request,
|
|
name="partials/printer_form.html",
|
|
context={"clients": clients, "drivers": drivers, "printer": None},
|
|
)
|
|
```
|
|
|
|
### Pattern 6: Grouping Printers by Client in Template (new in Phase 3, PRNT-08)
|
|
**What:** Query all printers with their client FK resolved. In the route handler, group into a dict `{client_name: [printer, ...]}` before passing to template. Avoids N+1 queries.
|
|
**When to use:** Printers page list view.
|
|
**Example:**
|
|
```python
|
|
from collections import defaultdict
|
|
printers = list(Printer.select(Printer, Client).join(Client, JOIN.LEFT_OUTER).order_by(Client.name, Printer.name))
|
|
grouped = defaultdict(list)
|
|
for p in printers:
|
|
label = p.client.name if p.client_id else "Unassigned"
|
|
grouped[label].append(p)
|
|
```
|
|
|
|
### Anti-Patterns to Avoid
|
|
- **`async def` route with Peewee calls:** Peewee is synchronous; calling it in an async context blocks the event loop. Use `def` handlers. (Established decision from Phase 2.)
|
|
- **Schema changes in Phase 3:** The full 4-table schema was created in Phase 1. Do not call `db.create_tables()` for any new table — it does not exist in the schema.
|
|
- **Free-text driver name entry:** The driver selection on the printer form must use the `Driver` FK dropdown (parsed from stored `driver_desc`), not a text input. This preserves the Phase 2 guarantee (DRV-03).
|
|
- **Disabling the port name field:** PRNT-03 says "user can override" — keep the field editable. Alpine.js only pre-fills when the field is empty or when IP changes and port hasn't been manually set.
|
|
- **N+1 queries for printer list:** Use `Printer.select(Printer, Client).join(Client, JOIN.LEFT_OUTER)` for the grouped list, not a loop with individual `.client` accesses.
|
|
|
|
## Don't Hand-Roll
|
|
|
|
| Problem | Don't Build | Use Instead | Why |
|
|
|---------|-------------|-------------|-----|
|
|
| Form field validation | Custom regex validator class | Peewee model constraints + FastAPI `Form(...)` validation | Peewee unique/null constraints enforce DB-level; FastAPI Form gives HTTP 422 on missing required fields |
|
|
| Client auto-create on printer save | Complex upsert logic | Separate POST /clients endpoint or inline create in printer route | Simpler and testable in isolation |
|
|
| Port name auto-derivation | Server-side computation on every request | Alpine.js `x-model` in-browser reactivity | PRNT-03 is a UX hint, not a business rule — belongs in the browser |
|
|
| Driver file re-upload for regeneration | New upload flow | `DriverStore.get_path(driver.sha256)` with stored FK | The sha256 FK already points to the persisted file on disk |
|
|
|
|
**Key insight:** Every data model needed for Phase 3 is already in SQLite. Phase 3 is purely UI wiring and route handlers — no new storage logic.
|
|
|
|
## Common Pitfalls
|
|
|
|
### Pitfall 1: Peewee ForeignKeyField Access Triggers N+1
|
|
**What goes wrong:** Template or route iterates printers and accesses `p.client.name` — each access fires a separate SELECT query.
|
|
**Why it happens:** Peewee lazy-loads FK relations by default.
|
|
**How to avoid:** Use `Printer.select(Printer, Client).join(Client, JOIN.LEFT_OUTER)` to fetch all data in one query. Pre-build a grouping dict in Python before passing to template.
|
|
**Warning signs:** Slow printer list page; SQL log shows many small SELECT queries per request.
|
|
|
|
### Pitfall 2: Alpine.js Port Derivation Overwrites User Edits
|
|
**What goes wrong:** User edits the port name field, then touches the IP field — Alpine.js replaces the edited port name.
|
|
**Why it happens:** If `x-model` on port always derives from IP, any IP input event overwrites.
|
|
**How to avoid:** Track whether the user has manually edited the port field. One approach: use a `portEdited` flag in `x-data`; set it on `@change` of the port input; only auto-derive when `!portEdited`.
|
|
**Warning signs:** PRNT-03 success criterion says "user can edit it" — test this explicitly.
|
|
|
|
### Pitfall 3: HTMX Target ID Mismatch on Error Response
|
|
**What goes wrong:** Error HTML fragment has a different `id` than the HTMX `hx-target`, so HTMX cannot swap it.
|
|
**Why it happens:** Error helper function uses a hardcoded ID that doesn't match the form's target.
|
|
**How to avoid:** Either pass the target ID as a parameter to `_error_response()`, or use a consistent convention (`printer-list` is always the target for the printer page).
|
|
**Warning signs:** Error messages not appearing after form submission; HTMX console warnings about missing targets.
|
|
|
|
### Pitfall 4: Printer Form Missing `driver_id` Dropdown Population
|
|
**What goes wrong:** Driver `<select>` 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
|
|
<!-- Alpine.js available from /static/alpine.min.js (already in base.html) -->
|
|
<div x-data="{ ip: '', port: '', portEdited: false }">
|
|
<label>IP Address</label>
|
|
<input type="text" name="ip_address" x-model="ip"
|
|
@input="if (!portEdited) port = 'IP_' + ip.replaceAll('.', '_')"
|
|
required>
|
|
|
|
<label>Port Name</label>
|
|
<input type="text" name="port_name" x-model="port"
|
|
@change="portEdited = true"
|
|
@keydown="portEdited = true">
|
|
</div>
|
|
```
|
|
|
|
### HTMX Delete with Confirmation (optional, for plan 03-01)
|
|
```html
|
|
<!-- HTMX hx-confirm attribute prevents accidental deletes -->
|
|
<button
|
|
hx-delete="/printers/{{ printer.id }}"
|
|
hx-target="#printer-list"
|
|
hx-swap="outerHTML"
|
|
hx-confirm="Delete printer '{{ printer.name }}'?"
|
|
>
|
|
Delete
|
|
</button>
|
|
```
|
|
|
|
### 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 `<select>` include A3, A5, Executive, etc.?
|
|
- Recommendation: Implement exactly A4, Letter, Legal for Phase 3 to match the requirement. More sizes can be added in Phase 4 or later without schema changes (it's a CharField).
|
|
|
|
2. **Inline client creation vs. separate /clients page**
|
|
- What we know: PRNT-08 requires client/tenant label assignment. Plan 03-02 covers "Client/tenant organization".
|
|
- What's unclear: Should creating a new client require navigating away from the printer form?
|
|
- Recommendation: Plan 03-02 creates both a /clients page (GET, POST for CRUD) and ensures the printer form's client dropdown refreshes. An HTMX-powered inline "Add new client" flow is a nice-to-have but not required — a separate /clients page first keeps the plan atomic.
|
|
|
|
3. **PRNT-10 exact scope boundary between Phase 3 and Phase 4**
|
|
- What we know: PRNT-10 says "regenerate its package without re-uploading drivers". Phase 4 generates the scripts. Phase 5 assembles the package.
|
|
- What's unclear: What should the "Regenerate" button do in Phase 3 before Phase 4 exists?
|
|
- Recommendation: Phase 3 implements the complete printer detail/edit page with a "Regenerate Package" button that is disabled (or links to a placeholder). The button will become functional when Phase 4's script generation endpoint exists. The Phase 3 obligation is that the printer config is fully retrievable and the driver FK is intact.
|
|
|
|
## Validation Architecture
|
|
|
|
### Test Framework
|
|
| Property | Value |
|
|
|----------|-------|
|
|
| Framework | pytest 8.x |
|
|
| Config file | none — discovered via standard pytest directory scan |
|
|
| Quick run command | `pytest tests/test_printer_crud.py -x -q` |
|
|
| Full suite command | `pytest tests/ -v` |
|
|
|
|
### Phase Requirements → Test Map
|
|
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|
|
|--------|----------|-----------|-------------------|-------------|
|
|
| PRNT-01 | POST /printers with valid name creates printer record | integration | `pytest tests/test_printer_crud.py::test_create_printer_persisted -x` | Wave 0 |
|
|
| PRNT-02 | POST /printers with valid IP creates printer record | integration | `pytest tests/test_printer_crud.py::test_create_printer_persisted -x` | Wave 0 |
|
|
| PRNT-03 | Alpine.js auto-populates port from IP in-browser | manual | Manual browser test: type IP, verify port auto-fills, verify it remains editable | n/a |
|
|
| PRNT-04 | POST /printers with duplex_mode stores correct value | integration | `pytest tests/test_printer_crud.py::test_create_printer_duplex -x` | Wave 0 |
|
|
| PRNT-05 | POST /printers with color_mode stores correct bool | integration | `pytest tests/test_printer_crud.py::test_create_printer_color_mode -x` | Wave 0 |
|
|
| PRNT-06 | POST /printers with paper_size stores correct value | integration | `pytest tests/test_printer_crud.py::test_create_printer_paper_size -x` | Wave 0 |
|
|
| PRNT-07 | POST /printers with collate stores correct bool | integration | `pytest tests/test_printer_crud.py::test_create_printer_collate -x` | Wave 0 |
|
|
| PRNT-08 | POST /printers with client_id assigns printer to client; GET /printers groups by client | integration | `pytest tests/test_printer_crud.py::test_printer_grouped_by_client -x` | Wave 0 |
|
|
| PRNT-09 | Created printer appears on GET /printers after creation | integration | `pytest tests/test_printer_crud.py::test_printer_survives_page_refresh -x` | Wave 0 |
|
|
| PRNT-10 | GET /printers/{id} shows driver info; driver file still accessible via DriverStore | integration | `pytest tests/test_printer_crud.py::test_printer_detail_shows_driver -x` | Wave 0 |
|
|
|
|
### Sampling Rate
|
|
- **Per task commit:** `pytest tests/test_printer_crud.py -x -q`
|
|
- **Per wave merge:** `pytest tests/ -v`
|
|
- **Phase gate:** Full suite green before `/gsd:verify-work`
|
|
|
|
### Wave 0 Gaps
|
|
- [ ] `tests/test_printer_crud.py` — covers PRNT-01 through PRNT-10
|
|
- [ ] `tests/test_client_crud.py` — covers Client creation, listing (can be merged into test_printer_crud.py if small)
|
|
|
|
*(conftest.py and framework already exist — no new infrastructure needed)*
|
|
|
|
## Sources
|
|
|
|
### Primary (HIGH confidence)
|
|
- `imptune/db/models.py` — Full Printer and Client schema already defined; field names and types verified by direct inspection
|
|
- `imptune/db/database.py` — init_db pattern, Peewee deferred init, WAL + foreign_keys verified
|
|
- `imptune/api/drivers.py` — HTMX partial swap, error fragment, sync handler, Jinja2 context patterns
|
|
- `imptune/templates/` — base.html, partials, Alpine.js availability verified
|
|
- `requirements.txt` — Exact library versions verified by direct inspection
|
|
|
|
### Secondary (MEDIUM confidence)
|
|
- Peewee 3.17 JOIN.LEFT_OUTER pattern — verified against Peewee changelog notes from known behavior in Phase 2 integration tests
|
|
- Alpine.js `x-data` / `x-model` / `@input` pattern for port derivation — standard Alpine.js 3.x reactive pattern; Alpine.js is already baked into static assets
|
|
|
|
### Tertiary (LOW confidence)
|
|
- `nulls_last()` availability on Peewee `order_by` — common Peewee pattern but not directly tested in Phase 2; fallback is Python-side sort if needed
|
|
|
|
## Metadata
|
|
|
|
**Confidence breakdown:**
|
|
- Standard stack: HIGH — all libraries verified from requirements.txt and existing code
|
|
- Architecture: HIGH — all patterns verified from Phase 2 working code
|
|
- Pitfalls: HIGH — most derived from documented Phase 2 decisions in STATE.md and direct code inspection
|
|
- Validation architecture: HIGH — test framework and conftest already exist; only test file creation is needed
|
|
|
|
**Research date:** 2026-04-10
|
|
**Valid until:** 2026-05-10 (stack is stable; no fast-moving dependencies)
|