Commit Graph

9 Commits

Author SHA1 Message Date
football2801 ece0defe3f feat(setup): "Claim this frame" checkbox for previously-bound MACs
CI / test (push) Has been cancelled
Use case: old owner sells the device to a friend. Friend holds the BOOT
button to wipe NVS, joins the device's AP, sets new WiFi. The old
owner's account is still bound to the MAC server-side, so without
explicit consent the friend would silently take over (or, worse, the
old owner's photos would keep displaying until claim).

Flow now:
  - GET /setup/{mac} detects MAC bound to anyone and renders a
    "Claim this frame as my own" checkbox + a banner explaining what
    the takeover wipes. Both register and login panels carry the
    checkbox; submitting either form without it bounces back through
    the index with a session-flashed error.
  - DeviceService::linkToUser now requires allowClaim=true to
    transfer ownership. Without it, throws DeviceClaimRequiredException
    that the controller catches and turns into the bounce-with-error.
  - On a successful claim, the takeover wipes:
      * old image-device approvals
      * device_image_history rows for the device
      * name, wakeTimes, currentImage*, lockedImage, nextPollExpectedAt
    so the new owner starts from a fresh slate, not inheriting the
    seller's "Living Room / 4:30 AM" preset.
  - Already-logged-in user visiting /setup/{mac} for someone else's
    device falls through to the form (instead of silently transferring
    on page load) so the checkbox is the only path.

Test matrix:
  - SetupControllerTest: 5 new functional cases — checkbox renders for
    bound MACs, register/login without checkbox bounce + retain old
    ownership, register WITH checkbox transfers + purges, logged-in
    other-user falls through to form.
  - DeviceServiceTest: 3 new unit cases — throw without consent,
    isClaimedByAnotherUser true/false matrix, takeover resets device
    state.

Coverage: 99.70% lines / 98.19% methods backend, 333 frontend tests
green via ddev tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 14:45:52 -04:00
football2801 a9ad014bd1 test: tighten coverage to 99.69% backend / 98.62% frontend
CI / test (push) Has been cancelled
Started: 89.08% backend / 97.01% frontend lines.
Landed: 99.69% backend / 98.62% frontend.

Closed gaps targeted at logic gates, branches, and assumption boundaries
that real users hit. Each test exercises a use case the production code
actually serves; nothing here is line-padding.

Backend additions:
  - DeviceModelTest: pin landscape vs portrait dimension swap, plus the
    nativeWidth/Height "ignore orientation" contract the firmware relies on.
  - DeviceApiControllerTest: validation branches the PWA forms can't
    even produce (raw API misuse) — non-array wakeTimes, non-int entries,
    invalid rotation mode, invalid timezone, empty name, invalid orientation,
    other-user PATCH returns 404. Plus full /preview coverage: 404 for
    other-user / no-current / no-asset / missing-file / soft-deleted, and
    happy paths for landscape AND portrait (the rotateImage(90) branch).
  - ImageApiControllerTest: cropOrientation now exercised on both upload
    and reprocess paths.
  - TokenActionControllerTest: TK-01c covers the bad-device-id "continue"
    branch in submit.
  - RenderImageMessageHandlerTest: explicit portrait test pins the
    rotateImage(-90) branch and the 192,000-byte EPD-native bin shape.
  - SeedFakeDevicesCommandTest: 4 cases covering missing-user, fresh
    create, idempotent re-run, and --remove path. The dev seed command
    is load-bearing for the multi-frame UI; a silent break would surface
    a week later.
  - RerenderAssetsCommandTest: reset + dispatch path, no-assets path.

Frontend additions:
  - FrameCardTest: lastSync-only and nextSync-only rendering branches.
  - HomeView.test:
    * + Add time fallback path when all 9 default candidates are taken.
    * Multi-day "in Nd" nextSync formatting (offline / huge-interval case).
    * Medium-horizon (5h) nextSync formats as clock-time + day label.
    * visibilitychange triggers a silent re-fetch.
    * add-photo handler creates input + navigates to /upload after pick.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 14:22:46 -04:00
football2801 2a8bf3895f chore(dev): DDEV setup so the test suite actually runs
CI / test (push) Has been cancelled
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>
2026-05-08 13:56:36 -04:00
football2801 e2a8ea4a7e feat(rotation): X-Boot-Reason: cold forces a resync regardless of schedule
CI / test (push) Has been cancelled
Cold-boot polls (firmware sends X-Boot-Reason: cold on UNDEFINED
wakeup cause) are treated as a deliberate "force a refresh" gesture
from the user — unplug → replug to re-pull whatever the web app
queued. Timer wakes still respect the wakeTimes schedule, so the
schedule-gated semantics aren't undermined.

Test: a cold-boot poll between scheduled wake times advances the
rotation and writes a fresh DeviceImageHistory row, while an
otherwise-identical timer-wake poll returns 304 without rotating.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 12:18:43 -04:00
football2801 5b3e2e47d7 fix(rotation): gate poll-driven advance() behind isDue() check
CI / test (push) Has been cancelled
Symptom: with wakeTimes=[4 AM, 9 PM, 9:15 PM], the frame rotated to a
fresh photo at 10:14 AM when the device was reconnected and polled.
The wakeTimes only governed *when* the device polled (via X-Interval-Ms);
they didn't gate whether the server picked a new image when it did.
Power-on or button-press polls would always rotate.

Fix: move the existing isDue() logic from AdvanceRotationMessageHandler
into RotationService as a public method, and gate
DeviceImageController::image so off-schedule polls return the device's
current image (which 304s when X-Current-Image-Id matches) rather than
calling advance(). The scheduler-driven handler still uses the same
isDue — both code paths now share one source of truth.

Tests:
  - DeviceImageControllerTest: new test asserting an off-schedule poll
    returns 304 without rotating; existing wakeTimes tests reworked to
    use slot lists that always have a past slot regardless of run time.
  - AdvanceRotationMessageHandlerTest: existing AR-04 through AR-07
    keep covering isDue's semantics — they now go through the service.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 12:14:32 -04:00
football2801 d11ddff912 feat(device): replace daily wakeHour with multi-time wakeTimes (minutes)
CI / test (push) Has been cancelled
Frame settings now offer two update-frequency modes: "at specific times" or
"every X minutes". Times are stored as an int[] of minutes-since-midnight,
allowing multiple slots per day at minute granularity. Backend computes the
earliest upcoming slot for X-Interval-Ms and uses the most-recent-past slot
as the rotation-due boundary. PWA settings sheet has hour/minute/AM-PM
dropdowns with + Add / trash, a live "next update" preview, and a note
that changes only take effect at the device's next sync.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 14:32:58 -04:00
football2801 b700a4a018 fix: include rendered_at in 304 cache check so re-renders invalidate
CI / test (push) Has been cancelled
After re-cropping an image, the renderer regenerates the .bin and
advances the asset's rendered_at, but the device's 304 short-circuit
still matched on (image_id, orientation) only — so the device kept
serving the old upside-down/stale bytes from its local cache despite
the server having freshly-rendered correct ones.

Adds device.current_rendered_at, populated whenever a 200 response is
served, and tightens the 304 condition to require all three (image id,
orientation, rendered_at) to match. The asset lookup now happens before
the 304 check so its rendered_at is in scope for the comparison.

No firmware change — this is server-side cache logic. Existing devices
get null current_rendered_at after the migration; their next poll falls
through 304 and re-fetches once, then the cache is in sync.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:16:48 -04:00
football2801 c387260ee7 fix: include orientation in device 304 cache check
CI / test (push) Has been cancelled
The 304 short-circuit at DeviceImageController only compared image IDs,
so flipping a device between landscape and portrait would not invalidate
the cache: the device kept showing the previously-rendered .bin even
after the user changed orientation in the webapp.

Now the device row tracks currentImageOrientation — set whenever a 200
binary response is sent — and the 304 path requires both image id AND
current orientation to match the device's stored orientation. An
orientation flip naturally falls through to the 200 path on the next
poll, the freshly-rendered portrait .bin is delivered, and the device
redraws.

No firmware change: the existing X-Current-Image-Id header from the
device is sufficient. Existing devices migrate cleanly — null
currentImageOrientation just forces one full re-send on first post-
migration poll, which is harmless.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 14:21:15 -04:00
football2801 12245759ac chore: stage all in-progress work before repo split
CI / test (push) Has been cancelled
Web app: new entities (Image, RenderedAsset, SharedImage, Token,
DeviceImageHistory), enums, repositories, controllers, message handlers,
migrations, tests, frontend upload/library/sticker UI, Vue components.

Firmware: EPD background screen binaries + gen scripts, setup_bg header.

Infra: ddev config, test bundle, gitignore coverage dir.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 12:11:31 -04:00