docs(03): create phase plan for printer configuration
Two plans covering PRNT-01 through PRNT-10: printer+client CRUD with Alpine.js port derivation (plan 01), and printer detail page with regeneration placeholder (plan 02). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -60,12 +60,11 @@ Plans:
|
|||||||
2. Saved printer appears under its client/tenant label after page refresh
|
2. Saved printer appears under its client/tenant label after page refresh
|
||||||
3. System auto-populates the port name field from the entered IP address (user can edit it)
|
3. System auto-populates the port name field from the entered IP address (user can edit it)
|
||||||
4. User can open a saved printer config and regenerate its package without uploading the driver again
|
4. User can open a saved printer config and regenerate its package without uploading the driver again
|
||||||
**Plans**: TBD
|
**Plans**: 2 plans
|
||||||
|
|
||||||
Plans:
|
Plans:
|
||||||
- [ ] 03-01: Printer config form and CRUD API (all fields, validation, SQLite persistence)
|
- [ ] 03-01-PLAN.md — Printer + client CRUD, form with all fields, Alpine.js port derivation, grouped list, integration tests
|
||||||
- [ ] 03-02: Client/tenant organization (label assignment, printer list grouped by client)
|
- [ ] 03-02-PLAN.md — Printer detail page, saved config retrieval, driver association display, regeneration placeholder
|
||||||
- [ ] 03-03: Saved config retrieval and regeneration flow
|
|
||||||
|
|
||||||
### Phase 4: Script Generation
|
### Phase 4: Script Generation
|
||||||
**Goal**: The system produces correct, production-ready PowerShell scripts that handle all Intune and RMM execution contexts
|
**Goal**: The system produces correct, production-ready PowerShell scripts that handle all Intune and RMM execution contexts
|
||||||
@@ -109,6 +108,6 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5
|
|||||||
|-------|----------------|--------|-----------|
|
|-------|----------------|--------|-----------|
|
||||||
| 1. Foundation | 3/3 | Complete | 2026-04-10 |
|
| 1. Foundation | 3/3 | Complete | 2026-04-10 |
|
||||||
| 2. Driver Management | 1/2 | In Progress| |
|
| 2. Driver Management | 1/2 | In Progress| |
|
||||||
| 3. Printer Configuration | 0/3 | Not started | - |
|
| 3. Printer Configuration | 0/2 | Not started | - |
|
||||||
| 4. Script Generation | 0/3 | Not started | - |
|
| 4. Script Generation | 0/3 | Not started | - |
|
||||||
| 5. Package Export | 0/3 | Not started | - |
|
| 5. Package Export | 0/3 | Not started | - |
|
||||||
|
|||||||
@@ -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,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>
|
||||||
Reference in New Issue
Block a user