Two related bugs that surfaced on the first 13.3" device's first photo:
1) Web-UI portrait preview was 90° sideways. DeviceApiController::
renderBinToPng rotated whenever the device was Portrait — correct
for V1 (landscape-native, Portrait => renderer rotated, so preview
un-rotates) but wrong for V2 (portrait-native — the renderer
doesn't rotate, so the preview shouldn't either). Now mirrors the
render-pipeline check: rotate only when `orientation !==
model->nativeOrientation()`. Two new functional tests pin the V2
portrait and V2 landscape PNG dimensions to guard against
regressions.
2) Cropped photo letterboxed on the 13.3" panel. CropEditor /
StickerCanvas / FrameCard had V1 dimensions hardcoded (1600×960
= 5:3 aspect). V2 is 4:3 (1200×1600 portrait / 1600×1200
landscape), so a "full crop" came out the wrong shape and the
server's white-canvas composite added bars. New `panelDims(model,
orientation)` helper in @/types is the single source of truth on
the frontend; matches DeviceModel::width/height on the server.
Threaded `model` through Device serializer → Device type →
UploadView → CropEditor / StickerCanvas, and HomeView → FrameCard.
FrameCard tests updated to cover all four model × orientation
placeholders.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Frontend (90.15→95.37 stmts / 91.83→97.01 lines):
- useDeviceMercure: full composable test suite via a fake EventSource —
open/merge/ignore-stale/parse-error/reconnect/dynamic-add/remove/
no-op-when-unconfigured/cleanup-on-unmount.
- HomeView: cover onTimePart's AM/PM and minute branches plus the
nextPollExpectedAt-null fallback paths in the next-update preview.
Backend (no instrumentation before; pcov was already in the image,
just needed a <coverage> block in phpunit.dist.xml):
- RotationService: one test per mode (NewestUpload, Random,
LeastRecentlyShown), one for never-shown sorting first under LRS,
and two for prioritizeNeverShown — narrows when never-shown exists,
falls through to mode otherwise.
- DeviceSerializer: contract test on the wire shape (REST + Mercure
use the same serializer; silent rename here would break live updates
instantly).
- MercurePublisher: topic format + JSON encoding + the swallow-
exceptions guarantee (a flaky hub must not break poll responses).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>