feat: frontend-only notifications and test button for them
Rafael Uzarowski committed
Jul 1, 2025 at 16:37 UTC
1225cae5c0b7819312f3954f153250de3bdbd928
3 files changed
+115
webui/components/notifications/notification-icons.html
+7
@@ -29,6 +29,13 @@
29
title="Create Test Notification">
30
<div class="notification-icon">🧪</div>
31
</button>
32
+
33
+ <!-- Test Frontend Error Button (for development) -->
34
+ <button class="notification-test-button frontend-error"
35
+ @click="toastFrontendError('Backend connection failed', 'Connection Error')"
36
+ title="Test Frontend Error Toast">
37
+ <div class="notification-icon">⚠️</div>
38
+ </button>
39
</div>
40
</body>
41
</html>
webui/css/notification.css
+21
@@ -109,6 +109,17 @@
109
font-size: 1.2rem;
110
}
111
112
+/* Frontend Error Test Button - distinct orange/red styling */
113
+.notification-test-button.frontend-error {
114
+ background: rgba(255, 152, 0, 0.15);
115
+ border-color: rgba(255, 152, 0, 0.4);
116
+}
117
+
118
+.notification-test-button.frontend-error:hover {
119
+ background: rgba(255, 152, 0, 0.25);
120
+ border-color: rgba(255, 152, 0, 0.6);
121
+}
122
+
123
/* Light Mode Styles */
124
.light-mode .notification-toggle {
125
background: rgba(0, 0, 0, 0.05);
@@ -140,6 +151,16 @@
151
border-color: rgba(33, 150, 243, 0.5);
152
}
153
154
+.light-mode .notification-test-button.frontend-error {
155
+ background: rgba(255, 152, 0, 0.15);
156
+ border-color: rgba(255, 152, 0, 0.4);
157
+}
158
+
159
+.light-mode .notification-test-button.frontend-error:hover {
160
+ background: rgba(255, 152, 0, 0.25);
161
+ border-color: rgba(255, 152, 0, 0.6);
162
+}
163
+
164
/* Animations */
165
@keyframes pulse {
166
0%, 100% { transform: scale(1); }
webui/js/notificationStore.js
+87
@@ -366,9 +366,96 @@ const model = {
366
// Legacy method for backward compatibility
367
toggleNotifications() {
368
this.openModal();
369
+ },
370
+
371
+ // NEW: Add frontend-only toast directly to stack (for connection errors, etc.)
372
+ addFrontendToast(type, message, title = "", display_time = 5) {
373
+ const timestamp = new Date().toISOString();
374
+ const notification = {
375
+ id: `frontend-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
376
+ type: type,
377
+ title: title,
378
+ message: message,
379
+ detail: "",
380
+ timestamp: timestamp,
381
+ display_time: display_time,
382
+ read: false,
383
+ frontend: true // Mark as frontend-only
384
+ };
385
+
386
+ // Create toast object with auto-dismiss timer
387
+ const toast = {
388
+ ...notification,
389
+ toastId: `toast-${notification.id}`,
390
+ addedAt: Date.now(),
391
+ autoRemoveTimer: null
392
+ };
393
+
394
+ // Add to bottom of stack (newest at bottom)
395
+ this.toastStack.push(toast);
396
+
397
+ // Enforce max stack limit (remove oldest from top)
398
+ if (this.toastStack.length > this.maxToastStack) {
399
+ const removed = this.toastStack.shift(); // Remove from top
400
+ if (removed.autoRemoveTimer) {
401
+ clearTimeout(removed.autoRemoveTimer);
402
+ }
403
+ }
404
+
405
+ // Set auto-dismiss timer
406
+ toast.autoRemoveTimer = setTimeout(() => {
407
+ this.removeFromToastStack(toast.toastId);
408
+ }, notification.display_time * 1000);
409
+
410
+ console.log(`Frontend toast added: ${notification.type} - ${notification.message}`);
411
+ return notification.id;
412
+ },
413
+
414
+ // NEW: Convenience methods for frontend-only notifications
415
+ frontendError(message, title = "Connection Error", display_time = 8) {
416
+ return this.addFrontendToast('error', message, title, display_time);
417
+ },
418
+
419
+ frontendWarning(message, title = "Warning", display_time = 5) {
420
+ return this.addFrontendToast('warning', message, title, display_time);
421
+ },
422
+
423
+ frontendInfo(message, title = "Info", display_time = 3) {
424
+ return this.addFrontendToast('info', message, title, display_time);
425
}
426
};
427
428
// Create and export the store
429
const store = createStore("notificationStore", model);
430
+
431
+// NEW: Global function for frontend error toasts (replaces toastFetchError)
432
+window.toastFrontendError = function(message, title = "Connection Error") {
433
+ if (window.Alpine && window.Alpine.store && window.Alpine.store('notificationStore')) {
434
+ return window.Alpine.store('notificationStore').frontendError(message, title);
435
+ } else {
436
+ // Fallback if Alpine/store not ready
437
+ console.error('Frontend Error:', title, '-', message);
438
+ return null;
439
+ }
440
+};
441
+
442
+// NEW: Additional global convenience functions
443
+window.toastFrontendWarning = function(message, title = "Warning") {
444
+ if (window.Alpine && window.Alpine.store && window.Alpine.store('notificationStore')) {
445
+ return window.Alpine.store('notificationStore').frontendWarning(message, title);
446
+ } else {
447
+ console.warn('Frontend Warning:', title, '-', message);
448
+ return null;
449
+ }
450
+};
451
+
452
+window.toastFrontendInfo = function(message, title = "Info") {
453
+ if (window.Alpine && window.Alpine.store && window.Alpine.store('notificationStore')) {
454
+ return window.Alpine.store('notificationStore').frontendInfo(message, title);
455
+ } else {
456
+ console.log('Frontend Info:', title, '-', message);
457
+ return null;
458
+ }
459
+};
460
+
461
export { store };