fix(home): "next sync" must reflect the schedule the device is *on*
CI / test (push) Has been cancelled

The card's "next sync" was computed locally as `lastSeenAt + interval`,
which broke the moment the user PATCHed a new interval: the device is
still asleep on whatever schedule was active at its last poll, but the
local record now has the new interval, so we'd display a misleading
"in 2m" after a 5→3 min change.

Fix: server stamps `nextPollExpectedAt` on every poll (200/304/204),
PWA reads it directly. The timestamp doesn't move when settings are
edited — only when the device actually polls and picks up a new
schedule. Same field also drives the settings-sheet "Next update"
preview, which had the same flaw.

Side effects:
- `markSeen()` now flushes on the 204 paths too — they previously
  set lastSeenAt without flushing (latent bug for devices with no
  approved images / missing assets).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 15:52:04 -04:00
parent eedd50b95c
commit 995445ed9e
22 changed files with 136 additions and 26 deletions
+1
View File
@@ -183,6 +183,7 @@ class DeviceApiController extends AbstractController
'uniquenessWindow' => $d->getUniquenessWindow(),
'linkedAt' => $d->getLinkedAt()->format(\DateTimeInterface::ATOM),
'lastSeenAt' => $d->getLastSeenAt()?->format(\DateTimeInterface::ATOM),
'nextPollExpectedAt' => $d->getNextPollExpectedAt()?->format(\DateTimeInterface::ATOM),
'lockedImageId' => $d->getLockedImage()?->getId(),
'currentImageId' => $d->getCurrentImage()?->getId(),
];
+11 -3
View File
@@ -61,6 +61,17 @@ class DeviceImageController extends AbstractController
$currentImageId = (int) $request->headers->get('X-Current-Image-Id', '-1');
$device->markSeen();
// Stamp when we expect the device to call back — the PWA reads this
// directly so its "next sync" label reflects the schedule the device
// is actually on, not the freshly-saved one that won't reach it
// until that next poll.
$device->setNextPollExpectedAt(
(new \DateTimeImmutable())->modify('+' . (int) ceil($intervalMs / 1000) . ' seconds')
);
// Flush up-front so the 204/no_image/no_asset paths persist these too
// (they previously didn't flush at all — latent bug for lastSeenAt).
$em->flush();
// Locked image bypasses rotation entirely.
$image = $device->getLockedImage() ?? $this->rotation->advance($device);
@@ -111,9 +122,6 @@ class DeviceImageController extends AbstractController
// would otherwise have set this. Without the assignment, currentImage
// stays stale — Home would keep showing the previous photo even
// though the device has been confirming the new one for cycles.
// Also flush so markSeen() above is persisted on every 304 (lastSeenAt
// would otherwise freeze whenever the device polls and gets no
// change, causing the status badge to drift to "offline").
$device->setCurrentImage($image);
$em->flush();