Skip to content

Add BLE support - #46

Merged
heavyrubberslave merged 13 commits into
mainfrom
feat/ble-support
Jul 4, 2026
Merged

heavyrubberslave merged 13 commits into
mainfrom
feat/ble-support

Conversation

@heavyrubberslave

@heavyrubberslave heavyrubberslave commented Jun 27, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Added debounced text inputs for device attributes and optional chart sizing (width/height).
    • Improved device support and UI richness: firmware/RSSI “signal strength”, “connected since”, and updated device icon mappings.
    • Added a dedicated device-notification flow and richer, event-specific automation handling.
  • Bug Fixes

    • Improved real-time device lifecycle handling: device disconnect now clears affected data stores and refreshes settings asynchronously.
    • Improved device attribute reconciliation to keep values in sync.
  • Chores

    • CI/release now installs strictly from the lockfile; linting/formatting and test quote consistency were updated.

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@heavyrubberslave, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 36 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c73b46df-4536-44cf-975c-58c296c45fcc

📥 Commits

Reviewing files that changed from the base of the PR and between b5be4b7 and 5001019.

📒 Files selected for processing (3)
  • src/components/device/DebouncedTextField.vue
  • src/shims-vue.d.ts
  • src/vueSocketIOClient.d.ts
📝 Walkthrough

Walkthrough

This PR adds typed Socket.IO contracts and related app/store wiring, introduces new device models and controls, and updates CI/lint/tooling plus broad quote-format normalization across the frontend.

Changes

Frontend feature and typing changes

Layer / File(s) Summary
Typed socket contract and plugin wiring
src/types/socket.ts, src/plugins/vueSocketIOClient.ts, src/shims-vue.d.ts, src/vueSocketIOClient.d.ts
Defines shared socket event types and switches the Vue Socket.IO plugin to AppSocket with a required-socket composable.
DeviceCommunicator uses AppSocket
src/helper/DeviceCommunicator.ts
Updates the communicator to accept AppSocket and removes timeout-driven update handling.
Device models and attribute helpers
src/model/devices/Device.ts, src/model/devices/airotic/DeviceAirotic.ts, src/utils/utils.ts
Adds FloatDeviceAttribute, WirelessDevice, DeviceAirotic, deepMergeReactive, and new device attribute type guards.
Device controls and shared input behavior
src/components/DeviceInfo.vue, src/components/icons/DeviceIcon.vue, src/components/device/DebouncedTextField.vue, src/components/device/control/**/*.vue
Adds debounced text input and Airotic control, updates generic/device-specific controls, and changes icon/info rendering for the new device/type model.
Store updates for devices, health, notifications, and automation
src/stores/deviceNotifications.ts, src/stores/devices.ts, src/stores/health.ts, src/stores/automation.ts, src/stores/app.ts, src/stores/backend.ts, src/stores/settings.ts
Adds notification state, changes device merging and clearing, retargets health store socket typing, and rewrites the automation template to explicit event handlers.
App.vue socket listener wiring
src/App.vue
Registers connect, disconnect, device lifecycle, notification, console-log, and settings-refresh handlers with optional chaining.
Chart and Monaco typings
src/components/chart/StreamLineChart.vue, src/helper/ChartHelper.ts, src/components/automation/MonacoEditor.vue
Adds chart sizing props and updates Monaco extra typings for notification events and typed overloads.
Single-quote formatting normalization
many components, views, stores, models, plugins, and config files
Normalizes string/import quoting and related formatting without changing runtime behavior in those spans.
CI, lint, and dependency configuration updates
.github/workflows/*, eslint.config.ts, package.json, tsconfig.app.json
Switches installs to frozen lockfile mode, replaces prettier-based lint setup, removes prettier packages, adds vue-color, and drops baseUrl from tsconfig.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Backend
  participant AppSocket
  participant App
  participant DevicesStore
  participant DeviceNotificationsStore
  participant SettingsStore

  Backend-->>AppSocket: connect / device events / settingsChanged
  AppSocket-->>App: optional-chained listeners
  App->>DevicesStore: add/remove/clear/update device state
  App->>DeviceNotificationsStore: dispatch/remove/clear notifications
  App->>SettingsStore: getServerSettings()
Loading

Possibly related PRs

Suggested labels: minor

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title says BLE support, but the diff is dominated by socket typing, device model refactors, and new device controls with no clear BLE-specific change. Rename it to reflect the main change, e.g. "Add Airotic device support and socket/type refactors".
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ble-support

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@heavyrubberslave
heavyrubberslave marked this pull request as ready for review July 4, 2026 10:58

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/helper/DeviceCommunicator.ts (1)

8-29: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Restore the receiveUpdates suppression around optimistic updates.
setAttribute() now emits immediately, but src/stores/devices.ts:updateDevice() still deep-merges incoming payloads whenever receiveUpdates is true. Since nothing flips that flag off here anymore, a fast server echo can overwrite the just-set local value and cause flicker until the device catches up.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/helper/DeviceCommunicator.ts` around lines 8 - 29, The optimistic update
path in DeviceCommunicator.setAttribute is missing the receiveUpdates
suppression, so a fast server echo can still be merged back by updateDevice in
src/stores/devices.ts and overwrite the local value. Restore the temporary
receiveUpdates=false/true guard around the immediate emit in setAttribute, and
make sure the existing receiveUpdates check in updateDevice continues to skip
incoming echoes while the optimistic local update is in flight.
src/stores/health.ts (1)

62-94: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Avoid stacking healthMetrics listeners on reconnect. App.vue calls healthStore.init(io) on every 'connect', and init() adds a new socket.on('healthMetrics', ...) handler each time. Remove the previous listener before registering a new one, or register this handler once outside the reconnect path, to prevent duplicate chart updates and a growing listener leak.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/stores/health.ts` around lines 62 - 94, The health metrics socket handler
is being registered repeatedly because init() adds a new
socket.on('healthMetrics', ...) listener every time App.vue reconnects. Update
init(socket) in health store to either remove any existing healthMetrics
listener before attaching a new one, or make the listener registration happen
only once so state.value and chartData updates do not fire multiple times after
reconnects. Use the init function and the healthMetrics event handler as the
main touchpoints.
🧹 Nitpick comments (2)
src/components/device/DebouncedTextField.vue (1)

41-64: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

No cleanup of debounceTimer on unmount.

If the component unmounts while a debounce timer is pending, emitValue fires after unmount (harmless no-op, but wasteful and inconsistent with clean teardown). Consider clearing the timer in onBeforeUnmount.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/device/DebouncedTextField.vue` around lines 41 - 64, The
DebouncedTextField component leaves a pending debounce timeout alive after
unmount, so clear it during teardown to avoid `emitValue` running after the
component is destroyed. Update the `DebouncedTextField.vue` logic around
`debounceTimer` by adding cleanup in `onBeforeUnmount`, and make sure any
pending timer is cancelled alongside the existing timeout handling in the
debounced emit flow.
src/components/automation/MonacoEditor.vue (1)

61-96: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

DeviceEventType/DeviceEvent look unused after the onEvent overload rework.

The new onEvent overloads pass Device (and Device, DeviceNotification) directly rather than a DeviceEvent wrapper, leaving DeviceEventType/DeviceEvent as dead ambient types in the injected facts.d.ts for automation scripts. Low impact (editor-only typings) but worth pruning for clarity.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/automation/MonacoEditor.vue` around lines 61 - 96, Prune the
dead ambient event typings in MonacoEditor.vue by removing the unused
DeviceEventType and DeviceEvent declarations from the injected facts.d.ts block,
since onEvent now passes Device directly (and Device, DeviceNotification for
notifications). Keep the remaining DeviceAttribute and onEvent overloads intact,
and make sure only the symbols still referenced by automation scripts remain
declared.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/App.vue`:
- Around line 36-39: The disconnect handler in App.vue clears devicesStore but
leaves deviceNotificationsStore state behind, so stale notifications can survive
reconnects. Update the io?.on('disconnect') callback to also reset
deviceNotificationsStore, using the new clear/remove action proposed in
deviceNotificationsStore so its latest map is emptied alongside
devicesStore.clear().
- Line 34: The call to healthStore.init in App.vue is passing a possibly null
AppSocket, which does not match the expected non-null type. Narrow io before
invoking healthStore.init, or use a non-null assertion only if the socket is
guaranteed to exist at that point, and make sure the init call in the App
component only runs with a valid AppSocket instance.

In `@src/components/automation/CreateForm.vue`:
- Around line 14-29: The script name validation in scriptNameRules is rejecting
exactly 64-character names because the length check uses a strict less-than
comparison. Update the length rule in CreateForm.vue so the script name
validator allows values up to and including 64 characters, keeping the existing
error message and the rest of the rules unchanged.

In `@src/components/device/control/DeviceAiroticControl.vue`:
- Around line 41-52: The Cancel flow in DeviceAiroticControl.vue only closes the
menus and leaves the live v-model state in localRestColor and localBreathInColor
unchanged. Update the cancel/dismiss handling for the restColorMenu and
breathInColorMenu dialogs to restore those refs from the device’s current color
before setting the menu closed, so reopening the picker shows the actual device
value. Apply the same reset behavior in the related rest/breath-in color
handlers and keep applyRestColor/applyBreathInColor using the refreshed local
refs.
- Around line 157-163: The color trigger blocks in DeviceAiroticControl.vue are
click-only divs, so make the rest color and breath-in triggers keyboard
accessible by treating them as buttons. Add button semantics and focusability to
the trigger containers, and wire keyboard activation (Enter/Space) to the same
handlers that set restColorMenu or the breath-in dialog state; reuse the
existing trigger markup around restColorCss and the breath-in equivalent so both
behave consistently.
- Around line 23-27: The parseColor helper in DeviceAiroticControl.vue does not
handle malformed color strings because Number conversion can produce NaN and the
current nullish fallback will not replace it. Update parseColor to validate each
parsed component after splitting the value, and default any non-finite or
missing channel to 0 before returning the { r, g, b } object. Keep the fix
localized to parseColor so callers that rely on its output never receive rgb
values with NaN components.
- Around line 88-95: The `watch(latestNotification, ...)` in
`DeviceAiroticControl` only updates `breathState` on future notifications, so it
can be stale on mount if the store already contains a matching `colorChange`
event. Update the component to initialize `breathState` from the current
`latestNotification` (or use an immediate watch) so the existing notification
state is applied as soon as the control panel loads, while keeping the current
`colorChange`/`breathInColor` mapping logic.

In `@src/components/device/control/virtual/DeviceVirtualTtsControl.vue`:
- Around line 57-64: Restore the dynamic queuing label in
DeviceVirtualTtsControl.vue by using the existing queuing state instead of the
hardcoded switch text. Update the v-switch label to reflect
props.device.attributes.queuing.value via the removed queuingLabel computed
logic (or an equivalent computed/helper), and keep changeQueuing tied to the
same switch so the label always matches the current enabled/disabled state.

In `@src/components/device/DebouncedTextField.vue`:
- Around line 17-19: The value emitted from DebouncedTextField remains a string
for number inputs, which causes downstream device attribute updates to receive
the wrong type. Update emitValue() in DebouncedTextField.vue to detect
props.type === 'number' and coerce the value to a number before calling
emit('update:modelValue', ...). Keep the change localized to the
DebouncedTextField component so GenericDeviceControl.vue continues to receive
correctly typed int/float values.

In `@src/shims-vue.d.ts`:
- Around line 1-8: The $socket type is declared inconsistently as Socket/typeof
Socket while the plugin actually provides an AppSocket instance, and the
augmentation is split across multiple declaration files. Update the
ComponentCustomProperties.$socket declaration to use AppSocket and consolidate
the Vue module augmentation into a single place, referencing the existing
AppSocket and ComponentCustomProperties symbols so this.$socket exposes the
correct event contracts everywhere.

In `@src/stores/deviceNotifications.ts`:
- Around line 16-33: The deviceNotifications store has no way to remove stale
entries, so add a cleanup action in useDeviceNotificationsStore (such as clear()
and/or remove(deviceId)) alongside dispatch and getLatest, and make App.vue call
it from the disconnect/deviceDisconnected flow just like devicesStore.clear().
Ensure the new action deletes outdated latest records so getLatest() cannot
return notifications for disconnected devices or future deviceId re-use.

In `@src/utils/utils.ts`:
- Around line 55-57: `isStringDeviceAttribute` is misnamed and has an incorrect
type predicate because it returns true for `float` and `int` attributes as well
as `str`. Update the guard in `utils.ts` so `isStringDeviceAttribute` only
matches the `str` type, or otherwise rename/refactor it to reflect the broader
intent if it is meant to cover all value-bearing attributes. Keep the predicate
aligned with its declared return type `StrDeviceAttribute` and the callers in
`GenericDeviceControl.vue`.

In `@src/views/SettingsView.vue`:
- Around line 45-58: The SettingsView.vue editor setup in storeEditorInstance()
adds a global window resize handler but never cleans it up, which can leave
stale listeners active after the view is left. Keep a reference to the resize
callback used in the editor.layout logic, and register a matching removal in
onBeforeUnmount() so the handler is unregistered when the component unmounts.
Make sure the cleanup is tied to the same handler instance created in
storeEditorInstance() to avoid stacking listeners on repeated visits.

---

Outside diff comments:
In `@src/helper/DeviceCommunicator.ts`:
- Around line 8-29: The optimistic update path in
DeviceCommunicator.setAttribute is missing the receiveUpdates suppression, so a
fast server echo can still be merged back by updateDevice in
src/stores/devices.ts and overwrite the local value. Restore the temporary
receiveUpdates=false/true guard around the immediate emit in setAttribute, and
make sure the existing receiveUpdates check in updateDevice continues to skip
incoming echoes while the optimistic local update is in flight.

In `@src/stores/health.ts`:
- Around line 62-94: The health metrics socket handler is being registered
repeatedly because init() adds a new socket.on('healthMetrics', ...) listener
every time App.vue reconnects. Update init(socket) in health store to either
remove any existing healthMetrics listener before attaching a new one, or make
the listener registration happen only once so state.value and chartData updates
do not fire multiple times after reconnects. Use the init function and the
healthMetrics event handler as the main touchpoints.

---

Nitpick comments:
In `@src/components/automation/MonacoEditor.vue`:
- Around line 61-96: Prune the dead ambient event typings in MonacoEditor.vue by
removing the unused DeviceEventType and DeviceEvent declarations from the
injected facts.d.ts block, since onEvent now passes Device directly (and Device,
DeviceNotification for notifications). Keep the remaining DeviceAttribute and
onEvent overloads intact, and make sure only the symbols still referenced by
automation scripts remain declared.

In `@src/components/device/DebouncedTextField.vue`:
- Around line 41-64: The DebouncedTextField component leaves a pending debounce
timeout alive after unmount, so clear it during teardown to avoid `emitValue`
running after the component is destroyed. Update the `DebouncedTextField.vue`
logic around `debounceTimer` by adding cleanup in `onBeforeUnmount`, and make
sure any pending timer is cancelled alongside the existing timeout handling in
the debounced emit flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7e43deb1-95a6-4ba2-ba45-051667d3e8fb

📥 Commits

Reviewing files that changed from the base of the PR and between 52800b4 and eeb38f4.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (81)
  • .github/workflows/release.yml
  • .github/workflows/test.yml
  • cypress/integration/example.spec.ts
  • cypress/support/index.ts
  • eslint.config.ts
  • package.json
  • src/App.vue
  • src/components/DeviceInfo.vue
  • src/components/ServerStatusOverlay.vue
  • src/components/__tests__/HelloWorld.spec.ts
  • src/components/automation/CreateForm.vue
  • src/components/automation/LogViewer.vue
  • src/components/automation/MonacoEditor.vue
  • src/components/chart/StreamLineChart.vue
  • src/components/device/DebouncedSlider.vue
  • src/components/device/DebouncedTextField.vue
  • src/components/device/DeviceCard.vue
  • src/components/device/DeviceControl.vue
  • src/components/device/control/DeviceAiroticControl.vue
  • src/components/device/control/DeviceEstim2bControl.vue
  • src/components/device/control/DeviceZc95Control.vue
  • src/components/device/control/GenericDeviceControl.vue
  • src/components/device/control/slvctrlplus/DeviceAirValveControl.vue
  • src/components/device/control/slvctrlplus/DeviceDisplayControl.vue
  • src/components/device/control/slvctrlplus/DeviceDistanceControl.vue
  • src/components/device/control/slvctrlplus/DeviceEt312Control.vue
  • src/components/device/control/slvctrlplus/DeviceNogasmControl.vue
  • src/components/device/control/slvctrlplus/DeviceStrikerMk2Control.vue
  • src/components/device/control/virtual/DeviceVirtualDisplayControl.vue
  • src/components/device/control/virtual/DeviceVirtualPiperTtsControl.vue
  • src/components/device/control/virtual/DeviceVirtualRandomGeneratorControl.vue
  • src/components/device/control/virtual/DeviceVirtualTtsControl.vue
  • src/components/icons/DeviceIcon.vue
  • src/helper/ChartHelper.ts
  • src/helper/DeviceCommunicator.ts
  • src/helper/TimeoutHelper.ts
  • src/layouts/LayoutWithMenu.vue
  • src/main.ts
  • src/model/MapRule.ts
  • src/model/devices/Device.ts
  • src/model/devices/airotic/DeviceAirotic.ts
  • src/model/devices/estim2b/DeviceEstim2b.ts
  • src/model/devices/slvctrl/DeviceAirValve.ts
  • src/model/devices/slvctrl/DeviceDisplay.ts
  • src/model/devices/slvctrl/DeviceDistance.ts
  • src/model/devices/slvctrl/DeviceEt312.ts
  • src/model/devices/slvctrl/DeviceNogasm.ts
  • src/model/devices/slvctrl/DeviceStrikerMk2.ts
  • src/model/devices/virtual/VirtualDeviceDisplay.ts
  • src/model/devices/virtual/VirtualDevicePiperTts.ts
  • src/model/devices/virtual/VirtualDeviceTts.ts
  • src/model/devices/virtual/VirtualRandomGenerator.ts
  • src/model/devices/zc95/DeviceZc95.ts
  • src/plugins/vueSocketIOClient.ts
  • src/plugins/vuetify.ts
  • src/plugins/webfontloader.ts
  • src/router/index.ts
  • src/shims-vue.d.ts
  • src/shims-vuetify.d.ts
  • src/stores/app.ts
  • src/stores/automation.ts
  • src/stores/backend.ts
  • src/stores/deviceNotifications.ts
  • src/stores/devices.ts
  • src/stores/health.ts
  • src/stores/settings.ts
  • src/types/socket.ts
  • src/types/utility.ts
  • src/utils/apiFetch.ts
  • src/utils/utils.ts
  • src/views/AutomationView.vue
  • src/views/BackendUrl.vue
  • src/views/DevicesView.vue
  • src/views/MissionControlSingleView.vue
  • src/views/MissionControlView.vue
  • src/views/SettingsView.vue
  • src/views/SystemHealthView.vue
  • src/vueSocketIOClient.d.ts
  • tsconfig.app.json
  • vite.config.ts
  • vue.config.js
💤 Files with no reviewable changes (2)
  • src/helper/TimeoutHelper.ts
  • tsconfig.app.json

Comment thread src/App.vue
Comment thread src/App.vue
Comment thread src/components/automation/CreateForm.vue
Comment thread src/components/device/control/DeviceAiroticControl.vue
Comment thread src/components/device/control/DeviceAiroticControl.vue
Comment thread src/components/device/DebouncedTextField.vue
Comment thread src/shims-vue.d.ts
Comment thread src/stores/deviceNotifications.ts
Comment thread src/utils/utils.ts
Comment thread src/views/SettingsView.vue
@heavyrubberslave heavyrubberslave added the minor Creates a new minor release if merged label Jul 4, 2026
@heavyrubberslave
heavyrubberslave merged commit 3145643 into main Jul 4, 2026
2 checks passed
@heavyrubberslave
heavyrubberslave deleted the feat/ble-support branch July 4, 2026 16:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

minor Creates a new minor release if merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant