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