diff --git a/python/api/settings_get.py b/python/api/settings_get.py index f807bc60a..5b5bf95c7 100644 --- a/python/api/settings_get.py +++ b/python/api/settings_get.py @@ -6,3 +6,7 @@ class GetSettings(ApiHandler): async def process(self, input: dict, request: Request) -> dict | Response: set = settings.convert_out(settings.get_settings()) return {"settings": set} + + @classmethod + def get_methods(cls) -> list[str]: + return ["GET", "POST"] diff --git a/webui/components/notifications/notification-toast-stack.html b/webui/components/notifications/notification-toast-stack.html index d726fa902..c4337a8c8 100644 --- a/webui/components/notifications/notification-toast-stack.html +++ b/webui/components/notifications/notification-toast-stack.html @@ -56,7 +56,7 @@ position: absolute; bottom: 5px; /* Spacing from the bottom of the zero-height container */ padding-right: 5px; - z-index: 1500; + z-index: 15000; display: flex; flex-direction: column-reverse; /* Stack toasts upwards */ gap: 8px; diff --git a/webui/components/settings/tunnel/tunnel-section.html b/webui/components/settings/tunnel/tunnel-section.html new file mode 100644 index 000000000..cc385e943 --- /dev/null +++ b/webui/components/settings/tunnel/tunnel-section.html @@ -0,0 +1,371 @@ + + + + + + + + + + + + +
+ +
+ + + + + + + \ No newline at end of file diff --git a/webui/components/settings/tunnel/tunnel-store.js b/webui/components/settings/tunnel/tunnel-store.js new file mode 100644 index 000000000..60a2eed41 --- /dev/null +++ b/webui/components/settings/tunnel/tunnel-store.js @@ -0,0 +1,430 @@ +import { createStore } from "/js/AlpineStore.js"; +import * as Sleep from "/js/sleep.js"; + +// define the model object holding data and functions +const model = { + isLoading: false, + tunnelLink: "", + linkGenerated: false, + loadingText: "", + qrCodeInstance: null, + provider: "cloudflared", + + init() { + this.checkTunnelStatus(); + }, + + generateQRCode() { + if (!this.tunnelLink) return; + + const qrContainer = document.getElementById("qrcode-tunnel"); + if (!qrContainer) return; + + // Clear any existing QR code + qrContainer.innerHTML = ""; + + try { + // Generate new QR code + this.qrCodeInstance = new QRCode(qrContainer, { + text: this.tunnelLink, + width: 128, + height: 128, + colorDark: "#000000", + colorLight: "#ffffff", + correctLevel: QRCode.CorrectLevel.M, + }); + } catch (error) { + console.error("Error generating QR code:", error); + qrContainer.innerHTML = + '
QR code generation failed
'; + } + }, + + async checkTunnelStatus() { + try { + const response = await fetchApi("/tunnel_proxy", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ action: "get" }), + }); + + const data = await response.json(); + + if (data.success && data.tunnel_url) { + // Update the stored URL if it's different from what we have + if (this.tunnelLink !== data.tunnel_url) { + this.tunnelLink = data.tunnel_url; + localStorage.setItem("agent_zero_tunnel_url", data.tunnel_url); + } + this.linkGenerated = true; + // Generate QR code for the tunnel URL + Sleep.Skip().then(() => this.generateQRCode()); + } else { + // Check if we have a stored tunnel URL + const storedTunnelUrl = localStorage.getItem("agent_zero_tunnel_url"); + + if (storedTunnelUrl) { + // Use the stored URL but verify it's still valid + const verifyResponse = await fetchApi("/tunnel_proxy", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ action: "verify", url: storedTunnelUrl }), + }); + + const verifyData = await verifyResponse.json(); + + if (verifyData.success && verifyData.is_valid) { + this.tunnelLink = storedTunnelUrl; + this.linkGenerated = true; + // Generate QR code for the tunnel URL + Sleep.Skip().then(() => this.generateQRCode()); + } else { + // Clear stale URL + localStorage.removeItem("agent_zero_tunnel_url"); + this.tunnelLink = ""; + this.linkGenerated = false; + } + } else { + // No stored URL, show the generate button + this.tunnelLink = ""; + this.linkGenerated = false; + } + } + } catch (error) { + console.error("Error checking tunnel status:", error); + this.tunnelLink = ""; + this.linkGenerated = false; + } + }, + + async refreshLink() { + // Call generate but with a confirmation first + if ( + confirm( + "Are you sure you want to generate a new tunnel URL? The old URL will no longer work." + ) + ) { + + this.isLoading = true; + this.loadingText = "Refreshing tunnel..."; + + // Change refresh button appearance + const refreshButton = document.querySelector("#tunnel-settings-section .refresh-link-button"); + const originalContent = refreshButton.innerHTML; + refreshButton.innerHTML = + 'progress_activity Refreshing...'; + refreshButton.disabled = true; + refreshButton.classList.add("refreshing"); + + try { + // First stop any existing tunnel + const stopResponse = await fetchApi("/tunnel_proxy", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ action: "stop" }), + }); + + // Check if stopping was successful + const stopData = await stopResponse.json(); + if (!stopData.success) { + console.warn("Warning: Couldn't stop existing tunnel cleanly"); + // Continue anyway since we want to create a new one + } + + // Then generate a new one + await this.generateLink(); + } catch (error) { + console.error("Error refreshing tunnel:", error); + window.toastFrontendError("Error refreshing tunnel", "Tunnel Error"); + this.isLoading = false; + this.loadingText = ""; + } finally { + // Reset refresh button + refreshButton.innerHTML = originalContent; + refreshButton.disabled = false; + refreshButton.classList.remove("refreshing"); + } + } + }, + + async generateLink() { + // First check if authentication is enabled + try { + const authCheckResponse = await fetchApi("/settings_get"); + const authData = await authCheckResponse.json(); + + // Find the auth_login and auth_password in the settings + let hasAuth = false; + + if (authData && authData.settings && authData.settings.sections) { + for (const section of authData.settings.sections) { + if (section.fields) { + const authLoginField = section.fields.find( + (field) => field.id === "auth_login" + ); + const authPasswordField = section.fields.find( + (field) => field.id === "auth_password" + ); + + if ( + authLoginField && + authPasswordField && + authLoginField.value && + authPasswordField.value + ) { + hasAuth = true; + break; + } + } + } + } + + // If no authentication is set, warn the user + if (!hasAuth) { + const proceed = confirm( + "WARNING: No authentication is configured for your Agent Zero instance.\n\n" + + "Creating a public tunnel without authentication means anyone with the URL " + + "can access your Agent Zero instance.\n\n" + + "It is recommended to set up authentication in the Settings > Authentication section " + + "before creating a public tunnel.\n\n" + + "Do you want to proceed anyway?" + ); + + if (!proceed) { + return; // User cancelled + } + } + } catch (error) { + console.error("Error checking authentication status:", error); + // Continue anyway if we can't check auth status + } + + this.isLoading = true; + this.loadingText = "Creating tunnel..."; + + // Change create button appearance + const createButton = document.querySelector("#tunnel-settings-section .tunnel-actions .btn-ok"); + if (createButton) { + createButton.innerHTML = + 'progress_activity Creating...'; + createButton.disabled = true; + createButton.classList.add("creating"); + } + + try { + // Call the backend API to create a tunnel + const response = await fetchApi("/tunnel_proxy", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + action: "create", + provider: this.provider, + // port: window.location.port || (window.location.protocol === 'https:' ? 443 : 80) + }), + }); + + const data = await response.json(); + + if (data.success && data.tunnel_url) { + // Store the tunnel URL in localStorage for persistence + localStorage.setItem("agent_zero_tunnel_url", data.tunnel_url); + + this.tunnelLink = data.tunnel_url; + this.linkGenerated = true; + + // Generate QR code for the tunnel URL + Sleep.Skip().then(() => this.generateQRCode()); + + // Show success message to confirm creation + window.toastFrontendInfo( + "Tunnel created successfully", + "Tunnel Status" + ); + } else { + // The tunnel might still be starting up, check again after a delay + this.loadingText = "Tunnel creation taking longer than expected..."; + + // Wait for 5 seconds and check if the tunnel is running + await new Promise((resolve) => setTimeout(resolve, 5000)); + + // Check if tunnel is running now + try { + const statusResponse = await fetchApi("/tunnel_proxy", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ action: "get" }), + }); + + const statusData = await statusResponse.json(); + + if (statusData.success && statusData.tunnel_url) { + // Tunnel is now running, we can update the UI + localStorage.setItem( + "agent_zero_tunnel_url", + statusData.tunnel_url + ); + this.tunnelLink = statusData.tunnel_url; + this.linkGenerated = true; + + // Generate QR code for the tunnel URL + Sleep.Skip().then(() => this.generateQRCode()); + + window.toastFrontendInfo( + "Tunnel created successfully", + "Tunnel Status" + ); + return; + } + } catch (statusError) { + console.error("Error checking tunnel status:", statusError); + } + + // If we get here, the tunnel really failed to start + const errorMessage = + data.message || "Failed to create tunnel. Please try again."; + window.toastFrontendError(errorMessage, "Tunnel Error"); + console.error("Tunnel creation failed:", data); + } + } catch (error) { + window.toastFrontendError("Error creating tunnel", "Tunnel Error"); + console.error("Error creating tunnel:", error); + } finally { + this.isLoading = false; + this.loadingText = ""; + + // Reset create button if it's still in the DOM + const createButton = document.querySelector("#tunnel-settings-section .tunnel-actions .btn-ok"); + if (createButton) { + createButton.innerHTML = + 'play_circle Create Tunnel'; + createButton.disabled = false; + createButton.classList.remove("creating"); + } + } + }, + + async stopTunnel() { + if ( + confirm( + "Are you sure you want to stop the tunnel? The URL will no longer be accessible." + ) + ) { + this.isLoading = true; + this.loadingText = "Stopping tunnel..."; + + try { + // Call the backend to stop the tunnel + const response = await fetchApi("/tunnel_proxy", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ action: "stop" }), + }); + + const data = await response.json(); + + if (data.success) { + // Clear the stored URL + localStorage.removeItem("agent_zero_tunnel_url"); + + // Clear QR code + const qrContainer = document.getElementById("qrcode-tunnel"); + if (qrContainer) { + qrContainer.innerHTML = ""; + } + this.qrCodeInstance = null; + + // Update UI state + this.tunnelLink = ""; + this.linkGenerated = false; + + window.toastFrontendInfo( + "Tunnel stopped successfully", + "Tunnel Status" + ); + } else { + window.toastFrontendError("Failed to stop tunnel", "Tunnel Error"); + + // Reset stop button + stopButton.innerHTML = originalStopContent; + stopButton.disabled = false; + stopButton.classList.remove("stopping"); + } + } catch (error) { + window.toastFrontendError("Error stopping tunnel", "Tunnel Error"); + console.error("Error stopping tunnel:", error); + + // Reset stop button + stopButton.innerHTML = originalStopContent; + stopButton.disabled = false; + stopButton.classList.remove("stopping"); + } finally { + this.isLoading = false; + this.loadingText = ""; + } + } + }, + + copyToClipboard() { + if (!this.tunnelLink) return; + + const copyButton = document.querySelector("#tunnel-settings-section .copy-link-button"); + const originalContent = copyButton.innerHTML; + + navigator.clipboard + .writeText(this.tunnelLink) + .then(() => { + // Update button to show success state + copyButton.innerHTML = + 'check Copied!'; + copyButton.classList.add("copy-success"); + + // Show toast notification + window.toastFrontendInfo( + "Tunnel URL copied to clipboard!", + "Clipboard" + ); + + // Reset button after 2 seconds + setTimeout(() => { + copyButton.innerHTML = originalContent; + copyButton.classList.remove("copy-success"); + }, 2000); + }) + .catch((err) => { + console.error("Failed to copy URL: ", err); + window.toastFrontendError( + "Failed to copy tunnel URL", + "Clipboard Error" + ); + + // Show error state + copyButton.innerHTML = + 'close Failed'; + copyButton.classList.add("copy-error"); + + // Reset button after 2 seconds + setTimeout(() => { + copyButton.innerHTML = originalContent; + copyButton.classList.remove("copy-error"); + }, 2000); + }); + }, +}; + +// convert it to alpine store +const store = createStore("tunnelStore", model); + +// export for use in other files +export { store }; diff --git a/webui/css/tunnel.css b/webui/css/tunnel.css deleted file mode 100644 index 6759d1ae7..000000000 --- a/webui/css/tunnel.css +++ /dev/null @@ -1,262 +0,0 @@ -/* Tunnel Modal Styles */ -.tunnel-container { - padding: 1rem; - width: 100%; -} - -.tunnel-description { - margin-bottom: 1.5rem; - text-align: center; -} - -.tunnel-actions { - display: flex; - justify-content: center; - margin-top: 2rem; - margin-bottom: 2rem; -} - -.tunnel-link-container { - margin-top: 1rem; - display: flex; - flex-direction: column; - gap: 1rem; -} - -.tunnel-link-field { - display: flex; - align-items: center; - margin-bottom: 0.5rem; - background-color: var(--bg-color-secondary); - border: 1px solid var(--border-color); - border-radius: 4px; -} - -.tunnel-link-input { - flex: 1; - padding: 0.75rem; - background-color: transparent; - border: none; - color: var(--text-color); - font-size: 0.9rem; - outline: none; -} - -.copy-link-button { - padding: 0.5rem 0.75rem; - background: none; - border: none; - border-left: 1px solid var(--border-color); - color: var(--text-color); - cursor: pointer; - transition: background-color 0.2s; -} - -.copy-link-button:hover { - background-color: var(--bg-color-tertiary); -} - -.copy-link-button i, -.refresh-link-button i, -.btn i { - margin-right: 6px; -} - -.tunnel-link-info { - margin-top: 1rem; - font-size: 1rem; - color: var(--text-color); - text-align: center; - line-height: 1.5; -} - -.loading-spinner { - display: flex; - justify-content: center; - align-items: center; - margin-top: 2rem; - margin-bottom: 2rem; - min-height: 38px; - font-style: italic; - color: var(--text-color-secondary); -} - -.loading-spinner i { - font-size: 1.5rem; - margin-right: 10px; - color: var(--accent-color); -} - -.refresh-link-button { - padding: 0.5rem 0.75rem; - background: none; - border: none; - border-left: 1px solid var(--border-color); - color: var(--text-color); - cursor: pointer; - transition: background-color 0.2s; -} - -.refresh-link-button:hover { - background-color: var(--bg-color-tertiary); - color: var(--accent-color); -} - -.tunnel-link-persistence { - margin-top: 0.75rem; - font-size: 0.95rem; - color: rgba(255, 255, 255, 0.7); - text-align: center; - font-style: italic; -} - -.btn-danger { - background-color: #dc3545; - color: white; - border: none; -} - -.btn-danger:hover { - background-color: #bd2130; -} - -.stop-tunnel-container { - margin-top: 20px; - display: flex; - justify-content: center; -} - -/* Section title icon styling */ -.section-title i { - margin-right: 8px; -} - -/* Copy button states */ -.copy-success { - background-color: rgba(40, 167, 69, 0.15) !important; - color: #28a745 !important; - border-left: 1px solid rgba(40, 167, 69, 0.5) !important; - transition: all 0.3s ease-in-out; -} - -.copy-error { - background-color: rgba(220, 53, 69, 0.15) !important; - color: #dc3545 !important; - border-left: 1px solid rgba(220, 53, 69, 0.5) !important; - transition: all 0.3s ease-in-out; -} - -/* Animation for copy button */ -@keyframes pulse { - 0% { transform: scale(1); } - 50% { transform: scale(1.05); } - 100% { transform: scale(1); } -} - -.copy-success, .copy-error { - animation: pulse 0.5s; -} - -/* Refresh button state */ -.refreshing { - opacity: 0.7; - pointer-events: none; - background-color: rgba(108, 117, 125, 0.15) !important; - border-left: 1px solid rgba(108, 117, 125, 0.5) !important; -} - -/* Create and Stop button states */ -.creating, .stopping { - opacity: 0.8; - pointer-events: none; - cursor: not-allowed; -} - -.creating { - background-color: rgba(0, 123, 255, 0.7) !important; -} - -.stopping { - background-color: rgba(220, 53, 69, 0.7) !important; -} - -/* QR Code Styles */ -.tunnel-qr-container { - display: flex; - align-items: center; - justify-content: flex-start; - gap: 1rem; - margin: 0.5rem 0; - padding: 1rem; - background-color: var(--bg-color-secondary); - border: 1px solid var(--border-color); - border-radius: 8px; -} - -.tunnel-qr-code { - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - min-width: 128px; - min-height: 128px; - background-color: white; - border-radius: 8px; - padding: 8px; -} - -.tunnel-qr-code canvas, -.tunnel-qr-code img { - border-radius: 4px; - max-width: 100%; - max-height: 100%; -} - -.tunnel-qr-label { - font-size: 0.9rem; - color: var(--text-color-secondary); - text-align: center; - line-height: 1.4; - flex: 1; -} - -.qr-error { - color: var(--error-color, #dc3545); - font-size: 0.8rem; - text-align: center; - padding: 1rem; - background-color: rgba(220, 53, 69, 0.1); - border-radius: 4px; - border: 1px solid rgba(220, 53, 69, 0.2); -} - -/* Responsive design for QR code container */ -@media (max-width: 640px) { - .tunnel-qr-container { - flex-direction: column; - text-align: center; - gap: 0.75rem; - padding: 0.75rem; - } - - .tunnel-qr-code { - min-width: 100px; - min-height: 100px; - align-self: center; - } - - .tunnel-qr-label { - text-align: center; - } -} - -/* Light mode adjustments */ -.light-mode .tunnel-qr-code { - background-color: #ffffff; - border: 1px solid #e0e0e0; -} - -.light-mode .qr-error { - background-color: rgba(220, 53, 69, 0.05); - color: #dc3545; -} diff --git a/webui/index.html b/webui/index.html index 893eb69c7..93d9ce8a2 100644 --- a/webui/index.html +++ b/webui/index.html @@ -17,7 +17,6 @@ - @@ -689,69 +688,7 @@
-
Flare Tunnel
-
Create a secure public URL to access your Agent Zero instance anytime, anywhere.
- -
-
-
-
Tunnel provider
-
Select provider for public tunnel
-
-
- -
-
- -
- progress_activity - -
- - -
- - - -
- -
-
-
+
diff --git a/webui/js/settings.js b/webui/js/settings.js index 6d781a5a9..06d80c60e 100644 --- a/webui/js/settings.js +++ b/webui/js/settings.js @@ -67,19 +67,6 @@ const settingsModalProxy = { } } } - - // When switching to the tunnel tab, initialize tunnelSettings - if (tabName === 'tunnel') { - console.log('Switching to tunnel tab, initializing tunnelSettings'); - const tunnelElement = document.querySelector('[x-data="tunnelSettings"]'); - if (tunnelElement) { - const tunnelData = Alpine.$data(tunnelElement); - if (tunnelData && typeof tunnelData.checkTunnelStatus === 'function') { - // Check tunnel status - tunnelData.checkTunnelStatus(); - } - } - } }, 10); }, diff --git a/webui/js/tunnel.js b/webui/js/tunnel.js deleted file mode 100644 index f41249af0..000000000 --- a/webui/js/tunnel.js +++ /dev/null @@ -1,386 +0,0 @@ - -// Tunnel settings for the Settings modal -document.addEventListener('alpine:init', () => { - Alpine.data('tunnelSettings', () => ({ - isLoading: false, - tunnelLink: '', - linkGenerated: false, - loadingText: '', - qrCodeInstance: null, - - init() { - this.checkTunnelStatus(); - }, - - generateQRCode() { - if (!this.tunnelLink) return; - - const qrContainer = document.getElementById('qrcode-tunnel'); - if (!qrContainer) return; - - // Clear any existing QR code - qrContainer.innerHTML = ''; - - try { - // Generate new QR code - this.qrCodeInstance = new QRCode(qrContainer, { - text: this.tunnelLink, - width: 128, - height: 128, - colorDark: "#000000", - colorLight: "#ffffff", - correctLevel: QRCode.CorrectLevel.M - }); - } catch (error) { - console.error('Error generating QR code:', error); - qrContainer.innerHTML = '
QR code generation failed
'; - } - }, - - async checkTunnelStatus() { - try { - const response = await fetchApi('/tunnel_proxy', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ action: 'get' }), - }); - - const data = await response.json(); - - if (data.success && data.tunnel_url) { - // Update the stored URL if it's different from what we have - if (this.tunnelLink !== data.tunnel_url) { - this.tunnelLink = data.tunnel_url; - localStorage.setItem('agent_zero_tunnel_url', data.tunnel_url); - } - this.linkGenerated = true; - // Generate QR code for the tunnel URL - this.$nextTick(() => this.generateQRCode()); - } else { - // Check if we have a stored tunnel URL - const storedTunnelUrl = localStorage.getItem('agent_zero_tunnel_url'); - - if (storedTunnelUrl) { - // Use the stored URL but verify it's still valid - const verifyResponse = await fetchApi('/tunnel_proxy', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ action: 'verify', url: storedTunnelUrl }), - }); - - const verifyData = await verifyResponse.json(); - - if (verifyData.success && verifyData.is_valid) { - this.tunnelLink = storedTunnelUrl; - this.linkGenerated = true; - // Generate QR code for the tunnel URL - this.$nextTick(() => this.generateQRCode()); - } else { - // Clear stale URL - localStorage.removeItem('agent_zero_tunnel_url'); - this.tunnelLink = ''; - this.linkGenerated = false; - } - } else { - // No stored URL, show the generate button - this.tunnelLink = ''; - this.linkGenerated = false; - } - } - } catch (error) { - console.error('Error checking tunnel status:', error); - this.tunnelLink = ''; - this.linkGenerated = false; - } - }, - - async refreshLink() { - // Call generate but with a confirmation first - if (confirm("Are you sure you want to generate a new tunnel URL? The old URL will no longer work.")) { - this.isLoading = true; - this.loadingText = 'Refreshing tunnel...'; - - // Change refresh button appearance - const refreshButton = document.querySelector('.refresh-link-button'); - const originalContent = refreshButton.innerHTML; - refreshButton.innerHTML = 'progress_activity Refreshing...'; - refreshButton.disabled = true; - refreshButton.classList.add('refreshing'); - - try { - // First stop any existing tunnel - const stopResponse = await fetchApi('/tunnel_proxy', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ action: 'stop' }), - }); - - // Check if stopping was successful - const stopData = await stopResponse.json(); - if (!stopData.success) { - console.warn("Warning: Couldn't stop existing tunnel cleanly"); - // Continue anyway since we want to create a new one - } - - // Then generate a new one - await this.generateLink(); - } catch (error) { - console.error("Error refreshing tunnel:", error); - window.toastFrontendError("Error refreshing tunnel", "Tunnel Error"); - this.isLoading = false; - this.loadingText = ''; - } finally { - // Reset refresh button - refreshButton.innerHTML = originalContent; - refreshButton.disabled = false; - refreshButton.classList.remove('refreshing'); - } - } - }, - - async generateLink() { - // First check if authentication is enabled - try { - const authCheckResponse = await fetchApi('/settings_get'); - const authData = await authCheckResponse.json(); - - // Find the auth_login and auth_password in the settings - let hasAuth = false; - - if (authData && authData.settings && authData.settings.sections) { - for (const section of authData.settings.sections) { - if (section.fields) { - const authLoginField = section.fields.find(field => field.id === 'auth_login'); - const authPasswordField = section.fields.find(field => field.id === 'auth_password'); - - if (authLoginField && authPasswordField && - authLoginField.value && authPasswordField.value) { - hasAuth = true; - break; - } - } - } - } - - // If no authentication is set, warn the user - if (!hasAuth) { - const proceed = confirm( - "WARNING: No authentication is configured for your Agent Zero instance.\n\n" + - "Creating a public tunnel without authentication means anyone with the URL " + - "can access your Agent Zero instance.\n\n" + - "It is recommended to set up authentication in the Settings > Authentication section " + - "before creating a public tunnel.\n\n" + - "Do you want to proceed anyway?" - ); - - if (!proceed) { - return; // User cancelled - } - } - } catch (error) { - console.error("Error checking authentication status:", error); - // Continue anyway if we can't check auth status - } - - this.isLoading = true; - this.loadingText = 'Creating tunnel...'; - - // Get provider from the parent settings modal scope - const modalEl = document.getElementById('settingsModal'); - const modalAD = Alpine.$data(modalEl); - const provider = modalAD.provider || 'cloudflared'; // Default to cloudflared if not set - - // Change create button appearance - const createButton = document.querySelector('.tunnel-actions .btn-ok'); - if (createButton) { - createButton.innerHTML = 'progress_activity Creating...'; - createButton.disabled = true; - createButton.classList.add('creating'); - } - - try { - // Call the backend API to create a tunnel - const response = await fetchApi('/tunnel_proxy', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - action: 'create', - provider: provider - // port: window.location.port || (window.location.protocol === 'https:' ? 443 : 80) - }), - }); - - const data = await response.json(); - - if (data.success && data.tunnel_url) { - // Store the tunnel URL in localStorage for persistence - localStorage.setItem('agent_zero_tunnel_url', data.tunnel_url); - - this.tunnelLink = data.tunnel_url; - this.linkGenerated = true; - - // Generate QR code for the tunnel URL - this.$nextTick(() => this.generateQRCode()); - - // Show success message to confirm creation - window.toastFrontendInfo("Tunnel created successfully", "Tunnel Status"); - } else { - // The tunnel might still be starting up, check again after a delay - this.loadingText = 'Tunnel creation taking longer than expected...'; - - // Wait for 5 seconds and check if the tunnel is running - await new Promise(resolve => setTimeout(resolve, 5000)); - - // Check if tunnel is running now - try { - const statusResponse = await fetchApi('/tunnel_proxy', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ action: 'get' }), - }); - - const statusData = await statusResponse.json(); - - if (statusData.success && statusData.tunnel_url) { - // Tunnel is now running, we can update the UI - localStorage.setItem('agent_zero_tunnel_url', statusData.tunnel_url); - this.tunnelLink = statusData.tunnel_url; - this.linkGenerated = true; - - // Generate QR code for the tunnel URL - this.$nextTick(() => this.generateQRCode()); - - window.toastFrontendInfo("Tunnel created successfully", "Tunnel Status"); - return; - } - } catch (statusError) { - console.error("Error checking tunnel status:", statusError); - } - - // If we get here, the tunnel really failed to start - const errorMessage = data.message || "Failed to create tunnel. Please try again."; - window.toastFrontendError(errorMessage, "Tunnel Error"); - console.error("Tunnel creation failed:", data); - } - } catch (error) { - window.toastFrontendError("Error creating tunnel", "Tunnel Error"); - console.error("Error creating tunnel:", error); - } finally { - this.isLoading = false; - this.loadingText = ''; - - // Reset create button if it's still in the DOM - const createButton = document.querySelector('.tunnel-actions .btn-ok'); - if (createButton) { - createButton.innerHTML = 'play_circle Create Tunnel'; - createButton.disabled = false; - createButton.classList.remove('creating'); - } - } - }, - - async stopTunnel() { - if (confirm("Are you sure you want to stop the tunnel? The URL will no longer be accessible.")) { - this.isLoading = true; - this.loadingText = 'Stopping tunnel...'; - - - try { - // Call the backend to stop the tunnel - const response = await fetchApi('/tunnel_proxy', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ action: 'stop' }), - }); - - const data = await response.json(); - - if (data.success) { - // Clear the stored URL - localStorage.removeItem('agent_zero_tunnel_url'); - - // Clear QR code - const qrContainer = document.getElementById('qrcode-tunnel'); - if (qrContainer) { - qrContainer.innerHTML = ''; - } - this.qrCodeInstance = null; - - // Update UI state - this.tunnelLink = ''; - this.linkGenerated = false; - - window.toastFrontendInfo("Tunnel stopped successfully", "Tunnel Status"); - } else { - window.toastFrontendError("Failed to stop tunnel", "Tunnel Error"); - - // Reset stop button - stopButton.innerHTML = originalStopContent; - stopButton.disabled = false; - stopButton.classList.remove('stopping'); - } - } catch (error) { - window.toastFrontendError("Error stopping tunnel", "Tunnel Error"); - console.error("Error stopping tunnel:", error); - - // Reset stop button - stopButton.innerHTML = originalStopContent; - stopButton.disabled = false; - stopButton.classList.remove('stopping'); - } finally { - this.isLoading = false; - this.loadingText = ''; - } - } - }, - - copyToClipboard() { - if (!this.tunnelLink) return; - - const copyButton = document.querySelector('.copy-link-button'); - const originalContent = copyButton.innerHTML; - - navigator.clipboard.writeText(this.tunnelLink) - .then(() => { - // Update button to show success state - copyButton.innerHTML = 'check Copied!'; - copyButton.classList.add('copy-success'); - - // Show toast notification - window.toastFrontendInfo("Tunnel URL copied to clipboard!", "Clipboard"); - - // Reset button after 2 seconds - setTimeout(() => { - copyButton.innerHTML = originalContent; - copyButton.classList.remove('copy-success'); - }, 2000); - }) - .catch(err => { - console.error('Failed to copy URL: ', err); - window.toastFrontendError("Failed to copy tunnel URL", "Clipboard Error"); - - // Show error state - copyButton.innerHTML = 'close Failed'; - copyButton.classList.add('copy-error'); - - // Reset button after 2 seconds - setTimeout(() => { - copyButton.innerHTML = originalContent; - copyButton.classList.remove('copy-error'); - }, 2000); - }); - } - })); -});