44783d0f46
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>
582 lines
28 KiB
Markdown
582 lines
28 KiB
Markdown
---
|
||
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
|