Mirrors aqua-iq's pattern but adapted for pictureFrame's stack:
postgres 16, php 8.4, node 22, imagick + pcov via apt extras,
Mercure hub at https://pictureframe.ddev.site/.well-known/mercure,
and four custom commands — `ddev tests`, `ddev coverage`,
`ddev frontend` (vite HMR), `ddev worker`.
Also restores dev deps (DAMA, Doctrine fixtures, symfony/uid) that
got dropped during earlier composer reshuffles, and adds a separate
`db_test` connection in .env.test so DAMA's transactional isolation
doesn't share state with whatever dev is mid-experiment with.
Two test fixes the new env exposed:
- RotationServiceTest::test_prioritize_never_shown_falls_through_when_all_shown
needed uniquenessWindow=2 so the recent-window filter wipes the
set and the fallback restores the full pool — otherwise window=1
excluded the most-recently-served image and the assertion drifted.
- DeviceImageControllerTest::test_locked_image_served_without_rotation_advance
was asserting currentImage stays null on a lock poll, but the
controller intentionally sets currentImage on the lock path so
Home reflects the live frame state. Now asserts both the
currentImage update AND that no DeviceImageHistory row was
written (the actual rotation-bypass guarantee).
Backend coverage (full suite via `ddev coverage`): 89.08% lines /
92.24% methods / 74.36% classes — the first real number we've had.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This dep was silently dropped by the earlier `composer require
symfony/mercure-bundle --no-scripts` on the prod host (it ran in
no-dev mode and removed packages not currently referenced in
require/require-dev), which crash-looped the worker when
Symfony\Component\Scheduler\Trigger\CronExpressionTrigger tried to
parse the hourly RunImageCleanupMessage cron.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Subscribe per-device with a Symfony Mercure hub: server publishes a fresh
device payload after every poll (200/304/204), every PATCH, and every
lock/unlock. The frontend opens one EventSource per device topic and
splats inbound JSON straight into the devices store — same shape as
GET /api/devices, so no envelope handling.
Topic: https://pictureframe.edholm.me/devices/{id}
Stack mirrors aqua-iq:
- symfony/mercure-bundle + config/packages/mercure.yaml
- App\Service\MercurePublisher (errors swallowed + logged; a flaky hub
must not break a poll response)
- App\Service\DeviceSerializer extracted as the single source of truth
for the wire shape (REST + Mercure share it)
- Frontend useDeviceMercure() composable: opens/closes EventSources to
match the device list reactively, reconnects on hub-side closes
- SpaController exposes MERCURE_PUBLIC_URL via window.__PF_MERCURE_URL__
Production compose adds a dunglas/mercure container with Traefik labels
for pictureframe.edholm.me/.well-known/mercure (handled separately on
the host since the file isn't in this repo).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>