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 -});