Skip to content

frontend: show snackbar notifications on parameter changes#3868

Open
Williangalvani wants to merge 1 commit intobluerobotics:masterfrom
Williangalvani:parameter_snackbar
Open

frontend: show snackbar notifications on parameter changes#3868
Williangalvani wants to merge 1 commit intobluerobotics:masterfrom
Williangalvani:parameter_snackbar

Conversation

@Williangalvani
Copy link
Copy Markdown
Member

@Williangalvani Williangalvani commented Apr 8, 2026

changes snackbar to a stacking-alerters layout, showing up to 5 simultaneously.
Shows ALL parameter changes, but only when in developer mode

Screenshot 2026-04-08 at 15 47 47

Summary by Sourcery

Introduce stacked alert notifications in the frontend and emit developer-mode messages for autopilot parameter changes once all parameters are loaded.

New Features:

  • Display multiple alert messages concurrently in a centered stacked layout instead of a single snackbar.
  • Emit info-level messages for parameter value changes when all parameters are loaded and developer mode is enabled.

Enhancements:

  • Refine parameter loading status checks via an allParametersLoaded helper in ParameterFetcher.
  • Extend the message manager with support for removing previously registered callbacks.
  • Report parameter updates with old values only when the value actually changes.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Apr 8, 2026

Reviewer's Guide

Refactors the global Alerter component from a single snackbar to a stack of dismissible alerts, introduces queueing and eviction logic for up to five concurrent messages, and wires parameter updates to emit dev‑mode notifications once all parameters are loaded, including minimal support in the parameter table and message manager.

Sequence diagram for dev-mode parameter change notifications

sequenceDiagram
  actor Developer
  participant Autopilot
  participant ParameterFetcher
  participant ParametersTable
  participant MessageManager
  participant Alerter

  Developer->>Autopilot: Change parameter value
  Autopilot-->>ParameterFetcher: PARAM_VALUE message

  ParameterFetcher->>ParametersTable: updateParam(param_name, trimmed_value)
  ParametersTable-->>ParameterFetcher: ChangeInfo or null

  ParameterFetcher->>ParameterFetcher: allParametersLoaded()
  alt ChangeInfo not null AND allParametersLoaded AND dev mode
    ParameterFetcher->>MessageManager: emitMessage(Info, formatted message)
    MessageManager->>Alerter: callback(level, msg)
    Alerter->>Alerter: showAlert(entry)
    Alerter->>Developer: Display alert in stack
  else No notification conditions met
    note over ParameterFetcher: No message emitted
  end

  opt Auto timeout
    Alerter->>Alerter: dismiss(id)
  end

  opt Manual dismiss
    Developer->>Alerter: Click dismiss
    Alerter->>Alerter: dismiss(id)
  end
Loading

Class diagram for updated parameter notifications and Alerter stack

classDiagram

class MessageLevel {
  <<enumeration>>
  Success
  Error
  Warning
  Info
  Critical
}

class AlertEntry {
  +number id
  +MessageLevel level
  +string message
}

class Alerter {
  +AlertEntry[] alerts
  +AlertEntry[] queue
  +showAlert(entry AlertEntry) void
  +dismiss(id number) void
  +promoteFromQueue() void
  +startDrain() void
  +stopDrain() void
  +alertType(level MessageLevel) string
  +getTimeout(level MessageLevel) number
}

class MessageManager {
  -Array~function~ callbacks
  +addCallback(callback function) void
  +removeCallback(callback function) void
  +emitMessage(level MessageLevel, msg string) void
}

class ParameterFetcher {
  -number loaded_params_count
  -number total_params_count
  -ParametersTable parameter_table
  +allParametersLoaded() boolean
  +reset() void
  +requestParamsWatchdog() void
  +handleParamUpdate(param_name string, param_value number, param_index number) void
}

class ParametersTable {
  -Record~number, Parameter~ parametersDict
  +updateParam(param_name string, param_value number) ChangeInfo
  +parameters() Parameter[]
}

class ChangeInfo {
  +number oldValue
}

class Parameter {
  +number id
  +string name
  +number value
}

Alerter ..> AlertEntry
Alerter ..> MessageLevel
MessageManager ..> MessageLevel
ParameterFetcher ..> ParametersTable
ParameterFetcher ..> MessageManager
ParameterFetcher ..> MessageLevel
ParametersTable ..> Parameter
ParametersTable ..> ChangeInfo
MessageManager --> Alerter : invokes callbacks
Loading

File-Level Changes

Change Details Files
Refactor Alerter from a single v-snackbar to a stacked, queue-backed v-alert system with auto-dismiss and draining.
  • Replace v-snackbar template with a fixed-position stack of v-alerts using v-slide-y-reverse-transition and basic spacing styles.
  • Introduce AlertEntry model, global id/callback/timer state, and component data for active alerts plus a queue.
  • Add methods to show alerts with per-level timeouts, dismiss alerts, promote queued alerts, and periodically evict timed alerts when the stack is full.
  • Map MessageLevel values to Vuetify v-alert types, consolidating Critical into error and defaulting to info.
core/frontend/src/components/app/Alerter.vue
Expose parameter loading completion status and use it to guard the parameter request watchdog and change notifications.
  • Add allParametersLoaded() helper on ParameterFetcher and use it in requestParamsWatchdog instead of duplicating the condition.
  • Ensure the watchdog early-returns when parameters are already all loaded but at least one has been fetched.
core/frontend/src/types/autopilot/parameter-fetcher.ts
Emit informational messages for parameter value changes in dev mode once all parameters are loaded.
  • Change updateParam to return the previous value when a real change occurs, or null when the parameter is not yet present or unchanged.
  • In ParameterFetcher.handleParamValue, call updateParam for index 65535, and when it reports a change, emit an Info-level message via message_manager if dev mode is enabled and all parameters are loaded.
  • Keep benign logging for updates received before the parameter table is populated.
core/frontend/src/types/autopilot/parameter-fetcher.ts
core/frontend/src/types/autopilot/parameter-table.ts
Allow message-manager callbacks to be removed to avoid leaks when components are destroyed.
  • Add removeCallback(callback) method that removes a previously registered callback from the internal callbacks array.
  • Use removeCallback from Alerter.beforeDestroy to unregister its message handler and stop the drain timer.
core/frontend/src/libs/message-manager.ts
core/frontend/src/components/app/Alerter.vue

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • The Alerter component relies on module-scoped nextId, boundCallback, and drainTimer, which can cause interference or incorrect cleanup if multiple instances are ever mounted; consider moving these into component instance state (data/created) so each instance manages its own subscription and timers.
  • In Alerter.showAlert, the setTimeout callbacks are never cleared when an alert is dismissed or the component is destroyed, so timeouts may fire after the component is gone; it would be safer to track these timers per-alert and clear them on dismiss/beforeDestroy.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The Alerter component relies on module-scoped `nextId`, `boundCallback`, and `drainTimer`, which can cause interference or incorrect cleanup if multiple instances are ever mounted; consider moving these into component instance state (`data`/`created`) so each instance manages its own subscription and timers.
- In `Alerter.showAlert`, the `setTimeout` callbacks are never cleared when an alert is dismissed or the component is destroyed, so timeouts may fire after the component is gone; it would be safer to track these timers per-alert and clear them on `dismiss`/`beforeDestroy`.

## Individual Comments

### Comment 1
<location path="core/frontend/src/components/app/Alerter.vue" line_range="38-47" />
<code_context>
+  message: string
+}
+
+let nextId = 0
+let boundCallback: ((level: MessageLevel, msg: string) => void) | null = null
+let drainTimer: ReturnType<typeof setInterval> | null = null
+
 export default Vue.extend({
   name: 'ErrorMessage',
   data() {
     return {
-      level: undefined as MessageLevel|undefined,
-      message: '',
-      show: false,
+      alerts: [] as AlertEntry[],
+      queue: [] as AlertEntry[],
+    }
+  },
+  mounted() {
+    boundCallback = (level: MessageLevel, message: string) => {
+      nextId += 1
+      const entry = { id: nextId, level, message }
+      if (this.alerts.length < MAX_VISIBLE) {
+        this.showAlert(entry)
+      } else {
+        this.queue.push(entry)
+        this.startDrain()
+      }
+    }
+    message_manager.addCallback(boundCallback)
+  },
+  beforeDestroy() {
+    if (boundCallback) {
+      message_manager.removeCallback(boundCallback)
</code_context>
<issue_to_address>
**issue (bug_risk):** Using module-level state for the callback and timer will behave incorrectly if multiple Alerter instances are mounted.

Because `boundCallback` and `drainTimer` are module-scoped, all `Alerter` instances share them. The last-mounted instance overwrites `boundCallback`, so when an earlier instance is destroyed its `beforeDestroy` calls `removeCallback` with the wrong reference. That instance’s callback remains registered, causing leaks and messages targeting a destroyed component.

Store these on the instance instead (e.g. `this.boundCallback`, `this.drainTimer`) so each component registers and cleans up its own callback and timer correctly.
</issue_to_address>

### Comment 2
<location path="core/frontend/src/components/app/Alerter.vue" line_range="93-101" />
<code_context>
+        this.stopDrain()
+      }
+    },
+    startDrain() {
+      if (drainTimer) return
+      drainTimer = setInterval(() => {
+        const evictIdx = this.alerts.findIndex((a) => this.getTimeout(a.level) > 0)
+        if (evictIdx !== -1) {
+          this.dismiss(this.alerts[evictIdx].id)
+        }
+      }, DRAIN_INTERVAL_MS)
+    },
+    stopDrain() {
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Queue drain logic can stall permanently when all visible alerts have no timeout.

In `startDrain`, eviction only targets alerts with `getTimeout(level) > 0`. If the visible `alerts` are all non-expiring (e.g. `Error`/`Critical`) and new alerts keep arriving, `evictIdx` stays `-1`, the timer runs indefinitely, the queue grows, and queued alerts never surface.

You may want a fallback (e.g. evict the oldest when the queue is non-empty, even if timeouts are 0), or clearly document that starving queued alerts in this scenario is intentional behavior.

```suggestion
    startDrain() {
      if (drainTimer) return
      drainTimer = setInterval(() => {
        const evictIdx = this.alerts.findIndex((a) => this.getTimeout(a.level) > 0)

        if (evictIdx !== -1) {
          // Prefer to evict alerts that have a timeout configured
          this.dismiss(this.alerts[evictIdx].id)
        } else if (this.queue.length > 0 && this.alerts.length > 0) {
          // Fallback: ensure the queue continues to drain even if all visible alerts are non-expiring
          // Evict the oldest visible alert to make room for queued ones
          this.dismiss(this.alerts[0].id)
        }
      }, DRAIN_INTERVAL_MS)
    },
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Emit an info-level snackbar when a MAVLink parameter value changes
after initial loading is complete, only in developer mode. Refactor
Alerter to render a stack of v-alert components so multiple
notifications are visible at once, with auto-dismiss timers and a cap
of 5 visible alerts. Persistent alerts (error/critical) are preserved
when evicting to stay within the cap.

Made-with: Cursor
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