frdel commited on
Commit
e93507d
·
1 Parent(s): 0ff51c3

tunnel component refactor

Browse files
python/api/settings_get.py CHANGED
@@ -6,3 +6,7 @@ class GetSettings(ApiHandler):
6
  async def process(self, input: dict, request: Request) -> dict | Response:
7
  set = settings.convert_out(settings.get_settings())
8
  return {"settings": set}
 
 
 
 
 
6
  async def process(self, input: dict, request: Request) -> dict | Response:
7
  set = settings.convert_out(settings.get_settings())
8
  return {"settings": set}
9
+
10
+ @classmethod
11
+ def get_methods(cls) -> list[str]:
12
+ return ["GET", "POST"]
webui/components/notifications/notification-toast-stack.html CHANGED
@@ -56,7 +56,7 @@
56
  position: absolute;
57
  bottom: 5px; /* Spacing from the bottom of the zero-height container */
58
  padding-right: 5px;
59
- z-index: 1500;
60
  display: flex;
61
  flex-direction: column-reverse; /* Stack toasts upwards */
62
  gap: 8px;
 
56
  position: absolute;
57
  bottom: 5px; /* Spacing from the bottom of the zero-height container */
58
  padding-right: 5px;
59
+ z-index: 15000;
60
  display: flex;
61
  flex-direction: column-reverse; /* Stack toasts upwards */
62
  gap: 8px;
webui/components/settings/tunnel/tunnel-section.html ADDED
@@ -0,0 +1,371 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <html>
2
+
3
+ <head>
4
+ <title></title>
5
+
6
+ <!-- Import the alpine store -->
7
+ <script type="module">
8
+ import { store } from "/components/settings/tunnel/tunnel-store.js";
9
+ </script>
10
+ </head>
11
+
12
+ <body>
13
+
14
+ <!-- This construct of x-data + x-if is used to ensure the component is only rendered when the store is available -->
15
+ <div x-data>
16
+ <template x-if="$store.tunnelStore">
17
+
18
+ <!-- Keep in mind that <template> can have only one root element inside -->
19
+ <div id="tunnel-settings-section">
20
+ <div class="section-title">Flare Tunnel</div>
21
+ <div class="section-description">Create a secure public URL to access your Agent Zero instance anytime,
22
+ anywhere.</div>
23
+ <!-- Tunnel content UI -->
24
+ <div class="tunnel-container">
25
+ <div class="field">
26
+ <div class="field-label">
27
+ <div class="field-title">Tunnel provider</div>
28
+ <div class="field-description">Select provider for public tunnel</div>
29
+ </div>
30
+ <div class="field-control">
31
+ <select id="tunnel-provider" x-model="$store.tunnelStore.provider"
32
+ :disabled="$store.tunnelStore.isLoading">
33
+ <option value="cloudflared">Cloudflare</option>
34
+ <option value="serveo">Serveo</option>
35
+ </select>
36
+ </div>
37
+ </div>
38
+ <!-- Loading spinner for tunnel operations -->
39
+ <div class="loading-spinner" x-show="$store.tunnelStore.isLoading">
40
+ <span class="icon material-symbols-outlined spin">progress_activity</span>
41
+ <span x-text="$store.tunnelStore.loadingText || 'Processing tunnel request...'"></span>
42
+ </div>
43
+
44
+ <!-- Tunnel content when not loading -->
45
+ <div x-show="!$store.tunnelStore.isLoading">
46
+ <!-- Tunnel link display when generated -->
47
+ <div class="tunnel-link-container" x-show="$store.tunnelStore.linkGenerated">
48
+ <div class="tunnel-link-field">
49
+ <input type="text" class="tunnel-link-input" :value="$store.tunnelStore.tunnelLink"
50
+ readonly>
51
+ <div class="buttons-row">
52
+ <button class="copy-link-button" @click="$store.tunnelStore.copyToClipboard()"
53
+ title="Copy to clipboard">
54
+ <span class="icon material-symbols-outlined">content_copy</span> Copy
55
+ </button>
56
+ <button class="refresh-link-button" @click="$store.tunnelStore.refreshLink()"
57
+ title="Generate new URL">
58
+ <span class="icon material-symbols-outlined">refresh</span> Refresh
59
+ </button>
60
+ </div>
61
+ </div>
62
+ <div class="tunnel-qr-container">
63
+ <div class="tunnel-qr-code" id="qrcode-tunnel">
64
+ <!-- QR code will be generated here -->
65
+ </div>
66
+ <div class="tunnel-qr-label">Scan with mobile device</div>
67
+ </div>
68
+ <div class="tunnel-link-info">
69
+ Share this URL to allow others to access your Agent Zero instance.
70
+ </div>
71
+ <div class="tunnel-link-persistence">
72
+ This URL will persist until you stop the tunnel or restart the Docker container.
73
+ </div>
74
+ <div class="stop-tunnel-container">
75
+ <button class="btn btn-danger" @click="$store.tunnelStore.stopTunnel()">
76
+ <span class="icon material-symbols-outlined">stop_circle</span> Stop Tunnel
77
+ </button>
78
+ </div>
79
+ </div>
80
+ <!-- Generate tunnel button when no link exists -->
81
+ <div class="tunnel-actions" x-show="!$store.tunnelStore.linkGenerated">
82
+ <button class="btn btn-ok" @click="$store.tunnelStore.generateLink()">
83
+ <span class="icon material-symbols-outlined">play_circle</span> Create Tunnel
84
+ </button>
85
+ </div>
86
+ </div>
87
+ </div>
88
+ </div>
89
+
90
+ </template>
91
+ </div>
92
+
93
+ <!-- Optional style for the component -->
94
+ <style>
95
+ /* Tunnel Modal Styles */
96
+ .tunnel-container {
97
+ padding: 1rem;
98
+ width: 100%;
99
+ }
100
+
101
+ .tunnel-description {
102
+ margin-bottom: 1.5rem;
103
+ text-align: center;
104
+ }
105
+
106
+ .tunnel-actions {
107
+ display: flex;
108
+ justify-content: center;
109
+ margin-top: 2rem;
110
+ margin-bottom: 2rem;
111
+ }
112
+
113
+ .tunnel-link-container {
114
+ margin-top: 1rem;
115
+ display: flex;
116
+ flex-direction: column;
117
+ gap: 1rem;
118
+ }
119
+
120
+ .tunnel-link-field {
121
+ display: flex;
122
+ align-items: center;
123
+ margin-bottom: 0.5rem;
124
+ background-color: var(--bg-color-secondary);
125
+ border: 1px solid var(--border-color);
126
+ border-radius: 4px;
127
+ }
128
+
129
+ .tunnel-link-input {
130
+ flex: 1;
131
+ padding: 0.75rem;
132
+ background-color: transparent;
133
+ border: none;
134
+ color: var(--text-color);
135
+ font-size: 0.9rem;
136
+ outline: none;
137
+ }
138
+
139
+ .copy-link-button {
140
+ padding: 0.5rem 0.75rem;
141
+ background: none;
142
+ border: none;
143
+ border-left: 1px solid var(--border-color);
144
+ color: var(--text-color);
145
+ cursor: pointer;
146
+ transition: background-color 0.2s;
147
+ }
148
+
149
+ .copy-link-button:hover {
150
+ background-color: var(--bg-color-tertiary);
151
+ }
152
+
153
+ .copy-link-button i,
154
+ .refresh-link-button i,
155
+ .btn i {
156
+ margin-right: 6px;
157
+ }
158
+
159
+ .tunnel-link-info {
160
+ margin-top: 1rem;
161
+ font-size: 1rem;
162
+ color: var(--text-color);
163
+ text-align: center;
164
+ line-height: 1.5;
165
+ }
166
+
167
+ .loading-spinner {
168
+ display: flex;
169
+ justify-content: center;
170
+ align-items: center;
171
+ margin-top: 2rem;
172
+ margin-bottom: 2rem;
173
+ min-height: 38px;
174
+ font-style: italic;
175
+ color: var(--text-color-secondary);
176
+ }
177
+
178
+ .loading-spinner i {
179
+ font-size: 1.5rem;
180
+ margin-right: 10px;
181
+ color: var(--accent-color);
182
+ }
183
+
184
+ .refresh-link-button {
185
+ padding: 0.5rem 0.75rem;
186
+ background: none;
187
+ border: none;
188
+ border-left: 1px solid var(--border-color);
189
+ color: var(--text-color);
190
+ cursor: pointer;
191
+ transition: background-color 0.2s;
192
+ }
193
+
194
+ .refresh-link-button:hover {
195
+ background-color: var(--bg-color-tertiary);
196
+ color: var(--accent-color);
197
+ }
198
+
199
+ .tunnel-link-persistence {
200
+ margin-top: 0.75rem;
201
+ font-size: 0.95rem;
202
+ color: rgba(255, 255, 255, 0.7);
203
+ text-align: center;
204
+ font-style: italic;
205
+ }
206
+
207
+ .btn-danger {
208
+ background-color: #dc3545;
209
+ color: white;
210
+ border: none;
211
+ }
212
+
213
+ .btn-danger:hover {
214
+ background-color: #bd2130;
215
+ }
216
+
217
+ .stop-tunnel-container {
218
+ margin-top: 20px;
219
+ display: flex;
220
+ justify-content: center;
221
+ }
222
+
223
+ /* Section title icon styling */
224
+ .section-title i {
225
+ margin-right: 8px;
226
+ }
227
+
228
+ /* Copy button states */
229
+ .copy-success {
230
+ background-color: rgba(40, 167, 69, 0.15) !important;
231
+ color: #28a745 !important;
232
+ border-left: 1px solid rgba(40, 167, 69, 0.5) !important;
233
+ transition: all 0.3s ease-in-out;
234
+ }
235
+
236
+ .copy-error {
237
+ background-color: rgba(220, 53, 69, 0.15) !important;
238
+ color: #dc3545 !important;
239
+ border-left: 1px solid rgba(220, 53, 69, 0.5) !important;
240
+ transition: all 0.3s ease-in-out;
241
+ }
242
+
243
+ /* Animation for copy button */
244
+ @keyframes pulse {
245
+ 0% {
246
+ transform: scale(1);
247
+ }
248
+
249
+ 50% {
250
+ transform: scale(1.05);
251
+ }
252
+
253
+ 100% {
254
+ transform: scale(1);
255
+ }
256
+ }
257
+
258
+ .copy-success,
259
+ .copy-error {
260
+ animation: pulse 0.5s;
261
+ }
262
+
263
+ /* Refresh button state */
264
+ .refreshing {
265
+ opacity: 0.7;
266
+ pointer-events: none;
267
+ background-color: rgba(108, 117, 125, 0.15) !important;
268
+ border-left: 1px solid rgba(108, 117, 125, 0.5) !important;
269
+ }
270
+
271
+ /* Create and Stop button states */
272
+ .creating,
273
+ .stopping {
274
+ opacity: 0.8;
275
+ pointer-events: none;
276
+ cursor: not-allowed;
277
+ }
278
+
279
+ .creating {
280
+ background-color: rgba(0, 123, 255, 0.7) !important;
281
+ }
282
+
283
+ .stopping {
284
+ background-color: rgba(220, 53, 69, 0.7) !important;
285
+ }
286
+
287
+ /* QR Code Styles */
288
+ .tunnel-qr-container {
289
+ display: flex;
290
+ align-items: center;
291
+ justify-content: flex-start;
292
+ gap: 1rem;
293
+ margin: 0.5rem 0;
294
+ padding: 1rem;
295
+ background-color: var(--bg-color-secondary);
296
+ border: 1px solid var(--border-color);
297
+ border-radius: 8px;
298
+ }
299
+
300
+ .tunnel-qr-code {
301
+ flex-shrink: 0;
302
+ display: flex;
303
+ align-items: center;
304
+ justify-content: center;
305
+ min-width: 128px;
306
+ min-height: 128px;
307
+ background-color: white;
308
+ border-radius: 8px;
309
+ padding: 8px;
310
+ }
311
+
312
+ .tunnel-qr-code canvas,
313
+ .tunnel-qr-code img {
314
+ border-radius: 4px;
315
+ max-width: 100%;
316
+ max-height: 100%;
317
+ }
318
+
319
+ .tunnel-qr-label {
320
+ font-size: 0.9rem;
321
+ color: var(--text-color-secondary);
322
+ text-align: center;
323
+ line-height: 1.4;
324
+ flex: 1;
325
+ }
326
+
327
+ .qr-error {
328
+ color: var(--error-color, #dc3545);
329
+ font-size: 0.8rem;
330
+ text-align: center;
331
+ padding: 1rem;
332
+ background-color: rgba(220, 53, 69, 0.1);
333
+ border-radius: 4px;
334
+ border: 1px solid rgba(220, 53, 69, 0.2);
335
+ }
336
+
337
+ /* Responsive design for QR code container */
338
+ @media (max-width: 640px) {
339
+ .tunnel-qr-container {
340
+ flex-direction: column;
341
+ text-align: center;
342
+ gap: 0.75rem;
343
+ padding: 0.75rem;
344
+ }
345
+
346
+ .tunnel-qr-code {
347
+ min-width: 100px;
348
+ min-height: 100px;
349
+ align-self: center;
350
+ }
351
+
352
+ .tunnel-qr-label {
353
+ text-align: center;
354
+ }
355
+ }
356
+
357
+ /* Light mode adjustments */
358
+ .light-mode .tunnel-qr-code {
359
+ background-color: #ffffff;
360
+ border: 1px solid #e0e0e0;
361
+ }
362
+
363
+ .light-mode .qr-error {
364
+ background-color: rgba(220, 53, 69, 0.05);
365
+ color: #dc3545;
366
+ }
367
+ </style>
368
+
369
+ </body>
370
+
371
+ </html>
webui/components/settings/tunnel/tunnel-store.js ADDED
@@ -0,0 +1,430 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createStore } from "/js/AlpineStore.js";
2
+ import * as Sleep from "/js/sleep.js";
3
+
4
+ // define the model object holding data and functions
5
+ const model = {
6
+ isLoading: false,
7
+ tunnelLink: "",
8
+ linkGenerated: false,
9
+ loadingText: "",
10
+ qrCodeInstance: null,
11
+ provider: "cloudflared",
12
+
13
+ init() {
14
+ this.checkTunnelStatus();
15
+ },
16
+
17
+ generateQRCode() {
18
+ if (!this.tunnelLink) return;
19
+
20
+ const qrContainer = document.getElementById("qrcode-tunnel");
21
+ if (!qrContainer) return;
22
+
23
+ // Clear any existing QR code
24
+ qrContainer.innerHTML = "";
25
+
26
+ try {
27
+ // Generate new QR code
28
+ this.qrCodeInstance = new QRCode(qrContainer, {
29
+ text: this.tunnelLink,
30
+ width: 128,
31
+ height: 128,
32
+ colorDark: "#000000",
33
+ colorLight: "#ffffff",
34
+ correctLevel: QRCode.CorrectLevel.M,
35
+ });
36
+ } catch (error) {
37
+ console.error("Error generating QR code:", error);
38
+ qrContainer.innerHTML =
39
+ '<div class="qr-error">QR code generation failed</div>';
40
+ }
41
+ },
42
+
43
+ async checkTunnelStatus() {
44
+ try {
45
+ const response = await fetchApi("/tunnel_proxy", {
46
+ method: "POST",
47
+ headers: {
48
+ "Content-Type": "application/json",
49
+ },
50
+ body: JSON.stringify({ action: "get" }),
51
+ });
52
+
53
+ const data = await response.json();
54
+
55
+ if (data.success && data.tunnel_url) {
56
+ // Update the stored URL if it's different from what we have
57
+ if (this.tunnelLink !== data.tunnel_url) {
58
+ this.tunnelLink = data.tunnel_url;
59
+ localStorage.setItem("agent_zero_tunnel_url", data.tunnel_url);
60
+ }
61
+ this.linkGenerated = true;
62
+ // Generate QR code for the tunnel URL
63
+ Sleep.Skip().then(() => this.generateQRCode());
64
+ } else {
65
+ // Check if we have a stored tunnel URL
66
+ const storedTunnelUrl = localStorage.getItem("agent_zero_tunnel_url");
67
+
68
+ if (storedTunnelUrl) {
69
+ // Use the stored URL but verify it's still valid
70
+ const verifyResponse = await fetchApi("/tunnel_proxy", {
71
+ method: "POST",
72
+ headers: {
73
+ "Content-Type": "application/json",
74
+ },
75
+ body: JSON.stringify({ action: "verify", url: storedTunnelUrl }),
76
+ });
77
+
78
+ const verifyData = await verifyResponse.json();
79
+
80
+ if (verifyData.success && verifyData.is_valid) {
81
+ this.tunnelLink = storedTunnelUrl;
82
+ this.linkGenerated = true;
83
+ // Generate QR code for the tunnel URL
84
+ Sleep.Skip().then(() => this.generateQRCode());
85
+ } else {
86
+ // Clear stale URL
87
+ localStorage.removeItem("agent_zero_tunnel_url");
88
+ this.tunnelLink = "";
89
+ this.linkGenerated = false;
90
+ }
91
+ } else {
92
+ // No stored URL, show the generate button
93
+ this.tunnelLink = "";
94
+ this.linkGenerated = false;
95
+ }
96
+ }
97
+ } catch (error) {
98
+ console.error("Error checking tunnel status:", error);
99
+ this.tunnelLink = "";
100
+ this.linkGenerated = false;
101
+ }
102
+ },
103
+
104
+ async refreshLink() {
105
+ // Call generate but with a confirmation first
106
+ if (
107
+ confirm(
108
+ "Are you sure you want to generate a new tunnel URL? The old URL will no longer work."
109
+ )
110
+ ) {
111
+
112
+ this.isLoading = true;
113
+ this.loadingText = "Refreshing tunnel...";
114
+
115
+ // Change refresh button appearance
116
+ const refreshButton = document.querySelector("#tunnel-settings-section .refresh-link-button");
117
+ const originalContent = refreshButton.innerHTML;
118
+ refreshButton.innerHTML =
119
+ '<span class="icon material-symbols-outlined spin">progress_activity</span> Refreshing...';
120
+ refreshButton.disabled = true;
121
+ refreshButton.classList.add("refreshing");
122
+
123
+ try {
124
+ // First stop any existing tunnel
125
+ const stopResponse = await fetchApi("/tunnel_proxy", {
126
+ method: "POST",
127
+ headers: {
128
+ "Content-Type": "application/json",
129
+ },
130
+ body: JSON.stringify({ action: "stop" }),
131
+ });
132
+
133
+ // Check if stopping was successful
134
+ const stopData = await stopResponse.json();
135
+ if (!stopData.success) {
136
+ console.warn("Warning: Couldn't stop existing tunnel cleanly");
137
+ // Continue anyway since we want to create a new one
138
+ }
139
+
140
+ // Then generate a new one
141
+ await this.generateLink();
142
+ } catch (error) {
143
+ console.error("Error refreshing tunnel:", error);
144
+ window.toastFrontendError("Error refreshing tunnel", "Tunnel Error");
145
+ this.isLoading = false;
146
+ this.loadingText = "";
147
+ } finally {
148
+ // Reset refresh button
149
+ refreshButton.innerHTML = originalContent;
150
+ refreshButton.disabled = false;
151
+ refreshButton.classList.remove("refreshing");
152
+ }
153
+ }
154
+ },
155
+
156
+ async generateLink() {
157
+ // First check if authentication is enabled
158
+ try {
159
+ const authCheckResponse = await fetchApi("/settings_get");
160
+ const authData = await authCheckResponse.json();
161
+
162
+ // Find the auth_login and auth_password in the settings
163
+ let hasAuth = false;
164
+
165
+ if (authData && authData.settings && authData.settings.sections) {
166
+ for (const section of authData.settings.sections) {
167
+ if (section.fields) {
168
+ const authLoginField = section.fields.find(
169
+ (field) => field.id === "auth_login"
170
+ );
171
+ const authPasswordField = section.fields.find(
172
+ (field) => field.id === "auth_password"
173
+ );
174
+
175
+ if (
176
+ authLoginField &&
177
+ authPasswordField &&
178
+ authLoginField.value &&
179
+ authPasswordField.value
180
+ ) {
181
+ hasAuth = true;
182
+ break;
183
+ }
184
+ }
185
+ }
186
+ }
187
+
188
+ // If no authentication is set, warn the user
189
+ if (!hasAuth) {
190
+ const proceed = confirm(
191
+ "WARNING: No authentication is configured for your Agent Zero instance.\n\n" +
192
+ "Creating a public tunnel without authentication means anyone with the URL " +
193
+ "can access your Agent Zero instance.\n\n" +
194
+ "It is recommended to set up authentication in the Settings > Authentication section " +
195
+ "before creating a public tunnel.\n\n" +
196
+ "Do you want to proceed anyway?"
197
+ );
198
+
199
+ if (!proceed) {
200
+ return; // User cancelled
201
+ }
202
+ }
203
+ } catch (error) {
204
+ console.error("Error checking authentication status:", error);
205
+ // Continue anyway if we can't check auth status
206
+ }
207
+
208
+ this.isLoading = true;
209
+ this.loadingText = "Creating tunnel...";
210
+
211
+ // Change create button appearance
212
+ const createButton = document.querySelector("#tunnel-settings-section .tunnel-actions .btn-ok");
213
+ if (createButton) {
214
+ createButton.innerHTML =
215
+ '<span class="icon material-symbols-outlined spin">progress_activity</span> Creating...';
216
+ createButton.disabled = true;
217
+ createButton.classList.add("creating");
218
+ }
219
+
220
+ try {
221
+ // Call the backend API to create a tunnel
222
+ const response = await fetchApi("/tunnel_proxy", {
223
+ method: "POST",
224
+ headers: {
225
+ "Content-Type": "application/json",
226
+ },
227
+ body: JSON.stringify({
228
+ action: "create",
229
+ provider: this.provider,
230
+ // port: window.location.port || (window.location.protocol === 'https:' ? 443 : 80)
231
+ }),
232
+ });
233
+
234
+ const data = await response.json();
235
+
236
+ if (data.success && data.tunnel_url) {
237
+ // Store the tunnel URL in localStorage for persistence
238
+ localStorage.setItem("agent_zero_tunnel_url", data.tunnel_url);
239
+
240
+ this.tunnelLink = data.tunnel_url;
241
+ this.linkGenerated = true;
242
+
243
+ // Generate QR code for the tunnel URL
244
+ Sleep.Skip().then(() => this.generateQRCode());
245
+
246
+ // Show success message to confirm creation
247
+ window.toastFrontendInfo(
248
+ "Tunnel created successfully",
249
+ "Tunnel Status"
250
+ );
251
+ } else {
252
+ // The tunnel might still be starting up, check again after a delay
253
+ this.loadingText = "Tunnel creation taking longer than expected...";
254
+
255
+ // Wait for 5 seconds and check if the tunnel is running
256
+ await new Promise((resolve) => setTimeout(resolve, 5000));
257
+
258
+ // Check if tunnel is running now
259
+ try {
260
+ const statusResponse = await fetchApi("/tunnel_proxy", {
261
+ method: "POST",
262
+ headers: {
263
+ "Content-Type": "application/json",
264
+ },
265
+ body: JSON.stringify({ action: "get" }),
266
+ });
267
+
268
+ const statusData = await statusResponse.json();
269
+
270
+ if (statusData.success && statusData.tunnel_url) {
271
+ // Tunnel is now running, we can update the UI
272
+ localStorage.setItem(
273
+ "agent_zero_tunnel_url",
274
+ statusData.tunnel_url
275
+ );
276
+ this.tunnelLink = statusData.tunnel_url;
277
+ this.linkGenerated = true;
278
+
279
+ // Generate QR code for the tunnel URL
280
+ Sleep.Skip().then(() => this.generateQRCode());
281
+
282
+ window.toastFrontendInfo(
283
+ "Tunnel created successfully",
284
+ "Tunnel Status"
285
+ );
286
+ return;
287
+ }
288
+ } catch (statusError) {
289
+ console.error("Error checking tunnel status:", statusError);
290
+ }
291
+
292
+ // If we get here, the tunnel really failed to start
293
+ const errorMessage =
294
+ data.message || "Failed to create tunnel. Please try again.";
295
+ window.toastFrontendError(errorMessage, "Tunnel Error");
296
+ console.error("Tunnel creation failed:", data);
297
+ }
298
+ } catch (error) {
299
+ window.toastFrontendError("Error creating tunnel", "Tunnel Error");
300
+ console.error("Error creating tunnel:", error);
301
+ } finally {
302
+ this.isLoading = false;
303
+ this.loadingText = "";
304
+
305
+ // Reset create button if it's still in the DOM
306
+ const createButton = document.querySelector("#tunnel-settings-section .tunnel-actions .btn-ok");
307
+ if (createButton) {
308
+ createButton.innerHTML =
309
+ '<span class="icon material-symbols-outlined">play_circle</span> Create Tunnel';
310
+ createButton.disabled = false;
311
+ createButton.classList.remove("creating");
312
+ }
313
+ }
314
+ },
315
+
316
+ async stopTunnel() {
317
+ if (
318
+ confirm(
319
+ "Are you sure you want to stop the tunnel? The URL will no longer be accessible."
320
+ )
321
+ ) {
322
+ this.isLoading = true;
323
+ this.loadingText = "Stopping tunnel...";
324
+
325
+ try {
326
+ // Call the backend to stop the tunnel
327
+ const response = await fetchApi("/tunnel_proxy", {
328
+ method: "POST",
329
+ headers: {
330
+ "Content-Type": "application/json",
331
+ },
332
+ body: JSON.stringify({ action: "stop" }),
333
+ });
334
+
335
+ const data = await response.json();
336
+
337
+ if (data.success) {
338
+ // Clear the stored URL
339
+ localStorage.removeItem("agent_zero_tunnel_url");
340
+
341
+ // Clear QR code
342
+ const qrContainer = document.getElementById("qrcode-tunnel");
343
+ if (qrContainer) {
344
+ qrContainer.innerHTML = "";
345
+ }
346
+ this.qrCodeInstance = null;
347
+
348
+ // Update UI state
349
+ this.tunnelLink = "";
350
+ this.linkGenerated = false;
351
+
352
+ window.toastFrontendInfo(
353
+ "Tunnel stopped successfully",
354
+ "Tunnel Status"
355
+ );
356
+ } else {
357
+ window.toastFrontendError("Failed to stop tunnel", "Tunnel Error");
358
+
359
+ // Reset stop button
360
+ stopButton.innerHTML = originalStopContent;
361
+ stopButton.disabled = false;
362
+ stopButton.classList.remove("stopping");
363
+ }
364
+ } catch (error) {
365
+ window.toastFrontendError("Error stopping tunnel", "Tunnel Error");
366
+ console.error("Error stopping tunnel:", error);
367
+
368
+ // Reset stop button
369
+ stopButton.innerHTML = originalStopContent;
370
+ stopButton.disabled = false;
371
+ stopButton.classList.remove("stopping");
372
+ } finally {
373
+ this.isLoading = false;
374
+ this.loadingText = "";
375
+ }
376
+ }
377
+ },
378
+
379
+ copyToClipboard() {
380
+ if (!this.tunnelLink) return;
381
+
382
+ const copyButton = document.querySelector("#tunnel-settings-section .copy-link-button");
383
+ const originalContent = copyButton.innerHTML;
384
+
385
+ navigator.clipboard
386
+ .writeText(this.tunnelLink)
387
+ .then(() => {
388
+ // Update button to show success state
389
+ copyButton.innerHTML =
390
+ '<span class="icon material-symbols-outlined">check</span> Copied!';
391
+ copyButton.classList.add("copy-success");
392
+
393
+ // Show toast notification
394
+ window.toastFrontendInfo(
395
+ "Tunnel URL copied to clipboard!",
396
+ "Clipboard"
397
+ );
398
+
399
+ // Reset button after 2 seconds
400
+ setTimeout(() => {
401
+ copyButton.innerHTML = originalContent;
402
+ copyButton.classList.remove("copy-success");
403
+ }, 2000);
404
+ })
405
+ .catch((err) => {
406
+ console.error("Failed to copy URL: ", err);
407
+ window.toastFrontendError(
408
+ "Failed to copy tunnel URL",
409
+ "Clipboard Error"
410
+ );
411
+
412
+ // Show error state
413
+ copyButton.innerHTML =
414
+ '<span class="icon material-symbols-outlined">close</span> Failed';
415
+ copyButton.classList.add("copy-error");
416
+
417
+ // Reset button after 2 seconds
418
+ setTimeout(() => {
419
+ copyButton.innerHTML = originalContent;
420
+ copyButton.classList.remove("copy-error");
421
+ }, 2000);
422
+ });
423
+ },
424
+ };
425
+
426
+ // convert it to alpine store
427
+ const store = createStore("tunnelStore", model);
428
+
429
+ // export for use in other files
430
+ export { store };
webui/css/tunnel.css DELETED
@@ -1,262 +0,0 @@
1
- /* Tunnel Modal Styles */
2
- .tunnel-container {
3
- padding: 1rem;
4
- width: 100%;
5
- }
6
-
7
- .tunnel-description {
8
- margin-bottom: 1.5rem;
9
- text-align: center;
10
- }
11
-
12
- .tunnel-actions {
13
- display: flex;
14
- justify-content: center;
15
- margin-top: 2rem;
16
- margin-bottom: 2rem;
17
- }
18
-
19
- .tunnel-link-container {
20
- margin-top: 1rem;
21
- display: flex;
22
- flex-direction: column;
23
- gap: 1rem;
24
- }
25
-
26
- .tunnel-link-field {
27
- display: flex;
28
- align-items: center;
29
- margin-bottom: 0.5rem;
30
- background-color: var(--bg-color-secondary);
31
- border: 1px solid var(--border-color);
32
- border-radius: 4px;
33
- }
34
-
35
- .tunnel-link-input {
36
- flex: 1;
37
- padding: 0.75rem;
38
- background-color: transparent;
39
- border: none;
40
- color: var(--text-color);
41
- font-size: 0.9rem;
42
- outline: none;
43
- }
44
-
45
- .copy-link-button {
46
- padding: 0.5rem 0.75rem;
47
- background: none;
48
- border: none;
49
- border-left: 1px solid var(--border-color);
50
- color: var(--text-color);
51
- cursor: pointer;
52
- transition: background-color 0.2s;
53
- }
54
-
55
- .copy-link-button:hover {
56
- background-color: var(--bg-color-tertiary);
57
- }
58
-
59
- .copy-link-button i,
60
- .refresh-link-button i,
61
- .btn i {
62
- margin-right: 6px;
63
- }
64
-
65
- .tunnel-link-info {
66
- margin-top: 1rem;
67
- font-size: 1rem;
68
- color: var(--text-color);
69
- text-align: center;
70
- line-height: 1.5;
71
- }
72
-
73
- .loading-spinner {
74
- display: flex;
75
- justify-content: center;
76
- align-items: center;
77
- margin-top: 2rem;
78
- margin-bottom: 2rem;
79
- min-height: 38px;
80
- font-style: italic;
81
- color: var(--text-color-secondary);
82
- }
83
-
84
- .loading-spinner i {
85
- font-size: 1.5rem;
86
- margin-right: 10px;
87
- color: var(--accent-color);
88
- }
89
-
90
- .refresh-link-button {
91
- padding: 0.5rem 0.75rem;
92
- background: none;
93
- border: none;
94
- border-left: 1px solid var(--border-color);
95
- color: var(--text-color);
96
- cursor: pointer;
97
- transition: background-color 0.2s;
98
- }
99
-
100
- .refresh-link-button:hover {
101
- background-color: var(--bg-color-tertiary);
102
- color: var(--accent-color);
103
- }
104
-
105
- .tunnel-link-persistence {
106
- margin-top: 0.75rem;
107
- font-size: 0.95rem;
108
- color: rgba(255, 255, 255, 0.7);
109
- text-align: center;
110
- font-style: italic;
111
- }
112
-
113
- .btn-danger {
114
- background-color: #dc3545;
115
- color: white;
116
- border: none;
117
- }
118
-
119
- .btn-danger:hover {
120
- background-color: #bd2130;
121
- }
122
-
123
- .stop-tunnel-container {
124
- margin-top: 20px;
125
- display: flex;
126
- justify-content: center;
127
- }
128
-
129
- /* Section title icon styling */
130
- .section-title i {
131
- margin-right: 8px;
132
- }
133
-
134
- /* Copy button states */
135
- .copy-success {
136
- background-color: rgba(40, 167, 69, 0.15) !important;
137
- color: #28a745 !important;
138
- border-left: 1px solid rgba(40, 167, 69, 0.5) !important;
139
- transition: all 0.3s ease-in-out;
140
- }
141
-
142
- .copy-error {
143
- background-color: rgba(220, 53, 69, 0.15) !important;
144
- color: #dc3545 !important;
145
- border-left: 1px solid rgba(220, 53, 69, 0.5) !important;
146
- transition: all 0.3s ease-in-out;
147
- }
148
-
149
- /* Animation for copy button */
150
- @keyframes pulse {
151
- 0% { transform: scale(1); }
152
- 50% { transform: scale(1.05); }
153
- 100% { transform: scale(1); }
154
- }
155
-
156
- .copy-success, .copy-error {
157
- animation: pulse 0.5s;
158
- }
159
-
160
- /* Refresh button state */
161
- .refreshing {
162
- opacity: 0.7;
163
- pointer-events: none;
164
- background-color: rgba(108, 117, 125, 0.15) !important;
165
- border-left: 1px solid rgba(108, 117, 125, 0.5) !important;
166
- }
167
-
168
- /* Create and Stop button states */
169
- .creating, .stopping {
170
- opacity: 0.8;
171
- pointer-events: none;
172
- cursor: not-allowed;
173
- }
174
-
175
- .creating {
176
- background-color: rgba(0, 123, 255, 0.7) !important;
177
- }
178
-
179
- .stopping {
180
- background-color: rgba(220, 53, 69, 0.7) !important;
181
- }
182
-
183
- /* QR Code Styles */
184
- .tunnel-qr-container {
185
- display: flex;
186
- align-items: center;
187
- justify-content: flex-start;
188
- gap: 1rem;
189
- margin: 0.5rem 0;
190
- padding: 1rem;
191
- background-color: var(--bg-color-secondary);
192
- border: 1px solid var(--border-color);
193
- border-radius: 8px;
194
- }
195
-
196
- .tunnel-qr-code {
197
- flex-shrink: 0;
198
- display: flex;
199
- align-items: center;
200
- justify-content: center;
201
- min-width: 128px;
202
- min-height: 128px;
203
- background-color: white;
204
- border-radius: 8px;
205
- padding: 8px;
206
- }
207
-
208
- .tunnel-qr-code canvas,
209
- .tunnel-qr-code img {
210
- border-radius: 4px;
211
- max-width: 100%;
212
- max-height: 100%;
213
- }
214
-
215
- .tunnel-qr-label {
216
- font-size: 0.9rem;
217
- color: var(--text-color-secondary);
218
- text-align: center;
219
- line-height: 1.4;
220
- flex: 1;
221
- }
222
-
223
- .qr-error {
224
- color: var(--error-color, #dc3545);
225
- font-size: 0.8rem;
226
- text-align: center;
227
- padding: 1rem;
228
- background-color: rgba(220, 53, 69, 0.1);
229
- border-radius: 4px;
230
- border: 1px solid rgba(220, 53, 69, 0.2);
231
- }
232
-
233
- /* Responsive design for QR code container */
234
- @media (max-width: 640px) {
235
- .tunnel-qr-container {
236
- flex-direction: column;
237
- text-align: center;
238
- gap: 0.75rem;
239
- padding: 0.75rem;
240
- }
241
-
242
- .tunnel-qr-code {
243
- min-width: 100px;
244
- min-height: 100px;
245
- align-self: center;
246
- }
247
-
248
- .tunnel-qr-label {
249
- text-align: center;
250
- }
251
- }
252
-
253
- /* Light mode adjustments */
254
- .light-mode .tunnel-qr-code {
255
- background-color: #ffffff;
256
- border: 1px solid #e0e0e0;
257
- }
258
-
259
- .light-mode .qr-error {
260
- background-color: rgba(220, 53, 69, 0.05);
261
- color: #dc3545;
262
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
webui/index.html CHANGED
@@ -17,7 +17,6 @@
17
  <link rel="stylesheet" href="css/speech.css">
18
  <link rel="stylesheet" href="css/history.css">
19
  <link rel="stylesheet" href="css/scheduler-datepicker.css">
20
- <link rel="stylesheet" href="css/tunnel.css">
21
  <link rel="stylesheet" href="css/notification.css">
22
 
23
  <!-- Flatpickr for datetime picker -->
@@ -689,69 +688,7 @@
689
 
690
  <!-- Tunnel section content - only visible in External Services tab -->
691
  <div id="section-tunnel" class="section" x-show="activeTab === 'external'">
692
- <div class="section-title">Flare Tunnel</div>
693
- <div class="section-description">Create a secure public URL to access your Agent Zero instance anytime, anywhere.</div>
694
- <!-- Tunnel content UI -->
695
- <div class="tunnel-container" x-data="tunnelSettings">
696
- <div class="field">
697
- <div class="field-label">
698
- <div class="field-title">Tunnel provider</div>
699
- <div class="field-description">Select provider for public tunnel</div>
700
- </div>
701
- <div class="field-control">
702
- <select id="tunnel-provider" x-model="provider" :disabled="isLoading">
703
- <option value="cloudflared">Cloudflare</option>
704
- <option value="serveo">Serveo</option>
705
- </select>
706
- </div>
707
- </div>
708
- <!-- Loading spinner for tunnel operations -->
709
- <div class="loading-spinner" x-show="isLoading">
710
- <span class="icon material-symbols-outlined spin">progress_activity</span>
711
- <span x-text="loadingText || 'Processing tunnel request...'"></span>
712
- </div>
713
-
714
- <!-- Tunnel content when not loading -->
715
- <div x-show="!isLoading">
716
- <!-- Tunnel link display when generated -->
717
- <div class="tunnel-link-container" x-show="linkGenerated">
718
- <div class="tunnel-link-field">
719
- <input type="text" class="tunnel-link-input" :value="tunnelLink" readonly>
720
- <div class="buttons-row">
721
- <button class="copy-link-button" @click="copyToClipboard()" title="Copy to clipboard">
722
- <span class="icon material-symbols-outlined">content_copy</span> Copy
723
- </button>
724
- <button class="refresh-link-button" @click="refreshLink()" title="Generate new URL">
725
- <span class="icon material-symbols-outlined">refresh</span> Refresh
726
- </button>
727
- </div>
728
- </div>
729
- <div class="tunnel-qr-container">
730
- <div class="tunnel-qr-code" id="qrcode-tunnel">
731
- <!-- QR code will be generated here -->
732
- </div>
733
- <div class="tunnel-qr-label">Scan with mobile device</div>
734
- </div>
735
- <div class="tunnel-link-info">
736
- Share this URL to allow others to access your Agent Zero instance.
737
- </div>
738
- <div class="tunnel-link-persistence">
739
- This URL will persist until you stop the tunnel or restart the Docker container.
740
- </div>
741
- <div class="stop-tunnel-container">
742
- <button class="btn btn-danger" @click="stopTunnel()">
743
- <span class="icon material-symbols-outlined">stop_circle</span> Stop Tunnel
744
- </button>
745
- </div>
746
- </div>
747
- <!-- Generate tunnel button when no link exists -->
748
- <div class="tunnel-actions" x-show="!linkGenerated">
749
- <button class="btn btn-ok" @click="generateLink()">
750
- <span class="icon material-symbols-outlined">play_circle</span> Create Tunnel
751
- </button>
752
- </div>
753
- </div>
754
- </div>
755
  </div>
756
  </div>
757
 
 
17
  <link rel="stylesheet" href="css/speech.css">
18
  <link rel="stylesheet" href="css/history.css">
19
  <link rel="stylesheet" href="css/scheduler-datepicker.css">
 
20
  <link rel="stylesheet" href="css/notification.css">
21
 
22
  <!-- Flatpickr for datetime picker -->
 
688
 
689
  <!-- Tunnel section content - only visible in External Services tab -->
690
  <div id="section-tunnel" class="section" x-show="activeTab === 'external'">
691
+ <x-component path="settings/tunnel/tunnel-section.html" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
692
  </div>
693
  </div>
694
 
webui/js/settings.js CHANGED
@@ -67,19 +67,6 @@ const settingsModalProxy = {
67
  }
68
  }
69
  }
70
-
71
- // When switching to the tunnel tab, initialize tunnelSettings
72
- if (tabName === 'tunnel') {
73
- console.log('Switching to tunnel tab, initializing tunnelSettings');
74
- const tunnelElement = document.querySelector('[x-data="tunnelSettings"]');
75
- if (tunnelElement) {
76
- const tunnelData = Alpine.$data(tunnelElement);
77
- if (tunnelData && typeof tunnelData.checkTunnelStatus === 'function') {
78
- // Check tunnel status
79
- tunnelData.checkTunnelStatus();
80
- }
81
- }
82
- }
83
  }, 10);
84
  },
85
 
 
67
  }
68
  }
69
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  }, 10);
71
  },
72
 
webui/js/tunnel.js DELETED
@@ -1,386 +0,0 @@
1
-
2
- // Tunnel settings for the Settings modal
3
- document.addEventListener('alpine:init', () => {
4
- Alpine.data('tunnelSettings', () => ({
5
- isLoading: false,
6
- tunnelLink: '',
7
- linkGenerated: false,
8
- loadingText: '',
9
- qrCodeInstance: null,
10
-
11
- init() {
12
- this.checkTunnelStatus();
13
- },
14
-
15
- generateQRCode() {
16
- if (!this.tunnelLink) return;
17
-
18
- const qrContainer = document.getElementById('qrcode-tunnel');
19
- if (!qrContainer) return;
20
-
21
- // Clear any existing QR code
22
- qrContainer.innerHTML = '';
23
-
24
- try {
25
- // Generate new QR code
26
- this.qrCodeInstance = new QRCode(qrContainer, {
27
- text: this.tunnelLink,
28
- width: 128,
29
- height: 128,
30
- colorDark: "#000000",
31
- colorLight: "#ffffff",
32
- correctLevel: QRCode.CorrectLevel.M
33
- });
34
- } catch (error) {
35
- console.error('Error generating QR code:', error);
36
- qrContainer.innerHTML = '<div class="qr-error">QR code generation failed</div>';
37
- }
38
- },
39
-
40
- async checkTunnelStatus() {
41
- try {
42
- const response = await fetchApi('/tunnel_proxy', {
43
- method: 'POST',
44
- headers: {
45
- 'Content-Type': 'application/json',
46
- },
47
- body: JSON.stringify({ action: 'get' }),
48
- });
49
-
50
- const data = await response.json();
51
-
52
- if (data.success && data.tunnel_url) {
53
- // Update the stored URL if it's different from what we have
54
- if (this.tunnelLink !== data.tunnel_url) {
55
- this.tunnelLink = data.tunnel_url;
56
- localStorage.setItem('agent_zero_tunnel_url', data.tunnel_url);
57
- }
58
- this.linkGenerated = true;
59
- // Generate QR code for the tunnel URL
60
- this.$nextTick(() => this.generateQRCode());
61
- } else {
62
- // Check if we have a stored tunnel URL
63
- const storedTunnelUrl = localStorage.getItem('agent_zero_tunnel_url');
64
-
65
- if (storedTunnelUrl) {
66
- // Use the stored URL but verify it's still valid
67
- const verifyResponse = await fetchApi('/tunnel_proxy', {
68
- method: 'POST',
69
- headers: {
70
- 'Content-Type': 'application/json',
71
- },
72
- body: JSON.stringify({ action: 'verify', url: storedTunnelUrl }),
73
- });
74
-
75
- const verifyData = await verifyResponse.json();
76
-
77
- if (verifyData.success && verifyData.is_valid) {
78
- this.tunnelLink = storedTunnelUrl;
79
- this.linkGenerated = true;
80
- // Generate QR code for the tunnel URL
81
- this.$nextTick(() => this.generateQRCode());
82
- } else {
83
- // Clear stale URL
84
- localStorage.removeItem('agent_zero_tunnel_url');
85
- this.tunnelLink = '';
86
- this.linkGenerated = false;
87
- }
88
- } else {
89
- // No stored URL, show the generate button
90
- this.tunnelLink = '';
91
- this.linkGenerated = false;
92
- }
93
- }
94
- } catch (error) {
95
- console.error('Error checking tunnel status:', error);
96
- this.tunnelLink = '';
97
- this.linkGenerated = false;
98
- }
99
- },
100
-
101
- async refreshLink() {
102
- // Call generate but with a confirmation first
103
- if (confirm("Are you sure you want to generate a new tunnel URL? The old URL will no longer work.")) {
104
- this.isLoading = true;
105
- this.loadingText = 'Refreshing tunnel...';
106
-
107
- // Change refresh button appearance
108
- const refreshButton = document.querySelector('.refresh-link-button');
109
- const originalContent = refreshButton.innerHTML;
110
- refreshButton.innerHTML = '<span class="icon material-symbols-outlined spin">progress_activity</span> Refreshing...';
111
- refreshButton.disabled = true;
112
- refreshButton.classList.add('refreshing');
113
-
114
- try {
115
- // First stop any existing tunnel
116
- const stopResponse = await fetchApi('/tunnel_proxy', {
117
- method: 'POST',
118
- headers: {
119
- 'Content-Type': 'application/json',
120
- },
121
- body: JSON.stringify({ action: 'stop' }),
122
- });
123
-
124
- // Check if stopping was successful
125
- const stopData = await stopResponse.json();
126
- if (!stopData.success) {
127
- console.warn("Warning: Couldn't stop existing tunnel cleanly");
128
- // Continue anyway since we want to create a new one
129
- }
130
-
131
- // Then generate a new one
132
- await this.generateLink();
133
- } catch (error) {
134
- console.error("Error refreshing tunnel:", error);
135
- window.toastFrontendError("Error refreshing tunnel", "Tunnel Error");
136
- this.isLoading = false;
137
- this.loadingText = '';
138
- } finally {
139
- // Reset refresh button
140
- refreshButton.innerHTML = originalContent;
141
- refreshButton.disabled = false;
142
- refreshButton.classList.remove('refreshing');
143
- }
144
- }
145
- },
146
-
147
- async generateLink() {
148
- // First check if authentication is enabled
149
- try {
150
- const authCheckResponse = await fetchApi('/settings_get');
151
- const authData = await authCheckResponse.json();
152
-
153
- // Find the auth_login and auth_password in the settings
154
- let hasAuth = false;
155
-
156
- if (authData && authData.settings && authData.settings.sections) {
157
- for (const section of authData.settings.sections) {
158
- if (section.fields) {
159
- const authLoginField = section.fields.find(field => field.id === 'auth_login');
160
- const authPasswordField = section.fields.find(field => field.id === 'auth_password');
161
-
162
- if (authLoginField && authPasswordField &&
163
- authLoginField.value && authPasswordField.value) {
164
- hasAuth = true;
165
- break;
166
- }
167
- }
168
- }
169
- }
170
-
171
- // If no authentication is set, warn the user
172
- if (!hasAuth) {
173
- const proceed = confirm(
174
- "WARNING: No authentication is configured for your Agent Zero instance.\n\n" +
175
- "Creating a public tunnel without authentication means anyone with the URL " +
176
- "can access your Agent Zero instance.\n\n" +
177
- "It is recommended to set up authentication in the Settings > Authentication section " +
178
- "before creating a public tunnel.\n\n" +
179
- "Do you want to proceed anyway?"
180
- );
181
-
182
- if (!proceed) {
183
- return; // User cancelled
184
- }
185
- }
186
- } catch (error) {
187
- console.error("Error checking authentication status:", error);
188
- // Continue anyway if we can't check auth status
189
- }
190
-
191
- this.isLoading = true;
192
- this.loadingText = 'Creating tunnel...';
193
-
194
- // Get provider from the parent settings modal scope
195
- const modalEl = document.getElementById('settingsModal');
196
- const modalAD = Alpine.$data(modalEl);
197
- const provider = modalAD.provider || 'cloudflared'; // Default to cloudflared if not set
198
-
199
- // Change create button appearance
200
- const createButton = document.querySelector('.tunnel-actions .btn-ok');
201
- if (createButton) {
202
- createButton.innerHTML = '<span class="icon material-symbols-outlined spin">progress_activity</span> Creating...';
203
- createButton.disabled = true;
204
- createButton.classList.add('creating');
205
- }
206
-
207
- try {
208
- // Call the backend API to create a tunnel
209
- const response = await fetchApi('/tunnel_proxy', {
210
- method: 'POST',
211
- headers: {
212
- 'Content-Type': 'application/json',
213
- },
214
- body: JSON.stringify({
215
- action: 'create',
216
- provider: provider
217
- // port: window.location.port || (window.location.protocol === 'https:' ? 443 : 80)
218
- }),
219
- });
220
-
221
- const data = await response.json();
222
-
223
- if (data.success && data.tunnel_url) {
224
- // Store the tunnel URL in localStorage for persistence
225
- localStorage.setItem('agent_zero_tunnel_url', data.tunnel_url);
226
-
227
- this.tunnelLink = data.tunnel_url;
228
- this.linkGenerated = true;
229
-
230
- // Generate QR code for the tunnel URL
231
- this.$nextTick(() => this.generateQRCode());
232
-
233
- // Show success message to confirm creation
234
- window.toastFrontendInfo("Tunnel created successfully", "Tunnel Status");
235
- } else {
236
- // The tunnel might still be starting up, check again after a delay
237
- this.loadingText = 'Tunnel creation taking longer than expected...';
238
-
239
- // Wait for 5 seconds and check if the tunnel is running
240
- await new Promise(resolve => setTimeout(resolve, 5000));
241
-
242
- // Check if tunnel is running now
243
- try {
244
- const statusResponse = await fetchApi('/tunnel_proxy', {
245
- method: 'POST',
246
- headers: {
247
- 'Content-Type': 'application/json',
248
- },
249
- body: JSON.stringify({ action: 'get' }),
250
- });
251
-
252
- const statusData = await statusResponse.json();
253
-
254
- if (statusData.success && statusData.tunnel_url) {
255
- // Tunnel is now running, we can update the UI
256
- localStorage.setItem('agent_zero_tunnel_url', statusData.tunnel_url);
257
- this.tunnelLink = statusData.tunnel_url;
258
- this.linkGenerated = true;
259
-
260
- // Generate QR code for the tunnel URL
261
- this.$nextTick(() => this.generateQRCode());
262
-
263
- window.toastFrontendInfo("Tunnel created successfully", "Tunnel Status");
264
- return;
265
- }
266
- } catch (statusError) {
267
- console.error("Error checking tunnel status:", statusError);
268
- }
269
-
270
- // If we get here, the tunnel really failed to start
271
- const errorMessage = data.message || "Failed to create tunnel. Please try again.";
272
- window.toastFrontendError(errorMessage, "Tunnel Error");
273
- console.error("Tunnel creation failed:", data);
274
- }
275
- } catch (error) {
276
- window.toastFrontendError("Error creating tunnel", "Tunnel Error");
277
- console.error("Error creating tunnel:", error);
278
- } finally {
279
- this.isLoading = false;
280
- this.loadingText = '';
281
-
282
- // Reset create button if it's still in the DOM
283
- const createButton = document.querySelector('.tunnel-actions .btn-ok');
284
- if (createButton) {
285
- createButton.innerHTML = '<span class="icon material-symbols-outlined">play_circle</span> Create Tunnel';
286
- createButton.disabled = false;
287
- createButton.classList.remove('creating');
288
- }
289
- }
290
- },
291
-
292
- async stopTunnel() {
293
- if (confirm("Are you sure you want to stop the tunnel? The URL will no longer be accessible.")) {
294
- this.isLoading = true;
295
- this.loadingText = 'Stopping tunnel...';
296
-
297
-
298
- try {
299
- // Call the backend to stop the tunnel
300
- const response = await fetchApi('/tunnel_proxy', {
301
- method: 'POST',
302
- headers: {
303
- 'Content-Type': 'application/json',
304
- },
305
- body: JSON.stringify({ action: 'stop' }),
306
- });
307
-
308
- const data = await response.json();
309
-
310
- if (data.success) {
311
- // Clear the stored URL
312
- localStorage.removeItem('agent_zero_tunnel_url');
313
-
314
- // Clear QR code
315
- const qrContainer = document.getElementById('qrcode-tunnel');
316
- if (qrContainer) {
317
- qrContainer.innerHTML = '';
318
- }
319
- this.qrCodeInstance = null;
320
-
321
- // Update UI state
322
- this.tunnelLink = '';
323
- this.linkGenerated = false;
324
-
325
- window.toastFrontendInfo("Tunnel stopped successfully", "Tunnel Status");
326
- } else {
327
- window.toastFrontendError("Failed to stop tunnel", "Tunnel Error");
328
-
329
- // Reset stop button
330
- stopButton.innerHTML = originalStopContent;
331
- stopButton.disabled = false;
332
- stopButton.classList.remove('stopping');
333
- }
334
- } catch (error) {
335
- window.toastFrontendError("Error stopping tunnel", "Tunnel Error");
336
- console.error("Error stopping tunnel:", error);
337
-
338
- // Reset stop button
339
- stopButton.innerHTML = originalStopContent;
340
- stopButton.disabled = false;
341
- stopButton.classList.remove('stopping');
342
- } finally {
343
- this.isLoading = false;
344
- this.loadingText = '';
345
- }
346
- }
347
- },
348
-
349
- copyToClipboard() {
350
- if (!this.tunnelLink) return;
351
-
352
- const copyButton = document.querySelector('.copy-link-button');
353
- const originalContent = copyButton.innerHTML;
354
-
355
- navigator.clipboard.writeText(this.tunnelLink)
356
- .then(() => {
357
- // Update button to show success state
358
- copyButton.innerHTML = '<span class="icon material-symbols-outlined">check</span> Copied!';
359
- copyButton.classList.add('copy-success');
360
-
361
- // Show toast notification
362
- window.toastFrontendInfo("Tunnel URL copied to clipboard!", "Clipboard");
363
-
364
- // Reset button after 2 seconds
365
- setTimeout(() => {
366
- copyButton.innerHTML = originalContent;
367
- copyButton.classList.remove('copy-success');
368
- }, 2000);
369
- })
370
- .catch(err => {
371
- console.error('Failed to copy URL: ', err);
372
- window.toastFrontendError("Failed to copy tunnel URL", "Clipboard Error");
373
-
374
- // Show error state
375
- copyButton.innerHTML = '<span class="icon material-symbols-outlined">close</span> Failed';
376
- copyButton.classList.add('copy-error');
377
-
378
- // Reset button after 2 seconds
379
- setTimeout(() => {
380
- copyButton.innerHTML = originalContent;
381
- copyButton.classList.remove('copy-error');
382
- }, 2000);
383
- });
384
- }
385
- }));
386
- });