frdel commited on
Commit
9c089c7
·
1 Parent(s): 6d7cc0d

notifications redesign

Browse files
docs/designs/backup-specification-frontend.md CHANGED
@@ -820,12 +820,12 @@ const model = {
820
 
821
  if (response.ok) {
822
  const blob = await response.blob();
823
- const url = window.URL.createObjectURL(blob);
824
  const a = document.createElement('a');
825
  a.href = url;
826
  a.download = `${backupName}.zip`;
827
  a.click();
828
- window.URL.revokeObjectURL(url);
829
  }
830
  } catch (error) {
831
  console.error('Download error:', error);
@@ -1298,9 +1298,9 @@ Use existing `openModal()` and `closeModal()` functions from the global modal sy
1298
  Use existing Agent Zero toast system for consistent user feedback:
1299
  ```javascript
1300
  // Use established toast patterns
1301
- window.toast("Backup created successfully", "success");
1302
- window.toast("Restore completed", "success");
1303
- window.toast("Error creating backup", "error");
1304
  ```
1305
 
1306
  #### ACE Editor Integration
@@ -1417,10 +1417,10 @@ formatTimestamp(timestamp) {
1417
  // Use existing error handling patterns
1418
  try {
1419
  const result = await backupOperation();
1420
- window.toast("Operation completed successfully", "success");
1421
  } catch (error) {
1422
  console.error('Backup error:', error);
1423
- window.toast(`Error: ${error.message}`, "error");
1424
  }
1425
  ```
1426
 
 
820
 
821
  if (response.ok) {
822
  const blob = await response.blob();
823
+ const url = globalThis.URL.createObjectURL(blob);
824
  const a = document.createElement('a');
825
  a.href = url;
826
  a.download = `${backupName}.zip`;
827
  a.click();
828
+ globalThis.URL.revokeObjectURL(url);
829
  }
830
  } catch (error) {
831
  console.error('Download error:', error);
 
1298
  Use existing Agent Zero toast system for consistent user feedback:
1299
  ```javascript
1300
  // Use established toast patterns
1301
+ globalThis.toast("Backup created successfully", "success");
1302
+ globalThis.toast("Restore completed", "success");
1303
+ globalThis.toast("Error creating backup", "error");
1304
  ```
1305
 
1306
  #### ACE Editor Integration
 
1417
  // Use existing error handling patterns
1418
  try {
1419
  const result = await backupOperation();
1420
+ globalThis.toast("Operation completed successfully", "success");
1421
  } catch (error) {
1422
  console.error('Backup error:', error);
1423
+ globalThis.toast(`Error: ${error.message}`, "error");
1424
  }
1425
  ```
1426
 
webui/components/notifications/notification-store.js CHANGED
@@ -2,557 +2,643 @@ import { createStore } from "/js/AlpineStore.js";
2
  import * as API from "/js/api.js";
3
 
4
  const model = {
5
- notifications: [],
6
- loading: false,
7
- lastNotificationVersion: 0,
8
- lastNotificationGuid: "",
9
- unreadCount: 0,
10
-
11
- // NEW: Toast stack management
12
- toastStack: [],
13
- maxToastStack: 5,
14
-
15
- // Initialize the notification store
16
- initialize() {
17
- this.loading = true;
18
- this.updateUnreadCount();
19
- this.removeOldNotifications();
20
- this.toastStack = [];
21
-
22
- // Auto-cleanup old notifications and toasts
23
- setInterval(() => {
24
- this.removeOldNotifications();
25
- this.cleanupExpiredToasts();
26
- }, 5 * 60 * 1000); // Every 5 minutes
27
- },
28
-
29
- // Update notifications from polling data
30
- updateFromPoll(pollData) {
31
- if (!pollData) return;
32
-
33
- const isFirstLoad = !this.lastNotificationGuid && pollData.notifications_guid;
34
-
35
- // Check if GUID changed (system restart)
36
- if (pollData.notifications_guid !== this.lastNotificationGuid) {
37
- this.lastNotificationVersion = 0;
38
- this.notifications = [];
39
- this.toastStack = []; // Clear toast stack on restart
40
- this.lastNotificationGuid = pollData.notifications_guid || '';
41
- }
42
-
43
- // Process new notifications and add to toast stack
44
- if (pollData.notifications && pollData.notifications.length > 0) {
45
- pollData.notifications.forEach(notification => {
46
- const isNew = !this.notifications.find(n => n.id === notification.id);
47
- this.addOrUpdateNotification(notification);
48
-
49
- // Add new unread notifications to toast stack
50
- if (isNew && !notification.read) {
51
- this.addToToastStack(notification);
52
- }
53
- });
54
- }
55
-
56
- // Update version tracking
57
- this.lastNotificationVersion = pollData.notifications_version || 0;
58
- this.lastNotificationGuid = pollData.notifications_guid || '';
59
-
60
- // Update UI state
61
- this.updateUnreadCount();
62
- this.removeOldNotifications();
63
 
64
- // Limit notifications to prevent memory issues (keep most recent)
65
- if (this.notifications.length > 50) {
66
- // Sort by timestamp and keep newest 50
67
- this.notifications.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
68
- this.notifications = this.notifications.slice(0, 50);
69
- }
70
- },
71
-
72
- // NEW: Add notification to toast stack
73
- addToToastStack(notification) {
74
- // If notification has a group, remove any existing toasts with the same group
75
- if (notification.group && notification.group.trim() !== "") {
76
- const existingToastIndex = this.toastStack.findIndex(t =>
77
- t.group === notification.group
78
- );
79
-
80
- if (existingToastIndex >= 0) {
81
- const existingToast = this.toastStack[existingToastIndex];
82
- if (existingToast.autoRemoveTimer) {
83
- clearTimeout(existingToast.autoRemoveTimer);
84
- }
85
- this.toastStack.splice(existingToastIndex, 1);
86
- }
87
- }
88
 
89
- // Create toast object with auto-dismiss timer
90
- const toast = {
91
- ...notification,
92
- toastId: `toast-${notification.id}`,
93
- addedAt: Date.now(),
94
- autoRemoveTimer: null
95
- };
96
-
97
- // Add to bottom of stack (newest at bottom)
98
- this.toastStack.push(toast);
99
-
100
- // Enforce max stack limit (remove oldest from top)
101
- if (this.toastStack.length > this.maxToastStack) {
102
- const removed = this.toastStack.shift(); // Remove from top
103
- if (removed.autoRemoveTimer) {
104
- clearTimeout(removed.autoRemoveTimer);
105
- }
106
  }
 
 
107
 
108
- // Set auto-dismiss timer
109
- toast.autoRemoveTimer = setTimeout(() => {
110
- this.removeFromToastStack(toast.toastId);
111
- }, notification.display_time * 1000);
112
- },
113
-
114
- // NEW: Remove toast from stack
115
- removeFromToastStack(toastId) {
116
- const index = this.toastStack.findIndex(t => t.toastId === toastId);
117
- if (index >= 0) {
118
- const toast = this.toastStack[index];
119
- if (toast.autoRemoveTimer) {
120
- clearTimeout(toast.autoRemoveTimer);
121
- }
122
- this.toastStack.splice(index, 1);
123
- }
124
- },
125
-
126
- // NEW: Clear entire toast stack
127
- clearToastStack() {
128
- this.toastStack.forEach(toast => {
129
- if (toast.autoRemoveTimer) {
130
- clearTimeout(toast.autoRemoveTimer);
131
- }
132
- });
133
- this.toastStack = [];
134
- },
135
-
136
- // NEW: Clean up expired toasts (backup cleanup)
137
- cleanupExpiredToasts() {
138
- const now = Date.now();
139
- this.toastStack = this.toastStack.filter(toast => {
140
- const age = now - toast.addedAt;
141
- const maxAge = toast.display_time * 1000;
142
-
143
- if (age > maxAge) {
144
- if (toast.autoRemoveTimer) {
145
- clearTimeout(toast.autoRemoveTimer);
146
- }
147
- return false;
148
- }
149
- return true;
150
- });
151
- },
152
-
153
- // NEW: Handle toast click (opens modal)
154
- async handleToastClick(toastId) {
155
- await this.openModal();
156
- // Modal opening will clear toast stack via markAllAsRead
157
- },
158
-
159
- // Add or update a notification
160
- addOrUpdateNotification(notification) {
161
- const existingIndex = this.notifications.findIndex(n => n.id === notification.id);
162
-
163
- if (existingIndex >= 0) {
164
- // Update existing notification
165
- this.notifications[existingIndex] = notification;
166
- } else {
167
- // Add new notification at the beginning (most recent first)
168
- this.notifications.unshift(notification);
169
- }
170
- },
171
-
172
- // Update unread count
173
- updateUnreadCount() {
174
- this.unreadCount = this.notifications.filter(n => !n.read).length;
175
- },
176
-
177
- // Mark notification as read
178
- async markAsRead(notificationId) {
179
- const notification = this.notifications.find(n => n.id === notificationId);
180
- if (notification && !notification.read) {
181
- notification.read = true;
182
- this.updateUnreadCount();
183
-
184
- // Sync with backend (non-blocking)
185
- try {
186
- await API.callJsonApi('notifications_mark_read', {
187
- notification_ids: [notificationId]
188
- });
189
- } catch (error) {
190
- console.error('Failed to sync notification read status:', error);
191
- // Don't revert the UI change - user experience should not be affected
192
- }
193
  }
194
- },
 
 
195
 
196
- // Enhanced: Mark all as read and clear toast stack
197
- async markAllAsRead() {
198
- const unreadNotifications = this.notifications.filter(n => !n.read);
199
- if (unreadNotifications.length === 0) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
 
201
- // Update UI immediately
202
- this.notifications.forEach(notification => {
203
- notification.read = true;
204
- });
205
- this.updateUnreadCount();
206
-
207
- // Clear toast stack when marking all as read
208
- this.clearToastStack();
209
-
210
- // Sync with backend (non-blocking)
211
- try {
212
- await API.callJsonApi('notifications_mark_read', {
213
- mark_all: true
214
- });
215
- } catch (error) {
216
- console.error('Failed to sync mark all as read:', error);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  }
218
- },
219
-
220
- // Clear all notifications
221
- async clearAll() {
222
- this.notifications = [];
223
- this.unreadCount = 0;
224
- this.clearToastStack(); // Also clear toast stack
225
-
226
- // Note: We don't sync clear with backend as notifications are stored in memory only
227
- },
228
-
229
- // Get notifications by type
230
- getNotificationsByType(type) {
231
- return this.notifications.filter(n => n.type === type);
232
- },
233
-
234
- // Get notifications for display: ALL unread + read from last 5 minutes
235
- getDisplayNotifications() {
236
- const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
237
-
238
- return this.notifications.filter(notification => {
239
- // Always show unread notifications
240
- if (!notification.read) {
241
- return true;
242
- }
243
-
244
- // Show read notifications only if they're from the last 5 minutes
245
- const notificationDate = new Date(notification.timestamp);
246
- return notificationDate > fiveMinutesAgo;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  });
248
- },
249
-
250
- // Get recent notifications (last 5) - kept for backwards compatibility
251
- getRecentNotifications() {
252
- return this.notifications.slice(0, 5);
253
- },
254
-
255
- // Get notification by ID
256
- getNotificationById(id) {
257
- return this.notifications.find(n => n.id === id);
258
- },
259
-
260
- // Remove old notifications (older than 1 hour)
261
- removeOldNotifications() {
262
- const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
263
- const initialCount = this.notifications.length;
264
- this.notifications = this.notifications.filter(n =>
265
- new Date(n.timestamp) > oneHourAgo
266
- );
267
-
268
- if (this.notifications.length !== initialCount) {
269
- this.updateUnreadCount();
270
- }
271
- },
272
-
273
- // Format timestamp for display
274
- formatTimestamp(timestamp) {
275
- const date = new Date(timestamp);
276
- const now = new Date();
277
- const diffMs = now - date;
278
- const diffMins = Math.floor(diffMs / 60000);
279
- const diffHours = Math.floor(diffMs / 3600000);
280
- const diffDays = Math.floor(diffMs / 86400000);
281
-
282
- if (diffMins < 1) return 'Just now';
283
- if (diffMins < 60) return `${diffMins}m ago`;
284
- if (diffHours < 24) return `${diffHours}h ago`;
285
- if (diffDays < 7) return `${diffDays}d ago`;
286
-
287
- return date.toLocaleDateString();
288
- },
289
-
290
- // Get CSS class for notification type
291
- getNotificationClass(type) {
292
- const classes = {
293
- info: "notification-info",
294
- success: "notification-success",
295
- warning: "notification-warning",
296
- error: "notification-error",
297
- progress: "notification-progress"
298
- };
299
- return classes[type] || "notification-info";
300
- },
301
-
302
- // Get CSS class for notification item including read state
303
- getNotificationItemClass(notification) {
304
- const typeClass = this.getNotificationClass(notification.type);
305
- const readClass = notification.read ? "read" : "unread";
306
- return `notification-item ${typeClass} ${readClass}`;
307
- },
308
-
309
- // Get icon for notification type (Google Material Icons)
310
- getNotificationIcon(type) {
311
- const icons = {
312
- info: "info",
313
- success: "check_circle",
314
- warning: "warning",
315
- error: "error",
316
- progress: "hourglass_empty"
317
- };
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)
323
- async createNotification(type, message, title = "", detail = "", display_time = 3, group = "") {
324
- try {
325
- const response = await window.sendJsonData('/notification_create', {
326
- type: type,
327
- message: message,
328
- title: title,
329
- detail: detail,
330
- display_time: display_time,
331
- group: group
332
- });
333
-
334
- if (response.success) {
335
- return response.notification_id;
336
- } else {
337
- console.error('Failed to create notification:', response.error);
338
- return null;
339
- }
340
- } catch (error) {
341
- console.error('Error creating notification:', error);
342
- return null;
343
- }
344
- },
345
-
346
- // Convenience methods for different notification types
347
- async info(message, title = "", detail = "", display_time = 3, group = "") {
348
- return await this.createNotification('info', message, title, detail, display_time, group);
349
- },
350
-
351
- async success(message, title = "", detail = "", display_time = 3, group = "") {
352
- return await this.createNotification('success', message, title, detail, display_time, group);
353
- },
354
-
355
- async warning(message, title = "", detail = "", display_time = 3, group = "") {
356
- return await this.createNotification('warning', message, title, detail, display_time, group);
357
- },
358
-
359
- async error(message, title = "", detail = "", display_time = 3, group = "") {
360
- return await this.createNotification('error', message, title, detail, display_time, group);
361
- },
362
-
363
- async progress(message, title = "", detail = "", display_time = 3, group = "") {
364
- return await this.createNotification('progress', message, title, detail, display_time, group);
365
- },
366
-
367
- // Enhanced: Open modal and clear toast stack
368
- async openModal() {
369
- // Import the standard modal system
370
- const { openModal } = await import("/js/modals.js");
371
- await openModal("notifications/notification-modal.html");
372
-
373
- // Clear toast stack when modal opens
374
- this.clearToastStack();
375
- },
376
-
377
- // Legacy method for backward compatibility
378
- toggleNotifications() {
379
- this.openModal();
380
- },
381
-
382
- // NEW: Check if backend connection is available
383
- isConnected() {
384
- // Use the global connection status from index.js, but default to true if undefined
385
- // This handles the case where polling hasn't run yet but backend is actually available
386
- const pollingStatus = typeof window.getConnectionStatus === 'function' ? window.getConnectionStatus() : undefined;
387
-
388
- // If polling status is explicitly false, respect that
389
- if (pollingStatus === false) {
390
- return false;
391
- }
392
-
393
- // If polling status is undefined/true, assume backend is available
394
- // (since the page loaded successfully, backend must be working)
395
- return true;
396
- },
397
-
398
- // NEW: Add frontend-only toast directly to stack (renamed from original addFrontendToast)
399
- addFrontendToastOnly(type, message, title = "", display_time = 5, group = "") {
400
- const timestamp = new Date().toISOString();
401
- const notification = {
402
- id: `frontend-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
403
- type: type,
404
- title: title,
405
- message: message,
406
- detail: "",
407
- timestamp: timestamp,
408
- display_time: display_time,
409
- read: false,
410
- frontend: true, // Mark as frontend-only
411
- group: group
412
- };
413
-
414
- // If notification has a group, remove any existing toasts with the same group
415
- if (group && group.trim() !== "") {
416
- const existingToastIndex = this.toastStack.findIndex(t =>
417
- t.group === group
418
- );
419
-
420
- if (existingToastIndex >= 0) {
421
- const existingToast = this.toastStack[existingToastIndex];
422
- if (existingToast.autoRemoveTimer) {
423
- clearTimeout(existingToast.autoRemoveTimer);
424
- }
425
- this.toastStack.splice(existingToastIndex, 1);
426
- }
427
- }
428
-
429
- // Create toast object with auto-dismiss timer
430
- const toast = {
431
- ...notification,
432
- toastId: `toast-${notification.id}`,
433
- addedAt: Date.now(),
434
- autoRemoveTimer: null
435
- };
436
-
437
- // Add to bottom of stack (newest at bottom)
438
- this.toastStack.push(toast);
439
-
440
- // Enforce max stack limit (remove oldest from top)
441
- if (this.toastStack.length > this.maxToastStack) {
442
- const removed = this.toastStack.shift(); // Remove from top
443
- if (removed.autoRemoveTimer) {
444
- clearTimeout(removed.autoRemoveTimer);
445
- }
446
- }
447
 
448
- // Set auto-dismiss timer
449
- toast.autoRemoveTimer = setTimeout(() => {
450
- this.removeFromToastStack(toast.toastId);
451
- }, notification.display_time * 1000);
452
-
453
- return notification.id;
454
- },
455
-
456
- // NEW: Enhanced frontend toast that tries backend first, falls back to frontend-only
457
- async addFrontendToast(type, message, title = "", display_time = 5, group = "") {
458
- // Try to send to backend first if connected
459
- if (this.isConnected()) {
460
- try {
461
- const notificationId = await this.createNotification(type, message, title, "", display_time, group);
462
- if (notificationId) {
463
- // Backend handled it, notification will arrive via polling
464
- return notificationId;
465
- }
466
- } catch (error) {
467
- console.log(`Backend unavailable for notification, showing as frontend-only: ${error.message || error}`);
468
- }
469
- } else {
470
- console.log('Backend disconnected, showing as frontend-only toast');
471
- }
472
 
473
- // Fallback to frontend-only toast
474
- return this.addFrontendToastOnly(type, message, title, display_time, group);
475
- },
476
 
477
- // NEW: Convenience methods for frontend notifications (updated to use new backend-first logic)
478
- async frontendError(message, title = "Connection Error", display_time = 8, group = "") {
479
- return await this.addFrontendToast('error', message, title, display_time, group);
480
- },
481
 
482
- async frontendWarning(message, title = "Warning", display_time = 5, group = "") {
483
- return await this.addFrontendToast('warning', message, title, display_time, group);
484
- },
485
 
486
- async frontendInfo(message, title = "Info", display_time = 3, group = "") {
487
- return await this.addFrontendToast('info', message, title, display_time, group);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
488
  }
489
- };
490
-
491
- // Create and export the store
492
- const store = createStore("notificationStore", model);
493
-
494
- // NEW: Global function for frontend error toasts (replaces toastFetchError)
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 {
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
502
- console.error('Frontend Error:', title, '-', message);
503
- return null;
504
- }
505
- } else {
506
- // Fallback if Alpine/store not ready
507
- console.error('Frontend Error:', title, '-', message);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
508
  return null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
509
  }
510
- };
511
 
512
- // NEW: Additional global convenience functions
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 {
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);
520
- return null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
521
  }
522
- } else {
523
- console.warn('Frontend Warning:', title, '-', message);
524
- return null;
525
  }
526
- };
527
 
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 {
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);
535
- return null;
536
- }
537
- } else {
538
- console.log('Frontend Info:', title, '-', message);
539
- return null;
 
 
 
 
 
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 };
 
 
 
 
 
 
 
 
2
  import * as API from "/js/api.js";
3
 
4
  const model = {
5
+ notifications: [],
6
+ loading: false,
7
+ lastNotificationVersion: 0,
8
+ lastNotificationGuid: "",
9
+ unreadCount: 0,
10
+
11
+ // NEW: Toast stack management
12
+ toastStack: [],
13
+ maxToastStack: 5,
14
+
15
+ init() {
16
+ this.initialize();
17
+
18
+ setTimeout(function () {
19
+ this.frontendInfo("Notification store initialized 1");
20
+ // this.frontendInfo("Notification store initialized 2");
21
+ // this.frontendInfo("Notification store initialized 3");
22
+ }.bind(this), 100);
23
+ },
24
+
25
+ // Initialize the notification store
26
+ initialize() {
27
+ this.loading = true;
28
+ this.updateUnreadCount();
29
+ this.removeOldNotifications();
30
+ this.toastStack = [];
31
+
32
+ // Auto-cleanup old notifications and toasts
33
+ setInterval(() => {
34
+ this.removeOldNotifications();
35
+ this.cleanupExpiredToasts();
36
+ }, 5 * 60 * 1000); // Every 5 minutes
37
+ },
38
+
39
+ // Update notifications from polling data
40
+ updateFromPoll(pollData) {
41
+ if (!pollData) return;
42
+
43
+ // Check if GUID changed (system restart)
44
+ if (pollData.notifications_guid !== this.lastNotificationGuid) {
45
+ this.lastNotificationVersion = 0;
46
+ this.notifications = [];
47
+ this.toastStack = []; // Clear toast stack on restart
48
+ this.lastNotificationGuid = pollData.notifications_guid || "";
49
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
50
 
51
+ // Process new notifications and add to toast stack
52
+ if (pollData.notifications && pollData.notifications.length > 0) {
53
+ pollData.notifications.forEach((notification) => {
54
+ const isNew = !this.notifications.find((n) => n.id === notification.id);
55
+ this.addOrUpdateNotification(notification);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
 
57
+ // Add new unread notifications to toast stack
58
+ if (isNew && !notification.read) {
59
+ this.addToToastStack(notification);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  }
61
+ });
62
+ }
63
 
64
+ // Update version tracking
65
+ this.lastNotificationVersion = pollData.notifications_version || 0;
66
+ this.lastNotificationGuid = pollData.notifications_guid || "";
67
+
68
+ // Update UI state
69
+ this.updateUnreadCount();
70
+ // this.removeOldNotifications();
71
+
72
+ // Limit notifications to prevent memory issues (keep most recent)
73
+ if (this.notifications.length > 50) {
74
+ // Sort by timestamp and keep newest 50
75
+ this.notifications.sort(
76
+ (a, b) => new Date(b.timestamp) - new Date(a.timestamp)
77
+ );
78
+ this.notifications = this.notifications.slice(0, 50);
79
+ }
80
+ },
81
+
82
+ // NEW: Add notification to toast stack
83
+ addToToastStack(notification) {
84
+ // If notification has a group, remove any existing toasts with the same group
85
+ if (notification.group && notification.group.trim() !== "") {
86
+ const existingToastIndex = this.toastStack.findIndex(
87
+ (t) => t.group === notification.group
88
+ );
89
+
90
+ if (existingToastIndex >= 0) {
91
+ const existingToast = this.toastStack[existingToastIndex];
92
+ if (existingToast.autoRemoveTimer) {
93
+ clearTimeout(existingToast.autoRemoveTimer);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  }
95
+ this.toastStack.splice(existingToastIndex, 1);
96
+ }
97
+ }
98
 
99
+ // Create toast object with auto-dismiss timer
100
+ const toast = {
101
+ ...notification,
102
+ toastId: `toast-${notification.id}`,
103
+ addedAt: Date.now(),
104
+ autoRemoveTimer: null,
105
+ };
106
+
107
+ // Add to bottom of stack (newest at bottom)
108
+ this.toastStack.push(toast);
109
+
110
+ // Enforce max stack limit (remove oldest from top)
111
+ if (this.toastStack.length > this.maxToastStack) {
112
+ const removed = this.toastStack.shift(); // Remove from top
113
+ if (removed.autoRemoveTimer) {
114
+ clearTimeout(removed.autoRemoveTimer);
115
+ }
116
+ }
117
 
118
+ // Set auto-dismiss timer
119
+ toast.autoRemoveTimer = setTimeout(() => {
120
+ this.removeFromToastStack(toast.toastId);
121
+ }, notification.display_time * 1000);
122
+ },
123
+
124
+ // NEW: Remove toast from stack
125
+ removeFromToastStack(toastId) {
126
+ const index = this.toastStack.findIndex((t) => t.toastId === toastId);
127
+ if (index >= 0) {
128
+ const toast = this.toastStack[index];
129
+ if (toast.autoRemoveTimer) {
130
+ clearTimeout(toast.autoRemoveTimer);
131
+ }
132
+ this.toastStack.splice(index, 1);
133
+ }
134
+ },
135
+
136
+ // NEW: Clear entire toast stack
137
+ clearToastStack() {
138
+ this.toastStack.forEach((toast) => {
139
+ if (toast.autoRemoveTimer) {
140
+ clearTimeout(toast.autoRemoveTimer);
141
+ }
142
+ });
143
+ this.toastStack = [];
144
+ },
145
+
146
+ // NEW: Clean up expired toasts (backup cleanup)
147
+ cleanupExpiredToasts() {
148
+ const now = Date.now();
149
+ this.toastStack = this.toastStack.filter((toast) => {
150
+ const age = now - toast.addedAt;
151
+ const maxAge = toast.display_time * 1000;
152
+
153
+ if (age > maxAge) {
154
+ if (toast.autoRemoveTimer) {
155
+ clearTimeout(toast.autoRemoveTimer);
156
  }
157
+ return false;
158
+ }
159
+ return true;
160
+ });
161
+ },
162
+
163
+ // NEW: Handle toast click (opens modal)
164
+ async handleToastClick(toastId) {
165
+ await this.openModal();
166
+ // Modal opening will clear toast stack via markAllAsRead
167
+ },
168
+
169
+ // Add or update a notification
170
+ addOrUpdateNotification(notification) {
171
+ const existingIndex = this.notifications.findIndex(
172
+ (n) => n.id === notification.id
173
+ );
174
+
175
+ if (existingIndex >= 0) {
176
+ // Update existing notification
177
+ this.notifications[existingIndex] = notification;
178
+ } else {
179
+ // Add new notification at the beginning (most recent first)
180
+ this.notifications.unshift(notification);
181
+ }
182
+ },
183
+
184
+ // Update unread count
185
+ updateUnreadCount() {
186
+ const unread = this.notifications.filter((n) => !n.read).length;
187
+ if (this.unreadCount !== unread) this.unreadCount = unread;
188
+ },
189
+
190
+ // Mark notification as read
191
+ async markAsRead(notificationId) {
192
+ const notification = this.notifications.find(
193
+ (n) => n.id === notificationId
194
+ );
195
+ if (notification && !notification.read) {
196
+ notification.read = true;
197
+ this.updateUnreadCount();
198
+
199
+ // Sync with backend (non-blocking)
200
+ try {
201
+ await API.callJsonApi("notifications_mark_read", {
202
+ notification_ids: [notificationId],
203
  });
204
+ } catch (error) {
205
+ console.error("Failed to sync notification read status:", error);
206
+ // Don't revert the UI change - user experience should not be affected
207
+ }
208
+ }
209
+ },
210
+
211
+ // Enhanced: Mark all as read and clear toast stack
212
+ async markAllAsRead() {
213
+ const unreadNotifications = this.notifications.filter((n) => !n.read);
214
+ if (unreadNotifications.length === 0) return;
215
+
216
+ // Update UI immediately
217
+ this.notifications.forEach((notification) => {
218
+ notification.read = true;
219
+ });
220
+ this.updateUnreadCount();
221
+
222
+ // Clear toast stack when marking all as read
223
+ this.clearToastStack();
224
+
225
+ // Sync with backend (non-blocking)
226
+ try {
227
+ await API.callJsonApi("notifications_mark_read", {
228
+ mark_all: true,
229
+ });
230
+ } catch (error) {
231
+ console.error("Failed to sync mark all as read:", error);
232
+ }
233
+ },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
 
235
+ // Clear all notifications
236
+ async clearAll() {
237
+ this.notifications = [];
238
+ this.unreadCount = 0;
239
+ this.clearToastStack(); // Also clear toast stack
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
 
241
+ // Note: We don't sync clear with backend as notifications are stored in memory only
242
+ },
 
243
 
244
+ // Get notifications by type
245
+ getNotificationsByType(type) {
246
+ return this.notifications.filter((n) => n.type === type);
247
+ },
248
 
249
+ // Get notifications for display: ALL unread + read from last 5 minutes
250
+ getDisplayNotifications() {
251
+ const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
252
 
253
+ return this.notifications.filter((notification) => {
254
+ // Always show unread notifications
255
+ if (!notification.read) {
256
+ return true;
257
+ }
258
+
259
+ // Show read notifications only if they're from the last 5 minutes
260
+ const notificationDate = new Date(notification.timestamp);
261
+ return notificationDate > fiveMinutesAgo;
262
+ });
263
+ },
264
+
265
+ // Get recent notifications (last 5) - kept for backwards compatibility
266
+ getRecentNotifications() {
267
+ return this.notifications.slice(0, 5);
268
+ },
269
+
270
+ // Get notification by ID
271
+ getNotificationById(id) {
272
+ return this.notifications.find((n) => n.id === id);
273
+ },
274
+
275
+ // Remove old notifications (older than 1 hour)
276
+ removeOldNotifications() {
277
+ const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
278
+ const initialCount = this.notifications.length;
279
+ this.notifications = this.notifications.filter(
280
+ (n) => new Date(n.timestamp) > oneHourAgo
281
+ );
282
+
283
+ if (this.notifications.length !== initialCount) {
284
+ this.updateUnreadCount();
285
  }
286
+ },
287
+
288
+ // Format timestamp for display
289
+ formatTimestamp(timestamp) {
290
+ const date = new Date(timestamp);
291
+ const now = new Date();
292
+ const diffMs = now - date;
293
+ const diffMins = Math.floor(diffMs / 60000);
294
+ const diffHours = Math.floor(diffMs / 3600000);
295
+ const diffDays = Math.floor(diffMs / 86400000);
296
+
297
+ if (diffMins < 1) return "Just now";
298
+ if (diffMins < 60) return `${diffMins}m ago`;
299
+ if (diffHours < 24) return `${diffHours}h ago`;
300
+ if (diffDays < 7) return `${diffDays}d ago`;
301
+
302
+ return date.toLocaleDateString();
303
+ },
304
+
305
+ // Get CSS class for notification type
306
+ getNotificationClass(type) {
307
+ const classes = {
308
+ info: "notification-info",
309
+ success: "notification-success",
310
+ warning: "notification-warning",
311
+ error: "notification-error",
312
+ progress: "notification-progress",
313
+ };
314
+ return classes[type] || "notification-info";
315
+ },
316
+
317
+ // Get CSS class for notification item including read state
318
+ getNotificationItemClass(notification) {
319
+ const typeClass = this.getNotificationClass(notification.type);
320
+ const readClass = notification.read ? "read" : "unread";
321
+ return `notification-item ${typeClass} ${readClass}`;
322
+ },
323
+
324
+ // Get icon for notification type (Google Material Icons)
325
+ getNotificationIcon(type) {
326
+ const icons = {
327
+ info: "info",
328
+ success: "check_circle",
329
+ warning: "warning",
330
+ error: "error",
331
+ progress: "hourglass_empty",
332
+ };
333
+ const iconName = icons[type] || "info";
334
+ return `<span class="material-symbols-outlined">${iconName}</span>`;
335
+ },
336
+
337
+ // Create notification via backend (will appear via polling)
338
+ async createNotification(
339
+ type,
340
+ message,
341
+ title = "",
342
+ detail = "",
343
+ display_time = 3,
344
+ group = ""
345
+ ) {
346
+ try {
347
+ const response = await globalThis.sendJsonData("/notification_create", {
348
+ type: type,
349
+ message: message,
350
+ title: title,
351
+ detail: detail,
352
+ display_time: display_time,
353
+ group: group,
354
+ });
355
+
356
+ if (response.success) {
357
+ return response.notification_id;
358
+ } else {
359
+ console.error("Failed to create notification:", response.error);
360
  return null;
361
+ }
362
+ } catch (error) {
363
+ console.error("Error creating notification:", error);
364
+ return null;
365
+ }
366
+ },
367
+
368
+ // Convenience methods for different notification types
369
+ async info(message, title = "", detail = "", display_time = 3, group = "") {
370
+ return await this.createNotification(
371
+ "info",
372
+ message,
373
+ title,
374
+ detail,
375
+ display_time,
376
+ group
377
+ );
378
+ },
379
+
380
+ async success(
381
+ message,
382
+ title = "",
383
+ detail = "",
384
+ display_time = 3,
385
+ group = ""
386
+ ) {
387
+ return await this.createNotification(
388
+ "success",
389
+ message,
390
+ title,
391
+ detail,
392
+ display_time,
393
+ group
394
+ );
395
+ },
396
+
397
+ async warning(
398
+ message,
399
+ title = "",
400
+ detail = "",
401
+ display_time = 3,
402
+ group = ""
403
+ ) {
404
+ return await this.createNotification(
405
+ "warning",
406
+ message,
407
+ title,
408
+ detail,
409
+ display_time,
410
+ group
411
+ );
412
+ },
413
+
414
+ async error(message, title = "", detail = "", display_time = 3, group = "") {
415
+ return await this.createNotification(
416
+ "error",
417
+ message,
418
+ title,
419
+ detail,
420
+ display_time,
421
+ group
422
+ );
423
+ },
424
+
425
+ async progress(
426
+ message,
427
+ title = "",
428
+ detail = "",
429
+ display_time = 3,
430
+ group = ""
431
+ ) {
432
+ return await this.createNotification(
433
+ "progress",
434
+ message,
435
+ title,
436
+ detail,
437
+ display_time,
438
+ group
439
+ );
440
+ },
441
+
442
+ // Enhanced: Open modal and clear toast stack
443
+ async openModal() {
444
+ // Import the standard modal system
445
+ const { openModal } = await import("/js/modals.js");
446
+ await openModal("notifications/notification-modal.html");
447
+
448
+ // Clear toast stack when modal opens
449
+ this.clearToastStack();
450
+ },
451
+
452
+ // Legacy method for backward compatibility
453
+ toggleNotifications() {
454
+ this.openModal();
455
+ },
456
+
457
+ // NEW: Check if backend connection is available
458
+ isConnected() {
459
+ // Use the global connection status from index.js, but default to true if undefined
460
+ // This handles the case where polling hasn't run yet but backend is actually available
461
+ const pollingStatus =
462
+ typeof globalThis.getConnectionStatus === "function"
463
+ ? globalThis.getConnectionStatus()
464
+ : undefined;
465
+
466
+ // If polling status is explicitly false, respect that
467
+ if (pollingStatus === false) {
468
+ return false;
469
  }
 
470
 
471
+ // If polling status is undefined/true, assume backend is available
472
+ // (since the page loaded successfully, backend must be working)
473
+ return true;
474
+ },
475
+
476
+ // NEW: Add frontend-only toast directly to stack (renamed from original addFrontendToast)
477
+ addFrontendToastOnly(
478
+ type,
479
+ message,
480
+ title = "",
481
+ display_time = 5,
482
+ group = ""
483
+ ) {
484
+ const timestamp = new Date().toISOString();
485
+ const notification = {
486
+ id: `frontend-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
487
+ type: type,
488
+ title: title,
489
+ message: message,
490
+ detail: "",
491
+ timestamp: timestamp,
492
+ display_time: display_time,
493
+ read: false,
494
+ frontend: true, // Mark as frontend-only
495
+ group: group,
496
+ };
497
+
498
+ // If notification has a group, remove any existing toasts with the same group
499
+ if (group && group.trim() !== "") {
500
+ const existingToastIndex = this.toastStack.findIndex(
501
+ (t) => t.group === group
502
+ );
503
+
504
+ if (existingToastIndex >= 0) {
505
+ const existingToast = this.toastStack[existingToastIndex];
506
+ if (existingToast.autoRemoveTimer) {
507
+ clearTimeout(existingToast.autoRemoveTimer);
508
  }
509
+ this.toastStack.splice(existingToastIndex, 1);
510
+ }
 
511
  }
 
512
 
513
+ // Create toast object with auto-dismiss timer
514
+ const toast = {
515
+ ...notification,
516
+ toastId: `toast-${notification.id}`,
517
+ addedAt: Date.now(),
518
+ autoRemoveTimer: null,
519
+ };
520
+
521
+ // Add to bottom of stack (newest at bottom)
522
+ this.toastStack.push(toast);
523
+
524
+ // Enforce max stack limit (remove oldest from top)
525
+ if (this.toastStack.length > this.maxToastStack) {
526
+ const removed = this.toastStack.shift(); // Remove from top
527
+ if (removed.autoRemoveTimer) {
528
+ clearTimeout(removed.autoRemoveTimer);
529
+ }
530
  }
 
531
 
532
+ // Set auto-dismiss timer
533
+ toast.autoRemoveTimer = setTimeout(() => {
534
+ this.removeFromToastStack(toast.toastId);
535
+ }, notification.display_time * 1000);
536
+
537
+ return notification.id;
538
+ },
539
+
540
+ // NEW: Enhanced frontend toast that tries backend first, falls back to frontend-only
541
+ async addFrontendToast(
542
+ type,
543
+ message,
544
+ title = "",
545
+ display_time = 5,
546
+ group = ""
547
+ ) {
548
+ // Try to send to backend first if connected
549
+ if (this.isConnected()) {
550
+ try {
551
+ const notificationId = await this.createNotification(
552
+ type,
553
+ message,
554
+ title,
555
+ "",
556
+ display_time,
557
+ group
558
+ );
559
+ if (notificationId) {
560
+ // Backend handled it, notification will arrive via polling
561
+ return notificationId;
562
  }
563
+ } catch (error) {
564
+ console.log(
565
+ `Backend unavailable for notification, showing as frontend-only: ${
566
+ error.message || error
567
+ }`
568
+ );
569
+ }
570
  } else {
571
+ console.log("Backend disconnected, showing as frontend-only toast");
 
572
  }
573
+
574
+ // Fallback to frontend-only toast
575
+ return this.addFrontendToastOnly(type, message, title, display_time, group);
576
+ },
577
+
578
+ // NEW: Convenience methods for frontend notifications (updated to use new backend-first logic)
579
+ async frontendError(
580
+ message,
581
+ title = "Connection Error",
582
+ display_time = 8,
583
+ group = ""
584
+ ) {
585
+ return await this.addFrontendToast(
586
+ "error",
587
+ message,
588
+ title,
589
+ display_time,
590
+ group
591
+ );
592
+ },
593
+
594
+ async frontendWarning(
595
+ message,
596
+ title = "Warning",
597
+ display_time = 5,
598
+ group = ""
599
+ ) {
600
+ return await this.addFrontendToast(
601
+ "warning",
602
+ message,
603
+ title,
604
+ display_time,
605
+ group
606
+ );
607
+ },
608
+
609
+ async frontendInfo(message, title = "Info", display_time = 3, group = "") {
610
+ return await this.addFrontendToast(
611
+ "info",
612
+ message,
613
+ title,
614
+ display_time,
615
+ group
616
+ );
617
+ },
618
+
619
+ async frontendSuccess(
620
+ message,
621
+ title = "Success",
622
+ display_time = 3,
623
+ group = ""
624
+ ) {
625
+ return await this.addFrontendToast(
626
+ "success",
627
+ message,
628
+ title,
629
+ display_time,
630
+ group
631
+ );
632
+ },
633
  };
634
 
635
+ // Create and export the store
636
+ const store = createStore("notificationStore", model);
637
  export { store };
638
+
639
+
640
+ // add toasts to global for backward compatibility with older scripts
641
+ globalThis.toastFrontendInfo = store.frontendInfo.bind(store);
642
+ globalThis.toastFrontendSuccess = store.frontendSuccess.bind(store);
643
+ globalThis.toastFrontendWarning = store.frontendWarning.bind(store);
644
+ globalThis.toastFrontendError = store.frontendError.bind(store);
webui/components/notifications/notification-toast-stack.html CHANGED
@@ -33,9 +33,9 @@
33
  <div class="toast-message"
34
  x-text="toast.message">
35
  </div>
36
- <div class="toast-timestamp"
37
  x-text="$store.notificationStore.formatTimestamp(toast.timestamp)">
38
- </div>
39
  </div>
40
 
41
  <!-- Toast Dismiss -->
@@ -53,16 +53,17 @@
53
  <style>
54
  /* Toast Stack Container */
55
  .toast-stack-container {
56
- position: fixed;
57
- bottom: 20px;
58
- right: 20px;
59
  z-index: 1500;
60
  display: flex;
61
- flex-direction: column;
62
  gap: 8px;
63
  pointer-events: none;
64
  max-width: 400px;
65
- width: 100%;
 
66
  }
67
 
68
  /* Individual Toast Items */
@@ -72,20 +73,18 @@
72
  align-items: flex-start;
73
  gap: 12px;
74
  padding: 16px;
75
- background: rgba(0, 0, 0, 0.9);
76
- border: 1px solid rgba(255, 255, 255, 0.2);
77
  border-radius: 8px;
78
  cursor: pointer;
79
  transition: all 0.2s ease;
80
- backdrop-filter: blur(10px);
81
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
82
  position: relative;
83
  min-height: 60px;
84
  }
85
 
86
  .toast-item:hover {
87
- background: rgba(0, 0, 0, 0.95);
88
- border-color: rgba(255, 255, 255, 0.3);
89
  transform: translateY(-2px);
90
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
91
  }
@@ -93,34 +92,33 @@
93
  /* Toast Type Styling */
94
  .toast-item.notification-info {
95
  border-left: 4px solid #2196F3;
96
- background: rgba(33, 150, 243, 0.1);
97
  }
98
 
99
  .toast-item.notification-success {
100
  border-left: 4px solid #4CAF50;
101
- background: rgba(76, 175, 80, 0.1);
102
  }
103
 
104
  .toast-item.notification-warning {
105
  border-left: 4px solid #FF9800;
106
- background: rgba(255, 152, 0, 0.1);
107
  }
108
 
109
  .toast-item.notification-error {
110
  border-left: 4px solid #F44336;
111
- background: rgba(244, 67, 54, 0.1);
112
  }
113
 
114
  .toast-item.notification-progress {
115
  border-left: 4px solid #9C27B0;
116
- background: rgba(156, 39, 176, 0.1);
117
  }
118
 
119
  /* Toast Icon */
120
  .toast-icon {
121
  font-size: 1.2rem;
122
  line-height: 1;
123
- opacity: 0.9;
124
  min-width: 24px;
125
  text-align: center;
126
  margin-top: 2px;
@@ -155,7 +153,7 @@
155
 
156
  /* Toast Dismiss */
157
  .toast-dismiss {
158
- background: rgba(255, 255, 255, 0.1);
159
  border: none;
160
  border-radius: 4px;
161
  color: var(--color-text);
@@ -163,16 +161,12 @@
163
  padding: 4px 6px;
164
  font-size: 0.8rem;
165
  transition: all 0.2s ease;
166
- opacity: 0;
167
- }
168
-
169
- .toast-item:hover .toast-dismiss {
170
- opacity: 1;
171
  }
172
 
173
  .toast-dismiss:hover {
174
- background: rgba(255, 255, 255, 0.2);
175
- color: #fff;
176
  }
177
 
178
  /* Toast Animations */
@@ -188,25 +182,6 @@
188
  transform: translateX(100%) scale(0.8);
189
  }
190
 
191
- /* Light Mode Styles */
192
- .light-mode .toast-item {
193
- background: rgba(255, 255, 255, 0.95);
194
- border-color: rgba(0, 0, 0, 0.1);
195
- color: var(--color-text);
196
- }
197
-
198
- .light-mode .toast-item:hover {
199
- background: rgba(255, 255, 255, 0.98);
200
- border-color: rgba(0, 0, 0, 0.2);
201
- }
202
-
203
- .light-mode .toast-dismiss {
204
- background: rgba(0, 0, 0, 0.1);
205
- }
206
-
207
- .light-mode .toast-dismiss:hover {
208
- background: rgba(0, 0, 0, 0.2);
209
- }
210
 
211
  /* Mobile Responsive */
212
  @media (max-width: 768px) {
 
33
  <div class="toast-message"
34
  x-text="toast.message">
35
  </div>
36
+ <!-- <div class="toast-timestamp"
37
  x-text="$store.notificationStore.formatTimestamp(toast.timestamp)">
38
+ </div> -->
39
  </div>
40
 
41
  <!-- Toast Dismiss -->
 
53
  <style>
54
  /* Toast Stack Container */
55
  .toast-stack-container {
56
+ position: absolute;
57
+ bottom: 5px; /* Spacing from the bottom of the zero-height container */
58
+ padding-right: 5px;
59
  z-index: 1500;
60
  display: flex;
61
+ flex-direction: column-reverse; /* Stack toasts upwards */
62
  gap: 8px;
63
  pointer-events: none;
64
  max-width: 400px;
65
+ width: calc(100% - 10px);
66
+ align-items: flex-end;
67
  }
68
 
69
  /* Individual Toast Items */
 
73
  align-items: flex-start;
74
  gap: 12px;
75
  padding: 16px;
76
+ background: var(--color-panel);
77
+ border: 1px solid var(--color-border);
78
  border-radius: 8px;
79
  cursor: pointer;
80
  transition: all 0.2s ease;
81
+ /* backdrop-filter: blur(10px); */
82
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
83
  position: relative;
84
  min-height: 60px;
85
  }
86
 
87
  .toast-item:hover {
 
 
88
  transform: translateY(-2px);
89
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
90
  }
 
92
  /* Toast Type Styling */
93
  .toast-item.notification-info {
94
  border-left: 4px solid #2196F3;
95
+ /* background: rgba(33, 150, 243, 0.1); */
96
  }
97
 
98
  .toast-item.notification-success {
99
  border-left: 4px solid #4CAF50;
100
+ /* background: rgba(76, 175, 80, 0.1); */
101
  }
102
 
103
  .toast-item.notification-warning {
104
  border-left: 4px solid #FF9800;
105
+ /* background: rgba(255, 152, 0, 0.1); */
106
  }
107
 
108
  .toast-item.notification-error {
109
  border-left: 4px solid #F44336;
110
+ /* background: rgba(244, 67, 54, 0.1); */
111
  }
112
 
113
  .toast-item.notification-progress {
114
  border-left: 4px solid #9C27B0;
115
+ /* background: rgba(156, 39, 176, 0.1); */
116
  }
117
 
118
  /* Toast Icon */
119
  .toast-icon {
120
  font-size: 1.2rem;
121
  line-height: 1;
 
122
  min-width: 24px;
123
  text-align: center;
124
  margin-top: 2px;
 
153
 
154
  /* Toast Dismiss */
155
  .toast-dismiss {
156
+ background: transparent; /* No background by default */
157
  border: none;
158
  border-radius: 4px;
159
  color: var(--color-text);
 
161
  padding: 4px 6px;
162
  font-size: 0.8rem;
163
  transition: all 0.2s ease;
164
+ opacity: 1; /* Always visible */
 
 
 
 
165
  }
166
 
167
  .toast-dismiss:hover {
168
+ background: var(--color-panel);
169
+ color: var(--color-primary);
170
  }
171
 
172
  /* Toast Animations */
 
182
  transform: translateX(100%) scale(0.8);
183
  }
184
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
 
186
  /* Mobile Responsive */
187
  @media (max-width: 768px) {
webui/components/settings/backup/backup-store.js CHANGED
@@ -1,9 +1,9 @@
1
  import { createStore } from "/js/AlpineStore.js";
2
 
3
  // Global function references
4
- const sendJsonData = window.sendJsonData;
5
- const toast = window.toast;
6
- const fetchApi = window.fetchApi;
7
 
8
  // ⚠️ CRITICAL: The .env file contains API keys and essential configuration.
9
  // This file is REQUIRED for Agent Zero to function and must be backed up.
 
1
  import { createStore } from "/js/AlpineStore.js";
2
 
3
  // Global function references
4
+ const sendJsonData = globalThis.sendJsonData;
5
+ const toast = globalThis.toast;
6
+ const fetchApi = globalThis.fetchApi;
7
 
8
  // ⚠️ CRITICAL: The .env file contains API keys and essential configuration.
9
  // This file is REQUIRED for Agent Zero to function and must be backed up.
webui/index.css CHANGED
@@ -68,6 +68,21 @@
68
  hue-rotate(177deg) brightness(87%) contrast(85%);
69
  }
70
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  /* Reset and Base Styles */
72
  body,
73
  html {
@@ -402,6 +417,7 @@ img {
402
  }
403
 
404
  #time-date-container {
 
405
  position: fixed;
406
  right: var(--spacing-md);
407
  display: flex;
@@ -1687,22 +1703,6 @@ a:active {
1687
  color: inherit;
1688
  }
1689
 
1690
- /* Light mode class */
1691
- .light-mode {
1692
- --color-background: var(--color-background-light);
1693
- --color-text: var(--color-text-light);
1694
- --color-primary: var(--color-primary-light);
1695
- --color-secondary: var(--color-secondary-light);
1696
- --color-accent: var(--color-accent-light);
1697
- --color-message-bg: var(--color-message-bg-light);
1698
- --color-message-text: var(--color-message-text-light);
1699
- --color-panel: var(--color-panel-light);
1700
- --color-border: var(--color-border-light);
1701
- --color-input: var(--color-input-light);
1702
- --color-input-focus: var(--color-input-focus-light);
1703
- }
1704
-
1705
-
1706
 
1707
  .light-mode .connected {
1708
  color: #4caf50;
 
68
  hue-rotate(177deg) brightness(87%) contrast(85%);
69
  }
70
 
71
+ /* Light mode class */
72
+ .light-mode {
73
+ --color-background: var(--color-background-light);
74
+ --color-text: var(--color-text-light);
75
+ --color-primary: var(--color-primary-light);
76
+ --color-secondary: var(--color-secondary-light);
77
+ --color-accent: var(--color-accent-light);
78
+ --color-message-bg: var(--color-message-bg-light);
79
+ --color-message-text: var(--color-message-text-light);
80
+ --color-panel: var(--color-panel-light);
81
+ --color-border: var(--color-border-light);
82
+ --color-input: var(--color-input-light);
83
+ --color-input-focus: var(--color-input-focus-light);
84
+ }
85
+
86
  /* Reset and Base Styles */
87
  body,
88
  html {
 
417
  }
418
 
419
  #time-date-container {
420
+ z-index: 1000;
421
  position: fixed;
422
  right: var(--spacing-md);
423
  display: flex;
 
1703
  color: inherit;
1704
  }
1705
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1706
 
1707
  .light-mode .connected {
1708
  color: #4caf50;
webui/index.html CHANGED
@@ -27,7 +27,7 @@
27
  <script src="vendor/flatpickr/flatpickr.min.js"></script>
28
 
29
  <script>
30
- window.safeCall = function (name, ...args) {
31
  if (window[name]) window[name](...args)
32
  }
33
  </script>
@@ -35,7 +35,7 @@
35
  <!-- Pre-initialize schedulerSettings to ensure Alpine doesn't miss it -->
36
  <script>
37
  // Pre-define schedulerSettings skeleton to ensure it's available to Alpine
38
- window.schedulerSettings = function() {
39
  return {
40
  tasks: [],
41
  isLoading: true,
@@ -214,7 +214,7 @@
214
  tasks: [],
215
  selected: '',
216
  openTaskDetail(taskId) {
217
- window.openTaskDetail(taskId);
218
  }
219
  }"
220
  style="display: none;">
@@ -285,7 +285,7 @@
285
  <span>Autoscroll</span>
286
  <label class="switch">
287
  <input id="auto-scroll-switch" type="checkbox" x-model="autoScroll"
288
- x-effect="window.safeCall('toggleAutoScroll',autoScroll)">
289
  <span class="slider"></span>
290
  </label>
291
  </li>
@@ -309,7 +309,7 @@
309
  <span>Show thoughts</span>
310
  <label class="switch">
311
  <input type="checkbox" x-model="showThoughts"
312
- x-effect="window.safeCall('toggleThoughts',showThoughts)">
313
  <span class="slider"></span>
314
  </label>
315
  </li>
@@ -317,7 +317,7 @@
317
  <span>Show JSON</span>
318
  <label class="switch">
319
  <input type="checkbox" x-model="showJson"
320
- x-effect="window.safeCall('toggleJson',showJson)">
321
  <span class="slider"></span>
322
  </label>
323
  </li>
@@ -325,7 +325,7 @@
325
  <span>Show utility messages</span>
326
  <label class="switch">
327
  <input type="checkbox" x-model="showUtils"
328
- x-effect="window.safeCall('toggleUtils',showUtils)">
329
  <span class="slider"></span>
330
  </label>
331
  </li>
@@ -360,7 +360,9 @@
360
  </div>
361
 
362
  <!-- NEW: Toast Stack Component -->
363
- <x-component path="notifications/notification-toast-stack.html"></x-component>
 
 
364
 
365
  <div id="toast" class="toast">
366
  <div class="toast__content">
@@ -485,7 +487,7 @@
485
  <p>Files</p>
486
  </button>
487
 
488
- <button class="text-button" id="history_inspect" @click="window.openHistoryModal()">
489
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="5 10 85 85">
490
  <path fill="currentColor"
491
  d="m59.572,57.949c-.41,0-.826-.105-1.207-.325l-9.574-5.528c-.749-.432-1.21-1.231-1.21-2.095v-14.923c0-1.336,1.083-2.419,2.419-2.419s2.419,1.083,2.419,2.419v13.526l8.364,4.829c1.157.668,1.554,2.148.886,3.305-.448.776-1.261,1.21-2.097,1.21Zm30.427-7.947c0,10.684-4.161,20.728-11.716,28.283-6.593,6.59-15.325,10.69-24.59,11.544-1.223.113-2.448.169-3.669.169-7.492,0-14.878-2.102-21.22-6.068l-15.356,5.733c-.888.331-1.887.114-2.557-.556s-.887-1.669-.556-2.557l5.733-15.351c-4.613-7.377-6.704-16.165-5.899-24.891.854-9.266,4.954-17.998,11.544-24.588,7.555-7.555,17.6-11.716,28.285-11.716s20.73,4.161,28.285,11.716c7.555,7.555,11.716,17.599,11.716,28.283Zm-15.137-24.861c-13.71-13.71-36.018-13.71-49.728,0-11.846,11.846-13.682,30.526-4.365,44.417.434.647.53,1.464.257,2.194l-4.303,11.523,11.528-4.304c.274-.102.561-.153.846-.153.474,0,.944.139,1.348.41,13.888,9.315,32.568,7.479,44.417-4.365,13.707-13.708,13.706-36.014,0-49.723Zm-24.861-4.13c-15.989,0-28.996,13.006-28.996,28.992s13.008,28.992,28.996,28.992c1.336,0,2.419-1.083,2.419-2.419s-1.083-2.419-2.419-2.419c-13.32,0-24.157-10.835-24.157-24.153s10.837-24.153,24.157-24.153,24.153,10.835,24.153,24.153c0,1.336,1.083,2.419,2.419,2.419s2.419-1.083,2.419-2.419c0-15.986-13.006-28.992-28.992-28.992Zm25.041,33.531c-1.294.347-2.057,1.673-1.71,2.963.343,1.289,1.669,2.057,2.963,1.71,1.289-.343,2.053-1.669,1.71-2.963-.347-1.289-1.673-2.057-2.963-1.71Zm-2.03,6.328c-1.335,0-2.419,1.084-2.419,2.419s1.084,2.419,2.419,2.419,2.419-1.084,2.419-2.419-1.084-2.419-2.419-2.419Zm-3.598,5.587c-1.289-.347-2.615.416-2.963,1.71-.343,1.289.421,2.615,1.71,2.963,1.294.347,2.62-.421,2.963-1.71.347-1.294-.416-2.62-1.71-2.963Zm-4.919,4.462c-1.157-.667-2.638-.27-3.306.887-.667,1.157-.27,2.638.887,3.305,1.157.668,2.638.27,3.306-.887.667-1.157.27-2.638-.887-3.306Zm-9.327,3.04c-.946.946-.946,2.478,0,3.42.942.946,2.473.946,3.42,0,.946-.942.946-2.473,0-3.42-.946-.946-2.478-.946-3.42,0Z">
@@ -494,7 +496,7 @@
494
  <p>History</p>
495
  </button>
496
 
497
- <button class="text-button" id="ctx_window" @click="window.openCtxWindowModal()">
498
  <svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="17 15 70 70" fill="currentColor">
499
  <path
500
  d="m63 25c1.1016 0 2-0.89844 2-2s-0.89844-2-2-2h-26c-1.1016 0-2 0.89844-2 2s0.89844 2 2 2z">
 
27
  <script src="vendor/flatpickr/flatpickr.min.js"></script>
28
 
29
  <script>
30
+ globalThis.safeCall = function (name, ...args) {
31
  if (window[name]) window[name](...args)
32
  }
33
  </script>
 
35
  <!-- Pre-initialize schedulerSettings to ensure Alpine doesn't miss it -->
36
  <script>
37
  // Pre-define schedulerSettings skeleton to ensure it's available to Alpine
38
+ globalThis.schedulerSettings = function() {
39
  return {
40
  tasks: [],
41
  isLoading: true,
 
214
  tasks: [],
215
  selected: '',
216
  openTaskDetail(taskId) {
217
+ globalThis.openTaskDetail(taskId);
218
  }
219
  }"
220
  style="display: none;">
 
285
  <span>Autoscroll</span>
286
  <label class="switch">
287
  <input id="auto-scroll-switch" type="checkbox" x-model="autoScroll"
288
+ x-effect="globalThis.safeCall('toggleAutoScroll',autoScroll)">
289
  <span class="slider"></span>
290
  </label>
291
  </li>
 
309
  <span>Show thoughts</span>
310
  <label class="switch">
311
  <input type="checkbox" x-model="showThoughts"
312
+ x-effect="globalThis.safeCall('toggleThoughts',showThoughts)">
313
  <span class="slider"></span>
314
  </label>
315
  </li>
 
317
  <span>Show JSON</span>
318
  <label class="switch">
319
  <input type="checkbox" x-model="showJson"
320
+ x-effect="globalThis.safeCall('toggleJson',showJson)">
321
  <span class="slider"></span>
322
  </label>
323
  </li>
 
325
  <span>Show utility messages</span>
326
  <label class="switch">
327
  <input type="checkbox" x-model="showUtils"
328
+ x-effect="globalThis.safeCall('toggleUtils',showUtils)">
329
  <span class="slider"></span>
330
  </label>
331
  </li>
 
360
  </div>
361
 
362
  <!-- NEW: Toast Stack Component -->
363
+ <div style="position: relative; height: 0;">
364
+ <x-component path="notifications/notification-toast-stack.html"></x-component>
365
+ </div>
366
 
367
  <div id="toast" class="toast">
368
  <div class="toast__content">
 
487
  <p>Files</p>
488
  </button>
489
 
490
+ <button class="text-button" id="history_inspect" @click="globalThis.openHistoryModal()">
491
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="5 10 85 85">
492
  <path fill="currentColor"
493
  d="m59.572,57.949c-.41,0-.826-.105-1.207-.325l-9.574-5.528c-.749-.432-1.21-1.231-1.21-2.095v-14.923c0-1.336,1.083-2.419,2.419-2.419s2.419,1.083,2.419,2.419v13.526l8.364,4.829c1.157.668,1.554,2.148.886,3.305-.448.776-1.261,1.21-2.097,1.21Zm30.427-7.947c0,10.684-4.161,20.728-11.716,28.283-6.593,6.59-15.325,10.69-24.59,11.544-1.223.113-2.448.169-3.669.169-7.492,0-14.878-2.102-21.22-6.068l-15.356,5.733c-.888.331-1.887.114-2.557-.556s-.887-1.669-.556-2.557l5.733-15.351c-4.613-7.377-6.704-16.165-5.899-24.891.854-9.266,4.954-17.998,11.544-24.588,7.555-7.555,17.6-11.716,28.285-11.716s20.73,4.161,28.285,11.716c7.555,7.555,11.716,17.599,11.716,28.283Zm-15.137-24.861c-13.71-13.71-36.018-13.71-49.728,0-11.846,11.846-13.682,30.526-4.365,44.417.434.647.53,1.464.257,2.194l-4.303,11.523,11.528-4.304c.274-.102.561-.153.846-.153.474,0,.944.139,1.348.41,13.888,9.315,32.568,7.479,44.417-4.365,13.707-13.708,13.706-36.014,0-49.723Zm-24.861-4.13c-15.989,0-28.996,13.006-28.996,28.992s13.008,28.992,28.996,28.992c1.336,0,2.419-1.083,2.419-2.419s-1.083-2.419-2.419-2.419c-13.32,0-24.157-10.835-24.157-24.153s10.837-24.153,24.157-24.153,24.153,10.835,24.153,24.153c0,1.336,1.083,2.419,2.419,2.419s2.419-1.083,2.419-2.419c0-15.986-13.006-28.992-28.992-28.992Zm25.041,33.531c-1.294.347-2.057,1.673-1.71,2.963.343,1.289,1.669,2.057,2.963,1.71,1.289-.343,2.053-1.669,1.71-2.963-.347-1.289-1.673-2.057-2.963-1.71Zm-2.03,6.328c-1.335,0-2.419,1.084-2.419,2.419s1.084,2.419,2.419,2.419,2.419-1.084,2.419-2.419-1.084-2.419-2.419-2.419Zm-3.598,5.587c-1.289-.347-2.615.416-2.963,1.71-.343,1.289.421,2.615,1.71,2.963,1.294.347,2.62-.421,2.963-1.71.347-1.294-.416-2.62-1.71-2.963Zm-4.919,4.462c-1.157-.667-2.638-.27-3.306.887-.667,1.157-.27,2.638.887,3.305,1.157.668,2.638.27,3.306-.887.667-1.157.27-2.638-.887-3.306Zm-9.327,3.04c-.946.946-.946,2.478,0,3.42.942.946,2.473.946,3.42,0,.946-.942.946-2.473,0-3.42-.946-.946-2.478-.946-3.42,0Z">
 
496
  <p>History</p>
497
  </button>
498
 
499
+ <button class="text-button" id="ctx_window" @click="globalThis.openCtxWindowModal()">
500
  <svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="17 15 70 70" fill="currentColor">
501
  <path
502
  d="m63 25c1.1016 0 2-0.89844 2-2s-0.89844-2-2-2h-26c-1.1016 0-2 0.89844-2 2s0.89844 2 2 2z">
webui/index.js CHANGED
@@ -4,8 +4,9 @@ import * as css from "/js/css.js";
4
  import { sleep } from "/js/sleep.js";
5
  import { store as attachmentsStore } from "/components/chat/attachments/attachmentsStore.js";
6
  import { store as speechStore } from "/components/chat/speech/speech-store.js";
 
7
 
8
- window.fetchApi = api.fetchApi; // TODO - backward compatibility for non-modular scripts, remove once refactored to alpine
9
 
10
  const leftPanel = document.getElementById("left-panel");
11
  const rightPanel = document.getElementById("right-panel");
@@ -69,8 +70,8 @@ function handleResize() {
69
  }
70
  }
71
 
72
- window.addEventListener("load", handleResize);
73
- window.addEventListener("resize", handleResize);
74
 
75
  document.addEventListener("DOMContentLoaded", () => {
76
  const overlay = document.getElementById("sidebar-overlay");
@@ -184,7 +185,7 @@ function toastFetchError(text, error) {
184
  ).catch((e) => console.error("Failed to show connection error toast:", e));
185
  }
186
  }
187
- window.toastFetchError = toastFetchError;
188
 
189
  chatInput.addEventListener("keydown", (e) => {
190
  if (e.key === "Enter" && !e.shiftKey) {
@@ -241,7 +242,7 @@ function setMessage(id, type, heading, content, temp, kvps = null) {
241
  return result;
242
  }
243
 
244
- window.loadKnowledge = async function () {
245
  const input = document.createElement("input");
246
  input.type = "file";
247
  input.accept = ".txt,.pdf,.csv,.html,.json,.md";
@@ -300,7 +301,7 @@ export const sendJsonData = async function (url, data) {
300
  // const jsonResponse = await response.json();
301
  // return jsonResponse;
302
  };
303
- window.sendJsonData = sendJsonData;
304
 
305
  function generateGUID() {
306
  return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
@@ -316,7 +317,7 @@ function getConnectionStatus() {
316
 
317
  function setConnectionStatus(connected) {
318
  connectionStatus = connected;
319
- if (window.Alpine && timeDate) {
320
  const statusIconEl = timeDate.querySelector(".status-icon");
321
  if (statusIconEl) {
322
  const statusIcon = Alpine.$data(statusIconEl);
@@ -388,7 +389,7 @@ async function poll() {
388
  notificationStore.updateFromPoll(response);
389
 
390
  //set ui model vars from backend
391
- if (window.Alpine && inputSection) {
392
  const inputAD = Alpine.$data(inputSection);
393
  if (inputAD) {
394
  inputAD.paused = response.paused;
@@ -401,7 +402,7 @@ async function poll() {
401
  // Update chats list and sort by created_at time (newer first)
402
  let chatsAD = null;
403
  let contexts = response.contexts || [];
404
- if (window.Alpine && chatsSection) {
405
  chatsAD = Alpine.$data(chatsSection);
406
  if (chatsAD) {
407
  chatsAD.contexts = contexts.sort(
@@ -412,7 +413,7 @@ async function poll() {
412
 
413
  // Update tasks list and sort by creation time (newer first)
414
  const tasksSection = document.getElementById("tasks-section");
415
- if (window.Alpine && tasksSection) {
416
  const tasksAD = Alpine.$data(tasksSection);
417
  if (tasksAD) {
418
  let tasks = response.tasks || [];
@@ -572,15 +573,15 @@ function updateProgress(progress, active) {
572
  }
573
  }
574
 
575
- window.pauseAgent = async function (paused) {
576
  try {
577
  const resp = await sendJsonData("/pause", { paused: paused, context });
578
  } catch (e) {
579
- window.toastFetchError("Error pausing agent", e);
580
  }
581
  };
582
 
583
- window.resetChat = async function (ctxid = null) {
584
  try {
585
  const resp = await sendJsonData("/chat_reset", {
586
  context: ctxid === null ? context : ctxid,
@@ -588,20 +589,20 @@ window.resetChat = async function (ctxid = null) {
588
  resetCounter++;
589
  if (ctxid === null) updateAfterScroll();
590
  } catch (e) {
591
- window.toastFetchError("Error resetting chat", e);
592
  }
593
  };
594
 
595
- window.newChat = async function () {
596
  try {
597
  setContext(generateGUID());
598
  updateAfterScroll();
599
  } catch (e) {
600
- window.toastFetchError("Error creating new chat", e);
601
  }
602
  };
603
 
604
- window.killChat = async function (id) {
605
  if (!id) {
606
  console.error("No chat ID provided for deletion");
607
  return;
@@ -638,7 +639,7 @@ window.killChat = async function (id) {
638
  toast("Chat deleted successfully", "success");
639
  } catch (e) {
640
  console.error("Error deleting chat:", e);
641
- window.toastFetchError("Error deleting chat", e);
642
  }
643
  };
644
 
@@ -700,7 +701,7 @@ function ensureProperTabSelection(contextId) {
700
  return false;
701
  }
702
 
703
- window.selectChat = async function (id) {
704
  if (id === context) return; //already selected
705
 
706
  // Check if we need to switch tabs based on the context type
@@ -751,7 +752,7 @@ export const setContext = function (id) {
751
  chatHistory.innerHTML = "";
752
 
753
  // Update both selected states
754
- if (window.Alpine) {
755
  if (chatsSection) {
756
  const chatsAD = Alpine.$data(chatsSection);
757
  if (chatsAD) chatsAD.selected = id;
@@ -774,15 +775,15 @@ export const getChatBasedId = function (id) {
774
  return context + "-" + resetCounter + "-" + id;
775
  };
776
 
777
- window.toggleAutoScroll = async function (_autoScroll) {
778
  autoScroll = _autoScroll;
779
  };
780
 
781
- window.toggleJson = async function (showJson) {
782
  css.toggleCssProperty(".msg-json", "display", showJson ? "block" : "none");
783
  };
784
 
785
- window.toggleThoughts = async function (showThoughts) {
786
  css.toggleCssProperty(
787
  ".msg-thoughts",
788
  "display",
@@ -790,7 +791,7 @@ window.toggleThoughts = async function (showThoughts) {
790
  );
791
  };
792
 
793
- window.toggleUtils = async function (showUtils) {
794
  css.toggleCssProperty(
795
  ".message-util",
796
  "display",
@@ -798,7 +799,7 @@ window.toggleUtils = async function (showUtils) {
798
  );
799
  };
800
 
801
- window.toggleDarkMode = function (isDark) {
802
  if (isDark) {
803
  document.body.classList.remove("light-mode");
804
  document.body.classList.add("dark-mode");
@@ -810,13 +811,13 @@ window.toggleDarkMode = function (isDark) {
810
  localStorage.setItem("darkMode", isDark);
811
  };
812
 
813
- window.toggleSpeech = function (isOn) {
814
  console.log("Speech:", isOn);
815
  localStorage.setItem("speech", isOn);
816
  if (!isOn) speechStore.stopAudio();
817
  };
818
 
819
- window.nudge = async function () {
820
  try {
821
  const resp = await sendJsonData("/nudge", { ctxid: getContext() });
822
  } catch (e) {
@@ -824,7 +825,7 @@ window.nudge = async function () {
824
  }
825
  };
826
 
827
- window.restart = async function () {
828
  try {
829
  if (!getConnectionStatus()) {
830
  await toastFrontendError(
@@ -872,7 +873,7 @@ document.addEventListener("DOMContentLoaded", () => {
872
  toggleDarkMode(isDarkMode);
873
  });
874
 
875
- window.loadChats = async function () {
876
  try {
877
  const fileContents = await readJsonFiles();
878
  const response = await sendJsonData("/chat_load", { chats: fileContents });
@@ -896,7 +897,7 @@ window.loadChats = async function () {
896
  }
897
  };
898
 
899
- window.saveChat = async function () {
900
  try {
901
  const response = await sendJsonData("/chat_export", { ctxid: context });
902
 
@@ -1007,12 +1008,12 @@ function toast(text, type = "info", timeout = 5000) {
1007
  }
1008
 
1009
  }
1010
- window.toast = toast;
1011
 
1012
  // OLD: hideToast function removed - now using new notification system
1013
 
1014
  function scrollChanged(isAtBottom) {
1015
- if (window.Alpine && autoScrollSwitch) {
1016
  const inputAS = Alpine.$data(autoScrollSwitch);
1017
  if (inputAS) {
1018
  inputAS.autoScroll = isAtBottom;
@@ -1124,7 +1125,7 @@ function activateTab(tabName) {
1124
  chatsSection.style.display = "";
1125
 
1126
  // Get the available contexts from Alpine.js data
1127
- const chatsAD = window.Alpine ? Alpine.$data(chatsSection) : null;
1128
  const availableContexts = chatsAD?.contexts || [];
1129
 
1130
  // Restore previous chat selection
@@ -1148,7 +1149,7 @@ function activateTab(tabName) {
1148
  tasksSection.style.flexDirection = "column";
1149
 
1150
  // Get the available tasks from Alpine.js data
1151
- const tasksAD = window.Alpine ? Alpine.$data(tasksSection) : null;
1152
  const availableTasks = tasksAD?.tasks || [];
1153
 
1154
  // Restore previous task selection
@@ -1200,7 +1201,7 @@ function initializeActiveTab() {
1200
  // Open the scheduler detail view for a specific task
1201
  function openTaskDetail(taskId) {
1202
  // Wait for Alpine.js to be fully loaded
1203
- if (window.Alpine) {
1204
  // Get the settings modal button and click it to ensure all init logic happens
1205
  const settingsButton = document.getElementById("settings");
1206
  if (settingsButton) {
@@ -1215,7 +1216,7 @@ function openTaskDetail(taskId) {
1215
  }
1216
 
1217
  // Get the Alpine.js data for the modal
1218
- const modalData = window.Alpine ? Alpine.$data(modalEl) : null;
1219
 
1220
  // Use a timeout to ensure the modal is fully rendered
1221
  setTimeout(() => {
@@ -1234,7 +1235,7 @@ function openTaskDetail(taskId) {
1234
  }
1235
 
1236
  // Get the Alpine.js data for the scheduler component
1237
- const schedulerData = window.Alpine
1238
  ? Alpine.$data(schedulerComponent)
1239
  : null;
1240
 
@@ -1253,4 +1254,4 @@ function openTaskDetail(taskId) {
1253
  }
1254
 
1255
  // Make the function available globally
1256
- window.openTaskDetail = openTaskDetail;
 
4
  import { sleep } from "/js/sleep.js";
5
  import { store as attachmentsStore } from "/components/chat/attachments/attachmentsStore.js";
6
  import { store as speechStore } from "/components/chat/speech/speech-store.js";
7
+ import { store as notificationStore } from "/components/notifications/notification-store.js";
8
 
9
+ globalThis.fetchApi = api.fetchApi; // TODO - backward compatibility for non-modular scripts, remove once refactored to alpine
10
 
11
  const leftPanel = document.getElementById("left-panel");
12
  const rightPanel = document.getElementById("right-panel");
 
70
  }
71
  }
72
 
73
+ globalThis.addEventListener("load", handleResize);
74
+ globalThis.addEventListener("resize", handleResize);
75
 
76
  document.addEventListener("DOMContentLoaded", () => {
77
  const overlay = document.getElementById("sidebar-overlay");
 
185
  ).catch((e) => console.error("Failed to show connection error toast:", e));
186
  }
187
  }
188
+ globalThis.toastFetchError = toastFetchError;
189
 
190
  chatInput.addEventListener("keydown", (e) => {
191
  if (e.key === "Enter" && !e.shiftKey) {
 
242
  return result;
243
  }
244
 
245
+ globalThis.loadKnowledge = async function () {
246
  const input = document.createElement("input");
247
  input.type = "file";
248
  input.accept = ".txt,.pdf,.csv,.html,.json,.md";
 
301
  // const jsonResponse = await response.json();
302
  // return jsonResponse;
303
  };
304
+ globalThis.sendJsonData = sendJsonData;
305
 
306
  function generateGUID() {
307
  return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
 
317
 
318
  function setConnectionStatus(connected) {
319
  connectionStatus = connected;
320
+ if (globalThis.Alpine && timeDate) {
321
  const statusIconEl = timeDate.querySelector(".status-icon");
322
  if (statusIconEl) {
323
  const statusIcon = Alpine.$data(statusIconEl);
 
389
  notificationStore.updateFromPoll(response);
390
 
391
  //set ui model vars from backend
392
+ if (globalThis.Alpine && inputSection) {
393
  const inputAD = Alpine.$data(inputSection);
394
  if (inputAD) {
395
  inputAD.paused = response.paused;
 
402
  // Update chats list and sort by created_at time (newer first)
403
  let chatsAD = null;
404
  let contexts = response.contexts || [];
405
+ if (globalThis.Alpine && chatsSection) {
406
  chatsAD = Alpine.$data(chatsSection);
407
  if (chatsAD) {
408
  chatsAD.contexts = contexts.sort(
 
413
 
414
  // Update tasks list and sort by creation time (newer first)
415
  const tasksSection = document.getElementById("tasks-section");
416
+ if (globalThis.Alpine && tasksSection) {
417
  const tasksAD = Alpine.$data(tasksSection);
418
  if (tasksAD) {
419
  let tasks = response.tasks || [];
 
573
  }
574
  }
575
 
576
+ globalThis.pauseAgent = async function (paused) {
577
  try {
578
  const resp = await sendJsonData("/pause", { paused: paused, context });
579
  } catch (e) {
580
+ globalThis.toastFetchError("Error pausing agent", e);
581
  }
582
  };
583
 
584
+ globalThis.resetChat = async function (ctxid = null) {
585
  try {
586
  const resp = await sendJsonData("/chat_reset", {
587
  context: ctxid === null ? context : ctxid,
 
589
  resetCounter++;
590
  if (ctxid === null) updateAfterScroll();
591
  } catch (e) {
592
+ globalThis.toastFetchError("Error resetting chat", e);
593
  }
594
  };
595
 
596
+ globalThis.newChat = async function () {
597
  try {
598
  setContext(generateGUID());
599
  updateAfterScroll();
600
  } catch (e) {
601
+ globalThis.toastFetchError("Error creating new chat", e);
602
  }
603
  };
604
 
605
+ globalThis.killChat = async function (id) {
606
  if (!id) {
607
  console.error("No chat ID provided for deletion");
608
  return;
 
639
  toast("Chat deleted successfully", "success");
640
  } catch (e) {
641
  console.error("Error deleting chat:", e);
642
+ globalThis.toastFetchError("Error deleting chat", e);
643
  }
644
  };
645
 
 
701
  return false;
702
  }
703
 
704
+ globalThis.selectChat = async function (id) {
705
  if (id === context) return; //already selected
706
 
707
  // Check if we need to switch tabs based on the context type
 
752
  chatHistory.innerHTML = "";
753
 
754
  // Update both selected states
755
+ if (globalThis.Alpine) {
756
  if (chatsSection) {
757
  const chatsAD = Alpine.$data(chatsSection);
758
  if (chatsAD) chatsAD.selected = id;
 
775
  return context + "-" + resetCounter + "-" + id;
776
  };
777
 
778
+ globalThis.toggleAutoScroll = async function (_autoScroll) {
779
  autoScroll = _autoScroll;
780
  };
781
 
782
+ globalThis.toggleJson = async function (showJson) {
783
  css.toggleCssProperty(".msg-json", "display", showJson ? "block" : "none");
784
  };
785
 
786
+ globalThis.toggleThoughts = async function (showThoughts) {
787
  css.toggleCssProperty(
788
  ".msg-thoughts",
789
  "display",
 
791
  );
792
  };
793
 
794
+ globalThis.toggleUtils = async function (showUtils) {
795
  css.toggleCssProperty(
796
  ".message-util",
797
  "display",
 
799
  );
800
  };
801
 
802
+ globalThis.toggleDarkMode = function (isDark) {
803
  if (isDark) {
804
  document.body.classList.remove("light-mode");
805
  document.body.classList.add("dark-mode");
 
811
  localStorage.setItem("darkMode", isDark);
812
  };
813
 
814
+ globalThis.toggleSpeech = function (isOn) {
815
  console.log("Speech:", isOn);
816
  localStorage.setItem("speech", isOn);
817
  if (!isOn) speechStore.stopAudio();
818
  };
819
 
820
+ globalThis.nudge = async function () {
821
  try {
822
  const resp = await sendJsonData("/nudge", { ctxid: getContext() });
823
  } catch (e) {
 
825
  }
826
  };
827
 
828
+ globalThis.restart = async function () {
829
  try {
830
  if (!getConnectionStatus()) {
831
  await toastFrontendError(
 
873
  toggleDarkMode(isDarkMode);
874
  });
875
 
876
+ globalThis.loadChats = async function () {
877
  try {
878
  const fileContents = await readJsonFiles();
879
  const response = await sendJsonData("/chat_load", { chats: fileContents });
 
897
  }
898
  };
899
 
900
+ globalThis.saveChat = async function () {
901
  try {
902
  const response = await sendJsonData("/chat_export", { ctxid: context });
903
 
 
1008
  }
1009
 
1010
  }
1011
+ globalThis.toast = toast;
1012
 
1013
  // OLD: hideToast function removed - now using new notification system
1014
 
1015
  function scrollChanged(isAtBottom) {
1016
+ if (globalThis.Alpine && autoScrollSwitch) {
1017
  const inputAS = Alpine.$data(autoScrollSwitch);
1018
  if (inputAS) {
1019
  inputAS.autoScroll = isAtBottom;
 
1125
  chatsSection.style.display = "";
1126
 
1127
  // Get the available contexts from Alpine.js data
1128
+ const chatsAD = globalThis.Alpine ? Alpine.$data(chatsSection) : null;
1129
  const availableContexts = chatsAD?.contexts || [];
1130
 
1131
  // Restore previous chat selection
 
1149
  tasksSection.style.flexDirection = "column";
1150
 
1151
  // Get the available tasks from Alpine.js data
1152
+ const tasksAD = globalThis.Alpine ? Alpine.$data(tasksSection) : null;
1153
  const availableTasks = tasksAD?.tasks || [];
1154
 
1155
  // Restore previous task selection
 
1201
  // Open the scheduler detail view for a specific task
1202
  function openTaskDetail(taskId) {
1203
  // Wait for Alpine.js to be fully loaded
1204
+ if (globalThis.Alpine) {
1205
  // Get the settings modal button and click it to ensure all init logic happens
1206
  const settingsButton = document.getElementById("settings");
1207
  if (settingsButton) {
 
1216
  }
1217
 
1218
  // Get the Alpine.js data for the modal
1219
+ const modalData = globalThis.Alpine ? Alpine.$data(modalEl) : null;
1220
 
1221
  // Use a timeout to ensure the modal is fully rendered
1222
  setTimeout(() => {
 
1235
  }
1236
 
1237
  // Get the Alpine.js data for the scheduler component
1238
+ const schedulerData = globalThis.Alpine
1239
  ? Alpine.$data(schedulerComponent)
1240
  : null;
1241
 
 
1254
  }
1255
 
1256
  // Make the function available globally
1257
+ globalThis.openTaskDetail = openTaskDetail;