Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 151 additions & 46 deletions src/content/docs/gcs/User Interface/UI Components/error-toast.mdx
Original file line number Diff line number Diff line change
@@ -1,49 +1,41 @@
---
title: Error Toast
title: Alert Toast
---

import { Aside } from '@astrojs/starlight/components';

<Aside type="caution" title="This feature is not complete!">
While this component has been merged into main, it is still missing backend integration and may be highly subject to change.
</Aside>

The `ErrorToast.vue` component is used to display [errors and warnings](#errors-and-warnings) generated by abnormal vehicle or mission statuses.
The `AlertToast.vue` component is used to display [errors and warnings](#errors-and-warnings) generated by abnormal vehicle or mission statuses.

This component uses the [Button](https://www.shadcn-vue.com/docs/components/button.html) and [Sonner](https://www.shadcn-vue.com/docs/components/sonner.html) components from [shadcn/vue](https://www.shadcn-vue.com/docs/introduction.html).

## Features

- Row of placeholder buttons to generate toasts.
- This component is not complete as it is missing backend integration. To trigger errors/warnings, we have test buttons at the button of the screen.
- [Toasts sync between screens](#toast-synchronization).
- [Toasts position themselves based on the current screen](#toast-positioning).
- Toasts don't [expire](#toast-expiration) nor have a [limit](#toast-limit).
- [Backend Integration](#backend-integration).
- Row of placeholder buttons to generate/test toasts.


*MISSING: Toasts do not persist through page refreshes. This can be added when this component is integrated with the backend.*

### Toast Synchronization

Because we have two application windows or screens, we need a way to synchronize the toasts and interactions between them. Since we do not have backend integration for toasts yet, the current solution is to use Tauri listeners/emitters. This approach will very likely be replaced in the future.
Because we have two application windows or screens, we need a way to synchronize the toasts and interactions between them. The system uses Tauri listeners/emitters to ensure alerts appear consistently across all windows.

As mentioned earlier, these toasts do not persist on page refreshes. If you refresh one screen/page, the toasts will be out of sync.

First, we need a data structure to store the toasts. Just for our purposes, we can use `Maps` which offer fast lookups, flexible types, and preserved order of the added toasts.

```js
// src/components/ErrorToast.vue
const toastMap = new Map<string, string>();
var id: String;
```
#### Event System

Next, setup the listeners for each event-- creating toast, dismissing toast, and dismissing all toasts.
First, we set up the listeners for each event: creating toast, dismissing toast, and dismissing all toasts.

When creating the toast, we create a Dismiss button under the `action` property.

Do note that Dismiss All is emitted by a test button, opposed to a button that is located on the toast itself like the regular Dismiss.
Do note that Dismiss All is emitted by a test button, as opposed to a button that is located on the toast itself like the regular Dismiss.

The AlertToast.vue component listens for three events:


```js
// src/components/ErrorToast.vue
// src/components/AlertToast.vue
listen("create-toast", (event) => {
const { id, type, title, description } = event.payload as {
id: string;
Expand All @@ -52,59 +44,58 @@ listen("create-toast", (event) => {
description: string;
};

const toastId = toast[type](title, {
toast[type](title, {
id,
description,
duration: Infinity,
action: { label: "Dismiss", onClick: () => emit("dismiss-toast", { id }) } // Dismiss button on toast
action: { label: "Dismiss", onClick: () => emit("dismiss-toast", { id }) }
});

toastMap.set(id, String(toastId));
});

listen("dismiss-toast", (event) => {
const { id } = event.payload as { id: string };
toast.dismiss(id);
console.log(`Alert cleared: ${id}`);
});

listen("dismiss-all-toasts", (event) => {
listen("dismiss-all-toasts", () => {
toast.dismiss();
console.log(`All toast cleared`);
});
```

Lastly, setup the test buttons to emit the events. Here's one button as an example.

```vue
// src/components/ErrorToast.vue
// src/components/AlertToast.vue
<Button
variant="outline"
@click="
() => {
id = generateId();
emit('create-toast', {
id,
type: 'error',
title: 'Error: Connection Failure',
description: `Unable to connect to \${vehicle}`
});
}
"
>
variant="outline"
@click="
() => {
emit('create-toast', {
id: 'test_connection_error',
type: 'error',
title: 'Error: Connection Failure',
description: 'Unable to connect to ERU (test)'
});
}
"
>
Connection Error
</Button>
```


### Toast Positioning

We do not want the toasts to cover the sidebars, which contain critical information. Thus, we position the `ErrorToast` component based on which screen it is.
We do not want the toasts to cover the sidebars, which contain critical information. Thus, we position the `AlertToast` component based on which screen it is.

On the `Camera Screen`, the toasts will be positioned at the bottom-right. On the `Over View` screen, the toasts will be positioned at the bottom-left.

In the toast component, we retrieve the route and then set `toasterPosition` as the result of the computation.

```js
// src/components/ErrorToast.vue
// src/components/AlertToast.vue
const route = useRoute();

const toasterPosition = computed(() => {
Expand All @@ -115,7 +106,7 @@ const toasterPosition = computed(() => {
Then under `<template>`, add the position property.

```js
// src/components/ErrorToast.vue
// src/components/AlertToast.vue
<template>
<Toaster richColors :position="toasterPosition" />
...
Expand Down Expand Up @@ -145,7 +136,8 @@ By default, the Sonner component has a set expiration and limit for the toasts.
Under an event listener that creates a toast, we set the `duration` property to `Infinity`.

```ts
// src/components/ErrorToast.vue
// src/components/AlertToast.vue

listen("create-toast", (event) => {
...

Expand Down Expand Up @@ -191,8 +183,6 @@ function dispatch(action: Action) {
...
```

#### Implementation

## Errors and Warnings

Here is the list of Errors or Warnings that can be generated.
Expand All @@ -208,6 +198,121 @@ Weak signal integrity/connection lost to [vehicle]!
- **Warning: Vehicle Proximity**
[Vehicle 1] and [Vehicle 2] are within [distance] ft of each other!

## Backend Integration

The alert system is fully integrated with the backend through a reactive monitoring architecture. When telemetry data is received from RabbitMQ and processed by the Rust backend, it flows through TauRPC to update the `TelemetryStore` (Pinia).
The `StoresSync` module subscribes to telemetry state changes and triggers the `checkAlerts()` function from the `alertMonitoring` module on every update.
This module evaluates all vehicle conditions against configured thresholds and emits Tauri events `(create-toast, dismiss-toast)` when alerts are triggered or resolved.
The AlertToast.vue component listens for these events and displays the corresponding toasts.

### Alert Monitoring Module

The `src/lib/alertMonitoring.ts` module contains all alert detection logic:

#### Configurable Thresholds

```typescript
// src/lib/alertMonitoring.ts
export const ALERT_THRESHOLDS = {
SIGNAL_STRENGTH: -70, // dBm
LOW_BATTERY: 20, // percent
PROXIMITY: 100, // feet
};
```

Adjust these values to change alert sensitivity.

#### Debouncing Configuration

```typescript
// src/lib/alertMonitoring.ts
const ALERT_UPDATE_DEBOUNCE = 3000; // 3 seconds
```

This prevents excessive toast updates for the same alert. If a condition persists, the alert will update every 3 seconds with the latest data.
Increase/decrease this value to adjust update frequency

#### Example: Signal Strength Alert

```typescript
// src/lib/alertMonitoring.ts
const checkSignalStrength = (vehicle: string, signalStrength: number): void => {
if (signalStrength < ALERT_THRESHOLDS.SIGNAL_STRENGTH) {
emitAlert(
vehicle,
"signal_strength",
"warning",
"Warning: Signal Integrity",
`Weak signal integrity/connection lost to ${vehicle}!`
);
} else {
clearAlert(vehicle, "signal_strength");
}
};
```

### Alert Detection Flow

1. **Telemetry Update**: Backend receives new telemetry data from RabbitMQ
2. **Store Update**: TauRPC emits `on_updated` event → TelemetryStore syncs
3. **Subscription Trigger**: StoresSync detects telemetry state change
4. **Condition Check**: `alertMonitoring.checkAlerts()` evaluates all conditions
5. **Alert Emission**: If condition met → emit `create-toast` event
6. **Toast Display**: AlertToast.vue receives event → displays toast


### Integration with StoresSync

In `src/lib/StoresSync.ts`, the telemetry store is monitored for changes:

```typescript
// src/lib/StoresSync.ts
import { checkAlerts } from "./alertMonitoring";

export const establishTaurpcConnection = () => {
// ... existing setup ...

telemetryStore = telemetryPiniaStore();

// Subscribe to telemetry changes and check for alerts
telemetryStore.$subscribe((mutation, state) => {
checkAlerts(state.telemetryState);
});
};
```

### Alert Deduplication and Updates

#### Key-Based System

Each alert has a unique key generated from the vehicle and alert type:

```typescript
// src/components/AlertToast.vue
const getAlertKey = (vehicle: string, type: string): string => {
return `${vehicle}_${type}`; // e.g., "ERU_signal_strength"
};
```

This key serves two purposes:
1. **Deduplication**: Prevents multiple toasts for the same condition
2. **Toast ID**: Used by vue-sonner to update existing toasts

#### How Vue-Sonner Handles Updates

When an alert is emitted with the same ID as an existing toast:
- Vue-sonner **updates** the existing toast in-place
- The toast does **not** disappear and reappear
- Content smoothly transitions to the new description

#### Important Behavior

If a user manually dismisses a toast using the "Dismiss" button, but the underlying condition still exists:
- The toast will **reappear** on the next telemetry update (after debounce period)
- This is intentional - alerts represent real conditions that need attention
- To permanently dismiss: **resolve the underlying condition** (e.g., improve signal strength)


## Gallery

![Error Toast](../assets/Error-Toast.webp)