Skip to content

feat(ui/ux): UI rework & upgrade - #75

Merged
keyldev merged 15 commits into
mainfrom
feat/grid-toolbar-ux
Sep 27, 2026
Merged

keyldev merged 15 commits into
mainfrom
feat/grid-toolbar-ux

Conversation

@keyldev

@keyldev keyldev commented Sep 27, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

UI redesign for couple of screens.

Related

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

- Live | Stills segmented switch replaces the camera toggle
- Grid size is one dropdown on the tabs row (per-layout), showing the current shape
- Layout actions move into the active tab's menu (… / right-click); + Layout closes the tab row
- Center wide Lucide glyphs (camera, volume) on tile and camera-page buttons
- Re-importing a backup (or importing onto a fresh install) no longer duplicates tabs
- Existing same-named layout gets the imported grid size and tiles
- Test: importing twice keeps one layout per name
- Bare TcpClient() is a dual-mode IPv6 socket; with IPv6 off or dropped by a VPN it fails on IPv4 cameras (WSAEADDRNOTAVAIL)
- Every camera read offline in the library, grid and Health Center while streams played fine
- Resolve the host and try each address with a matching IPv4/IPv6 socket
- Search by name/IP/group, status chips with live counts, 20 cameras per page
- One row per camera: preview slot + status dot, host, layouts, web/SSH/more
- Row menu (… / right-click) holds edit, device, files, delete and a Layouts submenu that replaces the "in grid" checkbox
- Header regrouped: Find on network, QR, Add camera; groups and refresh as icons
…calendar

- Camera / period / motion-only filters, day headers with totals, 20 rows per page
- Dense rows: play, camera, time range, duration, size, motion / in-progress badges, menu (show in folder, copy file, delete)
- Calendar fills recording days by count, dots event-only days, follows the camera/motion filter, localized weekdays
- Empty month offers a jump to the nearest month with recordings
- Camera / type / period filters (default 7 days), day headers, 20 rows per page
- Bursts of one camera and kind within a minute fold into one episode with merged class counts
- Rows show what was detected (localized COCO classes), play the covering recording from the event moment, open the camera
- Simulate motion moves into the header overflow; chip style no longer picks up Fluent's checked fill
- Camera dropdown becomes a picker: search by name/IP/group, tick several cameras, per-camera counts with a bar
- Defaults to cameras that have events in the period/type; "All cameras" lists every one
- Selected cameras show as removable chips; clicking a camera name in a row filters to it
- CameraPickerViewModel + Controls/CameraPickerView carry the search, counts, multi-select and chips
- Events moves onto it unchanged; Recordings swaps its camera dropdown for it (counts = recordings per camera)
- Calendar highlight follows the ticked cameras
- Status badge + stat tiles; when the engine isn't running, explain how it starts
- Per-camera detection list: on/off switch, class chips, fps/threshold, REC, 24 h count, Configure (camera editor via shared CameraEditService); enable on more cameras from a searchable list
- Last 24 h class summary with a link to Events filtered to AI detections
- Detected classes render as icon chips (person, car, truck/bus, bike) here and in Events rows
- Header shows camera + date/time/length/size; previous/next recording and a ⋯ menu (folder, copy, delete)
- Transport: ±10 s, wall-clock time, 0.5×–8× speed (IPlaybackSession.Rate), Space/←/→/Home/End hotkeys
- Clip mode replaces the export bar; save the current frame into snapshots
- Event sidebar hidden when empty, localized filter, class-icon rows; auto-continue into an adjacent recording
- IPlaybackSession.StepFrame: forward decodes one frame, backward re-seeks to the previous one
- Seek target now survives across packets, so seeks land on the exact frame instead of the keyframe
- Shared FfmpegAudioDecoder (live + file); file audio plays at 1x only; MediaInfo.HasAudio
- Frame step buttons and , / . keys; PgUp/PgDn for previous/next recording
- Mute + volume shared with the live page (M key), shown when the file has audio
- Fullscreen via button, F or F11; Esc exits; survives previous/next and auto-continue
- Centre the header ellipsis; event sidebar now really hides below the compact breakpoint
- Address + login first; Connect detects OpenIPC/ONVIF, fills streams, ports and name, and shows a live frame
- Failure card says what went wrong (unreachable, credentials, no video)
- Advanced settings in collapsible rows with one-line summaries; per-field validation
- Pasted rtsp:// links are split into fields; new group inline; dark header replaces the title bar
- Quick ONVIF/mDNS scan starts on open; rows show kind, model, address and their own Add
- Cameras already in the library listed separately; empty state points to deep scan or manual entry
- Deep scan in a collapsible footer: subnets, extra range, start/stop, live progress
- Add hands the device to the camera editor, which connects straight away with the session login
@keyldev
keyldev merged commit 23c05c0 into main Sep 27, 2026
5 checks passed
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Redesign camera workflows, events, recordings, and playback

✨ Enhancement 🐞 Bug fix 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Redesign live, library, discovery, analytics, events, and archive screens for faster navigation
 and filtering.
• Add camera connection diagnostics and richer recording playback, including audio, frame capture,
 and clip controls.
• Fix IPv4 camera status checks and duplicate layouts on backup import.
Diagram

graph TD
  Views["Avalonia views"] --> Models["Page view models"] --> Services["App services"] --> Contracts["Core contracts"] --> Video["FFmpeg sessions"]
  Services --> Network["Network probes"]
  Models --> Storage["Repositories"]
  Contracts --> Storage
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Separate UI redesign from media and networking changes
  • ➕ Smaller reviews and clearer regression boundaries.
  • ➕ Playback and connection behavior could receive dedicated tests before integration.
  • ➖ Requires coordinating dependent view-model and service changes across PRs.
  • ➖ Temporarily leaves redesigned controls without their intended behavior.
2. Extract shared filtering and pagination logic
  • ➕ Reduces repeated pager and filter-state code across library, events, and recordings.
  • ➕ Creates a focused unit-test surface for list behavior.
  • ➖ Adds an abstraction across lists with different grouping, counts, and calendar rules.
  • ➖ Could complicate this UI-focused delivery.

Recommendation: Keep the shared audio decoder and camera connection service: both provide useful boundaries around behavior reused by multiple screens. For this PR, prioritize cross-platform UI checks and targeted tests for camera connection, filtering, and playback in addition to the new backup test.

Files changed (59) +7411 / -2263

Enhancement (51) +7274 / -2070
CameraPickerView.axamlAdd shared camera filter control +110/-0

Add shared camera filter control

• Adds a searchable, count-aware multi-select picker with removable camera chips for events and recordings.

src/OpenIPC.Viewer.App/Controls/CameraPickerView.axaml

CameraPickerView.axaml.csInitialize the shared camera picker +11/-0

Initialize the shared camera picker

• Adds the Avalonia code-behind for the new picker control.

src/OpenIPC.Viewer.App/Controls/CameraPickerView.axaml.cs

LayoutTabConverters.csSupport active tabs, grid icons, and dynamic resources +73/-4

Support active tabs, grid icons, and dynamic resources

• Adds converters for active-layout actions, selected grid-size appearance, and icon resource lookup.

src/OpenIPC.Viewer.App/Converters/LayoutTabConverters.cs

OpenCameraMessage.csExtend navigation messages for playback and analytics +14/-3

Extend navigation messages for playback and analytics

• Carries playback start position and speed, archive reload requests, player fullscreen state, and detection-event navigation.

src/OpenIPC.Viewer.App/Messages/OpenCameraMessage.cs

CameraConnectService.csProbe and verify camera connections +295/-0

Probe and verify camera connections

• Identifies Majestic and ONVIF devices, chooses RTSP streams, verifies a decoded frame, and returns diagnostics and a preview.

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

CameraEditorFactory.csInject camera connection orchestration into editors +7/-8

Inject camera connection orchestration into editors

• Constructs editor view models with the new connection service and dialog dependency.

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

CameraEditorResultExtensions.csPersist metadata discovered during camera setup +20/-0

Persist metadata discovered during camera setup

• Saves ONVIF and Majestic findings from the editor result to the camera directory.

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

DetectionClasses.csFormat detection classes for UI summaries +54/-0

Format detection classes for UI summaries

• Adds localized detection labels, icon selection, and parsing of class counts from event summaries.

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

Localizer.csTranslate redesigned camera and archive workflows +425/-137

Translate redesigned camera and archive workflows

• Adds and revises English and Russian text for filters, discovery, camera connection, analytics, events, recordings, and playback.

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

RecordingPlayerPageFactory.csWire new player dependencies +14/-1

Wire new player dependencies

• Supplies recording and snapshot repositories, audio monitoring, and user settings to the expanded player view model.

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

Buttons.axamlAdd reusable segmented and dense-button styles +130/-0

Add reusable segmented and dense-button styles

• Introduces segmented controls, filter chips, row icons, and layout-tab icon styles used across redesigned screens.

src/OpenIPC.Viewer.App/Styles/Buttons.axaml

Theme.axamlAdd icons for redesigned workflows +45/-0

Add icons for redesigned workflows

• Adds grid-size, navigation, device, transport, and detection-class glyph resources.

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

AnalyticsPageViewModel.csExpand analytics into a detection control center +187/-43

Expand analytics into a detection control center

• Exposes engine diagnostics, per-camera detection configuration and activity, and a 24-hour class summary with links to events.

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

ArchiveCalendarViewModel.csAlign calendar activity with recording filters +93/-20

Align calendar activity with recording filters

• Highlights selectable recording days, distinguishes event-only days, summarizes each month, and offers a nearest-recording-month jump.

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

CameraLibraryPageViewModel.csFilter and page cameras with layout membership +301/-71

Filter and page cameras with layout membership

• Adds search, live status counts, 20-row pages, and per-layout membership actions without re-probing on each filter change.

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

CameraPickerViewModel.csModel shared multi-camera selection +200/-0

Model shared multi-camera selection

• Maintains searchable camera choices, per-camera counts, selected chips, and selection notifications for events and recordings.

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

CameraEditorViewModel.csRework camera editing around Connect +531/-232

Rework camera editing around Connect

• Parses pasted addresses, validates fields, probes streams and device capabilities, and presents connection results alongside collapsible advanced settings.

src/OpenIPC.Viewer.App/ViewModels/Dialogs/CameraEditorViewModel.cs

DiscoveryDialogViewModel.csSeparate quick discovery from optional deep scans +227/-235

Separate quick discovery from optional deep scans

• Runs passive discovery first, supports cancellable subnet sweeps, separates new and known cameras, and hands selected devices to the editor.

src/OpenIPC.Viewer.App/ViewModels/Dialogs/DiscoveryDialogViewModel.cs

EventsPageViewModel.csAdd event filtering, burst episodes, and playback links +374/-66

Add event filtering, burst episodes, and playback links

• Adds camera, kind, and period filters; folds nearby events into episodes; pages results and opens matching recordings near an event.

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

GridPageViewModel.csExpose live mode and current grid size +14/-1

Expose live mode and current grid size

• Adds bindings for the Live/Stills segmented switch and the active layout's grid-size label.

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

MainWindowViewModel.csRoute detection events and player fullscreen +50/-6

Route detection events and player fullscreen

• Handles analytics-to-events navigation, recording start offsets and speed, archive reloads, and fullscreen state across neighboring recordings.

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

RecordingPlayerPageViewModel.csExpand recorded-video playback controls +528/-86

Expand recorded-video playback controls

• Adds audio, speed, frame stepping and capture, neighboring recordings, continuous playback, fullscreen, file actions, and revised clip controls.

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

RecordingsPageViewModel.csFilter and page the recordings archive +259/-27

Filter and page the recordings archive

• Adds multi-camera, motion, period, and calendar filters, day-grouped pages, summaries, and file actions.

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

CameraEditorContent.axamlRedesign the add/edit camera form +465/-180

Redesign the add/edit camera form

• Presents address and credentials first, a connection result or failure card, and collapsible advanced camera settings.

src/OpenIPC.Viewer.App/Views/Dialogs/CameraEditorContent.axaml

CameraEditorContent.axaml.csHandle editor redirects and address entry +58/-3

Handle editor redirects and address entry

• Adds discovery and QR entry points, address normalization after paste or focus loss, auto-connect, and pending-probe cancellation.

src/OpenIPC.Viewer.App/Views/Dialogs/CameraEditorContent.axaml.cs

CameraEditorWindow.axamlUse a custom editor window header +4/-1

Use a custom editor window header

• Changes the editor window to a border-only, content-sized host for its redesigned header.

src/OpenIPC.Viewer.App/Views/Dialogs/CameraEditorWindow.axaml

CameraEditorWindow.axaml.csEnable dragging from the editor header +8/-1

Enable dragging from the editor header

• Starts native window movement when the custom header is pressed.

src/OpenIPC.Viewer.App/Views/Dialogs/CameraEditorWindow.axaml.cs

DiscoveryDialogContent.axamlRedesign discovery results and scan controls +285/-173

Redesign discovery results and scan controls

• Presents immediate quick-scan results, separate known cameras, per-device Add actions, and an explicit deep-scan section.

src/OpenIPC.Viewer.App/Views/Dialogs/DiscoveryDialogContent.axaml

DiscoveryDialogContent.axaml.csStart scans and hand selected devices to editing +31/-8

Start scans and hand selected devices to editing

• Begins passive scanning on display, handles per-row Add and manual entry, and cancels discovery when closing.

src/OpenIPC.Viewer.App/Views/Dialogs/DiscoveryDialogContent.axaml.cs

DiscoveryDialogWindow.axamlUse a custom discovery window header +4/-1

Use a custom discovery window header

• Hosts the redesigned discovery dialog in a border-only window with its own header.

src/OpenIPC.Viewer.App/Views/Dialogs/DiscoveryDialogWindow.axaml

DiscoveryDialogWindow.axaml.csEnable dragging from the discovery header +8/-0

Enable dragging from the discovery header

• Connects pointer presses on the custom header to native window dragging.

src/OpenIPC.Viewer.App/Views/Dialogs/DiscoveryDialogWindow.axaml.cs

AnalyticsPage.axamlPresent engine metrics and camera detection settings +288/-75

Present engine metrics and camera detection settings

• Replaces the simple analytics list with diagnostic tiles, enabled-camera controls, class chips, and recent-activity navigation.

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

ArchiveCalendarView.axamlDistinguish recording days from event-only days +39/-9

Distinguish recording days from event-only days

• Adds localized weekdays, recording-intensity styling, event-only dots, month totals, and a nearest-month shortcut.

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

CameraLibraryPage.axamlReplace camera cards with a searchable dense list +331/-188

Replace camera cards with a searchable dense list

• Adds responsive camera rows, search and status chips, pagination, quick actions, and per-camera context menus.

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

CameraLibraryPage.axaml.csDrive camera row menus and page scrolling +95/-1

Drive camera row menus and page scrolling

• Builds per-layout menu choices, dispatches row actions, refreshes layout membership, and scrolls to the top on page changes.

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

EventsPage.axamlShow filterable, grouped event episodes +280/-73

Show filterable, grouped event episodes

• Adds camera and time filters, event-kind chips, day-grouped rows, paging, and recording playback actions.

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

EventsPage.axaml.csReset event-list scroll position on paging +17/-0

Reset event-list scroll position on paging

• Tracks view-model changes and returns the event list to its top when the selected page changes.

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

GridPage.axamlRegroup the live grid toolbar +197/-117

Regroup the live grid toolbar

• Adds a Live/Stills segment, moves grid size beside layout tabs, and places layout actions in tab context menus.

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

GridPage.axaml.csHandle active-tab context actions +47/-0

Handle active-tab context actions

• Opens layout menus from tabs or right-click, selects the targeted layout, and runs its camera, rename, or delete action.

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

RecordingPlayerPage.axamlRedesign recorded-video transport and timeline +442/-180

Redesign recorded-video transport and timeline

• Adds fullscreen playback controls, audio and speed controls, frame and file actions, neighboring-recording navigation, and a revised event sidebar.

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

RecordingPlayerPage.axaml.csAdd playback keyboard shortcuts and VM activation +73/-0

Add playback keyboard shortcuts and VM activation

• Handles transport, seek, frame-step, audio, navigation, and fullscreen hotkeys; activates a replacement player view model.

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

RecordingsPage.axamlRedesign the recordings list and filters +243/-101

Redesign the recordings list and filters

• Adds a shared camera picker, period and motion filters, grouped recording rows, paging, and row menus beside the calendar.

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

RecordingsPage.axaml.csHandle recording rows, menus, and paging +72/-0

Handle recording rows, menus, and paging

• Plays tapped rows, routes file actions from context menus, and resets list scroll position on page changes.

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

ISnapshotService.csAllow saving an encoded frame as a snapshot +7/-0

Allow saving an encoded frame as a snapshot

• Adds a snapshot-service contract accepting JPEG bytes, camera identity, and recording-frame time.

src/OpenIPC.Viewer.Core/Snapshots/ISnapshotService.cs

SnapshotService.csPersist frames captured from recordings +7/-5

Persist frames captured from recordings

• Routes recorded-frame JPEGs through existing snapshot storage and metadata creation using their playback timestamp.

src/OpenIPC.Viewer.Core/Snapshots/SnapshotService.cs

SnapshotSource.csIdentify recording-derived snapshots +3/-0

Identify recording-derived snapshots

• Adds a Recording source value for frames saved from archive playback.

src/OpenIPC.Viewer.Core/Snapshots/SnapshotSource.cs

IMediaProbe.csReport whether a file contains audio +2/-1

Report whether a file contains audio

• Extends probed media information with an optional audio-presence flag.

src/OpenIPC.Viewer.Core/Video/IMediaProbe.cs

IPlaybackSession.csExpose playback speed and frame stepping +9/-0

Expose playback speed and frame stepping

• Adds rate control and single-frame stepping to the playback-session contract.

src/OpenIPC.Viewer.Core/Video/IPlaybackSession.cs

FfmpegAudioDecoder.csShare FFmpeg audio decoding across sessions +203/-0

Share FFmpeg audio decoding across sessions

• Extracts audio stream decoding and stereo PCM resampling for use by live and recorded playback.

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

FfmpegMediaProbe.csDetect playable audio in recording files +9/-1

Detect playable audio in recording files

• Checks media streams for an audio codec supported by the current FFmpeg build.

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

FfmpegPlaybackSession.csAdd audio, speed, and frame stepping to file playback +85/-12

Add audio, speed, and frame stepping to file playback

• Emits decoded audio at normal speed, supports rate-adjusted presentation and frame stepping, and flushes audio after seeks.

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

Bug fix (3) +44 / -9
SingleCameraPage.axamlCenter camera-page volume glyphs +2/-2

Center camera-page volume glyphs

• Adjusts mute and volume icon alignment in the single-camera controls.

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

TcpReachabilityProbe.csMatch TCP sockets to resolved address families +22/-4

Match TCP sockets to resolved address families

• Tries resolved IPv4 and IPv6 addresses with matching socket families so IPv4 cameras remain reachable when IPv6 is unavailable.

src/OpenIPC.Viewer.Infrastructure/Net/TcpReachabilityProbe.cs

SqliteConfigBackupService.csMerge imported layouts by name +20/-3

Merge imported layouts by name

• Reuses same-named layouts during import and applies imported grid size and tiles instead of creating duplicate tabs.

src/OpenIPC.Viewer.Infrastructure/Persistence/SqliteConfigBackupService.cs

Refactor (3) +68 / -184
CameraEditService.csShare existing-camera edit workflow +53/-0

Share existing-camera edit workflow

• Centralizes opening, saving, and persisting detected metadata for edits initiated from the library or analytics page.

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

DiscoveryDialogFactory.csMove connection probing out of discovery +5/-5

Move connection probing out of discovery

• Stops injecting ONVIF connection probing into discovery and exposes cached scan-session state for the add-camera flow.

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

FfmpegVideoSession.csReuse the shared decoder for live audio +10/-179

Reuse the shared decoder for live audio

• Replaces inline live-stream audio setup and resampling with the extracted FFmpeg decoder.

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

Tests (1) +23 / -0
SqliteConfigBackupServiceTests.csTest idempotent layout import +23/-0

Test idempotent layout import

• Verifies importing a backup twice merges Default and named layouts while retaining tiles and grid size.

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

Other (1) +2 / -0
SharedComposition.csRegister camera connection and editing services +2/-0

Register camera connection and editing services

• Adds the new application services to shared dependency injection.

src/OpenIPC.Viewer.Composition/SharedComposition.cs

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

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Action required

1. Imports leave stale cameras in layouts 🐞 Bug ≡ Correctness
Description
ImportAsync reuses a same-named layout but calls SetTilesAsync only when the filtered imported
tileIds collection is non-empty. When the imported layout has no tiles or none of its camera IDs
exist locally, the call that would delete the existing LayoutTiles rows is skipped, leaving the
layout's previous tiles in place.
Code

src/OpenIPC.Viewer.Infrastructure/Persistence/SqliteConfigBackupService.cs[R173-174]

            if (tileIds.Count > 0)
-                await _layouts.SetTilesAsync(newId, tileIds, ct).ConfigureAwait(false);
+                await _layouts.SetTilesAsync(layoutId, tileIds, ct).ConfigureAwait(false);
Evidence
The name-based merge reuses the existing layout ID, while the zero-count guard bypasses
SetTilesAsync when no imported camera IDs resolve. Because SetTilesAsync deletes existing
LayoutTiles before inserting the supplied IDs, skipping it leaves the prior rows untouched rather
than replacing them with empty membership.

src/OpenIPC.Viewer.Infrastructure/Persistence/SqliteConfigBackupService.cs[168-174]
src/OpenIPC.Viewer.Infrastructure/Persistence/SqliteLayoutRepository.cs[101-117]
src/OpenIPC.Viewer.Infrastructure/Persistence/SqliteConfigBackupService.cs[157-174]
src/OpenIPC.Viewer.Infrastructure/Persistence/SqliteLayoutRepository.cs[104-108]

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

## Issue description
`ImportAsync` skips replacing tile membership when an imported layout has no locally valid camera IDs, preserving stale tiles on a same-named layout.

## Fix Focus Areas
- src/OpenIPC.Viewer.Infrastructure/Persistence/SqliteConfigBackupService.cs[156-174]
- tests/OpenIPC.Viewer.Infrastructure.Tests/SqliteConfigBackupServiceTests.cs[1-100]

## Recommended Fix
Call `SetTilesAsync(layoutId, tileIds, ct)` unconditionally after filtering imported tile IDs, including when the list is empty; it already clears existing membership before inserting replacement rows. Add a test that imports an empty layout over a same-named layout with existing tiles.

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



Remediation recommended

2. Busy sites show understated detections 🐞 Bug ≡ Correctness
Description
ReloadAsync calculates the 24-hour camera and class totals from a detection query capped at 10,000
rows. Once a site records more than 10,000 detections in that window, older qualifying events are
excluded from both displayed summaries without any indication of truncation.
Code

src/OpenIPC.Viewer.App/ViewModels/AnalyticsPageViewModel.cs[R114-115]

+            var detections = await _events.ListAsync(null, EventKind.Detection, since, 10000, ct).ConfigureAwait(true);
+            var perCamera = detections.GroupBy(e => e.CameraId).ToDictionary(g => g.Key, g => g.Count());
Evidence
The repository applies the requested limit after sorting events newest-first. Both the per-camera
grouping and class summary use only that limited result.

src/OpenIPC.Viewer.App/ViewModels/AnalyticsPageViewModel.cs[113-133]
src/OpenIPC.Viewer.Infrastructure/Persistence/SqliteEventRepository.cs[24-44]

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 24-hour detection summaries silently undercount when more than 10,000 events exist.
## Fix Focus Areas
- src/OpenIPC.Viewer.App/ViewModels/AnalyticsPageViewModel.cs[113-133]
- src/OpenIPC.Viewer.Infrastructure/Persistence/SqliteEventRepository.cs[24-44]
## Recommended Fix
Aggregate camera and class counts over the entire time window, using a repository aggregation or paginated reads rather than a fixed-limit event list.

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


3. Some recordings offer sound but play silent 🐞 Bug ≡ Correctness
Description
FfmpegMediaProbe sets HasAudio when any audio stream has a decoder, while
FfmpegAudioDecoder.TryOpen selects only the first audio stream. For a recording whose first audio
stream is unsupported but a later one is supported, the player enables listening even though its
playback session produces no audio frames.
Code

src/OpenIPC.Viewer.Video/Pipeline/FfmpegMediaProbe.cs[R48-51]

+                var ap = fmtCtx->streams[i]->codecpar;
+                // Only audio this build can actually decode counts.
+                if (ap->codec_type == AVMediaType.AVMEDIA_TYPE_AUDIO && ffmpeg.avcodec_find_decoder(ap->codec_id) != null)
+                    hasAudio = true;
Evidence
The probe checks every audio stream, whereas decoder setup stops at the first and returns null if
its codec lacks a decoder. The playback session opens that decoder once, and the recording page uses
the probe result to enable its audio controls.

src/OpenIPC.Viewer.Video/Pipeline/FfmpegMediaProbe.cs[45-51]
src/OpenIPC.Viewer.Video/Pipeline/FfmpegAudioDecoder.cs[44-62]
src/OpenIPC.Viewer.Video/Pipeline/FfmpegPlaybackSession.cs[283-286]
src/OpenIPC.Viewer.App/ViewModels/RecordingPlayerPageViewModel.cs[288-329]

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

## Issue description
Audio probing can advertise a supported stream that playback never selects.
## Fix Focus Areas
- src/OpenIPC.Viewer.Video/Pipeline/FfmpegMediaProbe.cs[45-51]
- src/OpenIPC.Viewer.Video/Pipeline/FfmpegAudioDecoder.cs[44-62]
## Recommended Fix
Make probing and decoder setup select audio streams consistently, preferably by trying subsequent streams when the first cannot be decoded.

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


4. Event filters can show an older period 🐞 Bug ≡ Correctness
Description
OnPeriodChanged starts ReloadAsync fire-and-forget without cancelling or versioning an earlier
reload, while ReloadAsync writes its fetched events into shared _events after multiple awaited
repository calls. When a user selects periods in quick succession, an older request can finish last
and replace the list with its old time range even though Period now holds the newer selection.
Code

src/OpenIPC.Viewer.App/ViewModels/EventsPageViewModel.cs[R105-108]

+    // Period changes what is loaded; cameras, kind and grouping only re-slice.
+    partial void OnPeriodChanged(EventPeriod value) => _ = ReloadAsync(CancellationToken.None);
+    partial void OnKindFilterChanged(EventKindFilter value) => ApplyFilter(resetPage: true);
+    partial void OnGroupBurstsChanged(bool value) => ApplyFilter(resetPage: true);
Evidence
The newly added period callback launches requests without awaiting, cancelling, or identifying them.
Each reload computes its period query before awaiting, then later unconditionally clears and
repopulates shared state, so completion order rather than the selected period decides the displayed
data.

src/OpenIPC.Viewer.App/ViewModels/EventsPageViewModel.cs[105-108]
src/OpenIPC.Viewer.App/ViewModels/EventsPageViewModel.cs[211-243]

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

Issue description
Period changes can overlap event reloads, allowing a stale request to overwrite the results for the user's current period.

Fix Focus Areas
- src/OpenIPC.Viewer.App/ViewModels/EventsPageViewModel.cs[105-108]
- src/OpenIPC.Viewer.App/ViewModels/EventsPageViewModel.cs[211-243]

Recommended Fix
Maintain a reload cancellation token source or monotonically increasing reload generation. Capture it before each reload and, after awaited repository calls, only update `_events`, `_recordings`, loading state, and filtered rows if that request remains current; cancel the preceding request when the period changes.

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


Grey Divider

Context sources
Review mode: 🧠 Deep: This is a highly dense, cross-cutting change spanning 59 files and 199 hunks across UI, view-model, camera connectivity, playback/video pipelines, persistence, and core interfaces, creating many independent paths where a second review pass could catch subtle defects.

Grey Divider

Tip of the day
💡 Did you know, you can describe a rule in plain language on the Rules page and Qodo drafts it for you

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +114 to +115
var detections = await _events.ListAsync(null, EventKind.Detection, since, 10000, ct).ConfigureAwait(true);
var perCamera = detections.GroupBy(e => e.CameraId).ToDictionary(g => g.Key, g => g.Count());

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. Busy sites show understated detections 🐞 Bug ≡ Correctness

ReloadAsync calculates the 24-hour camera and class totals from a detection query capped at 10,000
rows. Once a site records more than 10,000 detections in that window, older qualifying events are
excluded from both displayed summaries without any indication of truncation.
Agent Prompt
## Issue description
The 24-hour detection summaries silently undercount when more than 10,000 events exist.
## Fix Focus Areas
- src/OpenIPC.Viewer.App/ViewModels/AnalyticsPageViewModel.cs[113-133]
- src/OpenIPC.Viewer.Infrastructure/Persistence/SqliteEventRepository.cs[24-44]
## Recommended Fix
Aggregate camera and class counts over the entire time window, using a repository aggregation or paginated reads rather than a fixed-limit event list.

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

Comment on lines +48 to +51
var ap = fmtCtx->streams[i]->codecpar;
// Only audio this build can actually decode counts.
if (ap->codec_type == AVMediaType.AVMEDIA_TYPE_AUDIO && ffmpeg.avcodec_find_decoder(ap->codec_id) != null)
hasAudio = true;

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. Some recordings offer sound but play silent 🐞 Bug ≡ Correctness

FfmpegMediaProbe sets HasAudio when any audio stream has a decoder, while
FfmpegAudioDecoder.TryOpen selects only the first audio stream. For a recording whose first audio
stream is unsupported but a later one is supported, the player enables listening even though its
playback session produces no audio frames.
Agent Prompt
## Issue description
Audio probing can advertise a supported stream that playback never selects.
## Fix Focus Areas
- src/OpenIPC.Viewer.Video/Pipeline/FfmpegMediaProbe.cs[45-51]
- src/OpenIPC.Viewer.Video/Pipeline/FfmpegAudioDecoder.cs[44-62]
## Recommended Fix
Make probing and decoder setup select audio streams consistently, preferably by trying subsequent streams when the first cannot be decoded.

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

Comment on lines 173 to +174
if (tileIds.Count > 0)
await _layouts.SetTilesAsync(newId, tileIds, ct).ConfigureAwait(false);
await _layouts.SetTilesAsync(layoutId, tileIds, ct).ConfigureAwait(false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Imports leave stale cameras in layouts 🐞 Bug ≡ Correctness

ImportAsync reuses a same-named layout but calls SetTilesAsync only when the filtered imported
tileIds collection is non-empty. When the imported layout has no tiles or none of its camera IDs
exist locally, the call that would delete the existing LayoutTiles rows is skipped, leaving the
layout's previous tiles in place.
Agent Prompt
## Issue description
`ImportAsync` skips replacing tile membership when an imported layout has no locally valid camera IDs, preserving stale tiles on a same-named layout.

## Fix Focus Areas
- src/OpenIPC.Viewer.Infrastructure/Persistence/SqliteConfigBackupService.cs[156-174]
- tests/OpenIPC.Viewer.Infrastructure.Tests/SqliteConfigBackupServiceTests.cs[1-100]

## Recommended Fix
Call `SetTilesAsync(layoutId, tileIds, ct)` unconditionally after filtering imported tile IDs, including when the list is empty; it already clears existing membership before inserting replacement rows. Add a test that imports an empty layout over a same-named layout with existing tiles.

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

Comment on lines +105 to +108
// Period changes what is loaded; cameras, kind and grouping only re-slice.
partial void OnPeriodChanged(EventPeriod value) => _ = ReloadAsync(CancellationToken.None);
partial void OnKindFilterChanged(EventKindFilter value) => ApplyFilter(resetPage: true);
partial void OnGroupBurstsChanged(bool value) => ApplyFilter(resetPage: true);

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. Event filters can show an older period 🐞 Bug ≡ Correctness

OnPeriodChanged starts ReloadAsync fire-and-forget without cancelling or versioning an earlier
reload, while ReloadAsync writes its fetched events into shared _events after multiple awaited
repository calls. When a user selects periods in quick succession, an older request can finish last
and replace the list with its old time range even though Period now holds the newer selection.
Agent Prompt
Issue description
Period changes can overlap event reloads, allowing a stale request to overwrite the results for the user's current period.

Fix Focus Areas
- src/OpenIPC.Viewer.App/ViewModels/EventsPageViewModel.cs[105-108]
- src/OpenIPC.Viewer.App/ViewModels/EventsPageViewModel.cs[211-243]

Recommended Fix
Maintain a reload cancellation token source or monotonically increasing reload generation. Capture it before each reload and, after awaited repository calls, only update `_events`, `_recordings`, loading state, and filtered rows if that request remains current; cancel the preceding request when the period changes.

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

@keyldev
keyldev deleted the feat/grid-toolbar-ux branch September 27, 2026 16:59
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