From e27d0285ac2e4e71a62270a25d2862d60f124f59 Mon Sep 17 00:00:00 2001 From: Kawa Date: Fri, 10 Apr 2026 12:35:52 +0200 Subject: [PATCH] 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) --- .planning/ROADMAP.md | 9 +- .../03-printer-configuration/03-01-PLAN.md | 313 ++++++++++++++++++ .../03-printer-configuration/03-02-PLAN.md | 207 ++++++++++++ 3 files changed, 524 insertions(+), 5 deletions(-) create mode 100644 .planning/phases/03-printer-configuration/03-01-PLAN.md create mode 100644 .planning/phases/03-printer-configuration/03-02-PLAN.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 21d4f13..18a6ce9 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -60,12 +60,11 @@ Plans: 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) 4. User can open a saved printer config and regenerate its package without uploading the driver again -**Plans**: TBD +**Plans**: 2 plans Plans: -- [ ] 03-01: Printer config form and CRUD API (all fields, validation, SQLite persistence) -- [ ] 03-02: Client/tenant organization (label assignment, printer list grouped by client) -- [ ] 03-03: Saved config retrieval and regeneration flow +- [ ] 03-01-PLAN.md — Printer + client CRUD, form with all fields, Alpine.js port derivation, grouped list, integration tests +- [ ] 03-02-PLAN.md — Printer detail page, saved config retrieval, driver association display, regeneration placeholder ### Phase 4: Script Generation **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 | | 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 | - | | 5. Package Export | 0/3 | Not started | - | diff --git a/.planning/phases/03-printer-configuration/03-01-PLAN.md b/.planning/phases/03-printer-configuration/03-01-PLAN.md new file mode 100644 index 0000000..a24f4eb --- /dev/null +++ b/.planning/phases/03-printer-configuration/03-01-PLAN.md @@ -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" +--- + + +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. + + + +@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md +@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/03-printer-configuration/03-RESEARCH.md + + + + +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"

{message}

", + 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) +``` +
+
+ + + + + Task 1: Write failing integration tests for printer and client CRUD + tests/test_printer_crud.py + + - 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 + + +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 `

` 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 `
` 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"

{message}

", 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: + - `

Printers

` + - Section with `

Add Printer

` containing `{% include "partials/printer_form.html" %}` + - Section with `

Printer Library

` containing `{% include "partials/printer_list.html" %}` + +**5. Create `imptune/templates/partials/printer_form.html`:** +- Wrap in `
` for Alpine.js reactivity. +- Form with `hx-post="/printers"`, `hx-target="#printer-list"`, `hx-swap="outerHTML"`. +- Fields: + - Printer Name: `` + - IP Address: `` + - Port Name: `` (PRNT-03) + - Driver: `` + - Duplex Mode: `` + - Color Mode: `` (default checked = True) + - Paper Size: `` + - Collate: `` (default checked = True) + - Client: `` + - Submit button: `` + +**6. Create `imptune/templates/partials/printer_list.html`:** +- `
` +- If grouped is empty: `

No printers configured yet.

` +- Else: for each `(client_name, printers)` in grouped.items(): `

{{ client_name }}

` then a `` 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`. `

Clients

`. +- Form: `` with name input and submit button. +- `
`: 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: +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 + + 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) + + + +- 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 + + + +After completion, create `.planning/phases/03-printer-configuration/03-01-SUMMARY.md` + diff --git a/.planning/phases/03-printer-configuration/03-02-PLAN.md b/.planning/phases/03-printer-configuration/03-02-PLAN.md new file mode 100644 index 0000000..4a77e48 --- /dev/null +++ b/.planning/phases/03-printer-configuration/03-02-PLAN.md @@ -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" +--- + + +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. + + + +@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md +@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md + + + +@.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 + + + + +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.""" +``` + + + + + + + Task 1: Write failing test for printer detail page + tests/test_printer_crud.py + + - 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. + + +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). + + + 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 + + Three new tests exist and fail (RED state). Existing tests still pass. + + + + Task 2: Implement printer detail route, template, and list navigation links + imptune/api/printers.py, imptune/api/pages.py, imptune/templates/partials/printer_detail.html, imptune/templates/partials/printer_list.html + +**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: +``` +

{{ printer.name }}

+
+

Configuration

+
+
IP Address
{{ printer.ip_address }}
+
Port Name
{{ printer.port_name }}
+
Duplex Mode
{{ printer.duplex_mode }}
+
Color Mode
{{ "Color" if printer.color_mode else "Grayscale" }}
+
Paper Size
{{ printer.paper_size }}
+
Collate
{{ "Yes" if printer.collate else "No" }}
+
Client
{{ printer.client.name if printer.client_id else "Unassigned" }}
+
+ +

Driver

+ {% if printer.driver_id %} +
+
Package
{{ printer.driver.original_filename }}
+
Driver Name(s)
{{ driver_names | join(", ") }}
+
Architecture
{{ printer.driver.architecture or "Unknown" }}
+
+ {% else %} +

No driver assigned

+ {% endif %} + +

Actions

+ + Back to Printers +
+``` + +**3. Update `imptune/templates/partials/printer_list.html`:** +Make printer names clickable: change the Name `
` from plain text to `{{ p.name }}`. + +**4. Adjust file path:** The detail template is `imptune/templates/printer_detail.html` (full page, not partial). + +Run all tests 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 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. + + + + + +- `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 + + + +- 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 + + + +After completion, create `.planning/phases/03-printer-configuration/03-02-SUMMARY.md` +