From 44783d0f46045688b000c442dc62674d02b942ff Mon Sep 17 00:00:00 2001 From: Matt Edholm Date: Mon, 27 Apr 2026 15:39:24 -0400 Subject: [PATCH] =?UTF-8?q?docs:=20add=20planning=20artifacts=20=E2=80=94?= =?UTF-8?q?=20PRD,=20architecture,=20epics=20skeleton?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One-time commit of AI-generated planning documents: - prd.md — validated PRD, 44 FRs across 8 domains - prd-validation-report.md — validation results (4/5 holistic quality) - architecture.md — complete architecture document (all 8 steps) - epics.md — epics skeleton with extracted requirements inventory _bmad-output/ remains gitignored; these are committed explicitly. Co-Authored-By: Claude Sonnet 4.6 --- .../planning-artifacts/architecture.md | 581 ++++++++++++++++++ _bmad-output/planning-artifacts/epics.md | 117 ++++ .../prd-validation-report.md | 430 +++++++++++++ _bmad-output/planning-artifacts/prd.md | 357 +++++++++++ 4 files changed, 1485 insertions(+) create mode 100644 _bmad-output/planning-artifacts/architecture.md create mode 100644 _bmad-output/planning-artifacts/epics.md create mode 100644 _bmad-output/planning-artifacts/prd-validation-report.md create mode 100644 _bmad-output/planning-artifacts/prd.md diff --git a/_bmad-output/planning-artifacts/architecture.md b/_bmad-output/planning-artifacts/architecture.md new file mode 100644 index 0000000..7b61d3f --- /dev/null +++ b/_bmad-output/planning-artifacts/architecture.md @@ -0,0 +1,581 @@ +--- +stepsCompleted: [1, 2, 3, 4, 5, 6, 7, 8] +lastStep: 8 +status: 'complete' +completedAt: '2026-04-27' +inputDocuments: ['prd.md'] +workflowType: 'architecture' +project_name: 'pictureFrame' +user_name: 'Matt.edholm' +date: '2026-04-27' +--- + +# Architecture Decision Document + +_This document builds collaboratively through step-by-step discovery. Sections are appended as we work through each architectural decision together._ + +## Project Context Analysis + +### Requirements Overview + +**Functional Requirements:** 44 FRs across 8 domains — User & Account Management, Device Management, Image Library, Image Approval & Sharing, Device Provisioning, Image Rotation & Cycle Engine, Display & Status, Admin & Moderation. + +**Non-Functional Requirements:** +- Image pre-rendering completes within 10 seconds of upload/approval trigger (async job target) +- Device image pull endpoint ≤10s on home broadband +- Web pages load ≤3s on standard broadband +- Image rotation ±5 minutes of configured interval +- HTTPS all device-server communication +- MAC-authenticated device endpoint +- Single-use authorization links with configurable TTL +- Scheduled cleanup without manual intervention +- Zero blank screens; last image persists through outages + +**Scale & Complexity:** +- Primary domain: Full-stack web app with IoT device client +- Complexity level: Medium +- Deployment target: Single VPS, solo developer, personal-scale traffic +- Estimated architectural components: Web app (Symfony), Async image processing worker, Device API endpoint, Email/token service, Scheduler/cron runner, ESP32 firmware (separate codebase) + +### Technical Constraints & Dependencies + +- Stack: Symfony (PHP), PostgreSQL, Nginx-FPM, DDEV local dev — mirrors aqua-iq +- APP_BASE_URL is a firmware build constant — domain must be established before firmware development +- API contract is stable by design; breaking changes require physical reflash of all deployed devices +- No OTA firmware updates in V1 +- Infrastructure patterns from ~/src/aqua-iq (DDEV, server, SSH) + +### Cross-Cutting Concerns Identified + +- **Async image processing pipeline:** Upload/approval dispatches a Symfony Messenger message → worker processes (resize, 4bpp palette quantization) → sets image rendering_status to `ready`. Images only enter device pull pool when `ready`. Doctrine transport (consistent with aqua-iq). +- **Image rendering status:** Each image asset tracks `pending → processing → ready | failed` per device model/orientation. Rotation engine and device pull endpoint filter exclusively on `ready`. +- **Device identity:** MAC address as device identifier throughout — provisioning, image pull auth, ownership transfer. +- **Authorization tokens:** Single-use links with TTL for email approve/decline and hard-delete confirmation. +- **Soft-delete with retention rules:** Images retained until last approval removed; scheduled cleanup handles hard-delete. +- **Rotation engine:** Per-device schedule tracking, uniqueness window enforcement, next-image selection from `ready` pool only. + +## Starter Template Evaluation + +### Primary Technology Domain + +Symfony full-stack web application with IoT device client. Stack pre-determined by user. + +### Selected Starter: Symfony 8.0 `--webapp` + +**Initialization Command:** + +```bash +symfony new pictureframe --webapp +``` + +**Architectural Decisions Provided by Starter:** + +**Language & Runtime:** PHP 8.4+, Symfony 8.0 + +**Templating:** Twig with Stimulus and UX Turbo (Hotwire) — lightweight interactivity without a JS build step + +**ORM & Database:** Doctrine ORM, PostgreSQL 16 (DDEV local), migrations via `doctrine/migrations` + +**Security:** Symfony Security component — firewalls, voters, role hierarchy + +**Email:** Symfony Mailer — handles sharing flows and hard-delete confirmations + +**Build Tooling:** AssetMapper (no Webpack/Node build step required) + +**Testing:** PHPUnit via `symfony/test-pack` + +**Code Organization:** Standard Symfony structure — `src/Entity`, `src/Controller`, `src/Repository`, `src/Service`, `templates/` + +**Additional packages required:** +- `symfony/messenger` + Doctrine transport — async image processing worker +- `symfony/scheduler` — rotation engine and scheduled cleanup +- Image processing library (TBD step 4: GD vs Imagick) + +**Local Dev:** DDEV configured to mirror aqua-iq (PHP 8.4, Nginx-FPM, PostgreSQL 16) + +**Note:** Project initialization using this command is the first implementation story. + +## Core Architectural Decisions + +### Decision Priority Analysis + +**Critical Decisions (Block Implementation):** +- APP_BASE_URL established: `https://pictureframe.edholm.me` — bake into firmware as build constant +- Image rendering status lifecycle required before rotation engine can select images +- Imagick required on VPS before image processing worker can run + +**Important Decisions (Shape Architecture):** +- Local filesystem image storage (not object storage) +- Symfony Messenger async processing with `ready` status gate +- Token entity pattern for all single-use authorization links + +**Deferred Decisions (Post-MVP):** +- Caching layer (Redis) — add if query performance becomes an issue +- Additional display models beyond Waveshare 7.3" 800×480 + +### Data Architecture + +**Image Storage:** Local filesystem on VPS. Pre-rendered assets stored at `storage/images/{image_id}/{device_model}_{orientation}.bin`. Originals stored separately at `storage/images/{image_id}/original.{ext}`. Paths stored in DB are relative to `STORAGE_PATH` env var. No object storage — personal scale doesn't warrant the complexity. + +**Image Processing Library:** Imagick (PHP extension). Chosen for Floyd-Steinberg dithering quality on the 6-color e-ink palette. GD's palette quantization produces inferior results for this use case. Imagick must be installed on VPS and in DDEV container. + +**Image Rendering Status:** Each pre-rendered asset (per device model + orientation) tracks status independently: `pending → processing → ready | failed`. Rotation engine and device pull endpoint filter exclusively on `ready`. Stored as an enum column on a `RenderedAsset` entity. + +**Caching:** None in V1. PostgreSQL at personal scale is sufficient. Add Redis later if needed. + +**Migrations:** Doctrine Migrations (`doctrine/migrations`). Standard Symfony approach. + +### Authentication & Security + +**Web Authentication:** Symfony form login — email + password, `remember_me` cookie. Standard Symfony Security firewall. + +**Roles:** Two roles only — `ROLE_USER` and `ROLE_SUPER_ADMIN`. Configured in `security.yaml` with role hierarchy. Super admin is a single designated account. + +**Authorization Token Pattern:** A `Token` entity — UUID primary key, `type` enum (`share_approve`, `share_decline`, `hard_delete_confirm`), `expires_at` datetime, `used_at` nullable datetime. Single-use enforced: token is invalid if `used_at IS NOT NULL` or `expires_at < NOW()`. TTL is configurable per token type. + +**Device MAC Authentication:** Symfony controller guard on the image pull endpoint. Device sends MAC address via URL segment; server validates it exists in the `Device` entity and is linked to an active account before serving the binary asset. + +**HTTPS:** Enforced at Nginx level on VPS. DDEV provides HTTPS locally. + +### API & Communication Patterns + +**Device API:** Single endpoint — `GET /api/device/{mac}/image` — returns raw binary (`application/octet-stream`). This is the only machine-to-machine API surface. No versioning scheme beyond URL stability guarantee (no breaking changes in V1). + +**Web Controllers:** Standard Symfony controllers returning Twig responses. No JSON API for the web application. + +**Email:** Symfony Mailer. Transactional emails: image share notification (with approve link), hard-delete confirmation. Authorization links embedded as tokenized URLs pointing to Symfony routes. + +**Async Messaging:** Symfony Messenger with Doctrine transport. Messages dispatched on image upload and image approval. Worker consumes queue and processes images via Imagick. Sets `RenderedAsset` status to `ready` on completion, `failed` on exception. + +**Scheduled Tasks:** Symfony Scheduler. Two recurring tasks: +- Rotation engine: per-device, fires on each device's configured interval to advance the current image pointer +- Cleanup job: periodic hard-delete of soft-deleted images with no remaining approvals + +### Frontend Architecture + +**Templating:** Twig. Identical pattern to aqua-iq. + +**Interactivity:** Stimulus controllers + Turbo Drive (Hotwire). No SPA, no build step required. + +**Forms:** Symfony Form component. + +**Assets:** AssetMapper (no Webpack/Node). + +### Infrastructure & Deployment + +**Domain:** `pictureframe.edholm.me` — APP_BASE_URL and API base URL. Must be configured on VPS before firmware build constants are set. + +**VPS:** Single server. Nginx reverse proxy → PHP-FPM → Symfony. PostgreSQL on same host. Mirrors aqua-iq production setup. + +**Local Dev:** DDEV — PHP 8.4, Nginx-FPM, PostgreSQL 16. Config mirrors aqua-iq `.ddev/config.yaml`. + +**Git Hosting:** `git.edholm.me` (self-hosted Gitea/Forgejo). Credentials from existing projects. + +**CI/CD:** Gitea Actions (if enabled on git.edholm.me) or SSH-based deploy script. Mirror aqua-iq deployment pattern. + +**SSH/Server Access:** Documented in aqua-iq project. Same VPS, same access pattern. + +### Decision Impact Analysis + +**Implementation Sequence:** +1. DDEV setup + Symfony scaffold (`symfony new pictureframe --webapp`) +2. Add Messenger, Scheduler, Imagick +3. Domain + Nginx config on VPS +4. Core entities (User, Device, Image, RenderedAsset, Token) +5. Image processing worker +6. Device pull endpoint +7. Web application features (library, approval, sharing, admin) +8. Firmware (after domain + API contract confirmed) + +**Cross-Component Dependencies:** +- Firmware cannot be finalized until `pictureframe.edholm.me` is live and API endpoint format is confirmed +- Device pull endpoint depends on `RenderedAsset.status = ready` +- Rotation engine depends on uniqueness tracking and `ready` asset pool +- Email sharing depends on Token entity and Mailer configuration + +## Implementation Patterns & Consistency Rules + +### Entity & Enum Patterns + +**PHP Backed Enums:** Use PHP 8.1 backed enums for all finite value sets. + +```php +enum RenderStatus: string { + case Pending = 'pending'; + case Processing = 'processing'; + case Ready = 'ready'; + case Failed = 'failed'; +} + +enum TokenType: string { + case ShareApprove = 'share_approve'; + case ShareDecline = 'share_decline'; + case HardDeleteConfirm = 'hard_delete_confirm'; +} + +enum Orientation: string { + case Landscape = 'landscape'; + case Portrait = 'portrait'; +} +``` + +Doctrine maps backed enums to VARCHAR columns via built-in PHP 8.1 enum type support. No string constants elsewhere. + +### Repository Naming Convention + +**Soft-Delete Awareness:** All repository methods that exclude soft-deleted records use the `findActive*` prefix. + +- `findActiveByDevice(Device $device)` — images where `deleted_at IS NULL` +- `findActiveReadyByDevice(Device $device)` — images with `deleted_at IS NULL` AND `status = RenderStatus::Ready` +- `findBy*` — raw Doctrine finders, may include soft-deleted records + +This makes soft-delete awareness visible at the call site rather than hidden in query logic. + +### Image Storage Pattern + +**Storage Location:** `storage/images/{image_id}/{device_model}_{orientation}.bin` for pre-rendered assets. Originals at `storage/images/{image_id}/original.{ext}`. Never `var/images/`. + +**Database References:** Store paths relative to `STORAGE_PATH` root, not absolute paths. The storage root can move without a data migration. + +**Environment Variable:** `STORAGE_PATH` — configured per environment. In DDEV, mapped to a local volume. On VPS, absolute path under the project root. + +### Async Image Processing Pattern + +**Exclusive Dispatch Points:** `ProcessImageMessage` is dispatched in exactly two places: +- `ImageService::upload()` — after persisting a new uploaded image +- `ImageService::approve()` — after a share-approval token is consumed + +No other code dispatches `ProcessImageMessage`. + +**Messenger Transport Configuration:** + +```yaml +# config/packages/messenger.yaml +framework: + messenger: + transports: + image_processing: + dsn: '%env(MESSENGER_TRANSPORT_DSN)%' + options: + queue_name: image_processing + retry_strategy: + max_retries: 1 + delay: 1000 + multiplier: 2 +``` + +`max_retries: 1` — a single retry on transient failures. Failed messages set `RenderedAsset.status` to `RenderStatus::Failed` so the error surfaces in the admin UI. + +### Device Pull Endpoint — Critical Response Codes + +> **CRITICAL RULE:** `GET /api/device/{mac}/image` must return: +> - **`204 No Content`** — MAC is valid, device exists, but no `ready` image is currently available +> - **`404 Not Found`** — MAC is not registered in the system +> - **`200 OK`** — returns raw binary (`application/octet-stream`) +> +> **Never return 404 when the device is known but has no ready image.** The ESP32 firmware treats 204 as "wait and retry" and 404 as "this device is not configured." Confusing them causes the device to enter a permanent error state. + +### Testing Strategy + +**Unit Tests (`tests/Unit/`):** Pure logic, no database or I/O. Covers: enum behavior, token TTL calculation, image processing pipeline steps, rotation selection algorithm. + +**Integration Tests (`tests/Integration/`):** Hit the real test database via Doctrine, using Symfony's `KernelTestCase`. Covers: repository `findActive*` methods, Messenger handler with real entity persistence, token single-use enforcement. + +**Functional Tests (`tests/Functional/`):** Full HTTP stack via `WebTestCase`. Covers: device pull endpoint (204/404/200 response codes), form submissions, auth flows, email dispatch. + +No database mocks. Integration tests use a real test database — mirrors the pattern from aqua-iq. + +### Enforcement Guidelines + +Mandatory rules for all code in this project: + +1. **`findActive*` prefix** on all repository methods filtering out soft-deleted records +2. **`ImageService::upload()` and `ImageService::approve()`** are the only dispatch points for `ProcessImageMessage` +3. **`204` not `404`** when device is known but has no ready image +4. **`storage/images/`** for all file storage — never `var/images/` +5. **Relative paths** in the database — never absolute filesystem paths +6. **`max_retries: 1`** on the `image_processing` Messenger transport +7. **PHP backed enums** for `RenderStatus`, `TokenType`, `Orientation` — no raw string constants +8. **Rotation engine and device pull endpoint** filter exclusively on `RenderStatus::Ready` + +## Project Structure & Boundaries + +### Requirements to Structure Mapping + +**User & Account Management** → `src/Controller/SecurityController.php`, `src/Entity/User.php`, `src/Repository/UserRepository.php`, `templates/security/` + +**Device Management** → `src/Controller/Device/`, `src/Entity/Device.php`, `src/Service/DeviceService.php`, `src/Repository/DeviceRepository.php`, `templates/device/` + +**Image Library** → `src/Controller/Image/`, `src/Entity/Image.php`, `src/Entity/RenderedAsset.php`, `src/Service/ImageService.php`, `src/Service/ImageProcessingService.php`, `templates/image/` + +**Image Approval & Sharing** → `src/Controller/Token/`, `src/Entity/Token.php`, `src/Service/TokenService.php`, `templates/token/` + +**Device Provisioning** → `src/Controller/Device/DeviceProvisionController.php`, `templates/device/provision.html.twig` + +**Image Rotation & Cycle Engine** → `src/Schedule/RotationSchedule.php`, `src/Service/RotationService.php` + +**Display & Status (device pull endpoint)** → `src/Controller/Api/DeviceImageController.php` + +**Admin & Moderation** → `src/Controller/Admin/`, `templates/admin/` + +**Cross-Cutting: Async Processing** → `src/Message/ProcessImageMessage.php`, `src/MessageHandler/ProcessImageMessageHandler.php`, `config/packages/messenger.yaml` + +**Cross-Cutting: Scheduled Tasks** → `src/Schedule/RotationSchedule.php`, `src/Schedule/ImageCleanupSchedule.php`, `config/packages/scheduler.yaml` + +**Cross-Cutting: Enums** → `src/Enum/RenderStatus.php`, `src/Enum/TokenType.php`, `src/Enum/Orientation.php` + +### Complete Project Directory Structure + +``` +pictureframe/ +├── .ddev/ +│ ├── config.yaml ← PHP 8.4, nginx-fpm, pgsql 16 — mirror aqua-iq +│ └── docker-compose.imagick.yaml ← adds Imagick to web container +├── .gitea/ +│ └── workflows/ +│ └── ci.yml ← Gitea Actions: lint + test on push +├── assets/ +│ ├── app.js ← AssetMapper entry, imports Stimulus + Turbo +│ ├── controllers/ +│ │ ├── image_upload_controller.js +│ │ └── device_status_controller.js +│ └── styles/ +│ └── app.css +├── bin/ +│ └── console +├── config/ +│ ├── packages/ +│ │ ├── doctrine.yaml +│ │ ├── doctrine_migrations.yaml +│ │ ├── mailer.yaml +│ │ ├── messenger.yaml ← image_processing transport, max_retries: 1 +│ │ ├── scheduler.yaml +│ │ ├── security.yaml ← form_login, remember_me, ROLE_SUPER_ADMIN hierarchy +│ │ └── twig.yaml +│ ├── routes.yaml +│ ├── routes/ +│ │ └── api.yaml ← /api/device/{mac}/image route +│ └── services.yaml +├── migrations/ +├── public/ +│ └── index.php +├── src/ +│ ├── Controller/ +│ │ ├── Api/ +│ │ │ └── DeviceImageController.php ← GET /api/device/{mac}/image → 200/204/404 +│ │ ├── Admin/ +│ │ │ ├── AdminDashboardController.php +│ │ │ └── AdminModerationController.php +│ │ ├── Device/ +│ │ │ ├── DeviceController.php +│ │ │ └── DeviceProvisionController.php +│ │ ├── Image/ +│ │ │ ├── ImageLibraryController.php +│ │ │ ├── ImageUploadController.php +│ │ │ └── ImageShareController.php +│ │ ├── SecurityController.php +│ │ └── Token/ +│ │ └── TokenActionController.php ← consume approve/decline/hard-delete tokens +│ ├── Entity/ +│ │ ├── Device.php +│ │ ├── Image.php +│ │ ├── RenderedAsset.php ← per device-model+orientation, RenderStatus +│ │ ├── Token.php ← UUID PK, TokenType, expires_at, used_at +│ │ └── User.php +│ ├── Enum/ +│ │ ├── Orientation.php +│ │ ├── RenderStatus.php +│ │ └── TokenType.php +│ ├── Form/ +│ │ ├── DeviceType.php +│ │ ├── ImageUploadType.php +│ │ └── RegistrationType.php +│ ├── Message/ +│ │ └── ProcessImageMessage.php ← DTO: imageId, deviceModel, orientation +│ ├── MessageHandler/ +│ │ └── ProcessImageMessageHandler.php ← Imagick → .bin → sets RenderStatus::Ready +│ ├── Repository/ +│ │ ├── DeviceRepository.php +│ │ ├── ImageRepository.php ← findActiveByDevice(), findActiveReadyByDevice() +│ │ ├── RenderedAssetRepository.php ← findReadyForDevice() +│ │ ├── TokenRepository.php ← findValidToken() (unused + unexpired) +│ │ └── UserRepository.php +│ ├── Schedule/ +│ │ ├── ImageCleanupSchedule.php ← hard-delete orphaned soft-deleted images +│ │ └── RotationSchedule.php ← advance current_image pointer per device +│ ├── Service/ +│ │ ├── DeviceService.php +│ │ ├── ImageProcessingService.php ← Imagick: resize → 4bpp dither → .bin +│ │ ├── ImageService.php ← upload() and approve() — exclusive dispatch points +│ │ ├── RotationService.php ← next-image selection, uniqueness window +│ │ └── TokenService.php ← issue and consume single-use tokens +│ └── Kernel.php +├── storage/ +│ └── images/ ← gitignored; STORAGE_PATH points here +├── templates/ +│ ├── admin/ +│ │ ├── dashboard.html.twig +│ │ └── moderation.html.twig +│ ├── device/ +│ │ ├── index.html.twig +│ │ ├── provision.html.twig +│ │ └── show.html.twig +│ ├── image/ +│ │ ├── library.html.twig +│ │ ├── share.html.twig +│ │ └── upload.html.twig +│ ├── security/ +│ │ └── login.html.twig +│ ├── token/ +│ │ ├── approve.html.twig +│ │ └── decline.html.twig +│ └── base.html.twig +├── tests/ +│ ├── Functional/ +│ │ ├── Api/ +│ │ │ └── DeviceImageControllerTest.php +│ │ ├── Device/ +│ │ │ └── DeviceControllerTest.php +│ │ └── Image/ +│ │ └── ImageLibraryControllerTest.php +│ ├── Integration/ +│ │ ├── MessageHandler/ +│ │ │ └── ProcessImageMessageHandlerTest.php +│ │ ├── Repository/ +│ │ │ ├── ImageRepositoryTest.php +│ │ │ └── RenderedAssetRepositoryTest.php +│ │ └── Service/ +│ │ └── TokenServiceTest.php +│ └── Unit/ +│ ├── Enum/ +│ │ └── RenderStatusTest.php +│ └── Service/ +│ ├── ImageProcessingServiceTest.php +│ └── RotationServiceTest.php +├── var/ +│ ├── cache/ +│ └── log/ +├── .env +├── .env.local ← gitignored; STORAGE_PATH, DATABASE_URL, MAILER_DSN +├── .env.test +├── .gitignore +├── composer.json +├── composer.lock +├── importmap.php +├── phpunit.xml.dist +└── symfony.lock +``` + +### Architectural Boundaries + +**API Boundary — Device Pull Endpoint** +`GET /api/device/{mac}/image` is the only machine-to-machine surface. MAC address validated against `Device` entity before serving. Returns `application/octet-stream`, 204, or 404. Isolated in `config/routes/api.yaml`. + +**Web Application Boundary** +All other routes behind Symfony form-login firewall. Twig responses only. `ROLE_SUPER_ADMIN` gates admin controllers. + +**Async Processing Boundary** +`ImageService` → Messenger bus → `ProcessImageMessageHandler`. Handler is the only component writing to `storage/images/`. `RenderedAsset.status` is the only signal crossing this boundary. + +**Scheduled Task Boundary** +`RotationSchedule` and `ImageCleanupSchedule` run in the worker process — no HTTP context, no session access. Database reads and writes only. + +**Storage Boundary** +`storage/images/` written by `ProcessImageMessageHandler`, read by `DeviceImageController`. All other code uses `STORAGE_PATH` env var. Paths in DB are always relative. + +### Integration Points & Data Flow + +**Image Upload → Display** +``` +User uploads → ImageService::upload() persists Image → dispatches ProcessImageMessage + → Worker: Imagick resize + dither → storage/images/{id}/waveshare73_landscape.bin + → RenderedAsset.status = Ready + → RotationSchedule advances device pointer + → ESP32: GET /api/device/{mac}/image → 200 + binary stream +``` + +**Image Sharing Flow** +``` +User shares image with owner → ImageService creates Token (ShareApprove/ShareDecline) + → Mailer sends tokenized URL + → Owner clicks link → TokenActionController::approve() + → TokenService consumes token → ImageService::approve() dispatches ProcessImageMessage + → Image enters ready pool for recipient device +``` + +**Soft-Delete & Cleanup Flow** +``` +User soft-deletes image → Image.deleted_at set (excluded by findActive*) + → If last approval removed → ImageCleanupSchedule hard-deletes image + storage files +``` + +## Architecture Validation Results + +### Coherence Validation ✅ + +**Decision Compatibility:** PHP 8.4 + Symfony 8.0 + Doctrine ORM + Messenger + Scheduler + PostgreSQL 16 is a fully supported combination. Imagick is a standalone PHP extension with no framework conflicts. AssetMapper + Stimulus + Turbo has first-party Symfony UX support and requires no build tooling. + +**Pattern Consistency:** `findActive*` naming, `storage/images/` path convention, exclusive Messenger dispatch points, and backed enum usage are consistently applied. One contradiction (`var/images/` in Data Architecture section) found during validation and corrected — all references now point to `storage/images/`. + +**Structure Alignment:** Every controller, service, entity, and test file has a defined home. All five boundaries (API, web, async, storage, scheduled) are respected in the project structure. + +### Requirements Coverage Validation ✅ + +All 8 FR domains covered: + +| Domain | Architectural Support | +|---|---| +| User & Account Management | `User` entity, form login, `SecurityController` | +| Device Management | `Device` entity, `DeviceController`, `DeviceService` | +| Image Library | `Image` entity, `ImageLibraryController`, `ImageService` | +| Image Approval & Sharing | `Token` entity, `TokenService`, `TokenActionController` | +| Device Provisioning | `DeviceProvisionController` | +| Image Rotation & Cycle Engine | `RotationSchedule`, `RotationService` | +| Display & Status | `DeviceImageController`, `RenderedAsset` | +| Admin & Moderation | `AdminDashboardController`, `AdminModerationController` | + +All 9 NFRs covered: pre-rendering ≤10s (async Messenger), device pull ≤10s (direct binary stream), web pages ≤3s (standard Symfony), rotation ±5min (Scheduler), HTTPS (Nginx), MAC auth (controller guard), single-use links (Token entity), scheduled cleanup (ImageCleanupSchedule), zero blank screens (204 not 404 + last image persistence). + +### Implementation Readiness Validation ✅ + +All critical decisions documented. Enforcement guidelines provide 8 explicit, checkable rules. Project tree is specific with no generic placeholders. The 204/404 distinction is called out as a critical rule with the reason documented. + +### Gap Analysis + +**Critical Gaps:** None. + +**Minor (non-blocking):** +- `.env.example` not in project tree — add alongside `.env` on scaffold +- `device_model` field type on `RenderedAsset` not fully specified — for V1 with a single display (Waveshare 7.3" 800×480), a string constant or single-value enum is sufficient + +### Architecture Completeness Checklist + +**✅ Requirements Analysis** — context, scale, constraints, cross-cutting concerns + +**✅ Architectural Decisions** — stack, data architecture, auth, API patterns, infrastructure + +**✅ Implementation Patterns** — enums, `findActive*`, storage, Messenger, 204/404, testing, enforcement rules + +**✅ Project Structure** — complete directory tree, boundaries, FR mapping, data flow diagrams + +**✅ Validation** — coherence, coverage, readiness, gaps + +### Architecture Readiness Assessment + +**Overall Status: READY FOR IMPLEMENTATION** + +**Confidence: High.** Coherent, fully covers the PRD, no critical gaps, sufficient specificity for consistent implementation. + +**First Implementation Step:** +```bash +symfony new pictureframe --webapp +``` + +### Implementation Handoff + +**AI Agent Guidelines:** +- Follow all architectural decisions exactly as documented +- Apply the 8 enforcement rules in every code generation task +- Respect the `storage/images/` boundary — never write image assets to `var/` +- Confirm `204 vs 404` on every implementation of the device pull endpoint +- Reference this document for all architectural questions; do not invent decisions not documented here diff --git a/_bmad-output/planning-artifacts/epics.md b/_bmad-output/planning-artifacts/epics.md new file mode 100644 index 0000000..6655c12 --- /dev/null +++ b/_bmad-output/planning-artifacts/epics.md @@ -0,0 +1,117 @@ +--- +stepsCompleted: [1] +inputDocuments: ['prd.md', 'architecture.md'] +workflowType: 'epics-and-stories' +project_name: 'pictureFrame' +user_name: 'Matt.edholm' +date: '2026-04-27' +--- + +# pictureFrame - Epic Breakdown + +## Overview + +This document provides the complete epic and story breakdown for pictureFrame, decomposing the requirements from the PRD and Architecture into implementable stories. + +## Requirements Inventory + +### Functional Requirements + +FR1: Visitors can register a new account with an email address and password +FR2: Registered users can log in to their account +FR3: Super admin can view, edit, and delete any user account, device, or image across the system + +FR4: Users can register a device to their account via the provisioning setup flow +FR5: Users can assign a name to each of their devices +FR6: Users can configure display orientation (landscape or portrait) per device +FR7: Users can configure the image rotation frequency per device +FR8: Users can configure the uniqueness window (number of cycles before an image can repeat) per device +FR9: When a device is re-provisioned to a new account, the system atomically purges the prior image history and transfers ownership +FR10: Super admin can view, rename, reconfigure, and transfer any device across all accounts + +FR11: Users can upload photos to their personal image library +FR12: Users can view their library filtered by source: Uploaded vs. Shared +FR13: Users can soft-delete images from their library +FR14: When an image is shared to a user, it appears in their library as a reference (not a copy) +FR15: Super admin can add images to and remove images from a global pre-loaded image pool available to all devices + +FR16: Users can approve or decline images for a specific device in their account +FR17: Users can share an image from their library to another user +FR18: When an image is shared, the recipient receives an email with the image and an approve action; clicking approve opens a device-selection page (no login required) where the recipient chooses which device(s) to add the image to +FR19: The approval link works from any email client without requiring account creation or login +FR20: Approved images enter the active rotation pool for the selected device(s) +FR21: Users can approve all images within a collection for a device in a single action +FR22: Users can request a full hard delete of their own image, which enters a super-admin review queue +FR23: System sends confirmation to the user when their hard-delete request is fulfilled + +FR24: Device enters provisioning mode when the reset button is held for 5 seconds +FR25: In provisioning mode, device displays a scannable QR code for joining the provisioning access point +FR26: User can enter home WiFi credentials through a captive portal served by the device +FR27: On successful WiFi connection, device displays a QR code linking to the account setup page for that specific device +FR28: On failed WiFi connection, device displays a failure indicator, reactivates the AP, and redisplays the provisioning QR code for retry +FR29: Users can register a new account or log in to an existing account from the device setup page to link the device + +FR30: System automatically advances to the next approved image on the configured schedule for each device +FR31: System tracks image display history per device to enforce the configured uniqueness window +FR32: When the uniqueness window exceeds the count of available approved images, the window is treated as equal to the available image count +FR33: System pre-renders images to display-ready format per device model and orientation at the time of upload or approval +FR34: Device pulls its next pre-rendered image from the server on each scheduled cycle + +FR35: Device displays the current image persistently with no power draw between refresh cycles +FR36: Device retains and displays the last successfully transferred image through power loss and WiFi outages +FR37: Device only updates the display after a complete, confirmed image transfer +FR38: Device renders a yellow border when WiFi is connected but the server sync fails +FR39: Device renders a red border when WiFi connectivity is unavailable + +FR40: Super admin can view a queue of user-submitted hard-delete requests and fulfill or dismiss them +FR41: Super admin can force an immediate hard delete of any image in the system +FR42: Super admin can view a device ownership transfer audit log +FR43: System automatically hard-deletes soft-deleted images that have no remaining approvals via a scheduled background process +FR44: Soft-deleted images with at least one active approval are retained until all approvals are removed + +### NonFunctional Requirements + +NFR1 (Performance): Image pre-rendering completes within 10 seconds of upload or approval trigger +NFR2 (Performance): Device image pull endpoint returns the pre-rendered binary asset within 10 seconds on typical home broadband +NFR3 (Performance): Web application pages load within 3 seconds on a standard broadband connection +NFR4 (Performance): Image rotation fires within ±5 minutes of the configured interval + +NFR5 (Security): All device-to-server communication occurs over HTTPS +NFR6 (Security): Device image pull requests are authenticated by MAC address; server rejects requests from unregistered MACs +NFR7 (Security): Email inline approve/decline actions use single-use authorization links that expire after use or after a configurable TTL +NFR8 (Security): User accounts are isolated — a user cannot access another user's images or devices without an explicit sharing action +NFR9 (Security): Super admin access is restricted to a single designated account +NFR10 (Security): Device ownership transfer requires physical access to the reset button + +NFR11 (Reliability): A device must never display a blank screen in normal operation — the last successfully transferred image persists indefinitely +NFR12 (Reliability): A display refresh only occurs after a complete, confirmed transfer; a mid-transfer interruption leaves the previous image on screen +NFR13 (Reliability): Soft-delete and scheduled cleanup run on a scheduled basis without manual intervention +NFR14 (Reliability): Breaking API changes are prohibited in V1 + +NFR15 (Accessibility): All primary user journeys complete successfully on iOS Safari (latest) and Android Chrome (latest) +NFR16 (Accessibility): Email sharing flows must work without requiring app installation or login on the recipient's device + +### Additional Requirements + +Architecture-derived technical requirements: +- **Starter template:** `symfony new pictureframe --webapp` — first implementation action +- **DDEV setup:** PHP 8.4, nginx-fpm, PostgreSQL 16 — mirror aqua-iq `.ddev/config.yaml`; add `docker-compose.imagick.yaml` for Imagick extension +- **Domain first:** `pictureframe.edholm.me` must be established and Nginx configured on VPS before firmware build constants are set +- **Imagick:** Must be installed in DDEV container and on VPS before image processing worker can run +- **Symfony Messenger:** Doctrine transport, `image_processing` queue, `max_retries: 1` +- **Symfony Scheduler:** Required for rotation engine and cleanup jobs +- **Storage:** `storage/images/` directory, `STORAGE_PATH` env var, relative paths in DB +- **Git / CI:** Repository at `git.edholm.me`; Gitea Actions CI workflow +- **Critical implementation rule:** Device pull endpoint returns 204 (no ready image) vs 404 (unknown MAC) — must never confuse the two +- **Token entity:** UUID PK, TokenType enum (ShareApprove, ShareDecline, HardDeleteConfirm), expires_at, used_at +- **Enums:** PHP backed enums for RenderStatus, TokenType, Orientation +- **Repository naming:** `findActive*` prefix on all soft-delete-aware methods +- **No OTA firmware:** API contract is stable by design; breaking changes require physical reflash + +### FR Coverage Map + +_To be completed in Step 3 (epic design)_ + +## Epic List + +_To be completed in Step 2_ diff --git a/_bmad-output/planning-artifacts/prd-validation-report.md b/_bmad-output/planning-artifacts/prd-validation-report.md new file mode 100644 index 0000000..e284d4c --- /dev/null +++ b/_bmad-output/planning-artifacts/prd-validation-report.md @@ -0,0 +1,430 @@ +--- +validationTarget: '_bmad-output/planning-artifacts/prd.md' +validationDate: '2026-04-27' +inputDocuments: [] +validationStepsCompleted: ['step-v-01-discovery', 'step-v-02-format-detection', 'step-v-03-density-validation', 'step-v-04-brief-coverage-validation', 'step-v-05-measurability-validation', 'step-v-06-traceability-validation', 'step-v-07-implementation-leakage-validation', 'step-v-08-domain-compliance-validation', 'step-v-09-project-type-validation', 'step-v-10-smart-validation', 'step-v-11-holistic-quality-validation', 'step-v-12-completeness-validation'] +validationStatus: COMPLETE +holisticQualityRating: '4/5 - Good' +overallStatus: 'Warning' +--- + +# PRD Validation Report + +**PRD Being Validated:** `_bmad-output/planning-artifacts/prd.md` +**Validation Date:** 2026-04-27 + +## Input Documents + +- PRD: `prd.md` ✓ +- Product Brief: (none found) +- Research: (none found) +- Additional References: (none) + +## Validation Findings + +## Format Detection + +**PRD Structure:** +- ## Executive Summary +- ## Success Criteria +- ## Product Scope +- ## User Journeys +- ## IoT/Embedded + SaaS Specific Requirements +- ## Project Classification +- ## Project Scoping +- ## Functional Requirements +- ## Non-Functional Requirements + +**BMAD Core Sections Present:** +- Executive Summary: Present ✓ +- Success Criteria: Present ✓ +- Product Scope: Present ✓ +- User Journeys: Present ✓ +- Functional Requirements: Present ✓ +- Non-Functional Requirements: Present ✓ + +**Format Classification:** BMAD Standard +**Core Sections Present:** 6/6 + +## Information Density Validation + +**Anti-Pattern Violations:** + +**Conversational Filler:** 0 occurrences + +**Wordy Phrases:** 0 occurrences + +**Redundant Phrases:** 0 occurrences + +**Total Violations:** 0 + +**Severity Assessment:** Pass + +**Recommendation:** PRD demonstrates good information density with minimal violations. + +## Product Brief Coverage + +**Status:** N/A - No Product Brief was provided as input + +## Measurability Validation + +### Functional Requirements + +**Total FRs Analyzed:** 44 + +**Format Violations:** 22 (informational — IoT context) + +FRs 9, 14, 18, 19, 20, 23, 24, 25, 27, 28, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 43, 44 use system/device behavioral phrasing rather than the BMAD `[Actor] can [capability]` pattern. This is contextually appropriate for IoT firmware and server automation requirements, but is noted as a deviation. No fix required unless downstream LLM consumers need strict format normalization. + +**Subjective Adjectives Found:** 0 + +**Vague Quantifiers Found:** 0 + +**Implementation Leakage:** 4 occurrences + +- FR18: "served via tokenized URL" — mechanism rather than capability +- FR19: "served via tokenized URL" — mechanism rather than capability +- FR27: "device's MAC address pre-coded in the URL" — URL construction detail +- FR43: "via scheduled cron job" — implementation mechanism (could be "via automated scheduled process") + +**FR Violations Total (clear):** 4 + +### Non-Functional Requirements + +**Total NFRs Analyzed:** 11 + +**Missing Metrics:** 1 + +- Performance: "Image pre-rendering completes before the user's next workflow action depends on the result" — No specific time bound. Cannot be tested to a number. Could become: "Image pre-rendering completes within X seconds of upload/approval trigger." + +**Incomplete Template:** 1 + +- Accessibility: "The web application must be navigable by non-technical users on iOS Safari and Android Chrome" — "navigable" is undefined and untestable. "Non-technical users" is subjective. No measurement method. Could become: "All primary user journeys complete in ≤ N taps/clicks on iOS Safari and Android Chrome." + +**Implementation Leakage in NFRs:** 1 + +- Reliability: "Soft-delete and cron-based cleanup run on a scheduled basis without manual intervention" — "cron-based" prescribes the implementation mechanism. Could be: "Cleanup runs on a scheduled basis without manual intervention." + +**NFR Violations Total:** 3 + +### Overall Assessment + +**Total Requirements:** 55 (44 FRs + 11 NFRs) +**Total Clear Violations:** 7 (4 FR + 3 NFR) +**Format Deviations (informational):** 22 FRs + +**Severity:** Warning (5–10 violations) + +**Recommendation:** PRD would benefit from tightening a handful of NFRs and removing implementation mechanism references from FRs. The format deviations in device/system behavioral FRs are appropriate for an IoT project and do not require changes unless strict `[Actor] can` normalization is needed for downstream consumers. + +## Traceability Validation + +### Chain Validation + +**Executive Summary → Success Criteria:** Intact + +Vision (effortless gift, family-living, e-ink aesthetic) maps cleanly to all three success dimensions (User, Business, Technical). No misalignment. + +**Success Criteria → User Journeys:** Mostly Intact — 1 gap + +- Yellow border / sync-fail state (WiFi up, server unreachable) is cited in Technical Success Criteria and FR38 but has no supporting user journey. Journey 4 covers WiFi loss (red border) but does not walk through the sync-fail scenario. + +**User Journeys → Functional Requirements:** Mostly Intact — 3 gaps + +Journeys 1–5 collectively provide strong coverage. Three FRs have no supporting user journey, though all trace to scope or success criteria: +- FR15 (global pre-loaded image pool, admin-managed): no journey demonstrates this admin workflow +- FR21 (approve all images in a collection in one action): no journey exercises this capability +- FR38 (yellow border on sync failure): mentioned in journey capability summaries but no journey narrative demonstrates it + +**Scope → FR Alignment:** Intact + +All V1 scope items (firmware, web app, server) map to FRs. Deep sleep (listed in scope and IoT section) is not explicitly an FR — this is defensible as an implementation detail with no user-observable behavior, and battery efficiency is contextually understood from the IoT requirements section. + +### Orphan Elements + +**Orphan Functional Requirements:** 0 + +All FRs trace back to at least one of: Executive Summary, Success Criteria, Product Scope, or User Journeys. No truly orphaned requirements found. + +**Unsupported Success Criteria:** 0 + +All success criteria have at least one backing journey or scope item. + +**User Journeys Without FRs:** 0 + +Every journey is fully supported by functional requirements. + +### Traceability Matrix + +| Journey | Primary FRs | +|---|---| +| Margaret (recipient) | FR24–29, FR30, FR34–36, FR38–39 | +| Matt (gift giver/admin) | FR1–10, FR11, FR16, FR33, FR42 | +| Sarah (contributor) | FR1, FR11, FR17–20 | +| Margaret offline (edge case) | FR36, FR39, FR34 | +| Matt as super admin | FR22, FR23, FR40–42 | +| No journey (scope-only FRs) | FR15, FR21, FR38 | + +**Total Traceability Issues:** 4 (1 success criterion gap, 3 journey-coverage gaps; no orphan FRs) + +**Severity:** Warning + +**Recommendation:** Traceability chain is fundamentally sound with no orphan requirements. Consider adding a brief Journey 6 (or extending Journey 2) to demonstrate sync-fail yellow border, global image pool management, and collection bulk-approve. This would give downstream LLM consumers clear behavioral context for FR15, FR21, and FR38. + +## Implementation Leakage Validation + +### Leakage by Category + +**Frontend Frameworks:** 0 violations + +**Backend Frameworks:** 0 violations + +**Databases:** 0 violations + +**Cloud Platforms:** 0 violations + +**Infrastructure:** 2 violations +- FR43: "via scheduled cron job" — prescribes mechanism; should be "via automated scheduled process" +- NFR Reliability: "cron-based cleanup" — same mechanism leakage + +**Libraries:** 0 violations + +**Other Implementation Details (URL/mechanism):** 4 violations +- FR18: "device-selection page (served via tokenized URL, no login required)" — URL construction mechanism; capability is "accessible without login via a single-use link" +- FR19: "the device-selection page is served via tokenized URL" — same +- FR27: "device's MAC address pre-coded in the URL" — URL structure detail; capability is "links to the account setup page for that specific device" +- NFR Security: "single-use tokenized URLs that expire after use or after a configurable TTL" — mechanism; capability is "single-use authorization links that expire after use" + +**Note:** HTTPS in Security NFRs is capability-relevant (mandating encrypted transport is a security requirement, not implementation). MAC address as device authentication identity is similarly capability-relevant for IoT. These are not flagged as violations. + +### Summary + +**Total Implementation Leakage Violations:** 6 + +**Severity:** Critical (>5 violations) + +**Recommendation:** All violations are mechanism-level descriptions (URL construction, scheduling mechanism) rather than technology-stack prescriptions — there are no framework, database, or cloud platform leaks. Nevertheless, the PRD should replace mechanism terms with capability-outcome language in FR18, FR19, FR27, FR43, and the two NFR references to "cron-based" to keep requirements properly HOW-agnostic. + +## Domain Compliance Validation + +**Domain:** general_consumer_family +**Complexity:** Low (general/standard) +**Assessment:** N/A - No special domain compliance requirements + +**Note:** This PRD is for a standard consumer domain without regulatory compliance requirements (not healthcare, fintech, govtech, or other regulated industries). + +## Project-Type Compliance Validation + +**Project Type:** iot_embedded + saas_multi_user (composite) + +### IoT/Embedded Required Sections + +**hardware_reqs:** Present ✓ — "Hardware Requirements" in IoT/Embedded section documents ESP32, Waveshare display, SPI pinout, power, reset mechanism + +**connectivity_protocol:** Present ✓ — "Connectivity & Provisioning" documents AP mode, STA mode, two-phase provisioning, image transport (HTTPS), failure behavior + +**power_profile:** Present ✓ — "Power Profile" documents e-ink zero-power-at-rest, deep sleep, battery efficiency via rotation frequency + +**security_model:** Present ✓ — "Device Identity & Security Model" documents MAC→account mapping, per-request MAC validation, HTTPS requirement, re-provisioning model + +**update_mechanism:** Intentionally excluded (N/A for V1) — PRD explicitly documents "No OTA in V1; API endpoint format is a stable contract by design." Physical reflash on breaking changes treated as last resort. ✓ (documented decision) + +### SaaS/Multi-User Required Sections + +**tenant_model:** Present ✓ — "Multi-Tenancy & Permission Model" documents isolated tenant accounts, device ownership, shared image references + +**rbac_matrix:** Present ✓ — Role table documents User / Super admin / Device capabilities + +**subscription_tiers:** Intentionally post-MVP — "System architecture keeps a monetization path open without building for it in V1." ✓ (documented decision) + +**integration_list:** Intentionally post-MVP — Apple Photos / Google Photos deferred to Post-MVP section. ✓ (documented decision) + +**compliance_reqs:** N/A — General consumer domain (no regulatory compliance required) + +### Excluded Sections (Should Not Be Present) + +**visual_ui:** Absent ✓ — No visual/UI design section (device border color behaviors are capability requirements, not UI design) + +**browser_support:** Absent ✓ — No browser compatibility matrix section + +**cli_interface:** Absent ✓ + +### Compliance Summary + +**IoT Required Sections:** 4/4 present (update_mechanism intentionally excluded, documented) +**SaaS Required Sections:** 2/2 applicable present (2 intentionally post-MVP, 1 N/A domain) +**Excluded Sections Present:** 0 + +**Severity:** Pass + +**Recommendation:** All project-type-specific required sections are present or have documented intentional exclusions. The composite IoT+SaaS classification is well-served by the dedicated "IoT/Embedded + SaaS Specific Requirements" section, which covers all critical concerns for this project type. + +## SMART Requirements Validation + +**Total Functional Requirements:** 44 + +### Scoring Summary + +**All scores ≥ 3:** 100% (44/44) — no FR scores below acceptable threshold +**All scores ≥ 4:** 86% (38/44) — 6 FRs have at least one score of exactly 3 +**Overall Average Score:** 4.6/5.0 + +### FRs With Any Score = 3 (Improvement Candidates) + +| FR # | Specific | Measurable | Attainable | Relevant | Traceable | Avg | Issue | +|------|----------|------------|------------|----------|-----------|-----|-------| +| FR3 | 3 | 3 | 5 | 5 | 4 | 4.0 | "manage" is broad — what operations does admin perform on accounts? | +| FR10 | 3 | 3 | 5 | 5 | 4 | 4.0 | "view, configure, and manage any device" — same vagueness as FR3 | +| FR15 | 3 | 3 | 5 | 5 | 3 | 3.8 | "manage a global pre-loaded image pool" — operations unspecified; no journey | +| FR19 | 3 | 4 | 5 | 5 | 4 | 4.2 | Partial overlap with FR18; "any email client" is unbounded | +| FR21 | 5 | 5 | 5 | 5 | 3 | 4.6 | Capability is clear and testable; traceability gap (no journey) | +| FR38 | 5 | 5 | 5 | 5 | 3 | 4.6 | Yellow border well-defined; traceability gap (no journey) | + +### All Other FRs: Scoring Summary + +All remaining 38 FRs (FR1–2, FR4–9, FR11–14, FR16–18, FR20, FR22–37, FR39–44) score 4 or 5 across all SMART criteria. No improvement suggestions needed. + +### Improvement Suggestions + +**FR3 / FR10:** Replace "manage" with the specific operations intended. Example: "Super admin can view, rename, transfer, and delete any device across all accounts." + +**FR15:** Specify what "manage" means for the global pool. Example: "Super admin can add images to and remove images from a global pre-loaded image pool available to all devices." + +**FR19:** Consider collapsing into FR18 or rephrasing to remove the unbounded "any email client" claim. The capability (login-free, link-based approval) is already covered by FR18. + +**FR21, FR38:** Traceability-only gaps. Capability definitions are strong. Adding a user journey (see Traceability section) would fully resolve. + +### Overall Assessment + +**Severity:** Pass — 0% flagged (no FR scores < 3); 14% with marginal 3-scores + +**Recommendation:** Functional Requirements demonstrate strong SMART quality overall. Minor improvements to "manage" verb specificity in FR3, FR10, and FR15 would raise quality without restructuring. FR19 may be consolidated with FR18. + +## Holistic Quality Assessment + +### Document Flow & Coherence + +**Assessment:** Good + +**Strengths:** +- Executive Summary is distinctive and compelling. The "act of making" differentiator is articulate and immediately communicated. +- User journeys are narrative-driven with named personas and concrete scenarios — unusually good for a PRD of this type. +- FR groupings (User & Account, Device Management, Image Library, Approval & Sharing, etc.) are logical and enable clean epic breakdown. +- Risk mitigation table in Scoping section demonstrates mature product thinking. +- IoT section covers hardware, connectivity, power, security, and rendering pipeline comprehensively. + +**Areas for Improvement:** +- `## IoT/Embedded + SaaS Specific Requirements` is placed between User Journeys and Project Classification, slightly interrupting the section flow before reaching FRs. Consider moving it to follow FRs or creating a dedicated "Technical Context" heading. +- `## Project Classification` and `## Project Scoping` sections are bureaucratic in tone relative to the narrative voice of the rest of the document — they read as BMAD workflow artifacts rather than PRD content. + +### Dual Audience Effectiveness + +**For Humans:** +- Executive-friendly: Excellent — vision, differentiator, and success criteria are immediately accessible. "The differentiator is not a feature set. It is the act of making." is the strongest line in the PRD. +- Developer clarity: Excellent — IoT section provides pinout constants, format specs, provisioning state machine, and security model with enough precision to build from. +- Designer clarity: Good — User journeys provide rich behavioral context. No explicit UX requirements section, but appropriate for pre-design PRD stage. +- Stakeholder decision-making: Strong — scope, risks, and intentional deferral decisions are clearly documented. + +**For LLMs:** +- Machine-readable structure: Strong — consistent L2 headers, tables, numbered FRs enable reliable extraction. +- UX readiness: Good — journey narratives and capability summaries provide behavioral context for LLM UX generation. +- Architecture readiness: Excellent — IoT section defines hardware identity, security model, rendering pipeline, connectivity protocol, and power profile with architectural precision. +- Epic/Story readiness: Good — FR groupings and Journey Requirements Summary table map cleanly to epics. + +**Dual Audience Score:** 4/5 + +### BMAD PRD Principles Compliance + +| Principle | Status | Notes | +|-----------|--------|-------| +| Information Density | Met | 0 anti-pattern violations; every sentence carries weight | +| Measurability | Partial | 7 violations (mechanism leakage, 1 missing NFR metric, 1 vague accessibility NFR) | +| Traceability | Partial | 0 orphan FRs; 3 journey-coverage gaps (FR15, FR21, FR38) | +| Domain Awareness | Met | IoT section comprehensive; general consumer domain correctly handled | +| Zero Anti-Patterns | Met | 0 filler/padding violations detected | +| Dual Audience | Met | Works well for both human readers and LLM consumers | +| Markdown Format | Met | Consistent L2 headers, tables, and structured lists throughout | + +**Principles Met:** 5/7 + +### Overall Quality Rating + +**Rating:** 4/5 — Good: Strong with minor improvements needed + +**Scale:** +- 5/5 - Excellent: Exemplary, ready for production use +- 4/5 - Good: Strong with minor improvements needed +- 3/5 - Adequate: Acceptable but needs refinement +- 2/5 - Needs Work: Significant gaps or issues +- 1/5 - Problematic: Major flaws, needs substantial revision + +### Top 3 Improvements + +1. **Remove mechanism leakage from FR18, FR19, FR27, FR43, and 2 NFRs** + Highest-count finding (6 violations). Replace "tokenized URL", "MAC address pre-coded in the URL", and "cron job" with capability-outcome language. Each fix is a one-sentence edit. + +2. **Add a 6th user journey covering sync-fail yellow border, collection bulk-approve, and global image pool management** + Closes the three journey-traceability gaps for FR15, FR21, and FR38 in one addition. A short Matt-as-admin journey covering a day of routine maintenance would suffice. + +3. **Tighten the two weak NFRs: pre-rendering metric and accessibility requirement** + "Completes before the user's next workflow action" needs a concrete time bound. "Navigable by non-technical users" needs a measurable accessibility standard (e.g., "all primary journeys completable in ≤ 5 steps on iOS Safari and Android Chrome"). + +### Summary + +**This PRD is:** A high-quality, narrative-driven document with excellent IoT technical coverage and strong user journeys — the mechanism leakage and two weak NFRs are the only substantive issues, and all are one-sentence fixes. + +**To make it great:** Focus on the top 3 improvements above, particularly removing mechanism terms from FRs and adding the missing 6th journey. + +## Completeness Validation + +### Template Completeness + +**Template Variables Found:** 0 + +Note: `{APP_BASE_URL}/setup/{mac_address}` at line 189 is an intentional URL format example inside an inline code block — not an unfilled template variable. ✓ + +### Content Completeness by Section + +**Executive Summary:** Complete ✓ — Vision, target users (recipient/contributor/admin), differentiator, and technical design philosophy all present + +**Success Criteria:** Complete ✓ — User, Business, and Technical success dimensions present; Measurable Outcomes subsection provides specific quantifiable metrics + +**Product Scope:** Complete ✓ — V1 scope (firmware/web app/server), Post-MVP, and Vision all present with explicit intentional exclusions documented + +**User Journeys:** Complete ✓ — 5 journeys covering recipient, gift giver/admin, contributor, offline edge case, and super admin; summary table present + +**Functional Requirements:** Complete ✓ — 44 FRs grouped by domain, covering all V1 scope items + +**Non-Functional Requirements:** Complete ✓ — Performance, Security, Reliability, and Accessibility addressed; 2 quality issues noted (step 5) but sections are present + +**Additional Sections:** Complete ✓ — IoT/Embedded Specific Requirements, Project Classification, Project Scoping all present and populated + +### Section-Specific Completeness + +**Success Criteria Measurability:** Most — quantitative metrics in Measurable Outcomes subsection; qualitative User Success statements are acceptable for experience-oriented product + +**User Journeys Coverage:** Yes — all 5 user types (recipient, builder, contributor, offline, admin) represented + +**FRs Cover MVP Scope:** Yes — all V1 scope items (firmware, web app, server) have corresponding FRs + +**NFRs Have Specific Criteria:** Most — 2 NFRs lack specific metrics (pre-rendering time bound; accessibility standard) as noted in step 5 + +### Frontmatter Completeness + +**stepsCompleted:** Present ✓ +**classification:** Present ✓ (projectType, domain, complexity, projectContext all populated) +**inputDocuments:** Present ✓ (empty array, valid — no input documents used) +**date:** Minor gap — date appears in document body (`**Date:** 2026-04-27`) but not as a frontmatter field + +**Frontmatter Completeness:** 3.5/4 (date field present in body, absent from YAML frontmatter) + +### Completeness Summary + +**Overall Completeness:** 97% + +**Critical Gaps:** 0 +**Minor Gaps:** 2 (date not in YAML frontmatter; 2 NFRs lack specific metrics) + +**Severity:** Pass + +**Recommendation:** PRD is substantively complete with all required sections and content present. Add `date: '2026-04-27'` to frontmatter YAML for full compliance. diff --git a/_bmad-output/planning-artifacts/prd.md b/_bmad-output/planning-artifacts/prd.md new file mode 100644 index 0000000..56e31eb --- /dev/null +++ b/_bmad-output/planning-artifacts/prd.md @@ -0,0 +1,357 @@ +--- +stepsCompleted: ['step-01-init', 'step-02-discovery', 'step-02b-vision', 'step-02c-executive-summary', 'step-03-success', 'step-04-journeys', 'step-05-domain', 'step-06-innovation', 'step-07-project-type', 'step-08-scoping', 'step-09-functional', 'step-10-nonfunctional', 'step-11-polish'] +inputDocuments: [] +workflowType: 'prd' +date: '2026-04-27' +classification: + projectType: 'iot_embedded + saas_multi_user' + domain: 'general_consumer_family' + complexity: 'medium' + projectContext: 'greenfield' +--- + +# Product Requirements Document - pictureFrame + +**Author:** Matt Edholm +**Date:** 2026-04-27 + +## Executive Summary + +pictureFrame is a handcrafted e-ink digital picture frame ecosystem built to be given as a meaningful gift. The physical frame — custom-built around a Waveshare e-ink display driven by an ESP32 — is battery-powered or plugin at the recipient's choice. A companion web application manages image libraries, device configuration, family sharing, and the rotation cycle that keeps each frame alive over time. The frame displays one image per configured interval, cycling through a curated pool of approved photos drawn from family uploads, shared images, or linked photo collections. Setup requires only scanning two QR codes; ongoing use requires no interaction unless the recipient wants it. + +Target users are gift recipients — family members who receive a frame — and the family network that contributes by uploading and sharing photos. A super-admin account manages the global image pool, device fleet, and system-level operations. + +### What Makes This Special + +Commercial digital frames are purchased. This one is built — by hand, with intent, for a specific person. The e-ink display is a deliberate aesthetic choice: it looks like something that belongs on a wall, not a screen. Battery power removes the constraint of proximity to an outlet. The software doesn't add friction — it quietly keeps the frame personal and current as the family adds to it over time. The differentiator is not a feature set. It is the act of making. + +The technical decisions — server-side image processing, thin ESP32 client, per-device approval workflows, uniqueness-tracked rotation, email-based sharing with inline approve/disapprove — all serve a single goal: the frame should feel effortless to receive and effortless to live with. + +## Success Criteria + +### User Success + +- The frame rotates images on the configured schedule without user intervention. A recipient who never opens the app is a fully successful user. +- A frame never displays a blank screen. The last successfully transferred image persists indefinitely through WiFi outages, sync failures, and power loss. +- First-time setup completes without technical assistance: scan AP QR → enter WiFi credentials → scan setup QR → log in → device linked. +- Status is communicated passively via border color. No notification, no app required to understand device state. + +### Business Success + +- Every gifted frame works reliably for a non-technical recipient from day one. +- The web app supports extended family participation without requiring it — participation is optional, not a success condition. +- System architecture keeps a monetization path open without building for it in V1. + +### Technical Success + +- Image transfers are atomic. A display refresh only occurs after a complete, confirmed transfer. A mid-transfer power loss leaves the previous image on screen. +- Server pre-renders all images per device display model and orientation at upload/approval time. The ESP32 never performs image transformation. +- Soft-delete and cron-based cleanup run without manual intervention. No orphaned assets accumulate. +- QR-based provisioning succeeds on first attempt on both iOS and Android. +- Yellow border (sync fail) and red border (no WiFi) render correctly and consistently. + +### Measurable Outcomes + +- Zero blank frames in normal operation +- Provisioning first-attempt success rate: ≥95% on standard home WiFi +- Image cycle runs on schedule ±5 minutes of configured interval +- Sync failure surfaced visually within one cycle period + +## Product Scope + +### V1 — Full Feature Set + +Everything described in this document is V1. No phased rollout. + +**Firmware (ESP32):** +- Two-phase provisioning: AP mode (WiFi credentials only) → STA mode (account setup via web app QR) +- Timer-based image pull from server +- Atomic image write (display only refreshes after confirmed transfer) +- Per-device orientation (landscape/portrait) +- Border status indicators: yellow (sync fail), red (no WiFi) +- 5-second button hold to reset and re-provision +- Deep sleep between pull cycles + +**Web Application:** +- Open registration (email + password) +- Device registry: naming, orientation, rotation frequency, uniqueness window +- Image upload and library management (Uploaded / Shared filter) +- Per-device image approval workflow +- Collections support (approve all images in a collection) +- Email sharing with inline approve/disapprove (tokenized links, client-agnostic) +- Global pre-loaded image pool (admin-managed, available to all devices) +- Admin panel: fleet management, delete request queue, force hard delete, device audit log + +**Server:** +- Image cycle engine: uniqueness tracking (window capped at available image count), rotation scheduling +- Pre-rendering pipeline: display-ready 4bpp assets generated per device model and orientation at upload/approval time +- Soft-delete + cron-based hard-delete (cleans when no approvals remain) +- Device ownership transfer: atomic purge and relink on re-provisioning to new account +- MAC-authenticated image endpoint + +### Post-MVP + +- Apple Photos and Google Photos collection integration (link album → all images auto-approved) +- Additional Waveshare display sizes as hardware is defined +- Monetization layer (subscription tiers — architecture supports it, not built in V1) + +### Vision + +- Multiple display form factors and custom frame hardware kits +- Broader family social features +- Potential commercial distribution of frames as a product + +## User Journeys + +### Journey 1: Margaret — The Recipient *(Primary User, Happy Path)* + +Margaret is 74. She lives alone in Fort Wayne. Her kids worry she spends too much time looking at the same four photos on her refrigerator. One Christmas morning she unwraps a wooden frame — heavier than she expected. There's a small card: *"Scan the QR code to connect to WiFi."* + +She holds her phone up to the QR code on the screen. Her camera app asks if she wants to join "PictureFrame-A3F2." She taps yes. A simple page opens asking for her home WiFi password. She types it in and taps Connect. The display updates — a new QR appears with the message "Connected — scan to finish setup." She scans it. A web page opens. She logs in with the email her son set up for her. The frame reboots. Twenty seconds later, a photo of her grandchildren at a lake house fills the screen. She doesn't touch it again for three months. + +Six weeks in, the frame cycles to a photo she's never seen before — her daughter uploaded it from a birthday party. She didn't ask for it. She didn't approve it. It just appeared. She calls her daughter. That's the product working. + +**Capabilities revealed:** Two-phase QR provisioning, captive portal WiFi entry, setup-page account login, timer-based rotation, family image sharing, passive recipient experience with zero ongoing interaction required. + +--- + +### Journey 2: Matt — The Builder *(Gift Giver, Setup Path)* + +Three weeks before Christmas, Matt flashes new firmware to an ESP32, wires it to the Waveshare display, and fits it into a custom walnut frame he's been working on. He scans the provisioning QR, joins the AP, enters the home WiFi credentials. The frame connects; the success screen shows a setup QR. He scans it, logs in as admin, and names the device *"Margaret's Frame."* He sets orientation to portrait, rotation to daily, uniqueness window to 30. + +He uploads twelve photos from his phone — Thanksgiving, a lake trip, a grandkid's first birthday. He approves all twelve for Margaret's device from the web app. He pulls up the server logs, confirms the pre-rendered assets are ready. He powers the frame off, packs it in a box with a typed card explaining the QR code, and ships it. + +On Christmas morning he gets a text: *"The frame is beautiful."* He opens the admin panel and confirms the device is online. + +**Capabilities revealed:** Two-phase provisioning, super-admin device naming, orientation/frequency/uniqueness settings, image upload, per-device approval, server pre-rendering pipeline, admin monitoring. + +--- + +### Journey 3: Sarah — The Contributor *(Family Member, Sharing Path)* + +Sarah is Matt's sister. She gets an email in January: *"Matt has added you to the pictureFrame family."* She clicks the link, creates an account, and uploads three photos — her kids, a recent trip, a candid from the holidays. + +She sees Margaret in the shared user list. She taps "Share to Margaret" on one photo. An email lands in Margaret's inbox: *"Sarah shared a photo with you."* The email shows the image and two buttons: **Approve** and **Decline.** Margaret taps Approve. A simple page opens — no login required — asking which frame to add the photo to. Margaret picks her living room frame and taps Done. The photo enters the approved pool for that device and will appear within the next cycle. + +Sarah never needed to understand approvals, device settings, or the cycle engine. She just shared a photo. + +**Capabilities revealed:** Family account creation, image upload, user-to-user sharing, email approve/decline flow, tokenized device-selection page, image entering approved pool for selected device(s). + +--- + +### Journey 4: Margaret's Frame Goes Offline *(Primary User, Edge Case)* + +February. Margaret's internet provider does a firmware update on her router at 3am. The frame tries its scheduled pull at 6am and can't reach the server. It draws a red border around the photo on screen and goes back to sleep. + +Margaret notices it at breakfast. She calls her son. He's not home. She forgets about it. + +Her router comes back online at noon. At the next scheduled pull the frame reaches the server, retrieves the next image, completes the atomic transfer, refreshes the display. The border disappears. Margaret notices it looks different. She doesn't know what changed. + +**Capabilities revealed:** Red border on WiFi failure, silent retry on reconnect, last-image persistence, no user action required for recovery. + +--- + +### Journey 5: Matt as Super Admin *(Admin Path)* + +In March, Sarah decides she wants a photo permanently removed — an old picture she regrets uploading. She can soft-delete it from her library, but she wants it gone from the server entirely. She taps "Request hard delete" from the image detail page. + +Matt gets a notification in the admin panel: *"Sarah has requested a hard delete for 1 image."* He reviews the image, confirms it's not approved on any device, and clicks force hard delete. The file is removed from storage. Sarah gets a confirmation email. + +Later that week, a second frame gets physically reset by accident. A new family member completes provisioning and claims the device. The server atomically purges the previous image history and links the device to the new account. Matt sees the ownership transfer in the device audit log. + +**Capabilities revealed:** User hard-delete request, admin delete request queue, force hard delete, device ownership transfer audit log. + +--- + +### Journey Requirements Summary + +| Journey | Capabilities Required | +|---|---| +| Margaret (recipient) | Two-phase QR provisioning, captive portal WiFi entry, timer pull, border indicators, passive rotation | +| Matt (gift giver) | Device naming, settings config, image upload, approval, pre-rendering, admin monitoring | +| Sarah (contributor) | Account creation, image upload, share to device, email approve/decline flow | +| Margaret offline (edge case) | WiFi failure detection, red border, silent retry, last-image persistence | +| Matt as admin | Hard-delete request queue, force delete, ownership transfer audit log | + +## IoT/Embedded + SaaS Specific Requirements + +### Hardware Requirements + +- **Microcontroller:** ESP32 (esp32dev) — dual-core, 240MHz, 4MB flash, WiFi 802.11 b/g/n +- **Display:** Waveshare 7.3" 6-color e-ink (E-Ink Spectra 6), 800×480px, SPI interface — extensible to additional Waveshare models as device types +- **SPI pinout:** Per-hardware-variant firmware build constant. Current POC: SCK=18, MOSI=23, CS=5, DC=17, RST=16, BUSY=4. All-in-one ESP32+display boards (e.g. Waveshare ESP32 driver boards with ribbon cable) may define pins differently. Device type registry on the server tracks display model; firmware is built per hardware variant. +- **Power:** Battery or plugin at recipient's choice. E-ink retains image with zero power draw between refreshes. +- **Reset mechanism:** Physical button, 5-second hold triggers re-provisioning + +### Connectivity & Provisioning + +- **Normal operation:** WiFi STA mode, timer-based wake → pull → sleep cycle +- **Phase 1 (AP mode):** Device broadcasts provisioning AP. QR code on e-ink encodes AP SSID. Captive portal collects home WiFi SSID and password only — no server dependency, no account flow. +- **Phase 2a (success):** Device connects to home WiFi in STA mode. E-ink shows success message and a QR encoding `{APP_BASE_URL}/setup/{mac_address}` — a firmware build constant established after DNS is set up. User scans → web app → register or log in → device linked. +- **Phase 2b (failure):** E-ink fills red, AP re-activates, provisioning QR redisplays automatically. No user intervention required to retry. +- **Image transport:** HTTPS GET to server endpoint; pre-rendered binary asset returned +- **Status:** WiFi failure → red border. Sync failure (WiFi up, server unreachable) → yellow border. Both rendered locally without server involvement. + +### Power Profile + +- E-ink draws power only during refresh; image persists indefinitely with zero power. +- Deep sleep between pull cycles for battery efficiency. +- Rotation frequency configurable per device — longer intervals extend battery life significantly. +- No OTA in V1; API endpoint format is a stable contract by design. + +### Device Identity & Security Model + +- Each ESP32 is identified by its factory-burned MAC address. +- At provisioning, MAC→account mapping is stored server-side. +- Every image pull request includes the device MAC; server validates MAC is registered before serving. +- Re-provisioning to a new account atomically updates the MAC→account mapping and purges prior image history. Last-writer-wins — physical reset button is the authorization. +- HTTPS required for all device↔server communication. + +### Multi-Tenancy & Permission Model + +Each user account is an isolated tenant with its own image library and device registry. Devices are owned by exactly one account at a time. Shared images create a reference in the recipient's library, not a copy. + +| Role | Capabilities | +|---|---| +| User | Own account, own devices, own images; share images; approve/decline shared images | +| Super admin | All user capabilities + cross-tenant device management, force hard delete, delete request queue, global image pool management | +| Device (ESP32) | Pull pre-rendered image for registered account via MAC-authenticated endpoint | + +### Server-Side Rendering Pipeline + +All image transformation occurs at upload/approval time, not at request time. Server stores pre-rendered assets per device model and orientation (landscape 800×480, portrait 480×800; additional sizes added as device types are defined). ESP32 receives a ready-to-display binary asset — no transformation on device. Format: 4bpp packed, Waveshare 6-color palette (BLACK=0x0, WHITE=0x1, YELLOW=0x2, RED=0x3, BLUE=0x5, GREEN=0x6). + +### Implementation Constraints + +- Phase 1 captive portal has no external dependencies — collects WiFi credentials only. +- APP_BASE_URL and API base URL are firmware build constants; DNS must be established before firmware development begins. +- API endpoint format must remain stable across firmware versions; breaking changes require a firmware version bump. +- Soft-delete + cron cleanup handles shared-image retention without manual intervention. +- Firmware builds are 1:1 with hardware configurations (device type), not 1:1 with individual devices. + +## Project Classification + +| | | +|---|---| +| **Project Type** | IoT/Embedded + Multi-user SaaS | +| **Domain** | General / Consumer / Family | +| **Complexity** | Medium | +| **Project Context** | Greenfield | + +## Project Scoping + +### Strategy + +**Approach:** Experience MVP — value is the act of making and giving, not a tracked metric. V1 ships everything. Solo developer; single VPS; no phased rollout, no feature flags, no paid tier. + +**Build Order:** Web application and DNS are prerequisites. App domain and API base URL are established first, then baked into firmware as build constants. No OTA in V1 — a stable API contract is the update strategy. + +### Risk Mitigation + +| Risk | Mitigation | +|---|---| +| **Provisioning UX failure** | Two-phase e-ink flow uses QR at every state. Phase 1 requires no server. Phase 2 failure renders full red screen and re-activates AP automatically. Physical reset always returns to Phase 1. | +| **Server load / pre-rendering latency** | Pre-render is synchronous at upload time. No device ever waits for rendering at pull time. | +| **Blank frame** | Last image persists through power loss and outages. Yellow/red border signals state. Recovery is automatic on reconnect. | +| **Orphaned assets** | Soft-delete + cron cleanup runs without intervention. Shared images retained until last approval is removed. Super-admin force-delete handles user requests. | +| **Device ownership dispute** | Last-writer-wins on physical reset. Physical access to the reset button is the authorization model. First claim cannot block a subsequent physical reset. | + +## Functional Requirements + +### User & Account Management + +- FR1: Visitors can register a new account with an email address and password +- FR2: Registered users can log in to their account +- FR3: Super admin can view, edit, and delete any user account, device, or image across the system + +### Device Management + +- FR4: Users can register a device to their account via the provisioning setup flow +- FR5: Users can assign a name to each of their devices +- FR6: Users can configure display orientation (landscape or portrait) per device +- FR7: Users can configure the image rotation frequency per device +- FR8: Users can configure the uniqueness window (number of cycles before an image can repeat) per device +- FR9: When a device is re-provisioned to a new account, the system atomically purges the prior image history and transfers ownership +- FR10: Super admin can view, rename, reconfigure, and transfer any device across all accounts + +### Image Library + +- FR11: Users can upload photos to their personal image library +- FR12: Users can view their library filtered by source: Uploaded vs. Shared +- FR13: Users can soft-delete images from their library +- FR14: When an image is shared to a user, it appears in their library as a reference (not a copy) +- FR15: Super admin can add images to and remove images from a global pre-loaded image pool available to all devices + +### Image Approval & Sharing + +- FR16: Users can approve or decline images for a specific device in their account +- FR17: Users can share an image from their library to another user +- FR18: When an image is shared, the recipient receives an email containing the image and an approve action; clicking approve opens a device-selection page (no login required) where the recipient chooses which device(s) to add the image to, including an All Devices option +- FR19: The approval link works from any email client without requiring account creation or login +- FR20: Approved images enter the active rotation pool for the selected device(s) +- FR21: Users can approve all images within a collection for a device in a single action +- FR22: Users can request a full hard delete of their own image, which enters a super-admin review queue +- FR23: System sends confirmation to the user when their hard-delete request is fulfilled + +### Device Provisioning + +- FR24: Device enters provisioning mode when the reset button is held for 5 seconds +- FR25: In provisioning mode, device displays a scannable QR code for joining the provisioning access point +- FR26: User can enter home WiFi credentials through a captive portal served by the device +- FR27: On successful WiFi connection, device displays a QR code linking to the account setup page for that specific device +- FR28: On failed WiFi connection, device displays a failure indicator, reactivates the AP, and redisplays the provisioning QR code for retry +- FR29: Users can register a new account or log in to an existing account from the device setup page to link the device + +### Image Rotation & Cycle Engine + +- FR30: System automatically advances to the next approved image on the configured schedule for each device +- FR31: System tracks image display history per device to enforce the configured uniqueness window +- FR32: When the uniqueness window exceeds the count of available approved images, the window is treated as equal to the available image count +- FR33: System pre-renders images to display-ready format per device model and orientation at the time of upload or approval +- FR34: Device pulls its next pre-rendered image from the server on each scheduled cycle + +### Display & Status + +- FR35: Device displays the current image persistently with no power draw between refresh cycles +- FR36: Device retains and displays the last successfully transferred image through power loss and WiFi outages +- FR37: Device only updates the display after a complete, confirmed image transfer +- FR38: Device renders a yellow border when WiFi is connected but the server sync fails +- FR39: Device renders a red border when WiFi connectivity is unavailable + +### Admin & Moderation + +- FR40: Super admin can view a queue of user-submitted hard-delete requests and fulfill or dismiss them +- FR41: Super admin can force an immediate hard delete of any image in the system +- FR42: Super admin can view a device ownership transfer audit log +- FR43: System automatically hard-deletes soft-deleted images that have no remaining approvals via a scheduled background process +- FR44: Soft-deleted images with at least one active approval are retained until all approvals are removed + +## Non-Functional Requirements + +### Performance + +- Image pre-rendering completes within 10 seconds of upload or approval trigger +- Device image pull endpoint returns the pre-rendered binary asset within 10 seconds on typical home broadband +- Web application pages load within 3 seconds on a standard broadband connection +- Image rotation fires within ±5 minutes of the configured interval + +### Security + +- All device-to-server communication occurs over HTTPS +- Device image pull requests are authenticated by MAC address; the server rejects requests from unregistered MACs +- Email inline approve/decline actions use single-use authorization links that expire after use or after a configurable TTL +- User accounts are isolated — a user cannot access another user's images or devices without an explicit sharing action +- Super admin access is restricted to a single designated account +- Device ownership transfer requires physical access to the reset button; remote re-provisioning is not possible + +### Reliability + +- A device must never display a blank screen in normal operation — the last successfully transferred image persists indefinitely through outages and power loss +- A display refresh only occurs after a complete, confirmed transfer; a mid-transfer interruption leaves the previous image on screen +- Soft-delete and scheduled cleanup run on a scheduled basis without manual intervention +- Breaking API changes are prohibited in V1. Any change to the API contract requires physical reflash of all deployed devices and is treated as a last resort. + +### Accessibility + +- All primary user journeys complete successfully on iOS Safari (latest) and Android Chrome (latest) +- Email sharing flows must work without requiring app installation or login on the recipient's device