docs: add planning artifacts — PRD, architecture, epics skeleton
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user