Commit initial
This commit is contained in:
@@ -0,0 +1,313 @@
|
||||
---
|
||||
phase: 03-printer-configuration
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- tests/test_printer_crud.py
|
||||
- imptune/api/printers.py
|
||||
- imptune/api/clients.py
|
||||
- imptune/api/pages.py
|
||||
- imptune/main.py
|
||||
- imptune/templates/printers.html
|
||||
- imptune/templates/clients.html
|
||||
- imptune/templates/partials/printer_list.html
|
||||
- imptune/templates/partials/printer_form.html
|
||||
autonomous: false
|
||||
requirements:
|
||||
- PRNT-01
|
||||
- PRNT-02
|
||||
- PRNT-03
|
||||
- PRNT-04
|
||||
- PRNT-05
|
||||
- PRNT-06
|
||||
- PRNT-07
|
||||
- PRNT-08
|
||||
- PRNT-09
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "User can fill in a printer form with name, IP, port, duplex, color, paper size, collate and save it"
|
||||
- "Port name auto-populates from IP address (user can still edit it)"
|
||||
- "User can assign a printer to a client/tenant label"
|
||||
- "Saved printer appears in a grouped list after page refresh"
|
||||
- "User can create a new client from the clients page"
|
||||
artifacts:
|
||||
- path: "imptune/api/printers.py"
|
||||
provides: "POST /printers endpoint with Form parsing and validation"
|
||||
exports: ["router"]
|
||||
- path: "imptune/api/clients.py"
|
||||
provides: "POST /clients and GET /clients endpoints"
|
||||
exports: ["router"]
|
||||
- path: "imptune/templates/printers.html"
|
||||
provides: "Printer list page grouped by client"
|
||||
- path: "imptune/templates/partials/printer_form.html"
|
||||
provides: "Printer create form with all fields, Alpine.js port derivation"
|
||||
- path: "imptune/templates/partials/printer_list.html"
|
||||
provides: "Grouped printer list fragment for HTMX swap"
|
||||
- path: "tests/test_printer_crud.py"
|
||||
provides: "Integration tests for PRNT-01 through PRNT-09"
|
||||
key_links:
|
||||
- from: "imptune/templates/partials/printer_form.html"
|
||||
to: "/printers"
|
||||
via: "hx-post form submission"
|
||||
pattern: "hx-post.*printers"
|
||||
- from: "imptune/api/printers.py"
|
||||
to: "imptune/db/models.py"
|
||||
via: "Printer.create() and Client.select()"
|
||||
pattern: "Printer\\.create|Client\\.select"
|
||||
- from: "imptune/main.py"
|
||||
to: "imptune/api/printers.py"
|
||||
via: "app.include_router(printers.router)"
|
||||
pattern: "include_router.*printers"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Implement printer and client CRUD with full form, grouped list display, and persistence.
|
||||
|
||||
Purpose: This is the core of Phase 3 — technicians need to configure printer parameters, assign to clients, and see saved configs persist across sessions. All form fields (PRNT-01 through PRNT-07), client assignment (PRNT-08), and persistence (PRNT-09) are covered.
|
||||
|
||||
Output: Working /printers and /clients pages with HTMX-powered form submission, Alpine.js port auto-derivation, and grouped printer list.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/03-printer-configuration/03-RESEARCH.md
|
||||
|
||||
<interfaces>
|
||||
<!-- Key types and contracts from existing codebase. Use directly. -->
|
||||
|
||||
From imptune/db/models.py:
|
||||
```python
|
||||
class Client(BaseModel):
|
||||
name = CharField(unique=True)
|
||||
created_at = DateTimeField(default=datetime.utcnow)
|
||||
|
||||
class Driver(BaseModel):
|
||||
sha256 = CharField(unique=True, index=True)
|
||||
original_filename = CharField()
|
||||
size_bytes = IntegerField()
|
||||
uploaded_at = DateTimeField(default=datetime.utcnow)
|
||||
driver_desc = CharField(null=True) # JSON list of driver names
|
||||
inf_filename = CharField(null=True)
|
||||
architecture = CharField(null=True)
|
||||
has_cat_file = BooleanField(default=False)
|
||||
|
||||
class Printer(BaseModel):
|
||||
name = CharField()
|
||||
ip_address = CharField()
|
||||
port_name = CharField()
|
||||
client = ForeignKeyField(Client, null=True, backref="printers")
|
||||
driver = ForeignKeyField(Driver, null=True, backref="printers")
|
||||
duplex_mode = CharField(default="OneSided")
|
||||
color_mode = BooleanField(default=True)
|
||||
paper_size = CharField(default="A4")
|
||||
collate = BooleanField(default=True)
|
||||
created_at = DateTimeField(default=datetime.utcnow)
|
||||
updated_at = DateTimeField(default=datetime.utcnow)
|
||||
```
|
||||
|
||||
From imptune/api/drivers.py (established error pattern):
|
||||
```python
|
||||
def _error_response(message: str, status_code: int = 400) -> HTMLResponse:
|
||||
return HTMLResponse(
|
||||
content=f"<div id='driver-list' class='error'><p>{message}</p></div>",
|
||||
status_code=status_code,
|
||||
)
|
||||
```
|
||||
|
||||
From tests/conftest.py (test fixtures):
|
||||
```python
|
||||
@pytest.fixture
|
||||
def client(tmp_data_dir):
|
||||
from imptune.main import app
|
||||
with TestClient(app) as c:
|
||||
yield c
|
||||
|
||||
@pytest.fixture
|
||||
def tmp_data_dir(tmp_path, monkeypatch):
|
||||
data_dir = tmp_path / "data"
|
||||
data_dir.mkdir()
|
||||
monkeypatch.setenv("DATA_DIR", str(data_dir))
|
||||
import imptune.config as cfg
|
||||
cfg.DATA_DIR = str(data_dir)
|
||||
cfg.DB_PATH = str(data_dir / "imptune.db")
|
||||
cfg.DRIVERS_DIR = str(data_dir / "drivers")
|
||||
return data_dir
|
||||
```
|
||||
|
||||
From imptune/main.py (router registration pattern):
|
||||
```python
|
||||
app.include_router(health.router)
|
||||
app.include_router(pages.router)
|
||||
app.include_router(drivers.router)
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Write failing integration tests for printer and client CRUD</name>
|
||||
<files>tests/test_printer_crud.py</files>
|
||||
<behavior>
|
||||
- test_create_printer_persisted: POST /printers with name="Test Printer", ip_address="192.168.1.100", port_name="IP_192_168_1_100" returns 200; GET /printers contains "Test Printer" (covers PRNT-01, PRNT-02, PRNT-09)
|
||||
- test_create_printer_duplex: POST /printers with duplex_mode="LongEdge"; verify Printer record has duplex_mode="LongEdge" (covers PRNT-04)
|
||||
- test_create_printer_color_mode: POST /printers with color_mode=false; verify Printer record has color_mode=False (covers PRNT-05)
|
||||
- test_create_printer_paper_size: POST /printers with paper_size="Letter"; verify Printer record has paper_size="Letter" (covers PRNT-06)
|
||||
- test_create_printer_collate: POST /printers with collate=false; verify Printer record has collate=False (covers PRNT-07)
|
||||
- test_create_client: POST /clients with name="Contoso" returns 200; GET /clients contains "Contoso"
|
||||
- test_printer_grouped_by_client: Create client "Contoso", POST /printers with client_id=contoso.id; GET /printers HTML contains "Contoso" as group header (covers PRNT-08)
|
||||
- test_create_printer_missing_name: POST /printers with empty name returns 400 (validation)
|
||||
- test_create_printer_invalid_ip: POST /printers with ip_address="" returns 400 (validation)
|
||||
- test_delete_printer: POST /printers to create, then DELETE /printers/{id} returns 200; printer no longer in GET /printers
|
||||
</behavior>
|
||||
<action>
|
||||
Create `tests/test_printer_crud.py` with all tests listed above. Use the existing `client` fixture from conftest.py (which provides TestClient with lifespan-triggered init_db). Follow the same pattern as `test_driver_upload.py`:
|
||||
- Import `pytest` and use `client` fixture
|
||||
- POST form data via `client.post("/printers", data={...})` (not JSON, form-encoded)
|
||||
- POST client creation via `client.post("/clients", data={"name": "Contoso"})`
|
||||
- For verifying DB state, import `Printer` and `Client` from `imptune.db.models` inside each test
|
||||
- For grouped list test, check that GET /printers response HTML contains the client name in an `<h3>` or `<section>` header
|
||||
- For boolean fields (color_mode, collate): HTML checkboxes send "on" when checked, nothing when unchecked. Use `data={"color_mode": ""}` for false and `data={"color_mode": "on"}` for true. Design tests accordingly.
|
||||
- All tests should FAIL initially (routes don't exist yet). Run them to confirm RED state.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd C:/Users/SebastienQUEROL/Documents/projets/ImpTune && python -m pytest tests/test_printer_crud.py -x -q 2>&1 | head -30</automated>
|
||||
</verify>
|
||||
<done>All tests exist and fail with connection/404 errors (RED state). No test passes yet.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Implement printer and client CRUD routes, templates, and wire routers</name>
|
||||
<files>imptune/api/printers.py, imptune/api/clients.py, imptune/api/pages.py, imptune/main.py, imptune/templates/printers.html, imptune/templates/clients.html, imptune/templates/partials/printer_form.html, imptune/templates/partials/printer_list.html</files>
|
||||
<action>
|
||||
**1. Create `imptune/api/clients.py`:**
|
||||
- `router = APIRouter(prefix="/clients")`
|
||||
- `POST /clients`: Accept `name: str = Form(...)`. Validate non-empty. Create `Client.create(name=name)`. Handle IntegrityError (duplicate name) with 400 error. Return redirect or HTMX partial.
|
||||
- Follow established pattern: sync `def` handlers, Jinja2Templates from same path as drivers.py.
|
||||
|
||||
**2. Create `imptune/api/printers.py`:**
|
||||
- `router = APIRouter(prefix="/printers")`
|
||||
- `POST /printers`: Accept all form fields via `Form(...)`:
|
||||
- `name: str = Form(...)` (required)
|
||||
- `ip_address: str = Form(...)` (required)
|
||||
- `port_name: str = Form(...)` (required)
|
||||
- `duplex_mode: str = Form("OneSided")` — validate value in ("OneSided", "LongEdge", "ShortEdge")
|
||||
- `color_mode: str = Form("")` — checkbox: "on" = True, "" = False. Convert: `bool(color_mode)`
|
||||
- `paper_size: str = Form("A4")` — validate value in ("A4", "Letter", "Legal")
|
||||
- `collate: str = Form("")` — same checkbox pattern as color_mode
|
||||
- `client_id: str = Form("")` — empty string = None, otherwise int FK
|
||||
- `driver_id: str = Form("")` — empty string = None, otherwise int FK
|
||||
- Validate: name not empty, ip_address not empty. On failure return `_error_response(msg)` with `<div id="printer-list">` wrapper (same HTMX pattern as drivers.py).
|
||||
- On success: `Printer.create(...)` with all fields. Return the updated printer list partial via `_render_printer_list(request)`.
|
||||
- `DELETE /printers/{printer_id}`: Delete printer by ID. Return updated printer list partial.
|
||||
- Helper `_render_printer_list(request)`: Query `Printer.select(Printer, Client).join(Client, JOIN.LEFT_OUTER).order_by(Client.name, Printer.name)`, group into `defaultdict(list)` by client name ("Unassigned" for null client_id), pass `grouped` to `partials/printer_list.html`.
|
||||
- Helper `_error_response(message, status_code=400)`: Return `HTMLResponse(content=f"<div id='printer-list' class='error'><p>{message}</p></div>", status_code=status_code)`.
|
||||
|
||||
**3. Update `imptune/api/pages.py`:**
|
||||
Add two new page routes (import Client, Printer, Driver, json, JOIN from peewee):
|
||||
- `GET /printers`: Render `printers.html` with `grouped` printers (same query as `_render_printer_list`), plus `clients` list and `driver_data` list for form dropdowns.
|
||||
- `GET /clients`: Render `clients.html` with `clients = list(Client.select().order_by(Client.name))`.
|
||||
|
||||
**4. Create `imptune/templates/printers.html`:**
|
||||
- Extends `base.html`. Contains:
|
||||
- `<h1>Printers</h1>`
|
||||
- Section with `<h2>Add Printer</h2>` containing `{% include "partials/printer_form.html" %}`
|
||||
- Section with `<h2>Printer Library</h2>` containing `{% include "partials/printer_list.html" %}`
|
||||
|
||||
**5. Create `imptune/templates/partials/printer_form.html`:**
|
||||
- Wrap in `<div x-data="{ ip: '{{ printer.ip_address if printer else '' }}', port: '{{ printer.port_name if printer else '' }}', portEdited: {{ 'true' if printer else 'false' }} }">` for Alpine.js reactivity.
|
||||
- Form with `hx-post="/printers"`, `hx-target="#printer-list"`, `hx-swap="outerHTML"`.
|
||||
- Fields:
|
||||
- Printer Name: `<input type="text" name="name" required>`
|
||||
- IP Address: `<input type="text" name="ip_address" x-model="ip" @input="if (!portEdited) port = 'IP_' + ip.replaceAll('.', '_')" required>`
|
||||
- Port Name: `<input type="text" name="port_name" x-model="port" @change="portEdited = true" @keydown="portEdited = true">` (PRNT-03)
|
||||
- Driver: `<select name="driver_id"><option value="">-- No driver --</option>{% for item in driver_data %}<option value="{{ item.driver.id }}">{{ item.driver.original_filename }} ({{ item.names | join(', ') }})</option>{% endfor %}</select>`
|
||||
- Duplex Mode: `<select name="duplex_mode"><option value="OneSided">One-Sided</option><option value="LongEdge">Long Edge</option><option value="ShortEdge">Short Edge</option></select>`
|
||||
- Color Mode: `<input type="checkbox" name="color_mode" value="on" checked>` (default checked = True)
|
||||
- Paper Size: `<select name="paper_size"><option value="A4">A4</option><option value="Letter">Letter</option><option value="Legal">Legal</option></select>`
|
||||
- Collate: `<input type="checkbox" name="collate" value="on" checked>` (default checked = True)
|
||||
- Client: `<select name="client_id"><option value="">-- Unassigned --</option>{% for c in clients %}<option value="{{ c.id }}">{{ c.name }}</option>{% endfor %}</select>`
|
||||
- Submit button: `<button type="submit">Save Printer</button>`
|
||||
|
||||
**6. Create `imptune/templates/partials/printer_list.html`:**
|
||||
- `<div id="printer-list">`
|
||||
- If grouped is empty: `<p>No printers configured yet.</p>`
|
||||
- Else: for each `(client_name, printers)` in grouped.items(): `<h3>{{ client_name }}</h3>` then a `<table>` with columns: Name, IP, Driver, Duplex, Paper, Actions. Each row has a Delete button with `hx-delete="/printers/{{ p.id }}" hx-target="#printer-list" hx-swap="outerHTML" hx-confirm="Delete '{{ p.name }}'?"`.
|
||||
|
||||
**7. Create `imptune/templates/clients.html`:**
|
||||
- Extends `base.html`. `<h1>Clients</h1>`.
|
||||
- Form: `<form hx-post="/clients" hx-target="#client-list" hx-swap="outerHTML">` with name input and submit button.
|
||||
- `<div id="client-list">`: Table of clients (Name, Created, Printer Count). Printer count via `Client.printers` backref — pass pre-computed count from route.
|
||||
|
||||
**8. Update `imptune/main.py`:**
|
||||
- Add imports: `from imptune.api import clients, printers`
|
||||
- Add: `app.include_router(printers.router)` and `app.include_router(clients.router)`
|
||||
|
||||
After all files are created, run the full test suite to confirm GREEN state.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd C:/Users/SebastienQUEROL/Documents/projets/ImpTune && python -m pytest tests/test_printer_crud.py -x -q && python -m pytest tests/ -v</automated>
|
||||
</verify>
|
||||
<done>All test_printer_crud.py tests pass (GREEN). Full test suite passes. GET /printers shows form with all fields. POST /printers creates and persists printer. Printers grouped by client name in list. GET /clients shows client list with creation form.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<name>Task 3: Verify printer form and Alpine.js port auto-derivation in browser</name>
|
||||
<files>imptune/templates/partials/printer_form.html</files>
|
||||
<action>
|
||||
Human verifies the complete printer configuration flow in a browser, especially the Alpine.js port auto-derivation (PRNT-03) which cannot be tested via pytest.
|
||||
|
||||
What was built: Complete printer configuration form with Alpine.js port auto-derivation (PRNT-03), all form fields (PRNT-01 through PRNT-07), client assignment (PRNT-08), and persistence (PRNT-09). Also a /clients page for client management.
|
||||
|
||||
Steps to verify:
|
||||
1. Start app: `docker compose up` (or `uvicorn imptune.main:app --reload`)
|
||||
2. Navigate to /clients — create a client "Contoso"
|
||||
3. Navigate to /printers — verify empty state message
|
||||
4. Fill in printer form:
|
||||
- Name: "HP LaserJet 4050"
|
||||
- IP: "192.168.1.100" — verify port name auto-fills to "IP_192_168_1_100"
|
||||
- Manually edit port name to "CUSTOM_PORT" — change IP to "10.0.0.1" — verify port stays "CUSTOM_PORT" (not overwritten)
|
||||
- Select duplex "Long Edge", uncheck Color, paper "Letter", check Collate
|
||||
- Select client "Contoso"
|
||||
- Click Save
|
||||
5. Verify printer appears under "Contoso" group heading
|
||||
6. Refresh page — verify printer still appears (persistence)
|
||||
7. Click Delete on the printer — confirm deletion dialog — verify it disappears
|
||||
</action>
|
||||
<verify>Human confirms all 7 steps pass in browser</verify>
|
||||
<done>Alpine.js port auto-derivation works correctly: auto-fills from IP, preserves manual edits. Full CRUD flow verified visually.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `pytest tests/test_printer_crud.py -x -q` — all printer CRUD tests pass
|
||||
- `pytest tests/ -v` — full suite green (no regressions)
|
||||
- GET /printers renders form with all required fields
|
||||
- POST /printers persists to SQLite and returns updated list
|
||||
- Printers are grouped by client name in the list display
|
||||
- Alpine.js port derivation works in browser (manual checkpoint)
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- All PRNT-01 through PRNT-09 requirements verified by tests or manual check
|
||||
- Printer form has: name, IP, port (auto-derived), duplex select, color checkbox, paper select, collate checkbox, client select, driver select
|
||||
- Printer list groups by client with "Unassigned" fallback
|
||||
- Client CRUD works on /clients page
|
||||
- No N+1 queries (LEFT_OUTER JOIN used)
|
||||
- Full test suite green
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/03-printer-configuration/03-01-SUMMARY.md`
|
||||
</output>
|
||||
@@ -0,0 +1,166 @@
|
||||
---
|
||||
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*
|
||||
@@ -0,0 +1,207 @@
|
||||
---
|
||||
phase: 03-printer-configuration
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["03-01"]
|
||||
files_modified:
|
||||
- tests/test_printer_crud.py
|
||||
- imptune/api/printers.py
|
||||
- imptune/api/pages.py
|
||||
- imptune/templates/partials/printer_detail.html
|
||||
autonomous: true
|
||||
requirements:
|
||||
- PRNT-10
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "User can open a saved printer config and see all fields pre-populated"
|
||||
- "User can see the associated driver info on the detail page"
|
||||
- "A regenerate button is visible (disabled/placeholder until Phase 4)"
|
||||
artifacts:
|
||||
- path: "imptune/templates/partials/printer_detail.html"
|
||||
provides: "Printer detail view with all fields and driver info"
|
||||
- path: "imptune/api/printers.py"
|
||||
provides: "GET /printers/{id} detail endpoint"
|
||||
key_links:
|
||||
- from: "imptune/templates/partials/printer_list.html"
|
||||
to: "/printers/{id}"
|
||||
via: "printer name link in list row"
|
||||
pattern: "href.*printers.*id"
|
||||
- from: "imptune/api/printers.py"
|
||||
to: "imptune/db/models.py"
|
||||
via: "Printer.get_by_id with driver FK access"
|
||||
pattern: "Printer\\.get_by_id|printer\\.driver"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Implement the printer detail/edit page so saved configs can be retrieved and prepared for regeneration.
|
||||
|
||||
Purpose: PRNT-10 requires that a user can open a saved printer config and regenerate its package without re-uploading drivers. Phase 3's scope is: the config is fully retrievable, driver FK is intact, and a "Regenerate" button exists (placeholder until Phase 4 delivers script generation). This also adds printer name links in the list for navigation.
|
||||
|
||||
Output: GET /printers/{id} detail page with pre-populated fields, driver info display, and regeneration placeholder button.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/03-printer-configuration/03-RESEARCH.md
|
||||
@.planning/phases/03-printer-configuration/03-01-SUMMARY.md
|
||||
|
||||
<interfaces>
|
||||
<!-- Contracts from Plan 01 that this plan builds on -->
|
||||
|
||||
From imptune/api/printers.py (created in Plan 01):
|
||||
```python
|
||||
router = APIRouter(prefix="/printers")
|
||||
|
||||
def _render_printer_list(request: Request) -> HTMLResponse:
|
||||
"""Returns partials/printer_list.html with grouped printers."""
|
||||
|
||||
def _error_response(message: str, status_code: int = 400) -> HTMLResponse:
|
||||
"""HTMX-friendly error fragment."""
|
||||
```
|
||||
|
||||
From imptune/db/models.py:
|
||||
```python
|
||||
class Printer(BaseModel):
|
||||
name = CharField()
|
||||
ip_address = CharField()
|
||||
port_name = CharField()
|
||||
client = ForeignKeyField(Client, null=True, backref="printers")
|
||||
driver = ForeignKeyField(Driver, null=True, backref="printers")
|
||||
duplex_mode = CharField(default="OneSided")
|
||||
color_mode = BooleanField(default=True)
|
||||
paper_size = CharField(default="A4")
|
||||
collate = BooleanField(default=True)
|
||||
|
||||
class Driver(BaseModel):
|
||||
sha256 = CharField(unique=True)
|
||||
original_filename = CharField()
|
||||
driver_desc = CharField(null=True) # JSON list of driver names
|
||||
```
|
||||
|
||||
From imptune/storage/driver_store.py:
|
||||
```python
|
||||
class DriverStore:
|
||||
def get_path(self, sha256: str) -> Path:
|
||||
"""Returns path to stored driver ZIP."""
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Write failing test for printer detail page</name>
|
||||
<files>tests/test_printer_crud.py</files>
|
||||
<behavior>
|
||||
- test_printer_detail_shows_driver: Create a Driver record (via direct Peewee insert with sha256, original_filename, driver_desc=json.dumps(["HP Universal"])), create a Printer with driver FK set. GET /printers/{id} returns 200 with HTML containing printer name, IP, and "HP Universal" driver name.
|
||||
- test_printer_detail_not_found: GET /printers/9999 returns 404.
|
||||
- test_printer_detail_no_driver: Create a Printer with driver=None. GET /printers/{id} returns 200, HTML does not crash, shows "No driver assigned" or similar.
|
||||
</behavior>
|
||||
<action>
|
||||
Append three new tests to the existing `tests/test_printer_crud.py` file (created in Plan 01):
|
||||
- `test_printer_detail_shows_driver`: Use the `client` fixture. Create a Driver record directly via `Driver.create(sha256="abc123", original_filename="test.zip", size_bytes=1000, driver_desc=json.dumps(["HP Universal"]))`. Create a Printer with `driver=driver_obj`. GET `/printers/{printer.id}` and assert 200 status. Assert "HP Universal" appears in response text. Assert printer name appears.
|
||||
- `test_printer_detail_not_found`: GET `/printers/9999` returns 404.
|
||||
- `test_printer_detail_no_driver`: Create Printer with driver=None. GET `/printers/{printer.id}` returns 200. Assert "No driver assigned" or similar text in response.
|
||||
|
||||
Run tests to confirm RED state (route does not exist yet).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd C:/Users/SebastienQUEROL/Documents/projets/ImpTune && python -m pytest tests/test_printer_crud.py::test_printer_detail_shows_driver tests/test_printer_crud.py::test_printer_detail_not_found tests/test_printer_crud.py::test_printer_detail_no_driver -x -q 2>&1 | head -20</automated>
|
||||
</verify>
|
||||
<done>Three new tests exist and fail (RED state). Existing tests still pass.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Implement printer detail route, template, and list navigation links</name>
|
||||
<files>imptune/api/printers.py, imptune/api/pages.py, imptune/templates/partials/printer_detail.html, imptune/templates/partials/printer_list.html</files>
|
||||
<action>
|
||||
**1. Add GET /printers/{printer_id} to `imptune/api/pages.py`:**
|
||||
- Route: `@router.get("/printers/{printer_id}", response_class=HTMLResponse)`
|
||||
- Handler: `def printer_detail(request: Request, printer_id: int):`
|
||||
- Query: `Printer.select(Printer, Client, Driver).join(Client, JOIN.LEFT_OUTER).switch(Printer).join(Driver, JOIN.LEFT_OUTER).where(Printer.id == printer_id).first()`
|
||||
- If not found: return HTMLResponse with 404 status and a simple error page.
|
||||
- If found: parse `printer.driver.driver_desc` (JSON) into driver_names list if driver exists. Pass `printer`, `driver_names`, and `driver` to template.
|
||||
- Render `printers.html` but with a detail block, OR create a dedicated detail template that extends base.html. Prefer: render `partials/printer_detail.html` inside the printers page layout.
|
||||
|
||||
Actually, simpler approach: create a standalone detail page.
|
||||
- Render: `templates.TemplateResponse(request=request, name="printer_detail.html", context={"printer": printer, "driver_names": driver_names})`
|
||||
- This requires creating `imptune/templates/printer_detail.html` (NOT a partial — a full page).
|
||||
|
||||
**2. Create `imptune/templates/printer_detail.html`:**
|
||||
Extends `base.html`. Content:
|
||||
```
|
||||
<h1>{{ printer.name }}</h1>
|
||||
<article>
|
||||
<h2>Configuration</h2>
|
||||
<dl>
|
||||
<dt>IP Address</dt><dd>{{ printer.ip_address }}</dd>
|
||||
<dt>Port Name</dt><dd>{{ printer.port_name }}</dd>
|
||||
<dt>Duplex Mode</dt><dd>{{ printer.duplex_mode }}</dd>
|
||||
<dt>Color Mode</dt><dd>{{ "Color" if printer.color_mode else "Grayscale" }}</dd>
|
||||
<dt>Paper Size</dt><dd>{{ printer.paper_size }}</dd>
|
||||
<dt>Collate</dt><dd>{{ "Yes" if printer.collate else "No" }}</dd>
|
||||
<dt>Client</dt><dd>{{ printer.client.name if printer.client_id else "Unassigned" }}</dd>
|
||||
</dl>
|
||||
|
||||
<h2>Driver</h2>
|
||||
{% if printer.driver_id %}
|
||||
<dl>
|
||||
<dt>Package</dt><dd>{{ printer.driver.original_filename }}</dd>
|
||||
<dt>Driver Name(s)</dt><dd>{{ driver_names | join(", ") }}</dd>
|
||||
<dt>Architecture</dt><dd>{{ printer.driver.architecture or "Unknown" }}</dd>
|
||||
</dl>
|
||||
{% else %}
|
||||
<p>No driver assigned</p>
|
||||
{% endif %}
|
||||
|
||||
<h2>Actions</h2>
|
||||
<button disabled aria-busy="false" title="Available after script generation is implemented (Phase 4)">
|
||||
Regenerate Package
|
||||
</button>
|
||||
<a href="/printers" role="button" class="secondary">Back to Printers</a>
|
||||
</article>
|
||||
```
|
||||
|
||||
**3. Update `imptune/templates/partials/printer_list.html`:**
|
||||
Make printer names clickable: change the Name `<td>` from plain text to `<a href="/printers/{{ p.id }}">{{ p.name }}</a>`.
|
||||
|
||||
**4. Adjust file path:** The detail template is `imptune/templates/printer_detail.html` (full page, not partial).
|
||||
|
||||
Run all tests to confirm GREEN state.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd C:/Users/SebastienQUEROL/Documents/projets/ImpTune && python -m pytest tests/test_printer_crud.py -x -q && python -m pytest tests/ -v</automated>
|
||||
</verify>
|
||||
<done>All tests pass (GREEN). GET /printers/{id} shows full printer config with driver info. "Regenerate Package" button is visible but disabled. Printer names in list are clickable links to detail page. Full test suite green.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `pytest tests/test_printer_crud.py -x -q` — all tests pass including new detail tests
|
||||
- `pytest tests/ -v` — full suite green
|
||||
- GET /printers/{id} displays all printer fields and driver info
|
||||
- Printer names in list link to detail page
|
||||
- "Regenerate Package" button visible but disabled
|
||||
- 404 returned for nonexistent printer IDs
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- PRNT-10 verified: saved config retrievable with driver association intact, regeneration button present (placeholder)
|
||||
- Detail page shows all configured fields (name, IP, port, duplex, color, paper, collate, client, driver)
|
||||
- Driver info displayed from FK relationship (no re-upload needed)
|
||||
- Full test suite green with no regressions
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/03-printer-configuration/03-02-SUMMARY.md`
|
||||
</output>
|
||||
@@ -0,0 +1,112 @@
|
||||
---
|
||||
phase: 03-printer-configuration
|
||||
plan: 02
|
||||
subsystem: ui
|
||||
tags: [fastapi, jinja2, htmx, peewee, sqlite]
|
||||
|
||||
# Dependency graph
|
||||
requires:
|
||||
- phase: 03-01
|
||||
provides: Printer and Client CRUD endpoints, DB models, printer_list partial template
|
||||
|
||||
provides:
|
||||
- GET /printers/{id} detail route with LEFT OUTER JOINs on Client and Driver
|
||||
- printer_detail.html full-page template showing all config fields and driver info
|
||||
- Disabled "Regenerate Package" button (Phase 4 placeholder)
|
||||
- Clickable printer name links in printer_list.html navigating to detail page
|
||||
|
||||
affects:
|
||||
- 04-script-generation (regenerate button placeholder ready to wire up)
|
||||
|
||||
# Tech tracking
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "TDD RED/GREEN cycle: failing tests committed first, then implementation"
|
||||
- "LEFT OUTER JOIN chain with .switch(Printer) for multi-FK queries in Peewee"
|
||||
- "Null-safe driver_desc parse: check printer.driver_id before json.loads"
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- imptune/templates/printer_detail.html
|
||||
modified:
|
||||
- imptune/api/pages.py
|
||||
- imptune/templates/partials/printer_list.html
|
||||
- tests/test_printer_crud.py
|
||||
|
||||
key-decisions:
|
||||
- "Detail page is a full-page template (not partial) — simpler than partial injection into printers.html"
|
||||
- "Route lives in pages.py (not printers.py) because it returns a full HTML page, not an HTMX fragment"
|
||||
|
||||
patterns-established:
|
||||
- "Full-page detail routes in pages.py; HTMX fragment routes in api/printers.py"
|
||||
- "Disabled placeholder buttons for Phase N+1 features with descriptive title attribute"
|
||||
|
||||
requirements-completed:
|
||||
- PRNT-10
|
||||
|
||||
# Metrics
|
||||
duration: 2min
|
||||
completed: 2026-04-10
|
||||
---
|
||||
|
||||
# Phase 3 Plan 02: Printer Detail Page Summary
|
||||
|
||||
**GET /printers/{id} detail page with pre-populated config fields, associated driver info via FK, and disabled Regenerate Package button placeholder for Phase 4**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** ~2 min
|
||||
- **Started:** 2026-04-10T12:03:29Z
|
||||
- **Completed:** 2026-04-10T12:05:56Z
|
||||
- **Tasks:** 2
|
||||
- **Files modified:** 4
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- Printer detail route with Peewee multi-FK LEFT OUTER JOIN queries returning 200 or 404
|
||||
- Full-page Jinja2 template showing all 8 config fields, driver package name, driver names list, and architecture
|
||||
- Graceful "No driver assigned" display when driver FK is null
|
||||
- Printer names in list view are now clickable navigation links to their detail pages
|
||||
- 3 new integration tests; full suite at 61 passing
|
||||
|
||||
## Task Commits
|
||||
|
||||
1. **Task 1: Write failing tests for printer detail page** - `6e7892e` (test)
|
||||
2. **Task 2: Implement printer detail route, template, and list nav links** - `cad664c` (feat)
|
||||
|
||||
**Plan metadata:** (committed next)
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `imptune/api/pages.py` - Added GET /printers/{printer_id} route with LEFT OUTER JOIN on Client and Driver
|
||||
- `imptune/templates/printer_detail.html` - Full-page detail template with config, driver info, and regenerate placeholder
|
||||
- `imptune/templates/partials/printer_list.html` - Printer name column wrapped in anchor tag linking to detail page
|
||||
- `tests/test_printer_crud.py` - Added 3 tests: detail with driver, 404 not found, detail without driver
|
||||
|
||||
## Decisions Made
|
||||
|
||||
- Detail page uses a full-page template (not a partial) to avoid coupling it to the printers list layout
|
||||
- Route placed in `pages.py` since it returns a full HTML page, keeping HTMX fragment routes in `api/printers.py`
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
None - plan executed exactly as written.
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
None.
|
||||
|
||||
## User Setup Required
|
||||
|
||||
None - no external service configuration required.
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
- PRNT-10 satisfied: saved configs are retrievable with driver association intact
|
||||
- "Regenerate Package" button is present and disabled, ready for Phase 4 to wire up
|
||||
- No blockers for Phase 4 script generation work
|
||||
|
||||
---
|
||||
*Phase: 03-printer-configuration*
|
||||
*Completed: 2026-04-10*
|
||||
@@ -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>
|
||||
## 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)
|
||||
@@ -0,0 +1,107 @@
|
||||
---
|
||||
phase: 3
|
||||
slug: printer-configuration
|
||||
status: draft
|
||||
nyquist_compliant: true
|
||||
wave_0_complete: false
|
||||
created: 2026-04-10
|
||||
nyquist_audited: 2026-04-13
|
||||
nyquist_auditor: Claude (gsd-executor, plan 08-03)
|
||||
---
|
||||
|
||||
# Phase 3 — Validation Strategy
|
||||
|
||||
> Per-phase validation contract for feedback sampling during execution.
|
||||
|
||||
---
|
||||
|
||||
## Test Infrastructure
|
||||
|
||||
| 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` |
|
||||
| **Estimated runtime** | ~5 seconds |
|
||||
|
||||
---
|
||||
|
||||
## Sampling Rate
|
||||
|
||||
- **After every task commit:** Run `pytest tests/test_printer_crud.py -x -q`
|
||||
- **After every plan wave:** Run `pytest tests/ -v`
|
||||
- **Before `/gsd:verify-work`:** Full suite must be green
|
||||
- **Max feedback latency:** 5 seconds
|
||||
|
||||
---
|
||||
|
||||
## Per-Task Verification Map
|
||||
|
||||
| Task ID | Plan | Wave | Requirement | Test Type | Automated Command | File Exists | Status |
|
||||
|---------|------|------|-------------|-----------|-------------------|-------------|--------|
|
||||
| 03-01-01 | 01 | 1 | PRNT-01, PRNT-02 | integration | `pytest tests/test_printer_crud.py::test_create_printer_persisted -x` | ❌ W0 | ⬜ pending |
|
||||
| 03-01-02 | 01 | 1 | PRNT-04 | integration | `pytest tests/test_printer_crud.py::test_create_printer_duplex -x` | ❌ W0 | ⬜ pending |
|
||||
| 03-01-03 | 01 | 1 | PRNT-05 | integration | `pytest tests/test_printer_crud.py::test_create_printer_color_mode -x` | ❌ W0 | ⬜ pending |
|
||||
| 03-01-04 | 01 | 1 | PRNT-06 | integration | `pytest tests/test_printer_crud.py::test_create_printer_paper_size -x` | ❌ W0 | ⬜ pending |
|
||||
| 03-01-05 | 01 | 1 | PRNT-07 | integration | `pytest tests/test_printer_crud.py::test_create_printer_collate -x` | ❌ W0 | ⬜ pending |
|
||||
| 03-01-06 | 01 | 1 | PRNT-09 | integration | `pytest tests/test_printer_crud.py::test_printer_survives_page_refresh -x` | ❌ W0 | ⬜ pending |
|
||||
| 03-02-01 | 02 | 1 | PRNT-08 | integration | `pytest tests/test_printer_crud.py::test_printer_grouped_by_client -x` | ❌ W0 | ⬜ pending |
|
||||
| 03-03-01 | 03 | 2 | PRNT-10 | integration | `pytest tests/test_printer_crud.py::test_printer_detail_shows_driver -x` | ❌ W0 | ⬜ pending |
|
||||
| 03-01-XX | 01 | 1 | PRNT-03 | manual | Manual browser test | n/a | ⬜ pending |
|
||||
|
||||
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
|
||||
|
||||
---
|
||||
|
||||
## Wave 0 Requirements
|
||||
|
||||
- [ ] `tests/test_printer_crud.py` — stubs for PRNT-01 through PRNT-10 (integration tests)
|
||||
- [ ] `tests/test_client_crud.py` — stubs for client creation and listing (can merge into test_printer_crud.py)
|
||||
|
||||
*Existing infrastructure covers framework and conftest.py — no new infrastructure needed.*
|
||||
|
||||
---
|
||||
|
||||
## Manual-Only Verifications
|
||||
|
||||
| Behavior | Requirement | Why Manual | Test Instructions |
|
||||
|----------|-------------|------------|-------------------|
|
||||
| Alpine.js auto-populates port name from IP | PRNT-03 | Browser-side Alpine.js reactivity cannot be tested with pytest TestClient | 1. Open /printers/new 2. Type "192.168.1.100" in IP field 3. Verify port field shows "IP_192_168_1_100" 4. Edit port field manually 5. Change IP field 6. Verify port field keeps manual value |
|
||||
|
||||
---
|
||||
|
||||
## Nyquist Record
|
||||
|
||||
> Audited 2026-04-13 by Claude (gsd-executor, plan 08-03). One row per Phase 3 success criterion derived from `milestones/v1.0-ROADMAP.md` Phase 3 goal + plan outcomes (PRNT-01..10), cross-checked against `03-VERIFICATION.md` (10/10 observable truths verified 2026-04-10, 9 automated + 1 human-needed on PRNT-03) and `REQUIREMENTS.md` v1.0 PRNT-0x block. Evidence cites committed tests, source lines, and the dated VERIFICATION report. Status values: `pass` / `fail-fix-v1.1` / `deferred-v1.2` / `wont-do`.
|
||||
>
|
||||
> **Phase 3 goal (v1.0-ROADMAP.md):** *"Technicians configure all printer parameters, assign printers to clients, and regenerate saved configs without re-uploading drivers."*
|
||||
|
||||
| # | Success Criterion | Observable Check | Evidence | Status | Notes |
|
||||
|---|-------------------|------------------|----------|--------|-------|
|
||||
| 1 | **PRNT-01** — User can set a printer display name | `pytest tests/test_printer_crud.py::test_create_printer_persisted` verifies `name` field posted to `POST /printers` is persisted and rendered in `GET /printers` | `tests/test_printer_crud.py::test_create_printer_persisted`; `imptune/api/printers.py` `POST /printers` handler (commit 356c2ee); `imptune/templates/partials/printer_form.html` `name` input; 03-VERIFICATION.md row 1 (2026-04-10) | pass | |
|
||||
| 2 | **PRNT-02** — User can set a printer IP address or hostname | `pytest tests/test_printer_crud.py::test_create_printer_persisted` includes `ip_address` field in POST body; persisted value round-trips through `GET /printers` | `tests/test_printer_crud.py::test_create_printer_persisted`; `imptune/api/printers.py` POST handler (`ip_address: str = Form(...)`); `printer_form.html` IP field with `x-model`; 03-VERIFICATION.md row 1 | pass | |
|
||||
| 3 | **PRNT-03** — System auto-suggests port name from IP (user can override, manual edits preserved) | `pytest tests/e2e/test_port_autofill.py` — real Chromium via Playwright, types IP into form, asserts port field auto-fills with `IP_x_x_x_x`, then edits port manually, changes IP, asserts manual value preserved | `tests/e2e/test_port_autofill.py` (Phase 9 UX-02 commit 322fc20 test, 37a06da docs); `imptune/templates/partials/printer_form.html` Alpine.js `x-data`/`x-model`/`portEdited` guard (commit 356c2ee); `.planning/phases/09-ux-tech-debt-closure/09-02-SUMMARY.md` (UX-02 complete 2026-04-13); REQUIREMENTS.md v1.1 UX-02 = Complete; 03-VERIFICATION.md row 2 | pass | **Historical gap closed via Phase 9 / UX-02 fixing commits.** At v1.0 audit (03-VERIFICATION.md 2026-04-10) this was the sole `NEEDS HUMAN` truth — Alpine.js reactivity cannot execute inside FastAPI TestClient. Phase 9 Plan 02 added a Playwright headless-chromium live-browser e2e test that exercises the @input handler and the `portEdited` manual-edit lock. Closed as `pass` citing the fixing commits, consistent with the 08-01 (row 14 → Phase 10 RTVAL-01) and 08-02 (row 6 → Phase 9 UX-01) precedents. |
|
||||
| 4 | **PRNT-04** — User can set duplex mode | `pytest tests/test_printer_crud.py::test_create_printer_duplex` posts `duplex_mode=LongEdge` and verifies persisted value | `tests/test_printer_crud.py::test_create_printer_duplex`; `printer_form.html` `duplex_mode` select (OneSided/LongEdge/ShortEdge); `imptune/api/printers.py` POST handler mapping; 03-VERIFICATION.md row 1 + PRNT-04 coverage row | pass | |
|
||||
| 5 | **PRNT-05** — User can set color vs. grayscale default | `pytest tests/test_printer_crud.py::test_create_printer_color_mode` posts form without the `color_mode` checkbox and asserts the persisted value is `False` (checkbox-to-bool conversion) | `tests/test_printer_crud.py::test_create_printer_color_mode`; `printer_form.html` `color_mode` checkbox; `imptune/api/printers.py` checkbox-to-bool conversion in POST handler; 03-VERIFICATION.md PRNT-05 row | pass | |
|
||||
| 6 | **PRNT-06** — User can set paper size | `pytest tests/test_printer_crud.py::test_create_printer_paper_size` posts `paper_size=A4` (plus Letter/Legal variants) and verifies persisted value | `tests/test_printer_crud.py::test_create_printer_paper_size`; `printer_form.html` `paper_size` select (A4/Letter/Legal); 03-VERIFICATION.md PRNT-06 row | pass | |
|
||||
| 7 | **PRNT-07** — User can set collate on/off | `pytest tests/test_printer_crud.py::test_create_printer_collate` posts form without `collate` checkbox and asserts persisted value is `False` | `tests/test_printer_crud.py::test_create_printer_collate`; `printer_form.html` `collate` checkbox; `imptune/api/printers.py` checkbox-to-bool conversion; 03-VERIFICATION.md PRNT-07 row | pass | |
|
||||
| 8 | **PRNT-08** — User can assign a printer to a client/tenant label | `pytest tests/test_printer_crud.py::test_printer_grouped_by_client` creates printers under distinct clients and asserts `GET /printers` renders `<h3>` group headers per client (LEFT OUTER JOIN) | `tests/test_printer_crud.py::test_printer_grouped_by_client`; `imptune/api/printers.py` `_render_printer_list` uses `Printer.select(Printer, Client).join(Client, JOIN.LEFT_OUTER)`; `printer_form.html` client select; `imptune/templates/partials/printer_list.html` group headers; 03-VERIFICATION.md rows 3 + 4 | pass | |
|
||||
| 9 | **PRNT-09** — Printer configurations are persisted in SQLite across sessions | `pytest tests/test_printer_crud.py::test_printer_survives_page_refresh` (+ `test_create_printer_persisted`) — posts printer, re-queries via `GET /printers`, asserts DB count and rendered HTML both show the record | `tests/test_printer_crud.py::test_create_printer_persisted`, `::test_printer_survives_page_refresh`; `imptune/db/models.py` Printer model (Phase 1 schema); 03-VERIFICATION.md row 4 | pass | |
|
||||
| 10 | **PRNT-10** — User can regenerate a package from saved config without re-uploading drivers (Phase 3 scope: detail page loads full config with driver FK intact; regenerate button placeholder until Phase 4) | `pytest tests/test_printer_crud.py::test_printer_detail_shows_driver` — `GET /printers/{id}` returns full-page detail with all 7 config fields and driver name (`HP Universal`) pre-populated; `::test_printer_detail_no_driver` covers missing-driver fallback | `tests/test_printer_crud.py::test_printer_detail_shows_driver`, `::test_printer_detail_no_driver`; `imptune/api/pages.py` `GET /printers/{id}` uses `.switch(Printer).join(Driver, JOIN.LEFT_OUTER)`; `imptune/templates/printer_detail.html` (disabled regenerate button, Phase-4 scoped); 03-VERIFICATION.md rows 6 + 7 + 8 | pass | Full regeneration workflow is a Phase 4 deliverable per 03 plan scope. Phase 3 scope = config retrievable with driver FK intact + placeholder button. Verified SATISFIED (partial) in 03-VERIFICATION.md; the "partial" refers to the Phase-4 button wiring, not a Phase 3 gap. |
|
||||
|
||||
**Audit outcome:** 10/10 rows `pass`. No `fail-fix-v1.1`, `deferred-v1.2`, or `wont-do` rows. Phase 3 is Nyquist-compliant: every PRNT-0x success criterion has exactly one observable check with cited, committed evidence. The PRNT-03 Alpine.js IP→port live-browser gap (only `NEEDS HUMAN` truth in 03-VERIFICATION.md) is captured as row 3 and closed via Phase 9 / UX-02 Playwright e2e fixing commits — fully honoring the CONTEXT.md locked-decision pattern.
|
||||
|
||||
---
|
||||
|
||||
## Validation Sign-Off
|
||||
|
||||
- [ ] All tasks have `<automated>` verify or Wave 0 dependencies
|
||||
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify
|
||||
- [ ] Wave 0 covers all MISSING references
|
||||
- [ ] No watch-mode flags
|
||||
- [ ] Feedback latency < 5s
|
||||
- [x] `nyquist_compliant: true` set in frontmatter
|
||||
- [x] Nyquist audit complete — 2026-04-13 — Sébastien QUEROL
|
||||
|
||||
**Approval:** Nyquist-audited 2026-04-13 by Claude (gsd-executor, plan 08-03) — 10/10 pass; signed off 2026-04-13 by Sébastien QUEROL (index: v1.0-VALIDATION-INDEX.md)
|
||||
@@ -0,0 +1,127 @@
|
||||
---
|
||||
phase: 03-printer-configuration
|
||||
verified: 2026-04-10T12:30:00Z
|
||||
status: human_needed
|
||||
score: 9/10 must-haves verified (automated); 10/10 upon human confirmation of PRNT-03
|
||||
re_verification: false
|
||||
human_verification:
|
||||
- test: "Port name auto-derivation from IP address"
|
||||
expected: "Typing an IP in the form auto-fills the port name as IP_x_x_x_x; after manually editing the port name, changing the IP does NOT overwrite the manual value"
|
||||
why_human: "Alpine.js x-data reactivity cannot be exercised via pytest/TestClient; the @input and @change handlers on the IP and port fields require a real browser to execute"
|
||||
---
|
||||
|
||||
# Phase 03: Printer Configuration Verification Report
|
||||
|
||||
**Phase Goal:** Printer configuration management — CRUD operations for printers, clients, detail/edit views
|
||||
**Verified:** 2026-04-10T12:30:00Z
|
||||
**Status:** human_needed — all automated checks pass; one Alpine.js behavior (PRNT-03) requires browser confirmation
|
||||
**Re-verification:** No — initial verification
|
||||
|
||||
---
|
||||
|
||||
## Goal Achievement
|
||||
|
||||
### Observable Truths
|
||||
|
||||
| # | Truth | Status | Evidence |
|
||||
|----|-------|--------|----------|
|
||||
| 1 | User can fill in a printer form with name, IP, port, duplex, color, paper size, collate and save it | VERIFIED | `test_create_printer_persisted` passes; all 9 form fields present in `printer_form.html` and mapped in `POST /printers` handler |
|
||||
| 2 | Port name auto-populates from IP address (user can still edit it) | NEEDS HUMAN | `printer_form.html` contains correct Alpine.js `@input` handler and `portEdited` guard; runtime behavior untestable without a browser |
|
||||
| 3 | User can assign a printer to a client/tenant label | VERIFIED | `test_printer_grouped_by_client` passes; `client_id` FK wired in `create_printer`; client select in form template |
|
||||
| 4 | Saved printer appears in a grouped list after page refresh | VERIFIED | `test_create_printer_persisted` checks GET /printers; `test_printer_grouped_by_client` confirms `<h3>` group headers rendered |
|
||||
| 5 | User can create a new client from the clients page | VERIFIED | `test_create_client` passes; `POST /clients` endpoint functional; `clients.html` contains HTMX form |
|
||||
| 6 | User can open a saved printer config and see all fields pre-populated | VERIFIED | `test_printer_detail_shows_driver` passes; `printer_detail.html` renders all 8 config fields |
|
||||
| 7 | User can see the associated driver info on the detail page | VERIFIED | `test_printer_detail_shows_driver` confirms driver name ("HP Universal") in response; `test_printer_detail_no_driver` confirms "No driver assigned" fallback |
|
||||
| 8 | A regenerate button is visible (disabled/placeholder until Phase 4) | VERIFIED | `printer_detail.html` line 28: `<button disabled aria-busy="false" title="Available after script generation is implemented (Phase 4)">Regenerate Package</button>` |
|
||||
| 9 | User can delete a printer | VERIFIED | `test_delete_printer` passes; `DELETE /printers/{id}` endpoint functional; delete button with `hx-delete` in `printer_list.html` |
|
||||
| 10 | Full test suite passes with no regressions | VERIFIED | 61 tests pass across full suite |
|
||||
|
||||
**Score:** 9/10 automated truths verified (Truth 2 pending human confirmation)
|
||||
|
||||
---
|
||||
|
||||
## Required Artifacts
|
||||
|
||||
| Artifact | Expected | Status | Details |
|
||||
|----------|----------|--------|---------|
|
||||
| `imptune/api/printers.py` | POST /printers with form parsing and validation; DELETE /printers/{id} | VERIFIED | 115 lines; all form fields, checkbox-to-bool conversion, FK resolution, error handling, `_render_printer_list` helper |
|
||||
| `imptune/api/clients.py` | POST /clients and GET (partial) endpoints | VERIFIED | 55 lines; duplicate-name IntegrityError handling, HTMX partial return |
|
||||
| `imptune/api/pages.py` | GET /printers, GET /clients, GET /printers/{id} page routes | VERIFIED | All three routes present with correct LEFT OUTER JOIN queries and template rendering |
|
||||
| `imptune/main.py` | Registers printers and clients routers | VERIFIED | Lines 34-35: `app.include_router(printers.router)` and `app.include_router(clients.router)` |
|
||||
| `imptune/templates/printers.html` | Printer list page grouped by client | VERIFIED | Extends base.html; includes printer_form.html and printer_list.html partials |
|
||||
| `imptune/templates/printer_detail.html` | Detail page with all fields, driver info, regenerate button | VERIFIED | Full-page template with 34 lines; all 7 config fields, conditional driver section, disabled regenerate button |
|
||||
| `imptune/templates/partials/printer_form.html` | Form with all 9 fields, Alpine.js port derivation | VERIFIED | All fields present: name, IP (x-model), port (x-model with portEdited guard), driver select, duplex, color checkbox, paper, collate checkbox, client select |
|
||||
| `imptune/templates/partials/printer_list.html` | Grouped printer list fragment for HTMX swap | VERIFIED | `<div id="printer-list">`; group by client in `<h3>` headers; delete buttons with `hx-delete`; printer name links to detail |
|
||||
| `imptune/templates/clients.html` | Clients page with creation form | VERIFIED | Extends base.html; HTMX form targeting `#client-list`; includes client_list.html partial |
|
||||
| `imptune/templates/partials/client_list.html` | Client table partial | VERIFIED | `<div id="client-list">`; table with name and created_at columns; empty state message |
|
||||
| `tests/test_printer_crud.py` | Integration tests for PRNT-01 through PRNT-10 | VERIFIED | 13 tests, all passing; covers all required behaviors including detail, 404, driver FK |
|
||||
|
||||
---
|
||||
|
||||
## Key Link Verification
|
||||
|
||||
| From | To | Via | Status | Details |
|
||||
|------|----|-----|--------|---------|
|
||||
| `printer_form.html` | `/printers` | `hx-post="/printers"` | VERIFIED | Line 2: `<form hx-post="/printers" hx-target="#printer-list" hx-swap="outerHTML">` |
|
||||
| `imptune/api/printers.py` | `imptune/db/models.py` | `Printer.create()` and `Client.select()` | VERIFIED | Lines 92-103: `Printer.create(...)` with all fields; `_render_printer_list` queries `Printer.select(Printer, Client).join(Client, JOIN.LEFT_OUTER)` |
|
||||
| `imptune/main.py` | `imptune/api/printers.py` | `app.include_router(printers.router)` | VERIFIED | Line 34 of main.py |
|
||||
| `imptune/main.py` | `imptune/api/clients.py` | `app.include_router(clients.router)` | VERIFIED | Line 35 of main.py |
|
||||
| `printer_list.html` | `/printers/{id}` | printer name link | VERIFIED | Line 25: `<td><a href="/printers/{{ p.id }}">{{ p.name }}</a></td>` |
|
||||
| `imptune/api/pages.py` | `imptune/db/models.py` | `Printer.get_by_id` with driver FK | VERIFIED | Lines 81-87: LEFT OUTER JOIN chain with `.switch(Printer).join(Driver, JOIN.LEFT_OUTER)` |
|
||||
|
||||
---
|
||||
|
||||
## Requirements Coverage
|
||||
|
||||
| Requirement | Source Plan | Description | Status | Evidence |
|
||||
|-------------|------------|-------------|--------|----------|
|
||||
| PRNT-01 | 03-01 | User can set printer display name | SATISFIED | `name` field in form; `test_create_printer_persisted` verifies persistence |
|
||||
| PRNT-02 | 03-01 | User can set printer IP address or hostname | SATISFIED | `ip_address` field in form; `test_create_printer_persisted` verifies |
|
||||
| PRNT-03 | 03-01 | System auto-suggests port name from IP (user can override) | NEEDS HUMAN | Alpine.js logic present and correct in template; browser verification required |
|
||||
| PRNT-04 | 03-01 | User can set duplex mode | SATISFIED | `duplex_mode` select with 3 options; `test_create_printer_duplex` verifies LongEdge |
|
||||
| PRNT-05 | 03-01 | User can set color vs. grayscale default | SATISFIED | `color_mode` checkbox; `test_create_printer_color_mode` verifies False when unchecked |
|
||||
| PRNT-06 | 03-01 | User can set paper size | SATISFIED | `paper_size` select with A4/Letter/Legal; `test_create_printer_paper_size` verifies |
|
||||
| PRNT-07 | 03-01 | User can set collate on/off | SATISFIED | `collate` checkbox; `test_create_printer_collate` verifies False when unchecked |
|
||||
| PRNT-08 | 03-01 | User can assign printer to a client/tenant label | SATISFIED | `client_id` FK select; `test_printer_grouped_by_client` verifies grouping |
|
||||
| PRNT-09 | 03-01 | Printer configurations are persisted in SQLite across sessions | SATISFIED | `test_create_printer_persisted` verifies DB count and GET /printers shows saved record |
|
||||
| PRNT-10 | 03-02 | User can regenerate a package from saved config without re-uploading drivers | SATISFIED (partial) | Detail page loads full config with driver FK intact (`test_printer_detail_shows_driver`); regenerate button present but disabled — full regeneration is a Phase 4 deliverable per plan scope |
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns Found
|
||||
|
||||
None. Scanned `imptune/api/printers.py`, `imptune/api/clients.py`, `imptune/api/pages.py`, `imptune/templates/printers.html`, `imptune/templates/printer_detail.html` for TODO/FIXME/placeholder comments, empty return values, and console.log-only handlers. No issues found.
|
||||
|
||||
The disabled "Regenerate Package" button is intentional scope deferral (Phase 4), not a stub — documented in plan and REQUIREMENTS.md.
|
||||
|
||||
---
|
||||
|
||||
## Human Verification Required
|
||||
|
||||
### 1. Alpine.js Port Auto-Derivation (PRNT-03)
|
||||
|
||||
**Test:** Start the app (`uvicorn imptune.main:app --reload`). Navigate to `/printers`. In the printer form:
|
||||
1. Type `192.168.1.100` into the IP Address field.
|
||||
2. Verify that the Port Name field auto-fills to `IP_192_168_1_100` as you type.
|
||||
3. Manually edit the Port Name field to `CUSTOM_PORT`.
|
||||
4. Change the IP Address to `10.0.0.1`.
|
||||
5. Verify the Port Name remains `CUSTOM_PORT` (not overwritten by the IP change).
|
||||
|
||||
**Expected:** Auto-fill works during step 2; manual edit lock works during step 5.
|
||||
|
||||
**Why human:** Alpine.js `@input` and `@change` handlers with `portEdited` flag execute in-browser JavaScript. The FastAPI `TestClient` does not run a JavaScript engine, so this behavior cannot be tested via pytest.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Phase 03 goal is substantively achieved. All 10 requirement IDs (PRNT-01 through PRNT-10) are implemented with real code — no stubs, no placeholder routes, no empty handlers. The full test suite (61 tests) passes cleanly.
|
||||
|
||||
The only item requiring human confirmation is PRNT-03 (Alpine.js port auto-derivation from IP). The implementation is correct — the `x-data` block, `x-model` bindings, `@input` handler, and `portEdited` guard are all present in `printer_form.html` — but this is JavaScript behavior that only executes in a browser.
|
||||
|
||||
PRNT-10's "regenerate" button is disabled by design. The plan explicitly scopes Phase 3's PRNT-10 deliverable as "config retrievable with driver FK intact, regenerate button present as placeholder." The full regeneration workflow is Phase 4's responsibility. This is not a gap.
|
||||
|
||||
---
|
||||
|
||||
_Verified: 2026-04-10T12:30:00Z_
|
||||
_Verifier: Claude (gsd-verifier)_
|
||||
Reference in New Issue
Block a user