launcher-ui.html (25024B)
1 <!doctype html> 2 <html> 3 <head> 4 <meta charset="utf-8" /> 5 <meta name="viewport" content="width=device-width, initial-scale=1" /> 6 <title>Bzl Launcher</title> 7 <style> 8 body { 9 font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial; 10 margin: 0; 11 background: #0b0b10; 12 color: #eee; 13 } 14 .wrap { 15 max-width: 1000px; 16 margin: 0 auto; 17 padding: 18px; 18 } 19 .card { 20 background: #141420; 21 border: 1px solid rgba(255, 255, 255, 0.1); 22 border-radius: 14px; 23 padding: 14px; 24 margin: 12px 0; 25 } 26 .row { 27 display: flex; 28 gap: 12px; 29 flex-wrap: wrap; 30 align-items: center; 31 } 32 .btn { 33 border-radius: 999px; 34 padding: 10px 14px; 35 border: 1px solid rgba(255, 255, 255, 0.18); 36 background: rgba(255, 255, 255, 0.06); 37 color: #eee; 38 cursor: pointer; 39 font-weight: 700; 40 } 41 .btn.primary { 42 background: linear-gradient(180deg, rgba(255, 140, 0, 0.95), rgba(255, 80, 160, 0.95)); 43 color: #140812; 44 border: 0; 45 } 46 .btn.danger { 47 background: rgba(255, 80, 120, 0.18); 48 border-color: rgba(255, 80, 120, 0.35); 49 } 50 label { 51 font-size: 12px; 52 opacity: 0.85; 53 } 54 input { 55 background: rgba(255, 255, 255, 0.08); 56 color: #eee; 57 border: 1px solid rgba(255, 255, 255, 0.14); 58 border-radius: 10px; 59 padding: 10px 12px; 60 min-width: 160px; 61 } 62 .mono { 63 font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; 64 font-size: 12px; 65 } 66 pre { 67 background: #0f0f18; 68 border: 1px solid rgba(255, 255, 255, 0.1); 69 border-radius: 12px; 70 padding: 12px; 71 max-height: 340px; 72 overflow: auto; 73 white-space: pre-wrap; 74 } 75 .pill { 76 display: inline-block; 77 padding: 2px 10px; 78 border-radius: 999px; 79 border: 1px solid rgba(255, 255, 255, 0.14); 80 background: rgba(255, 255, 255, 0.06); 81 font-size: 12px; 82 } 83 .hint { 84 opacity: 0.75; 85 font-size: 12px; 86 margin-top: 6px; 87 } 88 </style> 89 </head> 90 <body> 91 <div class="wrap"> 92 <h2 style="margin: 6px 0 0 0">Bzl Launcher</h2> 93 <div style="opacity: 0.8; margin-top: 6px">Local control panel for starting/stopping Bzl and an optional Cloudflare tunnel.</div> 94 95 <div class="card"> 96 <div class="row" style="justify-content: space-between"> 97 <div> 98 <div><b>Status</b> <span id="statusPill" class="pill">Checking…</span></div> 99 <div class="mono" style="opacity: 0.85; margin-top: 6px" id="statusText"></div> 100 </div> 101 <div class="row"> 102 <button class="btn primary" id="btnStart">Start Bzl</button> 103 <button class="btn danger" id="btnStop">Stop Bzl</button> 104 <button class="btn" id="btnOpen">Open site</button> 105 </div> 106 </div> 107 </div> 108 109 <div class="card"> 110 <div class="row" style="justify-content: space-between"> 111 <div> 112 <div><b>Settings</b></div> 113 <div class="hint">Writes to <span class="mono">.env</span>. Bzl will read it on startup.</div> 114 </div> 115 <div class="row"> 116 <button class="btn" id="btnSave">Save</button> 117 <button class="btn" id="btnReload">Reload</button> 118 </div> 119 </div> 120 <div class="row" style="margin-top: 12px"> 121 <div> 122 <label>PORT</label><br /> 123 <input id="inpPort" value="__PORT__" /> 124 </div> 125 <div> 126 <label>HOST</label><br /> 127 <input id="inpHost" value="__HOST__" /> 128 </div> 129 <div> 130 <label>REGISTRATION_CODE</label><br /> 131 <input id="inpReg" value="__REGISTRATION_CODE__" placeholder="(optional)" /> 132 </div> 133 <div> 134 <label>CLOUDFLARED_TUNNEL (named)</label><br /> 135 <input id="inpTunnel" value="__CLOUDFLARED_TUNNEL__" placeholder="(optional)" /> 136 </div> 137 <div> 138 <label>CLOUDFLARED_CONFIG (optional)</label><br /> 139 <input id="inpCfg" value="__CLOUDFLARED_CONFIG__" placeholder="%USERPROFILE%\\.cloudflared\\config.yml" /> 140 </div> 141 </div> 142 </div> 143 144 <div class="card"> 145 <div class="row" style="justify-content: space-between"> 146 <div> 147 <div><b>Updates</b> <span id="updPill" class="pill">Checking...</span></div> 148 <div class="hint" id="updText">Checks GitHub releases for updates and preserves your <span class="mono">data/</span> and <span class="mono">.env</span>.</div> 149 <div class="row" style="margin-top: 10px"> 150 <label style="display: flex; gap: 8px; align-items: center"> 151 <input id="chkUpdEnable" type="checkbox" /> 152 Enable updates (opt-in) 153 </label> 154 <select id="updSelect" style="flex: 1; min-width: 240px"></select> 155 <button class="btn" id="btnUpdNotes">Release notes</button> 156 </div> 157 </div> 158 <div class="row"> 159 <button class="btn" id="btnUpdCheck">Check</button> 160 <button class="btn primary" id="btnUpdApply">Apply update</button> 161 </div> 162 </div> 163 </div> 164 165 <div class="card"> 166 <div class="row" style="justify-content: space-between"> 167 <div> 168 <div><b>Cloudflare tunnel (optional)</b></div> 169 <div class="hint">Quick tunnel prints a public URL in the log. Named tunnel requires <span class="mono">CLOUDFLARED_TUNNEL</span>.</div> 170 </div> 171 <div class="row"> 172 <button class="btn" id="btnTunnelQuick">Start quick tunnel</button> 173 <button class="btn" id="btnTunnelNamed">Start named tunnel</button> 174 <button class="btn danger" id="btnTunnelStop">Stop tunnel</button> 175 </div> 176 </div> 177 <div class="hint" style="margin-top: 8px"> 178 Share link: <span class="mono" id="publicUrl">(waiting for tunnel...)</span> 179 <button class="btn" id="btnCopyPublic" style="padding: 6px 10px; margin-left: 8px">Copy</button> 180 </div> 181 </div> 182 183 <div class="card"> 184 <div class="row" style="justify-content: space-between"> 185 <div> 186 <div><b>Named tunnel setup (choose your own domain)</b> <span id="cfPill" class="pill">Checking...</span></div> 187 <div class="hint">Runs <span class="mono">cloudflared</span> setup steps for you. You still need a domain on Cloudflare.</div> 188 <div class="hint" style="margin-top: 8px"> 189 Login link: <span class="mono" id="cfLoginUrl">(click Login)</span> 190 <button class="btn" id="btnCopyLogin" style="padding: 6px 10px; margin-left: 8px">Copy</button> 191 </div> 192 </div> 193 <div class="row"> 194 <button class="btn" id="btnCfRefresh">Refresh</button> 195 <button class="btn danger" id="btnCfCancel">Cancel</button> 196 </div> 197 </div> 198 199 <div class="row" style="margin-top: 12px"> 200 <div> 201 <label>Tunnel name</label><br /> 202 <input id="inpCfName" list="cfTunnelNames" placeholder="bzl" /> 203 <datalist id="cfTunnelNames"></datalist> 204 </div> 205 <div style="flex: 1; min-width: 260px"> 206 <label>Hostname</label><br /> 207 <input id="inpCfHost" placeholder="bzl.example.com" style="width: 100%" /> 208 </div> 209 <div style="flex: 1; min-width: 260px"> 210 <label>Credentials file</label><br /> 211 <input id="inpCfCreds" list="cfCredsList" placeholder="Pick from list..." style="width: 100%" /> 212 <datalist id="cfCredsList"></datalist> 213 <div class="hint" id="cfCredsHint"></div> 214 </div> 215 <div style="flex: 1; min-width: 260px"> 216 <label>Config path</label><br /> 217 <input id="inpCfCfg" list="cfCfgList" placeholder="Pick from list..." style="width: 100%" /> 218 <datalist id="cfCfgList"></datalist> 219 </div> 220 </div> 221 222 <div class="row" style="margin-top: 12px"> 223 <button class="btn primary" id="btnCfLogin">1) Login</button> 224 <button class="btn" id="btnCfCreate">2) Create tunnel</button> 225 <button class="btn" id="btnCfRoute">3) Route DNS</button> 226 <button class="btn" id="btnCfWriteCfg">4) Write config.yml</button> 227 <div class="hint" id="cfStatusText" style="margin-left: 6px"></div> 228 </div> 229 </div> 230 231 <div class="card"> 232 <div class="row" style="justify-content: space-between"> 233 <div><b>Logs</b> <span class="pill mono">live</span></div> 234 <button class="btn" id="btnClear">Clear</button> 235 </div> 236 <pre id="log" class="mono"></pre> 237 </div> 238 </div> 239 240 <script> 241 const API_TOKEN = "__API_TOKEN__"; 242 const logEl = document.getElementById("log"); 243 const statusPill = document.getElementById("statusPill"); 244 const statusText = document.getElementById("statusText"); 245 const cfPill = document.getElementById("cfPill"); 246 const cfStatusText = document.getElementById("cfStatusText"); 247 const inpPort = document.getElementById("inpPort"); 248 const inpHost = document.getElementById("inpHost"); 249 const inpReg = document.getElementById("inpReg"); 250 const inpTunnel = document.getElementById("inpTunnel"); 251 const inpCfg = document.getElementById("inpCfg"); 252 const inpCfName = document.getElementById("inpCfName"); 253 const inpCfHost = document.getElementById("inpCfHost"); 254 const inpCfCreds = document.getElementById("inpCfCreds"); 255 const inpCfCfg = document.getElementById("inpCfCfg"); 256 const cfCredsHint = document.getElementById("cfCredsHint"); 257 const publicUrlEl = document.getElementById("publicUrl"); 258 const updPill = document.getElementById("updPill"); 259 const updText = document.getElementById("updText"); 260 const cfLoginUrlEl = document.getElementById("cfLoginUrl"); 261 let cfTunnels = []; 262 let cfCredsById = new Map(); 263 const updEnabledKey = "bzlLauncherUpdatesEnabled"; 264 const updSelect = document.getElementById("updSelect"); 265 const updNotes = document.getElementById("btnUpdNotes"); 266 const updEnable = document.getElementById("chkUpdEnable"); 267 let updReleases = []; 268 let updSelected = null; 269 270 function append(line) { 271 logEl.textContent += line + "\n"; 272 logEl.scrollTop = logEl.scrollHeight; 273 } 274 275 function setPublicUrl(url) { 276 if (!publicUrlEl) return; 277 const u = String(url || "").trim(); 278 publicUrlEl.textContent = u || "(not available yet)"; 279 publicUrlEl.dataset.url = u; 280 } 281 282 async function copyText(text) { 283 const t = String(text || ""); 284 if (!t) return false; 285 try { 286 await navigator.clipboard.writeText(t); 287 return true; 288 } catch { 289 try { 290 const ta = document.createElement("textarea"); 291 ta.value = t; 292 ta.style.position = "fixed"; 293 ta.style.left = "-9999px"; 294 document.body.appendChild(ta); 295 ta.focus(); 296 ta.select(); 297 const ok = document.execCommand("copy"); 298 document.body.removeChild(ta); 299 return ok; 300 } catch { 301 return false; 302 } 303 } 304 } 305 306 async function api(path, body) { 307 const res = await fetch( 308 path, 309 body 310 ? { 311 method: "POST", 312 headers: { "Content-Type": "application/json", "X-Bzl-Launcher": API_TOKEN }, 313 body: JSON.stringify(body) 314 } 315 : {} 316 ); 317 return await res.json(); 318 } 319 320 async function refresh() { 321 const st = await api("/api/status"); 322 statusPill.textContent = st.healthy ? "Running" : st.serverRunning ? "Starting..." : "Stopped"; 323 const v = st.version ? ` v${st.version}` : ""; 324 statusText.textContent = st.localUrl + v + (st.healthy ? " (healthy)" : ""); 325 if (st.namedPublicUrl) setPublicUrl(st.namedPublicUrl); 326 } 327 328 async function refreshUpdate() { 329 if (!updPill || !updText) return; 330 const enabled = !!(updEnable && updEnable.checked); 331 if (!enabled) { 332 updPill.textContent = "Updates disabled"; 333 updText.textContent = "Enable updates to check GitHub releases and choose a version."; 334 if (updSelect) updSelect.innerHTML = ""; 335 updSelected = null; 336 if (updNotes) updNotes.disabled = true; 337 return; 338 } 339 try { 340 const st = await api("/api/update/releases"); 341 if (!st.ok) { 342 updPill.textContent = "Update check failed"; 343 updText.textContent = st.error || "Failed to check updates."; 344 return; 345 } 346 updReleases = Array.isArray(st.releases) ? st.releases : []; 347 const current = st.currentVersion ? `v${st.currentVersion}` : "(unknown)"; 348 const latest = updReleases[0]?.version ? `v${updReleases[0].version}` : "(unknown)"; 349 350 updPill.textContent = "Ready"; 351 updText.textContent = `Current ${current}. Select a version to view notes or apply. Latest: ${latest}.`; 352 353 if (updSelect) { 354 const opts = updReleases 355 .filter((r) => r && r.tag && r.asset && r.asset.url) 356 .map((r) => ({ 357 tag: r.tag, 358 label: `${r.version ? "v" + r.version : r.tag}${r.publishedAt ? " · " + r.publishedAt.slice(0, 10) : ""}${ 359 r.prerelease ? " · prerelease" : "" 360 }` 361 })); 362 updSelect.innerHTML = opts.map((o) => `<option value="${o.tag}">${o.label}</option>`).join(""); 363 const want = updSelected?.tag || opts[0]?.tag || ""; 364 if (want) updSelect.value = want; 365 updSelected = updReleases.find((r) => r.tag === updSelect.value) || null; 366 } 367 if (updNotes) updNotes.disabled = !(updSelected && updSelected.htmlUrl); 368 } catch (e) { 369 updPill.textContent = "Update check failed"; 370 updText.textContent = e?.message || String(e); 371 } 372 } 373 374 async function refreshCloudflared() { 375 const st = await api("/api/cloudflared/status"); 376 if (!st.exists) { 377 cfPill.textContent = "Missing cloudflared"; 378 cfStatusText.textContent = "Install cloudflared (Windows: winget install Cloudflare.cloudflared)."; 379 return; 380 } 381 cfPill.textContent = st.certExists ? "Logged in" : "Not logged in"; 382 const bits = []; 383 if (st.version) bits.push(st.version); 384 bits.push(st.certExists ? "cert ok" : "run Login"); 385 if (!st.configExists) { 386 bits.push("no config.yml"); 387 } else if (st.configTunnel && st.envTunnel && st.configTunnel !== st.envTunnel) { 388 bits.push("config tunnel mismatch"); 389 } else if (st.expectedCredentialsFile && st.configCredentialsFile && st.expectedCredentialsFile !== st.configCredentialsFile) { 390 bits.push("creds mismatch"); 391 } else { 392 bits.push("config ok"); 393 } 394 if (st.setupRunning) bits.push("running..."); 395 cfStatusText.textContent = bits.join(" \u00b7 "); 396 397 if (cfLoginUrlEl) { 398 const u = String(st.lastLoginUrl || "").trim(); 399 cfLoginUrlEl.textContent = u || "(click Login)"; 400 cfLoginUrlEl.dataset.url = u; 401 } 402 403 if (!inpCfCfg.value) inpCfCfg.value = st.configPath || st.defaultConfigPath || ""; 404 if (!inpCfName.value) inpCfName.value = "bzl"; 405 if (st.lastCreate) { 406 if (st.lastCreate.credentialsFile && !inpCfCreds.value) inpCfCreds.value = st.lastCreate.credentialsFile; 407 if (st.lastCreate.tunnelId && !inpTunnel.value) inpTunnel.value = st.lastCreate.tunnelId; 408 } 409 if (st.expectedCredentialsFile && !inpCfCreds.value) inpCfCreds.value = st.expectedCredentialsFile; 410 } 411 412 function setDatalistOptions(id, values) { 413 const el = document.getElementById(id); 414 if (!el) return; 415 const uniq = Array.from(new Set((values || []).filter(Boolean))); 416 el.innerHTML = uniq.map((v) => `<option value="${String(v).replaceAll('"', """)}"></option>`).join(""); 417 } 418 419 async function refreshCloudflaredFiles() { 420 try { 421 const f = await api("/api/cloudflared/files"); 422 if (!f.ok) return; 423 setDatalistOptions("cfCfgList", [f.dir ? f.dir + "\\\\config.yml" : "", ...(f.configCandidates || [])].filter(Boolean)); 424 setDatalistOptions( 425 "cfCredsList", 426 (f.credentials || []).map((c) => c.path) 427 ); 428 cfCredsById = new Map((f.credentials || []).filter((c) => c.tunnelId).map((c) => [c.tunnelId, c.path])); 429 if (!inpCfCfg.value && f.configCandidates && f.configCandidates[0]) inpCfCfg.value = f.configCandidates[0]; 430 } catch {} 431 } 432 433 async function refreshCloudflaredTunnels() { 434 try { 435 const r = await api("/api/cloudflared/tunnels"); 436 if (!r.ok) return; 437 cfTunnels = r.tunnels || []; 438 setDatalistOptions( 439 "cfTunnelNames", 440 cfTunnels.map((t) => t.name) 441 ); 442 } catch {} 443 } 444 445 document.getElementById("btnStart").onclick = async () => api("/api/start", { supervised: true }); 446 document.getElementById("btnStop").onclick = async () => api("/api/stop", {}); 447 document.getElementById("btnOpen").onclick = async () => api("/api/open", {}); 448 document.getElementById("btnTunnelQuick").onclick = async () => api("/api/tunnel/start", { mode: "quick" }); 449 document.getElementById("btnTunnelNamed").onclick = async () => api("/api/tunnel/start", { mode: "named" }); 450 document.getElementById("btnTunnelStop").onclick = async () => api("/api/tunnel/stop", {}); 451 if (updEnable) { 452 updEnable.checked = localStorage.getItem(updEnabledKey) === "1"; 453 updEnable.onchange = () => { 454 localStorage.setItem(updEnabledKey, updEnable.checked ? "1" : "0"); 455 refreshUpdate(); 456 }; 457 } 458 document.getElementById("btnUpdCheck").onclick = async () => refreshUpdate(); 459 if (updSelect) { 460 updSelect.onchange = () => { 461 updSelected = updReleases.find((r) => r.tag === updSelect.value) || null; 462 if (updNotes) updNotes.disabled = !(updSelected && updSelected.htmlUrl); 463 }; 464 } 465 if (updNotes) { 466 updNotes.onclick = () => { 467 const url = updSelected?.htmlUrl || ""; 468 if (url) window.open(url, "_blank", "noopener,noreferrer"); 469 }; 470 } 471 document.getElementById("btnUpdApply").onclick = async () => { 472 if (!updEnable || !updEnable.checked) { 473 append("update: enable updates first"); 474 return; 475 } 476 const tag = updSelected?.tag || (updSelect ? updSelect.value : ""); 477 if (!tag) { 478 append("update: pick a version first"); 479 return; 480 } 481 append(`update: applying ${tag} (launcher will close)...`); 482 const r = await api("/api/update/apply", { tag }); 483 append(r.ok ? "update: applying now (re-open launcher after it exits)" : `update: failed (${r.error || "?"})`); 484 }; 485 document.getElementById("btnCopyPublic").onclick = async () => { 486 const u = publicUrlEl?.dataset?.url || ""; 487 const ok = await copyText(u); 488 append(ok ? `copied: ${u}` : "copy failed"); 489 }; 490 { 491 const btn = document.getElementById("btnCopyLogin"); 492 if (btn) { 493 btn.onclick = async () => { 494 const u = cfLoginUrlEl?.dataset?.url || ""; 495 const ok = await copyText(u); 496 append(ok ? `copied: ${u}` : "copy failed"); 497 }; 498 } 499 } 500 document.getElementById("btnCfRefresh").onclick = async () => { 501 await refreshCloudflared(); 502 await refreshCloudflaredFiles(); 503 await refreshCloudflaredTunnels(); 504 }; 505 document.getElementById("btnCfCancel").onclick = async () => api("/api/cloudflared/cancel", {}); 506 document.getElementById("btnCfLogin").onclick = async () => { 507 append("cloudflared: login starting (browser will open)"); 508 await api("/api/cloudflared/login", {}); 509 refreshCloudflared(); 510 }; 511 document.getElementById("btnCfCreate").onclick = async () => { 512 const name = inpCfName.value || "bzl"; 513 const r = await api("/api/cloudflared/create", { name }); 514 if (r.tunnelId) inpTunnel.value = r.tunnelId; 515 if (r.credentialsFile) inpCfCreds.value = r.credentialsFile; 516 if (inpCfCfg.value) inpCfg.value = inpCfCfg.value; 517 append( 518 r.ok 519 ? `${r.reused ? "cloudflared: using existing tunnel" : "cloudflared: created tunnel"} (${r.tunnelId || name})` 520 : `cloudflared: create failed (${r.error || r.code || "?"})` 521 ); 522 refreshCloudflared(); 523 }; 524 document.getElementById("btnCfRoute").onclick = async () => { 525 const tunnel = inpTunnel.value || inpCfName.value; 526 const hostname = inpCfHost.value; 527 const r = await api("/api/cloudflared/route-dns", { tunnel, hostname, force: true }); 528 append(r.ok ? `cloudflared: routed ${hostname}` : `cloudflared: route failed (${r.error || r.code || "?"})`); 529 }; 530 document.getElementById("btnCfWriteCfg").onclick = async () => { 531 const tunnelId = inpTunnel.value; 532 const hostname = inpCfHost.value; 533 const configPath = inpCfCfg.value; 534 const credentialsFile = inpCfCreds.value; 535 const port = inpPort.value; 536 const r = await api("/api/cloudflared/write-config", { tunnelId, hostname, configPath, credentialsFile, port }); 537 append(r.ok ? `cloudflared: wrote config (${r.configPath})` : `cloudflared: write config failed (${r.error || "?"})`); 538 if (r.ok) inpCfg.value = r.configPath; 539 refreshCloudflared(); 540 }; 541 542 function uuidFromText(s) { 543 const m = String(s || "").match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i); 544 return m ? m[0] : ""; 545 } 546 547 inpCfCreds.onchange = () => { 548 const id = uuidFromText(inpCfCreds.value); 549 if (id) { 550 inpTunnel.value = id; 551 append(`cloudflared: selected creds -> tunnel ${id}`); 552 } 553 if (cfCredsHint) { 554 const t = (cfTunnels || []).find((x) => x.id === id); 555 cfCredsHint.textContent = id ? `Detected: ${t?.name ? t.name + " " : ""}(${id})` : ""; 556 } 557 refreshCloudflared(); 558 }; 559 inpCfName.onchange = () => { 560 const name = String(inpCfName.value || "").trim(); 561 const t = (cfTunnels || []).find((x) => x.name === name); 562 if (t?.id) { 563 inpTunnel.value = t.id; 564 if (!inpCfCreds.value) { 565 const p = cfCredsById.get(t.id); 566 if (p) inpCfCreds.value = p; 567 } 568 append(`cloudflared: selected tunnel ${name} -> ${t.id}`); 569 } 570 if (cfCredsHint) { 571 cfCredsHint.textContent = t?.id ? `Selected tunnel: ${name} (${t.id})` : ""; 572 } 573 }; 574 document.getElementById("btnSave").onclick = async () => { 575 await api("/api/config", { 576 PORT: inpPort.value, 577 HOST: inpHost.value, 578 REGISTRATION_CODE: inpReg.value, 579 CLOUDFLARED_TUNNEL: inpTunnel.value, 580 CLOUDFLARED_CONFIG: inpCfg.value 581 }); 582 append("settings saved"); 583 }; 584 document.getElementById("btnReload").onclick = () => location.reload(); 585 document.getElementById("btnClear").onclick = () => { 586 logEl.textContent = ""; 587 }; 588 589 (async () => { 590 const snap = await api("/api/log"); 591 logEl.textContent = (snap.lines || []).join("\n") + "\n"; 592 refresh(); 593 refreshUpdate(); 594 refreshCloudflared(); 595 refreshCloudflaredFiles(); 596 refreshCloudflaredTunnels(); 597 setInterval(refresh, 1000); 598 setInterval(refreshUpdate, 60000); 599 setInterval(refreshCloudflared, 3000); 600 setInterval(refreshCloudflaredFiles, 5000); 601 setInterval(refreshCloudflaredTunnels, 30000); 602 })(); 603 604 const es = new EventSource("/api/log/stream"); 605 es.onmessage = (ev) => { 606 try { 607 const msg = JSON.parse(ev.data || "{}"); 608 if (msg.line) { 609 append(msg.line); 610 const m = String(msg.line).match(/https?:\/\/[^\s]+/i); 611 if (m && /trycloudflare\.com/i.test(m[0])) setPublicUrl(m[0]); 612 } 613 } catch {} 614 }; 615 </script> 616 </body> 617 </html>