Skip to content

Subsystem 4b: Smart Alarms (#alarms channel, pair/validate, @everyone + team-chat relay on trigger)#23

Merged
HandyS11 merged 17 commits into
developfrom
feat/smart-alarms
Jun 22, 2026
Merged

Subsystem 4b: Smart Alarms (#alarms channel, pair/validate, @everyone + team-chat relay on trigger)#23
HandyS11 merged 17 commits into
developfrom
feat/smart-alarms

Conversation

@HandyS11

Copy link
Copy Markdown
Owner

Summary

Adds Smart Alarms end-to-end, the second slice of subsystem 4 (smart devices), on the socket-trigger model established by the smart-device refactor (#22): alarms work exactly like smart switches — primed on connect, reacting to the in-game SmartDeviceTriggeredEvent (the entity id is the switch-vs-alarm discriminant). The FCM alarm push is not used for attribution (it's a server-level "poke", not per-alarm).

Flow: pair an alarm in-game → FCM OnSmartAlarmPairing → user-validated "Add it?" prompt in a per-server #alarms channel → on Accept, a persistent per-alarm embed. On (re)connect the supervisor primes each managed alarm (registering the socket's interest). When the alarm goes active in-game, its embed updates (state + "last triggered N ago") and — per the alarm's toggles — pings @everyone in #alarms and/or relays a 🚨 {name} triggered line to in-game team chat. Controls: @everyone toggle, relay-to-team-chat toggle, Rename. Validated/opt-in; any guild member operates them; unreachable shown inline when the server isn't connected.

What's included

  • Domain/Persistence: SmartAlarm entity (socket-state: LastIsActive + LastTriggeredUtc), IAlarmStore/AlarmStore (UpdateStateAsync, toggles, idempotent AddAsync), one SmartAlarms migration.
  • Pairing: PairingNotification.EntityKind threads PairedEntityKind; PairingHandler routes switch→SwitchPairedEvent (unchanged) / alarm→AlarmPairedEvent; FCM source subscribes OnSmartAlarmPairing.
  • Connections: ConnectionSupervisor.PrimeDevicesAsync also primes alarms (via an extracted PrimeEntityIdsAsync DRY helper) — the only Connections edit; the generic SmartDeviceTriggeredEvent seam is unchanged.
  • Workspace: per-server #alarms channel + EN/FR channel.alarms.name + AlarmChannelLocator.
  • New Features.Alarms project: AlarmEmbedRenderer, AlarmPairingCoordinator, IAlarmRefresher/AlarmRefresher, AlarmStateRelay (active-edge ping/relay, ExistsAsync filter, unreachable on disconnect), per-slice AlarmLocalizer + IAlarmChannelPoster/DiscordAlarmChannelPoster, thin AlarmComponentModule + rename modal, AlarmsHostedService, AddAlarms DI + Host wiring.

Design notes

  • A single SmartDeviceTriggeredEvent is consumed by both SwitchStateRelay (filters to switch ids) and AlarmStateRelay (filters to alarm ids); the event bus fans out per-subscriber, and per-store unique (GuildId, ServerId, EntityId) indexes make switch↔alarm cross-talk structurally impossible.
  • Only the active edge notifies: a deactivation re-renders the embed but does not ping/relay. Team-chat relay failures are swallowed (never block the embed update or the ping).
  • Switch behavior is unregressed (the EntityKind default keeps the switch path identical).

Verification

  • Full suite: 520 tests across 12 assemblies, 0 failures, none dropped.
  • Strict build 0 warnings / 0 errors (-warnaserror); jb ReformatAndReorder clean.
  • EF: exactly one SmartAlarms migration, no other drift.

🤖 Generated with Claude Code

HandyS11 and others added 17 commits June 22, 2026 22:48
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…catalog)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix 1: Update stale XML comment on AlarmLocalizationCatalog.Default to reference the actual per-slice AlarmLocalizer constructor instead of non-existent shared ILocalizer type.

Fix 2: Extract IAlarmLocalizer interface (matching ISwitchLocalizer pattern) with Get(key, culture) and Get(key, culture, params args) methods. Make AlarmLocalizer implement it with <inheritdoc /> on public methods. Update AlarmEmbedRenderer ctor to accept IAlarmLocalizer instead of concrete AlarmLocalizer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…oordinator

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ert prompt replacement

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ay, unreachable)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wire the smart-alarms slice live: AlarmsHostedService drives three
consume loops (AlarmPairedEvent → coordinator, SmartDeviceTriggeredEvent
→ relay, ConnectionStatusChangedEvent → relay); AddAlarms registers all
slice singletons and the hosted service; Host references the Alarms
project and calls AddAlarms() after AddSwitches(). AlarmRegistrationTests
covers both descriptor presence and captive-dependency-free resolution.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Whitespace-only line-wrap reformats produced by dotnet jb cleanupcode
--profile=ReformatAndReorder. No semantic changes.

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

Add an IAlarmRefresher.RefreshAsync(SmartAlarm, ...) overload that renders an
already-loaded alarm, skipping the per-alarm store.GetAsync. The id-based method
now fetches-then-delegates via a shared RenderAndPostAsync helper.
HandleConnectionStatusAsync passes the alarms it already listed instead of
re-querying each by id.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@HandyS11 HandyS11 merged commit 0bd467a into develop Jun 22, 2026
2 checks passed
@HandyS11 HandyS11 deleted the feat/smart-alarms branch June 22, 2026 23:05
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.

1 participant