Skip to content

Feat: Frame events — PopupText popups and Banner notifications#644

Open
rgregg wants to merge 32 commits into
immichFrame:mainfrom
rgregg:rgregg-notification-ux
Open

Feat: Frame events — PopupText popups and Banner notifications#644
rgregg wants to merge 32 commits into
immichFrame:mainfrom
rgregg:rgregg-notification-ux

Conversation

@rgregg
Copy link
Copy Markdown
Contributor

@rgregg rgregg commented May 18, 2026

Summary

Adds a client-side event notification system to ImmichFrame, surfaced through a new POST /api/events endpoint. Two display modes:

  • PopupText — full-screen centered modal that pauses the slideshow until dismissed or auto-timeout.
  • Banner — passive top-of-screen overlay that does not pause the slideshow. Tap to dismiss; auto-dismiss on timeout; newer banners with the same category replace the older one.

Both modes can be active simultaneously (popup + banner coexist).

New settings

General:
  EventHostEnabled: false                # default off
  EventPollingIntervalSeconds: 2
  EventDefaultTimeoutMs: 15000

New API

  • POST /api/events — enqueue an event (validated; type must start with frame.ui.)
  • GET /api/events/next?deviceId=…&mode=… — kiosk poll endpoint; mode filter (PopupText | Banner) is optional
  • POST /api/events/{eventId}/ack?deviceId=… — kiosk reports Shown / Closed / Timeout / Dismissed / Error
  • GET /api/events/pending?deviceId=… — diagnostics

Example banner POST:

curl -X POST http://host:8080/api/events \
  -H 'Content-Type: application/json' \
  -d '{
    "deviceId": "<id>",
    "id": "calendar-reminder-…",
    "type": "frame.ui.banner",
    "mode": "Banner",
    "message": "Coffee with Sam in 15 minutes",
    "category": "banner.calendar",
    "timeoutMs": 8000
  }'

Implementation notes

  • In-memory queue (InMemoryFrameEventQueue) supports priority ordering, category-based replacement, expiry, and per-mode active tracking so a popup and a banner don't shadow each other in PeekNext.
  • Frontend (Svelte 5) splits the active-event store into activePopupEvent and activeBannerEvent; the home page polls both modes every interval and renders PopupTextOverlay + BannerOverlay from EventOverlayHost.
  • Slideshow pause behavior is gated on activePopupEvent only — banners never interrupt the slideshow.
  • Backwards-compatible: when EventHostEnabled is false (default), no new behavior surfaces.

Tests

  • 89 Core tests (incl. 8 new queue tests covering priority, category replacement, expiry, per-mode tracking)
  • 18 WebApi tests (incl. 5 new validator tests covering PopupText and Banner rules)
  • All passing

Design spec and implementation plan committed under docs/superpowers/specs/ and docs/superpowers/plans/ if useful for review context.

Test plan

  • dotnet test — 107/107 pass
  • cd immichFrame.Web && npm run check — zero errors
  • npm run build — succeeds
  • With EventHostEnabled: true, POST a Banner; verify it appears at top of frame and slideshow keeps advancing
  • POST a PopupText; verify it pauses the slideshow
  • POST both; verify they coexist; tap banner — only banner dismisses
  • With EventHostEnabled: false (default), POSTs return 404 — no behavior change for users who don't opt in

Summary by CodeRabbit

  • New Features

    • Full frame event system with Popup and Banner modes, client polling, per-mode active events, and REST endpoints to post, fetch next (mode-filtered), ack, and inspect pending events.
    • Client UX: banner and popup overlays, polling controls, stores, and acknowledgement flow; new client settings for enabling/tuning events.
  • Tests

    • Extensive unit tests covering queue semantics, validation, diagnostics, and settings bounds.
  • Documentation

    • Configuration and design docs for Event Host and Banner notifications.

Review Change Stack

rgregg and others added 24 commits May 18, 2026 02:18
…TimeoutMs settings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tifications

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without this, FrameEventMode serializes as 0/1 instead of
"PopupText"/"Close", causing the frontend string comparison to fail.
The global converter was breaking asset type serialization (IMAGE as
string instead of int). Use [JsonConverter] on FrameEventMode and
FrameEventAckStatus instead.
The auto-generated API client type was missing the event properties,
so eventHostEnabled was never reaching the frontend config store.
Remove eventHostEnabled gate from startEventPolling — the poll loop
already checks it each iteration. This avoids timing issues where the
config store may not be populated when the function is first called.
Also remove dead $effect block and debug console.warn calls.
- Add seconds-remaining countdown indicator to the popup dismiss area
- Convert event components to Svelte 5 $props() syntax
- Fix weather temperature showing degree symbol twice by using
  weather.unit from the API instead of hardcoded degree symbol
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 18, 2026

📝 Walkthrough

Walkthrough

Implements a full frame event system: domain models, an in-memory per-device queue with mode-aware semantics, API endpoints for enqueue/peek/ack and diagnostics, validator and DI wiring, frontend polling/stores and overlays for PopupText/Banner, configuration settings, tests, and documentation.

Changes

Frame Event Notification System

Layer / File(s) Summary
Core domain models and queue interface
ImmichFrame.Core/Events/*, ImmichFrame.Core/Interfaces/IFrameEventQueue.cs, ImmichFrame.Core/Interfaces/IServerSettings.cs
Public FrameEvent, FrameEventMode (PopupText/Close/Banner), FrameEventAckStatus, FrameEventAction, FrameEventInput types; IFrameEventQueue interface defining enqueue, peek-by-mode, acknowledge, remove-by-category, and snapshot operations; IGeneralSettings extended with event host enable, polling interval, and default timeout.
In-memory event queue implementation and tests
ImmichFrame.Core/Services/InMemoryFrameEventQueue.cs, ImmichFrame.Core.Tests/Events/InMemoryFrameEventQueueTests.cs
Per-device concurrent queue with deduplication, priority-ordered peek, mode-scoped filtering, category-based removal, acknowledgment tracking, expiration by timeout, and 16 comprehensive test cases covering empty queue, duplicate rejection, category replacement, mode filtering, ack semantics, and cross-mode interactions.
Web API DTOs, validator, and controller
ImmichFrame.WebApi/Models/Events/*, ImmichFrame.WebApi/Services/FrameEventValidator.cs, ImmichFrame.WebApi/Controllers/EventsController.cs, ImmichFrame.WebApi/Program.cs
FrameEventRequestDtoFrameEvent mapping; FrameEventResponseDto and diagnostics DTOs; FrameEventAckRequestDto; FrameEventValidator enforcing required fields, type prefix frame.ui., mode-specific message constraints, and timeout defaulting; EventsController endpoints for POST /events, GET /events/next (optional mode), POST /events/{id}/ack, and GET /events/pending; DI registration.
Server settings and example resources
ImmichFrame.WebApi/Models/ServerSettings.cs, ImmichFrame.WebApi/Helpers/Config/ServerSettingsV1.cs, docker/Settings.example.*, test resources
GeneralSettings and ServerSettingsV1 gain EventHostEnabled, EventPollingIntervalSeconds, and EventDefaultTimeoutMs with [Range] attributes and clamping; adapter wiring; updated test JSON/YAML and Docker example entries; bounds tests added.
Frontend config store and event service
immichFrame.Web/src/lib/stores/config.store.ts, immichFrame.Web/src/lib/events/event-service.ts, immichFrame.Web/src/lib/immichFrameApi.ts
ClientSettingsDto extended with event fields; configStore typed for UX defaults with patch() merging and default eventHostEnabled=false; event service exports activePopupEvent and activeBannerEvent stores, startEventPolling()/stopEventPolling(), concurrent per-mode polling, acknowledgeEvent(), and abort-aware delay helper.
Svelte overlay components and host
immichFrame.Web/src/lib/components/events/PopupTextOverlay.svelte, BannerOverlay.svelte, EventOverlayHost.svelte
PopupTextOverlay renders title/message/actions with backdrop/keyboard dismiss and countdown; BannerOverlay shows top fixed banner with dismiss; EventOverlayHost conditionally renders overlays by mode and forwards dismiss callbacks.
Home page integration
immichFrame.Web/src/lib/components/home-page/home-page.svelte
Subscribes to popup/banner stores; manages independent lifecycle per mode including one-time Shown ack, per-event timeout or default, auto-dismiss, and slideshow pause for popups only; comprehensive mount/unmount/destroy cleanup.
Tests and helpers
ImmichFrame.WebApi.Tests/*, ImmichFrame.Core.Tests/*
FrameEventValidator tests (success/failure), InMemoryFrameEventQueue tests covering priority/category/mode/ack semantics, EventSettingsBoundsTests for clamping, and ConfigLoaderTest update to respect RangeAttribute.
Documentation and plans
docs/docs/getting-started/configuration.md, docs/superpowers/*
New Event Host configuration docs, design spec and implementation plan for Banner notifications, curl examples, and a smoke-test checklist.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client Device
  participant EventsAPI as EventsController
  participant Validator as FrameEventValidator
  participant Queue as InMemoryFrameEventQueue
  participant Service as Event Service

  Client->>EventsAPI: POST /api/events (FrameEventRequestDto)
  EventsAPI->>Validator: Validate(dto)
  Validator-->>EventsAPI: FrameEvent
  EventsAPI->>Queue: EnqueueAsync(FrameEvent)
  loop Polling interval
    Service->>EventsAPI: GET /api/events/next?deviceId=X&mode=PopupText
    Service->>EventsAPI: GET /api/events/next?deviceId=X&mode=Banner
    EventsAPI->>Queue: PeekNextAsync(deviceId, mode)
    Queue-->>EventsAPI: FrameEvent | null
    EventsAPI-->>Service: FrameEventResponseDto | 204
  end
  Service->>Store: Update activePopupEvent / activeBannerEvent
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

enhancement

Suggested reviewers

  • JW-CH

Poem

🐰 A rabbit reports with a twitch and a hop,
Events now arrive — popups, banners on top.
Queues sort by priority, a tidy parade,
Frontend and backend in tandem are made.
Hooray for the code — a brisk, joyful hop!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feat: Frame events — PopupText popups and Banner notifications' clearly and concisely summarizes the main feature additions (event system with two notification modes), which aligns with the comprehensive changes across the entire codebase (backend infrastructure, frontend UI, configuration, and documentation).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread ImmichFrame.WebApi/Controllers/EventsController.cs Fixed
Comment thread ImmichFrame.WebApi/Controllers/EventsController.cs Fixed
Comment thread ImmichFrame.WebApi/Controllers/EventsController.cs Fixed
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🧹 Nitpick comments (1)
docs/docs/getting-started/configuration.md (1)

195-207: ⚡ Quick win

Add an auth note to the curl example.

On Line 195, the example is easy to copy as-is, but setups with AuthenticationSecret enabled will fail without Authorization: Bearer .... Please add a one-line note (or alternate curl snippet) right under this example to prevent false-negative setup checks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/docs/getting-started/configuration.md` around lines 195 - 207, The curl
example that posts to /api/events fails when AuthenticationSecret is enabled
because it lacks an Authorization header; update the documentation right under
the shown curl snippet (the POST to /api/events) to either add a one-line note
telling users to include Authorization: Bearer <token> when AuthenticationSecret
is enabled or provide an alternate curl snippet that includes the header
(Authorization: Bearer <your-auth-token>) so authenticated setups won't get
false-negative errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/superpowers/plans/2026-05-18-banner-notifications.md`:
- Around line 1177-1193: The plan doc currently embeds sensitive host/user
specifics in the shell snippets (e.g., the command string "ssh hass-hub-01.lan",
the systemd/user service "immich-frame", and the path "/home/ryan/.Xauthority"
and script "frame.sh"); replace those concrete values with neutral placeholders
(e.g., <HOSTNAME>, <USER>, <USER_HOME> or variables like $HOST, $USER_HOME) and
update the example commands to use those placeholders so the content no longer
contains internal hostnames or personal home paths while preserving the
instruction semantics.

In `@docs/superpowers/specs/2026-05-18-banner-notifications-design.md`:
- Line 8: The summary sentence contains a typo: change "renderered" to
"rendered" in the document describing the new Banner notification mode for
ImmichFrame; update the sentence that references Banner, PopupText,
FrameEventMode, and the new overlay component so it reads "...and rendered by a
new overlay component." ensuring the rest of the text and identifiers
(ImmichFrame, frame-event, FrameEventMode, PopupText) remain unchanged.

In `@ImmichFrame.Core/Events/FrameEventAckStatus.cs`:
- Around line 8-11: The enum FrameEventAckStatus is missing the Dismissed member
required by the event contract; update the enum declaration in
FrameEventAckStatus to include Dismissed (e.g., add Dismissed alongside Shown,
Closed, Timeout, Error) so the ack lifecycle can represent dismiss
acknowledgements correctly and avoid remapping/rejection of dismiss events.

In `@ImmichFrame.Core/Services/InMemoryFrameEventQueue.cs`:
- Around line 59-60: The category-based replacement currently ignores
FrameEventMode, causing an event of one mode (e.g., PopupText) to evict another
(e.g., Banner) when categories match; update InMemoryFrameEventQueue to scope
category lookups and replacements by mode—either by including the mode in the
category key (e.g., combine FrameEventMode with category) or by maintaining a
per-mode category dictionary, and then use that scoped mapping wherever
_byCategory is accessed/modified (references: _byCategory, _activeEventIdByMode,
and the add/remove logic around lines handling category replacement and the
blocks noted at 78-82 and 172-173) so replacements only affect events of the
same FrameEventMode.

In `@immichFrame.Web/src/lib/components/elements/clock.svelte`:
- Line 93: The temperature expression can produce "undefined°" when
weather.temperature is absent; update the template so the temperature and unit
are only rendered when temperature is present. Replace the inline expression in
the div with a conditional/guard that checks weather.temperature (e.g., in the
weather-temperature div or a small helper like a computed/derived value) and
render either the formatted toFixed(1) plus unit (weather.unit ?? '°') when
non-null, or an empty string/placeholder when missing to avoid showing
"undefined".

In `@immichFrame.Web/src/lib/components/home-page/home-page.svelte`:
- Around line 192-207: The early return in handlePopupEvent when
!$configStore.eventHostEnabled skips necessary cleanup (timers/state) so ensure
you run the same cleanup sequence before returning: call clearPopupTimer(), set
currentPopup = null, popupShownAcked = false, lastPopupId = null, and if
popupPausedSlideshow and progressBar then resume via progressBar.play() and set
popupPausedSlideshow = false; you can either move that cleanup block above the
if (!($configStore.eventHostEnabled ?? false)) check or extract it to a helper
(e.g., cleanupPopupState) and invoke it both before the early return and in the
existing null-event branch to avoid duplicated logic.

In `@immichFrame.Web/src/lib/events/event-service.ts`:
- Around line 125-143: The delay() function for polling attaches an 'abort'
listener that only auto-removes if the signal aborts, causing listener
accumulation; fix it in the delay(durationMs: number, signal: AbortSignal)
implementation by extracting the abort handler into a const (e.g., const onAbort
= () => { clearTimeout(timeout); resolve(); }) and registering it with
signal.addEventListener('abort', onAbort, { once: true }), then in the timeout
callback (the normal resolution path) call signal.removeEventListener('abort',
onAbort) after clearTimeout/resolve so the handler is removed when the timer
completes; ensure the same handler reference is used for both addEventListener
and removeEventListener to prevent leaks.
- Around line 93-103: The poll and ack fetch handlers currently only handle
200/204 and silently ignore 4xx/5xx responses; update the fetch result handling
for the /api/events/next call (the fetch with url
`/api/events/next?deviceId=${encodeURIComponent(deviceId)}&mode=${mode}`) and
the corresponding ack fetch so that non-2xx responses are handled explicitly:
detect response.ok === false, call the appropriate logger (or console.error)
with response.status and response.statusText and any returned error body, and
either set the store to an error/state indicating failure or throw an Error to
propagate the failure so callers can react instead of leaving stale UI state.
Ensure the same explicit handling is added to the ack flow so failed
acknowledgements are logged and surfaced.

In `@ImmichFrame.WebApi/Controllers/EventsController.cs`:
- Around line 69-77: AckEvent currently dereferences request.Status without
checking that the request body exists; add a null check for the
FrameEventAckRequestDto request at the start of AckEvent and return BadRequest
(e.g., new { message = "request body is required" }) if request is null before
calling _queue.AckAsync(deviceId, eventId, request.Status, cancellationToken),
ensuring you still validate deviceId and _settings.EventHostEnabled as before.

In `@ImmichFrame.WebApi/Helpers/Config/ServerSettingsV1.cs`:
- Around line 59-61: The V1 settings are forwarded without validation; add
bounds checks when mapping/using ServerSettingsV1.EventPollingIntervalSeconds
and ServerSettingsV1.EventDefaultTimeoutMs (and honor EventHostEnabled) in the
V1 adapter path or mapping function so bad legacy values can't create tight
loops or immediate timeouts; enforce reasonable ranges (e.g.
EventPollingIntervalSeconds >= 1 and <= 3600, EventDefaultTimeoutMs >= 1000 and
<= 300000), clamp out-of-range values to defaults or the nearest bound, and emit
a warning log mentioning ServerSettingsV1.EventPollingIntervalSeconds /
ServerSettingsV1.EventDefaultTimeoutMs when adjustments are made.

In `@ImmichFrame.WebApi/Models/Events/FrameEventAckRequestDto.cs`:
- Around line 8-9: The Status property on FrameEventAckRequestDto is currently a
non-nullable enum which prevents [Required] from detecting missing JSON; change
the type of Status to a nullable enum (FrameEventAckStatus?) so the model binder
can set null for omitted fields and [Required] will validate correctly, then
review any usages of FrameEventAckRequestDto.Status (e.g., code reading Status)
and handle the null case or validate before use.

In `@ImmichFrame.WebApi/Models/ServerSettings.cs`:
- Around line 75-77: ServerSettings currently exposes
EventPollingIntervalSeconds and EventDefaultTimeoutMs without bounds checks,
allowing <=0 values that break event hosting; add validation to enforce sensible
minima (e.g., EventPollingIntervalSeconds >= 1 and EventDefaultTimeoutMs >= 1 or
a practical lower bound like 100), either by applying DataAnnotations (e.g.,
Range) to the properties or by enforcing and normalizing values in the
ServerSettings constructor or property setters, and surface validation errors
(throw ArgumentOutOfRangeException or similar) so invalid configuration cannot
be used when EventHostEnabled is true.

In `@ImmichFrame.WebApi/Services/FrameEventValidator.cs`:
- Around line 19-22: The Validate method in FrameEventValidator currently
dereferences dto without a null check; add an explicit null guard at the start
of FrameEventValidator.Validate(FrameEventRequestDto dto) that throws a
ValidationException (e.g., "dto is required" or "request body is required") when
dto is null, before accessing dto.DeviceId (and any other fields), so callers
receive a ValidationException instead of a NullReferenceException.

---

Nitpick comments:
In `@docs/docs/getting-started/configuration.md`:
- Around line 195-207: The curl example that posts to /api/events fails when
AuthenticationSecret is enabled because it lacks an Authorization header; update
the documentation right under the shown curl snippet (the POST to /api/events)
to either add a one-line note telling users to include Authorization: Bearer
<token> when AuthenticationSecret is enabled or provide an alternate curl
snippet that includes the header (Authorization: Bearer <your-auth-token>) so
authenticated setups won't get false-negative errors.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4251090a-f90b-429b-b3f7-6b49a74d3a99

📥 Commits

Reviewing files that changed from the base of the PR and between 8e94c69 and 0ae6180.

📒 Files selected for processing (36)
  • ImmichFrame.Core.Tests/Events/InMemoryFrameEventQueueTests.cs
  • ImmichFrame.Core/Events/FrameEvent.cs
  • ImmichFrame.Core/Events/FrameEventAckStatus.cs
  • ImmichFrame.Core/Events/FrameEventAction.cs
  • ImmichFrame.Core/Events/FrameEventInput.cs
  • ImmichFrame.Core/Events/FrameEventMode.cs
  • ImmichFrame.Core/Interfaces/IFrameEventQueue.cs
  • ImmichFrame.Core/Interfaces/IServerSettings.cs
  • ImmichFrame.Core/Services/InMemoryFrameEventQueue.cs
  • ImmichFrame.WebApi.Tests/Events/FrameEventValidatorTests.cs
  • ImmichFrame.WebApi.Tests/Resources/TestV1.json
  • ImmichFrame.WebApi.Tests/Resources/TestV2.json
  • ImmichFrame.WebApi.Tests/Resources/TestV2.yml
  • ImmichFrame.WebApi/Controllers/EventsController.cs
  • ImmichFrame.WebApi/Helpers/Config/ServerSettingsV1.cs
  • ImmichFrame.WebApi/Models/ClientSettingsDto.cs
  • ImmichFrame.WebApi/Models/Events/FrameEventAckRequestDto.cs
  • ImmichFrame.WebApi/Models/Events/FrameEventDiagnosticsDto.cs
  • ImmichFrame.WebApi/Models/Events/FrameEventRequestDto.cs
  • ImmichFrame.WebApi/Models/Events/FrameEventResponseDto.cs
  • ImmichFrame.WebApi/Models/ServerSettings.cs
  • ImmichFrame.WebApi/Program.cs
  • ImmichFrame.WebApi/Services/FrameEventValidator.cs
  • docker/Settings.example.json
  • docker/Settings.example.yml
  • docs/docs/getting-started/configuration.md
  • docs/superpowers/plans/2026-05-18-banner-notifications.md
  • docs/superpowers/specs/2026-05-18-banner-notifications-design.md
  • immichFrame.Web/src/lib/components/elements/clock.svelte
  • immichFrame.Web/src/lib/components/events/BannerOverlay.svelte
  • immichFrame.Web/src/lib/components/events/EventOverlayHost.svelte
  • immichFrame.Web/src/lib/components/events/PopupTextOverlay.svelte
  • immichFrame.Web/src/lib/components/home-page/home-page.svelte
  • immichFrame.Web/src/lib/events/event-service.ts
  • immichFrame.Web/src/lib/immichFrameApi.ts
  • immichFrame.Web/src/lib/stores/config.store.ts

Comment thread docs/superpowers/plans/2026-05-18-banner-notifications.md
Comment thread docs/superpowers/specs/2026-05-18-banner-notifications-design.md Outdated
Comment thread ImmichFrame.Core/Events/FrameEventAckStatus.cs Outdated
Comment thread ImmichFrame.Core/Services/InMemoryFrameEventQueue.cs Outdated

<div class="weather-location">{weather.location},</div>
<div class="weather-temperature">{weather.temperature?.toFixed(1)}°</div>
<div class="weather-temperature">{weather.temperature?.toFixed(1)}{weather.unit ?? '°'}</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid rendering undefined when temperature is missing.

weather.temperature?.toFixed(1) can render as undefined, producing undefined° (or undefined{unit}) in the UI. Add a fallback for missing temperature.

Proposed fix
-            <div class="weather-temperature">{weather.temperature?.toFixed(1)}{weather.unit ?? '°'}</div>
+            <div class="weather-temperature">
+                {weather.temperature == null ? '—' : `${weather.temperature.toFixed(1)}${weather.unit ?? '°'}`}
+            </div>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div class="weather-temperature">{weather.temperature?.toFixed(1)}{weather.unit ?? '°'}</div>
<div class="weather-temperature">
{weather.temperature == null ? '' : `${weather.temperature.toFixed(1)}${weather.unit ?? '°'}`}
</div>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@immichFrame.Web/src/lib/components/elements/clock.svelte` at line 93, The
temperature expression can produce "undefined°" when weather.temperature is
absent; update the template so the temperature and unit are only rendered when
temperature is present. Replace the inline expression in the div with a
conditional/guard that checks weather.temperature (e.g., in the
weather-temperature div or a small helper like a computed/derived value) and
render either the formatted toFixed(1) plus unit (weather.unit ?? '°') when
non-null, or an empty string/placeholder when missing to avoid showing
"undefined".

Comment thread ImmichFrame.WebApi/Controllers/EventsController.cs Outdated
Comment thread ImmichFrame.WebApi/Helpers/Config/ServerSettingsV1.cs Outdated
Comment thread ImmichFrame.WebApi/Models/Events/FrameEventAckRequestDto.cs Outdated
Comment thread ImmichFrame.WebApi/Models/ServerSettings.cs Outdated
Comment thread ImmichFrame.WebApi/Services/FrameEventValidator.cs
rgregg added 3 commits May 18, 2026 20:59
- Add Dismissed to FrameEventAckStatus
- Scope queue category replacement by mode so Banner/PopupText do not evict each other
- Make FrameEventAckRequestDto.Status nullable so [Required] validates missing JSON
- Null-guard FrameEventValidator.Validate and EventsController.AckEvent
- Strip CR/LF from user-supplied log values
The 'show unit from API' change was bundled into commit fcf9c15 along with
unrelated popup countdown work. Split out to its own branch
(rgregg-weather-unit-fix) so this PR stays focused on the notification
system.
rgregg added 5 commits May 18, 2026 21:13
- delay() removes its abort listener on normal timeout path (no leak under long uptime)
- pollOne / acknowledgeEvent log non-2xx responses
- handlePopupEvent / handleBannerEvent run full cleanup before eventHostEnabled
  early-return so a mid-flight disable does not leave the slideshow paused
- Replace embedded hostnames / home paths in plan with placeholders
- Fix 'renderered' typo in spec
- Add Authorization-header note under the curl example
Rework the banner overlay for readability at a distance: 90vw width,
centered black text on translucent white with backdrop blur, responsive
font size (20-40px), and emoji-capable font stack. Drop the timeout
progress bar since auto-dismiss is handled upstream.
EventPollingIntervalSeconds and EventDefaultTimeoutMs already carry
[Range] attributes, but binding from config doesn't enforce them — an
out-of-range value would silently propagate (e.g. a negative default
timeout would break every event without an explicit timeoutMs through
FrameEventValidator's fallback path).

Wrap both properties on GeneralSettings (V2) and ServerSettingsV1 with
clamping setters using the same bounds, and surface the [Range] on the
V1 adapter so reflection-based tests can derive the expected value.
Adds bounds-test coverage and adapts ConfigLoaderTest to honor [Range]
when checking int sentinels.
Translucent white card on a softly blurred backdrop with black text,
larger responsive typography (title/message/button), and an
emoji-capable font stack matching BannerOverlay. Buttons centered as a
group; the countdown moves to the top-right corner so it doesn't push
button layout. Card accepts a pointerdown anywhere except on action
buttons to dismiss, making the whole surface a tap target.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
immichFrame.Web/src/lib/components/events/PopupTextOverlay.svelte (1)

34-39: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prevent permanent lock when dismiss callback fails.

A rejected/throwing onDismiss leaves dismissing = true, so the popup can no longer be closed from user input.

Proposed fix
 async function dismiss(status: FrameEventAckStatus = 'Closed') {
 	if (dismissing) return;
 	dismissing = true;
-	await onDismiss(status);
-	dismissing = false;
+	try {
+		await onDismiss(status);
+	} finally {
+		dismissing = false;
+	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@immichFrame.Web/src/lib/components/events/PopupTextOverlay.svelte` around
lines 34 - 39, The dismiss function can permanently set dismissing = true if
onDismiss rejects; update the dismiss function (referencing dismiss, dismissing,
and onDismiss) to ensure dismissing is reset regardless of onDismiss outcome by
wrapping the await onDismiss(status) in a try/finally (or try/catch with
finally) so dismissing = false is always executed; if you catch errors, rethrow
or forward them as appropriate so behavior is preserved while preventing a
permanent lock.
immichFrame.Web/src/lib/components/events/BannerOverlay.svelte (1)

14-19: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Reset dismissing even when onDismiss fails.

If onDismiss throws, dismissing stays true and the banner becomes permanently non-dismissible for that render.

Proposed fix
 async function dismiss(status: FrameEventAckStatus = 'Dismissed') {
 	if (dismissing) return;
 	dismissing = true;
-	await onDismiss(status);
-	dismissing = false;
+	try {
+		await onDismiss(status);
+	} finally {
+		dismissing = false;
+	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@immichFrame.Web/src/lib/components/events/BannerOverlay.svelte` around lines
14 - 19, The dismiss function currently sets the local flag dismissing = true
and awaits onDismiss(status) but never resets dismissing if onDismiss throws,
leaving the banner permanently non-dismissible; modify the dismiss function (the
dismiss async function using the dismissing variable and calling onDismiss) to
ensure dismissing is reset to false in a finally block (i.e., set dismissing =
true before calling await onDismiss(status), and always set dismissing = false
in a finally so failures cannot leave the flag true).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@immichFrame.Web/src/lib/components/events/BannerOverlay.svelte`:
- Around line 14-19: The dismiss function currently sets the local flag
dismissing = true and awaits onDismiss(status) but never resets dismissing if
onDismiss throws, leaving the banner permanently non-dismissible; modify the
dismiss function (the dismiss async function using the dismissing variable and
calling onDismiss) to ensure dismissing is reset to false in a finally block
(i.e., set dismissing = true before calling await onDismiss(status), and always
set dismissing = false in a finally so failures cannot leave the flag true).

In `@immichFrame.Web/src/lib/components/events/PopupTextOverlay.svelte`:
- Around line 34-39: The dismiss function can permanently set dismissing = true
if onDismiss rejects; update the dismiss function (referencing dismiss,
dismissing, and onDismiss) to ensure dismissing is reset regardless of onDismiss
outcome by wrapping the await onDismiss(status) in a try/finally (or try/catch
with finally) so dismissing = false is always executed; if you catch errors,
rethrow or forward them as appropriate so behavior is preserved while preventing
a permanent lock.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 93e75508-2e3e-4b26-8c67-a35c19ecca74

📥 Commits

Reviewing files that changed from the base of the PR and between 472eb5c and 9cba76a.

📒 Files selected for processing (6)
  • ImmichFrame.WebApi.Tests/Helpers/Config/ConfigLoaderTest.cs
  • ImmichFrame.WebApi.Tests/Models/EventSettingsBoundsTests.cs
  • ImmichFrame.WebApi/Helpers/Config/ServerSettingsV1.cs
  • ImmichFrame.WebApi/Models/ServerSettings.cs
  • immichFrame.Web/src/lib/components/events/BannerOverlay.svelte
  • immichFrame.Web/src/lib/components/events/PopupTextOverlay.svelte

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants