fix: notification fixes, refactor and restart toast persistence
Rafael Uzarowski committed
Aug 1, 2025 at 16:42 UTC
9145c4d58da70cb45ca78c005122defae692946f
7 files changed
+78
-78
webui/components/notifications/notification-icons.html
+17
-15
@@ -1,26 +1,28 @@
1
<html>
2
<head>
3
<script type="module">
4
- import { store } from "/js/notificationStore.js";
4
+ import { store } from "/components/notifications/notification-store.js";
5
</script>
6
</head>
7
<body>
8
<div x-data>
9
- <!-- Notification Toggle Button -->
10
- <div class="notification-toggle"
11
- :class="{
12
- 'has-unread': $store.notificationStore.unreadCount > 0,
13
- 'has-notifications': $store.notificationStore.notifications.length > 0
14
- }"
15
- @click="$store.notificationStore.openModal()"
16
- title="View Notifications">
17
- <div class="notification-icon">
18
- 🔔
9
+ <template x-if="$store.notificationStore">
10
+ <!-- Notification Toggle Button -->
11
+ <div class="notification-toggle"
12
+ :class="{
13
+ 'has-unread': $store.notificationStore.unreadCount > 0,
14
+ 'has-notifications': $store.notificationStore.notifications.length > 0
15
+ }"
16
+ @click="$store.notificationStore.openModal()"
17
+ title="View Notifications">
18
+ <div class="notification-icon">
19
+ <span class="material-symbols-outlined">notifications</span>
20
+ </div>
21
+ <span x-show="$store.notificationStore.unreadCount > 0"
22
+ class="notification-badge"
23
+ x-text="$store.notificationStore.unreadCount"></span>
24
</div>
20
- <span x-show="$store.notificationStore.unreadCount > 0"
21
- class="notification-badge"
22
- x-text="$store.notificationStore.unreadCount"></span>
23
- </div>
25
+ </template>
26
</div>
27
</body>
28
</html>
webui/components/notifications/notification-modal.html
+15
-18
@@ -2,12 +2,7 @@
2
<head>
3
<title>Notifications</title>
4
<script type="module">
5
- import { store } from "/js/notificationStore.js";
6
-
7
- // Ensure notification store is available globally for the modal
8
- if (!window.notificationStore) {
9
- window.notificationStore = store;
10
- }
5
+ import { store } from "/components/notifications/notification-store.js";
6
</script>
7
</head>
8
<body>
@@ -16,24 +11,24 @@
11
<div class="modal-subheader">
12
<div class="notification-header-actions">
13
<button class="notification-action"
19
- @click="$store.notificationStore.clearAll()"
20
- :disabled="$store.notificationStore.getDisplayNotifications().length === 0"
14
+ @click="$store.notificationStore?.clearAll()"
15
+ :disabled="!$store.notificationStore || $store.notificationStore.getDisplayNotifications().length === 0"
16
title="Clear All">
22
- 🗑️ Clear All
17
+ <span class="material-symbols-outlined">delete</span> Clear All
18
</button>
19
</div>
20
</div>
21
22
<!-- Notifications List -->
28
- <div class="notification-list" x-show="$store.notificationStore.getDisplayNotifications().length > 0">
29
- <template x-for="notification in $store.notificationStore.getDisplayNotifications()" :key="notification.id">
23
+ <div class="notification-list" x-show="$store.notificationStore && $store.notificationStore.getDisplayNotifications().length > 0">
24
+ <template x-for="notification in ($store.notificationStore?.getDisplayNotifications() || [])" :key="notification.id">
25
<div class="notification-item"
26
x-data="{ expanded: false }"
32
- :class="$store.notificationStore.getNotificationItemClass(notification)"
33
- @click="$store.notificationStore.markAsRead(notification.id)">
27
+ :class="$store.notificationStore?.getNotificationItemClass(notification) || 'notification-item'"
28
+ @click="$store.notificationStore?.markAsRead(notification.id)">
29
30
<div class="notification-icon"
36
- x-html="$store.notificationStore.getNotificationIcon(notification.type)">
31
+ x-html="$store.notificationStore?.getNotificationIcon(notification.type) || ''">
32
</div>
33
34
<div class="notification-content">
@@ -45,7 +40,7 @@
40
x-text="notification.message">
41
</div>
42
<div class="notification-timestamp"
48
- x-text="$store.notificationStore.formatTimestamp(notification.timestamp)">
43
+ x-text="$store.notificationStore?.formatTimestamp(notification.timestamp) || notification.timestamp">
44
</div>
45
46
<!-- Expand Toggle Button (as last row element) -->
@@ -74,11 +69,13 @@
69
</div>
70
71
<!-- Empty State -->
77
- <div class="notification-empty" x-show="$store.notificationStore.getDisplayNotifications().length === 0">
78
- <div class="notification-empty-icon">🔔</div>
72
+ <div class="notification-empty" x-show="!$store.notificationStore || $store.notificationStore.getDisplayNotifications().length === 0">
73
+ <div class="notification-empty-icon">
74
+ <span class="material-symbols-outlined">notifications</span>
75
+ </div>
76
<p>No notifications to display</p>
77
<p style="font-size: 0.8rem; opacity: 0.7; margin-top: 0.5rem;"
81
- x-show="$store.notificationStore.notifications.length > 0">
78
+ x-show="$store.notificationStore && $store.notificationStore.notifications.length > 0">
79
All notifications have been read and are older than 5 minutes
80
</p>
81
</div>
webui/components/notifications/notification-store.js
renamed
+29
-13
@@ -306,16 +306,17 @@ const model = {
306
return `notification-item ${typeClass} ${readClass}`;
307
},
308
309
- // Get icon for notification type
309
+ // Get icon for notification type (Google Material Icons)
310
getNotificationIcon(type) {
311
const icons = {
312
- info: "ℹ️",
313
- success: "✅",
314
- warning: "⚠️",
315
- error: "❌",
316
- progress: "⏳"
312
+ info: "info",
313
+ success: "check_circle",
314
+ warning: "warning",
315
+ error: "error",
316
+ progress: "hourglass_empty"
317
};
318
- return icons[type] || "ℹ️";
318
+ const iconName = icons[type] || "info";
319
+ return `<span class="material-symbols-outlined">${iconName}</span>`;
320
},
321
322
// Create notification via backend (will appear via polling)
@@ -491,10 +492,10 @@ const model = {
492
const store = createStore("notificationStore", model);
493
494
// NEW: Global function for frontend error toasts (replaces toastFetchError)
494
-window.toastFrontendError = async function(message, title = "Connection Error") {
495
+window.toastFrontendError = async function(message, title = "Connection Error", display_time = 8, group = "") {
496
if (window.Alpine && window.Alpine.store && window.Alpine.store('notificationStore')) {
497
try {
497
- return await window.Alpine.store('notificationStore').frontendError(message, title);
498
+ return await window.Alpine.store('notificationStore').addFrontendToast('error', message, title, display_time, group);
499
} catch (error) {
500
console.error('Failed to create frontend error notification:', error);
501
// Fallback to console if something goes wrong
@@ -509,10 +510,10 @@ window.toastFrontendError = async function(message, title = "Connection Error")
510
};
511
512
// NEW: Additional global convenience functions
512
-window.toastFrontendWarning = async function(message, title = "Warning") {
513
+window.toastFrontendWarning = async function(message, title = "Warning", display_time = 5, group = "") {
514
if (window.Alpine && window.Alpine.store && window.Alpine.store('notificationStore')) {
515
try {
515
- return await window.Alpine.store('notificationStore').frontendWarning(message, title);
516
+ return await window.Alpine.store('notificationStore').addFrontendToast('warning', message, title, display_time, group);
517
} catch (error) {
518
console.error('Failed to create frontend warning notification:', error);
519
console.warn('Frontend Warning:', title, '-', message);
@@ -524,10 +525,10 @@ window.toastFrontendWarning = async function(message, title = "Warning") {
525
}
526
};
527
527
-window.toastFrontendInfo = async function(message, title = "Info") {
528
+window.toastFrontendInfo = async function(message, title = "Info", display_time = 3, group = "") {
529
if (window.Alpine && window.Alpine.store && window.Alpine.store('notificationStore')) {
530
try {
530
- return await window.Alpine.store('notificationStore').frontendInfo(message, title);
531
+ return await window.Alpine.store('notificationStore').addFrontendToast('info', message, title, display_time, group);
532
} catch (error) {
533
console.error('Failed to create frontend info notification:', error);
534
console.log('Frontend Info:', title, '-', message);
@@ -539,4 +540,19 @@ window.toastFrontendInfo = async function(message, title = "Info") {
540
}
541
};
542
543
+window.toastFrontendSuccess = async function(message, title = "Success", display_time = 3, group = "") {
544
+ if (window.Alpine && window.Alpine.store && window.Alpine.store('notificationStore')) {
545
+ try {
546
+ return await window.Alpine.store('notificationStore').addFrontendToast('success', message, title, display_time, group);
547
+ } catch (error) {
548
+ console.error('Failed to create frontend success notification:', error);
549
+ console.log('Frontend Success:', title, '-', message);
550
+ return null;
551
+ }
552
+ } else {
553
+ console.log('Frontend Success:', title, '-', message);
554
+ return null;
555
+ }
556
+};
557
+
558
export { store };
webui/components/notifications/notification-toast-stack.html
+8
-6
@@ -2,14 +2,15 @@
2
<head>
3
<title>Notification Toast Stack</title>
4
<script type="module">
5
- import { store } from "/js/notificationStore.js";
5
+ import { store } from "/components/notifications/notification-store.js";
6
</script>
7
</head>
8
<body>
9
<div x-data>
10
- <!-- Toast Stack Container -->
11
- <div class="toast-stack-container"
12
- x-show="$store.notificationStore.toastStack.length > 0">
10
+ <template x-if="$store.notificationStore">
11
+ <!-- Toast Stack Container -->
12
+ <div class="toast-stack-container"
13
+ x-show="$store.notificationStore.toastStack.length > 0">
14
15
<template x-for="(toast, index) in $store.notificationStore.toastStack" :key="toast.toastId">
16
<div class="toast-item"
@@ -41,11 +42,12 @@
42
<button class="toast-dismiss"
43
@click.stop="$store.notificationStore.removeFromToastStack(toast.toastId)"
44
title="Dismiss">
44
- ✕
45
+ <span class="material-symbols-outlined">close</span>
46
</button>
47
</div>
48
</template>
48
- </div>
49
+ </div>
50
+ </template>
51
</div>
52
53
<style>
webui/index.html
+2
-2
@@ -103,7 +103,7 @@
103
<script type="module" src="js/scheduler.js"></script>
104
<script type="module" src="js/speech.js"></script>
105
<script type="module" src="js/history.js"></script>
106
- <script type="module" src="js/notificationStore.js"></script>
106
+
107
<script type="module" src="index.js"></script>
108
109
<!-- Then load Alpine.js -->
@@ -388,7 +388,7 @@
388
<div>
389
<x-component path="/chat/attachments/inputPreview.html" />
390
</div>
391
-
391
+
392
<!-- Top row with input and buttons -->
393
<div class="input-row">
394
<!-- Attachment icon with tooltip -->
webui/index.js
+7
-13
@@ -812,14 +812,14 @@ window.nudge = async function () {
812
window.restart = async function () {
813
try {
814
if (!getConnectionStatus()) {
815
- toast("Backend disconnected, cannot restart.", "error");
815
+ await toastFrontendError("Backend disconnected, cannot restart.", "Restart Error");
816
return;
817
}
818
// First try to initiate restart
819
const resp = await sendJsonData("/restart", {});
820
} catch (e) {
821
- // Show restarting message
822
- toast("Restarting...", "info", 0);
821
+ // Show restarting message with no timeout and restart group
822
+ await toastFrontendInfo("Restarting...", "System Restart", 9999, "restart");
823
824
let retries = 0;
825
const maxRetries = 240; // Maximum number of retries (60 seconds with 250ms interval)
@@ -827,9 +827,9 @@ window.restart = async function () {
827
while (retries < maxRetries) {
828
try {
829
const resp = await sendJsonData("/health", {});
830
- // Server is back up, show success message
830
+ // Server is back up, show success message that replaces the restarting message
831
await new Promise((resolve) => setTimeout(resolve, 250));
832
- toast("Restarted", "success", 5000);
832
+ await toastFrontendSuccess("Restarted", "System Restart", 5, "restart");
833
return;
834
} catch (e) {
835
// Server still down, keep waiting
@@ -839,7 +839,7 @@ window.restart = async function () {
839
}
840
841
// If we get here, restart failed or took too long
842
- toast("Restart timed out or failed", "error", 5000);
842
+ await toastFrontendError("Restart timed out or failed", "Restart Error", 8, "restart");
843
}
844
};
845
@@ -1056,13 +1056,7 @@ document.addEventListener("DOMContentLoaded", function () {
1056
setupTabs();
1057
initializeActiveTab();
1058
1059
- // Initialize notification store early to ensure it's ready for polling
1060
- setTimeout(() => {
1061
- if (globalThis.Alpine?.store('notificationStore')) {
1062
- globalThis.Alpine.store('notificationStore').initialize();
1063
- console.log('Notification store initialized on DOM ready');
1064
- }
1065
- }, 100); // Small delay to ensure Alpine is ready
1059
+
1060
});
1061
1062
// Setup tabs functionality
webui/js/initFw.js
-11
@@ -1,6 +1,5 @@
1
import * as _modals from "./modals.js";
2
import * as _components from "./components.js";
3
-import "./notificationStore.js";
3
4
await import("../vendor/alpine/alpine.min.js");
5
@@ -12,13 +11,3 @@ Alpine.directive(
11
cleanup(() => onDestroy());
12
}
13
);
15
-
16
-// Initialize notification store when Alpine is ready
17
-document.addEventListener('alpine:init', () => {
18
- // Initialize the notification store
19
- setTimeout(() => {
20
- if (Alpine.store('notificationStore')) {
21
- Alpine.store('notificationStore').initialize();
22
- }
23
- }, 100);
24
-});