docs(01): create phase 1 foundation plans
Three plans covering Docker scaffold, SQLite schema, and .intunewin spike. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -29,7 +29,7 @@ Decimal phases appear between their surrounding integers in numeric order.
|
|||||||
2. The container has no Node.js dependency and starts from a single image
|
2. The container has no Node.js dependency and starts from a single image
|
||||||
3. A Python-generated .intunewin file uploads successfully to a real Intune tenant without format errors
|
3. A Python-generated .intunewin file uploads successfully to a real Intune tenant without format errors
|
||||||
4. SQLite database initializes automatically on first run with the correct schema
|
4. SQLite database initializes automatically on first run with the correct schema
|
||||||
**Plans**: TBD
|
**Plans**: 3 plans
|
||||||
|
|
||||||
Plans:
|
Plans:
|
||||||
- [ ] 01-01: Docker container scaffold (Dockerfile, python:3.12-slim-bookworm, volume, healthcheck)
|
- [ ] 01-01: Docker container scaffold (Dockerfile, python:3.12-slim-bookworm, volume, healthcheck)
|
||||||
|
|||||||
@@ -0,0 +1,238 @@
|
|||||||
|
---
|
||||||
|
phase: 01-foundation
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- Dockerfile
|
||||||
|
- docker-compose.yml
|
||||||
|
- requirements.txt
|
||||||
|
- requirements-dev.txt
|
||||||
|
- imptune/main.py
|
||||||
|
- imptune/config.py
|
||||||
|
- imptune/api/__init__.py
|
||||||
|
- imptune/api/pages.py
|
||||||
|
- imptune/api/health.py
|
||||||
|
- imptune/templates/base.html
|
||||||
|
- imptune/templates/dashboard.html
|
||||||
|
- imptune/static/app.css
|
||||||
|
- tests/__init__.py
|
||||||
|
- tests/conftest.py
|
||||||
|
- tests/test_health.py
|
||||||
|
- tests/test_static.py
|
||||||
|
autonomous: true
|
||||||
|
requirements:
|
||||||
|
- INFRA-01
|
||||||
|
- INFRA-02
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Running docker compose up starts the app and serves HTTP 200 on GET /health"
|
||||||
|
- "The container has no Node.js dependency and starts from a single python:3.12-slim-bookworm image"
|
||||||
|
- "All static assets (Pico CSS, HTMX, Alpine.js) are served from /static/ with zero CDN references in templates"
|
||||||
|
- "The app shell displays a sidebar with Dashboard, Drivers, Printers, Clients, Packages sections"
|
||||||
|
- "The app follows OS dark/light theme preference automatically"
|
||||||
|
artifacts:
|
||||||
|
- path: "Dockerfile"
|
||||||
|
provides: "Single-container build with baked-in static assets"
|
||||||
|
contains: "python:3.12-slim-bookworm"
|
||||||
|
- path: "docker-compose.yml"
|
||||||
|
provides: "Container orchestration with named volume"
|
||||||
|
contains: "imptune_data:/data"
|
||||||
|
- path: "imptune/main.py"
|
||||||
|
provides: "FastAPI app entrypoint with static files mount and router registration"
|
||||||
|
exports: ["app"]
|
||||||
|
- path: "imptune/api/health.py"
|
||||||
|
provides: "GET /health endpoint for Docker healthcheck"
|
||||||
|
exports: ["router"]
|
||||||
|
- path: "imptune/templates/base.html"
|
||||||
|
provides: "Layout template with sidebar navigation and static asset includes"
|
||||||
|
contains: "data-theme=\"auto\""
|
||||||
|
key_links:
|
||||||
|
- from: "Dockerfile"
|
||||||
|
to: "imptune/static/"
|
||||||
|
via: "curl downloads during build"
|
||||||
|
pattern: "curl.*pico\\.min\\.css"
|
||||||
|
- from: "imptune/main.py"
|
||||||
|
to: "imptune/api/health.py"
|
||||||
|
via: "include_router"
|
||||||
|
pattern: "include_router.*health"
|
||||||
|
- from: "imptune/templates/base.html"
|
||||||
|
to: "/static/"
|
||||||
|
via: "link and script tags"
|
||||||
|
pattern: "/static/.*\\.css|/static/.*\\.js"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Create the Docker container scaffold, FastAPI app shell with sidebar navigation, health endpoint, and all baked-in static assets (Pico CSS, HTMX, Alpine.js). This is the foundation every subsequent plan builds on.
|
||||||
|
|
||||||
|
Purpose: Establish the running container and app shell that satisfies INFRA-01 (single Docker container) and INFRA-02 (no Node.js, no external DB). All subsequent phases add features to this scaffold.
|
||||||
|
Output: A buildable Docker image that starts, serves the app shell on localhost:8000, and passes healthcheck.
|
||||||
|
</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/01-foundation/01-CONTEXT.md
|
||||||
|
@.planning/phases/01-foundation/01-RESEARCH.md
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Create Docker scaffold, FastAPI app, and app shell templates</name>
|
||||||
|
<files>
|
||||||
|
Dockerfile,
|
||||||
|
docker-compose.yml,
|
||||||
|
requirements.txt,
|
||||||
|
imptune/__init__.py,
|
||||||
|
imptune/main.py,
|
||||||
|
imptune/config.py,
|
||||||
|
imptune/api/__init__.py,
|
||||||
|
imptune/api/pages.py,
|
||||||
|
imptune/api/health.py,
|
||||||
|
imptune/templates/base.html,
|
||||||
|
imptune/templates/dashboard.html,
|
||||||
|
imptune/static/app.css
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
Create the full project scaffold following the architecture from RESEARCH.md. The app package is `imptune/` (not top-level modules).
|
||||||
|
|
||||||
|
**Dockerfile** (python:3.12-slim-bookworm base):
|
||||||
|
- WORKDIR /app
|
||||||
|
- Single RUN layer: apt-get install curl, mkdir -p /app/imptune/static, download Pico CSS v2 (pico.min.css), HTMX 2.x (htmx.min.js), Alpine.js 3.x (alpine.min.js) into /app/imptune/static/ using curl with --fail flag, then purge curl and clean apt cache
|
||||||
|
- COPY requirements.txt and pip install --no-cache-dir
|
||||||
|
- COPY imptune/ into /app/imptune/ and other root files
|
||||||
|
- VOLUME ["/data"]
|
||||||
|
- HEALTHCHECK using python stdlib urllib (not curl): `python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"`
|
||||||
|
- EXPOSE 8000
|
||||||
|
- CMD ["uvicorn", "imptune.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
|
|
||||||
|
**docker-compose.yml**:
|
||||||
|
- Service `imptune`, build context `.`, ports 8000:8000, volume `imptune_data:/data`, restart unless-stopped, env DATA_DIR=/data
|
||||||
|
|
||||||
|
**requirements.txt** (all dependencies for phases 1-5):
|
||||||
|
- fastapi==0.115.*, uvicorn[standard]==0.30.*, jinja2==3.1.*, python-multipart==0.0.9, pycryptodome==3.20.*, python-dotenv==1.0.*, peewee==3.17.*
|
||||||
|
|
||||||
|
**imptune/config.py**:
|
||||||
|
- Load DATA_DIR from env (default "/data"), PORT from env (default 8000)
|
||||||
|
- Derive DB_PATH as DATA_DIR/imptune.db, DRIVERS_DIR as DATA_DIR/drivers
|
||||||
|
|
||||||
|
**imptune/main.py**:
|
||||||
|
- Create FastAPI app (title="ImpTune")
|
||||||
|
- Mount StaticFiles from pathlib.Path(__file__).parent / "static" at "/static"
|
||||||
|
- Set up Jinja2Templates pointing to imptune/templates/
|
||||||
|
- Include health router and pages router
|
||||||
|
- Add startup event that creates DATA_DIR and DRIVERS_DIR directories if they don't exist
|
||||||
|
|
||||||
|
**imptune/api/health.py**:
|
||||||
|
- GET /health returning {"status": "ok"}
|
||||||
|
|
||||||
|
**imptune/api/pages.py**:
|
||||||
|
- GET / returning dashboard.html template (sync def, not async)
|
||||||
|
- Pass empty recent_printers=[] and recent_packages=[] context for now
|
||||||
|
|
||||||
|
**imptune/templates/base.html**:
|
||||||
|
- html lang="en" data-theme="auto" (Pico CSS auto dark/light)
|
||||||
|
- Head: meta charset, viewport, title "ImpTune", link to /static/pico.min.css, link to /static/app.css, script defer for alpine.min.js, script for htmx.min.js
|
||||||
|
- Body: flex container with persistent left sidebar nav and main content area
|
||||||
|
- Sidebar: flat equal-weight nav links for Dashboard (/), Drivers (/drivers), Printers (/printers), Clients (/clients), Packages (/packages). Use semantic nav element. Active link highlighted.
|
||||||
|
- Main: container class wrapping {% block content %}{% endblock %}
|
||||||
|
|
||||||
|
**imptune/templates/dashboard.html**:
|
||||||
|
- Extends base.html
|
||||||
|
- Quick action buttons at top: "New Printer", "Upload Driver", "Export Package" (links, non-functional in Phase 1 — link to # with disabled state)
|
||||||
|
- Recent activity section below: empty state message "No printers configured yet" and "No packages exported yet"
|
||||||
|
|
||||||
|
**imptune/static/app.css** (under 50 lines):
|
||||||
|
- Sidebar layout: flex, sidebar fixed width ~220px, main flex-grow
|
||||||
|
- Sidebar nav styling: vertical link list, active state highlight
|
||||||
|
- Quick action button row styling
|
||||||
|
- Keep minimal — Pico CSS handles most styling
|
||||||
|
|
||||||
|
All __init__.py files: empty or minimal.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd C:/Users/SebastienQUEROL/Documents/projets/ImpTune && python -c "from imptune.main import app; print('App created:', app.title)"</automated>
|
||||||
|
</verify>
|
||||||
|
<done>
|
||||||
|
- All files exist with correct content
|
||||||
|
- FastAPI app imports without errors
|
||||||
|
- Dockerfile builds (docker build .)
|
||||||
|
- docker-compose.yml is valid YAML
|
||||||
|
- Templates reference /static/ paths only (no CDN URLs)
|
||||||
|
- Sidebar has all 5 sections with equal weight
|
||||||
|
- data-theme="auto" is set on html element
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto" tdd="true">
|
||||||
|
<name>Task 2: Create test scaffold and write health + static asset tests</name>
|
||||||
|
<files>
|
||||||
|
requirements-dev.txt,
|
||||||
|
tests/__init__.py,
|
||||||
|
tests/conftest.py,
|
||||||
|
tests/test_health.py,
|
||||||
|
tests/test_static.py
|
||||||
|
</files>
|
||||||
|
<behavior>
|
||||||
|
- test_health_returns_200: GET /health returns 200 with {"status": "ok"}
|
||||||
|
- test_static_mount_exists: app has /static mount
|
||||||
|
- test_no_cdn_urls_in_templates: scanning all .html files in imptune/templates/ finds zero references to cdn.jsdelivr.net, unpkg.com, cdnjs.com, or any https:// URL in link/script tags
|
||||||
|
- test_dashboard_returns_200: GET / returns 200
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
**requirements-dev.txt**: pytest, httpx (for FastAPI TestClient alternative — use fastapi.testclient which uses httpx internally)
|
||||||
|
|
||||||
|
**tests/conftest.py**:
|
||||||
|
- Import TestClient from fastapi.testclient (uses httpx under the hood)
|
||||||
|
- Fixture `client` that creates TestClient(app) from imptune.main
|
||||||
|
- Fixture `tmp_data_dir` using tmp_path that sets DATA_DIR env var to a temp directory before importing app, and creates the temp SQLite path
|
||||||
|
|
||||||
|
**tests/test_health.py**:
|
||||||
|
- test_health_returns_200: client.get("/health") returns 200 and JSON body {"status": "ok"}
|
||||||
|
|
||||||
|
**tests/test_static.py**:
|
||||||
|
- test_no_cdn_urls_in_templates: glob all .html files in imptune/templates/, read each, assert no matches for CDN domains (cdn.jsdelivr.net, unpkg.com, cdnjs.com) or https:// in href/src attributes
|
||||||
|
- test_dashboard_returns_200: client.get("/") returns 200
|
||||||
|
|
||||||
|
Run tests to confirm they pass (GREEN). The no-CDN test validates INFRA-02 at the template level.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd C:/Users/SebastienQUEROL/Documents/projets/ImpTune && pip install -r requirements-dev.txt -q && python -m pytest tests/test_health.py tests/test_static.py -x -v</automated>
|
||||||
|
</verify>
|
||||||
|
<done>
|
||||||
|
- All 4 tests pass
|
||||||
|
- Health endpoint verified via TestClient
|
||||||
|
- No CDN URLs found in any template
|
||||||
|
- Dashboard page loads successfully
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- `python -m pytest tests/ -x -v` — all tests pass
|
||||||
|
- `python -c "from imptune.main import app; print(app.title)"` — prints "ImpTune"
|
||||||
|
- Visually inspect templates for /static/ references only (automated by test_no_cdn_urls)
|
||||||
|
- `docker compose build` succeeds (if Docker available)
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- FastAPI app starts and serves GET /health with 200
|
||||||
|
- Dashboard page renders with sidebar navigation (5 sections)
|
||||||
|
- All static assets referenced via /static/ paths, zero CDN URLs
|
||||||
|
- Docker image builds from python:3.12-slim-bookworm with no Node.js
|
||||||
|
- Test suite passes with 4+ green tests
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/01-foundation/01-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
---
|
||||||
|
phase: 01-foundation
|
||||||
|
plan: 02
|
||||||
|
type: execute
|
||||||
|
wave: 2
|
||||||
|
depends_on: ["01-01"]
|
||||||
|
files_modified:
|
||||||
|
- imptune/db/__init__.py
|
||||||
|
- imptune/db/database.py
|
||||||
|
- imptune/db/models.py
|
||||||
|
- imptune/storage/__init__.py
|
||||||
|
- imptune/storage/driver_store.py
|
||||||
|
- imptune/main.py
|
||||||
|
- tests/test_db.py
|
||||||
|
autonomous: true
|
||||||
|
requirements:
|
||||||
|
- INFRA-01
|
||||||
|
- INFRA-02
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "SQLite database initializes automatically on first run with all tables (Client, Driver, Printer, Icon)"
|
||||||
|
- "Database uses WAL journal mode and has foreign keys enabled"
|
||||||
|
- "Database file is created inside the DATA_DIR volume path, not inside the container filesystem"
|
||||||
|
- "Schema creation is idempotent — repeated startups do not fail or duplicate tables"
|
||||||
|
artifacts:
|
||||||
|
- path: "imptune/db/database.py"
|
||||||
|
provides: "Peewee SqliteDatabase instance with WAL mode and init_db function"
|
||||||
|
exports: ["db", "init_db"]
|
||||||
|
- path: "imptune/db/models.py"
|
||||||
|
provides: "All ORM models for phases 1-5 (BaseModel, Client, Driver, Printer, Icon)"
|
||||||
|
exports: ["BaseModel", "Client", "Driver", "Printer", "Icon"]
|
||||||
|
- path: "imptune/storage/driver_store.py"
|
||||||
|
provides: "SHA256 content-addressed file storage abstraction for driver packages"
|
||||||
|
exports: ["DriverStore"]
|
||||||
|
- path: "tests/test_db.py"
|
||||||
|
provides: "Database initialization and schema validation tests"
|
||||||
|
key_links:
|
||||||
|
- from: "imptune/main.py"
|
||||||
|
to: "imptune/db/database.py"
|
||||||
|
via: "startup event calling init_db()"
|
||||||
|
pattern: "init_db"
|
||||||
|
- from: "imptune/db/models.py"
|
||||||
|
to: "imptune/db/database.py"
|
||||||
|
via: "BaseModel.Meta.database = db"
|
||||||
|
pattern: "database = db"
|
||||||
|
- from: "imptune/db/database.py"
|
||||||
|
to: "imptune/config.py"
|
||||||
|
via: "DB_PATH from config"
|
||||||
|
pattern: "DB_PATH|DATA_DIR"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Create the full SQLite schema using Peewee ORM (all tables for phases 1-5) and the content-addressed driver storage abstraction. Wire database initialization into the FastAPI startup event.
|
||||||
|
|
||||||
|
Purpose: Establish the data layer that all subsequent phases depend on. The full schema is created upfront per the locked user decision, so later phases only add routes and logic — not schema changes. Satisfies INFRA-01 (SQLite auto-init) and INFRA-02 (no external DB).
|
||||||
|
Output: Working database module with all models, driver storage helper, and startup wiring.
|
||||||
|
</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/01-foundation/01-CONTEXT.md
|
||||||
|
@.planning/phases/01-foundation/01-RESEARCH.md
|
||||||
|
|
||||||
|
<interfaces>
|
||||||
|
<!-- From plan 01-01: key exports the executor needs -->
|
||||||
|
|
||||||
|
From imptune/config.py:
|
||||||
|
```python
|
||||||
|
DATA_DIR: str # env var, default "/data"
|
||||||
|
DB_PATH: str # DATA_DIR + "/imptune.db"
|
||||||
|
DRIVERS_DIR: str # DATA_DIR + "/drivers"
|
||||||
|
```
|
||||||
|
|
||||||
|
From imptune/main.py:
|
||||||
|
```python
|
||||||
|
app = FastAPI(title="ImpTune")
|
||||||
|
# startup event already creates DATA_DIR/DRIVERS_DIR directories
|
||||||
|
# Executor must ADD init_db() call to the existing startup event
|
||||||
|
```
|
||||||
|
</interfaces>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto" tdd="true">
|
||||||
|
<name>Task 1: Create Peewee models, database init, and driver storage</name>
|
||||||
|
<files>
|
||||||
|
imptune/db/__init__.py,
|
||||||
|
imptune/db/database.py,
|
||||||
|
imptune/db/models.py,
|
||||||
|
imptune/storage/__init__.py,
|
||||||
|
imptune/storage/driver_store.py
|
||||||
|
</files>
|
||||||
|
<behavior>
|
||||||
|
- test_create_tables: calling init_db() creates all 4 tables (client, driver, printer, icon) in a fresh SQLite file
|
||||||
|
- test_wal_mode: after init_db(), PRAGMA journal_mode returns "wal"
|
||||||
|
- test_foreign_keys: after init_db(), PRAGMA foreign_keys returns 1
|
||||||
|
- test_idempotent: calling init_db() twice does not raise an error
|
||||||
|
- test_driver_store_save: saving bytes returns their SHA256 hex digest and creates a file at DRIVERS_DIR/{sha256}
|
||||||
|
- test_driver_store_dedup: saving the same bytes twice results in one file on disk (not two)
|
||||||
|
- test_driver_store_get_path: get_path(sha256) returns the correct file path
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
**imptune/db/database.py**:
|
||||||
|
- Import SqliteDatabase from peewee, import DB_PATH from imptune.config
|
||||||
|
- Create db = SqliteDatabase(None) (deferred init — path set at runtime so tests can override)
|
||||||
|
- init_db() function: call db.init(DB_PATH, pragmas={"journal_mode": "wal", "foreign_keys": 1}), then db.connect(reuse_if_open=True), then import all models and call db.create_tables([Client, Driver, Printer, Icon], safe=True)
|
||||||
|
- Use deferred database pattern so tests can point at a temp file
|
||||||
|
|
||||||
|
**imptune/db/models.py** (full schema for all phases per locked decision):
|
||||||
|
- BaseModel with Meta.database = db
|
||||||
|
- Client: name (CharField unique), created_at (DateTimeField default utcnow)
|
||||||
|
- Driver: sha256 (CharField unique, indexed), original_filename (CharField), size_bytes (IntegerField), uploaded_at (DateTimeField default utcnow), driver_desc (CharField null=True), inf_filename (CharField null=True), architecture (CharField null=True), has_cat_file (BooleanField default=False)
|
||||||
|
- Printer: 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, updated_at (both DateTimeField default utcnow)
|
||||||
|
- Icon: printer (ForeignKeyField Printer unique backref="icons"), sha256 (CharField), original_filename (CharField), size_bytes (IntegerField), uploaded_at (DateTimeField default utcnow)
|
||||||
|
|
||||||
|
**imptune/storage/driver_store.py**:
|
||||||
|
- Class DriverStore with __init__(self, base_dir: str)
|
||||||
|
- save(self, data: bytes) -> str: compute SHA256, write to base_dir/{sha256} if not exists, return hex digest
|
||||||
|
- get_path(self, sha256: str) -> Path: return Path(base_dir) / sha256
|
||||||
|
- exists(self, sha256: str) -> bool: check if file exists
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd C:/Users/SebastienQUEROL/Documents/projets/ImpTune && python -m pytest tests/test_db.py -x -v</automated>
|
||||||
|
</verify>
|
||||||
|
<done>
|
||||||
|
- All 7 tests pass
|
||||||
|
- init_db() creates Client, Driver, Printer, Icon tables
|
||||||
|
- WAL mode and foreign keys enabled
|
||||||
|
- Idempotent — second call is no-op
|
||||||
|
- DriverStore deduplicates by SHA256
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Wire database init into FastAPI startup</name>
|
||||||
|
<files>
|
||||||
|
imptune/main.py
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
Modify the existing imptune/main.py (created by plan 01-01) to add database initialization on startup:
|
||||||
|
|
||||||
|
- Import init_db from imptune.db.database
|
||||||
|
- In the existing startup event handler, add a call to init_db() AFTER the directory creation logic
|
||||||
|
- This ensures the SQLite database is created inside DATA_DIR (which was just created/verified)
|
||||||
|
- Keep all existing code (StaticFiles mount, router includes, directory creation) — only ADD the init_db() call
|
||||||
|
|
||||||
|
Do NOT use async def for the startup handler — Peewee is sync-only. Use regular def with FastAPI's @app.on_event("startup") which already exists from plan 01-01.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd C:/Users/SebastienQUEROL/Documents/projets/ImpTune && python -m pytest tests/test_health.py tests/test_db.py -x -v</automated>
|
||||||
|
</verify>
|
||||||
|
<done>
|
||||||
|
- main.py imports and calls init_db() on startup
|
||||||
|
- Existing health and static tests still pass (no regression)
|
||||||
|
- Database tests pass with init triggered via app startup
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- `python -m pytest tests/ -x -v` — all tests pass (health + static + db)
|
||||||
|
- `python -c "from imptune.db.models import Client, Driver, Printer, Icon; print('Models OK')"` — imports without error
|
||||||
|
- `python -c "from imptune.storage.driver_store import DriverStore; print('DriverStore OK')"` — imports without error
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- SQLite database auto-creates on app startup with 4 tables
|
||||||
|
- WAL journal mode and foreign keys enabled via pragmas
|
||||||
|
- Database file lives at DATA_DIR/imptune.db (volume-mounted path)
|
||||||
|
- DriverStore saves files by SHA256 with deduplication
|
||||||
|
- All existing tests continue to pass (no regression)
|
||||||
|
- 7+ new tests pass for db and storage
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/01-foundation/01-02-SUMMARY.md`
|
||||||
|
</output>
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
---
|
||||||
|
phase: 01-foundation
|
||||||
|
plan: 03
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- imptune/generators/__init__.py
|
||||||
|
- imptune/generators/intunewin_builder.py
|
||||||
|
- tests/test_intunewin.py
|
||||||
|
autonomous: true
|
||||||
|
requirements:
|
||||||
|
- INFRA-02
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "A Python function produces a valid .intunewin file from a source directory and setup file name"
|
||||||
|
- "The .intunewin file contains an outer ZIP with IntuneWinPackage/Contents/IntunePackage.intunewin and IntuneWinPackage/Metadata/Detection.xml"
|
||||||
|
- "The encrypted blob uses the correct byte layout: HMAC-SHA256 (32 bytes) + IV (16 bytes) + AES-256-CBC ciphertext"
|
||||||
|
- "Detection.xml contains correct EncryptionKey, MacKey, InitializationVector, Mac, FileDigest values that match the actual encryption"
|
||||||
|
- "The inner ZIP uses DEFLATE compression and the outer ZIP uses STORED compression"
|
||||||
|
artifacts:
|
||||||
|
- path: "imptune/generators/intunewin_builder.py"
|
||||||
|
provides: "Python-native .intunewin file assembler using pycryptodome"
|
||||||
|
exports: ["build_intunewin"]
|
||||||
|
min_lines: 60
|
||||||
|
- path: "tests/test_intunewin.py"
|
||||||
|
provides: "Byte-level validation tests for .intunewin format"
|
||||||
|
min_lines: 80
|
||||||
|
key_links:
|
||||||
|
- from: "imptune/generators/intunewin_builder.py"
|
||||||
|
to: "pycryptodome"
|
||||||
|
via: "from Crypto.Cipher import AES"
|
||||||
|
pattern: "Crypto\\.Cipher"
|
||||||
|
- from: "imptune/generators/intunewin_builder.py"
|
||||||
|
to: "zipfile"
|
||||||
|
via: "stdlib zipfile for inner and outer ZIPs"
|
||||||
|
pattern: "zipfile\\.ZipFile"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Implement the Python-native .intunewin file builder as a time-boxed spike. This module generates .intunewin packages using AES-256-CBC encryption with HMAC-SHA256, producing the exact byte layout Intune expects.
|
||||||
|
|
||||||
|
Purpose: Validate the highest-risk unknown in the project — can Python generate a .intunewin file that Intune accepts? This spike runs independently of the web app and produces a standalone generator module reused in Phase 5. Supports INFRA-02 (no external binary dependencies like IntuneWinAppUtil.exe).
|
||||||
|
Output: A tested build_intunewin() function and comprehensive byte-level validation tests.
|
||||||
|
</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/01-foundation/01-CONTEXT.md
|
||||||
|
@.planning/phases/01-foundation/01-RESEARCH.md
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto" tdd="true">
|
||||||
|
<name>Task 1: Implement .intunewin builder with byte-level tests</name>
|
||||||
|
<files>
|
||||||
|
imptune/generators/__init__.py,
|
||||||
|
imptune/generators/intunewin_builder.py,
|
||||||
|
tests/test_intunewin.py
|
||||||
|
</files>
|
||||||
|
<behavior>
|
||||||
|
- test_output_is_valid_zip: build_intunewin() output file is a valid ZIP archive
|
||||||
|
- test_outer_zip_structure: outer ZIP contains exactly IntuneWinPackage/Contents/IntunePackage.intunewin and IntuneWinPackage/Metadata/Detection.xml
|
||||||
|
- test_outer_zip_stored: outer ZIP entries use ZIP_STORED compression (no extra compression on encrypted content)
|
||||||
|
- test_detection_xml_valid: Detection.xml is valid XML with ApplicationInfo root element in the correct namespace (http://schemas.microsoft.com/IntuneWin)
|
||||||
|
- test_detection_xml_fields: Detection.xml contains Name, UnencryptedContentSize, FileName, SetupFile, and full EncryptionInfo with all 8 sub-elements (EncryptionKey, MacKey, InitializationVector, Mac, MacAlgorithm, ProfileIdentifier, FileDigest, FileDigestAlgorithm)
|
||||||
|
- test_encrypted_blob_layout: the encrypted blob starts with 32 bytes (HMAC) + 16 bytes (IV) + remainder (ciphertext); total length = 48 + ciphertext length
|
||||||
|
- test_iv_is_16_bytes: IV extracted from Detection.xml base64-decodes to exactly 16 bytes (NOT 32 — critical per RESEARCH.md)
|
||||||
|
- test_encryption_key_is_32_bytes: EncryptionKey from Detection.xml base64-decodes to exactly 32 bytes
|
||||||
|
- test_mac_key_is_32_bytes: MacKey from Detection.xml base64-decodes to exactly 32 bytes
|
||||||
|
- test_hmac_matches: HMAC-SHA256 computed from MacKey over ciphertext matches the first 32 bytes of the blob AND the Mac value in Detection.xml
|
||||||
|
- test_decryption_roundtrip: using EncryptionKey and IV from Detection.xml, decrypt the ciphertext, unpad, and verify the result is a valid DEFLATE-compressed ZIP containing the original source files
|
||||||
|
- test_file_digest_matches: FileDigest in Detection.xml matches SHA256 of the decrypted plaintext ZIP
|
||||||
|
- test_unencrypted_content_size: UnencryptedContentSize in Detection.xml matches the byte length of the decrypted plaintext ZIP
|
||||||
|
- test_setup_file_in_detection_xml: SetupFile element matches the setup_file argument passed to build_intunewin
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
**imptune/generators/intunewin_builder.py**:
|
||||||
|
Implement build_intunewin(source_dir: str, setup_file: str, output_path: str) -> None following the skeleton from RESEARCH.md Pattern 3, with these specifics:
|
||||||
|
|
||||||
|
1. Create inner ZIP (DEFLATE compression) of all files in source_dir, preserving relative paths
|
||||||
|
2. Generate random keys: aes_key = os.urandom(32), mac_key = os.urandom(32), iv = os.urandom(16) — IV MUST be 16 bytes per the critical correction in RESEARCH.md
|
||||||
|
3. Encrypt with AES-256-CBC: cipher = AES.new(aes_key, AES.MODE_CBC, iv), ciphertext = cipher.encrypt(pad(plaintext, AES.block_size))
|
||||||
|
4. Compute HMAC-SHA256 of ciphertext using mac_key
|
||||||
|
5. Assemble encrypted blob: hmac_digest (32 bytes) + iv (16 bytes) + ciphertext
|
||||||
|
6. Compute file_digest = SHA256 of plaintext (the inner ZIP bytes before encryption)
|
||||||
|
7. Build Detection.xml with all required fields (see RESEARCH.md for exact schema). Use xml.etree.ElementTree for building and xml.dom.minidom for pretty printing. Set xmlns="http://schemas.microsoft.com/IntuneWin" on ApplicationInfo root.
|
||||||
|
8. Build outer ZIP (STORED compression) with two entries: IntuneWinPackage/Contents/IntunePackage.intunewin (the encrypted blob) and IntuneWinPackage/Metadata/Detection.xml
|
||||||
|
9. All base64 values in Detection.xml use standard base64 encoding (base64.b64encode)
|
||||||
|
|
||||||
|
**tests/test_intunewin.py**:
|
||||||
|
- Create a tmp_path fixture with a small test source directory (2-3 small text files, one named "install.ps1")
|
||||||
|
- Call build_intunewin(source_dir, "install.ps1", output_path) to generate the file
|
||||||
|
- Implement all tests from the behavior list above
|
||||||
|
- For the decryption roundtrip: extract EncryptionKey and IV from Detection.xml, use AES.new(key, AES.MODE_CBC, iv) to decrypt, unpad the result, verify it's a valid ZIP containing the original files
|
||||||
|
- For HMAC verification: extract MacKey from Detection.xml, compute hmac.new(mac_key, ciphertext, hashlib.sha256).digest(), compare to first 32 bytes of blob AND to Mac value in Detection.xml
|
||||||
|
|
||||||
|
The tests serve as the format specification — if they pass, the byte layout is correct. The only remaining validation is a real Intune upload (manual, Phase 5 gate).
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd C:/Users/SebastienQUEROL/Documents/projets/ImpTune && pip install pycryptodome -q && python -m pytest tests/test_intunewin.py -x -v</automated>
|
||||||
|
</verify>
|
||||||
|
<done>
|
||||||
|
- All 14 byte-level tests pass
|
||||||
|
- IV is confirmed 16 bytes (not 32)
|
||||||
|
- Decryption roundtrip succeeds: encrypt then decrypt recovers original files
|
||||||
|
- HMAC verification succeeds: computed HMAC matches blob header and Detection.xml Mac field
|
||||||
|
- Outer ZIP structure matches Intune's expected layout exactly
|
||||||
|
- Detection.xml has correct namespace and all required fields
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- `python -m pytest tests/test_intunewin.py -x -v` — all 14 tests pass
|
||||||
|
- `python -c "from imptune.generators.intunewin_builder import build_intunewin; print('Builder importable')"` — no import errors
|
||||||
|
- The .intunewin file produced can be opened as a ZIP and inspected manually (outer structure visible)
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- build_intunewin() produces a file with the exact byte layout Intune expects
|
||||||
|
- All crypto operations use correct key/IV sizes (32/32/16 bytes)
|
||||||
|
- HMAC and decryption roundtrip verified programmatically
|
||||||
|
- Detection.xml contains all 8 EncryptionInfo sub-elements with correct values
|
||||||
|
- Module is standalone — no dependency on the web framework or database
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/01-foundation/01-03-SUMMARY.md`
|
||||||
|
</output>
|
||||||
Reference in New Issue
Block a user