17 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 03-printer-configuration | 01 | execute | 1 |
|
false |
|
|
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.
<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>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/03-printer-configuration/03-RESEARCH.mdFrom imptune/db/models.py:
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):
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):
@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):
app.include_router(health.router)
app.include_router(pages.router)
app.include_router(drivers.router)
` or `` 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.
cd C:/Users/SebastienQUEROL/Documents/projets/ImpTune && python -m pytest tests/test_printer_crud.py -x -q 2>&1 | head -30
All tests exist and fail with connection/404 errors (RED state). No test passes yet.
Task 2: Implement printer and client CRUD routes, templates, and wire routers
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
**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.
cd C:/Users/SebastienQUEROL/Documents/projets/ImpTune && python -m pytest tests/test_printer_crud.py -x -q && python -m pytest tests/ -v
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.
Task 3: Verify printer form and Alpine.js port auto-derivation in browser
imptune/templates/partials/printer_form.html
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:
- Start app:
docker compose up (or uvicorn imptune.main:app --reload)
- Navigate to /clients — create a client "Contoso"
- Navigate to /printers — verify empty state message
- 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
- Verify printer appears under "Contoso" group heading
- Refresh page — verify printer still appears (persistence)
- Click Delete on the printer — confirm deletion dialog — verify it disappears
Human confirms all 7 steps pass in browser
Alpine.js port auto-derivation works correctly: auto-fills from IP, preserves manual edits. Full CRUD flow verified visually.
- `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)
<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>
After completion, create `.planning/phases/03-printer-configuration/03-01-SUMMARY.md`
2. Create imptune/api/printers.py:
router = APIRouter(prefix="/printers")POST /printers: Accept all form fields viaForm(...):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_modeclient_id: str = Form("")— empty string = None, otherwise int FKdriver_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): QueryPrinter.select(Printer, Client).join(Client, JOIN.LEFT_OUTER).order_by(Client.name, Printer.name), group intodefaultdict(list)by client name ("Unassigned" for null client_id), passgroupedtopartials/printer_list.html. - Helper
_error_response(message, status_code=400): ReturnHTMLResponse(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: Renderprinters.htmlwithgroupedprinters (same query as_render_printer_list), plusclientslist anddriver_datalist for form dropdowns.GET /clients: Renderclients.htmlwithclients = 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>
- Printer Name:
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 withhx-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 viaClient.printersbackref — 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)andapp.include_router(clients.router)
After all files are created, run the full test suite to confirm GREEN state. cd C:/Users/SebastienQUEROL/Documents/projets/ImpTune && python -m pytest tests/test_printer_crud.py -x -q && python -m pytest tests/ -v 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.
Task 3: Verify printer form and Alpine.js port auto-derivation in browser imptune/templates/partials/printer_form.html 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:
- Start app:
docker compose up(oruvicorn imptune.main:app --reload) - Navigate to /clients — create a client "Contoso"
- Navigate to /printers — verify empty state message
- 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
- Verify printer appears under "Contoso" group heading
- Refresh page — verify printer still appears (persistence)
- Click Delete on the printer — confirm deletion dialog — verify it disappears Human confirms all 7 steps pass in browser Alpine.js port auto-derivation works correctly: auto-fills from IP, preserves manual edits. Full CRUD flow verified visually.
<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>