Three plans covering UX-01 (driver upload 500 fix + inline HTMX OOB refresh on printer form), UX-02 (Playwright headless test for PRNT-03 IP->port auto-fill), and UX-03 (.ps1 script download routes + detail page links). VALIDATION.md finalized with real task IDs and nyquist_compliant=true. ROADMAP Phase 9 plan list filled in.
218 lines
10 KiB
Markdown
218 lines
10 KiB
Markdown
---
|
|
phase: 09-ux-tech-debt-closure
|
|
plan: 03
|
|
type: execute
|
|
wave: 1
|
|
depends_on: []
|
|
files_modified:
|
|
- imptune/api/scripts.py
|
|
- imptune/templates/printer_detail.html
|
|
- tests/test_script_download.py
|
|
- tests/test_packages.py
|
|
autonomous: true
|
|
requirements: [UX-03]
|
|
must_haves:
|
|
truths:
|
|
- "GET /printers/{id}/scripts/install.ps1 returns 200 with Content-Disposition: attachment; filename=install.ps1 and a non-empty PowerShell body"
|
|
- "GET /printers/{id}/scripts/uninstall.ps1 returns 200 with attachment disposition and uninstall content"
|
|
- "GET /printers/{id}/scripts/detect.ps1 returns 200 with attachment disposition and detect content"
|
|
- "printer_detail.html renders three direct download links for install/uninstall/detect in addition to existing package export buttons"
|
|
artifacts:
|
|
- path: "imptune/api/scripts.py"
|
|
provides: "Three new .ps1 route aliases alongside existing extensionless routes"
|
|
contains: "scripts/install.ps1"
|
|
- path: "imptune/templates/printer_detail.html"
|
|
provides: "Scripts section with 3 direct download <a role=button> links"
|
|
contains: "scripts/install.ps1"
|
|
- path: "tests/test_script_download.py"
|
|
provides: "Integration tests for the 3 new .ps1 routes"
|
|
contains: "test_install_ps1_route"
|
|
key_links:
|
|
- from: "imptune/templates/printer_detail.html"
|
|
to: "GET /printers/{id}/scripts/{install,uninstall,detect}.ps1"
|
|
via: '<a href="/printers/{{printer.id}}/scripts/install.ps1" role="button">'
|
|
pattern: 'scripts/(install|uninstall|detect)\.ps1'
|
|
- from: "imptune/api/scripts.py (.ps1 aliases)"
|
|
to: "imptune/generators/script_generator.render_{install,uninstall,detect}"
|
|
via: "delegation to the same handler logic as the existing extensionless routes"
|
|
pattern: "render_install|render_uninstall|render_detect"
|
|
---
|
|
|
|
<objective>
|
|
Add three `.ps1`-suffixed route aliases (`/printers/{id}/scripts/install.ps1`, `uninstall.ps1`, `detect.ps1`) alongside the existing extensionless routes in `imptune/api/scripts.py`, and wire three direct-download links into `printer_detail.html` next to the existing package export buttons.
|
|
|
|
Purpose: Closes UX-03 — technicians can download each script individually from the printer detail page without going through the package export flow.
|
|
Output: Three new API routes, three template links, two test cases.
|
|
|
|
Independent of 09-01 (no shared files). Can run in Wave 1 parallel with 09-01.
|
|
</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/ROADMAP.md
|
|
@.planning/phases/09-ux-tech-debt-closure/09-CONTEXT.md
|
|
@.planning/phases/09-ux-tech-debt-closure/09-RESEARCH.md
|
|
@.planning/phases/09-ux-tech-debt-closure/09-VALIDATION.md
|
|
@imptune/api/scripts.py
|
|
@imptune/templates/printer_detail.html
|
|
@imptune/generators/script_generator.py
|
|
@tests/test_packages.py
|
|
|
|
<interfaces>
|
|
<!-- Existing contracts this plan extends -->
|
|
|
|
From imptune/api/scripts.py:
|
|
```python
|
|
router = APIRouter(prefix="/printers")
|
|
|
|
def _get_printer_and_driver(printer_id: int):
|
|
"""Returns ((printer, driver, driver_name), None) on success or (None, PlainTextResponse) on error."""
|
|
...
|
|
|
|
@router.get("/{printer_id}/scripts/install")
|
|
def get_install_script(printer_id: int):
|
|
# validates, calls render_install(...), returns PlainTextResponse with
|
|
# Content-Disposition: attachment; filename="install.ps1"
|
|
...
|
|
|
|
@router.get("/{printer_id}/scripts/uninstall") # similar
|
|
@router.get("/{printer_id}/scripts/detect") # similar
|
|
```
|
|
|
|
From imptune/generators/script_generator:
|
|
`render_install(printer_name, ip_address, port_name, driver_name, inf_filename, duplex_mode, color_mode, paper_size, collate) -> str`
|
|
`render_uninstall(printer_name, driver_name, port_name) -> str`
|
|
`render_detect(printer_name) -> str`
|
|
|
|
Existing printer_detail.html Export section (lines 48-51):
|
|
```html
|
|
<h2>Export</h2>
|
|
<a href="/printers/{{ printer.id }}/packages/ninja" role="button">Download NinjaRMM ZIP</a>
|
|
<a href="/printers/{{ printer.id }}/packages/intunewin" role="button">Download .intunewin</a>
|
|
```
|
|
|
|
Guard: the Export section is wrapped in `{% if has_driver %}` — the new Scripts section must be inside the same guard (no scripts without a driver).
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 1: Wave 0 — failing tests for .ps1 routes + detail page script links</name>
|
|
<files>tests/test_script_download.py, tests/test_packages.py</files>
|
|
<behavior>
|
|
- test_install_ps1_route: GET /printers/{id}/scripts/install.ps1 with a printer that has a driver assigned returns 200, `Content-Disposition` contains `attachment; filename="install.ps1"`, body is non-empty and starts with a PowerShell-ish marker (e.g., contains `Add-Printer` or `$PSScriptRoot`).
|
|
- test_uninstall_ps1_route: same for /scripts/uninstall.ps1 — contains `Remove-Printer`.
|
|
- test_detect_ps1_route: same for /scripts/detect.ps1 — contains `Get-Printer`.
|
|
- test_ps1_routes_missing_printer: GET /printers/99999/scripts/install.ps1 returns 404.
|
|
- test_ps1_routes_no_driver: printer without a driver returns 422 (matches `_get_printer_and_driver` contract).
|
|
- test_detail_page_shows_script_links (added to TestCommandPreview class in tests/test_packages.py): GET printer detail page for a printer with a driver MUST contain the three href substrings `/printers/{id}/scripts/install.ps1`, `.../uninstall.ps1`, `.../detect.ps1`.
|
|
</behavior>
|
|
<action>
|
|
Step 1 — Create `tests/test_script_download.py`. Use the existing `client` fixture and the same printer+driver setup pattern used by `tests/test_packages.py::TestCommandPreview`. Reference that file for the exact fixture / seed-data recipe.
|
|
|
|
Step 2 — Add `test_detail_page_shows_script_links` to `TestCommandPreview` (or a sibling class if more natural) in `tests/test_packages.py`. It should seed a printer with a driver, GET `/printers/{id}`, and assert the three `.ps1` href substrings.
|
|
|
|
Step 3 — Run tests. All new tests MUST go RED (routes don't exist, template links don't exist).
|
|
|
|
Commit: `test(09-03): add failing .ps1 route and detail-page link tests`
|
|
</action>
|
|
<verify>
|
|
<automated>pytest tests/test_script_download.py tests/test_packages.py::TestCommandPreview::test_detail_page_shows_script_links -x</automated>
|
|
</verify>
|
|
<done>All 6 new tests exist and go RED. Failing output proves routes + links are missing.</done>
|
|
</task>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 2: Add .ps1 route aliases + printer_detail.html script links</name>
|
|
<files>imptune/api/scripts.py, imptune/templates/printer_detail.html</files>
|
|
<behavior>
|
|
- All 6 tests from Task 1 go GREEN.
|
|
- `pytest tests/ -x -q --ignore=tests/e2e` passes with no regressions.
|
|
- Existing extensionless `/scripts/install` routes continue to work unchanged.
|
|
</behavior>
|
|
<action>
|
|
Step 1 — In `imptune/api/scripts.py`, refactor the three existing handlers to use shared body logic and then add `.ps1` aliases. Minimal-churn approach:
|
|
|
|
```python
|
|
def _install_response(printer_id: int):
|
|
result, error = _get_printer_and_driver(printer_id)
|
|
if error is not None:
|
|
return error
|
|
printer, driver, driver_name = result
|
|
rendered = render_install(
|
|
printer_name=printer.name,
|
|
ip_address=printer.ip_address,
|
|
port_name=printer.port_name,
|
|
driver_name=driver_name,
|
|
inf_filename=driver.inf_filename,
|
|
duplex_mode=printer.duplex_mode,
|
|
color_mode=printer.color_mode,
|
|
paper_size=printer.paper_size,
|
|
collate=printer.collate,
|
|
)
|
|
return PlainTextResponse(
|
|
content=rendered,
|
|
headers={"Content-Disposition": 'attachment; filename="install.ps1"'},
|
|
)
|
|
|
|
@router.get("/{printer_id}/scripts/install")
|
|
def get_install_script(printer_id: int):
|
|
return _install_response(printer_id)
|
|
|
|
@router.get("/{printer_id}/scripts/install.ps1")
|
|
def get_install_script_ps1(printer_id: int):
|
|
return _install_response(printer_id)
|
|
```
|
|
|
|
Repeat for uninstall and detect. Keeps the existing behavior untouched (existing routes still respond 200) while adding the `.ps1` URL shape locked in 09-CONTEXT.md.
|
|
|
|
FastAPI caveat: route paths with a `.` are valid — no special escaping needed. Confirm both routes register by checking `pytest --collect-only` imports scripts.py without error and the OpenAPI path table (if generated) lists both.
|
|
|
|
Step 2 — Edit `imptune/templates/printer_detail.html`. Inside the existing `{% if has_driver %}` block, immediately after the `</div>` closing the Uninstall command block (line 47) and BEFORE `<h2>Export</h2>` (line 48), add:
|
|
```html
|
|
<h2>Scripts</h2>
|
|
<a href="/printers/{{ printer.id }}/scripts/install.ps1" role="button" class="secondary">
|
|
Download Install Script
|
|
</a>
|
|
<a href="/printers/{{ printer.id }}/scripts/uninstall.ps1" role="button" class="secondary">
|
|
Download Uninstall Script
|
|
</a>
|
|
<a href="/printers/{{ printer.id }}/scripts/detect.ps1" role="button" class="secondary">
|
|
Download Detect Script
|
|
</a>
|
|
```
|
|
|
|
Do NOT touch the existing Export section — UX-03 requires scripts "in addition to" package exports.
|
|
|
|
Step 3 — Run tests. All 6 green.
|
|
|
|
Commit: `feat(09-03): add .ps1 script download routes and detail-page links`
|
|
</action>
|
|
<verify>
|
|
<automated>pytest tests/test_script_download.py tests/test_packages.py::TestCommandPreview -x -v && pytest tests/ -x -q --ignore=tests/e2e</automated>
|
|
</verify>
|
|
<done>All new tests green, full non-e2e suite green, printer_detail.html shows 3 script download links alongside existing package export buttons.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `pytest tests/ -x -q --ignore=tests/e2e` green
|
|
- Manual eye check (captured in 09-VALIDATION.md manual section): start app, open a printer detail page with a driver assigned, click each of the 3 download links, confirm `install.ps1` / `uninstall.ps1` / `detect.ps1` files download with correct content
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- UX-03 observable truth achieved: technician on printer detail page clicks 3 direct download links and receives the individual .ps1 files
|
|
- Existing package export buttons remain untouched
|
|
- Existing extensionless script routes still work (backward compatible)
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/09-ux-tech-debt-closure/09-03-SUMMARY.md` documenting: files changed, whether `.ps1` was added as alias or rename (locked decision: alias), test results, link to commits.
|
|
</output>
|