Skip to content

New signals - #92

Open
trueroguan wants to merge 2 commits into
OpenIPC:mainfrom
trueroguan:signal
Open

trueroguan wants to merge 2 commits into
OpenIPC:mainfrom
trueroguan:signal

Conversation

@trueroguan

Copy link
Copy Markdown

Summary

Extends camera-event notifications and adds external actions:

  • Thresholds: min AI confidence, min event duration and min objects in frame. They gate both notifications and actions.
  • Notification style (desktop): the app's own toast with a built-in chime or custom sound and a volume control, or native Windows toasts managed in Windows Settings.
  • External actions: TCP/UDP signal (text or hex), HTTP webhook, e-mail and running a program/script. E-mail goes via SMTP (MailKit, login stored in Windows Credential Manager), the default MAPI client or Outlook. Actions have their own cooldown and fire during quiet hours by default.
  • New Settings → Notifications section. Each event episode now produces exactly one notification; the duplicate toast on episode close is gone.

Related

Phase 19 — Community & app-level (19.3 Notifications).

Type

  • Bug fix
  • Feature
  • Refactor / cleanup
  • Docs / CI
  • Other:

Checklist

  • Builds with 0 warnings (TreatWarningsAsErrors=true).
  • Tests pass (dotnet test); new Core logic has unit tests.
  • No layering violation — App references Core only (Infrastructure / Video / Devices wired via DI in a head).
  • Scope stays within one phase (didn't pull work from a later phase's "Не входит").
  • README / docs updated if public commands, options, or setup changed.

Platforms tested

  • Windows
  • Linux
  • macOS
  • Android
  • iOS
  • CI build only

Screenshots / notes

  • New dependency: MailKit 4.18.1.
  • Windows end-to-end run against local SMTP/TCP/UDP/HTTP sinks passed 12/12.
  • Also fixes the stale SshSessionIntegrationTests constructor call.

🤖 Generated with Claude Code

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Add threshold-gated camera alerts and external actions

✨ Enhancement 🐞 Bug fix 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Gate camera alerts and actions by confidence, duration, and object count; prevent duplicate
 episode toasts.
• Add desktop notification styles, configurable sounds, and TCP/UDP, webhook, e-mail, and program
 actions.
• Add a dedicated Notifications settings section, secure SMTP credentials, and event-action tests.
Diagram

graph TD
  A["Analytics ticks"] --> B["Event ingestion"] --> C["Episode coordinator"] --> D{"Thresholds pass"}
  S["Notification settings"] --> C
  D -->|notify| N["Desktop notification"]
  D -->|act| E["Action dispatcher"] --> X["External targets"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use a Windows SDK projection for native toasts
  • ➕ Typed WinRT APIs would reduce manual COM-vtable and lifetime-management risk.
  • ➖ May require Windows-specific targets or additional dependencies in a cross-platform desktop build.

Recommendation: Keep the shared policy in Core and transport implementations behind interfaces; that separation suits the existing architecture. Before committing to long-term maintenance of the raw WinRT implementation, verify whether a Windows SDK projection can preserve the current cross-platform build.

Files changed (43) +4347 / -156

Enhancement (25) +2996 / -41
DesktopNotificationService.csSelect app or Windows desktop notifications +36/-4

Select app or Windows desktop notifications

• Routes notifications to native Windows toasts when selected; otherwise displays app toasts and plays the configured sound.

src/OpenIPC.Viewer.App/Services/DesktopNotificationService.cs

ISystemNotificationSink.csDefine the native notification sink contract +31/-0

Define the native notification sink contract

• Introduces an interface for native toast availability, delivery, status, and opening OS notification settings.

src/OpenIPC.Viewer.App/Services/ISystemNotificationSink.cs

Localizer.csLocalize notification and action controls +148/-0

Localize notification and action controls

• Adds English and Russian text for thresholds, notification styles, sounds, credentials, and external-action settings.

src/OpenIPC.Viewer.App/Services/Localizer.cs

NotificationSoundService.csImport and play configurable notification sounds +151/-0

Import and play configurable notification sounds

• Manages the built-in chime or an imported, length-limited WAV clip, with volume control and playback caching.

src/OpenIPC.Viewer.App/Services/NotificationSoundService.cs

UserSettingsService.csExpose alert policy and protect concurrent settings edits +23/-0

Expose alert policy and protect concurrent settings edits

• Exposes thresholds and action settings to Core. Adds a functional update method that applies edits to the latest settings inside the save gate.

src/OpenIPC.Viewer.App/Services/UserSettingsService.cs

Theme.axamlAdd the Notifications section icon +2/-0

Add the Notifications section icon

• Defines a bell geometry for the new Settings navigation entry.

src/OpenIPC.Viewer.App/Themes/Theme.axaml

NotificationSettingsViewModel.csManage notification and external-action settings +575/-0

Manage notification and external-action settings

• Adds the dedicated settings view model with debounced persistence, sound import and preview, OS status, credential storage, and per-channel test commands.

src/OpenIPC.Viewer.App/ViewModels/NotificationSettingsViewModel.cs

SettingsPage.axamlAdd the Notifications settings page +340/-13

Add the Notifications settings page

• Adds controls for event thresholds, quiet hours, notification styles, sounds, and each external-action channel.

src/OpenIPC.Viewer.App/Views/Pages/SettingsPage.axaml

SettingsPage.axaml.csLoad and flush notification settings with page lifecycle +9/-2

Load and flush notification settings with page lifecycle

• Refreshes credentials and Windows notification status when the page attaches, and flushes pending edits when it detaches.

src/OpenIPC.Viewer.App/Views/Pages/SettingsPage.axaml.cs

AnalyticsMotionEventSource.csInclude detection peaks in analytics ticks +5/-1

Include detection peaks in analytics ticks

• Passes the highest detection confidence and detection count into motion ticks for threshold evaluation.

src/OpenIPC.Viewer.Core/Events/AnalyticsMotionEventSource.cs

CameraEvent.csCarry episode peak and activity metadata +9/-1

Carry episode peak and activity metadata

• Adds optional peak confidence, peak object count, and last-seen time to streamed camera events.

src/OpenIPC.Viewer.Core/Events/CameraEvent.cs

EventIngestionService.csPublish peak and progress updates for open episodes +62/-18

Publish peak and progress updates for open episodes

• Tracks peak values and last activity, publishing updates when peaks rise or ongoing activity crosses the progress interval.

src/OpenIPC.Viewer.Core/Events/EventIngestionService.cs

IMotionEventSource.csExtend motion ticks with detection metrics +6/-2

Extend motion ticks with detection metrics

• Adds optional confidence and object-count fields so analytics observations can reach event ingestion.

src/OpenIPC.Viewer.Core/Events/IMotionEventSource.cs

EventActionContext.csDefine action context and template rendering +156/-0

Define action context and template rendering

• Provides event facts and placeholder expansion for payloads, URLs, JSON bodies, messages, and program arguments.

src/OpenIPC.Viewer.Core/Notifications/EventActionContext.cs

IEventActionDispatcher.csDefine action and mail credential abstractions +41/-0

Define action and mail credential abstractions

• Adds contracts for dispatching or testing individual channels and storing SMTP credentials outside user settings.

src/OpenIPC.Viewer.Core/Notifications/IEventActionDispatcher.cs

NotificationSoundPlayer.csPlay alert clips on a private audio output +112/-0

Play alert clips on a private audio output

• Streams short PCM clips to a separate audio device instance, applying volume and replacing playback when a new alert arrives.

src/OpenIPC.Viewer.Core/Notifications/NotificationSoundPlayer.cs

PcmClip.csAdd PCM clips, WAV conversion, and built-in chime +267/-0

Add PCM clips, WAV conversion, and built-in chime

• Introduces clip volume and truncation, a WAV reader/writer, a decoder contract, and a generated two-note chime.

src/OpenIPC.Viewer.Core/Notifications/PcmClip.cs

SignalPayload.csEncode text and hexadecimal signal payloads +88/-0

Encode text and hexadecimal signal payloads

• Converts rendered signals to UTF-8 with escapes or raw bytes from a hex-prefixed payload.

src/OpenIPC.Viewer.Core/Notifications/SignalPayload.cs

IUserSettingsAccessor.csExpose thresholds and actions to Core +10/-0

Expose thresholds and actions to Core

• Extends the settings contract used by notification policy without introducing a dependency on App settings.

src/OpenIPC.Viewer.Core/Settings/IUserSettingsAccessor.cs

WindowsToastNotifier.csDeliver native Windows toasts +322/-0

Deliver native Windows toasts

• Registers an app identity and uses a dedicated WinRT worker to send native toasts, query notification status, and open Windows settings.

src/OpenIPC.Viewer.Desktop/Notifications/WindowsToastNotifier.cs

EventActionDispatcher.csDispatch network, mail, and program actions +235/-0

Dispatch network, mail, and program actions

• Runs enabled channels independently with timeouts and logging. Supports TCP/UDP signals, HTTP webhooks, e-mail transports, and local programs.

src/OpenIPC.Viewer.Infrastructure/Notifications/EventActionDispatcher.cs

MapiMailSender.csSend mail through the default Windows client +161/-0

Send mail through the default Windows client

• Adds Simple MAPI delivery on an STA thread with checks for an installed MAPI-capable mail client.

src/OpenIPC.Viewer.Infrastructure/Notifications/MapiMailSender.cs

OutlookMailSender.csSend mail through classic Outlook +82/-0

Send mail through classic Outlook

• Adds late-bound Outlook COM automation on an STA thread without an Office interop package.

src/OpenIPC.Viewer.Infrastructure/Notifications/OutlookMailSender.cs

SmtpMailSender.csSend event mail through SMTP +49/-0

Send event mail through SMTP

• Uses MailKit for MIME messages, selectable TLS modes, and credentials retrieved from the secure store.

src/OpenIPC.Viewer.Infrastructure/Notifications/SmtpMailSender.cs

FfmpegAudioClipDecoder.csDecode imported alert sounds with FFmpeg +76/-0

Decode imported alert sounds with FFmpeg

• Reuses the audio decoding pipeline to convert supported files to bounded, stereo PCM clips.

src/OpenIPC.Viewer.Video/Pipeline/FfmpegAudioClipDecoder.cs

Bug fix (2) +206 / -34
NotificationCoordinator.csGate and deduplicate episode deliveries +197/-33

Gate and deduplicate episode deliveries

• Applies shared thresholds to notifications and actions, tracks one delivery per channel per episode, and uses separate cooldown and quiet-hours policies.

src/OpenIPC.Viewer.Core/Notifications/NotificationCoordinator.cs

SshSessionIntegrationTests.csRepair SSH integration-test factory construction +9/-1

Repair SSH integration-test factory construction

• Supplies the required changed-host-key prompt argument to the session factory.

tests/OpenIPC.Viewer.Infrastructure.Tests/SshSessionIntegrationTests.cs

Refactor (1) +13 / -50
SettingsPageViewModel.csMove notification settings into their own section +13/-50

Move notification settings into their own section

• Replaces inline notification properties with the dedicated view model and updates section selection and About navigation.

src/OpenIPC.Viewer.App/ViewModels/SettingsPageViewModel.cs

Tests (5) +798 / -23
EventActionTests.csTest templates, payloads, and audio primitives +200/-0

Test templates, payloads, and audio primitives

• Covers placeholder rendering, JSON escaping, signal byte encoding, WAV conversion, volume, truncation, and chime generation.

tests/OpenIPC.Viewer.Core.Tests/Notifications/EventActionTests.cs

EventIngestionPeakTests.csTest open-episode peak and progress publication +136/-0

Test open-episode peak and progress publication

• Checks peak-rise updates, throttled last-seen progress, and the absence of detection peaks on plain motion.

tests/OpenIPC.Viewer.Core.Tests/Notifications/EventIngestionPeakTests.cs

NotificationCoordinatorTests.csTest thresholds and once-per-episode delivery +256/-23

Test thresholds and once-per-episode delivery

• Adds coverage for confidence, object, and activity-duration gates; duplicate suppression; action cooldown; quiet hours; and action-only delivery.

tests/OpenIPC.Viewer.Core.Tests/Notifications/NotificationCoordinatorTests.cs

EventActionDispatcherTests.csExercise external actions against local sinks +201/-0

Exercise external actions against local sinks

• Tests loopback TCP, UDP, and HTTP delivery, channel failure isolation, SMTP validation, and Windows batch execution.

tests/OpenIPC.Viewer.Infrastructure.Tests/EventActionDispatcherTests.cs

TestDoubles.csExtend fake settings for notification policy +5/-0

Extend fake settings for notification policy

• Provides threshold and disabled-action defaults required by the expanded settings interface.

tests/OpenIPC.Viewer.Infrastructure.Tests/TestDoubles.cs

Documentation (1) +8 / -2
README.mdDocument alert thresholds and external actions +8/-2

Document alert thresholds and external actions

• Describes configurable notification styles and sounds, threshold-gated alerts, and the available external actions.

README.md

Other (9) +326 / -6
Directory.Packages.propsPin MailKit for SMTP actions +4/-0

Pin MailKit for SMTP actions

• Adds MailKit 4.18.1 to central package versions for SMTP transport, including implicit TLS support.

Directory.Packages.props

UserSettings.csPersist notification preferences and actions +20/-0

Persist notification preferences and actions

• Adds style, sound, threshold, and nested external-action settings with defaults compatible with older settings files.

src/OpenIPC.Viewer.App/Services/UserSettings.cs

SharedComposition.csWire notification policy and action services +27/-4

Wire notification policy and action services

• Registers sound decoding, credential storage, the action dispatcher, and the settings view model; supplies camera names to the coordinator.

src/OpenIPC.Viewer.Composition/SharedComposition.cs

EventActionSettings.csDefine external-action configuration +78/-0

Define external-action configuration

• Models signal, webhook, e-mail, and program settings, including independent cooldown and default quiet-hours behavior.

src/OpenIPC.Viewer.Core/Notifications/EventActionSettings.cs

Composition.csRegister desktop sound and Windows toast implementations +28/-2

Register desktop sound and Windows toast implementations

• Creates private platform audio outputs for alerts and registers the native toast sink on supported Windows versions.

src/OpenIPC.Viewer.Desktop/Composition.cs

OpenIPC.Viewer.Desktop.csprojEnable unsafe Windows toast interop +2/-0

Enable unsafe Windows toast interop

• Allows unsafe blocks required by the native WinRT vtable implementation.

src/OpenIPC.Viewer.Desktop/OpenIPC.Viewer.Desktop.csproj

MailCredentialStores.csStore SMTP credentials outside user settings +161/-0

Store SMTP credentials outside user settings

• Uses Windows Credential Manager on Windows and the existing platform secrets store elsewhere.

src/OpenIPC.Viewer.Infrastructure/Notifications/MailCredentialStores.cs

OpenIPC.Viewer.Infrastructure.csprojReference MailKit in Infrastructure +1/-0

Reference MailKit in Infrastructure

• Adds the package reference used by the SMTP action.

src/OpenIPC.Viewer.Infrastructure/OpenIPC.Viewer.Infrastructure.csproj

ServerSettings.csImplement new settings defaults for Web +5/-0

Implement new settings defaults for Web

• Supplies disabled action and threshold defaults for the expanded Core settings interface.

src/OpenIPC.Viewer.Web/Backend/ServerSettings.cs

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (7) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Active events miss later deliveries 🐞 Bug ≡ Correctness
Description
NotificationCoordinator.Evaluate sets each channel's done flag before checking whether cooldown or
quiet hours permit delivery. If an episode first passes its thresholds while a channel is
suppressed, later progress updates cannot deliver through that channel even after the suppression
ends.
Code

src/OpenIPC.Viewer.Core/Notifications/NotificationCoordinator.cs[R167-170]

+        if (!ep.NotifyDone)
+        {
+            ep.NotifyDone = true;
+            if (ShouldNotifyLocked(e, nowLocal))
Evidence
Both flags are set before their respective gates; those gates check cooldown and quiet hours. The
ingestion service emits further updates for active episodes, but the flags prevent another attempt.

src/OpenIPC.Viewer.Core/Notifications/NotificationCoordinator.cs[159-205]
src/OpenIPC.Viewer.Core/Events/EventIngestionService.cs[98-114]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
An episode permanently marks a channel complete when its delivery gate rejects the first threshold-passing update.
## Fix Focus Areas
- src/OpenIPC.Viewer.Core/Notifications/NotificationCoordinator.cs[159-205]
## Recommended Fix
Set each done flag only when its channel is selected to deliver. Keep suppressed channels eligible on subsequent open-episode updates, while preserving the once-per-channel guarantee after selection.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. A failed toast also drops external actions 🐞 Bug ☼ Reliability
Description
NotificationCoordinator.DeliverAsync calls _sink.Show before dispatching actions inside one try
block that silently catches exceptions. If the notification sink throws, the selected actions are
skipped and the failure leaves no log entry.
Code

src/OpenIPC.Viewer.Core/Notifications/NotificationCoordinator.cs[R221-224]

+            if (notify)
+            {
+                var hasName = !string.IsNullOrWhiteSpace(name);
+                _sink.Show(new NotificationRequest(title, Describe(e, hasName ? name : null), e.Kind, e.CameraId));
Evidence
The action call occurs after _sink.Show in the same try block. Its catch discards every exception,
so a sink failure bypasses action dispatch.

src/OpenIPC.Viewer.Core/Notifications/NotificationCoordinator.cs[208-237]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A throwing notification sink prevents external actions from running for the same event, and the exception is discarded.
## Fix Focus Areas
- src/OpenIPC.Viewer.Core/Notifications/NotificationCoordinator.cs[208-237]
## Recommended Fix
Handle sink and dispatcher failures independently so action dispatch still runs after a sink exception. Log delivery exceptions with their stack traces.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Large sound imports can exhaust memory 🐞 Bug ➹ Performance
Description
NotificationSoundService.ImportAsync reads an entire WAV file through WavCodec.Read before
applying its 15-second truncation limit. A long uncompressed file causes a full data-chunk
allocation and a second PCM allocation during conversion, despite only a short clip being needed.
Code

src/OpenIPC.Viewer.App/Services/NotificationSoundService.cs[R120-121]

+                using var stream = File.OpenRead(sourcePath);
+                clip = WavCodec.Read(stream);
Evidence
The importer truncates only after reading; the WAV reader allocates the available data chunk, and
conversion allocates an output array based on all its frames. Custom playback also uses this reader.

src/OpenIPC.Viewer.App/Services/NotificationSoundService.cs[112-133]
src/OpenIPC.Viewer.Core/Notifications/PcmClip.cs[108-118]
src/OpenIPC.Viewer.Core/Notifications/PcmClip.cs[151-167]
src/OpenIPC.Viewer.App/Services/NotificationSoundService.cs[81-93]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
WAV import loads and converts the complete file before enforcing the custom-sound duration limit.
## Fix Focus Areas
- src/OpenIPC.Viewer.App/Services/NotificationSoundService.cs[112-133]
- src/OpenIPC.Viewer.Core/Notifications/PcmClip.cs[80-118]
## Recommended Fix
Pass the duration limit into WAV reading and read or convert no more frames than that limit permits. Keep playback loading subject to the same bound.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View review recommended (4)
4. Null action settings break notification saves 🐞 Bug ☼ Reliability
Description
NotificationSettingsViewModel.BuildActions calls Trim() on action fields copied directly from
deserialized settings without checking for null. If usersettings.json contains JSON null for an
action string, a subsequent notification edit or test invokes SaveNowAsync and raises a
NullReferenceException.
Code

src/OpenIPC.Viewer.App/ViewModels/NotificationSettingsViewModel.cs[412]

+        SignalHost: SignalHost.Trim(),
Evidence
Settings are deserialized without normalizing null members; the view model assigns those members
directly, and every save builds an action record using unconditional Trim calls.

src/OpenIPC.Viewer.App/Services/UserSettingsService.cs[103-113]
src/OpenIPC.Viewer.App/ViewModels/NotificationSettingsViewModel.cs[276-303]
src/OpenIPC.Viewer.App/ViewModels/NotificationSettingsViewModel.cs[361-430]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
JSON null action fields reach unconditional Trim calls when notification settings are saved.
## Fix Focus Areas
- src/OpenIPC.Viewer.App/ViewModels/NotificationSettingsViewModel.cs[276-303]
- src/OpenIPC.Viewer.App/ViewModels/NotificationSettingsViewModel.cs[406-430]
## Recommended Fix
Normalize nullable action strings to empty strings on load, and make construction of saved action settings null-safe.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Camera or event text can inject script commands 🐞 Bug ⛨ Security
Description
StartProgram renders ProgramArguments with EventActionTemplate.Render and no encoder, so
{camera} and {summary} go into the raw command-line string unchanged. The default template is
"{camera}" {kind} "{summary}", so a " in a camera name or detection summary ends the quoted
argument and adds extra arguments. For .bat/.cmd targets, cmd.exe parses that line, so &, |
or > in those values run extra commands.
Code

src/OpenIPC.Viewer.Infrastructure/Notifications/EventActionDispatcher.cs[R188-201]

+        var args = EventActionTemplate.Render(s.ProgramArguments ?? "", context);
+        var ext = Path.GetExtension(path).ToLowerInvariant();
+
+        ProcessStartInfo psi;
+        if (ext == ".ps1")
+        {
+            // A script the user picked themselves; run it without the profile and
+            // without tripping over the machine's default execution policy.
+            psi = new ProcessStartInfo("powershell.exe",
+                $"-NoProfile -NonInteractive -ExecutionPolicy Bypass -File \"{path}\" {args}");
+        }
+        else if (ext is ".exe" or ".bat" or ".cmd" or ".com" || !OperatingSystem.IsWindows())
+        {
+            psi = new ProcessStartInfo(path, args);
Evidence
Render only escapes substituted values when an encode delegate is passed. The HTTP channel passes
JsonEscape, but the program channel passes nothing. The default arguments put
{camera}/{summary} inside hand-written quotes, and the joined string goes to ProcessStartInfo as
one command line. .bat/.cmd take this path, which means cmd.exe interprets the line.

src/OpenIPC.Viewer.Core/Notifications/EventActionContext.cs[57-93]
src/OpenIPC.Viewer.Core/Notifications/EventActionSettings.cs[48-51]
src/OpenIPC.Viewer.Infrastructure/Notifications/EventActionDispatcher.cs[182-207]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Program action arguments are built by putting unescaped event values (camera name, summary) into a command-line string. Quotes split the arguments, and for .bat/.cmd files cmd.exe metacharacters run extra commands.

## Fix Focus Areas
- src/OpenIPC.Viewer.Infrastructure/Notifications/EventActionDispatcher.cs[182-207]
- src/OpenIPC.Viewer.Core/Notifications/EventActionSettings.cs[48-51]

## Recommended Fix
Pass an encoder to `EventActionTemplate.Render` for the program channel. It should strip or escape `"` and backslashes using Windows argv rules, and for .bat/.cmd it should also neutralise `& | < > ^ %`. A safer alternative is to tokenise the template first, render each token separately and add the results to `ProcessStartInfo.ArgumentList`. Scripts can also read the values from the OPENIPC_* environment variables that are already set.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. SMTP password can be sent unencrypted 🐞 Bug ⛨ Security
Description
SmtpMailSender.SendAsync maps the default SmtpSecurity value of "auto" to
StartTlsWhenAvailable on ports other than 465, then calls AuthenticateAsync without checking
whether TLS was negotiated. With the default port 587, a server that does not offer STARTTLS—or a
network attacker that strips it—can cause the login and password retrieved from credential storage
to travel in cleartext.
Code

src/OpenIPC.Viewer.Infrastructure/Notifications/SmtpMailSender.cs[R22-28]

+        var options = (s.SmtpSecurity ?? "auto").Trim().ToLowerInvariant() switch
+        {
+            "ssl" => SecureSocketOptions.SslOnConnect,
+            "starttls" => SecureSocketOptions.StartTls,
+            "none" => SecureSocketOptions.None,
+            _ => port == 465 ? SecureSocketOptions.SslOnConnect : SecureSocketOptions.StartTlsWhenAvailable,
+        };
Evidence
The settings default to port 587 and security "auto"; the sender’s switch selects opportunistic
StartTlsWhenAvailable for every port except 465. Authentication follows the connection without a
client.IsSecure check, so it can run when STARTTLS was not negotiated.

src/OpenIPC.Viewer.Infrastructure/Notifications/SmtpMailSender.cs[22-46]
src/OpenIPC.Viewer.Core/Notifications/EventActionSettings.cs[42-46]
src/OpenIPC.Viewer.Core/Notifications/EventActionSettings.cs[40-47]
src/OpenIPC.Viewer.Infrastructure/Notifications/SmtpMailSender.cs[21-28]
src/OpenIPC.Viewer.Infrastructure/Notifications/SmtpMailSender.cs[42-46]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The default SMTP `auto` mode permits authentication with stored credentials over plaintext when STARTTLS is unavailable or stripped.

## Fix Focus Areas
- src/OpenIPC.Viewer.Infrastructure/Notifications/SmtpMailSender.cs[21-46]
- src/OpenIPC.Viewer.Core/Notifications/EventActionSettings.cs[44-46]

## Recommended Fix
Require TLS before authenticating in the default mode: map `auto` on ports other than 465 to `SecureSocketOptions.StartTls`. If opportunistic STARTTLS must remain, check `client.IsSecure` before `AuthenticateAsync` and throw a clear error when it is false, unless the security mode was explicitly set to `none`. Keep any explicitly selected insecure mode clearly distinct from the default.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Open events rebuild the Events page every second 🐞 Bug ➹ Performance
Description
OnTick now publishes the open event again (same Id) at least once per second while ticks keep
arriving, and on every peak rise. EventsPageViewModel.OnLiveEvent answers each publication with a
full ApplyFilter on the UI thread, which regroups, refolds and repages up to 10,000 cached events
and clears/rebuilds PageItems. With several active cameras this means several full list rebuilds
per second while the Events page is open, which costs CPU and causes visible list churn.
Code

src/OpenIPC.Viewer.Core/Events/EventIngestionService.cs[R101-105]

+            if (!isNew && state.Published && (grew || tick.At - state.LastPublishedTickAt >= ProgressInterval))
+            {
+                update = Snapshot(state, endedAt: null);
+                state.LastPublishedTickAt = tick.At;
+            }
Evidence
Before this PR the stream carried only open and close per episode. The new throttled progress
updates reach every subscriber. The Events page subscribes to the same stream, and for every event
it posts to the UI thread and calls ApplyFilter, which recomputes over _events (LoadLimit 10000)
and clears PageItems.

src/OpenIPC.Viewer.Core/Events/EventIngestionService.cs[86-114]
src/OpenIPC.Viewer.App/ViewModels/EventsPageViewModel.cs[380-394]
src/OpenIPC.Viewer.App/ViewModels/EventsPageViewModel.cs[255-290]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new once-a-second progress updates of open events trigger a full Events page re-filter and rebuild each time.

## Fix Focus Areas
- src/OpenIPC.Viewer.Core/Events/EventIngestionService.cs[86-114]
- src/OpenIPC.Viewer.App/ViewModels/EventsPageViewModel.cs[380-394]

## Recommended Fix
One option is to publish progress updates on a separate observable that only NotificationCoordinator subscribes to. The other is to make EventsPageViewModel ignore updates for an event it already has (same Id, EndedAt still null) unless a field it displays changed, or to coalesce rebuilds with a debounce.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
Review mode: Auto: 🧠 Deep: Many independent notification, action, networking, credential, and platform paths changed.

Grey Divider

Tip of the day
💡 Did you know, you can see how risky a PR is at a glance with blast radius labels

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +167 to +170
if (!ep.NotifyDone)
{
ep.NotifyDone = true;
if (ShouldNotifyLocked(e, nowLocal))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

1. Active events miss later deliveries 🐞 Bug ≡ Correctness

NotificationCoordinator.Evaluate sets each channel's done flag before checking whether cooldown or
quiet hours permit delivery. If an episode first passes its thresholds while a channel is
suppressed, later progress updates cannot deliver through that channel even after the suppression
ends.
Agent Prompt
## Issue description
An episode permanently marks a channel complete when its delivery gate rejects the first threshold-passing update.
## Fix Focus Areas
- src/OpenIPC.Viewer.Core/Notifications/NotificationCoordinator.cs[159-205]
## Recommended Fix
Set each done flag only when its channel is selected to deliver. Keep suppressed channels eligible on subsequent open-episode updates, while preserving the once-per-channel guarantee after selection.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +221 to +224
if (notify)
{
var hasName = !string.IsNullOrWhiteSpace(name);
_sink.Show(new NotificationRequest(title, Describe(e, hasName ? name : null), e.Kind, e.CameraId));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

2. A failed toast also drops external actions 🐞 Bug ☼ Reliability

NotificationCoordinator.DeliverAsync calls _sink.Show before dispatching actions inside one try
block that silently catches exceptions. If the notification sink throws, the selected actions are
skipped and the failure leaves no log entry.
Agent Prompt
## Issue description
A throwing notification sink prevents external actions from running for the same event, and the exception is discarded.
## Fix Focus Areas
- src/OpenIPC.Viewer.Core/Notifications/NotificationCoordinator.cs[208-237]
## Recommended Fix
Handle sink and dispatcher failures independently so action dispatch still runs after a sink exception. Log delivery exceptions with their stack traces.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +120 to +121
using var stream = File.OpenRead(sourcePath);
clip = WavCodec.Read(stream);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

3. Large sound imports can exhaust memory 🐞 Bug ➹ Performance

NotificationSoundService.ImportAsync reads an entire WAV file through WavCodec.Read before
applying its 15-second truncation limit. A long uncompressed file causes a full data-chunk
allocation and a second PCM allocation during conversion, despite only a short clip being needed.
Agent Prompt
## Issue description
WAV import loads and converts the complete file before enforcing the custom-sound duration limit.
## Fix Focus Areas
- src/OpenIPC.Viewer.App/Services/NotificationSoundService.cs[112-133]
- src/OpenIPC.Viewer.Core/Notifications/PcmClip.cs[80-118]
## Recommended Fix
Pass the duration limit into WAV reading and read or convert no more frames than that limit permits. Keep playback loading subject to the same bound.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

CooldownSeconds: ActionsCooldown?.Value ?? 60,
SignalEnabled: SignalEnabled,
SignalProtocol: SignalProtocol?.Value ?? EventActionSettings.ProtocolTcp,
SignalHost: SignalHost.Trim(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

4. Null action settings break notification saves 🐞 Bug ☼ Reliability

NotificationSettingsViewModel.BuildActions calls Trim() on action fields copied directly from
deserialized settings without checking for null. If usersettings.json contains JSON null for an
action string, a subsequent notification edit or test invokes SaveNowAsync and raises a
NullReferenceException.
Agent Prompt
## Issue description
JSON null action fields reach unconditional Trim calls when notification settings are saved.
## Fix Focus Areas
- src/OpenIPC.Viewer.App/ViewModels/NotificationSettingsViewModel.cs[276-303]
- src/OpenIPC.Viewer.App/ViewModels/NotificationSettingsViewModel.cs[406-430]
## Recommended Fix
Normalize nullable action strings to empty strings on load, and make construction of saved action settings null-safe.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +188 to +201
var args = EventActionTemplate.Render(s.ProgramArguments ?? "", context);
var ext = Path.GetExtension(path).ToLowerInvariant();

ProcessStartInfo psi;
if (ext == ".ps1")
{
// A script the user picked themselves; run it without the profile and
// without tripping over the machine's default execution policy.
psi = new ProcessStartInfo("powershell.exe",
$"-NoProfile -NonInteractive -ExecutionPolicy Bypass -File \"{path}\" {args}");
}
else if (ext is ".exe" or ".bat" or ".cmd" or ".com" || !OperatingSystem.IsWindows())
{
psi = new ProcessStartInfo(path, args);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

5. Camera or event text can inject script commands 🐞 Bug ⛨ Security

StartProgram renders ProgramArguments with EventActionTemplate.Render and no encoder, so
{camera} and {summary} go into the raw command-line string unchanged. The default template is
"{camera}" {kind} "{summary}", so a " in a camera name or detection summary ends the quoted
argument and adds extra arguments. For .bat/.cmd targets, cmd.exe parses that line, so &, |
or > in those values run extra commands.
Agent Prompt
## Issue description
Program action arguments are built by putting unescaped event values (camera name, summary) into a command-line string. Quotes split the arguments, and for .bat/.cmd files cmd.exe metacharacters run extra commands.

## Fix Focus Areas
- src/OpenIPC.Viewer.Infrastructure/Notifications/EventActionDispatcher.cs[182-207]
- src/OpenIPC.Viewer.Core/Notifications/EventActionSettings.cs[48-51]

## Recommended Fix
Pass an encoder to `EventActionTemplate.Render` for the program channel. It should strip or escape `"` and backslashes using Windows argv rules, and for .bat/.cmd it should also neutralise `& | < > ^ %`. A safer alternative is to tokenise the template first, render each token separately and add the results to `ProcessStartInfo.ArgumentList`. Scripts can also read the values from the OPENIPC_* environment variables that are already set.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +22 to +28
var options = (s.SmtpSecurity ?? "auto").Trim().ToLowerInvariant() switch
{
"ssl" => SecureSocketOptions.SslOnConnect,
"starttls" => SecureSocketOptions.StartTls,
"none" => SecureSocketOptions.None,
_ => port == 465 ? SecureSocketOptions.SslOnConnect : SecureSocketOptions.StartTlsWhenAvailable,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

6. Smtp password can be sent unencrypted 🐞 Bug ⛨ Security

SmtpMailSender.SendAsync maps the default SmtpSecurity value of "auto" to
StartTlsWhenAvailable on ports other than 465, then calls AuthenticateAsync without checking
whether TLS was negotiated. With the default port 587, a server that does not offer STARTTLS—or a
network attacker that strips it—can cause the login and password retrieved from credential storage
to travel in cleartext.
Agent Prompt
## Issue description
The default SMTP `auto` mode permits authentication with stored credentials over plaintext when STARTTLS is unavailable or stripped.

## Fix Focus Areas
- src/OpenIPC.Viewer.Infrastructure/Notifications/SmtpMailSender.cs[21-46]
- src/OpenIPC.Viewer.Core/Notifications/EventActionSettings.cs[44-46]

## Recommended Fix
Require TLS before authenticating in the default mode: map `auto` on ports other than 465 to `SecureSocketOptions.StartTls`. If opportunistic STARTTLS must remain, check `client.IsSecure` before `AuthenticateAsync` and throw a clear error when it is false, unless the security mode was explicitly set to `none`. Keep any explicitly selected insecure mode clearly distinct from the default.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +101 to +105
if (!isNew && state.Published && (grew || tick.At - state.LastPublishedTickAt >= ProgressInterval))
{
update = Snapshot(state, endedAt: null);
state.LastPublishedTickAt = tick.At;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

7. Open events rebuild the events page every second 🐞 Bug ➹ Performance

OnTick now publishes the open event again (same Id) at least once per second while ticks keep
arriving, and on every peak rise. EventsPageViewModel.OnLiveEvent answers each publication with a
full ApplyFilter on the UI thread, which regroups, refolds and repages up to 10,000 cached events
and clears/rebuilds PageItems. With several active cameras this means several full list rebuilds
per second while the Events page is open, which costs CPU and causes visible list churn.
Agent Prompt
## Issue description
The new once-a-second progress updates of open events trigger a full Events page re-filter and rebuild each time.

## Fix Focus Areas
- src/OpenIPC.Viewer.Core/Events/EventIngestionService.cs[86-114]
- src/OpenIPC.Viewer.App/ViewModels/EventsPageViewModel.cs[380-394]

## Recommended Fix
One option is to publish progress updates on a separate observable that only NotificationCoordinator subscribes to. The other is to make EventsPageViewModel ignore updates for an event it already has (same Id, EndedAt still null) unless a field it displays changed, or to coalesce rebuilds with a debounce.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@keyldev

keyldev commented Oct 17, 2026

Copy link
Copy Markdown
Collaborator

Resolve comments and we will run workflow to doublecheck

@keyldev keyldev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for this, it's a solid piece of work. The threshold and episode logic in NotificationCoordinator holds up: thresholds are checked on every publication, each channel fires at most once per episode, and Core still has no package dependencies. The P/Invoke and COM layouts (WinRT toasts, CREDENTIALW, MAPI) check out, and Windows-only code is properly guarded. Locally the PR builds with 0 warnings.

CI is red for an unrelated reason: a new advisory on System.Security.Cryptography.Pkcs 10.0.0 (GHSA-2fmv-rv2c-59x7, published 2026-10-13) fails restore on main as well. That will be fixed on main; rebase once it lands.

A few things need changing before merge.

Must fix

  1. Command injection in the "run program" action (EventActionDispatcher.cs, StartProgram).

    • ProgramArguments is rendered and then passed as one raw command-line string. .bat/.cmd go through cmd /c, so & | > ^ % in the values are interpreted by the shell.
    • The default template quotes "{camera}", but the camera name isn't always typed by the user. Auto-detect fills it from the name the device reports (CameraEditorViewModel). A device on the LAN that calls itself x" & calc & " runs calc on every event.
    • The .ps1 and ShellExecute branches have the same quoting break.
    • Fix: split the template into tokens before rendering, render each token separately, and pass them through psi.ArgumentList. For .bat/.cmd, also reject or strip "%&|<>^ and line breaks in substituted values, or pass event data only through the OPENIPC_* environment variables. Those variables are currently not set on the ShellExecute branch, although the comment says scripts can rely on them.
  2. Event data can inject bytes into TCP/UDP payloads (SendSignalAsync).

    • The template is rendered first and the escapes are decoded after, so a camera named Gate\r\nDISARM or containing \x00 puts raw bytes into an alarm panel or PLC stream.
    • In hex: mode, a placeholder value that isn't hex throws.
    • Fix: decode escapes in the template before substituting values (or escape \ in the values), and reject placeholders in hex mode.
  3. SMTP "auto" can send the password in plain text (SmtpMailSender.cs).

    • On any port other than 465, "auto" maps to StartTlsWhenAvailable. Someone in the network path can strip the STARTTLS offer, and AUTH then goes out unencrypted.
    • Fix: when a login is configured, "auto" should use StartTls, which fails if STARTTLS is missing.

Should fix

  1. The Events page does a full refilter every second per active camera. EventIngestionService now publishes progress about once a second and on every peak rise. EventsPageViewModel.OnLiveEvent runs ApplyFilter over up to 10,000 events on the UI thread for each update. With several AI cameras active, that is several full rebuilds per second. Mark progress publications so the Events page can skip them, or throttle OnLiveEvent.
  2. An open event can arrive after the close. OpenAsync publishes only after await _repo.AddAsync, while the quiet-close timer runs independently. If the insert takes more than 5 s, the coordinator sees the close first and removes the episode. The late open event then creates a new episode that is never removed, and it notifies or acts a second time. Mark the state as closed and skip the late publish, or keep a short set of recently closed IDs in the coordinator.
  3. An unsaved SMTP password is silently lost. LoadAsync runs each time Settings is shown and overwrites SmtpUser/SmtpPassword from the store. FlushAsync doesn't save dirty credentials. A user who types a password and navigates away without pressing Save/Test gets empty fields back, and every e-mail action then fails authentication. Save the credentials in FlushAsync when they're dirty, or don't overwrite dirty fields on load.
  4. "Test notification" can crash the app. TestNotificationAsync awaits SaveNowAsync() without a try/catch. An IOException (antivirus or OneDrive locking usersettings.json.tmp, a full disk) escapes the AsyncRelayCommand and takes the process down. The other Test commands catch their errors.
  5. Importing a big WAV freezes the UI. WavCodec.Read runs synchronously on the UI thread and reads the entire data chunk before trimming to 15 s. A 1-hour WAV allocates hundreds of MB, and a file over 2 GB overflows the (int) cast into an ArgumentOutOfRangeException, which doesn't fall back to FFmpeg. Cap the read at MaxCustomLength and run it in Task.Run. Also reject absurd sample rates: a rate of 2^30 overflows BytesPerSecond to 0 and leaves the player loop spinning.
  6. MAPI and Outlook sends are unbounded. Each send gets a new STA thread that can't be cancelled; the 20 s timeout only stops waiting for it. A compose window or an Outlook security prompt keeps that thread alive, and a burst of events stacks up threads and dialogs. Allow one send at a time.
  7. Phone widths. The Test-button rows put a status TextBlock MaxWidth="420" inside a horizontal StackPanel, so long errors are clipped at about 360 dp. Fixed-width sliders also crowd the labels. Use a Grid (Auto,*) or put the status on its own line, and use b:Adaptive.CompactBelow like the other sections.

Nits

  • Cooldowns compare local wall-clock time. On the DST fall-back night now - at goes negative and blocks both channels for about an hour. Use UTC for cooldowns and local time only for quiet hours.
  • Actions are gated by the toast toggles (NotifyOnMotion/NotifyOnDetection) but ignore the NotificationsEnabled master switch. Turning off motion toasts silently turns off motion actions too. Either give actions their own filter or document this.
  • WindowsToastNotifier.Show after Dispose throws InvalidOperationException. RunAndWait catches this; Show doesn't.
  • The "Not an http(s) URL" warning logs the full webhook URL, which often carries a token. Log only the scheme and host.
  • The .ps1 branch always runs powershell.exe. Use pwsh off Windows, or take this branch only on Windows.
  • NotificationSoundPlayer never disposes superseded CancellationTokenSources, and ScheduleSave has the same leak.
  • The program-path hint and the README describe MAPI, Outlook and Credential Manager without saying they're Windows-only. Off Windows, "system secure storage" is actually the app's secrets.bin.
  • SettingsPageViewModel.Persist still uses the snapshot UpdateAsync(...), which can overwrite a notification save that is still in flight. Switch it to the functional overload.

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