finalize the notification system

Rafael Uzarowski committed Jul 12, 2025 at 16:27 UTC e32a04fb77d3706dc81d94b0c133299737ed3318
18 files changed +453 -334
docs/notifications.md new
+130
@@ -0,0 +1,130 @@
1 +# Agent Zero Notifications
2 +
3 +Quick guide for using the notification system in Agent Zero.
4 +
5 +## Backend Usage
6 +
7 +Use `AgentNotification` helper methods anywhere in your Python code:
8 +
9 +```python
10 +from python.helpers.notification import AgentNotification
11 +
12 +# Basic notifications
13 +AgentNotification.info("Operation completed")
14 +AgentNotification.success("File saved successfully", "File Manager")
15 +AgentNotification.warning("High CPU usage detected", "System Monitor")
16 +AgentNotification.error("Connection failed", "Network Error")
17 +AgentNotification.progress("Processing files...", "Task Progress")
18 +
19 +# With details and custom display time
20 +AgentNotification.info(
21 + message="System backup completed",
22 + title="Backup Manager",
23 + detail="<p>Backup size: <strong>2.4 GB</strong></p>",
24 + display_time=8 # seconds
25 +)
26 +
27 +# Grouped notifications (replaces previous in same group)
28 +AgentNotification.progress("Download: 25%", "File Download", group="download-status")
29 +AgentNotification.progress("Download: 75%", "File Download", group="download-status") # Replaces previous
30 +AgentNotification.progress("Download: Complete!", "File Download", group="download-status") # Replaces previous
31 +```
32 +
33 +## Frontend Usage
34 +
35 +Use the notification store in Alpine.js components:
36 +
37 +```javascript
38 +// Basic notifications
39 +$store.notificationStore.info("User logged in")
40 +$store.notificationStore.success("Settings saved", "Configuration")
41 +$store.notificationStore.warning("Session expiring soon")
42 +$store.notificationStore.error("Failed to load data")
43 +
44 +// With grouping
45 +$store.notificationStore.info("Connecting...", "Status", "", 3, "connection")
46 +$store.notificationStore.success("Connected!", "Status", "", 3, "connection") // Replaces previous
47 +
48 +// Frontend notifications with backend persistence (new feature!)
49 +$store.notificationStore.frontendError("Database timeout", "Connection Error")
50 +$store.notificationStore.frontendWarning("High memory usage", "Performance")
51 +$store.notificationStore.frontendInfo("Cache cleared", "System")
52 +```
53 +
54 +## Frontend Notifications with Backend Sync
55 +
56 +**New Feature**: Frontend notifications now automatically sync to the backend when connected, providing persistent history and cross-session availability.
57 +
58 +### How it Works:
59 +- **Backend Connected**: Notifications are sent to backend and appear via polling (persistent)
60 +- **Backend Disconnected**: Notifications show as frontend-only toasts (temporary)
61 +- **Automatic Fallback**: Seamless degradation when backend is unavailable
62 +
63 +### Global Functions:
64 +```javascript
65 +// These functions automatically try backend first, then fallback to frontend-only
66 +toastFrontendError("Server unreachable", "Connection Error")
67 +toastFrontendWarning("Slow connection detected")
68 +toastFrontendInfo("Reconnected successfully")
69 +```
70 +
71 +## HTML Usage
72 +
73 +```html
74 +<button @click="$store.notificationStore.success('Task completed!')">
75 + Complete Task
76 +</button>
77 +
78 +<button @click="$store.notificationStore.warning('Progress: 50%', 'Upload', '', 5, 'upload-progress')">
79 + Update Progress
80 +</button>
81 +
82 +<!-- Frontend notifications with backend sync -->
83 +<button @click="$store.notificationStore.frontendError('Connection failed', 'Network')">
84 + Report Connection Error
85 +</button>
86 +```
87 +
88 +## Notification Groups
89 +
90 +Groups ensure only the latest notification from each group is shown in the toast stack:
91 +
92 +```python
93 +# Progress updates - each new notification replaces the previous one
94 +AgentNotification.info("Starting backup...", group="backup-status")
95 +AgentNotification.progress("Backup: 30%", group="backup-status") # Replaces previous
96 +AgentNotification.progress("Backup: 80%", group="backup-status") # Replaces previous
97 +AgentNotification.success("Backup complete!", group="backup-status") # Replaces previous
98 +
99 +# Connection status - only show current state
100 +AgentNotification.warning("Disconnected", group="network")
101 +AgentNotification.info("Reconnecting...", group="network") # Replaces previous
102 +AgentNotification.success("Connected", group="network") # Replaces previous
103 +```
104 +
105 +## Parameters
106 +
107 +All notification methods support these parameters:
108 +
109 +- `message` (required): Main notification text
110 +- `title` (optional): Notification title
111 +- `detail` (optional): HTML content for expandable details
112 +- `display_time` (optional): Toast display duration in seconds (default: 3)
113 +- `group` (optional): Group identifier for replacement behavior
114 +
115 +## Types
116 +
117 +- **info** (ℹ️): General information
118 +- **success** (✅): Successful operations
119 +- **warning** (⚠️): Important alerts
120 +- **error** (❌): Error conditions
121 +- **progress** (⏳): Ongoing operations
122 +
123 +## Behavior
124 +
125 +- **Toast Display**: Notifications appear as toasts in the bottom-right corner
126 +- **Persistent History**: All notifications (including synced frontend ones) are stored in notification history
127 +- **Modal Access**: Full history accessible via the bell icon
128 +- **Auto-dismiss**: Toasts automatically disappear after `display_time`
129 +- **Group Replacement**: Notifications with the same group replace previous ones immediately
130 +- **Backend Sync**: Frontend notifications automatically sync to backend when connected
python/api/notification_create.py
+7 -6
@@ -15,6 +15,7 @@ class NotificationCreate(ApiHandler):
15 title = input.get("title", "")
16 detail = input.get("detail", "")
17 display_time = input.get("display_time", 3) # Default to 3 seconds
18 + group = input.get("group", "") # Group parameter for notification grouping
19
20 # Validate required fields
21 if not message:
@@ -38,17 +39,17 @@ class NotificationCreate(ApiHandler):
39 # Create notification using the appropriate helper method
40 try:
41 if notification_type == NotificationType.INFO:
41 - notification = AgentNotification.info(message, title, detail, display_time)
42 + notification = AgentNotification.info(message, title, detail, display_time, group)
43 elif notification_type == NotificationType.SUCCESS:
43 - notification = AgentNotification.success(message, title, detail, display_time)
44 + notification = AgentNotification.success(message, title, detail, display_time, group)
45 elif notification_type == NotificationType.WARNING:
45 - notification = AgentNotification.warning(message, title, detail, display_time)
46 + notification = AgentNotification.warning(message, title, detail, display_time, group)
47 elif notification_type == NotificationType.ERROR:
47 - notification = AgentNotification.error(message, title, detail, display_time)
48 + notification = AgentNotification.error(message, title, detail, display_time, group)
49 elif notification_type == NotificationType.PROGRESS:
49 - notification = AgentNotification.progress(message, title, detail, display_time)
50 + notification = AgentNotification.progress(message, title, detail, display_time, group)
51 else:
51 - notification = AgentNotification.info(message, title, detail, display_time)
52 + notification = AgentNotification.info(message, title, detail, display_time, group)
53
54 return {
55 "success": True,
python/helpers/notification.py
+14 -10
@@ -24,6 +24,7 @@ class NotificationItem:
24 display_time: int = 3 # Display duration in seconds, default 3 seconds
25 read: bool = False
26 id: str = ""
27 + group: str = "" # Group identifier for grouping related notifications
28
29 def __post_init__(self):
30 if not self.id:
@@ -47,6 +48,7 @@ class NotificationItem:
48 "timestamp": self.timestamp.isoformat(),
49 "display_time": self.display_time,
50 "read": self.read,
51 + "group": self.group,
52 }
53
54
@@ -64,6 +66,7 @@ class NotificationManager:
66 title: str = "",
67 detail: str = "",
68 display_time: int = 3,
69 + group: str = "",
70 ) -> NotificationItem:
71 # Create notification item
72 item = NotificationItem(
@@ -75,6 +78,7 @@ class NotificationManager:
78 detail=detail,
79 timestamp=datetime.now(timezone.utc),
80 display_time=display_time,
81 + group=group,
82 )
83
84 # Add to notifications
@@ -139,36 +143,36 @@ class NotificationManager:
143
144 class AgentNotification:
145 @staticmethod
142 - def info(message: str, title: str = "", detail: str = "", display_time: int = 3) -> NotificationItem:
146 + def info(message: str, title: str = "", detail: str = "", display_time: int = 3, group: str = "") -> NotificationItem:
147 from agent import AgentContext
148 return AgentContext.get_notification_manager().add_notification(
145 - NotificationType.INFO, message, title, detail, display_time
149 + NotificationType.INFO, message, title, detail, display_time, group
150 )
151
152 @staticmethod
149 - def success(message: str, title: str = "", detail: str = "", display_time: int = 3) -> NotificationItem:
153 + def success(message: str, title: str = "", detail: str = "", display_time: int = 3, group: str = "") -> NotificationItem:
154 from agent import AgentContext
155 return AgentContext.get_notification_manager().add_notification(
152 - NotificationType.SUCCESS, message, title, detail, display_time
156 + NotificationType.SUCCESS, message, title, detail, display_time, group
157 )
158
159 @staticmethod
156 - def warning(message: str, title: str = "", detail: str = "", display_time: int = 3) -> NotificationItem:
160 + def warning(message: str, title: str = "", detail: str = "", display_time: int = 3, group: str = "") -> NotificationItem:
161 from agent import AgentContext
162 return AgentContext.get_notification_manager().add_notification(
159 - NotificationType.WARNING, message, title, detail, display_time
163 + NotificationType.WARNING, message, title, detail, display_time, group
164 )
165
166 @staticmethod
163 - def error(message: str, title: str = "", detail: str = "", display_time: int = 3) -> NotificationItem:
167 + def error(message: str, title: str = "", detail: str = "", display_time: int = 3, group: str = "") -> NotificationItem:
168 from agent import AgentContext
169 return AgentContext.get_notification_manager().add_notification(
166 - NotificationType.ERROR, message, title, detail, display_time
170 + NotificationType.ERROR, message, title, detail, display_time, group
171 )
172
173 @staticmethod
170 - def progress(message: str, title: str = "", detail: str = "", display_time: int = 3) -> NotificationItem:
174 + def progress(message: str, title: str = "", detail: str = "", display_time: int = 3, group: str = "") -> NotificationItem:
175 from agent import AgentContext
176 return AgentContext.get_notification_manager().add_notification(
173 - NotificationType.PROGRESS, message, title, detail, display_time
177 + NotificationType.PROGRESS, message, title, detail, display_time, group
178 )
webui/components/notifications/notification-icons.html
+1 -16
@@ -2,12 +2,11 @@
2 <head>
3 <script type="module">
4 import { store } from "/js/notificationStore.js";
5 - console.log('Notification component script loaded');
5 </script>
6 </head>
7 <body>
8 <div x-data>
10 - <!-- Notification Toggle Button (always visible and clickable) -->
9 + <!-- Notification Toggle Button -->
10 <div class="notification-toggle"
11 :class="{
12 'has-unread': $store.notificationStore.unreadCount > 0,
@@ -22,20 +21,6 @@
21 class="notification-badge"
22 x-text="$store.notificationStore.unreadCount"></span>
23 </div>
25 -
26 - <!-- Test Notification Button (for development) -->
27 - <button class="notification-test-button"
28 - @click="$store.notificationStore.info('Test notification message', 'Test Title', '<p>This is test detail content with <strong>HTML</strong> formatting.</p>')"
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>
24 </div>
25 </body>
26 </html>
webui/components/notifications/notification-modal.html
-3
@@ -341,9 +341,6 @@
341 function notificationModalComponent() {
342 return {
343 init() {
344 - // Initialize component when modal loads
345 - console.log('Notification modal component initialized');
346 -
344 // Mark all notifications as read when modal opens
345 // This can be configured later if needed
346 setTimeout(() => {
webui/components/settings/backup/backup-store.js
+6 -5
@@ -3,6 +3,7 @@ import { createStore } from "/js/AlpineStore.js";
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.
@@ -375,9 +376,9 @@ const model = {
376 const fileList = this.previewFiles.map(f => f.path).join('\n');
377 try {
378 await navigator.clipboard.writeText(fileList);
378 - toast('File list copied to clipboard', 'success');
379 + window.toastFrontendInfo('File list copied to clipboard', 'Clipboard');
380 } catch (error) {
380 - toast('Failed to copy to clipboard', 'error');
381 + window.toastFrontendError('Failed to copy to clipboard', 'Clipboard Error');
382 }
383 },
384
@@ -419,7 +420,7 @@ const model = {
420 window.URL.revokeObjectURL(url);
421
422 this.addFileOperation('Backup created and downloaded successfully!');
422 - toast('Backup created and downloaded successfully', 'success');
423 + window.toastFrontendInfo('Backup created and downloaded successfully', 'Backup Status');
424 } else {
425 // Try to parse error response
426 const errorText = await response.text();
@@ -677,7 +678,7 @@ const model = {
678 }
679
680 if (warnings.length > 0) {
680 - toast(`Compatibility warnings: ${warnings.join(', ')}`, 'warning');
681 + window.toastFrontendWarning(`Compatibility warnings: ${warnings.join(', ')}`, 'Backup Compatibility');
682 }
683 },
684
@@ -746,7 +747,7 @@ const model = {
747
748 this.addFileOperation(`\nRestore completed: ${deletedCount} deleted, ${restoredCount} restored, ${skippedCount} skipped, ${errorCount} errors`);
749 this.restoreResult = result;
749 - toast('Restore completed successfully', 'success');
750 + window.toastFrontendInfo('Restore completed successfully', 'Restore Status');
751 } else {
752 this.error = result.error;
753 this.addFileOperation(`Error: ${result.error}`);
webui/components/settings/mcp/client/mcp-servers-store.js
-1
@@ -113,7 +113,6 @@ const model = {
113 scrollModal("mcp-servers-status");
114 } catch (error) {
115 console.error("Failed to apply MCP servers:", error);
116 - alert("Failed to apply MCP servers: " + error.message);
116 }
117 this.loading = false;
118 },
webui/css/notification.css
-53
@@ -83,42 +83,7 @@
83 transform: none;
84 }
85
86 -/* Test Button (for development) - positioned next to bell icon */
87 -.notification-test-button {
88 - display: flex;
89 - align-items: center;
90 - justify-content: center;
91 - width: 36px;
92 - height: 36px;
93 - background: rgba(33, 150, 243, 0.1);
94 - border: 1px solid rgba(33, 150, 243, 0.3);
95 - border-radius: 6px;
96 - color: var(--color-text);
97 - cursor: pointer;
98 - transition: all 0.2s ease;
99 - font-size: 0.85rem;
100 - margin-left: 0.5rem;
101 -}
86
103 -.notification-test-button:hover {
104 - background: rgba(33, 150, 243, 0.2);
105 - border-color: rgba(33, 150, 243, 0.5);
106 -}
107 -
108 -.notification-test-button .notification-icon {
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 -}
87
88 /* Light Mode Styles */
89 .light-mode .notification-toggle {
@@ -141,25 +106,7 @@
106 border-color: rgba(0, 0, 0, 0.1);
107 }
108
144 -.light-mode .notification-test-button {
145 - background: rgba(33, 150, 243, 0.1);
146 - border-color: rgba(33, 150, 243, 0.3);
147 -}
148 -
149 -.light-mode .notification-test-button:hover {
150 - background: rgba(33, 150, 243, 0.2);
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 -}
109
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 -}
110
111 /* Animations */
112 @keyframes pulse {
webui/index.js
+52 -95
@@ -23,7 +23,7 @@ const timeDate = document.getElementById("time-date-container");
23
24 let autoScroll = true;
25 let context = "";
26 -let connectionStatus = false;
26 +let connectionStatus = undefined; // undefined = not checked yet, true = connected, false = disconnected
27
28 // Initialize the toggle button
29 setupSidebarToggle();
@@ -161,20 +161,26 @@ export async function sendMessage() {
161 }
162 }
163 } catch (e) {
164 - toastFetchError("Error sending message", e);
164 + toastFetchError("Error sending message", e); // Will use new notification system
165 }
166 }
167
168 function toastFetchError(text, error) {
169 + console.error(text, error);
170 + // Use new frontend error notification system (async, but we don't need to wait)
171 + const errorMessage = error?.message || error?.toString() || "Unknown error";
172 +
173 if (getConnectionStatus()) {
170 - toast(`${text}: ${error.message}`, "error");
174 + // Backend is connected, just show the error
175 + toastFrontendError(`${text}: ${errorMessage}`).catch(e =>
176 + console.error('Failed to show error toast:', e)
177 + );
178 } else {
172 - toast(
173 - `${text} (it seems the backend is not running): ${error.message}`,
174 - "error"
179 + // Backend is disconnected, show connection error
180 + toastFrontendError(`${text} (backend appears to be disconnected): ${errorMessage}`, "Connection Error").catch(e =>
181 + console.error('Failed to show connection error toast:', e)
182 );
183 }
177 - console.error(text, error);
184 }
185 window.toastFetchError = toastFetchError;
186
@@ -373,7 +379,17 @@ async function poll() {
379
380 // Update notifications from response
381 if (globalThis.Alpine?.store('notificationStore')) {
376 - globalThis.Alpine.store('notificationStore').updateFromPoll(response);
382 + const notificationStore = globalThis.Alpine.store('notificationStore');
383 +
384 + // Ensure store is initialized
385 + if (!notificationStore.lastNotificationGuid && response.notifications_guid) {
386 + console.log('Initializing notification store on fresh load');
387 + notificationStore.initialize();
388 + }
389 +
390 + notificationStore.updateFromPoll(response);
391 + } else {
392 + console.warn('Notification store not available during poll');
393 }
394
395 //set ui model vars from backend
@@ -813,8 +829,6 @@ window.restart = async function () {
829 const resp = await sendJsonData("/health", {});
830 // Server is back up, show success message
831 await new Promise((resolve) => setTimeout(resolve, 250));
816 - hideToast();
817 - await new Promise((resolve) => setTimeout(resolve, 400));
832 toast("Restarted", "success", 5000);
833 return;
834 } catch (e) {
@@ -825,8 +839,6 @@ window.restart = async function () {
839 }
840
841 // If we get here, restart failed or took too long
828 - hideToast();
829 - await new Promise((resolve) => setTimeout(resolve, 400));
842 toast("Restart timed out or failed", "error", 5000);
843 }
844 };
@@ -955,95 +967,32 @@ function removeClassFromElement(element, className) {
967 }
968
969 function toast(text, type = "info", timeout = 5000) {
958 - const toast = document.getElementById("toast");
959 - const isVisible = toast.classList.contains("show");
960 -
961 - // Clear any existing timeout immediately
962 - if (toast.timeoutId) {
963 - clearTimeout(toast.timeoutId);
964 - toast.timeoutId = null;
965 - }
966 -
967 - // Function to update toast content and show it
968 - const updateAndShowToast = () => {
969 - // Update the toast content and type
970 - const title = type.charAt(0).toUpperCase() + type.slice(1);
971 - toast.querySelector(".toast__title").textContent = title;
972 - toast.querySelector(".toast__message").textContent = text;
973 -
974 - // Remove old classes and add new ones
975 - toast.classList.remove("toast--success", "toast--error", "toast--info");
976 - toast.classList.add(`toast--${type}`);
977 -
978 - // Show/hide copy button based on toast type
979 - const copyButton = toast.querySelector(".toast__copy");
980 - copyButton.style.display = type === "error" ? "inline-block" : "none";
981 -
982 - // Add the close button event listener
983 - const closeButton = document.querySelector(".toast__close");
984 - closeButton.onclick = () => {
985 - hideToast();
986 - };
987 -
988 - // Add the copy button event listener
989 - copyButton.onclick = () => {
990 - navigator.clipboard.writeText(text);
991 - copyButton.textContent = "Copied!";
992 - setTimeout(() => {
993 - copyButton.textContent = "Copy";
994 - }, 2000);
995 - };
996 -
997 - // Show the toast
998 - toast.style.display = "flex";
999 - // Force a reflow to ensure the animation triggers
1000 - void toast.offsetWidth;
1001 - toast.classList.add("show");
1002 -
1003 - // Set timeout if specified
1004 - if (timeout) {
1005 - const minTimeout = Math.max(timeout, 5000);
1006 - toast.timeoutId = setTimeout(() => {
1007 - hideToast();
1008 - }, minTimeout);
970 + // Convert timeout from milliseconds to seconds for new notification system
971 + const display_time = Math.max(timeout / 1000, 3); // Minimum 3 seconds
972 +
973 + // Use new frontend notification system based on type
974 + if (window.Alpine && window.Alpine.store && window.Alpine.store('notificationStore')) {
975 + const store = window.Alpine.store('notificationStore');
976 + switch (type.toLowerCase()) {
977 + case 'error':
978 + return store.frontendError(text, "Error", display_time);
979 + case 'success':
980 + return store.frontendInfo(text, "Success", display_time);
981 + case 'warning':
982 + return store.frontendWarning(text, "Warning", display_time);
983 + case 'info':
984 + default:
985 + return store.frontendInfo(text, "Info", display_time);
986 }
1010 - };
1011 -
1012 - if (isVisible) {
1013 - // If a toast is visible, hide it first then show the new one
1014 - toast.classList.remove("show");
1015 - toast.classList.add("hide");
1016 -
1017 - // Wait for hide animation to complete before showing new toast
1018 - setTimeout(() => {
1019 - toast.classList.remove("hide");
1020 - updateAndShowToast();
1021 - }, 400); // Match this with CSS transition duration
987 } else {
1023 - // If no toast is visible, show the new one immediately
1024 - updateAndShowToast();
988 + // Fallback if Alpine/store not ready
989 + console.log(`${type.toUpperCase()}: ${text}`);
990 + return null;
991 }
992 }
993 window.toast = toast;
994
1029 -function hideToast() {
1030 - const toast = document.getElementById("toast");
1031 -
1032 - // Clear any existing timeout
1033 - if (toast.timeoutId) {
1034 - clearTimeout(toast.timeoutId);
1035 - toast.timeoutId = null;
1036 - }
1037 -
1038 - toast.classList.remove("show");
1039 - toast.classList.add("hide");
1040 -
1041 - // Wait for the hide animation to complete before removing from display
1042 - setTimeout(() => {
1043 - toast.style.display = "none";
1044 - toast.classList.remove("hide");
1045 - }, 400); // Match this with CSS transition duration
1046 -}
995 +// OLD: hideToast function removed - now using new notification system
996
997 function scrollChanged(isAtBottom) {
998 if (window.Alpine && autoScrollSwitch) {
@@ -1106,6 +1055,14 @@ document.addEventListener("DOMContentLoaded", function () {
1055 setupSidebarToggle();
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
1066 });
1067
1068 // Setup tabs functionality
webui/js/file_browser.js
+6 -6
@@ -53,7 +53,7 @@ const fileBrowserModalProxy = {
53 this.browser.entries = [];
54 }
55 } catch (error) {
56 - window.toastFetchError("Error fetching files", error);
56 + window.toastFrontendError("Error fetching files: " + error.message, "File Browser Error");
57 this.browser.entries = [];
58 } finally {
59 this.isLoading = false;
@@ -134,7 +134,7 @@ const fileBrowserModalProxy = {
134 alert(`Error deleting file: ${await response.text()}`);
135 }
136 } catch (error) {
137 - window.toastFetchError("Error deleting file", error);
137 + window.toastFrontendError("Error deleting file: " + error.message, "File Delete Error");
138 alert("Error deleting file");
139 }
140 },
@@ -188,7 +188,7 @@ const fileBrowserModalProxy = {
188 alert(data.message);
189 }
190 } catch (error) {
191 - window.toastFetchError("Error uploading files", error);
191 + window.toastFrontendError("Error uploading files: " + error.message, "File Upload Error");
192 alert("Error uploading files");
193 }
194 },
@@ -215,7 +215,7 @@ const fileBrowserModalProxy = {
215 document.body.removeChild(link);
216 window.URL.revokeObjectURL(link.href);
217 } catch (error) {
218 - window.toastFetchError("Error downloading file", error);
218 + window.toastFrontendError("Error downloading file: " + error.message, "File Download Error");
219 alert("Error downloading file");
220 }
221 },
@@ -267,7 +267,7 @@ openFileLink = async function (path) {
267 try {
268 const resp = await window.sendJsonData("/file_info", { path });
269 if (!resp.exists) {
270 - window.toast("File does not exist.", "error");
270 + window.toastFrontendError("File does not exist.", "File Error");
271 return;
272 }
273
@@ -280,7 +280,7 @@ openFileLink = async function (path) {
280 });
281 }
282 } catch (e) {
283 - window.toastFetchError("Error opening file", e);
283 + window.toastFrontendError("Error opening file: " + e.message, "File Open Error");
284 }
285 };
286 window.openFileLink = openFileLink;
webui/js/history.js
+2 -2
@@ -8,7 +8,7 @@ export async function openHistoryModal() {
8 const size = hist.tokens
9 await showEditorModal(data, "markdown", `History ~${size} tokens`, "Conversation history visible to the LLM. History is compressed to fit into the context window over time.");
10 } catch (e) {
11 - window.toastFetchError("Error fetching history", e)
11 + window.toastFrontendError("Error fetching history: " + e.message, "Chat History Error");
12 return
13 }
14 }
@@ -20,7 +20,7 @@ export async function openCtxWindowModal() {
20 const size = win.tokens
21 await showEditorModal(data, "markdown", `Context window ~${size} tokens`, "Data passed to the LLM during last interaction. Contains system message, conversation history and RAG.");
22 } catch (e) {
23 - window.toastFetchError("Error fetching context", e)
23 + window.toastFrontendError("Error fetching context: " + e.message, "Context Error");
24 return
25 }
26 }
webui/js/image_modal.js
+8 -8
@@ -4,13 +4,13 @@ let activeIntervalId = null;
4 export async function openImageModal(src, refreshInterval = 0) {
5 try {
6 let imgSrc = src;
7 -
7 +
8 // Clear any existing refresh interval
9 if (activeIntervalId !== null) {
10 clearInterval(activeIntervalId);
11 activeIntervalId = null;
12 }
13 -
13 +
14 if (refreshInterval > 0) {
15 // Add or update timestamp to bypass cache
16 const addTimestamp = (url) => {
@@ -23,7 +23,7 @@ export async function openImageModal(src, refreshInterval = 0) {
23 const isImageViewerActive = () => {
24 const container = document.querySelector('#image-viewer-container');
25 if (!container) return false;
26 -
26 +
27 // Check if element or any parent is hidden
28 let element = container;
29 while (element) {
@@ -46,7 +46,7 @@ export async function openImageModal(src, refreshInterval = 0) {
46 tempImg.onerror = reject;
47 tempImg.src = nextSrc;
48 });
49 -
49 +
50 try {
51 // Wait for preload to complete
52 const loadedSrc = await preloadPromise;
@@ -58,9 +58,9 @@ export async function openImageModal(src, refreshInterval = 0) {
58 console.error('Failed to preload image:', err);
59 }
60 };
61 -
61 +
62 imgSrc = addTimestamp(src);
63 -
63 +
64 // Set up periodic refresh with preloading
65 activeIntervalId = setInterval(() => {
66 if (!isImageViewerActive()) {
@@ -77,11 +77,11 @@ export async function openImageModal(src, refreshInterval = 0) {
77
78 const html = `<div id="image-viewer-container"><img class="image-viewer-img" src="${imgSrc}" /></div>`;
79 const fileName = src.split("/").pop();
80 -
80 +
81 // Open the modal with the generated HTML
82 await window.genericModalProxy.openModal(fileName, "", html);
83 } catch (e) {
84 - window.toastFetchError("Error fetching history", e);
84 + window.toastFrontendError("Error fetching history: " + e.message, "Image History Error");
85 return;
86 }
87 }
webui/js/notificationStore.js
+119 -38
@@ -14,7 +14,6 @@ const model = {
14
15 // Initialize the notification store
16 initialize() {
17 - console.log("NotificationStore: Initializing with toast stack");
17 this.loading = true;
18 this.updateUnreadCount();
19 this.removeOldNotifications();
@@ -31,6 +30,8 @@ const model = {
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;
@@ -45,7 +46,7 @@ const model = {
46 const isNew = !this.notifications.find(n => n.id === notification.id);
47 this.addOrUpdateNotification(notification);
48
48 - // Add new notifications to toast stack
49 + // Add new unread notifications to toast stack
50 if (isNew && !notification.read) {
51 this.addToToastStack(notification);
52 }
@@ -70,6 +71,21 @@ const model = {
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,
@@ -93,8 +109,6 @@ const model = {
109 toast.autoRemoveTimer = setTimeout(() => {
110 this.removeFromToastStack(toast.toastId);
111 }, notification.display_time * 1000);
96 -
97 - console.log(`Toast added: ${notification.type} - ${notification.message}`);
112 },
113
114 // NEW: Remove toast from stack
@@ -106,7 +120,6 @@ const model = {
120 clearTimeout(toast.autoRemoveTimer);
121 }
122 this.toastStack.splice(index, 1);
109 - console.log(`Toast removed: ${toastId}`);
123 }
124 },
125
@@ -118,7 +131,6 @@ const model = {
131 }
132 });
133 this.toastStack = [];
121 - console.log('Toast stack cleared');
134 },
135
136 // NEW: Clean up expired toasts (backup cleanup)
@@ -140,7 +152,6 @@ const model = {
152
153 // NEW: Handle toast click (opens modal)
154 async handleToastClick(toastId) {
143 - console.log(`Toast clicked: ${toastId}`);
155 await this.openModal();
156 // Modal opening will clear toast stack via markAllAsRead
157 },
@@ -213,7 +224,6 @@ const model = {
224 this.clearToastStack(); // Also clear toast stack
225
226 // Note: We don't sync clear with backend as notifications are stored in memory only
216 - console.log('All notifications cleared');
227 },
228
229 // Get notifications by type
@@ -309,18 +319,18 @@ const model = {
319 },
320
321 // Create notification via backend (will appear via polling)
312 - async createNotification(type, message, title = "", detail = "", display_time = 3) {
322 + async createNotification(type, message, title = "", detail = "", display_time = 3, group = "") {
323 try {
324 const response = await window.sendJsonData('/notification_create', {
325 type: type,
326 message: message,
327 title: title,
328 detail: detail,
319 - display_time: display_time
329 + display_time: display_time,
330 + group: group
331 });
332
333 if (response.success) {
323 - console.log('Notification created:', response.notification_id);
334 return response.notification_id;
335 } else {
336 console.error('Failed to create notification:', response.error);
@@ -333,24 +343,24 @@ const model = {
343 },
344
345 // Convenience methods for different notification types
336 - async info(message, title = "", detail = "", display_time = 3) {
337 - return await this.createNotification('info', message, title, detail, display_time);
346 + async info(message, title = "", detail = "", display_time = 3, group = "") {
347 + return await this.createNotification('info', message, title, detail, display_time, group);
348 },
349
340 - async success(message, title = "", detail = "", display_time = 3) {
341 - return await this.createNotification('success', message, title, detail, display_time);
350 + async success(message, title = "", detail = "", display_time = 3, group = "") {
351 + return await this.createNotification('success', message, title, detail, display_time, group);
352 },
353
344 - async warning(message, title = "", detail = "", display_time = 3) {
345 - return await this.createNotification('warning', message, title, detail, display_time);
354 + async warning(message, title = "", detail = "", display_time = 3, group = "") {
355 + return await this.createNotification('warning', message, title, detail, display_time, group);
356 },
357
348 - async error(message, title = "", detail = "", display_time = 3) {
349 - return await this.createNotification('error', message, title, detail, display_time);
358 + async error(message, title = "", detail = "", display_time = 3, group = "") {
359 + return await this.createNotification('error', message, title, detail, display_time, group);
360 },
361
352 - async progress(message, title = "", detail = "", display_time = 3) {
353 - return await this.createNotification('progress', message, title, detail, display_time);
362 + async progress(message, title = "", detail = "", display_time = 3, group = "") {
363 + return await this.createNotification('progress', message, title, detail, display_time, group);
364 },
365
366 // Enhanced: Open modal and clear toast stack
@@ -368,8 +378,24 @@ const model = {
378 this.openModal();
379 },
380
371 - // NEW: Add frontend-only toast directly to stack (for connection errors, etc.)
372 - addFrontendToast(type, message, title = "", display_time = 5) {
381 + // NEW: Check if backend connection is available
382 + isConnected() {
383 + // Use the global connection status from index.js, but default to true if undefined
384 + // This handles the case where polling hasn't run yet but backend is actually available
385 + const pollingStatus = typeof window.getConnectionStatus === 'function' ? window.getConnectionStatus() : undefined;
386 +
387 + // If polling status is explicitly false, respect that
388 + if (pollingStatus === false) {
389 + return false;
390 + }
391 +
392 + // If polling status is undefined/true, assume backend is available
393 + // (since the page loaded successfully, backend must be working)
394 + return true;
395 + },
396 +
397 + // NEW: Add frontend-only toast directly to stack (renamed from original addFrontendToast)
398 + addFrontendToastOnly(type, message, title = "", display_time = 5, group = "") {
399 const timestamp = new Date().toISOString();
400 const notification = {
401 id: `frontend-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
@@ -380,9 +406,25 @@ const model = {
406 timestamp: timestamp,
407 display_time: display_time,
408 read: false,
383 - frontend: true // Mark as frontend-only
409 + frontend: true, // Mark as frontend-only
410 + group: group
411 };
412
413 + // If notification has a group, remove any existing toasts with the same group
414 + if (group && group.trim() !== "") {
415 + const existingToastIndex = this.toastStack.findIndex(t =>
416 + t.group === group
417 + );
418 +
419 + if (existingToastIndex >= 0) {
420 + const existingToast = this.toastStack[existingToastIndex];
421 + if (existingToast.autoRemoveTimer) {
422 + clearTimeout(existingToast.autoRemoveTimer);
423 + }
424 + this.toastStack.splice(existingToastIndex, 1);
425 + }
426 + }
427 +
428 // Create toast object with auto-dismiss timer
429 const toast = {
430 ...notification,
@@ -407,21 +449,41 @@ const model = {
449 this.removeFromToastStack(toast.toastId);
450 }, notification.display_time * 1000);
451
410 - console.log(`Frontend toast added: ${notification.type} - ${notification.message}`);
452 return notification.id;
453 },
454
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);
455 + // NEW: Enhanced frontend toast that tries backend first, falls back to frontend-only
456 + async addFrontendToast(type, message, title = "", display_time = 5, group = "") {
457 + // Try to send to backend first if connected
458 + if (this.isConnected()) {
459 + try {
460 + const notificationId = await this.createNotification(type, message, title, "", display_time, group);
461 + if (notificationId) {
462 + // Backend handled it, notification will arrive via polling
463 + return notificationId;
464 + }
465 + } catch (error) {
466 + console.log(`Backend unavailable for notification, showing as frontend-only: ${error.message || error}`);
467 + }
468 + } else {
469 + console.log('Backend disconnected, showing as frontend-only toast');
470 + }
471 +
472 + // Fallback to frontend-only toast
473 + return this.addFrontendToastOnly(type, message, title, display_time, group);
474 },
475
419 - frontendWarning(message, title = "Warning", display_time = 5) {
420 - return this.addFrontendToast('warning', message, title, display_time);
476 + // NEW: Convenience methods for frontend notifications (updated to use new backend-first logic)
477 + async frontendError(message, title = "Connection Error", display_time = 8, group = "") {
478 + return await this.addFrontendToast('error', message, title, display_time, group);
479 },
480
423 - frontendInfo(message, title = "Info", display_time = 3) {
424 - return this.addFrontendToast('info', message, title, display_time);
481 + async frontendWarning(message, title = "Warning", display_time = 5, group = "") {
482 + return await this.addFrontendToast('warning', message, title, display_time, group);
483 + },
484 +
485 + async frontendInfo(message, title = "Info", display_time = 3, group = "") {
486 + return await this.addFrontendToast('info', message, title, display_time, group);
487 }
488 };
489
@@ -429,9 +491,16 @@ const model = {
491 const store = createStore("notificationStore", model);
492
493 // NEW: Global function for frontend error toasts (replaces toastFetchError)
432 -window.toastFrontendError = function(message, title = "Connection Error") {
494 +window.toastFrontendError = async function(message, title = "Connection Error") {
495 if (window.Alpine && window.Alpine.store && window.Alpine.store('notificationStore')) {
434 - return window.Alpine.store('notificationStore').frontendError(message, title);
496 + try {
497 + return await window.Alpine.store('notificationStore').frontendError(message, title);
498 + } catch (error) {
499 + console.error('Failed to create frontend error notification:', error);
500 + // Fallback to console if something goes wrong
501 + console.error('Frontend Error:', title, '-', message);
502 + return null;
503 + }
504 } else {
505 // Fallback if Alpine/store not ready
506 console.error('Frontend Error:', title, '-', message);
@@ -440,18 +509,30 @@ window.toastFrontendError = function(message, title = "Connection Error") {
509 };
510
511 // NEW: Additional global convenience functions
443 -window.toastFrontendWarning = function(message, title = "Warning") {
512 +window.toastFrontendWarning = async function(message, title = "Warning") {
513 if (window.Alpine && window.Alpine.store && window.Alpine.store('notificationStore')) {
445 - return window.Alpine.store('notificationStore').frontendWarning(message, title);
514 + try {
515 + return await window.Alpine.store('notificationStore').frontendWarning(message, title);
516 + } catch (error) {
517 + console.error('Failed to create frontend warning notification:', error);
518 + console.warn('Frontend Warning:', title, '-', message);
519 + return null;
520 + }
521 } else {
522 console.warn('Frontend Warning:', title, '-', message);
523 return null;
524 }
525 };
526
452 -window.toastFrontendInfo = function(message, title = "Info") {
527 +window.toastFrontendInfo = async function(message, title = "Info") {
528 if (window.Alpine && window.Alpine.store && window.Alpine.store('notificationStore')) {
454 - return window.Alpine.store('notificationStore').frontendInfo(message, title);
529 + try {
530 + return await window.Alpine.store('notificationStore').frontendInfo(message, title);
531 + } catch (error) {
532 + console.error('Failed to create frontend info notification:', error);
533 + console.log('Frontend Info:', title, '-', message);
534 + return null;
535 + }
536 } else {
537 console.log('Frontend Info:', title, '-', message);
538 return null;
webui/js/scheduler.js
+21 -5
@@ -59,11 +59,27 @@ import { switchFromContext } from '../index.js';
59
60 // Add this near the top of the scheduler.js file, outside of any function
61 const showToast = function(message, type = 'info') {
62 - // Use the global toast function if available, otherwise fallback to console
63 - if (typeof window.toast === 'function') {
64 - window.toast(message, type);
62 + // Use new frontend notification system
63 + if (window.Alpine && window.Alpine.store && window.Alpine.store('notificationStore')) {
64 + const store = window.Alpine.store('notificationStore');
65 + switch (type.toLowerCase()) {
66 + case 'error':
67 + return store.frontendError(message, "Scheduler", 5);
68 + case 'success':
69 + return store.frontendInfo(message, "Scheduler", 3);
70 + case 'warning':
71 + return store.frontendWarning(message, "Scheduler", 4);
72 + case 'info':
73 + default:
74 + return store.frontendInfo(message, "Scheduler", 3);
75 + }
76 } else {
66 - console.log(`Toast (${type}): ${message}`);
77 + // Fallback to global toast function or console
78 + if (typeof window.toast === 'function') {
79 + window.toast(message, type);
80 + } else {
81 + console.log(`SCHEDULER ${type.toUpperCase()}: ${message}`);
82 + }
83 }
84 };
85
@@ -976,7 +992,7 @@ const fullComponentImplementation = function() {
992 }
993
994 showToast('Task deleted successfully', 'success');
979 -
995 +
996 // If we were viewing the detail of the deleted task, close the detail view
997 if (this.selectedTaskForDetail && this.selectedTaskForDetail.uuid === taskId) {
998 this.closeTaskDetail();
webui/js/settings.js
+20 -19
@@ -575,24 +575,25 @@ document.addEventListener('alpine:init', function () {
575 });
576 });
577
578 -// Show toast notification
578 +// Show toast notification - now uses new notification system
579 function showToast(message, type = 'info') {
580 - const toast = document.createElement('div');
581 - toast.className = `toast toast-${type}`;
582 - toast.textContent = message;
583 -
584 - document.body.appendChild(toast);
585 -
586 - // Trigger animation
587 - setTimeout(() => {
588 - toast.classList.add('show');
589 - }, 10);
590 -
591 - // Remove after delay
592 - setTimeout(() => {
593 - toast.classList.remove('show');
594 - setTimeout(() => {
595 - document.body.removeChild(toast);
596 - }, 300);
597 - }, 3000);
580 + // Use new frontend notification system based on type
581 + if (window.Alpine && window.Alpine.store && window.Alpine.store('notificationStore')) {
582 + const store = window.Alpine.store('notificationStore');
583 + switch (type.toLowerCase()) {
584 + case 'error':
585 + return store.frontendError(message, "Settings", 5);
586 + case 'success':
587 + return store.frontendInfo(message, "Settings", 3);
588 + case 'warning':
589 + return store.frontendWarning(message, "Settings", 4);
590 + case 'info':
591 + default:
592 + return store.frontendInfo(message, "Settings", 3);
593 + }
594 + } else {
595 + // Fallback if Alpine/store not ready
596 + console.log(`SETTINGS ${type.toUpperCase()}: ${message}`);
597 + return null;
598 + }
599 }
webui/js/speech.js
+4 -7
@@ -47,7 +47,7 @@ async function loadMicSettings() {
47 });
48 }
49 } catch (error) {
50 - window.toastFetchError("Failed to load speech settings", error);
50 + window.toastFrontendError("Failed to load speech settings: " + error.message, "Speech Settings Error");
51 console.error("Failed to load speech settings:", error);
52 }
53 }
@@ -205,7 +205,7 @@ class MicrophoneInput {
205 return true;
206 } catch (error) {
207 console.error("Microphone initialization error:", error);
208 - toast("Failed to access microphone. Please check permissions.", "error");
208 + window.toastFrontendError("Failed to access microphone. Please check permissions.", "Microphone Error");
209 return false;
210 }
211 }
@@ -295,7 +295,7 @@ class MicrophoneInput {
295 await this.updateCallback(result.text, true);
296 }
297 } catch (error) {
298 - window.toastFetchError("Transcription error", error);
298 + window.toastFrontendError("Transcription error: " + error.message, "Speech Recognition Error");
299 console.error("Transcription error:", error);
300 } finally {
301 this.audioChunks = [];
@@ -386,10 +386,7 @@ async function requestMicrophonePermission() {
386 return true;
387 } catch (err) {
388 console.error("Error accessing microphone:", err);
389 - toast(
390 - "Microphone access denied. Please enable microphone access in your browser settings.",
391 - "error"
392 - );
389 + window.toastFrontendError("Microphone access denied. Please enable microphone access in your browser settings.", "Microphone Error");
390 return false;
391 }
392 }
webui/js/speech_browser.js
+6 -6
@@ -178,7 +178,7 @@ class MicrophoneInput {
178 } catch (error) {
179
180 console.error('Microphone initialization error:', error);
181 - toast('Failed to access microphone. Please check permissions.', 'error');
181 + window.toastFrontendError('Failed to access microphone. Please check permissions.', 'Microphone Error');
182 return false;
183 }
184 }
@@ -193,7 +193,7 @@ class MicrophoneInput {
193 this.analyserNode.smoothingTimeConstant = 0.85;
194 this.mediaStreamSource.connect(this.analyserNode);
195 }
196 -
196 +
197
198 startAudioAnalysis() {
199 const analyzeFrame = () => {
@@ -251,7 +251,7 @@ class MicrophoneInput {
251 return;
252 }
253
254 - const audioBlob = new Blob(this.audioChunks, { type: 'audio/wav' });
254 + const audioBlob = new Blob(this.audioChunks, { type: 'audio/wav' });
255 const audioUrl = URL.createObjectURL(audioBlob);
256
257
@@ -268,7 +268,7 @@ class MicrophoneInput {
268 }
269 } catch (error) {
270 console.error('Transcription error:', error);
271 - toast('Transcription failed.', 'error');
271 + window.toastFrontendError('Transcription failed.', 'Speech Recognition Error');
272 } finally {
273 URL.revokeObjectURL(audioUrl);
274 this.audioChunks = [];
@@ -347,7 +347,7 @@ async function requestMicrophonePermission() {
347 return true;
348 } catch (err) {
349 console.error('Error accessing microphone:', err);
350 - toast('Microphone access denied. Please enable microphone access in your browser settings.', 'error');
350 + window.toastFrontendError('Microphone access denied. Please enable microphone access in your browser settings.', 'Microphone Error');
351 return false;
352 }
353 }
@@ -391,4 +391,4 @@ class Speech {
391 }
392
393 export const speech = new Speech();
394 -window.speech = speech
\ No newline at end of file
394 +window.speech = speech
webui/js/tunnel.js
+57 -54
@@ -24,9 +24,9 @@ document.addEventListener('alpine:init', () => {
24 },
25 body: JSON.stringify({ action: 'get' }),
26 });
27 -
27 +
28 const data = await response.json();
29 -
29 +
30 if (data.success && data.tunnel_url) {
31 // Update the stored URL if it's different from what we have
32 if (this.tunnelLink !== data.tunnel_url) {
@@ -37,7 +37,7 @@ document.addEventListener('alpine:init', () => {
37 } else {
38 // Check if we have a stored tunnel URL
39 const storedTunnelUrl = localStorage.getItem('agent_zero_tunnel_url');
40 -
40 +
41 if (storedTunnelUrl) {
42 // Use the stored URL but verify it's still valid
43 const verifyResponse = await fetchApi('/tunnel_proxy', {
@@ -47,9 +47,9 @@ document.addEventListener('alpine:init', () => {
47 },
48 body: JSON.stringify({ action: 'verify', url: storedTunnelUrl }),
49 });
50 -
50 +
51 const verifyData = await verifyResponse.json();
52 -
52 +
53 if (verifyData.success && verifyData.is_valid) {
54 this.tunnelLink = storedTunnelUrl;
55 this.linkGenerated = true;
@@ -77,14 +77,14 @@ document.addEventListener('alpine:init', () => {
77 if (confirm("Are you sure you want to generate a new tunnel URL? The old URL will no longer work.")) {
78 this.isLoading = true;
79 this.loadingText = 'Refreshing tunnel...';
80 -
80 +
81 // Change refresh button appearance
82 const refreshButton = document.querySelector('.refresh-link-button');
83 const originalContent = refreshButton.innerHTML;
84 refreshButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Refreshing...';
85 refreshButton.disabled = true;
86 refreshButton.classList.add('refreshing');
87 -
87 +
88 try {
89 // First stop any existing tunnel
90 const stopResponse = await fetchApi('/tunnel_proxy', {
@@ -94,19 +94,19 @@ document.addEventListener('alpine:init', () => {
94 },
95 body: JSON.stringify({ action: 'stop' }),
96 });
97 -
97 +
98 // Check if stopping was successful
99 const stopData = await stopResponse.json();
100 if (!stopData.success) {
101 console.warn("Warning: Couldn't stop existing tunnel cleanly");
102 // Continue anyway since we want to create a new one
103 }
104 -
104 +
105 // Then generate a new one
106 await this.generateLink();
107 } catch (error) {
108 console.error("Error refreshing tunnel:", error);
109 - window.toast("Error refreshing tunnel", "error", 3000);
109 + window.toastFrontendError("Error refreshing tunnel", "Tunnel Error");
110 this.isLoading = false;
111 this.loadingText = '';
112 } finally {
@@ -123,17 +123,17 @@ document.addEventListener('alpine:init', () => {
123 try {
124 const authCheckResponse = await fetchApi('/settings_get');
125 const authData = await authCheckResponse.json();
126 -
126 +
127 // Find the auth_login and auth_password in the settings
128 let hasAuth = false;
129 -
129 +
130 if (authData && authData.settings && authData.settings.sections) {
131 for (const section of authData.settings.sections) {
132 if (section.fields) {
133 const authLoginField = section.fields.find(field => field.id === 'auth_login');
134 const authPasswordField = section.fields.find(field => field.id === 'auth_password');
135 -
136 - if (authLoginField && authPasswordField &&
135 +
136 + if (authLoginField && authPasswordField &&
137 authLoginField.value && authPasswordField.value) {
138 hasAuth = true;
139 break;
@@ -141,7 +141,7 @@ document.addEventListener('alpine:init', () => {
141 }
142 }
143 }
144 -
144 +
145 // If no authentication is set, warn the user
146 if (!hasAuth) {
147 const proceed = confirm(
@@ -152,7 +152,7 @@ document.addEventListener('alpine:init', () => {
152 "before creating a public tunnel.\n\n" +
153 "Do you want to proceed anyway?"
154 );
155 -
155 +
156 if (!proceed) {
157 return; // User cancelled
158 }
@@ -161,13 +161,16 @@ document.addEventListener('alpine:init', () => {
161 console.error("Error checking authentication status:", error);
162 // Continue anyway if we can't check auth status
163 }
164 -
164 +
165 this.isLoading = true;
166 this.loadingText = 'Creating tunnel...';
167
168 + // Get provider from the parent settings modal scope
169 + const modalEl = document.getElementById('settingsModal');
170 + const modalAD = Alpine.$data(modalEl);
171 // Use the local provider setting
172 const provider = this.provider || 'serveo'; // Default to serveo if not set
170 -
173 +
174 // Change create button appearance
175 const createButton = document.querySelector('.tunnel-actions .btn-ok');
176 if (createButton) {
@@ -175,7 +178,7 @@ document.addEventListener('alpine:init', () => {
178 createButton.disabled = true;
179 createButton.classList.add('creating');
180 }
178 -
181 +
182 try {
183 // Call the backend API to create a tunnel
184 const response = await fetchApi('/tunnel_proxy', {
@@ -183,31 +186,31 @@ document.addEventListener('alpine:init', () => {
186 headers: {
187 'Content-Type': 'application/json',
188 },
186 - body: JSON.stringify({
189 + body: JSON.stringify({
190 action: 'create',
191 provider: provider
192 // port: window.location.port || (window.location.protocol === 'https:' ? 443 : 80)
193 }),
194 });
192 -
195 +
196 const data = await response.json();
194 -
197 +
198 if (data.success && data.tunnel_url) {
199 // Store the tunnel URL in localStorage for persistence
200 localStorage.setItem('agent_zero_tunnel_url', data.tunnel_url);
198 -
201 +
202 this.tunnelLink = data.tunnel_url;
203 this.linkGenerated = true;
201 -
204 +
205 // Show success message to confirm creation
203 - window.toast("Tunnel created successfully", "success", 3000);
206 + window.toastFrontendInfo("Tunnel created successfully", "Tunnel Status");
207 } else {
208 // The tunnel might still be starting up, check again after a delay
209 this.loadingText = 'Tunnel creation taking longer than expected...';
207 -
210 +
211 // Wait for 5 seconds and check if the tunnel is running
212 await new Promise(resolve => setTimeout(resolve, 5000));
210 -
213 +
214 // Check if tunnel is running now
215 try {
216 const statusResponse = await fetchApi('/tunnel_proxy', {
@@ -217,33 +220,33 @@ document.addEventListener('alpine:init', () => {
220 },
221 body: JSON.stringify({ action: 'get' }),
222 });
220 -
223 +
224 const statusData = await statusResponse.json();
222 -
225 +
226 if (statusData.success && statusData.tunnel_url) {
227 // Tunnel is now running, we can update the UI
228 localStorage.setItem('agent_zero_tunnel_url', statusData.tunnel_url);
229 this.tunnelLink = statusData.tunnel_url;
230 this.linkGenerated = true;
228 - window.toast("Tunnel created successfully", "success", 3000);
231 + window.toastFrontendInfo("Tunnel created successfully", "Tunnel Status");
232 return;
233 }
234 } catch (statusError) {
235 console.error("Error checking tunnel status:", statusError);
236 }
234 -
237 +
238 // If we get here, the tunnel really failed to start
239 const errorMessage = data.message || "Failed to create tunnel. Please try again.";
237 - window.toast(errorMessage, "error", 5000);
240 + window.toastFrontendError(errorMessage, "Tunnel Error");
241 console.error("Tunnel creation failed:", data);
242 }
243 } catch (error) {
241 - window.toast("Error creating tunnel", "error", 5000);
244 + window.toastFrontendError("Error creating tunnel", "Tunnel Error");
245 console.error("Error creating tunnel:", error);
246 } finally {
247 this.isLoading = false;
248 this.loadingText = '';
246 -
249 +
250 // Reset create button if it's still in the DOM
251 const createButton = document.querySelector('.tunnel-actions .btn-ok');
252 if (createButton) {
@@ -258,8 +261,8 @@ document.addEventListener('alpine:init', () => {
261 if (confirm("Are you sure you want to stop the tunnel? The URL will no longer be accessible.")) {
262 this.isLoading = true;
263 this.loadingText = 'Stopping tunnel...';
261 -
262 -
264 +
265 +
266 try {
267 // Call the backend to stop the tunnel
268 const response = await fetchApi('/tunnel_proxy', {
@@ -269,30 +272,30 @@ document.addEventListener('alpine:init', () => {
272 },
273 body: JSON.stringify({ action: 'stop' }),
274 });
272 -
275 +
276 const data = await response.json();
274 -
277 +
278 if (data.success) {
279 // Clear the stored URL
280 localStorage.removeItem('agent_zero_tunnel_url');
278 -
281 +
282 // Update UI state
283 this.tunnelLink = '';
284 this.linkGenerated = false;
282 -
283 - window.toast("Tunnel stopped successfully", "success", 3000);
285 +
286 + window.toastFrontendInfo("Tunnel stopped successfully", "Tunnel Status");
287 } else {
285 - window.toast("Failed to stop tunnel", "error", 3000);
286 -
288 + window.toastFrontendError("Failed to stop tunnel", "Tunnel Error");
289 +
290 // Reset stop button
291 stopButton.innerHTML = originalStopContent;
292 stopButton.disabled = false;
293 stopButton.classList.remove('stopping');
294 }
295 } catch (error) {
293 - window.toast("Error stopping tunnel", "error", 3000);
296 + window.toastFrontendError("Error stopping tunnel", "Tunnel Error");
297 console.error("Error stopping tunnel:", error);
295 -
298 +
299 // Reset stop button
300 stopButton.innerHTML = originalStopContent;
301 stopButton.disabled = false;
@@ -306,19 +309,19 @@ document.addEventListener('alpine:init', () => {
309
310 copyToClipboard() {
311 if (!this.tunnelLink) return;
309 -
312 +
313 const copyButton = document.querySelector('.copy-link-button');
314 const originalContent = copyButton.innerHTML;
312 -
315 +
316 navigator.clipboard.writeText(this.tunnelLink)
317 .then(() => {
318 // Update button to show success state
319 copyButton.innerHTML = '<i class="fas fa-check"></i> Copied!';
320 copyButton.classList.add('copy-success');
318 -
321 +
322 // Show toast notification
320 - window.toast("Tunnel URL copied to clipboard!", "success", 3000);
321 -
323 + window.toastFrontendInfo("Tunnel URL copied to clipboard!", "Clipboard");
324 +
325 // Reset button after 2 seconds
326 setTimeout(() => {
327 copyButton.innerHTML = originalContent;
@@ -327,12 +330,12 @@ document.addEventListener('alpine:init', () => {
330 })
331 .catch(err => {
332 console.error('Failed to copy URL: ', err);
330 - window.toast("Failed to copy tunnel URL", "error", 3000);
331 -
333 + window.toastFrontendError("Failed to copy tunnel URL", "Clipboard Error");
334 +
335 // Show error state
336 copyButton.innerHTML = '<i class="fas fa-times"></i> Failed';
337 copyButton.classList.add('copy-error');
335 -
338 +
339 // Reset button after 2 seconds
340 setTimeout(() => {
341 copyButton.innerHTML = originalContent;
@@ -341,4 +344,4 @@ document.addEventListener('alpine:init', () => {
344 });
345 }
346 }));
344 -});
\ No newline at end of file
347 +});