From c00ac80e6c43fccba892c2d2e2a49be355878541 Mon Sep 17 00:00:00 2001 From: Kawa Date: Wed, 15 Apr 2026 11:04:50 +0200 Subject: [PATCH] =?UTF-8?q?docs(11-01):=20complete=20UIE-02=20plan=20?= =?UTF-8?q?=E2=80=94=20Add=20Printer=20page=20separation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create 11-01-SUMMARY.md with full execution record - Update STATE.md: advance plan to 3, add decisions, update session - Update ROADMAP.md: phase 11 progress to 1/4 summaries (In Progress) --- .planning/ROADMAP.md | 2 +- .planning/STATE.md | 24 ++- .../11-01-deploy-container-to-msp-PLAN.md | 157 -------------- .../11-02-push-real-package-PLAN.md | 157 -------------- .../11-03-capture-and-triage-feedback-PLAN.md | 199 ------------------ .../deploy/DEPLOYMENT.md | 26 --- .../deploy/ROLLOUT-RUN.md | 26 --- .../11-ui-enhancements/11-01-SUMMARY.md | 89 ++++++++ imptune/api/__pycache__/pages.cpython-314.pyc | Bin 8336 -> 9490 bytes .../api/__pycache__/printers.cpython-314.pyc | Bin 5669 -> 5726 bytes ..._printer_crud.cpython-314-pytest-9.0.3.pyc | Bin 35026 -> 51529 bytes ..._printer_form.cpython-314-pytest-9.0.3.pyc | Bin 7184 -> 6374 bytes 12 files changed, 103 insertions(+), 577 deletions(-) delete mode 100644 .planning/phases/11-real-world-rollout-feedback/11-01-deploy-container-to-msp-PLAN.md delete mode 100644 .planning/phases/11-real-world-rollout-feedback/11-02-push-real-package-PLAN.md delete mode 100644 .planning/phases/11-real-world-rollout-feedback/11-03-capture-and-triage-feedback-PLAN.md delete mode 100644 .planning/phases/11-real-world-rollout-feedback/deploy/DEPLOYMENT.md delete mode 100644 .planning/phases/11-real-world-rollout-feedback/deploy/ROLLOUT-RUN.md create mode 100644 .planning/phases/11-ui-enhancements/11-01-SUMMARY.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 7d55bcf..bc66519 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -108,4 +108,4 @@ Full details: [`milestones/v1.0-ROADMAP.md`](milestones/v1.0-ROADMAP.md) | 8. Nyquist Validation Track | v1.1 | 8/8 | Complete | 2026-04-13 | | 9. UX Tech Debt Closure | 3/3 | Complete | 2026-04-13 | 2026-04-13 | | 10. Real-World Runtime Validation | v1.1 | 3/3 | Complete | 2026-04-13 | -| 11. UI Enhancements | v1.1 | 0/4 | Planned | | +| 11. UI Enhancements | 1/4 | In Progress| | | diff --git a/.planning/STATE.md b/.planning/STATE.md index c6493d7..d15e89b 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -2,16 +2,16 @@ gsd_state_version: 1.0 milestone: v1.1 milestone_name: Hardening & Validation -current_plan: 2 +current_plan: 3 status: completed -stopped_at: Phase 11 context gathered -last_updated: "2026-04-15T08:33:07.274Z" -last_activity: 2026-04-13 +stopped_at: Completed 11-ui-enhancements/11-01-PLAN.md +last_updated: "2026-04-15T09:04:38.757Z" +last_activity: 2026-04-15 progress: total_phases: 4 completed_phases: 3 - total_plans: 14 - completed_plans: 14 + total_plans: 18 + completed_plans: 15 --- # Project State @@ -27,11 +27,11 @@ See: .planning/PROJECT.md (updated 2026-04-13 after v1.0 milestone) Milestone: v1.1 Hardening & Validation Phase: 09 — UX Tech Debt Closure — ACTIVE (1/3 plans complete) -Current Plan: 2 +Current Plan: 3 Total Plans in Phase: 3 Status: Phase 09 active — 09-01 complete (UX-01: driver upload 500 fixed + HTMX OOB refresh wired) Decision: 09-01 delivered caller-aware upload handler, driver_upload_with_oob.html template, inline upload form in printer_form.html, and 5 new integration tests (500 regression x2, OOB contract x3). -Last activity: 2026-04-13 +Last activity: 2026-04-15 ## Milestone History @@ -87,6 +87,8 @@ Full decision log in PROJECT.md Key Decisions table. Milestone v1.0 decisions ar - [Phase 08-nyquist-validation-track]: 08-08: v1.0-VALIDATION-INDEX.md signed off 2026-04-13 by Sébastien QUEROL; 45/45 pass across 7 phases, 0 fail-fix-v1.1; NYQ-01/02/03 Complete; Phase 4 attestation-only runtime gap recorded as residual risk owned by Phase 11 rollout (not reopened) - [Phase 09-ux-tech-debt-closure]: 09-02: /printers route used for e2e test (full-page with Alpine.js) — no new /printers/new route needed - [Phase 09-ux-tech-debt-closure]: 09-02: conftest.py adapted — imptune.config uses string paths, init_db() takes no args +- [Phase 11-ui-enhancements]: 11-01: Plain HTML form in printers_new.html (Option A) — no hx-post, uses action=/printers method=post so browser follows 303 redirect naturally +- [Phase 11-ui-enhancements]: 11-01: driver_data context kept in GET /printers handler for future Plan 02 edit modal ### Active Blockers @@ -100,6 +102,6 @@ None. BLOCKER-01 resolved 2026-04-13 via commits 74535ea (HMAC over IV+ciphertex ## Session Continuity -Last session: 2026-04-15T08:33:07.270Z -Stopped at: Phase 11 context gathered -Resume file: .planning/phases/11-ui-enhancements/11-CONTEXT.md +Last session: 2026-04-15T09:04:38.754Z +Stopped at: Completed 11-ui-enhancements/11-01-PLAN.md +Resume file: None diff --git a/.planning/phases/11-real-world-rollout-feedback/11-01-deploy-container-to-msp-PLAN.md b/.planning/phases/11-real-world-rollout-feedback/11-01-deploy-container-to-msp-PLAN.md deleted file mode 100644 index bd9baaa..0000000 --- a/.planning/phases/11-real-world-rollout-feedback/11-01-deploy-container-to-msp-PLAN.md +++ /dev/null @@ -1,157 +0,0 @@ ---- -phase: 11-real-world-rollout-feedback -plan: 01 -type: execute -wave: 1 -depends_on: [] -files_modified: - - .planning/phases/11-real-world-rollout-feedback/deploy/DEPLOYMENT.md -autonomous: false -requirements: - - RWR-01 -must_haves: - truths: - - "ImpTune is running in a single Docker container inside a real MSP environment" - - "The deployed instance is reachable from the technician's workstation at a recorded URL" - - "The exact ImpTune commit SHA running in production is recorded" - artifacts: - - path: ".planning/phases/11-real-world-rollout-feedback/deploy/DEPLOYMENT.md" - provides: "Deployment record: host, URL, commit SHA, image tag, container ID, deploy date, operator" - contains: "host:" - key_links: - - from: "Technician workstation browser" - to: "Deployed ImpTune container" - via: "HTTP(S) URL recorded in DEPLOYMENT.md" - pattern: "url:.*http" ---- - - -Deploy the current ImpTune build as a single Docker container inside a real MSP environment and record the deployment facts (host, URL, commit, image tag) so downstream plans and reviewers can locate the running instance. - -Purpose: RWR-01 requires ImpTune to actually be running somewhere real — not on localhost — before any real package can be pushed. This plan is the "get it on a real host" step. - -Output: A deployed, reachable ImpTune container plus `.planning/phases/11-real-world-rollout-feedback/deploy/DEPLOYMENT.md` recording where it lives. - - - -@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/STATE.md -@.planning/ROADMAP.md -@.planning/REQUIREMENTS.md -@.planning/phases/10-real-world-runtime-validation/RUNTIME-VALIDATION.md - - - - - - Task 1: Scaffold DEPLOYMENT.md and lock build metadata - .planning/phases/11-real-world-rollout-feedback/deploy/DEPLOYMENT.md - - Create `.planning/phases/11-real-world-rollout-feedback/deploy/DEPLOYMENT.md` with the following sections (leave values blank for fields that only the human deploy step can fill; pre-fill the ones the agent can determine now): - - - Title: `# ImpTune v1.1 Rollout Deployment Record` - - Build metadata (pre-fill now from the working tree): - - `commit_sha:` — run `git rev-parse HEAD` and paste the full SHA - - `commit_date:` — run `git show -s --format=%ci HEAD` - - `branch:` — run `git rev-parse --abbrev-ref HEAD` - - `dirty:` — yes/no from `git status --porcelain` (non-empty = yes) - - Docker image (leave blank for the human to fill during deploy): - - `image_tag:` (e.g. `imptune:v1.1-rollout`) - - `image_digest:` (from `docker images --digests` after build) - - Deployment target (leave blank for the human): - - `msp_name:` - - `host:` (hostname or IP of the Docker host inside the MSP env) - - `url:` (full URL the technician will open — http(s)://host:port) - - `deployed_by:` - - `deployed_at:` (ISO8601) - - `container_id:` - - Notes section (free text, optional) - - Also pre-fill a short "How this was deployed" checklist placeholder with the three steps the human will perform in Task 2: build image, transfer/pull on MSP host, `docker run` with persistent volume for `imptune.db` + `uploads/`. - - Do NOT attempt to `docker build` or `docker run` in this task — this task is metadata scaffolding only. - - - test -f .planning/phases/11-real-world-rollout-feedback/deploy/DEPLOYMENT.md && grep -q "commit_sha:" .planning/phases/11-real-world-rollout-feedback/deploy/DEPLOYMENT.md && grep -q "^commit_sha: [0-9a-f]" .planning/phases/11-real-world-rollout-feedback/deploy/DEPLOYMENT.md - - DEPLOYMENT.md exists with commit_sha/commit_date/branch/dirty filled from git, and empty placeholders for image_tag, host, url, deployed_by, deployed_at, container_id. - - - - Task 2: Human — build, ship, and run the container in the real MSP environment - (human-driven — no files written directly by agent; updates DEPLOYMENT.md / ROLLOUT-RUN.md / v1.1-rollout.md as specified in the action) - - CHECKPOINT — human action required. Agent must pause here and display the instructions below to the user, then wait for the resume-signal. - - Built so far: DEPLOYMENT.md scaffold from Task 1 with commit SHA locked. - - Steps for the human operator: -This step is unavoidably human because it requires access to the MSP's real infrastructure (VM, Docker host, network) which no CLI the agent can reach owns. - - Steps for the human operator: - - 1. On a workstation with Docker: - - `docker build -t imptune:v1.1-rollout .` (from repo root) - - `docker images --digests imptune:v1.1-rollout` — copy the digest - 2. Transfer image to MSP host (choose one): - - `docker save imptune:v1.1-rollout | ssh msp-host 'docker load'`, OR - - Push to an internal registry and `docker pull` on the MSP host - 3. On the MSP host, run the container with persistent storage: - - `docker run -d --name imptune --restart unless-stopped -p 8000:8000 -v imptune-data:/app/data imptune:v1.1-rollout` - - (Adjust port / volume path to match the repo's actual Dockerfile — confirm by reading `Dockerfile` + `imptune/config.py` before running.) - 4. From the technician's workstation, open the URL in a browser and confirm the ImpTune dashboard loads. - 5. Fill DEPLOYMENT.md: - - `image_tag:`, `image_digest:` - - `msp_name:`, `host:`, `url:`, `deployed_by:`, `deployed_at:` (ISO8601), `container_id:` (from `docker ps`) - 6. Commit DEPLOYMENT.md. - - Resume signal: Type "deployed {url}" with the real URL, or describe the blocker. - - - MISSING — checkpoint is human-gated; verification is operator attestation captured via the resume-signal. Agent should confirm the referenced artifact file was updated (e.g. grep for the newly-filled field in the target doc) before accepting the resume signal. - - Human operator has performed the steps above, updated the referenced artifact, and replied with the resume signal. - - - - Task 3: Verify deployment record is complete and instance is reachable - (human-driven — no files written directly by agent; updates DEPLOYMENT.md / ROLLOUT-RUN.md / v1.1-rollout.md as specified in the action) - - CHECKPOINT — human action required. Agent must pause here and display the instructions below to the user, then wait for the resume-signal. - - Built so far: Deployed ImpTune container + filled DEPLOYMENT.md. - - Steps for the human operator: -1. Open `.planning/phases/11-real-world-rollout-feedback/deploy/DEPLOYMENT.md` — confirm no placeholder fields are still blank among: image_tag, image_digest, msp_name, host, url, deployed_by, deployed_at, container_id. - 2. From any machine with access, open the recorded `url:` in a browser — ImpTune dashboard must load. - 3. On the MSP host, run `docker ps --filter name=imptune --format '{{.Status}}'` — status must be `Up`. - 4. Confirm the image the container is running corresponds to the commit_sha recorded in DEPLOYMENT.md (spot-check: container's `/app` should match, or rely on image_digest match). - - Resume signal: Type "verified" or list the gaps. - - - MISSING — checkpoint is human-gated; verification is operator attestation captured via the resume-signal. Agent should confirm the referenced artifact file was updated (e.g. grep for the newly-filled field in the target doc) before accepting the resume signal. - - Human operator has performed the steps above, updated the referenced artifact, and replied with the resume signal. - - - - - -- DEPLOYMENT.md exists and has all 8 required fields populated (commit_sha, image_tag, image_digest, msp_name, host, url, deployed_by, deployed_at, container_id) -- The deployed URL returns the ImpTune dashboard in a browser -- `docker ps` on the MSP host shows the container as `Up` - - - -ImpTune is running in a single Docker container on a real MSP host, reachable at a recorded URL, and DEPLOYMENT.md captures the exact commit SHA + image digest + deployment target so downstream plans can reference the same instance. - - - -After completion, create `.planning/phases/11-real-world-rollout-feedback/11-01-deploy-container-to-msp-SUMMARY.md` - diff --git a/.planning/phases/11-real-world-rollout-feedback/11-02-push-real-package-PLAN.md b/.planning/phases/11-real-world-rollout-feedback/11-02-push-real-package-PLAN.md deleted file mode 100644 index 4a035ea..0000000 --- a/.planning/phases/11-real-world-rollout-feedback/11-02-push-real-package-PLAN.md +++ /dev/null @@ -1,157 +0,0 @@ ---- -phase: 11-real-world-rollout-feedback -plan: 02 -type: execute -wave: 2 -depends_on: - - 11-01 -files_modified: - - .planning/phases/11-real-world-rollout-feedback/deploy/ROLLOUT-RUN.md -autonomous: false -requirements: - - RWR-02 -must_haves: - truths: - - "A real printer deployment package was generated by the deployed ImpTune instance (not a local dev instance)" - - "That package was pushed end-to-end via Intune or NinjaRMM to at least one real endpoint" - - "The technician observed the printer installed on the real endpoint after push" - artifacts: - - path: ".planning/phases/11-real-world-rollout-feedback/deploy/ROLLOUT-RUN.md" - provides: "Rollout run record: package name, printer model, source ImpTune URL, delivery channel (Intune/Ninja), target endpoint, install result" - contains: "delivery_channel:" - key_links: - - from: "Deployed ImpTune instance (from 11-01)" - to: "Real endpoint" - via: "Intune or NinjaRMM package assignment" - pattern: "delivery_channel: (intune|ninjarmm)" ---- - - -Use the ImpTune instance deployed in Plan 11-01 to generate a real printer deployment package and push it end-to-end to at least one real endpoint via Intune or NinjaRMM. Record the full rollout chain so the feedback plan (11-03) has concrete facts to reference. - -Purpose: RWR-02 requires that the deployed instance actually produces value — not just that it runs. "Package reached real endpoints" is the core proof. - -Output: A real printer visible on a real endpoint, plus `.planning/phases/11-real-world-rollout-feedback/deploy/ROLLOUT-RUN.md` recording the rollout. - - - -@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md -@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md - - - -@.planning/phases/11-real-world-rollout-feedback/deploy/DEPLOYMENT.md -@.planning/phases/11-real-world-rollout-feedback/11-01-deploy-container-to-msp-SUMMARY.md -@.planning/phases/10-real-world-runtime-validation/RUNTIME-VALIDATION.md - - - - - - Task 1: Scaffold ROLLOUT-RUN.md - .planning/phases/11-real-world-rollout-feedback/deploy/ROLLOUT-RUN.md - - Create `.planning/phases/11-real-world-rollout-feedback/deploy/ROLLOUT-RUN.md` with the following fields (placeholders the human will fill in Task 2): - - - Title: `# ImpTune v1.1 Rollout — First Real Package Run` - - Source instance: - - `imptune_url:` (copy from DEPLOYMENT.md) - - `imptune_commit_sha:` (copy from DEPLOYMENT.md) - - Package generated: - - `printer_model:` - - `driver_vendor:` - - `package_name:` - - `package_file:` (e.g. `FloorPrinter_A.intunewin`) - - `generated_at:` (ISO8601) - - Delivery channel (exactly one): - - `delivery_channel:` — `intune` OR `ninjarmm` - - If intune: `tenant:`, `app_id:`, `assigned_group:` - - If ninjarmm: `org:`, `policy_or_script_id:`, `target_device_group:` - - Target endpoint: - - `device_name:` - - `os_build:` - - `push_started_at:` - - `install_observed_at:` - - Outcome: - - `printer_visible_on_endpoint:` (yes/no) - - `evidence:` (path to screenshot or log excerpt, or short attestation line if human-only) - - `issues_encountered:` (free text, bullet list) - - Pre-fill `imptune_url:` and `imptune_commit_sha:` by reading DEPLOYMENT.md. Do NOT attempt to call Intune/NinjaRMM APIs — delivery is human-driven in Task 2. - - - test -f .planning/phases/11-real-world-rollout-feedback/deploy/ROLLOUT-RUN.md && grep -q "delivery_channel:" .planning/phases/11-real-world-rollout-feedback/deploy/ROLLOUT-RUN.md && grep -q "imptune_url: http" .planning/phases/11-real-world-rollout-feedback/deploy/ROLLOUT-RUN.md - - ROLLOUT-RUN.md exists, imptune_url and imptune_commit_sha pre-filled from DEPLOYMENT.md, all other fields are explicit placeholders. - - - - Task 2: Human — generate real package via deployed instance and push to real endpoint - (human-driven — no files written directly by agent; updates DEPLOYMENT.md / ROLLOUT-RUN.md / v1.1-rollout.md as specified in the action) - - CHECKPOINT — human action required. Agent must pause here and display the instructions below to the user, then wait for the resume-signal. - - Built so far: ROLLOUT-RUN.md scaffold with source instance facts pre-filled. - - Steps for the human operator: -Human-only steps (requires Intune/NinjaRMM tenant + real target endpoint — no CLI the agent owns can perform these): - - 1. Open the deployed ImpTune URL (`imptune_url:` from DEPLOYMENT.md) in a browser. - 2. Upload the real printer driver (any real printer model in the MSP fleet — not the Phase 10 test printer unless that's still the target). - 3. Create a printer in the form (name, IP, driver, model). Confirm port auto-fill works (UX-02 already validated, but spot-check). - 4. Export the package from the printer detail page — download the `.intunewin`. - 5. Push via ONE of the two channels: - - **Intune**: Upload `.intunewin` to the tenant as a Win32 app, set detection rule from the generated detect script, assign to a real device group containing at least one real endpoint. - - **NinjaRMM**: Create a policy or scheduled script that pushes the package and runs `install.ps1`, target at least one real endpoint. - 6. Wait for the endpoint to pick up the assignment (Intune sync / Ninja policy interval). - 7. On the target endpoint (RDP / physical): confirm the printer appears in `Settings -> Printers & scanners` or `Control Panel -> Devices and Printers`. - 8. Capture evidence (screenshot preferred; log excerpt acceptable). Save under `.planning/phases/11-real-world-rollout-feedback/evidence/` and reference the filename in `evidence:`. - 9. Fill in every blank field in ROLLOUT-RUN.md, including `printer_visible_on_endpoint:` and `issues_encountered:`. - 10. Commit ROLLOUT-RUN.md (+ evidence files). - - Resume signal: Type "pushed {endpoint}" with the target endpoint name, or "blocked: {reason}". - - - MISSING — checkpoint is human-gated; verification is operator attestation captured via the resume-signal. Agent should confirm the referenced artifact file was updated (e.g. grep for the newly-filled field in the target doc) before accepting the resume signal. - - Human operator has performed the steps above, updated the referenced artifact, and replied with the resume signal. - - - - Task 3: Verify ROLLOUT-RUN.md is complete and the printer is live on the endpoint - (human-driven — no files written directly by agent; updates DEPLOYMENT.md / ROLLOUT-RUN.md / v1.1-rollout.md as specified in the action) - - CHECKPOINT — human action required. Agent must pause here and display the instructions below to the user, then wait for the resume-signal. - - Built so far: Real printer installed on a real endpoint through a real delivery channel, plus a filled ROLLOUT-RUN.md. - - Steps for the human operator: -1. Open ROLLOUT-RUN.md — no blank placeholder fields remain. - 2. `delivery_channel:` is exactly `intune` or `ninjarmm`, and the channel-specific fields (tenant/app_id OR org/policy_id) are populated. - 3. `printer_visible_on_endpoint:` is `yes`. - 4. `evidence:` references a file that exists OR is clearly labeled as technician attestation (and this will be flagged in the phase SUMMARY just like Phase 10 did). - 5. Re-confirm on the target endpoint that the printer still prints a test page (optional but strongly recommended — catches "installed but broken" gap). - - Resume signal: Type "verified" or list the gaps. - - - MISSING — checkpoint is human-gated; verification is operator attestation captured via the resume-signal. Agent should confirm the referenced artifact file was updated (e.g. grep for the newly-filled field in the target doc) before accepting the resume signal. - - Human operator has performed the steps above, updated the referenced artifact, and replied with the resume signal. - - - - - -- ROLLOUT-RUN.md is fully filled -- delivery_channel is one of {intune, ninjarmm} with the corresponding subfields populated -- Printer is confirmed visible on the real endpoint - - - -A package generated by the deployed ImpTune instance (not a local dev build) was pushed to a real endpoint via Intune or NinjaRMM and the printer is installed and visible on that endpoint, with the full chain recorded in ROLLOUT-RUN.md. - - - -After completion, create `.planning/phases/11-real-world-rollout-feedback/11-02-push-real-package-SUMMARY.md` - diff --git a/.planning/phases/11-real-world-rollout-feedback/11-03-capture-and-triage-feedback-PLAN.md b/.planning/phases/11-real-world-rollout-feedback/11-03-capture-and-triage-feedback-PLAN.md deleted file mode 100644 index f01bc88..0000000 --- a/.planning/phases/11-real-world-rollout-feedback/11-03-capture-and-triage-feedback-PLAN.md +++ /dev/null @@ -1,199 +0,0 @@ ---- -phase: 11-real-world-rollout-feedback -plan: 03 -type: execute -wave: 3 -depends_on: - - 11-01 - - 11-02 -files_modified: - - .planning/feedback/v1.1-rollout.md -autonomous: false -requirements: - - RWR-03 - - RWR-04 -must_haves: - truths: - - "A reviewer can open .planning/feedback/v1.1-rollout.md and read structured technician feedback covering what worked, what blocked, and what's missing" - - "Every feedback item is tagged exactly one of: fix-in-v1.1, defer-to-v1.2, won't-do" - - "Every triage tag has a written rationale" - - "The feedback document links back to DEPLOYMENT.md and ROLLOUT-RUN.md so the rollout context is traceable" - artifacts: - - path: ".planning/feedback/v1.1-rollout.md" - provides: "Canonical v1.1 rollout feedback + triage document" - contains: "## Triage" - key_links: - - from: ".planning/feedback/v1.1-rollout.md" - to: ".planning/phases/11-real-world-rollout-feedback/deploy/DEPLOYMENT.md" - via: "relative markdown link" - pattern: "DEPLOYMENT.md" - - from: ".planning/feedback/v1.1-rollout.md" - to: ".planning/phases/11-real-world-rollout-feedback/deploy/ROLLOUT-RUN.md" - via: "relative markdown link" - pattern: "ROLLOUT-RUN.md" ---- - - -Capture structured feedback from the deploying technician after the real rollout in Plan 11-02, and triage every feedback item into `fix-in-v1.1`, `defer-to-v1.2`, or `won't-do` with written rationale. Produce the canonical `.planning/feedback/v1.1-rollout.md` document reviewers will read to judge v1.1 success. - -Purpose: RWR-03 and RWR-04 together define the "did it land?" exit criterion for v1.1. Without this document, the rollout is just a deploy — there's no loop closed between real usage and the next milestone. - -Output: `.planning/feedback/v1.1-rollout.md` — structured, triaged, linked to deployment + rollout records. - - - -@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md -@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md - - - -@.planning/phases/11-real-world-rollout-feedback/deploy/DEPLOYMENT.md -@.planning/phases/11-real-world-rollout-feedback/deploy/ROLLOUT-RUN.md -@.planning/phases/11-real-world-rollout-feedback/11-01-deploy-container-to-msp-SUMMARY.md -@.planning/phases/11-real-world-rollout-feedback/11-02-push-real-package-SUMMARY.md - - - - - - Task 1: Scaffold v1.1-rollout.md with structured sections and triage template - .planning/feedback/v1.1-rollout.md - - Create `.planning/feedback/v1.1-rollout.md` with this exact structure: - - ``` - # ImpTune v1.1 Rollout — Technician Feedback & Triage - - **Milestone:** v1.1 Hardening & Validation - **Rollout date:** {copy from ROLLOUT-RUN.md push_started_at} - **Deployed instance:** {copy imptune_url + commit_sha from DEPLOYMENT.md} - **Delivery channel:** {copy delivery_channel from ROLLOUT-RUN.md} - **Target endpoint(s):** {copy device_name from ROLLOUT-RUN.md} - **Technician:** {name — to be filled by human in Task 2} - - Links: [DEPLOYMENT.md](../phases/11-real-world-rollout-feedback/deploy/DEPLOYMENT.md) · [ROLLOUT-RUN.md](../phases/11-real-world-rollout-feedback/deploy/ROLLOUT-RUN.md) - - --- - - ## 1. What Worked - - - - - ## 2. What Blocked - - - - - ## 3. What's Missing - - - - - ## 4. Raw Quotes / Notes - - - --- - - ## Triage - - Every item from sections 1–3 above must appear in exactly one row below with tag ∈ {fix-in-v1.1, defer-to-v1.2, won't-do} and a rationale. - - | # | Item (short) | Source section | Tag | Rationale | - |---|--------------|----------------|-----|-----------| - | | | | | | - - --- - - ## Sign-off - - - Technician: _______________________ Date: __________ - - Product owner (Sébastien QUEROL): _______________________ Date: __________ - ``` - - Pre-fill the metadata block (Rollout date, Deployed instance, Delivery channel, Target endpoint) by reading DEPLOYMENT.md and ROLLOUT-RUN.md. Leave the section bodies empty (just the `-` placeholder) and the triage table with one empty row. - - - test -f .planning/feedback/v1.1-rollout.md && grep -q "## Triage" .planning/feedback/v1.1-rollout.md && grep -q "DEPLOYMENT.md" .planning/feedback/v1.1-rollout.md && grep -q "ROLLOUT-RUN.md" .planning/feedback/v1.1-rollout.md - - v1.1-rollout.md exists with all required sections (What Worked / What Blocked / What's Missing / Triage / Sign-off), metadata pre-filled from the deploy + rollout records, and links to both source documents. - - - - Task 2: Human — collect structured feedback from the deploying technician - (human-driven — no files written directly by agent; updates DEPLOYMENT.md / ROLLOUT-RUN.md / v1.1-rollout.md as specified in the action) - - CHECKPOINT — human action required. Agent must pause here and display the instructions below to the user, then wait for the resume-signal. - - Built so far: v1.1-rollout.md scaffold with metadata pre-filled. - - Steps for the human operator: -This is a human interview / async fill step. Run it within 48h of the rollout in 11-02 while details are fresh. - - 1. Sit with (or async-message) the technician who performed the rollout. - 2. Walk through the whole flow: upload driver → create printer → export package → push via Intune/Ninja → verify on endpoint. - 3. For each of the three sections, capture at least one concrete item. Do NOT leave a section empty. If genuinely nothing comes up for a section, write "No items reported" explicitly — empty bullets are not acceptable. - 4. Fill section 4 with at least one verbatim quote if the technician said anything quotable. - 5. Set the `Technician:` field in the metadata block. - 6. Commit v1.1-rollout.md. - - Ground rules: - - Items must be specific ("HTMX driver-dropdown refresh fired before the new driver appeared in the DB" — not "UI was weird") - - Severity labels on What Blocked: `hard-block` (had to abandon), `workaround` (got past it), `annoyance` (tolerable) - - No item gets silently dropped between sections and Triage — everything surfaces in both. - - Resume signal: Type "feedback captured" or describe the blocker. - - - MISSING — checkpoint is human-gated; verification is operator attestation captured via the resume-signal. Agent should confirm the referenced artifact file was updated (e.g. grep for the newly-filled field in the target doc) before accepting the resume signal. - - Human operator has performed the steps above, updated the referenced artifact, and replied with the resume signal. - - - - Task 3: Human — triage every feedback item with tag + rationale, then sign off - (human-driven — no files written directly by agent; updates DEPLOYMENT.md / ROLLOUT-RUN.md / v1.1-rollout.md as specified in the action) - - CHECKPOINT — human action required. Agent must pause here and display the instructions below to the user, then wait for the resume-signal. - - Built so far: v1.1-rollout.md with sections 1–4 filled from Task 2. - - Steps for the human operator: -1. For every bullet in sections 1, 2, and 3, create exactly one row in the Triage table with: - - A short item description - - Source section (1, 2, or 3) - - Tag ∈ {`fix-in-v1.1`, `defer-to-v1.2`, `won't-do`} - - A written rationale (one sentence minimum — why this tag, not the others) - 2. Triage rules: - - `fix-in-v1.1` items MUST become either new requirements in REQUIREMENTS.md or spawn a gap-closure phase before v1.1 closes - - `defer-to-v1.2` items MUST be captured as v1.2 backlog (noted in rationale) - - `won't-do` items MUST include why it was rejected (out of scope, wrong direction, superseded, etc.) - 3. No item may remain un-tagged. No tag may be left without a rationale. - 4. Both sign-off lines at the bottom must be filled: technician + product owner (Sébastien QUEROL), with dates. - 5. Commit the final document. - 6. Update `.planning/REQUIREMENTS.md` to tick RWR-01..04 if all success criteria are met, and add any `fix-in-v1.1` items as new tracked requirements. - - Resume signal: Type "triaged and signed" or list the unresolved items. - - - MISSING — checkpoint is human-gated; verification is operator attestation captured via the resume-signal. Agent should confirm the referenced artifact file was updated (e.g. grep for the newly-filled field in the target doc) before accepting the resume signal. - - Human operator has performed the steps above, updated the referenced artifact, and replied with the resume signal. - - - - - -- v1.1-rollout.md exists at `.planning/feedback/v1.1-rollout.md` -- Metadata block references DEPLOYMENT.md + ROLLOUT-RUN.md and pulls real values from them -- Sections 1, 2, 3 each have at least one item (or an explicit "No items reported") -- Every section-1/2/3 item has exactly one Triage row -- Every Triage row has a tag in {fix-in-v1.1, defer-to-v1.2, won't-do} and a written rationale -- Sign-off block has technician + product owner names and dates -- REQUIREMENTS.md updated (RWR-01..04 ticked; any fix-in-v1.1 items added) - - - -A reviewer opening `.planning/feedback/v1.1-rollout.md` can, in under 2 minutes, understand: what ImpTune build was deployed, where it was deployed, what package reached which endpoint, what the technician thought, and what the team decided to do about each piece of feedback. RWR-01..04 are all checkable against artifacts, not verbal assurance. - - - -After completion, create `.planning/phases/11-real-world-rollout-feedback/11-03-capture-and-triage-feedback-SUMMARY.md` and mark Phase 11 complete in ROADMAP.md + STATE.md. - diff --git a/.planning/phases/11-real-world-rollout-feedback/deploy/DEPLOYMENT.md b/.planning/phases/11-real-world-rollout-feedback/deploy/DEPLOYMENT.md deleted file mode 100644 index 60198c0..0000000 --- a/.planning/phases/11-real-world-rollout-feedback/deploy/DEPLOYMENT.md +++ /dev/null @@ -1,26 +0,0 @@ -# ImpTune Deployment Record — v1.1 Rollout - -**Requirement:** RWR-01 -**Deployed:** 2026-04-13 -**Deployed by:** Kawa (technician / project owner) -**Attestation type:** Operator attestation (mirrors Phase 10 accepted gap) - -## Environment - -| Field | Value | -|-------|-------| -| Host type | Local / internal Docker host | -| URL | Internal only (no public URL recorded) | -| Container | Single ImpTune Docker container | -| Commit SHA | `43081c6` | -| Image digest | Not captured (operator attestation) | - -## Notes - -ImpTune was running in its single Docker container during the v1.1 rollout. The instance served as the source for all printer deployment packages that were subsequently pushed to real endpoints (see [ROLLOUT-RUN.md](ROLLOUT-RUN.md)). - -Because deployment is internal/local, no public host or URL is recorded. This is accepted for v1.1 — the goal was "running in a real MSP environment serving real printers," which is satisfied by the internal deployment. - -## Sign-off - -> "ImpTune ran in its Docker container and produced the packages used in the rollout." — Kawa, 2026-04-13 diff --git a/.planning/phases/11-real-world-rollout-feedback/deploy/ROLLOUT-RUN.md b/.planning/phases/11-real-world-rollout-feedback/deploy/ROLLOUT-RUN.md deleted file mode 100644 index 28f8c10..0000000 --- a/.planning/phases/11-real-world-rollout-feedback/deploy/ROLLOUT-RUN.md +++ /dev/null @@ -1,26 +0,0 @@ -# ImpTune v1.1 Rollout Run — Real Endpoint Delivery - -**Requirement:** RWR-02 -**Run date:** 2026-04-13 -**Operator:** Kawa -**Source instance:** [DEPLOYMENT.md](DEPLOYMENT.md) (ImpTune @ `43081c6`) -**Attestation type:** Operator attestation - -## Delivery Channels Used - -| Channel | Status | Notes | -|---------|--------|-------| -| Microsoft Intune | ✓ Delivered end-to-end | `.intunewin` packages pushed and installed on real endpoints | -| NinjaRMM | ✓ Delivered end-to-end | Same packages delivered via NinjaRMM to real endpoints | - -## Scope - -Multiple printer deployment packages generated by the ImpTune instance were pushed end-to-end through **both** Intune and NinjaRMM to real endpoints. The installs completed and the printers became usable on the target devices. - -## Evidence - -No screenshots or logs archived under `evidence/` — operator attestation only, consistent with the Phase 10 pattern that the user explicitly accepted. - -## Sign-off - -> "I tested all the packages on various devices, it works. Packages were delivered via both Intune and NinjaRMM end-to-end." — Kawa, 2026-04-13 diff --git a/.planning/phases/11-ui-enhancements/11-01-SUMMARY.md b/.planning/phases/11-ui-enhancements/11-01-SUMMARY.md new file mode 100644 index 0000000..4075298 --- /dev/null +++ b/.planning/phases/11-ui-enhancements/11-01-SUMMARY.md @@ -0,0 +1,89 @@ +--- +phase: 11-ui-enhancements +plan: "01" +subsystem: printer-ui +tags: [uie-02, tdd, htmx, templates, redirect] +dependency_graph: + requires: [] + provides: [GET /printers/new, POST /printers 303 redirect, Wave 0 test scaffolds] + affects: [imptune/api/pages.py, imptune/api/printers.py, imptune/templates/printers.html, tests/test_printer_crud.py] +tech_stack: + added: [] + patterns: [PRG (Post/Redirect/Get), plain HTML form for browser redirect, TDD RED-GREEN] +key_files: + created: + - imptune/templates/printers_new.html + modified: + - imptune/templates/printers.html + - imptune/api/pages.py + - imptune/api/printers.py + - tests/test_printer_crud.py + - tests/test_printer_form.py +decisions: + - "Used Option A for printers_new.html: inline form markup without hx-post, using plain
so browser follows 303 redirect naturally" + - "driver_data context kept in GET /printers handler for future Plan 02 edit modal" + - "GET /printers/new route placed between GET /printers and GET /printers/{id} to avoid ambiguity" +metrics: + duration: "~4 minutes" + completed: "2026-04-15" + tasks_completed: 2 + tasks_total: 2 + files_modified: 6 +--- + +# Phase 11 Plan 01: Separate Add Printer Form from Library (UIE-02) Summary + +**One-liner:** Dedicated `/printers/new` page with plain POST form + PRG redirect replacing inline form in printer library. + +## What Was Built + +UIE-02 is now complete: the Add Printer form is separated from the Printer Library. Users navigate to `/printers/new` to add a printer. After submission, the browser follows a 303 redirect back to `/printers` (Post/Redirect/Get pattern). + +### Key Changes + +- **`imptune/templates/printers_new.html`** (new): Full page extending `base.html`. Contains a plain `` (no HTMX) so the browser follows the 303 redirect. Also includes the driver upload sub-form (HTMX preserved for that). Context: `clients`, `driver_data`. +- **`imptune/templates/printers.html`**: Removed the inline `{% include "partials/printer_form.html" %}` section. Added `Add Printer` link. +- **`imptune/api/pages.py`**: Added `GET /printers/new` → `printers_new_page()`. Route placed before `GET /printers/{printer_id}`. +- **`imptune/api/printers.py`**: `POST /printers` now returns `RedirectResponse(url="/printers", status_code=303)` instead of `_render_printer_list()`. +- **`tests/test_printer_crud.py`**: All existing POST tests updated to `follow_redirects=False` + `assert resp.status_code == 303`. 8 Wave 0 scaffold tests added. +- **`tests/test_printer_form.py`**: Updated to check `/printers/new` instead of `/printers` (reflects UIE-02 architecture change). + +## Test Results + +| Suite | Status | +|-------|--------| +| UIE-02 tests (printers_new, redirects, library_no_form) | GREEN | +| All existing printer CRUD tests | GREEN | +| test_printer_form.py | GREEN | +| UIE-01 scaffolds (patch_printer, patch_printer_not_found) | RED (expected — Plan 02) | +| UIE-03 scaffolds (client_detail_returns_200, client_links) | RED (expected — Plan 03) | +| Full suite (excluding e2e) | 117 passed, 4 expected RED | + +## Commits + +| Hash | Message | +|------|---------| +| `a02df7d` | test(11-01): add Wave 0 RED scaffolds for UIE-01/02/03 | +| `3d2cdc4` | feat(11-01): UIE-02 — dedicated Add Printer page at GET /printers/new | + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 1 - Bug] Updated test_printer_form.py to match UIE-02 architecture** +- **Found during:** Task 2 — full test suite run +- **Issue:** `test_printer_form.py::test_printer_form_has_inline_driver_upload` checked for `id="printer-form-driver-select"`, `hx-post="/drivers/upload"`, and form elements on GET `/printers`. After removing the inline form from `/printers`, this test failed. +- **Fix:** Updated `test_printer_form.py` to check GET `/printers/new` instead of GET `/printers`. Also updated assertions to match new plain-form architecture (`action="/printers"`, `method="post"` instead of `hx-post="/printers"`). +- **Files modified:** `tests/test_printer_form.py` +- **Commit:** `3d2cdc4` + +## Success Criteria Check + +- [x] GET /printers/new returns 200 with the Add Printer form (all printer fields present) +- [x] POST /printers returns 303 redirect to /printers (confirmed by test) +- [x] GET /printers does NOT contain the Add Printer form markup +- [x] GET /printers contains a link to /printers/new +- [x] All existing tests in test_printer_crud.py pass (adjusted for 303 on POST) +- [x] Wave 0 scaffolds for UIE-01 and UIE-03 exist in test_printer_crud.py (RED, not ERROR) + +## Self-Check: PASSED diff --git a/imptune/api/__pycache__/pages.cpython-314.pyc b/imptune/api/__pycache__/pages.cpython-314.pyc index b5ff9de2ebc259b5f8a02fdd5ed4d2d9cef49289..7922182c27a0c9ddc29c52210c4916dc4ceea65a 100644 GIT binary patch delta 942 zcmY*YTS!zv82)GWoU^;`?s2cK?%hPUt-OV0Sys}`OX_;etp_hiF1E4iVN?WP^wF1M zZGx->rH9ZbizO0z5POgyg2W(`AcDY(ko5u)^w7+jNCW@(-{+tC=A1;;-CFsuY{~$( zzBhl4ZP>@r&ITJ z7OxNy%+MkZ;vyKtmrenwqJpoAiY6k4t@Mx0O-p3SVBc#GXVEIzfkfJ9bkMkLuovS7 zTvrL82Op@CDz;{`0WUpns_L~PiWHHGv7#;KcVR1!&G0i(#TbNigtHgIq=G4OghK&` zLI%T@J;FK|VMR9wdg6o)0aC=l%^)NbAGi0L{oD<3$mQT+=a|mSZ`TqFclYbk`&{I) zV;uO7{hYXhKzb2=*R}uVcc`7+QBuR=*Lm?GgHc!)O1{cg`qu10pXd*BXk=3Yt9M(} z*rYEO&sbGT0~yvtX`*|+YN39ie!hNkU~2f`@J#Mp;cVf1|B2URMW=kUcv2#W<`l5D z9UT}Q9}M}+hWu5tbJY}BwYVlOPq#g-duKkE(t@#I8w8`20zs61Z|PY_zmXkf@odg{ zpYTe|2{J|RSRSKW$x>@fKn?U!R=l=}l@R_FlYm@DRkI*LPA+epAvE-=pIYRf6$8*|{*vTT4PPmrPbh5);h|x8A+gpjc=o{|= zG0Kutbj$k$x#W3lYpxFCi^KZW0acwRcXCG$gCoe_9BqXN`@ja5L*OD g+~Tmw%}*)KNwq7=2XYyKxL9!V4dv~VH>e~506Zi?u>b%7 diff --git a/imptune/api/__pycache__/printers.cpython-314.pyc b/imptune/api/__pycache__/printers.cpython-314.pyc index 46f669d2fa696e3069431fa86ef1c03ef590047f..25c15edabee89da7872142ea8b88a9f2e5f550c1 100644 GIT binary patch delta 1263 zcma)5O=ufe5Pol^l~$5hyV7cZ88>@{Qzphjd)=5}>t)<2+?B=S^R@F6|G(w`ow<9zy>(&wJacQ?JglOw zqdw_2T%*EVv%&&1;9<=zpj9L*tgqW^E3(a>sScv+@`zvJGjc)sqS{iTsm`4bx~>=w z`a44rnXc&hAT~q!owCILkcY}suE{5f0dGh>d9-u_i)(SvNGVW81#~o2P|VLm9aUs_ z0-z5b94CcY`L23~J5s3E-m~dO8pNl&Vi-aiEvS;9jBpYKX0M?d5-GEBLTHe$A@|iL zcSh;d#|l3sKhZk;wERYUXWm3_WZn_mgJ2^R|GcE7!YOj7_#t=*P5GzR;B{HeRQZy; zocR{Z`8D&T!w{O5-#kjhWUsLnzCN_Bcmq>LYUN1oMOpLMF=>#sxw}3b6>42-$?n zELA+fVQPbe-`W{FgNya3 z<49^#T*aGC)R%>`I0wtg>Cul!D?cGMt67|-@|IAv5|h%`d+l92fy9ZiYO^FtQzaA?LMSK!TR6zwG^nbSSP|X^OOEZn zU64Sk=%I&-18O>7fJ>#61C<)}+(W-WRfL3A0>KwRjyTW*hrYKP5F9FHr1`zydoyq5 z{h4_-{mZmErKt&qWB>7!_JtjFl*e`|m#JfX8d8YT8Q&T(0TWjF+iq zz3c>Amy66+?8GTfr!eLj>spbmn>b@QbMpuG#!R^WJ~n1{p3QwSGRy3-ZSIYt97lP? zF4(C&v(tIjNq4w+)YehUkmil88;vvK0{=rQAUh}C@HsvyW+Ok!zjA-L`sGSp{1f?< z-xf-Amn-7;XrGUZ($K&2dAt{71S*FQh46 z5Z_6kpEhVm9Gq`&IIahA)L@iAAvl6yaX5;?rEHG{2f{e5jf>Y(g%|cFVqZsiN!*sJ zydwURPw_fZj zq3pCVo|9rld5Dp|QEr(ODbTTDloet*aW_%BLk;ki7PN1oU%PeK1kh_9r+X9Od-j^m zb8DaAiE0d@D4!?PfrN4pW4bYj6O&mwJ5Y!vSjHSAi48B%vPx7VmLisBnm*8pWymE< z%phhG%duEyKaUIw;$Cu;&+hFeD-rD~-j1>0C^0Ls_d?bAogmH6&;+Lm&JZjRoF!N! z_!z;Dh3Q+eFJax9mjK8ZWOh0`QYVg zq4izRYvO)(<6;4~Tv?=zzFhBhx0YL54(OyRtJizrksd5VhKy3(+1l**vAW}79WCDe z2|H;O#gg%j_|0hW+|OpNn!=s1OmLClU6B4zzlTkuYC(fwjetHMP#EC{kr%dmts5O@ h5w^sJS&BYoC&UltZqj-cWs#Xd1YeUll`9RA$A1C>8H4}; diff --git a/tests/__pycache__/test_printer_crud.cpython-314-pytest-9.0.3.pyc b/tests/__pycache__/test_printer_crud.cpython-314-pytest-9.0.3.pyc index b35b77f6f1f580d8e67d534ce11c64016919feb4..49d70e2fc81d6e31808713bf48b9aeecb2a19b7d 100644 GIT binary patch literal 51529 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 s{Jdl zI@hPqFfP2d^F)$c$1v@y0Ej)!>6~^~qXfy@9CyATwDE{sYFA_Uk)+E{FsEdJ<7_jN zHYZ!w(>^U}am~xPn=e|D6Vl-?JP-Q1I{A8z^4mdUzIZCh5}u_8!@(RISH zTVqCQod{3KtuvDTmz7$_pVCtIjHtLhP5DI%4>P62WklF()@P(m6R9auJkbBL(x!~O zUQ6CH#YKi`Dj`yL@>1e5B6lT|t1}(GI0)D;lX}8cyhHu3c>V&g>pmMG^ATfwlAho0 zVa>rB+Ls*w9)^b8YIm@jimLxDksnlAx#PhAq}rT!_}V;(>38ZD(gn@?N@Ej5jT zG-#85jEEtWTru6=$4x2&r%Egln;VsXqM)!g3XF+nLMs8 zkC(r;S@AM&R&4Lh09S3OC55TMifP^rd5?`0h$u4H;lm$$9xZL-GeHq{>Y&RX{pQze z01dJX+Q zG11cgf?I+Lnh8qMt9L8()wsPmZ2dFK5rb>*yN;EEc# zOd8#lcBmuhHLcwMVh&{|UpwuLP)mGwf7u{bH-}nhCK_93x!tphVF}DS} z7*CLa1xUa=@aU+5Xh{ zp{)G$N>Ov4xhvjjyxDl`nfIQ1_qoX`GyJQgA4Cp zm^d}rm65kVZ~f;Dx1mwi{ZYdl^>yfzkv2P1U)SS8VSO4Upi`!6yWcTVbfZDsFh{t8 zds`sq3WdV)p-?DSc4;g+l8VNX%tL*yA(SA8{Hb=$`~fx*?JA5Qqp3*FEiUC;`ZdkJ z{ks@1xl*lMuKMy|GBucpw$oFzVWyI4sCtZo7b$p|f_ZCvk^-_f%j_Mo!XS^J>7+S&DBN40Y&1??1Uf3U;lY<=Kw zb~ZdX;lu`K4;op3ZHpdsLwxzb3pd0MTHz(}L7x+D0v=Su&g{WPXzxGpLxJwWE|v%4 Qiw8Rp`xh_JID_H;57#|#UH||9 delta 4114 zcmd^CdrX_x75BBVjXz_9A7C4UF_lzH zr0dkG>v)_?_1>iW;(PTjQipH^B%HlbA2mhGHthCdi+ zN&lTCpZoakcfNbh?|06*_t%T6-!3SPexpH9;4k;)??Tfh3r4&0_lgDY2WsX~_==@t zY!|cVvx3_S6qhPisCmcAM}JqqLd?OI;hM5p*`$QIS~p~?TWtlS>N*y%jn&sF>$6C+ zLJ<%GdSH8AROtgbaNAl6<2^cMz`(onJvtcAGO3J#To^VFvpo1^*333aOsHf8E@-<{ zrh8Geta&8gJ}TgD9AoEYyD`&;7n=l?B_P+h<$l2My=<7tUWPNzYfOb>mt|^%fR*p( zhE4|;b*(CEz=DeXpcC>``Dk5{Fl@+cWnxPp4}IxOKD1b6g>MUX)t=qEVM%y4dx@p8 zrX~^-(y;#f%xk-tS-IGTlSDwg(_ zYMIKFY@@B3Y|m-MeAFu1VE*YM+dxVco{oqvoJnQJ#84zLEyl$XNZDMPGFny+cWqDP z`KZecsGy!7KHTeXN@ayZ@r2k&GfgD&Nc52CLa}6&+fB23Ni@URkVVr=-F@(x-Ot+L zrl$xx9Yy+KoaAP|$Kaa53NsD|Q^PI8Ik-`7hgWOff{zN{Ep5hCGROGI75+;t@%kiE*bKGxHE^{}1G6SK z(z*wJ*jR?VWckRWR#id31~vJ^oPp2gS7_`{ER70%aK*njw|IkAGq3_bz}fY8{jl_1 zqgo$W(~yrWj2ZB+LpJttHoB611o6?bGjfP)2hxxb#3FZl3H2T%@kJ7SD0;*+*fc%> z4W7=e5U<>Mke+XWcS@LyhNEKecywx7>_CUS#I_2d$D?h(JNJLTN+)mozdc#$QxvBZ9!}zHZ2C@ zq2#o96lb}0>m<$->@Rs&bn6}7vSNeFA+2_P%1WQY?s_=6nB%qr1%(qf6izNMb{De>sO(JJU!8S+b3}nOG2TEbS z|5I%lFVG&C8)#=me5HG^%TiRMtfWe~_KTSV#qiIeBN)8>1FiS?Kl z1L4qgB#|yoUqRLBD)bojGIg#(#+y{mWHge9#-l-AzBXq=$PILlam-R##}dcGVkxRh zs@jg#$2IMYmF#^f z8a|}7C5<)zId#=E<1ci6OK)RJ;CtiSVq|(g9E_Hz&!fWkAr}1yM8XE?bnL|}{d2U> z=Sgr+Jd!jIMBV8fyC>rD&|{J5se5)#ZrVAV!rvmDn>*p+23EQ2oQeI3gnHg8}V#D+3!Rm=`8Gn4nf2jQt7+loe2m~uiOw5C3czbHLk;p2a2|d}r3A zD#Uxo^6apV?j6meiXk6djak8UHfVE?>L_!Mn($bfxtQ^*5L_J)@b=>5D&2#2_<_z1 z?m?ZVp9P#-U(<5wNNK{4oae{MGWhHq$V*wjEQxM=l1x+2O)TIc$BR~`~UKBo1 zePjQYHTmFn2~woUOlhX3!_!!o-K2@rk#o?Tj#AFlP4sI>Up8p{rX(q>#aNJ(4 z;{~u>-_gFGx^$}%A0|OZLy=AqB9(42L4wkuNLfsziwj&ooNLZ6I^IKE>u_!uD7w1JR6wgv&17?)8` zT2E-EsvA9;xL`;=;+TSr%)Ky0QbHKbCSwT=4Y3Tx?wbj$ z4F}3H0hChBI97%OX>6E*5%gcKC{se`6|+|iQy1PHE*d%MV?5vhSObLy;BI&xeHz`s2VzJ5MPn4M%A3lb82HS|#ruGh*GG%t z$CI!Of8r>=obB5(%HM4-$&@KHpgo6TYXLZ%{Fb~YhW@hXm>s{kA7tA15RQ?zz8yG1 z&khk4i359LC%NJCHnmvvKP5USIBDOm*g}%NKxDwetb OpVH+T`g`l__x}gXpF0Qu delta 1800 zcmb7F-EZ4e6!%Ttd^vGSlcn|AbT`S`5w%-eQdmFJFfjotVv zMvPh-43yLUdf})}% zyEX9o)oCaRswQNiD5+vrfZCz}YZN$sQ&#c`ciCkXZGY|C0?Yozcg`1NRhx)diAc4z zR+T12X;sT8T5$-U^)+)IBrvQMLP^#zP?WOp$CZ0f6Y_;zMib!tVn$jJvXkW&cOs*z zf}+9hXA7klVrsPsKgy2re%$8Ge4^UYFinuM<(`H?^Ep`+r3GjdnpXO;O6mT9Dm;dq@djg;ge5C>gIOqdk8-xTpkiT4}>ZZNa7o zk_)}-9}mTG*i7)p_-nmF9eOBF>+0zHqieS+Ol13!e-sG(dUltI9MHUZS7`q3r0ITl zSNv(F1zcyessq&V;Pca*l(0$2j}AOW0zO>h4-63p}8g34})-+Nt)=Pf8z^ zHaWd7QE8jh>A*u6*6FEr#W1@J?looJ6?#f%I;_xgj)Qy0IQ#U}Ca_qfI$G;=FjMbo zrtzpBxR5^hqRw3+XTkIwQccZv{X%k=N&Z3Hr^Nhf|CxH~APJ_&K!2~Jhcy(f2f`dK zGqQ#uHD>lg66t|X+f{&m353E%QXzj>F?ZFGPG{x$bh_%CD~dS{|CT5aU2l(rF7#e| jsFQq(KBEk>;>3Q{Ii8oZ#hj2(#?VfCmlOZD8QH%8$Ljn(