diff --git a/app/Console/Commands/NotificationsSweepCommand.php b/app/Console/Commands/NotificationsSweepCommand.php index 2c50936..799806a 100644 --- a/app/Console/Commands/NotificationsSweepCommand.php +++ b/app/Console/Commands/NotificationsSweepCommand.php @@ -41,7 +41,9 @@ class NotificationsSweepCommand extends Command if (! Cache::has($key) && $notifier->notify('device_offline', __('notifications.device_offline_subject', ['name' => $device->name]), __('notifications.device_offline_body', ['name' => $device->name]))) { - Cache::put($key, true, now()->addDay()); + // Flag stays until the condition RESOLVES (cleared below) — one mail per + // continuous outage, not one every 24h. The long TTL is just a leak guard. + Cache::put($key, true, now()->addMonth()); } } else { Cache::forget($key); @@ -58,14 +60,16 @@ class NotificationsSweepCommand extends Command continue; } + // Missing reading defaults to 100 (no alert); a genuine 0% is a dead battery and MUST + // alert — so the threshold is simply < 20 (a real 0 included). $pct = (int) data_get($entity->state, 'state.percent', 100); $key = "notified:battery:{$entity->id}"; - if ($pct > 0 && $pct < 20) { + if ($pct < 20) { if (! Cache::has($key) && $notifier->notify('low_battery', __('notifications.low_battery_subject', ['name' => $entity->device->name]), __('notifications.low_battery_body', ['name' => $entity->device->name, 'percent' => $pct]))) { - Cache::put($key, true, now()->addDay()); + Cache::put($key, true, now()->addMonth()); } } elseif ($pct >= 25) { Cache::forget($key); // hysteresis so it doesn't flap around the threshold diff --git a/app/Livewire/Modals/ManagePerson.php b/app/Livewire/Modals/ManagePerson.php index aba106e..eded19c 100644 --- a/app/Livewire/Modals/ManagePerson.php +++ b/app/Livewire/Modals/ManagePerson.php @@ -58,6 +58,12 @@ class ManagePerson extends ModalComponent } } + /** Validate the picture as soon as it's chosen, so a non-image shows a friendly error. */ + public function updatedAvatar(): void + { + $this->validateOnly('avatar', ['avatar' => ['nullable', 'image', 'max:4096']]); + } + public function save() { $this->validate([ diff --git a/app/Livewire/Modals/SmtpSetup.php b/app/Livewire/Modals/SmtpSetup.php index 635bc5d..d95fa96 100644 --- a/app/Livewire/Modals/SmtpSetup.php +++ b/app/Livewire/Modals/SmtpSetup.php @@ -64,7 +64,8 @@ class SmtpSetup extends ModalComponent return [ 'host' => 'required|string|max:190', 'port' => 'required|integer|min:1|max:65535', - 'encryption' => 'required|in:,tls,ssl', + // '' (None) is a valid choice — 'required' would reject it, so validate membership only. + 'encryption' => 'in:,tls,ssl', 'username' => 'nullable|string|max:190', 'password' => $this->hasStoredPassword ? 'nullable|string|max:190' : 'nullable|string|max:190', 'fromEmail' => 'required|email|max:190', diff --git a/app/Services/NotificationService.php b/app/Services/NotificationService.php index c01198c..fa73702 100644 --- a/app/Services/NotificationService.php +++ b/app/Services/NotificationService.php @@ -33,7 +33,7 @@ class NotificationService return $this->send($subject, $body); } - /** Send unconditionally (used by the "send test" button). Returns success. */ + /** Send unconditionally (used by the "send test" button). Returns success, never throws. */ public function send(string $subject, string $body): bool { $cfg = $this->config(); @@ -43,11 +43,19 @@ class NotificationService $this->configureMailer($cfg); - Mail::mailer('homeos_smtp')->raw($body, function ($message) use ($cfg) { - $message->to($cfg['to_email']) - ->subject('[HomeOS] '.$subject) - ->from($cfg['from_email'] ?: 'homeos@localhost', $cfg['from_name'] ?: 'HomeOS'); - }); + try { + Mail::mailer('homeos_smtp')->raw($body, function ($message) use ($cfg) { + $message->to($cfg['to_email']) + ->subject('[HomeOS] '.$subject) + ->from($cfg['from_email'] ?: 'homeos@localhost', $cfg['from_name'] ?: 'HomeOS'); + }); + } catch (\Throwable $e) { + // A transient SMTP failure must not abort the caller (a sweep over many devices, an + // automation run). Log and report failure so the loop keeps going. + report($e); + + return false; + } return true; } @@ -64,5 +72,9 @@ class NotificationService 'password' => ($cfg['password'] ?? '') ?: null, 'timeout' => 10, ]]); + + // MailManager caches the resolved mailer by name; purge it so a long-running worker + // (Horizon) picks up config changes instead of reusing the first mailer it built. + Mail::purge('homeos_smtp'); } } diff --git a/resources/css/app.css b/resources/css/app.css index 0c713cc..0fc0cf0 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -109,6 +109,29 @@ .pulse-live { animation: pulse 2.2s ease-in-out infinite; } .reveal { opacity: 0; transform: translateY(10px); animation: reveal .5s cubic-bezier(.2,.7,.3,1) forwards; } + + /* Themed checkbox — the native one looks off on OLED dark; this matches the console. */ + .checkbox { + appearance: none; -webkit-appearance: none; + width: 1.05rem; height: 1.05rem; flex-shrink: 0; + border-radius: 5px; + border: 1px solid var(--color-line); + background: var(--color-raised); + cursor: pointer; position: relative; + transition: background-color .15s, border-color .15s; + } + .checkbox:hover { border-color: color-mix(in srgb, var(--color-accent) 50%, var(--color-line)); } + .checkbox:checked { background: var(--color-accent); border-color: var(--color-accent); } + .checkbox:checked::after { + content: ""; position: absolute; left: 5px; top: 2px; + width: 4px; height: 8px; + border: solid var(--color-base); border-width: 0 2px 2px 0; + transform: rotate(45deg); + } + .checkbox:focus-visible { + outline: 2px solid color-mix(in srgb, var(--color-accent) 55%, transparent); + outline-offset: 1px; + } } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } } diff --git a/resources/views/livewire/devices/show.blade.php b/resources/views/livewire/devices/show.blade.php index 2db745b..6f7a9c3 100644 --- a/resources/views/livewire/devices/show.blade.php +++ b/resources/views/livewire/devices/show.blade.php @@ -111,7 +111,7 @@ @if ($role) @endif diff --git a/resources/views/livewire/modals/create-automation.blade.php b/resources/views/livewire/modals/create-automation.blade.php index 4a3599e..4c8136b 100644 --- a/resources/views/livewire/modals/create-automation.blade.php +++ b/resources/views/livewire/modals/create-automation.blade.php @@ -93,7 +93,7 @@