docs: update architecture to reflect Vue 3 SPA frontend decision
Replace AssetMapper + Stimulus + Turbo with Vue 3 SPA (Vite, TypeScript strict, SCSS modules, Konva.js). Authenticated app is now a full SPA served by Symfony catch-all; public flows (provisioning, email approve/decline) remain Symfony + Twig. Add JSON API controllers for SPA, SpaController catch-all, updated directory structure, and revised implementation sequence. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -72,7 +72,7 @@ symfony new pictureframe --webapp
|
||||
|
||||
**Language & Runtime:** PHP 8.4+, Symfony 8.0
|
||||
|
||||
**Templating:** Twig with Stimulus and UX Turbo (Hotwire) — lightweight interactivity without a JS build step
|
||||
**Templating:** Twig — used only for public flows (provisioning setup, email approve/decline pages). Authenticated app is a Vue 3 SPA; Twig is not used there.
|
||||
|
||||
**ORM & Database:** Doctrine ORM, PostgreSQL 16 (DDEV local), migrations via `doctrine/migrations`
|
||||
|
||||
@@ -80,17 +80,19 @@ symfony new pictureframe --webapp
|
||||
|
||||
**Email:** Symfony Mailer — handles sharing flows and hard-delete confirmations
|
||||
|
||||
**Build Tooling:** AssetMapper (no Webpack/Node build step required)
|
||||
**Build Tooling:** Vite (Vue SPA, outputs to `public/build/`). AssetMapper not used — no Stimulus, no Turbo, no `importmap.php`.
|
||||
|
||||
**Testing:** PHPUnit via `symfony/test-pack`
|
||||
|
||||
**Code Organization:** Standard Symfony structure — `src/Entity`, `src/Controller`, `src/Repository`, `src/Service`, `templates/`
|
||||
**Code Organization:** Standard Symfony structure — `src/Entity`, `src/Controller`, `src/Repository`, `src/Service`, `templates/` (public flows only) + `frontend/` (Vue SPA source)
|
||||
|
||||
**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)
|
||||
|
||||
**Post-scaffold cleanup:** After `symfony new pictureframe --webapp`, remove `symfony/stimulus-bundle`, `symfony/ux-turbo`, and AssetMapper. Initialize the Vue SPA in `frontend/` with `npm create vite@latest frontend -- --template vue-ts`.
|
||||
|
||||
**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.
|
||||
@@ -141,7 +143,9 @@ symfony new pictureframe --webapp
|
||||
|
||||
**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.
|
||||
**Web Controllers (authenticated app):** Symfony controllers return JSON responses for all authenticated app API calls under the `/api/` prefix. No Twig rendering for authenticated routes — Symfony serves the SPA shell only. Controllers use `JsonResponse` or Symfony Serializer.
|
||||
|
||||
**Web Controllers (public flows):** `/setup/{mac}`, `/token/{uuid}/approve`, `/token/{uuid}/decline` return Twig responses. These are the only controllers that render HTML directly.
|
||||
|
||||
**Email:** Symfony Mailer. Transactional emails: image share notification (with approve link), hard-delete confirmation. Authorization links embedded as tokenized URLs pointing to Symfony routes.
|
||||
|
||||
@@ -153,13 +157,26 @@ symfony new pictureframe --webapp
|
||||
|
||||
### Frontend Architecture
|
||||
|
||||
**Templating:** Twig. Identical pattern to aqua-iq.
|
||||
**Authenticated App — Vue 3 SPA:**
|
||||
All authenticated routes are served by a Vue 3 SPA built with Vite + TypeScript strict mode. Vue Router handles client-side navigation; Pinia manages shared state (current user, device list, upload funnel state). SCSS modules scoped per SFC for component styles. Konva.js + Vue-Konva for the sticker canvas editor. No Stimulus, no Turbo, no AssetMapper.
|
||||
|
||||
**Interactivity:** Stimulus controllers + Turbo Drive (Hotwire). No SPA, no build step required.
|
||||
Symfony serves the SPA shell (`public/build/index.html`) via a catch-all route for all authenticated paths. Vue Router takes over client-side navigation from that point.
|
||||
|
||||
**Forms:** Symfony Form component.
|
||||
**Public Flows — Symfony + Twig:**
|
||||
Three routes remain as Symfony Twig pages with no Vue dependency:
|
||||
- `/setup/{mac}` — device provisioning setup (post-QR scan)
|
||||
- `/token/{uuid}/approve` — email approve page (no login required)
|
||||
- `/token/{uuid}/decline` — email decline page (no login required)
|
||||
|
||||
**Assets:** AssetMapper (no Webpack/Node).
|
||||
These must work with images disabled, CSS disabled, and screen reader only.
|
||||
|
||||
**TypeScript:** Strict mode. `frontend/src/types/` holds interfaces mirroring every Symfony API response shape: `Device`, `Image`, `StickerLayer`, `RenderedAsset`, `Token`. The compiler surfaces API contract drift before it reaches deployed devices.
|
||||
|
||||
**SCSS:** Modules scoped per SFC (`<style scoped lang="scss">`). Global tokens in `frontend/src/styles/global.scss`. No Tailwind, no utility CSS framework, no pre-built component library.
|
||||
|
||||
**Build:** Vite outputs to `public/build/`. `vite.config.ts` sets `outDir: '../public/build'`. The Symfony catch-all controller renders `public/build/index.html` directly.
|
||||
|
||||
**Forms:** Symfony Form component is not used for authenticated app forms — Vue handles all form logic. Symfony Form is only used in Twig public flows if needed.
|
||||
|
||||
### Infrastructure & Deployment
|
||||
|
||||
@@ -178,14 +195,15 @@ symfony new pictureframe --webapp
|
||||
### 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)
|
||||
1. DDEV setup + Symfony scaffold (`symfony new pictureframe --webapp`); remove Stimulus/Turbo/AssetMapper
|
||||
2. Vue SPA scaffold in `frontend/` (`npm create vite@latest frontend -- --template vue-ts`); configure `vite.config.ts` to output to `public/build/`
|
||||
3. Add Messenger, Scheduler, Imagick to Symfony
|
||||
4. Domain + Nginx config on VPS
|
||||
5. Core entities (User, Device, Image, RenderedAsset, Token)
|
||||
6. Image processing worker
|
||||
7. Symfony JSON API endpoints + SpaController catch-all
|
||||
8. Vue SPA features (library, upload funnel, sticker editor, approvals, admin)
|
||||
9. 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
|
||||
@@ -302,13 +320,13 @@ Mandatory rules for all code in this project:
|
||||
|
||||
### Requirements to Structure Mapping
|
||||
|
||||
**User & Account Management** → `src/Controller/SecurityController.php`, `src/Entity/User.php`, `src/Repository/UserRepository.php`, `templates/security/`
|
||||
**User & Account Management** → `src/Controller/SecurityController.php`, `src/Controller/Api/UserApiController.php`, `src/Entity/User.php`, `src/Repository/UserRepository.php`
|
||||
|
||||
**Device Management** → `src/Controller/Device/`, `src/Entity/Device.php`, `src/Service/DeviceService.php`, `src/Repository/DeviceRepository.php`, `templates/device/`
|
||||
**Device Management** → `src/Controller/Api/DeviceApiController.php`, `src/Entity/Device.php`, `src/Service/DeviceService.php`, `src/Repository/DeviceRepository.php`, `frontend/src/views/HomeView.vue`
|
||||
|
||||
**Image Library** → `src/Controller/Image/`, `src/Entity/Image.php`, `src/Entity/RenderedAsset.php`, `src/Service/ImageService.php`, `src/Service/ImageProcessingService.php`, `templates/image/`
|
||||
**Image Library** → `src/Controller/Api/ImageApiController.php`, `src/Entity/Image.php`, `src/Entity/RenderedAsset.php`, `src/Service/ImageService.php`, `src/Service/ImageProcessingService.php`, `frontend/src/views/LibraryView.vue`
|
||||
|
||||
**Image Approval & Sharing** → `src/Controller/Token/`, `src/Entity/Token.php`, `src/Service/TokenService.php`, `templates/token/`
|
||||
**Image Approval & Sharing** → `src/Controller/Token/TokenActionController.php`, `src/Entity/Token.php`, `src/Service/TokenService.php`, `templates/token/` (email approve/decline pages), `frontend/src/components/ApproveCard.vue`
|
||||
|
||||
**Device Provisioning** → `src/Controller/Device/DeviceProvisionController.php`, `templates/device/provision.html.twig`
|
||||
|
||||
@@ -316,7 +334,7 @@ Mandatory rules for all code in this project:
|
||||
|
||||
**Display & Status (device pull endpoint)** → `src/Controller/Api/DeviceImageController.php`
|
||||
|
||||
**Admin & Moderation** → `src/Controller/Admin/`, `templates/admin/`
|
||||
**Admin & Moderation** → `src/Controller/Admin/`, `frontend/src/views/AdminView.vue` (if applicable)
|
||||
|
||||
**Cross-Cutting: Async Processing** → `src/Message/ProcessImageMessage.php`, `src/MessageHandler/ProcessImageMessageHandler.php`, `config/packages/messenger.yaml`
|
||||
|
||||
@@ -333,14 +351,55 @@ pictureframe/
|
||||
│ └── 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
|
||||
│ └── ci.yml ← Gitea Actions: lint + test + vite build on push
|
||||
├── frontend/ ← Vue 3 SPA source (Vite + TypeScript)
|
||||
│ ├── src/
|
||||
│ │ ├── assets/
|
||||
│ │ │ └── stickers/ ← SVG sticker assets (Seasonal, Holidays, Fun, Family, Nature)
|
||||
│ │ ├── components/
|
||||
│ │ │ ├── base/
|
||||
│ │ │ │ ├── BaseButton.vue
|
||||
│ │ │ │ ├── BaseInput.vue
|
||||
│ │ │ │ ├── BaseBottomSheet.vue
|
||||
│ │ │ │ ├── BaseCard.vue
|
||||
│ │ │ │ ├── BaseChip.vue
|
||||
│ │ │ │ └── BaseToast.vue
|
||||
│ │ │ ├── FrameCard.vue
|
||||
│ │ │ ├── CropEditor.vue
|
||||
│ │ │ ├── StickerCanvas.vue
|
||||
│ │ │ ├── StickerTray.vue
|
||||
│ │ │ ├── DevicePicker.vue
|
||||
│ │ │ ├── PhotoThumb.vue
|
||||
│ │ │ ├── ShareSheet.vue
|
||||
│ │ │ ├── ApproveCard.vue
|
||||
│ │ │ └── BottomNav.vue
|
||||
│ │ ├── router/
|
||||
│ │ │ └── index.ts ← Vue Router; catch-all handled by Symfony
|
||||
│ │ ├── stores/
|
||||
│ │ │ ├── auth.ts ← current user, login state
|
||||
│ │ │ ├── devices.ts ← device list, current device
|
||||
│ │ │ └── upload.ts ← upload funnel state, sticker composition
|
||||
│ │ ├── styles/
|
||||
│ │ │ ├── _breakpoints.scss ← $bp-tablet: 640px, $bp-desktop: 960px
|
||||
│ │ │ ├── _tokens.scss ← CSS custom properties for all 6 themes
|
||||
│ │ │ └── global.scss ← reset, typography, base layout
|
||||
│ │ ├── types/
|
||||
│ │ │ ├── Device.ts
|
||||
│ │ │ ├── Image.ts
|
||||
│ │ │ ├── StickerLayer.ts ← { id, type, x, y, scale, rotation }
|
||||
│ │ │ ├── RenderedAsset.ts
|
||||
│ │ │ └── Token.ts
|
||||
│ │ ├── views/
|
||||
│ │ │ ├── HomeView.vue ← FrameList — FrameCard per device
|
||||
│ │ │ ├── LibraryView.vue ← photo grid, search, tabs (All/Mine/Shared)
|
||||
│ │ │ ├── UploadView.vue ← funnel: crop → sticker → device picker
|
||||
│ │ │ └── SettingsView.vue
|
||||
│ │ ├── App.vue
|
||||
│ │ └── main.ts
|
||||
│ ├── index.html
|
||||
│ ├── package.json
|
||||
│ ├── tsconfig.json
|
||||
│ └── vite.config.ts ← outDir: '../public/build'
|
||||
├── bin/
|
||||
│ └── console
|
||||
├── config/
|
||||
@@ -352,30 +411,30 @@ pictureframe/
|
||||
│ │ ├── scheduler.yaml
|
||||
│ │ ├── security.yaml ← form_login, remember_me, ROLE_SUPER_ADMIN hierarchy
|
||||
│ │ └── twig.yaml
|
||||
│ ├── routes.yaml
|
||||
│ ├── routes.yaml ← catch-all route → SpaController for authenticated paths
|
||||
│ ├── routes/
|
||||
│ │ └── api.yaml ← /api/device/{mac}/image route
|
||||
│ │ └── api.yaml ← /api/device/{mac}/image + /api/* app endpoints
|
||||
│ └── services.yaml
|
||||
├── migrations/
|
||||
├── public/
|
||||
│ ├── build/ ← gitignored; Vite output (index.html + hashed assets)
|
||||
│ └── index.php
|
||||
├── src/
|
||||
│ ├── Controller/
|
||||
│ │ ├── Api/
|
||||
│ │ │ └── DeviceImageController.php ← GET /api/device/{mac}/image → 200/204/404
|
||||
│ │ │ ├── DeviceImageController.php ← GET /api/device/{mac}/image → 200/204/404 (binary)
|
||||
│ │ │ ├── DeviceApiController.php ← JSON CRUD for devices (Vue SPA)
|
||||
│ │ │ ├── ImageApiController.php ← JSON CRUD for images, upload, share (Vue SPA)
|
||||
│ │ │ └── UserApiController.php ← current user, settings (Vue SPA)
|
||||
│ │ ├── Admin/
|
||||
│ │ │ ├── AdminDashboardController.php
|
||||
│ │ │ └── AdminModerationController.php
|
||||
│ │ ├── SpaController.php ← catch-all; renders public/build/index.html
|
||||
│ │ ├── SecurityController.php ← login/logout (form POST, JSON response)
|
||||
│ │ ├── Device/
|
||||
│ │ │ ├── DeviceController.php
|
||||
│ │ │ └── DeviceProvisionController.php
|
||||
│ │ ├── Image/
|
||||
│ │ │ ├── ImageLibraryController.php
|
||||
│ │ │ ├── ImageUploadController.php
|
||||
│ │ │ └── ImageShareController.php
|
||||
│ │ ├── SecurityController.php
|
||||
│ │ │ └── DeviceProvisionController.php ← GET/POST /setup/{mac} → Twig
|
||||
│ │ └── Token/
|
||||
│ │ └── TokenActionController.php ← consume approve/decline/hard-delete tokens
|
||||
│ │ └── TokenActionController.php ← /token/{uuid}/approve|decline → Twig
|
||||
│ ├── Entity/
|
||||
│ │ ├── Device.php
|
||||
│ │ ├── Image.php
|
||||
@@ -386,10 +445,6 @@ pictureframe/
|
||||
│ │ ├── Orientation.php
|
||||
│ │ ├── RenderStatus.php
|
||||
│ │ └── TokenType.php
|
||||
│ ├── Form/
|
||||
│ │ ├── DeviceType.php
|
||||
│ │ ├── ImageUploadType.php
|
||||
│ │ └── RegistrationType.php
|
||||
│ ├── Message/
|
||||
│ │ └── ProcessImageMessage.php ← DTO: imageId, deviceModel, orientation
|
||||
│ ├── MessageHandler/
|
||||
@@ -413,31 +468,20 @@ pictureframe/
|
||||
├── 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
|
||||
│ │ └── provision.html.twig ← /setup/{mac} public provisioning page
|
||||
│ ├── token/
|
||||
│ │ ├── approve.html.twig
|
||||
│ │ └── decline.html.twig
|
||||
│ └── base.html.twig
|
||||
│ │ ├── approve.html.twig ← email approve page (no login)
|
||||
│ │ └── decline.html.twig ← email decline page (no login)
|
||||
│ └── base_public.html.twig ← minimal base for public Twig pages only
|
||||
├── tests/
|
||||
│ ├── Functional/
|
||||
│ │ ├── Api/
|
||||
│ │ │ └── DeviceImageControllerTest.php
|
||||
│ │ ├── Device/
|
||||
│ │ │ └── DeviceControllerTest.php
|
||||
│ │ └── Image/
|
||||
│ │ └── ImageLibraryControllerTest.php
|
||||
│ │ │ ├── DeviceImageControllerTest.php
|
||||
│ │ │ ├── DeviceApiControllerTest.php
|
||||
│ │ │ └── ImageApiControllerTest.php
|
||||
│ │ └── Token/
|
||||
│ │ └── TokenActionControllerTest.php
|
||||
│ ├── Integration/
|
||||
│ │ ├── MessageHandler/
|
||||
│ │ │ └── ProcessImageMessageHandlerTest.php
|
||||
@@ -461,7 +505,6 @@ pictureframe/
|
||||
├── .gitignore
|
||||
├── composer.json
|
||||
├── composer.lock
|
||||
├── importmap.php
|
||||
├── phpunit.xml.dist
|
||||
└── symfony.lock
|
||||
```
|
||||
@@ -472,7 +515,7 @@ pictureframe/
|
||||
`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.
|
||||
Authenticated routes are gated by Symfony form-login firewall. `SpaController` serves `public/build/index.html` as the catch-all — Vue Router handles client-side navigation from there. All authenticated data flows through `/api/*` JSON endpoints. `ROLE_SUPER_ADMIN` gates admin API controllers. Public flows (`/setup/{mac}`, `/token/{uuid}/approve|decline`) are outside the firewall and return Twig responses — no Vue involved.
|
||||
|
||||
**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.
|
||||
@@ -513,7 +556,7 @@ User soft-deletes image → Image.deleted_at set (excluded by findActive*)
|
||||
|
||||
### 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.
|
||||
**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. Vue 3 + Vite + TypeScript strict + SCSS modules + Konva.js is a standard, well-supported SPA stack. The Symfony/Vue boundary is clean: Symfony serves the SPA shell and JSON API; Vue owns all authenticated UI rendering. No build-step conflicts — Vite and Symfony operate independently.
|
||||
|
||||
**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/`.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user