From eee6796299c42b0fffb69ab71a54d2f7e8cc1f91 Mon Sep 17 00:00:00 2001
From: frdel <38891707+frdel@users.noreply.github.com>
Date: Fri, 15 Aug 2025 11:22:28 +0200
Subject: [PATCH] tunnel component refactor
---
python/api/settings_get.py | 4 +
.../notification-toast-stack.html | 2 +-
.../settings/tunnel/tunnel-section.html | 371 +++++++++++++++
.../settings/tunnel/tunnel-store.js | 430 ++++++++++++++++++
webui/css/tunnel.css | 262 -----------
webui/index.html | 65 +--
webui/js/settings.js | 13 -
webui/js/tunnel.js | 386 ----------------
8 files changed, 807 insertions(+), 726 deletions(-)
create mode 100644 webui/components/settings/tunnel/tunnel-section.html
create mode 100644 webui/components/settings/tunnel/tunnel-store.js
delete mode 100644 webui/css/tunnel.css
delete mode 100644 webui/js/tunnel.js
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Flare Tunnel
+
Create a secure public URL to access your Agent Zero instance anytime,
+ anywhere.
+
+
+
+
+
Tunnel provider
+
Select provider for public tunnel
+
+
+
+
+
+
+
+ progress_activity
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Scan with mobile device
+
+
+ Share this URL to allow others to access your Agent Zero instance.
+
+
+ This URL will persist until you stop the tunnel or restart the Docker container.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Scan with mobile device
-
-
- Share this URL to allow others to access your Agent Zero instance.
-
-
- This URL will persist until you stop the tunnel or restart the Docker container.
-
-
-
-
-
-
-
-
-
-
-
+
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);
- });
- }
- }));
-});