USB device picker: show connected devices by name#569
Closed
dustinrue wants to merge 18 commits into
Closed
Conversation
Three-panel layout revised: - Panel 1 (left, ~1/3): profile list with dot, name, confidence score - Panel 2 (centre): profile name, threshold slider, exclusive toggle, live confidence badge, then rules list with match state and toolbar - Panel 3 (right): all global actions grouped by On Activate / On Deactivate, each with a checkbox to link/unlink to the selected profile — no add/edit here Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each reading row gets a [+] button that opens a QuickCreateRuleView sheet pre-filled with the sensor/key/value. User picks profile, sets weight, optionally negates, and saves. New Profile can be created inline. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…reate rule - ActionTypes.swift: add Action and ProfileActionLink SDK types - SensorTypes.swift: add Hashable + Identifiable to SensorReading/ObservationValue - AppDatabase.swift: v7 migration adds actions + profileActionLinks tables - ActionStore.swift: CRUD for Action and ProfileActionLinkStore - Backend.swift: wire actionStore + profileActionLinkStore - ProfileActivationManager.swift: execute via profileActionLinks -> actions - ControlPlaneStore.swift: publish actions/profileActionLinks, add CRUD methods - PreferencesView.swift: new tab order (Profiles, Actions, Sensors, General), min 860x520 - ProfilesTabView.swift: three-panel HSplitView with ProfileDetailPanel + ProfileActionsPanel - ProfileDetailView.swift: removed (absorbed into ProfilesTabView) - ActionsTabView.swift: global action library with CreateOrEditActionView sheet - QuickCreateRuleView.swift: pre-filled rule creation from a live sensor reading - SensorsTabView.swift: add [+] button column triggering QuickCreateRuleView sheet Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ActionConfigForm.swift handles every action type with purpose-built controls: - Shell Script: Browse button scoped to scripts, optional arguments field - Open File/App: Browse button (any file or .app) - Open and Hide: Browse button (.app bundles only) - Open URL: text field with scheme hint - Quit Application: picker from installed apps (/Applications + ~/Applications + /System/Applications), force-quit toggle - Speak Text: text field + voice picker populated from NSSpeechSynthesizer, grouped by locale, system default option - Mount Volume: URL field with smb/afp/nfs hint - Unmount Volume: picker of currently mounted volumes (non-root) - Desktop Background: image Browse button + All/Main display segmented control - Toggle WiFi / Prevent Sleep: Enable/Disable segmented control - Switch Network Location: picker loaded from networksetup -listlocations - Set Default Printer: picker from NSPrinter.printerNames - Run Shortcut: picker populated from shortcuts list --show-identifiers, auto-fills shortcutName display field on selection - Set Time Machine Destination: Browse button (directories) - Lock Keychain / Start Screen Saver / Start Time Machine: no-config message PathPanelConfig moved to ActionConfigForm.swift (was duplicated in CreateActionView). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sensors that have no enabled rules are now stopped when the settings window is closed, eliminating background polling from unused sensors. When settings opens, all sensors start so the user sees live readings for every sensor while creating rules. Run policy re-applies whenever rules change so newly-ruleless sensors stop promptly. Key changes: - SensorCoordinator: separate register() (no start) from add(); add startAll() and applyRunPolicy(neededIDs:) methods - Backend: registerSensor() now calls register() instead of add(), adds sensorIDsNeededForRules() and applyRunPolicy(settingsOpen:), applies policy after startup and after every rule refresh - PreferencesWindowController: accepts onOpen/onClose callbacks - AppDelegate: wires open to startAll, close to applyRunPolicy on backend Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When cpctl runs 'sensors readings', every registered sensor now calls refresh() before snapshots are returned, ensuring callers get current data rather than stale cached values. A new SensorCoordinator.refreshForQuery() method refreshes all registered sensors (running or stopped) without firing the snapshot callback, since this is a read-only inspection path. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous implementation only called refresh() on sensors, which is a no-op on BaseSensor (the default). Stopped sensors also had publishInactive() called on them, leaving their snapshots empty. New approach: - Running sensors: refresh() is called (effective for WiFi, FilePresence) - Stopped sensors: start() is called so most sensors call refreshSnapshot() synchronously and immediately have data; they are then stopped in a background task after the caller has collected the snapshot Sensors with deferred init (BluetoothSensor 2s TCC delay) still show as inactive on a query, which is acceptable. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Instead of having the user pick a reading key from the static list and then type an SSID by hand, the Wi-Fi sensor now gets its own network picker in CreateRuleView. When the modal opens (or the Wi-Fi sensor is selected), a one-shot CoreWLAN scan runs in the background and populates a dropdown with nearby SSIDs. Key behaviour: - readingKey is always forced to "ssid"; comparand = selected SSID name - The currently connected network is highlighted with a wifi icon and "(connected)" label so it is easy to identify - The scan result also includes the live SSID from the sensor snapshot and (when editing) the existing comparand, so the picker always has a sensible selection even when a network is not currently nearby - "Scan again" button lets the user re-scan without reopening the sheet - Negate hint explains that the flag inverts to "not connected to X" - operatorID is auto-seeded to "equals" when a network is chosen Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the generic Negate toggle + operator picker + comparand field with a simple two-option radio picker for WiFi SSID rules. The user sees only 'Connected' or 'Disconnected'; the negate flag is wired behind the scenes so the data model is unchanged. - wifiConditionSection: radio picker bound to negate via a computed Binding (connected = negate false, disconnected = negate true) - canSave: WiFi SSID rules skip the operatorID check since equals is always seeded automatically when a network is chosen - rulePreview: shows 'Wi-Fi: Connected to "X"' or 'Disconnected from' instead of the raw 'NOT ssid equals' form - wifiNetworkPicker caption updated to reference the Condition section Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…k refresh()
Verification findings:
WiFiSensor - correctly subscribed:
- CWEventType: ssidDidChange, bssidDidChange, linkDidChange, modeDidChange
- NotificationCenter mirrors for all four (belt-and-suspenders on macOS 14)
- refresh() overridden: re-fetches CWInterface and calls refreshSnapshot()
NetworkLinkSensor - correctly subscribed:
- SCDynamicStore watches State:/Network/Interface/.*/Link (regex pattern)
- CFRunLoopSource on main run loop fires refreshSnapshot() on any link change
- refresh() was missing (inherited no-op) -- now overrides to call refreshSnapshot()
Gap found in both: no sleep/wake handling. After sleep, interfaces
re-establish and the SCDynamicStore / CWWiFiClient callbacks will fire
eventually, but there is a stale-data window between wake and the first
hardware event. Fixed by:
- Backend: subscribes to NSWorkspace.didWakeNotification and calls
sensorCoordinator.refreshAllSensors() so all sensors re-read current
hardware state immediately on wake
- NetworkLinkSensor: override refresh() to call refreshSnapshot() so
the wake-triggered refreshAllSensors() actually re-reads link state
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs found: 1. object: client filter silently drops all CoreWLAN notifications. On macOS 12+, CWWiFiClient posts notifications with the CWInterface as the object, not the CWWiFiClient itself. Using object: client in the NotificationCenter observers meant every SSID/link change was silently filtered out. Fixed by changing to object: nil. 2. CWWiFiClient notifications are unreliable on modern macOS. Added NWPathMonitor (Network.framework) as a reliable backbone. NWPathMonitor fires immediately on start with current state and again whenever the WiFi path changes (connect, disconnect, interface going down). pathUpdateHandler dispatches to main queue and calls refreshSnapshot() so the sensor always reflects reality. The two mechanisms are complementary: - NWPathMonitor: reliable for connect/disconnect path changes - CWWiFiClient notifications (now unfiltered): for SSID roaming and other fine-grained events that don't change the network path Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…while open Rename: - PreferencesView -> SettingsView (file + type) - PreferencesWindowController -> SettingsWindowController (file + type) - Window title: 'ControlPlane' -> 'ControlPlane Settings' - Menu item: 'Preferences...' -> 'Settings...' (Cmd+, shortcut unchanged) - Selector: openPreferences -> openSettings Dock icon / Cmd+Tab: - SettingsWindowController.show() calls NSApp.setActivationPolicy(.regular) before makeKeyAndOrderFront so the Dock icon and Cmd+Tab entry are present from the very first visible frame - windowWillClose() reverts to NSApp.setActivationPolicy(.accessory) so the app disappears from the Dock and switcher once Settings is closed - LSUIElement in Info.plist is unchanged; the policy is toggled at runtime Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The flyout was referencing the old ProfileAction model; rewrote rebuildRunActionsMenu() to join ProfileActionLinks with Actions and Profiles, and runActionItem(_:) to execute via ProfileActionLink. Also updated the Combine subscription to observe actions, profileActionLinks, profiles, and actionTypes together. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Actions without a profile link appear under a Standalone section and run immediately with a placeholder profile context. Linked actions retain their profile/trigger grouping. Renames runActionItem to runLinkedActionItem and adds runStandaloneActionItem. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
One entry per action, sorted by name, with type in parentheses. No profile/trigger grouping — any action can be run on demand. Subscription narrowed to store.$actions + store.$actionTypes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
USBSensor now emits one boolean reading per connected device using the product name as the label and lowercase hex vid:pid as the key. Duplicate names are disambiguated with the ID in parentheses. Watched-but-disconnected keys still emit boolean(false) for the rule engine. CreateRuleView adds a usbDevicePicker that mirrors the Bluetooth picker: a live dropdown of connected devices with a green/grey indicator, a Device ID confirmation label, and a manual fallback text field for devices not currently plugged in. The rule preview now shows the human-readable device name instead of the hex key. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…oggle isUSBDeviceRule routes to the same wifiConditionSection so the user picks Connected or Disconnected; the negate flag is set behind the scenes. canSave and rulePreviewText updated to match. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The USB sensor starts when Settings opens but store.snapshots may not have been updated yet when CreateRuleView appears. Call refreshSnapshots() when the USB sensor is selected (onAppear or onChange) so the picker has current device data, mirroring how WiFi triggers a scan on open. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Owner
Author
|
Superseded — all changes from this branch were merged directly into feature/525-gui-buildout and are included in PR #550. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #545
Summary
USBSensor now emits one
SensorReadingper connected device using the IOKit product name as the label and lowercase hexvvvv:ppppas the key. Devices with duplicate names get the ID appended in parentheses to disambiguate. Watched-but-disconnected keys still emitboolean(false)so the rule engine always has a value to evaluate.CreateRuleView replaces the raw hex text field for the USB sensor with a
usbDevicePicker— the same pattern as the Bluetooth and Bonjour pickers:vendorID:productIDkey intoreadingKeyautomaticallyRule preview now shows the human-readable device name instead of the raw hex key.
Test plan
🤖 Generated with Claude Code