Skip to content

feat: redesign Profiles pane following design philosophy#492

Merged
tylerkron merged 8 commits intomainfrom
claude/hungry-hamilton-ce793b
Apr 19, 2026
Merged

feat: redesign Profiles pane following design philosophy#492
tylerkron merged 8 commits intomainfrom
claude/hungry-hamilton-ce793b

Conversation

@tylerkron
Copy link
Copy Markdown
Contributor

Summary

Redesigns the Profiles pane to match the design philosophy used by the Channels pane (#479) and Devices pane (#484) — dark, tile-based, inline drawer instead of a flyout, ambient status instead of modal errors. Also sweeps out the pre-redesign code that the new UI replaces, and addresses follow-up bugs and polish found during testing.

  • New Profiles pane: tile list with active/inactive states, inline 380px edit drawer, new-profile creation flow, ambient status bar showing count + active profile + LOGGING · LOCKED badge when a session is running. Tiles dim with a no-cursor and the + ADD PROFILE button is disabled while logging is active.
  • Dead-code cleanup: removed UpdateProfileFlyout, AddprofileDialog, AddProfileConfirmationDialog and their VMs (~1250 lines). DaqifiViewModel no longer owns any profile state or commands.
  • Bug fixes:
    • Null Channels crash when activating a profile whose device has zero active channels (?.ToList() in LoadProfilesFromXml left ProfileDevice.Channels null when the writer had intentionally omitted the <Channels> element). Normalized to empty list at load time.
    • Frequency slider was capped at 10000Hz but the device only accepts 1000Hz (matching the Devices pane's own cap). Both sliders now Maximum="1000".
  • UX improvement: clicking a second profile while one is active now prompts "'A' is currently active. Switch to 'B'?" and atomically deactivates A + activates B, instead of blocking with a "deactivate first" error.
  • Design-token alignment: switched ProfilesPane.xaml to use the shared tokens from Resources/DesignTokens.xaml (merged from main) — brushes are no longer duplicated locally, and the hardcoded amber on the LOGGING · LOCKED badge uses StatusAmber.
  • Confirm dialog matches the dark design system: replaced the MahApps white-card ShowMessageAsync with an in-pane overlay (scrim + SurfaceRaised card + pill buttons) wired to the VM via a TaskCompletionSource<bool> bridge.

Test plan

  • Create a new profile — name, device selection, channel selection, and frequency slider (1–1000Hz) all work
  • Activate a profile — status bar updates to show {N} SAVED · {name} ACTIVE
  • Deactivate by clicking the active tile — status bar clears
  • With profile A active, click profile B → confirm dialog appears matching the dark theme → choosing SWITCH atomically deactivates A and activates B
  • Choosing CANCEL on the switch dialog leaves A active
  • Clicking outside the dialog (scrim) acts as cancel
  • With a profile that has a device where no channels are selected: saving, deactivating, and re-activating do not crash (regression guard)
  • Start logging → profile tiles dim with no-cursor, + ADD PROFILE disables, LOGGING · LOCKED badge appears, clicking a tile or gear is a silent no-op
  • Stop logging → tiles become interactive again
  • Edit an existing profile via the gear — inline drawer opens, changes persist on close
  • Delete a profile that is not active — succeeds; deleting an active profile shows an inline error

🤖 Generated with Claude Code

tylerkron and others added 6 commits April 18, 2026 13:39
Replaces the list-view + flyout + modal-dialog workflow with a unified
dark Profiles pane: card tiles with ambient status stripes, and a single
inline settings drawer for both editing and creating profiles.

New files:
- ProfilesPaneViewModel – owns drawer state, activation logic (two-pass
  device matching), inline error messages, and new-profile creation form
- ProfilesPane.xaml / .xaml.cs – dark surface with profile cards and a
  right-side 400 px inline drawer (edit mode + new-profile mode)

MainWindow.xaml: replaces the legacy ListView + FAB + flyout content in
the Profiles tab with a single <local:ProfilesPane/> element.

The legacy UpdateProfileFlyout, AddprofileDialog, and
AddProfileConfirmationDialog are kept in place but no longer reachable
from the new pane.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Double-check follow-ups on the Profiles pane redesign:

- Logging-active lockout: block OpenEditDrawer, OpenNewDrawer, and
  ActivateProfile when logging is running; dim tiles with a "no"
  cursor and disable "+ ADD PROFILE" in both populated and empty
  states. Ordered the IsLoggingActive trigger last so hover can't
  un-dim a locked tile.
- Replaced instructional status bar text with ambient state:
  "{N} SAVED · {name} ACTIVE" plus an amber "LOGGING · LOCKED"
  badge bound to IsLoggingActive.
- SaveCurrentSettings now uses NewProfileName when non-empty; the
  date-stamped name is just the fallback.
- Guard empty-name in CloseDrawer before UpdateProfileInXml.
- Drawer width 400 → 380 to match the Channels pane.
- ShowError no longer delegates to OpenEditDrawer, so clearing and
  setting DrawerError aren't order-dependent.
- ActiveProfileName property with proper per-profile PropertyChanged
  subscribe/unsubscribe in Cleanup.

Deleted code made dead by the redesign:

- UpdateProfileFlyout, AddprofileDialog, AddProfileConfirmationDialog
  (views, code-behind, and view models).
- DaqifiViewModel: ShowAddProfileDialog, ShowAddProfileConfirmation,
  GetUpdateProfileAvailableDevice, RemoveProfile, OpenProfileSettings,
  GetAvailableChannels, SaveExistingSetting, ActivateProfile (all old
  RelayCommands) plus SelectedProfile, IsProfileSettingsOpen,
  UpdateProfileSelectedDevice, AvailableDevices, AvailableChannels
  fields/properties; removed the flyout declaration from MainWindow
  and the IsProfileSettingsOpen clear in CloseFlyouts.
- NewProfileChannelItem.TypeLabel (unused).

Net -1250 lines.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…n-ce793b

# Conflicts:
#	Daqifi.Desktop/MainWindow.xaml
#	Daqifi.Desktop/ViewModels/DaqifiViewModel.cs
…evice

LoadProfilesFromXml used `?.ToList()` for ProfileDevice.Channels, so when
the XML has no <Channels> element (which the writer intentionally omits
for devices with zero active channels — see UpdateProfileInXml:247-250
and AddAndRemoveProfileXml:298-306), Channels ends up null on the
loaded profile. That nulled any caller of pd.Channels.Where/.Select.

Repro: activate profile A → deactivate → click tile for profile B →
ArgumentNullException at ProfilesPaneViewModel.ActivateProfile line 250
(`pd.Channels.Where(c => c.IsChannelActive)`).

Normalize to an empty list at load time so every downstream caller can
trust the invariant. Also fixes two latent crashes on the same field in
UpdateProfileInXml:239 and AddAndRemoveProfileXml:299.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… profile

Two unrelated fixes to the Profiles pane activate flow.

1. Frequency slider cap
   The new-profile and edit-profile sliders allowed up to 10000Hz, but the
   device (matching the Devices pane's own cap at
   DevicesPanePrototype.xaml:678) only accepts up to 1000Hz. Profiles saved
   above 1000 would silently fail when pushed to the device. Align both
   ProfilesPane sliders to Maximum="1000".

2. Activating a different profile now offers to switch
   Previously, clicking a second profile while one was active opened the
   drawer with "Deactivate the current active profile first." — forcing
   two clicks for the common case. Now the user gets a yes/no dialog
   ("'A' is currently active. Switch to 'B'?") and on confirm we
   deactivate A and activate B in one action. We still validate that B has
   matching devices *before* asking, so canceling or a mismatch never
   leaves the user with nothing active.

Refactored the matching + apply logic into MatchProfileToConnected and
ApplyProfileToDevices helpers so the switch path reuses the same two-pass
(serial → model) matching as a fresh activate. ActivateProfileCommand is
now an AsyncRelayCommand<Profile> so the confirm dialog can await.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The MahApps MessageDialog (white card, blue theme) doesn't match the
dark, tile-based design system — it reads as a browser alert in the
middle of a carefully designed dark surface. Replace it with an
in-pane overlay styled with the shared design tokens:

  - Scrim over everything (same #A0000000 as the drawer scrim;
    click-to-dismiss behaves like a Cancel)
  - 420px centered card on SurfaceRaised with a BorderDim stroke
  - Title in TextPrimary, message in TextSecondary
  - Cancel (PillButton) + affirmative (AccentPillButton) right-aligned
  - Panel.ZIndex=20 so it sits above the profile drawer (ZIndex=10)

ViewModel now owns the dialog state (IsConfirmOpen, ConfirmTitle,
ConfirmMessage, ConfirmAffirmativeLabel) and exposes affirmative /
negative commands. A private TaskCompletionSource<bool> bridges the
async await surface: ShowConfirm returns Task<bool>, the two button
commands call CompleteConfirm which closes the dialog and resolves
the TCS. Cleanup resolves a pending TCS defensively so no awaiter is
leaked on unload.

Removes the MahApps.Metro.Controls / Dialogs usings — the VM no longer
needs to look up the MainWindow to show a dialog.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@tylerkron tylerkron requested a review from a team as a code owner April 18, 2026 20:53
@qodo-code-review
Copy link
Copy Markdown
Contributor

Review Summary by Qodo

Redesign Profiles pane with dark tiles, inline drawer, and unified workflow

✨ Enhancement 🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Redesign Profiles pane with dark tile-based UI, inline 380px drawer, and ambient status bar
• Replace modal dialogs and flyouts with unified in-pane workflow for profile creation, editing, and
  activation
• Add two-pass device matching (serial number then model) and atomic profile switching with
  confirmation
• Fix null Channels crash when profile device has zero active channels; cap frequency slider at
  1000Hz
• Remove ~1250 lines of dead code (UpdateProfileFlyout, AddprofileDialog,
  AddProfileConfirmationDialog VMs)
• Block profile operations and dim tiles with "no" cursor when logging is active; show "LOGGING ·
  LOCKED" badge
Diagram
flowchart LR
  A["Old UI<br/>ListView + Flyout<br/>+ Modal Dialogs"] -->|Replace| B["New ProfilesPane<br/>Dark Tiles<br/>+ Inline Drawer"]
  B -->|Owns| C["ProfilesPaneViewModel<br/>Activation Logic<br/>Device Matching<br/>Profile CRUD"]
  C -->|Two-Pass Match| D["Profile to Device<br/>Serial Number<br/>then Model"]
  C -->|Atomic Switch| E["Confirm Dialog<br/>Deactivate Old<br/>Activate New"]
  F["LoggingManager"] -->|Null Channels| G["Fix: Default to<br/>Empty List"]
  H["Frequency Slider"] -->|Cap| I["1000 Hz Max"]
Loading

Grey Divider

File Changes

1. Daqifi.Desktop/Loggers/LoggingManager.cs 🐞 Bug fix +4/-1

Fix null Channels crash with empty list default

Daqifi.Desktop/Loggers/LoggingManager.cs


2. Daqifi.Desktop/ViewModels/ProfilesPaneViewModel.cs ✨ Enhancement +490/-0

New unified ViewModel for Profiles pane redesign

Daqifi.Desktop/ViewModels/ProfilesPaneViewModel.cs


3. Daqifi.Desktop/View/ProfilesPane.xaml ✨ Enhancement +956/-0

New dark tile-based UI with inline drawer and confirm dialog

Daqifi.Desktop/View/ProfilesPane.xaml


View more (11)
4. Daqifi.Desktop/View/ProfilesPane.xaml.cs ✨ Enhancement +27/-0

Code-behind for ProfilesPane with lifecycle management

Daqifi.Desktop/View/ProfilesPane.xaml.cs


5. Daqifi.Desktop/ViewModels/DaqifiViewModel.cs ✨ Enhancement +3/-442

Remove profile state and commands; rename IsProfileSettingsOpen

Daqifi.Desktop/ViewModels/DaqifiViewModel.cs


6. Daqifi.Desktop/MainWindow.xaml ✨ Enhancement +1/-83

Replace old Profiles ListView with new ProfilesPane component

Daqifi.Desktop/MainWindow.xaml


7. Daqifi.Desktop/View/AddProfileConfirmationDialog.xaml.cs Miscellaneous +0/-24

Delete dead code-behind for old confirmation dialog

Daqifi.Desktop/View/AddProfileConfirmationDialog.xaml.cs


8. Daqifi.Desktop/View/AddprofileDialog.xaml.cs Miscellaneous +0/-81

Delete dead code-behind for old add-profile dialog

Daqifi.Desktop/View/AddprofileDialog.xaml.cs


9. Daqifi.Desktop/View/Flyouts/UpdateProfileFlyout.xaml.cs Miscellaneous +0/-199

Delete dead code-behind for old profile edit flyout

Daqifi.Desktop/View/Flyouts/UpdateProfileFlyout.xaml.cs


10. Daqifi.Desktop/ViewModels/AddProfileConfirmationDialogViewModel.cs Miscellaneous +0/-97

Delete dead ViewModel for old confirmation dialog

Daqifi.Desktop/ViewModels/AddProfileConfirmationDialogViewModel.cs


11. Daqifi.Desktop/ViewModels/AddProfileDialogViewModel.cs Miscellaneous +0/-270

Delete dead ViewModel for old add-profile dialog

Daqifi.Desktop/ViewModels/AddProfileDialogViewModel.cs


12. Daqifi.Desktop/View/AddProfileConfirmationDialog.xaml Miscellaneous +0/-18

Delete old confirmation dialog XAML markup

Daqifi.Desktop/View/AddProfileConfirmationDialog.xaml


13. Daqifi.Desktop/View/AddprofileDialog.xaml Miscellaneous +0/-82

Delete old add-profile dialog XAML markup

Daqifi.Desktop/View/AddprofileDialog.xaml


14. Daqifi.Desktop/View/Flyouts/UpdateProfileFlyout.xaml Miscellaneous +0/-134

Delete old profile edit flyout XAML markup

Daqifi.Desktop/View/Flyouts/UpdateProfileFlyout.xaml


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown
Contributor

qodo-code-review Bot commented Apr 18, 2026

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Missing #region member grouping📘 Rule violation ✧ Quality
Description
ProfilesPaneViewModel is a large class but does not use #region/#endregion directives to group
members. This reduces maintainability and violates the required organization standard for
non-trivial C# types.
Code

Daqifi.Desktop/ViewModels/ProfilesPaneViewModel.cs[R41-97]

+public partial class ProfilesPaneViewModel : ObservableObject
+{
+    private readonly AppLogger _logger = AppLogger.Instance;
+
+    public ObservableCollection<Profile> Profiles => LoggingManager.Instance.SubscribedProfiles;
+
+    [ObservableProperty] private bool _hasProfiles;
+    [ObservableProperty] private bool _isDrawerOpen;
+    [ObservableProperty] private bool _isNewProfile;
+    [ObservableProperty] private Profile? _selectedProfile;
+    [ObservableProperty] private bool _isLoggingActive;
+    [ObservableProperty] private string _drawerError = string.Empty;
+    [ObservableProperty] private bool _hasDrawerError;
+    [ObservableProperty] private string? _activeProfileName;
+
+    // In-pane confirm dialog state (used by ShowConfirm for profile switch, etc.).
+    [ObservableProperty] private bool _isConfirmOpen;
+    [ObservableProperty] private string _confirmTitle = string.Empty;
+    [ObservableProperty] private string _confirmMessage = string.Empty;
+    [ObservableProperty] private string _confirmAffirmativeLabel = "OK";
+    private TaskCompletionSource<bool>? _confirmTcs;
+
+    // New-profile form fields (active only when IsNewProfile = true)
+    [ObservableProperty] private string _newProfileName = string.Empty;
+    [ObservableProperty] private int _newProfileFrequency = 1000;
+    public ObservableCollection<NewProfileDeviceItem> NewDeviceItems { get; } = [];
+
+    public IRelayCommand<Profile> OpenEditDrawerCommand { get; }
+    public IRelayCommand OpenNewDrawerCommand { get; }
+    public IRelayCommand CloseDrawerCommand { get; }
+    public IAsyncRelayCommand<Profile> ActivateProfileCommand { get; }
+    public IRelayCommand<Profile> DeleteProfileCommand { get; }
+    public IRelayCommand SaveNewProfileCommand { get; }
+    public IRelayCommand SaveCurrentSettingsCommand { get; }
+    public IRelayCommand ConfirmAffirmativeCommand { get; }
+    public IRelayCommand ConfirmNegativeCommand { get; }
+
+    public ProfilesPaneViewModel()
+    {
+        OpenEditDrawerCommand = new RelayCommand<Profile>(OpenEditDrawer);
+        OpenNewDrawerCommand = new RelayCommand(OpenNewDrawer);
+        CloseDrawerCommand = new RelayCommand(CloseDrawer);
+        ActivateProfileCommand = new AsyncRelayCommand<Profile>(ActivateProfile);
+        DeleteProfileCommand = new RelayCommand<Profile>(DeleteProfile);
+        SaveNewProfileCommand = new RelayCommand(SaveNewProfile, CanSaveNewProfile);
+        SaveCurrentSettingsCommand = new RelayCommand(SaveCurrentSettings);
+        ConfirmAffirmativeCommand = new RelayCommand(() => CompleteConfirm(true));
+        ConfirmNegativeCommand = new RelayCommand(() => CompleteConfirm(false));
+
+        LoggingManager.Instance.PropertyChanged += OnLoggingManagerPropertyChanged;
+        IsLoggingActive = LoggingManager.Instance.Active;
+        HasProfiles = Profiles.Count > 0;
+        foreach (var p in Profiles) p.PropertyChanged += OnProfilePropertyChanged;
+        Profiles.CollectionChanged += OnProfilesCollectionChanged;
+        RefreshActiveProfileName();
+    }
+
Evidence
The compliance rule requires #region logical grouping for large/non-trivial C# types;
ProfilesPaneViewModel spans hundreds of lines with many members and has no #region sections.

Rule 244816: Use #region directives for logical grouping of members
Daqifi.Desktop/ViewModels/ProfilesPaneViewModel.cs[41-97]

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

## Issue description
`ProfilesPaneViewModel` is a large class but lacks `#region/#endregion` directives to group fields, commands, constructors, and methods.

## Issue Context
The rule applies to non-trivial C# types (many members / large line count). This class meets that threshold.

## Fix Focus Areas
- Daqifi.Desktop/ViewModels/ProfilesPaneViewModel.cs[41-97]
- Daqifi.Desktop/ViewModels/ProfilesPaneViewModel.cs[98-490]

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


2. ProfilesPane lacks XML docs📘 Rule violation ✧ Quality
Description
The new public ProfilesPane class and its public constructor do not have XML documentation
comments with <summary>. This violates the public API documentation requirement.
Code

Daqifi.Desktop/View/ProfilesPane.xaml.cs[R6-13]

+public partial class ProfilesPane : UserControl
+{
+    public ProfilesPane()
+    {
+        InitializeComponent();
+        Loaded   += OnLoaded;
+        Unloaded += OnUnloaded;
+    }
Evidence
The compliance rule requires XML documentation for new public types/members; ProfilesPane and its
public constructor are declared without preceding /// <summary> comments.

Rule 244813: Require XML documentation comments for all public API members
Daqifi.Desktop/View/ProfilesPane.xaml.cs[6-13]

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

## Issue description
`ProfilesPane` is a new public class but is missing required XML documentation comments.

## Issue Context
The rule requires `/// <summary>` docs for all new public types and members.

## Fix Focus Areas
- Daqifi.Desktop/View/ProfilesPane.xaml.cs[6-13]

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


3. Deactivate leaves stale subscriptions🐞 Bug ≡ Correctness
Description
ProfilesPaneViewModel deactivates by calling RemoveAllChannels() before
LoggingManager.Unsubscribe(), but RemoveAllChannels sets channel.IsActive=false and Unsubscribe only
removes channels that are still IsActive. This leaves channels stuck in
LoggingManager.SubscribedChannels after deactivation/switch, so downstream UI/state (e.g.,
ActiveChannels/CanToggleLogging) can remain incorrect.
Code

Daqifi.Desktop/ViewModels/ProfilesPaneViewModel.cs[R316-320]

+            else
+            {
+                device.RemoveAllChannels();
+                foreach (var ch in channels) LoggingManager.Instance.Unsubscribe(ch);
+            }
Evidence
In the new ProfilesPaneViewModel deactivation path, the device is told to disable all channels
first. The device implementation marks all DataChannels as inactive, and LoggingManager.Unsubscribe
filters on IsActive==true when finding a subscribed channel to remove. Therefore the subsequent
Unsubscribe calls will often no-op, leaving SubscribedChannels populated; DaqifiViewModel then
rebuilds ActiveChannels from SubscribedChannels and sets CanToggleLogging based on
ActiveChannels.Count.

Daqifi.Desktop/ViewModels/ProfilesPaneViewModel.cs[295-326]
Daqifi.Desktop/Device/AbstractStreamingDevice.cs[824-835]
Daqifi.Desktop/Loggers/LoggingManager.cs[405-442]
Daqifi.Desktop/ViewModels/DaqifiViewModel.cs[1722-1773]

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

### Issue description
When deactivating a profile, `ApplyProfileToDevices(..., activate: false)` calls `device.RemoveAllChannels()` before `LoggingManager.Instance.Unsubscribe(ch)`. Because `RemoveAllChannels()` sets `channel.IsActive = false` for all channels, `LoggingManager.Unsubscribe()` (which currently searches only for subscribed channels where `IsActive` is true) often fails to remove the channel from `SubscribedChannels`.

### Issue Context
This leaves stale entries in `LoggingManager.SubscribedChannels`, which then propagates into UI state (`ActiveChannels`, `CanToggleLogging`) and can cause incorrect behavior after profile deactivate/switch.

### Fix Focus Areas
- Daqifi.Desktop/ViewModels/ProfilesPaneViewModel.cs[295-321]
- Daqifi.Desktop/Loggers/LoggingManager.cs[425-442]

### What to change
1. In `ApplyProfileToDevices` deactivation branch, unsubscribe **before** calling `RemoveAllChannels()`.
2. Make `LoggingManager.Unsubscribe` robust by removing the `&& x.IsActive` filter (or otherwise allowing removal even if the channel was already marked inactive), so ordering mistakes or other deactivation paths don’t leave stale subscriptions.
3. (Optional but safer) When deactivating, consider unsubscribing all subscribed channels for the matched device serial number(s), not just those derived from the profile definition, to prevent stale subscriptions if the profile was edited while active.

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


View more (3)
4. Multiple public classes in file📘 Rule violation ⌂ Architecture
Description
ProfilesPaneViewModel.cs declares three public top-level classes, exceeding the single main class
per file limit. This increases coupling and violates the project’s file organization requirement.
Code

Daqifi.Desktop/ViewModels/ProfilesPaneViewModel.cs[R18-41]

+public partial class NewProfileDeviceItem : ObservableObject
+{
+    [ObservableProperty] private bool _isSelected;
+    public required IStreamingDevice Device { get; init; }
+    public string Name => Device.Name;
+    public string SerialNo => Device.DeviceSerialNo ?? string.Empty;
+    public ObservableCollection<NewProfileChannelItem> ChannelItems { get; } = [];
+}
+
+/// <summary>
+/// Wraps a channel for the new-profile creation form.
+/// </summary>
+public partial class NewProfileChannelItem : ObservableObject
+{
+    [ObservableProperty] private bool _isSelected;
+    public required IChannel Channel { get; init; }
+    public string Name => Channel.Name;
+}
+
+/// <summary>
+/// Backs the unified Profiles pane. Owns drawer state, profile activation,
+/// creation, and deletion without going through DaqifiViewModel.
+/// </summary>
+public partial class ProfilesPaneViewModel : ObservableObject
Evidence
The compliance rule limits each source file to a single main/public class, but this file introduces
multiple public top-level classes (NewProfileDeviceItem, NewProfileChannelItem, and
ProfilesPaneViewModel).

Rule 244819: Limit each source file to a single main class
Daqifi.Desktop/ViewModels/ProfilesPaneViewModel.cs[18-41]

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

## Issue description
`Daqifi.Desktop/ViewModels/ProfilesPaneViewModel.cs` contains multiple public top-level classes, violating the single-main-class-per-file rule.

## Issue Context
This file currently declares `NewProfileDeviceItem`, `NewProfileChannelItem`, and `ProfilesPaneViewModel` as public top-level classes.

## Fix Focus Areas
- Daqifi.Desktop/ViewModels/ProfilesPaneViewModel.cs[18-41]

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


5. Inline braces break Allman📘 Rule violation ✧ Quality
Description
The new code uses inline brace blocks (e.g., if (...) { ... }), which violates the Allman brace
style requirement. This reduces consistency with the project’s formatting standard.
Code

Daqifi.Desktop/ViewModels/ProfilesPaneViewModel.cs[R270-283]

+            var exact = connected.FirstOrDefault(cd =>
+                !string.IsNullOrEmpty(pd.DeviceSerialNo) &&
+                string.Equals(cd.DeviceSerialNo, pd.DeviceSerialNo, StringComparison.OrdinalIgnoreCase) &&
+                !claimed.Contains(cd));
+            if (exact != null) { result[pd] = exact; claimed.Add(exact); }
+        }
+
+        foreach (var pd in profile.Devices)
+        {
+            if (result.ContainsKey(pd)) continue;
+            var modelMatch = connected.FirstOrDefault(cd =>
+                cd.DevicePartNumber == pd.DevicePartName && !claimed.Contains(cd));
+            if (modelMatch != null) { result[pd] = modelMatch; claimed.Add(modelMatch); }
+        }
Evidence
The Allman rule forbids opening braces on the same line as conditionals and disallows inline blocks;
the if (exact != null) { ... } and if (modelMatch != null) { ... } statements violate this.

Rule 244808: Use Allman style for opening braces
Daqifi.Desktop/ViewModels/ProfilesPaneViewModel.cs[270-283]

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

## Issue description
Inline brace blocks (e.g., `if (...) { ... }`) violate the required Allman brace style.

## Issue Context
The project requires opening braces on their own line for conditionals/loops and discourages inline blocks.

## Fix Focus Areas
- Daqifi.Desktop/ViewModels/ProfilesPaneViewModel.cs[270-283]

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


6. Public members missing XML docs📘 Rule violation ✧ Quality
Description
New public types/members were added without XML documentation comments containing <summary>. This
violates the requirement that all public API members include XML docs for generated documentation
and maintainability.
Code

Daqifi.Desktop/ViewModels/ProfilesPaneViewModel.cs[R18-36]

+public partial class NewProfileDeviceItem : ObservableObject
+{
+    [ObservableProperty] private bool _isSelected;
+    public required IStreamingDevice Device { get; init; }
+    public string Name => Device.Name;
+    public string SerialNo => Device.DeviceSerialNo ?? string.Empty;
+    public ObservableCollection<NewProfileChannelItem> ChannelItems { get; } = [];
+}
+
+/// <summary>
+/// Wraps a channel for the new-profile creation form.
+/// </summary>
+public partial class NewProfileChannelItem : ObservableObject
+{
+    [ObservableProperty] private bool _isSelected;
+    public required IChannel Channel { get; init; }
+    public string Name => Channel.Name;
+}
+
Evidence
The rule requires XML doc comments for every new public member; this file adds public properties
like Device/Channel without any /// <summary> documentation immediately preceding them.

Rule 244813: Require XML documentation comments for all public API members
Daqifi.Desktop/ViewModels/ProfilesPaneViewModel.cs[18-36]

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

## Issue description
Public members were introduced without required XML documentation comments (`/// <summary>...`).

## Issue Context
This applies to newly added public classes and members (properties, constructors, methods, etc.). Consider either documenting them or reducing their visibility if they are not intended as public API.

## Fix Focus Areas
- Daqifi.Desktop/ViewModels/ProfilesPaneViewModel.cs[18-36]
- Daqifi.Desktop/ViewModels/ProfilesPaneViewModel.cs[68-96]

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



Remediation recommended

7. Missing NotifyPropertyChangedFor link📘 Rule violation ≡ Correctness
Description
HasDrawerError depends on DrawerError, but the DrawerError field is not annotated with
[NotifyPropertyChangedFor(nameof(HasDrawerError))]. This can lead to inconsistent UI update
behavior relative to the required MVVM Toolkit pattern.
Code

Daqifi.Desktop/ViewModels/ProfilesPaneViewModel.cs[R47-121]

+    [ObservableProperty] private bool _hasProfiles;
+    [ObservableProperty] private bool _isDrawerOpen;
+    [ObservableProperty] private bool _isNewProfile;
+    [ObservableProperty] private Profile? _selectedProfile;
+    [ObservableProperty] private bool _isLoggingActive;
+    [ObservableProperty] private string _drawerError = string.Empty;
+    [ObservableProperty] private bool _hasDrawerError;
+    [ObservableProperty] private string? _activeProfileName;
+
+    // In-pane confirm dialog state (used by ShowConfirm for profile switch, etc.).
+    [ObservableProperty] private bool _isConfirmOpen;
+    [ObservableProperty] private string _confirmTitle = string.Empty;
+    [ObservableProperty] private string _confirmMessage = string.Empty;
+    [ObservableProperty] private string _confirmAffirmativeLabel = "OK";
+    private TaskCompletionSource<bool>? _confirmTcs;
+
+    // New-profile form fields (active only when IsNewProfile = true)
+    [ObservableProperty] private string _newProfileName = string.Empty;
+    [ObservableProperty] private int _newProfileFrequency = 1000;
+    public ObservableCollection<NewProfileDeviceItem> NewDeviceItems { get; } = [];
+
+    public IRelayCommand<Profile> OpenEditDrawerCommand { get; }
+    public IRelayCommand OpenNewDrawerCommand { get; }
+    public IRelayCommand CloseDrawerCommand { get; }
+    public IAsyncRelayCommand<Profile> ActivateProfileCommand { get; }
+    public IRelayCommand<Profile> DeleteProfileCommand { get; }
+    public IRelayCommand SaveNewProfileCommand { get; }
+    public IRelayCommand SaveCurrentSettingsCommand { get; }
+    public IRelayCommand ConfirmAffirmativeCommand { get; }
+    public IRelayCommand ConfirmNegativeCommand { get; }
+
+    public ProfilesPaneViewModel()
+    {
+        OpenEditDrawerCommand = new RelayCommand<Profile>(OpenEditDrawer);
+        OpenNewDrawerCommand = new RelayCommand(OpenNewDrawer);
+        CloseDrawerCommand = new RelayCommand(CloseDrawer);
+        ActivateProfileCommand = new AsyncRelayCommand<Profile>(ActivateProfile);
+        DeleteProfileCommand = new RelayCommand<Profile>(DeleteProfile);
+        SaveNewProfileCommand = new RelayCommand(SaveNewProfile, CanSaveNewProfile);
+        SaveCurrentSettingsCommand = new RelayCommand(SaveCurrentSettings);
+        ConfirmAffirmativeCommand = new RelayCommand(() => CompleteConfirm(true));
+        ConfirmNegativeCommand = new RelayCommand(() => CompleteConfirm(false));
+
+        LoggingManager.Instance.PropertyChanged += OnLoggingManagerPropertyChanged;
+        IsLoggingActive = LoggingManager.Instance.Active;
+        HasProfiles = Profiles.Count > 0;
+        foreach (var p in Profiles) p.PropertyChanged += OnProfilePropertyChanged;
+        Profiles.CollectionChanged += OnProfilesCollectionChanged;
+        RefreshActiveProfileName();
+    }
+
+    private void OnProfilesCollectionChanged(object? sender,
+        System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
+    {
+        HasProfiles = Profiles.Count > 0;
+        if (e.OldItems != null)
+            foreach (Profile p in e.OldItems) p.PropertyChanged -= OnProfilePropertyChanged;
+        if (e.NewItems != null)
+            foreach (Profile p in e.NewItems) p.PropertyChanged += OnProfilePropertyChanged;
+        RefreshActiveProfileName();
+    }
+
+    private void OnProfilePropertyChanged(object? sender, PropertyChangedEventArgs e)
+    {
+        if (e.PropertyName == nameof(Profile.IsProfileActive))
+            RefreshActiveProfileName();
+    }
+
+    private void RefreshActiveProfileName() =>
+        ActiveProfileName = Profiles.FirstOrDefault(p => p.IsProfileActive)?.Name;
+
+    partial void OnDrawerErrorChanged(string value) => HasDrawerError = !string.IsNullOrEmpty(value);
+
+    partial void OnNewProfileNameChanged(string value) =>
+        SaveNewProfileCommand.NotifyCanExecuteChanged();
Evidence
The rule requires dependent properties to be declared via [NotifyPropertyChangedFor] on the
[ObservableProperty] field they depend on; DrawerError changes influence HasDrawerError but
there is no such attribute on _drawerError.

Rule 244798: Annotate dependent UI properties with [NotifyPropertyChangedFor] when using [ObservableProperty]
Daqifi.Desktop/ViewModels/ProfilesPaneViewModel.cs[47-121]

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 dependent UI property (`HasDrawerError`) is not declared via `[NotifyPropertyChangedFor]` on its source `[ObservableProperty]` (`_drawerError`).

## Issue Context
The project standard prefers MVVM Toolkit’s attribute-driven dependency notifications so dependent UI properties reliably update.

## Fix Focus Areas
- Daqifi.Desktop/ViewModels/ProfilesPaneViewModel.cs[47-121]

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


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Comment thread Daqifi.Desktop/ViewModels/ProfilesPaneViewModel.cs Outdated
Comment thread Daqifi.Desktop/ViewModels/ProfilesPaneViewModel.cs
Comment thread Daqifi.Desktop/ViewModels/ProfilesPaneViewModel.cs
Comment thread Daqifi.Desktop/ViewModels/ProfilesPaneViewModel.cs Outdated
Comment thread Daqifi.Desktop/View/ProfilesPane.xaml.cs
Comment thread Daqifi.Desktop/ViewModels/ProfilesPaneViewModel.cs
- Fix deactivate-order bug in ApplyProfileToDevices: call
  LoggingManager.Unsubscribe BEFORE AbstractStreamingDevice.RemoveAllChannels.
  Unsubscribe filters its lookup by IChannel.IsActive, and RemoveAllChannels
  clears IsActive on every channel, so the previous order caused Unsubscribe
  to silently no-op and leave stale subscriptions behind.
- Split NewProfileDeviceItem and NewProfileChannelItem into their own files
  so ProfilesPaneViewModel.cs hosts a single public class.
- Add #region grouping throughout ProfilesPaneViewModel (fields, properties,
  commands, constructor, handlers, drawer lifecycle, activation, confirm,
  CRUD, helpers, cleanup).
- Fix two Allman-brace violations in MatchProfileToConnected.
- Add XML docs on every public property, command, and method.
- Add XML docs on the ProfilesPane code-behind class and constructor.
- Replace [ObservableProperty] _hasDrawerError + OnDrawerErrorChanged partial
  with [NotifyPropertyChangedFor(nameof(HasDrawerError))] on _drawerError
  and a computed HasDrawerError property — single source of truth.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@tylerkron
Copy link
Copy Markdown
Contributor Author

All 6 inline Qodo items addressed in 2386e3d (threaded replies posted on each).

Also fixed the summary-only item from the Qodo review comment ("Missing NotifyPropertyChangedFor link — _hasDrawerError + OnDrawerErrorChanged partial"):

  • Removed the [ObservableProperty] private bool _hasDrawerError; backing field.
  • Removed the partial void OnDrawerErrorChanged(string value) => HasDrawerError = !string.IsNullOrEmpty(value); side-channel.
  • Annotated _drawerError with [NotifyPropertyChangedFor(nameof(HasDrawerError))] so PropertyChanged for HasDrawerError is raised automatically by the source generator.
  • Replaced HasDrawerError with a computed property: public bool HasDrawerError => !string.IsNullOrEmpty(DrawerError);.

Net effect: single source of truth (DrawerError), same public surface, no duplicated state. Build passes.

@github-actions
Copy link
Copy Markdown

📊 Code Coverage Report

Summary

Summary
Generated on: 4/19/2026 - 2:42:29 AM
Coverage date: 4/19/2026 - 2:41:56 AM - 4/19/2026 - 2:42:25 AM
Parser: MultiReport (4x Cobertura)
Assemblies: 3
Classes: 118
Files: 146
Line coverage: 17.9% (1507 of 8410)
Covered lines: 1507
Uncovered lines: 6903
Coverable lines: 8410
Total lines: 24802
Branch coverage: 18.9% (521 of 2754)
Covered branches: 521
Total branches: 2754
Method coverage: Feature is only available for sponsors

Coverage

DAQiFi - 17.7%
Name Line Branch
DAQiFi 17.7% 18.9%
Daqifi.Desktop.App 5.4% 0%
Daqifi.Desktop.Channel.AbstractChannel 40.9% 27.7%
Daqifi.Desktop.Channel.AnalogChannel 58.7% 25%
Daqifi.Desktop.Channel.Channel 11.5% 0%
Daqifi.Desktop.Channel.ChannelColorManager 100% 100%
Daqifi.Desktop.Channel.DataSample 91.6%
Daqifi.Desktop.Channel.DigitalChannel 65.2% 25%
Daqifi.Desktop.Commands.CompositeCommand 0% 0%
Daqifi.Desktop.Commands.HostCommands 0%
Daqifi.Desktop.Commands.WeakEventHandlerManager 0% 0%
Daqifi.Desktop.Configuration.FirewallConfiguration 90.6% 66.6%
Daqifi.Desktop.Configuration.WindowsFirewallWrapper 64% 68.4%
Daqifi.Desktop.ConnectionManager 42.4% 39.2%
Daqifi.Desktop.Converters.BoolToActiveStatusConverter 0% 0%
Daqifi.Desktop.Converters.BoolToConnectionStatusConverter 0% 0%
Daqifi.Desktop.Converters.BoolToStatusColorConverter 0% 0%
Daqifi.Desktop.Converters.BrushColorMatchConverter 0% 0%
Daqifi.Desktop.Converters.ConnectionTypeToColorConverter 0% 0%
Daqifi.Desktop.Converters.ConnectionTypeToUsbConverter 0% 0%
Daqifi.Desktop.Converters.InvertedBoolToVisibilityConverter 0% 0%
Daqifi.Desktop.Converters.ListToStringConverter 0% 0%
Daqifi.Desktop.Converters.NotNullToVisibilityConverter 0% 0%
Daqifi.Desktop.Converters.OxyColorToBrushConverter 0% 0%
Daqifi.Desktop.Converters.StringRightConverter 0% 0%
Daqifi.Desktop.Device.AbstractStreamingDevice 42.9% 38.6%
Daqifi.Desktop.Device.DeviceMessage 0%
Daqifi.Desktop.Device.Firmware.BootloaderSessionStreamingDeviceAdapter 0% 0%
Daqifi.Desktop.Device.Firmware.WifiPromptDelayProcessRunner 0% 0%
Daqifi.Desktop.Device.NativeMethods 100%
Daqifi.Desktop.Device.SerialDevice.SerialStreamingDevice 27.6% 30.8%
Daqifi.Desktop.Device.WiFiDevice.DaqifiStreamingDevice 40.9% 39.4%
Daqifi.Desktop.DialogService.DialogService 0% 0%
Daqifi.Desktop.DialogService.ServiceLocator 0% 0%
Daqifi.Desktop.DiskSpace.DiskSpaceCheckResult 100%
Daqifi.Desktop.DiskSpace.DiskSpaceEventArgs 100%
Daqifi.Desktop.DiskSpace.DiskSpaceMonitor 88.2% 86.6%
Daqifi.Desktop.DuplicateDeviceCheckResult 100%
Daqifi.Desktop.Exporter.OptimizedLoggingSessionExporter 66.5% 62.7%
Daqifi.Desktop.Exporter.SampleData 100%
Daqifi.Desktop.Helpers.BooleanConverter`1 0% 0%
Daqifi.Desktop.Helpers.BooleanToInverseBoolConverter 0% 0%
Daqifi.Desktop.Helpers.BooleanToVisibilityConverter 0%
Daqifi.Desktop.Helpers.EnumDescriptionConverter 100% 100%
Daqifi.Desktop.Helpers.IntToVisibilityConverter 0% 0%
Daqifi.Desktop.Helpers.MinMaxDownsampler 98.6% 97.9%
Daqifi.Desktop.Helpers.MyMultiValueConverter 0%
Daqifi.Desktop.Helpers.NaturalSortHelper 100% 100%
Daqifi.Desktop.Helpers.VersionHelper 98.2% 66.2%
Daqifi.Desktop.Logger.DatabaseLogger 0% 0%
Daqifi.Desktop.Logger.DatabaseMigrator 0% 0%
Daqifi.Desktop.Logger.DeviceLegendGroup 100% 100%
Daqifi.Desktop.Logger.LoggedSeriesLegendItem 0% 0%
Daqifi.Desktop.Logger.LoggingContext 100%
Daqifi.Desktop.Logger.LoggingContextDesignTimeFactory 0%
Daqifi.Desktop.Logger.LoggingManager 0% 0%
Daqifi.Desktop.Logger.LoggingSession 16% 5%
Daqifi.Desktop.Logger.PlotLogger 0% 0%
Daqifi.Desktop.Logger.SessionDeviceMetadata 80%
Daqifi.Desktop.Logger.SummaryLogger 0% 0%
Daqifi.Desktop.Logger.TimestampGapDetector 95% 83.3%
Daqifi.Desktop.Loggers.ImportOptions 0%
Daqifi.Desktop.Loggers.ImportProgress 0% 0%
Daqifi.Desktop.Loggers.SdCardSessionImporter 0% 0%
Daqifi.Desktop.MainWindow 0% 0%
Daqifi.Desktop.Migrations.AddSamplesSessionTimeIndex 0%
Daqifi.Desktop.Migrations.AddSessionDeviceMetadata 0%
Daqifi.Desktop.Migrations.AddSessionSampleCount 0%
Daqifi.Desktop.Migrations.InitialSQLiteMigration 0%
Daqifi.Desktop.Migrations.LoggingContextModelSnapshot 0%
Daqifi.Desktop.Models.AddProfileModel 0%
Daqifi.Desktop.Models.DaqifiSettings 80.5% 83.3%
Daqifi.Desktop.Models.DebugDataCollection 6.6% 0%
Daqifi.Desktop.Models.DebugDataModel 0% 0%
Daqifi.Desktop.Models.Notifications 0%
Daqifi.Desktop.Models.SdCardFile 0% 0%
Daqifi.Desktop.Services.WindowsPrincipalAdminChecker 0%
Daqifi.Desktop.Services.WpfMessageBoxService 0%
Daqifi.Desktop.UpdateVersion.VersionNotification 0% 0%
Daqifi.Desktop.View.ConnectionDialog 0% 0%
Daqifi.Desktop.View.DebugWindow 0% 0%
Daqifi.Desktop.View.DeviceLogsView 0% 0%
Daqifi.Desktop.View.DuplicateDeviceDialog 0% 0%
Daqifi.Desktop.View.ErrorDialog 0% 0%
Daqifi.Desktop.View.ExportDialog 0% 0%
Daqifi.Desktop.View.FirmwareDialog 0% 0%
Daqifi.Desktop.View.Flyouts.FirmwareFlyout 0% 0%
Daqifi.Desktop.View.Flyouts.LiveGraphFlyout 0% 0%
Daqifi.Desktop.View.Flyouts.LoggedSessionFlyout 0% 0%
Daqifi.Desktop.View.Flyouts.NotificationsFlyout 0% 0%
Daqifi.Desktop.View.Flyouts.SummaryFlyout 0% 0%
Daqifi.Desktop.View.MigrationStatusWindow 0% 0%
Daqifi.Desktop.View.MinimapInteractionController 0% 0%
Daqifi.Desktop.View.ProfilesPane 0% 0%
Daqifi.Desktop.View.Prototype.ChannelsPanePrototype 0% 0%
Daqifi.Desktop.View.Prototype.DevicesPanePrototype 0% 0%
Daqifi.Desktop.View.SuccessDialog 0% 0%
Daqifi.Desktop.ViewModels.ChannelsPaneViewModel 0% 0%
Daqifi.Desktop.ViewModels.ChannelTileViewModel 0% 0%
Daqifi.Desktop.ViewModels.ConnectionDialogViewModel 37.3% 39.1%
Daqifi.Desktop.ViewModels.DaqifiViewModel 17.5% 11%
Daqifi.Desktop.ViewModels.DeviceLogsViewModel 0% 0%
Daqifi.Desktop.ViewModels.DevicesPaneViewModel 0% 0%
Daqifi.Desktop.ViewModels.DeviceTileViewModel 0% 0%
Daqifi.Desktop.ViewModels.DuplicateDeviceDialogViewModel 0%
Daqifi.Desktop.ViewModels.ErrorDialogViewModel 0%
Daqifi.Desktop.ViewModels.ExportDialogViewModel 0% 0%
Daqifi.Desktop.ViewModels.FirmwareDialogViewModel 0% 0%
Daqifi.Desktop.ViewModels.NewProfileChannelItem 0%
Daqifi.Desktop.ViewModels.NewProfileDeviceItem 0% 0%
Daqifi.Desktop.ViewModels.ProfilesPaneViewModel 0% 0%
Daqifi.Desktop.ViewModels.SettingsViewModel 0% 0%
Daqifi.Desktop.ViewModels.SuccessDialogViewModel 85.7%
Daqifi.Desktop.WindowViewModelMapping.IWindowViewModelMappingsContract 0%
Daqifi.Desktop.WindowViewModelMapping.WindowViewModelMappings 0%
Sentry.Generated.BuildPropertyInitializer 100%
Daqifi.Desktop.Common - 30.8%
Name Line Branch
Daqifi.Desktop.Common 30.8% 16.6%
Daqifi.Desktop.Common.Loggers.AppLogger 33.7% 16.6%
Daqifi.Desktop.Common.Loggers.NoOpLogger 0%
Daqifi.Desktop.IO - 100%
Name Line Branch
Daqifi.Desktop.IO 100% ****
Daqifi.Desktop.IO.Messages.MessageEventArgs`1 100%

Coverage report generated by ReportGeneratorView full report in build artifacts

@tylerkron tylerkron merged commit f2a2979 into main Apr 19, 2026
14 checks passed
@tylerkron tylerkron deleted the claude/hungry-hamilton-ce793b branch April 19, 2026 02:55
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