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>
This commit is contained in:
2026-05-07 14:32:58 -04:00
parent 100e101d05
commit d11ddff912
29 changed files with 720 additions and 156 deletions
+16 -3
View File
@@ -85,8 +85,21 @@ class DeviceApiController extends AbstractController
$device->setRotationIntervalMinutes(max(1, (int) $body['rotationIntervalMinutes']));
}
if (array_key_exists('wakeHour', $body)) {
$device->setWakeHour($body['wakeHour'] === null ? null : (int) $body['wakeHour']);
if (array_key_exists('wakeTimes', $body)) {
$times = $body['wakeTimes'];
if (!is_array($times)) {
return $this->json(['error' => 'wakeTimes must be an array'], Response::HTTP_UNPROCESSABLE_ENTITY);
}
foreach ($times as $t) {
if (!is_int($t) && !(is_string($t) && ctype_digit($t))) {
return $this->json(['error' => 'wakeTimes must contain integers 0-1439 (minutes since midnight)'], Response::HTTP_UNPROCESSABLE_ENTITY);
}
$ti = (int) $t;
if ($ti < 0 || $ti > 1439) {
return $this->json(['error' => 'wakeTimes must contain integers 0-1439 (minutes since midnight)'], Response::HTTP_UNPROCESSABLE_ENTITY);
}
}
$device->setWakeTimes(array_map('intval', $times));
}
if (isset($body['timezone'])) {
@@ -165,7 +178,7 @@ class DeviceApiController extends AbstractController
'name' => $d->getName(),
'orientation' => $d->getOrientation()->value,
'rotationIntervalMinutes' => $d->getRotationIntervalMinutes(),
'wakeHour' => $d->getWakeHour(),
'wakeTimes' => $d->getWakeTimes(),
'timezone' => $d->getTimezone(),
'uniquenessWindow' => $d->getUniquenessWindow(),
'linkedAt' => $d->getLinkedAt()->format(\DateTimeInterface::ATOM),
+12 -5
View File
@@ -28,14 +28,21 @@ class DeviceImageController extends AbstractController
private function computeIntervalMs(Device $device): int
{
if ($device->getWakeHour() !== null) {
$wakeTimes = $device->getWakeTimes();
if (!empty($wakeTimes)) {
$tz = new \DateTimeZone($device->getTimezone());
$now = new \DateTimeImmutable('now', $tz);
$next = $now->setTime($device->getWakeHour(), 0, 0);
if ($next->getTimestamp() <= $now->getTimestamp()) {
$next = $next->modify('+1 day');
$earliest = null;
foreach ($wakeTimes as $minutes) {
$candidate = $now->setTime((int) ($minutes / 60), $minutes % 60, 0);
if ($candidate->getTimestamp() <= $now->getTimestamp()) {
$candidate = $candidate->modify('+1 day');
}
if ($earliest === null || $candidate < $earliest) {
$earliest = $candidate;
}
}
return (int) (($next->getTimestamp() - $now->getTimestamp()) * 1000);
return (int) (($earliest->getTimestamp() - $now->getTimestamp()) * 1000);
}
return $device->getRotationIntervalMinutes() * 60 * 1000;