fix(home): "next sync" must reflect the schedule the device is *on*
CI / test (push) Has been cancelled
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:
@@ -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(),
|
||||
];
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -71,6 +71,17 @@ class Device
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?\DateTimeImmutable $lastSeenAt = null;
|
||||
|
||||
/**
|
||||
* Server-stamped wall-clock time at which the device is expected to poll
|
||||
* next, computed as `now + computeIntervalMs($device)` at every successful
|
||||
* response. The PWA reads this directly so the displayed "next sync"
|
||||
* always reflects the schedule the device is *actually on* — not whatever
|
||||
* the user just saved through the settings sheet (which the device won't
|
||||
* pick up until that next poll).
|
||||
*/
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?\DateTimeImmutable $nextPollExpectedAt = null;
|
||||
|
||||
/**
|
||||
* Orientation in effect when currentImage was last served as a 200 response.
|
||||
* Used alongside currentImage's id to decide whether a poll can be answered
|
||||
@@ -151,6 +162,9 @@ class Device
|
||||
public function getLastSeenAt(): ?\DateTimeImmutable { return $this->lastSeenAt; }
|
||||
public function markSeen(): static { $this->lastSeenAt = new \DateTimeImmutable(); return $this; }
|
||||
|
||||
public function getNextPollExpectedAt(): ?\DateTimeImmutable { return $this->nextPollExpectedAt; }
|
||||
public function setNextPollExpectedAt(?\DateTimeImmutable $t): static { $this->nextPollExpectedAt = $t; return $this; }
|
||||
|
||||
public function getCurrentImageOrientation(): ?Orientation { return $this->currentImageOrientation; }
|
||||
public function setCurrentImageOrientation(?Orientation $o): static { $this->currentImageOrientation = $o; return $this; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user