';return}for(const w of u)o.insertAdjacentHTML("beforeend",`
+
+
${i(w)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `)}function p(){let o=safeSessionGet("dashcaddy-encryption-key");if(o)return o;const u=safeGet("dashcaddy-encryption-key");if(u)return safeSessionSet("dashcaddy-encryption-key",u),safeRemove("dashcaddy-encryption-key"),u;const w=new Uint8Array(32);return crypto.getRandomValues(w),o=Array.from(w,a=>a.toString(16).padStart(2,"0")).join(""),safeSessionSet("dashcaddy-encryption-key",o),o}const r=p();function E(o,u){if(!o)return"";const w=crypto.getRandomValues(new Uint8Array(8)),a=Array.from(w,L=>L.toString(16).padStart(2,"0")).join(""),I=new TextEncoder().encode(u+a);let B="";for(let L=0;LparseInt($,16))),A=atob(o.substring(17)),h=new TextEncoder().encode(u+B);let T="";for(let $=0;${["readonly","admin"].forEach(u=>{["token","username"].forEach(w=>{safeRemove(`${o}-${u}-${w}-enc`)})}),safeRemove(`${o}-token-enc`),safeRemove(`${o}-username-enc`)})}function v(o){const u=s(o,"readonly"),w=c(o,"readonly"),a=s(o,"admin"),I=c(o,"admin"),B=f(safeGet(`${o}-token-enc`),r),L=f(safeGet(`${o}-username-enc`),r);return{username:I||w||L,token:a||u||B,readonlyToken:u||B,readonlyUsername:w||L,adminToken:a||B,adminUsername:I||L}}document.getElementById("manage-tokens")?.addEventListener("click",()=>{g();const o=document.getElementById("token-management-modal"),u=e();n().forEach(w=>{const a=u[w];document.getElementById(`${w}-readonly-username`).value=a.readonly.username,document.getElementById(`${w}-readonly-token`).value=a.readonly.token,document.getElementById(`${w}-admin-username`).value=a.admin.username,document.getElementById(`${w}-admin-token`).value=a.admin.token,document.getElementById(`${w}-token-status`).textContent=""}),o.classList.add("show")}),document.getElementById("token-management-modal")?.addEventListener("click",o=>{const u=o.target.closest(".token-toggle");if(u){const w=u.dataset.target,a=document.getElementById(w);a.type==="password"?(a.type="text",u.textContent="\u{1F648}"):(a.type="password",u.textContent="\u{1F441}");return}o.target.id==="token-management-modal"&&o.target.classList.remove("show")}),document.getElementById("token-save")?.addEventListener("click",async()=>{const o=n();o.forEach(a=>{k(a,"readonly",document.getElementById(`${a}-readonly-username`).value.trim()),y(a,"readonly",document.getElementById(`${a}-readonly-token`).value.trim()),k(a,"admin",document.getElementById(`${a}-admin-username`).value.trim()),y(a,"admin",document.getElementById(`${a}-admin-token`).value.trim())});const u={};let w=!1;if(o.forEach(a=>{const I={},B=document.getElementById(`${a}-readonly-username`).value.trim(),L=document.getElementById(`${a}-readonly-token`).value.trim(),A=document.getElementById(`${a}-admin-username`).value.trim(),h=document.getElementById(`${a}-admin-token`).value.trim();B&&L&&(I.readonly={username:B,password:L},w=!0),A&&h&&(I.admin={username:A,password:h},w=!0),Object.keys(I).length>0&&(u[a]=I)}),w){o.forEach(a=>{u[a]&&(document.getElementById(`${a}-token-status`).textContent="Verifying...",document.getElementById(`${a}-token-status`).className="token-status")});try{const I=await(await secureFetch("/api/v1/dns/credentials",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({servers:u})})).json();I.results?o.forEach(B=>{const L=document.getElementById(`${B}-token-status`);if(!u[B]){L.textContent="";return}const A=I.results[B];A?.success?(L.textContent="\u2713 Verified & saved",L.className="token-status success"):A?.partial?(L.textContent="\u2713 "+A.partial,L.className="token-status success"):(L.textContent="\u2717 "+(A?.error||"Login failed"),L.className="token-status error")}):I.success?o.forEach(B=>{u[B]&&(document.getElementById(`${B}-token-status`).textContent="\u2713 Saved",document.getElementById(`${B}-token-status`).className="token-status success")}):o.forEach(B=>{u[B]&&(document.getElementById(`${B}-token-status`).textContent="\u2717 "+(I.error||"Failed"),document.getElementById(`${B}-token-status`).className="token-status error")})}catch(a){console.error("Failed to sync DNS credentials to backend:",a),o.forEach(I=>{u[I]&&(document.getElementById(`${I}-token-status`).textContent="\u2713 Saved locally (sync failed)",document.getElementById(`${I}-token-status`).className="token-status")})}}else o.forEach(a=>{document.getElementById(`${a}-token-status`).textContent=""});setTimeout(()=>{o.every(I=>{const B=document.getElementById(`${I}-token-status`)?.textContent;return!B||B.includes("\u2713")})&&closeModal("token-management-modal")},1500)}),document.getElementById("token-cancel")?.addEventListener("click",()=>{closeModal("token-management-modal")}),document.getElementById("token-clear-all")?.addEventListener("click",async()=>{if(confirm("Clear all stored DNS credentials? This cannot be undone.")){d(),n().forEach(o=>{document.getElementById(`${o}-readonly-username`).value="",document.getElementById(`${o}-readonly-token`).value="",document.getElementById(`${o}-admin-username`).value="",document.getElementById(`${o}-admin-token`).value="",document.getElementById(`${o}-token-status`).textContent="\u2713 Cleared",document.getElementById(`${o}-token-status`).className="token-status success"});try{await secureFetch("/api/v1/dns/credentials",{method:"DELETE"})}catch{}}}),window.getToken=s,window.getUsername=c,window.setToken=y,window.setUsername=k,window.getAllCredentials=e,window.getCredential=t,window.setCredential=m,window.getEncryptionKey=p,window.getDnsIds=n,window.getDnsDisplayName=i})(),(function(){function n(y,k,e=null){const d=document.getElementById(y+"-dot"),v=document.getElementById(y+"-pill"),o=document.getElementById(y+"-time"),u=document.querySelector(`[data-app="${y}"]`);d&&(d.classList.toggle("ok",k),d.classList.toggle("bad",!k)),v&&(v.textContent=k?"ON":"OFF",v.classList.toggle("on",k),v.classList.toggle("off",!k)),o&&e!==null&&(o.textContent=k?`${e}ms`:"timeout",o.className=`response-time ${i(e,k)}`),u&&u.setAttribute("data-status",k?"on":"off")}function i(y,k){return k?y<200?"excellent":y<500?"good":y<1e3?"fair":"slow":"timeout"}async function g(y){const k=performance.now();try{const e=await fetch("/probe/"+y,{cache:"no-store"}),d=performance.now(),v=Math.round(d-k);return{isUp:e.status>=200&&e.status<400||e.status===401||e.status===403,responseTime:v}}catch{const e=performance.now();return{isUp:!1,responseTime:Math.round(e-k)}}}window.APPS=[];let p=null,r=!1;async function E(){try{window.SkeletonLoader&&window.SkeletonLoader.show(6);const y=await fetch("/api/v1/services",{cache:"no-store"});y.ok?(window.APPS=await y.json(),window.SkeletonLoader&&window.SkeletonLoader.hide()):(console.error("Failed to load services:",y.status),window.SkeletonLoader&&window.SkeletonLoader.hide())}catch(y){console.error("Failed to load services:",y),window.SkeletonLoader&&window.SkeletonLoader.hide()}}function f(y){return buildServiceUrl(y)}function t(y,k,e){const d=document.createElement(y);return k&&(d.className=k),e&&(d.textContent=e),d}function m(){const y=document.getElementById("cards");y.innerHTML="";for(let k=0;k{P.stopPropagation(),window.openContainerLogsModal(e.containerId,e.name)},l.appendChild(x);const C=t("button","update-btn","\u2B06\uFE0F");C.title="Update container to latest version",C.id=`update-btn-${e.id}`,C.onclick=P=>{P.stopPropagation(),window.updateContainer(e.containerId,e.name,e.id)},l.appendChild(C)}if(e.logPath&&!e.containerId){const x=t("button","logs-btn","\u{1F4CB}");x.title="View application logs",x.onclick=C=>{C.stopPropagation(),window.openFileLogsModal(e.logPath,e.name)},l.appendChild(x)}if(e.isExternal||e.appTemplate||e.url){const x=t("button","creds-btn","\u{1F511}");x.title="Auto-login credentials",x.id=`creds-btn-${e.id}`,x.onclick=C=>{C.stopPropagation(),window.openServiceCredsModal(e)},l.appendChild(x)}if(e.id!=="internet"){const x=t("button","options-btn","\u2699\uFE0F");x.title="Edit service settings",x.onclick=C=>{C.stopPropagation(),window.openServiceEditModal(e)},l.appendChild(x)}if(e.id!=="internet"){const x=t("button","delete-btn","\u{1F5D1}\uFE0F");x.title="Delete this service",x.onclick=C=>{C.stopPropagation(),window.deleteService(e.id,e.name)},l.appendChild(x)}const S=t("button",null,"Open");S.onclick=()=>window.open(f(e.id),"_blank","noopener"),l.appendChild(S),d.appendChild(l),d.style.transitionDelay=`${Math.min(k*45,270)}ms`,y.appendChild(d)}requestAnimationFrame(()=>{y.querySelectorAll(".card").forEach(k=>k.classList.add("loaded"))}),window.groupRecipeCards&&requestAnimationFrame(()=>window.groupRecipeCards())}function s(y,k,e=null){const d=document.getElementById("dot-"+y+"-grid"),v=document.getElementById("badge-"+y),o=document.getElementById("time-"+y),u=document.querySelector(`[data-app="${y}"]`);d&&(d.classList.toggle("ok",k),d.classList.toggle("bad",!k)),v&&(v.textContent=k?"ON":"OFF",v.classList.toggle("on",k),v.classList.toggle("off",!k)),o&&e!==null&&(o.textContent=k?`${e}ms`:"timeout",o.className=`response-time ${i(e,k)}`),u&&u.setAttribute("data-status",k?"on":"off")}async function c(){if(p)return r=!0,p;function y(d,v=new Date){const o=document.getElementById("stamp");o&&(o.textContent=`${d}: ${new Date(v).toLocaleTimeString()}`)}function k(d){Object.keys(SITE.dnsServers).forEach(o=>{const u=d[o];u&&n(o,u.isUp,u.responseTime)}),d.internet&&n("internet",d.internet.isUp,d.internet.responseTime),window.APPS.forEach(o=>{const u=d[o.id];u&&s(o.id,u.isUp,u.responseTime)})}async function e(){const d=Object.keys(SITE.dnsServers),v=d.map(a=>g(a));v.push(g("internet"));const o=await Promise.all(v);d.forEach((a,I)=>n(a,o[I].isUp,o[I].responseTime));const u=o[o.length-1];n("internet",u.isUp,u.responseTime),(await Promise.all(window.APPS.map(async a=>{const I=await g(a.id);return{id:a.id,...I}}))).forEach(a=>{s(a.id,a.isUp,a.responseTime)})}return p=(async()=>{try{const d=await fetch("/api/v1/services/status",{cache:"no-store"});if(!d.ok)throw new Error(`Status refresh failed (${d.status})`);const v=await d.json();k(v.statuses||{}),y("last check",v.checkedAt||new Date)}catch(d){console.warn("Batched status refresh failed, falling back to direct probes:",d);try{await e(),y("last check")}catch(v){console.error("Dashboard refresh failed:",v),y("last failed")}}finally{p=null,r&&(r=!1,setTimeout(()=>{window.refreshAll()},0))}})(),p}document.querySelector(".top")?.addEventListener("click",y=>{const k=y.target.closest('[id$="-open"]');if(!k)return;const e=k.id.replace("-open","");SITE.dnsServers[e]&&window.open(f(e),"_blank","noopener")}),document.getElementById("ca-open")?.addEventListener("click",()=>window.open(f("ca"),"_blank","noopener")),document.getElementById("creds-btn-ca")?.addEventListener("click",y=>{y.stopPropagation();const k=window.APPS.find(e=>e.id==="ca");k&&window.openServiceCredsModal&&window.openServiceCredsModal(k)}),document.getElementById("options-btn-ca")?.addEventListener("click",y=>{y.stopPropagation();const k=window.APPS.find(e=>e.id==="ca");k&&window.openServiceEditModal&&window.openServiceEditModal(k)}),document.getElementById("delete-btn-ca")?.addEventListener("click",y=>{y.stopPropagation(),window.deleteService&&window.deleteService("ca","DashCA")}),window.loadServices=E,window.buildGrid=m,window.refreshAll=c,window.setQuick=n,window.setBadge=s,window.getResponseTimeClass=i,window.checkServiceWithTiming=g,window.serviceUrl=f,window.el=t})(),(function(){async function n(t){const s=await(await secureFetch(`/api/v1/dns/restart/${t}`,{method:"POST"})).json();if(!s.success)throw new Error(s.error||"Restart failed");return s}document.querySelector(".top")?.addEventListener("click",async t=>{const m=t.target.closest('[id$="-restart"]');if(!m)return;const s=m.id.replace("-restart","");if(SITE.dnsServers[s]&&confirm(`Restart ${s.toUpperCase()} service?`))try{await withButton(m,"...",()=>n(s)),setTimeout(window.refreshAll,DC.DELAYS.RELOAD)}catch(c){showNotification("Restart failed: "+c.message,"error")}});async function i(t,m){const s=document.getElementById(`${t}-update`),c=s?.textContent||"\u2B06\uFE0F";try{s.textContent="\u{1F50D}",s.disabled=!0,s.title="Checking for updates...";const k=await(await fetch(`/api/v1/dns/check-update?server=${encodeURIComponent(m)}`)).json();if(!k.success)throw new Error(k.error||"Failed to check for updates");if(!k.updateAvailable){s.textContent="\u2705",s.title=`Already on latest version (${k.currentVersion})`,showNotification(`${t.toUpperCase()} is already up to date! Current version: ${k.currentVersion}`,"info"),setTimeout(()=>{s.textContent=c,s.disabled=!1,s.title="Update DNS server"},3e3);return}if(!confirm(`Update available for ${t.toUpperCase()}!
+
+Current: ${k.currentVersion}
+New: ${k.updateVersion}
+
+`+(k.updateTitle?`${k.updateTitle}
+
+`:"")+`The DNS server will restart during the update.
+Proceed?`)){s.textContent=c,s.disabled=!1,s.title="Update DNS server";return}s.textContent="\u{1F504}",s.title="Updating...";const v=await(await secureFetch(`/api/v1/dns/update?server=${encodeURIComponent(m)}`,{method:"POST"})).json();if(!v.success)throw new Error(v.error||"Update failed");if(v.manualUpdateRequired){s.textContent="\u2B06\uFE0F",s.title=`Update available: ${v.newVersion}`;const o=v.downloadLink?`
+Download: ${v.downloadLink}`:"",u=v.instructionsLink?`
+Instructions: ${v.instructionsLink}`:"";showNotification(`${t.toUpperCase()} update requires manual installation. Current: ${v.previousVersion} \u2192 ${v.newVersion}. Please update manually on the host machine.`,"warning",8e3),s.disabled=!1;return}s.textContent="\u2705",s.title="Updated successfully!",showNotification(`${t.toUpperCase()} updated successfully! ${v.previousVersion} \u2192 ${v.newVersion}. Server is restarting...`,"success"),setTimeout(()=>{s.textContent=c,s.disabled=!1,s.title="Update DNS server",window.refreshAll()},1e4)}catch(y){console.error("DNS update error:",y),s.textContent="\u274C",s.title="Update failed",showNotification(`Failed to update ${t.toUpperCase()}: ${y.message}`,"error"),setTimeout(()=>{s.textContent=c,s.disabled=!1,s.title="Update DNS server"},3e3)}}document.querySelector(".top")?.addEventListener("click",t=>{const m=t.target.closest('[id$="-update"]');if(!m)return;const s=m.id.replace("-update","");SITE.dnsServers[s]&&i(s,SITE.dnsServers[s]?.ip)}),injectModal("dns-settings-modal",`
+
+
+
DNS Settings
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Manage credentials via Tokens in the toolbar
+
+
+
+
+
+
+
+
+
`);let g=null;function p(t){g=t;const m=SITE.dnsServers[t]||{},s=document.getElementById("dns-settings-modal");document.getElementById("dns-settings-title").textContent=`${(m.name||t).toUpperCase()} Settings`,document.getElementById("dns-edit-ip").value=m.ip||"",document.getElementById("dns-edit-port").value=m.port||DC.DEFAULTS.DNS_PORT,document.getElementById("dns-edit-name").value=m.name||"",s.classList.add("show")}async function r(){if(!g)return;const t=document.getElementById("dns-edit-ip").value.trim(),m=document.getElementById("dns-edit-port").value.trim()||DC.DEFAULTS.DNS_PORT,s=document.getElementById("dns-edit-name").value.trim();if(!t){showNotification("Server IP is required","warning");return}const c={dnsServers:{}};c.dnsServers[g]={ip:t,port:String(m)},s&&(c.dnsServers[g].name=s);try{const k=await(await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(c)})).json();k.success?(SITE.dnsServers[g]=c.dnsServers[g],showNotification(`${g.toUpperCase()} settings saved`,"success"),f(),window.refreshAll()):showNotification(k.error||"Failed to save settings","error")}catch(y){showNotification("Failed to save: "+y.message,"error")}}async function E(){if(g&&confirm(`Remove ${g.toUpperCase()} from dashboard? This won't affect the actual DNS server.`))try{const m=await(await secureFetch("/api/v1/config")).json();m.dnsServers&&delete m.dnsServers[g];const c=await(await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({dnsServers:m.dnsServers||{}})})).json();if(c.success){delete SITE.dnsServers[g];const y=document.querySelector(`.top [data-app="${g}"]`);y&&y.remove(),showNotification(`${g.toUpperCase()} removed from dashboard`,"success"),f()}else showNotification(c.error||"Failed to remove","error")}catch(t){showNotification("Failed to remove: "+t.message,"error")}}function f(){closeModal("dns-settings-modal"),g=null}document.getElementById("dns-settings-cancel")?.addEventListener("click",f),document.getElementById("dns-settings-save")?.addEventListener("click",r),document.getElementById("dns-settings-delete")?.addEventListener("click",E),document.getElementById("dns-settings-modal")?.addEventListener("click",t=>{t.target.id==="dns-settings-modal"&&f()}),document.querySelector(".top")?.addEventListener("click",t=>{const m=t.target.closest('[id$="-settings"]');if(!m)return;const s=m.id.replace("-settings","");SITE.dnsServers[s]&&(t.stopPropagation(),p(s))}),document.getElementById("refresh")?.addEventListener("click",window.refreshAll)})(),(function(){injectModal("logs-modal",`
+
",m.innerHTML=f,B("setup-step-summary")}async function D(m){try{const f=await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(m)});return f.ok?(await f.json(),!0):(console.error("Failed to save config to server:",f.status),!1)}catch(f){return console.error("Error saving config to server:",f),!1}}async function A(){const m={setupComplete:!0,configurationType:y,timestamp:new Date().toISOString(),timezone:document.getElementById("setup-timezone")?.value||Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC"};y==="homelab"?(m.tld=document.getElementById("setup-tld")?.value?.trim()||".home",m.caName=document.getElementById("setup-ca-name")?.value?.trim()||"",m.dns={provider:"technitium",ip:document.getElementById("setup-dns-ip")?.value?.trim()||"",port:document.getElementById("setup-dns-port")?.value?.trim()||DC.DEFAULTS.DNS_PORT,token:document.getElementById("setup-dns-token")?.value?.trim()||""},m.defaults={dnsType:"private",sslType:"internal",targetIP:"localhost"}):y==="simple"?(m.defaultIP=document.getElementById("setup-simple-ip")?.value?.trim()||"localhost",m.defaults={dnsType:"none",sslType:"none",targetIP:m.defaultIP}):y==="public"&&(m.domain=document.getElementById("setup-public-domain")?.value?.trim()||"",m.email=document.getElementById("setup-public-email")?.value?.trim()||"",m.routingMode=document.querySelector('input[name="routing-mode"]:checked')?.value||"subdirectory",m.defaults={dnsType:m.routingMode==="subdirectory"?"none":"public",sslType:"letsencrypt",targetIP:"localhost"});const f=await D(m);safeSet("dashcaddy-config",JSON.stringify(m)),safeSet("dashcaddy-setup","completed"),document.getElementById("setup-wizard").style.display="none";const c=y==="homelab"?"Professional Home Lab":y==="simple"?"Simple Setup":"Public Server",d=f?"server (shared across all devices)":"locally (this browser only)";showNotification(`Setup Complete! Configured for: ${c}. Settings saved to: ${d}`,"success",5e3),setTimeout(()=>location.reload(),500)}const $=document.getElementById("setup-step-1-next");$&&($.onclick=function(m){m.preventDefault();const f=document.querySelector('input[name="config-type"]:checked');f&&(y=f.value),B(y==="homelab"?"setup-step-homelab":y==="simple"?"setup-step-simple":y==="public"?"setup-step-public":"setup-step-homelab")});const P=document.getElementById("setup-skip");P&&(P.onclick=async function(m){m.preventDefault(),confirm("Skip setup? You can run it later from Settings.")&&(await D({setupComplete:!0,skipped:!0,timestamp:new Date().toISOString()}),safeSet("dashcaddy-setup","skipped"),document.getElementById("setup-wizard").style.display="none")});const C=document.getElementById("setup-tld");C&&(C.oninput=function(m){const f=m.target.value||".home",c=document.getElementById("tld-preview"),d=document.getElementById("tld-preview-2");c&&(c.textContent=f),d&&(d.textContent=f)});const H=document.getElementById("setup-homelab-back");H&&(H.onclick=function(m){m.preventDefault(),B("setup-step-1")});const x=document.getElementById("setup-homelab-next");x&&(x.onclick=function(m){m.preventDefault();const f=document.getElementById("setup-tld")?.value?.trim()||"",c=document.getElementById("setup-ca-name")?.value?.trim()||"",d=document.getElementById("setup-dns-ip")?.value?.trim()||"";if(!f||!f.startsWith(".")){showNotification("Please enter a valid TLD starting with a dot (e.g., .home)","warning");return}if(!c){showNotification("Please enter a Certificate Authority name","warning");return}if(!d){showNotification("Please enter your DNS server IP address","warning");return}N()});const O=document.getElementById("setup-simple-back");O&&(O.onclick=function(m){m.preventDefault(),B("setup-step-1")});const z=document.getElementById("setup-simple-next");z&&(z.onclick=function(m){m.preventDefault(),N()}),document.querySelectorAll('input[name="routing-mode"]').forEach(function(m){m.onchange=function(){var f=document.getElementById("dns-requirement-note");f&&(f.textContent=this.value==="subdirectory"?"Only one DNS record needed (for the main domain)":"You'll need to configure DNS manually for each subdomain")}});const S=document.getElementById("setup-public-back");S&&(S.onclick=function(m){m.preventDefault(),B("setup-step-1")});const E=document.getElementById("setup-public-next");E&&(E.onclick=function(m){m.preventDefault();const f=document.getElementById("setup-public-domain")?.value?.trim()||"",c=document.getElementById("setup-public-email")?.value?.trim()||"";if(!f){showNotification("Please enter your domain name","warning");return}if(!c||!c.includes("@")){showNotification("Please enter a valid email address","warning");return}N()});const k=document.getElementById("setup-summary-back");k&&(k.onclick=function(m){m.preventDefault(),y==="homelab"?B("setup-step-homelab"):y==="simple"?B("setup-step-simple"):y==="public"&&B("setup-step-public")});const b=document.getElementById("setup-finish");b&&(b.onclick=function(m){m.preventDefault(),A()}),window.getGlobalConfig=async function(){try{const f=await fetch("/api/v1/config");if(f.ok){const c=await f.json();if(c&&c.setupComplete)return c}}catch{console.warn("Could not fetch config from server")}const m=safeGet("dashcaddy-config");return m?JSON.parse(m):{setupComplete:!1,configurationType:"homelab",tld:".home",caName:"",defaults:{dnsType:"private",sslType:"internal",targetIP:"localhost"}}},window.resetSetupWizard=async function(){if(confirm("Reset DashCaddy configuration? This will show the setup wizard again.")){try{await secureFetch("/api/v1/config",{method:"DELETE"})}catch{console.warn("Could not delete server config")}safeRemove("dashcaddy-setup"),safeRemove("dashcaddy-config"),location.reload()}}})(),(function(){injectModal("app-selector-modal",`
+
+
Choose an App
+
+
+
+
+
+
`),injectModal("app-deploy-modal",`
+
+
Deploy Application
+
+
+
+
+
+
+
+ Your app will be available at: uptime.home
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ \u2713 Detected from existing media servers:
+
+
+
+
+
+
+
+
+
+
+
+ Select folders from existing servers, browse, or type paths manually. Separate multiple with commas.
+
+ Token expires in 4 minutes! Get it right before clicking Deploy. Leave empty to configure Plex manually later.
+
+
+
+
+
+
+
+
+
+ Checking Tailscale status...
+
+
+
+
+
+
+ \u2699\uFE0F Advanced Options
+
+
+
+
+
+
+
+
+
+ Use 'localhost' for same-host containers, or specific IP for remote services
+
+
+
+
+
+ Customize where container data is stored on the host. Media volumes are configured above.
+
+
+
+
+
+
+
+
+
+
+
+
+
`);const y="custom-apps";let h=null,L=null;const T=document.getElementById("app-selector-modal"),B=document.getElementById("app-selector-grid");async function N(){try{const t=await(await fetch("/api/v1/apps/templates")).json();if(t.success)return h=t.templates,L=t.categories,!0}catch(e){console.error("Failed to fetch app templates:",e)}return!1}async function D(e){try{return await(await fetch(`/api/v1/apps/ports/${e}/check`)).json()}catch(t){return console.error("Failed to check port:",t),{available:!0}}}async function A(e){try{const i=await(await fetch(`/api/v1/apps/ports/${e}/suggest`)).json();if(i.success)return i.suggestedPort}catch(t){console.error("Failed to get suggested port:",t)}return e}async function $(){if(B.innerHTML='
Loading app templates...
',!h&&!await N()){B.innerHTML='
Failed to load app templates. Please try again.
';return}B.innerHTML="";const e={};for(const[i,r]of Object.entries(h)){const p=r.category||"Other";e[p]||(e[p]=[]),e[p].push({id:i,...r})}const t=L?Object.keys(L):Object.keys(e).sort();for(const i of t){const r=e[i];if(!r||r.length===0)continue;r.sort((o,s)=>(s.popularity||0)-(o.popularity||0));const p=document.createElement("div");p.className="app-category-header";const v=L?.[i]||{};p.innerHTML=`${escapeHtml(v.icon||"")} ${escapeHtml(i)}`,v.color&&(p.style.borderBottomColor=v.color),B.appendChild(p),r.forEach(o=>{const s=document.createElement("div");s.className="app-option";const u=o.isDashboardWidget,l=u&&safeGet("widget-"+o.id+"-enabled")!=="false",g=u?`
${l?"ON":"OFF"}
`:"",I=!u&&o.difficulty?`
${escapeHtml(o.difficulty)}
`:"";s.innerHTML=`
+
${escapeHtml(o.icon||"\u{1F4E6}")}
+
${escapeHtml(o.name)}
+
${escapeHtml(o.description||"")}
+ ${g}${I}
+ `,u?s.onclick=()=>P(o,s):s.onclick=()=>C(o),B.appendChild(s)})}window.renderRecipeCards&&await window.renderRecipeCards(B)}function P(e,t){const i="widget-"+e.id+"-enabled",p=!(safeGet(i)!=="false");safeSet(i,String(p));const v=e.widgetSelector;if(v){const s=document.querySelector(v);s&&(s.style.display=p?"":"none")}const o=t.querySelector('div[style*="border-radius: 4px"]');o&&(o.textContent=p?"ON":"OFF",o.style.background=p?"#2ecc7130":"#e74c3c30",o.style.color=p?"#2ecc71":"#e74c3c"),showNotification(`${e.name} widget ${p?"enabled":"disabled"}`,"success",2e3)}async function C(e){const t=document.getElementById("app-deploy-modal"),i=document.getElementById("app-deploy-title"),r=document.getElementById("deploy-subdomain"),p=document.getElementById("deploy-url-preview"),v=document.getElementById("deploy-ip"),o=document.getElementById("deploy-port"),s=document.getElementById("deploy-tailscale-only"),u=document.getElementById("tailscale-status");try{const U=await(await secureFetch("/api/v1/apps/check-existing",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({appId:e.id})})).json();if(U.success&&U.exists){const J=U.container;confirm(`Found existing ${e.name} container:
+
+Container: ${J.name}
+Status: ${J.status}
+Port: ${J.primaryPort||"N/A"}
+
+Would you like to use this existing container?
+
+Click OK to configure DNS/Caddy for the existing container.
+Click Cancel to deploy a new container.`)&&(e._useExisting=!0,e._existingContainer=J)}}catch{}i.textContent=`Deploy ${e.name}`;const l=e.subdomain||e.id.replace(/-/g,"");r.value=l;const g=document.getElementById("subpath-compat-warning");if(g)if(SITE.routingMode==="subdirectory"){const F=e.subpathSupport||"strip";F==="none"?(g.style.display="block",g.innerHTML='⚠ '+e.name+" does not support subdirectory mode. It may not work correctly at a subpath."):F==="strip"?(g.style.display="block",g.innerHTML='ⓘ '+e.name+" has unverified subdirectory support. It may require additional configuration."):g.style.display="none"}else g.style.display="none";const I=SITE.defaults.dnsType||(SITE.configurationType==="public"?"public":"private"),w=SITE.defaults.sslType||(SITE.configurationType==="public"?"letsencrypt":"internal"),M=document.querySelector(`input[name="dns-type"][value="${I}"]`),R=document.querySelector(`input[name="ssl-type"][value="${w}"]`);M?M.checked=!0:document.querySelector('input[name="dns-type"][value="private"]').checked=!0,R?R.checked=!0:document.querySelector('input[name="ssl-type"][value="internal"]').checked=!0,v.value=SITE.defaults.targetIP||"localhost",s.checked=!1;const q=document.querySelector("#app-deploy-modal .flex-col-gap")?.closest("div"),_=document.querySelector("#app-deploy-modal details"),W=_?.querySelector("div");if(_&&W&&(SITE.configurationType==="public"||SITE.configurationType==="homelab")){const F=document.querySelectorAll('#app-deploy-modal input[name="dns-type"]')[0]?.closest("div.flex-col-gap")?.parentElement,U=document.querySelectorAll('#app-deploy-modal input[name="ssl-type"]')[0]?.closest("div.flex-col-gap")?.parentElement;F&&!F.dataset.moved&&(W.appendChild(F),F.dataset.moved="1"),U&&!U.dataset.moved&&(W.appendChild(U),U.dataset.moved="1")}const j=document.getElementById("media-path-section"),V=document.getElementById("deploy-media-path"),se=document.getElementById("media-path-description");if(e.mediaMount){j.style.display="block",V.value="",V.placeholder="/media/Movies, /media/TVShows or click Browse";const F=document.getElementById("detected-mounts-container"),U=document.getElementById("detected-mounts-list");try{const G=await(await fetch("/api/v1/media/detected-mounts")).json();if(G.success&&G.mounts.length>0){F.style.display="block",U.innerHTML="";const ee=[...new Set(G.mounts.map(Z=>Z.hostPath))];V.value=ee.join(", "),G.mounts.forEach(Z=>{const Y=document.createElement("button");Y.type="button";const le=ee.includes(Z.hostPath);Y.style.cssText=`padding: 8px 14px; font-size: 0.85rem; background: color-mix(in srgb, var(--success) ${le?"40%":"15%"}, var(--card-bg)); border: 1px solid var(--success); border-radius: 6px; cursor: pointer; color: var(--fg);`,Y.innerHTML=`${escapeHtml(Z.folderName)} from ${escapeHtml(Z.sourceImage)}`,Y.title=`${Z.hostPath} (from ${Z.sourceContainer})`,Y.onclick=()=>{const re=V.value.split(",").map(de=>de.trim()).filter(de=>de),ce=re.indexOf(Z.hostPath);ce>=0?(re.splice(ce,1),Y.style.background="color-mix(in srgb, var(--success) 15%, var(--card-bg))"):(re.push(Z.hostPath),Y.style.background="color-mix(in srgb, var(--success) 40%, var(--card-bg))"),V.value=re.join(", ")},U.appendChild(Y)})}else F.style.display="none"}catch{F.style.display="none"}document.getElementById("browse-media-btn").onclick=()=>{openFolderBrowser(V)}}else j.style.display="none",V.value="",document.getElementById("detected-mounts-container").style.display="none";const K=document.getElementById("plex-claim-section");K&&(e.id==="plex"||e.claimToken?(K.style.display="block",document.getElementById("deploy-plex-claim").value=""):K.style.display="none");const Q=document.getElementById("volume-mounts-section"),te=document.getElementById("volume-mounts-list");if(te.innerHTML="",e.docker?.volumes?.length){const F=e.mediaMount?.containerPath,U=e.docker.volumes.filter(J=>!J.includes("{{MEDIA_PATH}}")&&!(F&&J.endsWith(":"+F)));U.length>0?(Q.style.display="block",U.forEach((J,G)=>{const[ee,Z]=J.split(":"),Y=document.createElement("div");Y.style.cssText="display: flex; gap: 6px; align-items: center;",Y.innerHTML=`
+
+ \u2192 ${Z}
+
+ `,te.appendChild(Y),Y.querySelector(".vol-browse-btn").onclick=()=>{const le=Y.querySelector(".vol-host-path");openFolderBrowser(le)}})):Q.style.display="none"}else Q.style.display="none";const ne=e.defaultPort||8080;o.value="",o.placeholder=`Default: ${ne}`;let X=document.getElementById("deploy-port-status");X||(X=document.createElement("div"),X.id="deploy-port-status",X.style.cssText="font-size: 0.8rem; margin-top: 4px;",o.parentNode.appendChild(X));async function oe(){const F=o.value||ne;X.innerHTML='Checking port...';const U=await D(F);if(U.available)X.innerHTML=`Port ${escapeHtml(String(F))} is available`;else{const J=await A(ne);X.innerHTML=`
+ Port ${escapeHtml(F)} in use by ${escapeHtml(U.conflict?.usedBy||"unknown")}
+ `;const G=document.createElement("button");G.type="button",G.textContent=`Use ${J}`,G.style.cssText="margin-left: 8px; padding: 2px 8px; font-size: 0.75rem; cursor: pointer;",G.onclick=()=>{document.getElementById("deploy-port").value=J,X.innerHTML=`Using suggested port ${escapeHtml(String(J))}`},X.appendChild(G)}}let ie;o.oninput=function(){clearTimeout(ie),ie=setTimeout(oe,500)},oe();try{const U=await(await fetch("/api/v1/tailscale/status")).json();U.success&&U.installed&&U.connected?u.innerHTML=`
+ Connected
+ ${U.self?.hostname} (${U.self?.ip})
+ | ${U.deviceCount} devices
+ `:U.installed?u.innerHTML='Not connected':(u.innerHTML='Not available',s.disabled=!0)}catch{u.innerHTML='Could not check status'}function ae(){const F=r.value||"subdomain",U=document.querySelector('input[name="dns-type"]:checked').value,J=document.querySelector('input[name="ssl-type"]:checked').value;let G="";if(SITE.routingMode==="subdirectory"&&SITE.domain)G=`https://${SITE.domain}/${F}`;else if(U==="private")G=`${J==="none"?"http":"https"}://${buildDomain(F)}`;else if(U==="public"){const ee=J==="none"?"http":"https",Z=SITE.domain||F;G=SITE.domain?`${ee}://${F}.${SITE.domain}`:`${ee}://${F}`}else{const ee=o.value||e.defaultPort||DC.DEFAULTS.SERVICE_PORT;G=`http://${v.value}:${ee}`}p.textContent=G}r.oninput=ae,v.oninput=ae,o.oninput=ae,document.querySelectorAll('input[name="dns-type"]').forEach(F=>{F.onchange=ae}),document.querySelectorAll('input[name="ssl-type"]').forEach(F=>{F.onchange=ae}),ae(),T.classList.remove("show"),t.classList.add("show"),t.dataset.appTemplate=JSON.stringify(e)}async function H(e){const t=e.appTemplate,i=safeGetJSON(y,[]),r=t._useExisting&&t._existingContainer,p=i.find(v=>v.id===e.subdomain);if(!(p&&!r&&!confirm(`An app with subdomain "${e.subdomain}" already exists. Redeploy?`))){if(p){const v=i.indexOf(p);i.splice(v,1),safeSet(y,JSON.stringify(i))}if(r)e.port=t._existingContainer.primaryPort;else{const v=e.port||t.defaultPort||8080;showNotification(`Checking port ${v} availability...`,"info",0);const o=await D(v);if(!o.available){const s=await A(t.defaultPort||8080);if(confirm(`Port ${v} is already in use by ${o.conflict?.usedBy||"another container"}.
+
+Would you like to use port ${s} instead?`))e.port=s;else{showNotification("Deployment cancelled - port conflict","error",5e3);return}}}showNotification(r?`Configuring ${t.name} with existing container...`:`Deploying ${t.name}...`,"info",0);try{const v={appId:t.id,config:{subdomain:e.subdomain,ip:e.ip,createDns:e.dnsType==="private",port:e.port||t.defaultPort||null,sslType:e.sslType,dnsType:e.dnsType,tailscaleOnly:e.tailscaleOnly||!1,mediaPath:e.mediaPath||null,plexClaimToken:e.plexClaimToken||null,customVolumes:e.customVolumes||null}};r&&(v.config.useExisting=!0,v.config.existingContainerId=t._existingContainer.id,v.config.existingPort=t._existingContainer.primaryPort,!e.port&&t._existingContainer.primaryPort&&(v.config.port=t._existingContainer.primaryPort));const s=await(await secureFetch("/api/v1/apps/deploy",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(v)})).json();if(s.success){const u={id:e.subdomain,name:t.name,logo:`/assets/${t.id}.png`,containerId:s.containerId,url:s.url,ip:e.ip,appTemplate:t.id,tailscaleOnly:e.tailscaleOnly||!1};i.push(u),safeSet(y,JSON.stringify(i)),window.APPS&&!window.APPS.some(g=>g.id===t.id)&&(window.APPS.push(u),typeof window.buildGrid=="function"&&window.buildGrid(),typeof window.refreshAll=="function"&&setTimeout(()=>window.refreshAll(),500));let l=s.usedExisting?`${t.name} configured with existing container!
+URL: ${s.url}`:`${t.name} deployed successfully!
+URL: ${s.url}`;s.warning&&(l+=`
+
+\u26A0 Warning: ${s.warning}`),showNotification(l,"success",8e3),delete t._useExisting,delete t._existingContainer,s.url&&s.url.startsWith("https://")&&x(s.url,t.name),s.setupInstructions&&s.setupInstructions.length>0&&setTimeout(()=>{const g=s.setupInstructions.join(`
+`);showNotification(`Setup Instructions for ${t.name}: ${g}`,"info",1e4)},1e3)}else throw new Error(s.error||"Deployment failed")}catch(v){console.error("Deployment error:",v),showNotification(`Failed to deploy ${t.name}: ${v.message}`,"error",8e3)}}}async function x(e,t){showNotification(`\u23F3 Generating SSL certificate for ${t}...`,"warning",6e4);let i=0;const r=12,p=async()=>{i++;try{const v=await fetch(e,{method:"HEAD",mode:"no-cors"});return showNotification(`\u2705 ${t} is ready! SSL certificate generated.`,"success",5e3),!0}catch{return i{window.APPS.some(i=>i.id===t.id)||window.APPS.push(t)})}document.getElementById("add-service-btn")?.addEventListener("click",()=>{$(),T.classList.add("show")}),wireModal(T,document.getElementById("app-selector-cancel"));const z=document.getElementById("app-deploy-modal");document.getElementById("app-deploy-cancel")?.addEventListener("click",()=>{z.classList.remove("show")}),document.getElementById("app-deploy-confirm")?.addEventListener("click",()=>{const e=JSON.parse(z.dataset.appTemplate),t=document.getElementById("deploy-media-path").value.trim(),i=[];document.querySelectorAll("#volume-mounts-list .vol-host-path").forEach(p=>{i.push({hostPath:p.value.trim(),containerPath:p.dataset.containerPath})});const r={appTemplate:e,subdomain:document.getElementById("deploy-subdomain").value.trim(),dnsType:document.querySelector('input[name="dns-type"]:checked').value,sslType:document.querySelector('input[name="ssl-type"]:checked').value,ip:document.getElementById("deploy-ip").value.trim(),port:document.getElementById("deploy-port").value.trim(),tailscaleOnly:document.getElementById("deploy-tailscale-only").checked,mediaPath:t||null,plexClaimToken:document.getElementById("deploy-plex-claim")?.value.trim()||null,customVolumes:i.length>0?i:null};if(!r.subdomain){showNotification("Please enter a subdomain or domain name","warning");return}if(e.mediaMount?.required&&!t){showNotification("Please enter a media library path for this application","warning");return}z.classList.remove("show"),H(r)}),wireModal(z);const S=document.getElementById("folder-browser-modal"),E=document.getElementById("folder-browser-path"),k=document.getElementById("folder-browser-list"),b=document.getElementById("folder-browser-selected"),m=document.getElementById("folder-browser-selected-list");let f="",c=[],d=null;window.openFolderBrowser=function(e){d=e,c=e.value.split(",").map(t=>t.trim()).filter(t=>t),f="",n(),a(""),S.classList.add("show")};async function a(e){E.textContent=e||"Select a drive...",k.innerHTML='
');const y=document.getElementById("error-log-modal"),h=document.getElementById("error-log-content"),L=document.getElementById("view-error-logs"),T=document.getElementById("error-log-refresh"),B=document.getElementById("error-log-clear"),N=document.getElementById("error-log-close");async function D(){h.innerHTML='
`,$.style.display="block"}}function f(n){let e="";const t=n.services,i=["radarr","sonarr","prowlarr"];for(const v of i){const o=t[v];if(!o||o.status==="not_found"&&!o.url)continue;const s=S[v],u=E[v],l=o.status==="connected";e+=`
`}P.innerHTML=e}window.smartTestConnection=async function(n){const e=document.getElementById(`smart-${n}-url`),t=document.getElementById(`smart-${n}-key`),i=document.getElementById(`smart-${n}-status`),r=e?.value.trim(),p=t?.value.trim();if(!r||!p){i.innerHTML='Enter URL and API key';return}i.innerHTML='';try{const o=await(await secureFetch("/api/v1/arr/test-connection",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({service:n,url:r,apiKey:p})})).json();o.success?i.innerHTML=`✓ ${escapeHtml(o.appName||"Connected")} v${escapeHtml(o.version||"")}`:i.innerHTML=`✗ ${escapeHtml(o.error)}`}catch(v){i.innerHTML=`✗ ${escapeHtml(v.message)}`}};async function c(){k("progress"),C.innerHTML='
Connecting services...
';const n={};for(const t of["radarr","sonarr","prowlarr"]){const i=document.getElementById(`smart-${t}-url`)?.value.trim(),r=document.getElementById(`smart-${t}-key`)?.value.trim();r&&i?n[t]={apiKey:r,url:i}:r&&(n[t]={apiKey:r})}const e={services:Object.keys(n).length>0?n:void 0,configurePlex:document.getElementById("smart-opt-plex")?.checked,configureProwlarr:document.getElementById("smart-opt-prowlarr")?.checked,configureSeerr:document.getElementById("smart-opt-seerr")?.checked,saveCredentials:document.getElementById("smart-opt-save")?.checked};try{const i=await(await secureFetch("/api/v1/arr/smart-connect",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)})).json();let r="";for(const p of i.steps||[]){const v=p.status==="success"?'✓':'✗',o=p.status==="success"?"var(--muted)":"var(--bad-fg)";r+=`
"}}}window.__restoreServerBackup=async function(a){if(confirm("Restore from this server backup? This will overwrite current configuration."))try{var n=await secureFetch("/api/v1/backups/restore/"+a,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({restoreServices:!0,restoreConfig:!0})}),e=await n.json();e.success?(showNotification("Restore completed successfully!","success"),location.reload()):showNotification("Restore failed: "+(e.error||"Unknown error"),"error")}catch(t){showNotification("Restore error: "+t.message,"error")}},document.querySelector('[data-panel="backup-automated"]')?.addEventListener("click",m),document.querySelector('[data-panel="backup-history-tab"]')?.addEventListener("click",d)})(),(function(){injectModal("stats-modal",`
+
+
\u{1F4CA} Resource Monitor
+
+ Real-time and historical CPU, memory, network, and disk usage for containers.
+
\u{1F3E5}No health checks configured. Go to the Configure tab to add services.
';return}const a=Object.values(d.status);let n='
';n+='
',n+='
Service
Status
',n+='
Uptime 24h
Uptime 7d
',n+='
Avg Response
Last Check
';for(const e of a){const t=e.status==="up",i=t?"var(--dot-ok)":"var(--dot-bad)",r=e.uptime?.["24h"]??"-",p=e.uptime?.["7d"]??"-",v=e.avgResponseTime!=null?Math.round(e.avgResponseTime)+"ms":"-",o=e.timestamp?timeAgo(e.timestamp):"-";n+=`
';for(const r of i){const p=r.status==="resolved",v=p&&r.duration?r.duration<6e4?Math.round(r.duration/1e3)+"s":Math.round(r.duration/6e4)+"m":"-";e+='
',e+=`
${escapeHtml(r.serviceId)}
`,e+=`
${escapeHtml(r.type)}
`,e+=`
${S(r.severity)}
`,e+=`
${r.status}
`,e+=`
${v}
`,e+=`
${timeAgo(r.createdAt)}
`,e+="
"}e+="
"}N.innerHTML=e||'
\u{1F6A8}No incidents recorded yet.
'}catch(c){N.innerHTML=`
Failed: ${escapeHtml(c.message)}
`}}async function b(){try{const d=await(await fetch("/api/v1/health-checks/status")).json(),a=d.success&&d.status?Object.values(d.status):[];if(a.length===0){D.innerHTML='
\u2699\uFE0FNo health checks configured yet. Click "Add Health Check" below.
';return}let n='
';n+='
Service
Status
SLA Target
Actions
';for(const e of a){const t=e.status==="up";n+='
',n+=`
${escapeHtml(e.name||e.serviceId)}
`,n+=`
${t?"Up":"Down"}
`,n+=`
${e.sla?.target?e.sla.target+"%":"-"}
`,n+='
',n+=``,n+=``,n+="
"}n+="
",D.innerHTML=n}catch(c){D.innerHTML=`
Failed: ${escapeHtml(c.message)}
`}}function m(c,d,a,n,e,t,i){O=c||null,C.textContent=c?"Edit Health Check":"Add Health Check",document.getElementById("health-form-id").value=c||"",document.getElementById("health-form-id").disabled=!!c,document.getElementById("health-form-name").value=d||"",document.getElementById("health-form-url").value=a||"",document.getElementById("health-form-timeout").value=n||1e4,document.getElementById("health-form-codes").value=e||"200",document.getElementById("health-form-sla").value=t||99.9,document.getElementById("health-form-slow").value=i||5e3,P.style.display="",$.style.display="none"}function f(){P.style.display="none",$.style.display="",O=null}$?.addEventListener("click",()=>m("","","",1e4,"200",99.9,5e3)),H?.addEventListener("click",f),x?.addEventListener("click",async()=>{const c=O||document.getElementById("health-form-id").value.trim();if(!c)return showNotification("Service ID is required","warning");const d=document.getElementById("health-form-url").value.trim();if(!d)return showNotification("URL is required","warning");const a=document.getElementById("health-form-codes").value.split(",").map(e=>parseInt(e.trim())).filter(Boolean),n={name:document.getElementById("health-form-name").value.trim()||c,url:d,timeout:parseInt(document.getElementById("health-form-timeout").value)||1e4,expectedStatusCodes:a.length?a:[200],sla:{target:parseFloat(document.getElementById("health-form-sla").value)||99.9},slowResponseThreshold:parseInt(document.getElementById("health-form-slow").value)||5e3};try{x.textContent="Saving...",x.disabled=!0;const t=await(await secureFetch(`/api/v1/health-checks/${encodeURIComponent(c)}/configure`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})).json();if(!t.success)throw new Error(t.error||"Save failed");f(),b(),E()}catch(e){showNotification("Error: "+e.message,"error")}finally{x.textContent="Save",x.disabled=!1}}),document.addEventListener("health-edit",async c=>{const d=c.detail;m(d,"","",1e4,"200",99.9,5e3)}),document.addEventListener("health-delete",async c=>{const d=c.detail;if(confirm(`Delete health check for "${d}"?`))try{const n=await(await secureFetch(`/api/v1/health-checks/${encodeURIComponent(d)}/configure`,{method:"DELETE"})).json();if(!n.success)throw new Error(n.error);b(),E()}catch(a){showNotification("Error: "+a.message,"error")}}),h?.addEventListener("click",()=>{y?.classList.add("show"),E()}),wireModal(y,L),T?.addEventListener("click",E),document.querySelector('[data-panel="health-incidents"]')?.addEventListener("click",k),document.querySelector('[data-panel="health-config"]')?.addEventListener("click",b)})(),(function(){injectModal("updates-modal",`
+
+
\u2B06\uFE0F Update Management
+
+ Check for container image updates, apply them, and manage rollbacks.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\u{1F4E6} Click "Check for Updates" to scan containers.
+
+
+
+
+
+
+
Loading update history...
+
+
+
+
+
+
+
\u{1F916} Loading auto-update configuration...
+
+
+
+
+
+
+
+
DashCaddy
+
Loading...
+
+
Update available
+
+
+
+ New version:
+
+
+
+
+
+
+
+
+
+
+
\u{1F4E6}No self-update history.
+
+
+
+
+
+
+
+
+
+
`);const y=document.getElementById("updates-modal"),h=document.getElementById("updates-btn"),L=document.getElementById("updates-cancel"),T=document.getElementById("updates-check-btn"),B=document.getElementById("updates-available-container"),N=document.getElementById("updates-history-container"),D=document.getElementById("updates-auto-container"),A=document.getElementById("updates-last-check");async function $(){try{const v=await(await fetch("/api/v1/updates/available")).json();if(!v.success)throw new Error(v.error);const o=v.updates||[];if(o.length===0){B.innerHTML='
\u2705All containers are up to date.
',A.textContent="";return}let s='
';s+='
Container
Image
Current
Latest
Actions
';for(const u of o)s+='
',s+=`
${escapeHtml(u.containerName)}
`,s+=`
${escapeHtml(u.imageName)}
`,s+=`
${escapeHtml(u.currentDigest)}
`,s+=`
${escapeHtml(u.latestDigest)}
`,s+='
',s+=``,s+=``,s+="
";s+="
",B.innerHTML=s,A.textContent=o.length+" update(s) available",B.querySelectorAll(".update-now-btn").forEach(u=>{u.addEventListener("click",async()=>{const l=u.dataset.id,g=u.dataset.name;if(confirm(`Update "${g}" to the latest version? The container will restart.`)){u.textContent="Updating...",u.disabled=!0;try{const w=await(await secureFetch(`/api/v1/updates/update/${encodeURIComponent(l)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({autoRollback:!0})})).json();if(w.success)u.textContent="Done!",u.style.background="var(--ok-fg)",setTimeout(()=>$(),2e3);else throw new Error(w.error||"Update failed")}catch(I){u.textContent="Failed",u.style.color="var(--bad-fg)",showNotification("Update error: "+I.message,"error"),setTimeout(()=>{u.textContent="Update",u.disabled=!1,u.style.color="",u.style.background=""},3e3)}}})}),B.querySelectorAll(".rollback-btn").forEach(u=>{u.addEventListener("click",async()=>{const l=u.dataset.id,g=u.dataset.name;if(confirm(`Rollback "${g}" to its previous version?`)){u.textContent="Rolling back...",u.disabled=!0;try{const w=await(await secureFetch(`/api/v1/updates/rollback/${encodeURIComponent(l)}`,{method:"POST"})).json();if(w.success)u.textContent="Rolled back!",setTimeout(()=>$(),2e3);else throw new Error(w.error||"Rollback failed")}catch(I){u.textContent="Failed",showNotification("Rollback error: "+I.message,"error"),setTimeout(()=>{u.textContent="Rollback",u.disabled=!1},3e3)}}})})}catch(p){B.innerHTML=`
Failed: ${escapeHtml(p.message)}
`}}async function P(){T.textContent="\u{1F50D} Checking...",T.disabled=!0;try{const v=await(await secureFetch("/api/v1/updates/check",{method:"POST"})).json();if(!v.success)throw new Error(v.error);T.textContent="\u2705 Done!",await $()}catch(p){T.textContent="\u274C Failed",showNotification("Check error: "+p.message,"error")}setTimeout(()=>{T.textContent="\u{1F50D} Check for Updates",T.disabled=!1},3e3)}async function C(){try{N.innerHTML='
`}}const x=document.getElementById("dashcaddy-current-version"),O=document.getElementById("dashcaddy-update-badge"),z=document.getElementById("dashcaddy-update-details"),S=document.getElementById("dashcaddy-new-version"),E=document.getElementById("dashcaddy-changelog"),k=document.getElementById("dashcaddy-apply-btn"),b=document.getElementById("dashcaddy-check-btn"),m=document.getElementById("dashcaddy-rollback-btn"),f=document.getElementById("dashcaddy-status-bar"),c=document.getElementById("dashcaddy-history-container");let d=null;function a(p,v){f&&(f.style.display="block",f.style.background=v==="error"?"var(--bad-bg)":v==="success"?"var(--ok-bg)":"var(--bg)",f.style.color=v==="error"?"var(--bad-fg)":v==="success"?"var(--ok-fg)":"var(--fg)",f.textContent=p)}async function n(){try{const v=await(await fetch("/api/v1/system/version")).json();v.success&&(x.textContent="v"+v.version+(v.commit?" ("+v.commit.substring(0,7)+")":""))}catch{x.textContent="Unable to fetch version"}}async function e(p){p||(b.textContent="Checking...",b.disabled=!0);try{const o=await(await fetch("/api/v1/system/update-check")).json();if(d=o,o.success&&o.available&&o.remote){O.style.display="",z.style.display="",S.textContent="v"+o.remote.version,E.textContent=o.remote.changelog||"No changelog available.";const s=document.getElementById("updates-btn");if(s&&!s.querySelector(".update-dot")){const l=document.createElement("span");l.className="update-dot",l.style.cssText="position:absolute;top:2px;right:2px;width:8px;height:8px;border-radius:50%;background:var(--accent);",s.style.position="relative",s.appendChild(l)}const u=document.getElementById("updates-dashcaddy-tab");if(u&&!u.querySelector(".update-dot")){const l=document.createElement("span");l.className="update-dot",l.style.cssText="display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--accent);margin-left:4px;vertical-align:middle;",u.appendChild(l)}}else O.style.display="none",z.style.display="none",p||a("You are running the latest version.","success");p||(b.textContent="Check for Updates",b.disabled=!1)}catch(v){p||(a("Failed to check: "+v.message,"error"),b.textContent="Check for Updates",b.disabled=!1)}}async function t(){if(confirm("Apply DashCaddy update? The API container will restart.")){k.textContent="Updating...",k.disabled=!0,a("Downloading and applying update...","info");try{const v=await(await secureFetch("/api/v1/system/update-apply",{method:"POST"})).json();if(v.success)a("Update initiated: v"+(v.fromVersion||"?")+" \u2192 v"+(v.toVersion||"?")+". The container will restart shortly.","success"),k.textContent="Applied!",document.querySelectorAll(".update-dot").forEach(o=>o.remove());else throw new Error(v.error||"Update failed")}catch(p){a("Update failed: "+p.message,"error"),k.textContent="Update Now",k.disabled=!1}}}async function i(){try{const v=await(await fetch("/api/v1/system/update-history")).json(),o=v.success&&v.history?v.history:[];if(o.length===0){c.innerHTML='
\u{1F4E6}No self-update history.
';return}let s='
';s+='
When
Version
From
Status
';for(const u of o){const l=u.status==="success"?"\u2713 success":u.status==="pending"?"\u23F3 pending":u.status==="partial"?"\u26A0 partial":"\u2717 "+u.status,g=u.status==="success"?"var(--ok-fg)":u.status==="pending"?"var(--muted)":"var(--bad-fg)";s+='
"}}async function r(){try{const v=await(await fetch("/api/v1/system/rollback-versions")).json(),o=v.success&&v.versions?v.versions:[];if(o.length===0){showNotification("No rollback versions available.","info");return}const s=prompt(`Available rollback versions:
+`+o.join(`
+`)+`
+
+Enter version to rollback to:`);if(!s)return;if(!o.includes(s)){showNotification("Invalid version: "+s,"error");return}if(!confirm("Rollback DashCaddy to v"+s+"? The container will restart."))return;a("Rolling back to v"+s+"...","info");const l=await(await secureFetch("/api/v1/system/rollback",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({version:s})})).json();if(l.success)a("Rollback to v"+s+" initiated. Container will restart.","success");else throw new Error(l.error||"Rollback failed")}catch(p){a("Rollback failed: "+p.message,"error")}}b?.addEventListener("click",()=>e(!1)),k?.addEventListener("click",t),m?.addEventListener("click",r),T?.addEventListener("click",P),h?.addEventListener("click",()=>{y?.classList.add("show"),$()}),wireModal(y,L),document.querySelector('[data-panel="updates-history"]')?.addEventListener("click",C),document.querySelector('[data-panel="updates-auto"]')?.addEventListener("click",H),document.querySelector('[data-panel="updates-dashcaddy"]')?.addEventListener("click",()=>{n(),i(),d||e(!0)}),setTimeout(()=>e(!0),5e3)})(),(function(){injectModal("audit-modal",`
+
+
\u{1F4DC} Audit Log
+
+ Track all actions performed through the API.
+
+
+
+
+
+
+
+
+
+
+
+
Loading audit log...
+
+
+
+
+
+
+
+
+
`);const y=document.getElementById("audit-modal"),h=document.getElementById("audit-log-btn"),L=document.getElementById("audit-cancel"),T=document.getElementById("audit-refresh-btn"),B=document.getElementById("audit-clear-btn"),N=document.getElementById("audit-filter"),D=document.getElementById("audit-log-container"),A=document.getElementById("audit-load-more");let $=0;const P=50;async function C(H){try{H||($=0,D.innerHTML='
`}}h?.addEventListener("click",()=>{y?.classList.add("show"),C(!1)}),wireModal(y,L),T?.addEventListener("click",()=>C(!1)),N?.addEventListener("change",()=>C(!1)),A?.addEventListener("click",()=>C(!0)),B?.addEventListener("click",async()=>{if(confirm("Clear the entire audit log? This cannot be undone."))try{const x=await(await secureFetch("/api/v1/audit-logs",{method:"DELETE"})).json();x.success?C(!1):showNotification("Error: "+(x.error||"Clear failed"),"error")}catch(H){showNotification("Error: "+H.message,"error")}})})(),(function(){injectModal("weather-modal",`
Weather Settings
+
+
+
Enter a city name, postal code, or “City, Country”
+
+
+
+
+
+
+
+
`);const y="weather-location",h="weather-zip",L="weather-geo",T="weather-unit";!safeGet(y)&&safeGet(h)&&safeSet(y,safeGet(h));function B(){return safeGet(T)||"imperial"}function N(){return{icon:document.querySelector(".weather-icon"),temp:document.querySelector(".weather-temp"),condition:document.querySelector(".weather-condition"),location:document.querySelector(".weather-location"),wind:document.querySelector(".weather-wind")}}const D={0:"Clear sky",1:"Mainly clear",2:"Partly cloudy",3:"Overcast",45:"Fog",48:"Rime fog",51:"Light drizzle",53:"Drizzle",55:"Dense drizzle",56:"Freezing drizzle",57:"Dense freezing drizzle",61:"Light rain",63:"Moderate rain",65:"Heavy rain",66:"Light freezing rain",67:"Heavy freezing rain",71:"Light snow",73:"Moderate snow",75:"Heavy snow",77:"Snow grains",80:"Light showers",81:"Moderate showers",82:"Violent showers",85:"Light snow showers",86:"Heavy snow showers",95:"Thunderstorm",96:"Thunderstorm with hail",99:"Severe thunderstorm"},A={0:"\u2600\uFE0F",1:"\u{1F324}\uFE0F",2:"\u26C5",3:"\u2601\uFE0F",45:"\u{1F32B}\uFE0F",48:"\u{1F32B}\uFE0F",51:"\u{1F326}\uFE0F",53:"\u{1F326}\uFE0F",55:"\u{1F326}\uFE0F",56:"\u{1F328}\uFE0F",57:"\u{1F328}\uFE0F",61:"\u{1F326}\uFE0F",63:"\u{1F327}\uFE0F",65:"\u{1F327}\uFE0F",66:"\u{1F328}\uFE0F",67:"\u{1F328}\uFE0F",71:"\u{1F328}\uFE0F",73:"\u2744\uFE0F",75:"\u2744\uFE0F",77:"\u2744\uFE0F",80:"\u{1F326}\uFE0F",81:"\u{1F327}\uFE0F",82:"\u{1F327}\uFE0F",85:"\u{1F328}\uFE0F",86:"\u2744\uFE0F",95:"\u26C8\uFE0F",96:"\u26C8\uFE0F",99:"\u26C8\uFE0F"},$=["N","NNE","NE","ENE","E","ESE","SE","SSE","S","SSW","SW","WSW","W","WNW","NW","NNW"];function P(E){return $[Math.round(E/22.5)%16]}async function C(E){const k=safeGet(L);if(k)try{const d=JSON.parse(k);if(d.query===E)return d}catch{}const b=await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(E)}&count=1&language=en&format=json`);if(!b.ok)throw new Error("Geocoding failed");const m=await b.json();if(!m.results||!m.results.length)throw new Error("Location not found");const f=m.results[0],c={query:E,lat:f.latitude,lon:f.longitude,city:f.name,state:f.admin1||"",country:f.country||"",countryCode:f.country_code||""};return safeSet(L,JSON.stringify(c)),c}function H(E){return E.countryCode==="US"&&E.state?`${E.city}, ${E.state}`:E.country?`${E.city}, ${E.country}`:E.city}async function x(E){try{const k=await C(E),b=B(),m=b==="metric"?"celsius":"fahrenheit",f=b==="metric"?"kmh":"mph",c=`https://api.open-meteo.com/v1/forecast?latitude=${k.lat}&longitude=${k.lon}¤t=temperature_2m,weather_code,wind_speed_10m,wind_direction_10m&temperature_unit=${m}&wind_speed_unit=${f}`,d=await fetch(c);if(!d.ok)throw new Error("Weather fetch failed");const n=(await d.json()).current,e=n.weather_code;return{temp:Math.round(n.temperature_2m),condition:D[e]||"Unknown",icon:A[e]||"\u{1F324}\uFE0F",locationStr:H(k),windSpeed:Math.round(n.wind_speed_10m),windDir:P(n.wind_direction_10m),unit:b}}catch(k){return console.warn("Weather fetch failed:",k),null}}async function O(){const E=N();if(!E.icon||!E.temp||!E.condition||!E.location||!E.wind){console.warn("Weather widget elements not found");return}const k=safeGet(y);if(!k){E.location.textContent="Set Location",E.temp.textContent="--\xB0",E.condition.textContent="Click \u2699\uFE0F to configure",E.wind.textContent="--",E.icon.innerHTML='\u{1F324}\uFE0F';return}try{const b=await x(k);if(b){const m=b.unit==="metric"?"\xB0C":"\xB0F",f=b.unit==="metric"?"km/h":"mph";E.location.textContent=b.locationStr,E.temp.textContent=`${b.temp}${m}`,E.condition.textContent=b.condition,E.wind.textContent=`Wind: ${b.windSpeed} ${f} ${b.windDir}`,E.icon.innerHTML=`${escapeHtml(b.icon)}`}}catch(b){console.error("Weather update error:",b),E.location.textContent="Weather Error",E.temp.textContent="Error",E.condition.textContent="Failed to load",E.wind.textContent="--"}}const z=document.getElementById("weather-modal"),S=document.getElementById("weather-location-input");document.getElementById("weather-settings")?.addEventListener("click",()=>{S.value=safeGet(y)||"";const E=B(),k=z.querySelector(`input[name="weather-unit-radio"][value="${E}"]`);k&&(k.checked=!0),z.classList.add("show"),S.focus()}),document.getElementById("weather-cancel")?.addEventListener("click",()=>{z.classList.remove("show")}),document.getElementById("weather-save")?.addEventListener("click",()=>{const E=S.value.trim();if(E){safeGet(y)!==E&&safeSet(L,""),safeSet(y,E);const b=z.querySelector('input[name="weather-unit-radio"]:checked'),m=b?b.value:"imperial",f=B();safeSet(T,m),f!==m&&safeSet(L,""),z.classList.remove("show"),O()}else showNotification("Please enter a location (e.g., Hamburg, London, 90210)","warning")}),wireModal(z),document.addEventListener("keydown",E=>{E.key==="Escape"&&z.classList.contains("show")&&z.classList.remove("show")}),O(),setInterval(O,DC.POLL.WEATHER)})(),(function(){const y=document.getElementById("clock-widget"),h=document.getElementById("clock-render");if(!y||!h)return;const L=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],T=["January","February","March","April","May","June","July","August","September","October","November","December"],B=["XII","I","II","III","IV","V","VI","VII","VIII","IX","X","XI"];let N=safeGet("clock-style")||"default",D=-1,A=!1,$="",P="",C=null,H=null;function x(o){if(A||safeGet("clock-chimes")!=="true")return;A=!0;const s=parseInt(safeGet("clock-chime-volume")||"50",10)/100;let u=0;function l(){if(u>=o){A=!1;return}const g=new Audio("/assets/sounds/church-bell.mp3");g.volume=s,g.play().catch(()=>{}),u++,u{A=!1},2500)}l()}function O(o){return L[o.getDay()]+", "+T[o.getMonth()]+" "+o.getDate()+", "+o.getFullYear()}function z(){P="",C=null}function S(){return P!=="digital"&&(h.innerHTML='
Every app you deploy appears here as a card. Each one shows:
+
+
Status dot — green = online, red = offline (pulses when down)
+
ON/OFF badge — current state at a glance
+
Response time — how fast the service responds (color-coded)
+
Uptime bar — historical uptime percentage
+
+
Hover over a card to see action buttons: Logs, Restart, Update, Settings, and Open.
+ `,position:"top",showButtons:["previous","next"],showProgress:!0},priority:3},{id:"app-selector",element:"#add-service-btn",popover:{title:"App Selector — Deploy in One Click",description:`
+
Browse 50+ self-hosted apps organized by category:
+
+
Media (Plex, Jellyfin, Emby, Overseerr)
+
Downloads (*arr stack, qBittorrent, Transmission)
+
Productivity (Nextcloud, Vaultwarden, Gitea)
+
Monitoring (Uptime Kuma, Grafana, Prometheus)
+
+
Each app deploys with Docker, Caddy reverse proxy, and DNS — fully configured automatically.
+
+ ★ Premium:Recipes let you deploy entire stacks (e.g., full media server) in one click with pre-wired configs.
+
After changing Caddy configuration (adding reverse proxy rules, SSL settings, etc.), click here to apply the changes live.
+
This is a graceful reload — existing connections are not dropped.
+ `,position:"bottom",align:"end",showButtons:["previous","next"],showProgress:!0},priority:12,condition:()=>document.getElementById("reload-caddy-top")!==null},{id:"tour-complete",element:"#brand",popover:{title:"You're All Set!",description:`
+
That covers the essentials. A few tips to get the most out of DashCaddy:
+
+
Click any card to open that service directly
+
Keyboard shortcuts — press ? anytime to see them all
+
DashCA — visit your CA page to install the root certificate on any device
+
+
+
★ Unlock Premium
+
Premium adds powerful features for serious homelabbers:
+
+
Auto-Login SSO — sign into every app automatically, no more password juggling
+
Recipes — deploy full stacks (media server, dev environment) in one click
+
Docker Swarm — orchestrate multi-node clusters from your dashboard
+
+
Activate in Admin → License
+
+
You can restart this tour anytime from Admin → Help Tour.
+ `,position:"bottom",align:"start",showButtons:["previous","close"],showProgress:!0},priority:13}];function getTooltipDefinitions(){return TOOLTIP_DEFINITIONS}function getTooltipById(v){return TOOLTIP_DEFINITIONS.find(T=>T.id===v)||null}function getActiveTooltips(){return TOOLTIP_DEFINITIONS.filter(v=>{if(v.condition&&typeof v.condition=="function")try{return v.condition()}catch(T){return console.error(`[TooltipDefinitions] Error evaluating condition for ${v.id}:`,T),!1}return!0})}function getSortedTooltips(){return getActiveTooltips().sort((T,P)=>{const e=T.priority||999,t=P.priority||999;return e-t})}function getNewFeatureTooltips(){return getActiveTooltips().filter(T=>T.isNewFeature===!0).sort((T,P)=>{const e=T.priority||999,t=P.priority||999;return e-t})}window.TooltipDefinitions={TOOLTIP_DEFINITIONS,getTooltipDefinitions,getTooltipById,getActiveTooltips,getSortedTooltips,getNewFeatureTooltips},console.log("[TooltipDefinitions] Definitions loaded:",TOOLTIP_DEFINITIONS.length,"tooltips"),(function(v){"use strict";class T{constructor(e){this.progressTracker=e,this.modal=null,this.onTemplateSelected=null,console.log("[DnsTemplateSelector] Module loaded")}getDnsTemplates(){return[{id:"technitium",name:"Technitium DNS Server",description:"Modern DNS server with web UI for managing private zones",icon:"\u{1F310}",difficulty:"Easy",features:["Web-based management interface","Private zone management for .sami domain","DHCP server integration","DNS-over-HTTPS and DNS-over-TLS support"],recommended:!0},{id:"bind9",name:"BIND9 DNS Server",description:"Industry-standard DNS server - powerful and flexible",icon:"\u{1F527}",difficulty:"Advanced",features:["Industry standard DNS server","Full RFC compliance","Advanced zone management","DNSSEC support"],recommended:!1},{id:"pihole",name:"Pi-hole",description:"Network-wide ad blocker with DNS capabilities",icon:"\u{1F6E1}\uFE0F",difficulty:"Intermediate",features:["Ad blocking at DNS level","Web interface for management","DHCP server included","Query logging and statistics"],recommended:!1},{id:"powerdns",name:"PowerDNS",description:"High-performance DNS server with SQL backend",icon:"\u26A1",difficulty:"Intermediate",features:["SQL database backend","RESTful API for automation","Geographic load balancing","DNSSEC support"],recommended:!1},{id:"coredns",name:"CoreDNS",description:"Cloud-native DNS server - lightweight and flexible",icon:"\u2601\uFE0F",difficulty:"Intermediate",features:["Plugin-based architecture","Kubernetes-native","Lightweight and fast","Prometheus metrics"],recommended:!1}]}showTemplateSelector(){this.modal||this.createModal(),this.populateTemplates(),this.modal.style.display="flex",document.body.style.overflow="hidden"}createModal(){const e=document.createElement("div");e.id="dns-template-modal",e.className="dns-template-modal",e.innerHTML=`
+
+
+
\u{1F310} Choose a DNS Server
+
Setting up a DNS server is essential for managing your private .sami domain
+
+ `,t.querySelector(".dns-template-select-btn").addEventListener("click",()=>this.handleTemplateSelection(e)),t}handleTemplateSelection(e){console.log(`[DnsTemplateSelector] Template selected: ${e.id}`),this.close(),this.onTemplateSelected?this.onTemplateSelected(e):this.openAppSelector(e.id)}handleSetupLater(){console.log("[DnsTemplateSelector] DNS setup deferred"),this.progressTracker&&this.progressTracker.markDnsSetupDeferred(),this.close(),this.showNotification("DNS setup deferred. You can set it up later from the App Selector.")}openAppSelector(e){const t=document.querySelector('[onclick*="showAppSelector"]');t?(t.click(),setTimeout(()=>{const a=document.querySelector("#app-search");a&&(a.value=e,a.dispatchEvent(new Event("input",{bubbles:!0})))},300)):this.showNotification(`To deploy ${e}, use the App Selector and search for "${e}"`)}showNotification(e){const t=document.createElement("div");t.className="dns-template-notification",t.textContent=e,t.style.cssText=`
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ background: var(--card-base);
+ color: var(--fg);
+ padding: 15px 20px;
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
+ z-index: 10001;
+ max-width: 300px;
+ `,document.body.appendChild(t),setTimeout(()=>{t.style.opacity="0",t.style.transition="opacity 0.3s",setTimeout(()=>t.remove(),300)},3e3)}close(){this.modal&&(this.modal.style.display="none",document.body.style.overflow="")}}v.DnsTemplateSelector=T,console.log("[DnsTemplateSelector] Module loaded")})(window),(function(v){"use strict";class T{constructor(e,t,a){this.progressTracker=e,this.themeAdapter=t,this.dnsTemplateSelector=a,this.driver=null,this.currentStepIndex=0,this.isActive=!1,this.resizeHandler=null,this.layoutChangeHandler=null}async initializeDriver(){const e=v.driver?.js?.driver||v.driver?.driver||v.driver;if(typeof e!="function")return console.error("[TourManager] Driver.js not loaded or invalid. window.driver:",v.driver),!1;const t=this.themeAdapter.getDriverTheme();return this.driver=e({showProgress:!0,showButtons:["next","previous","close"],allowClose:!0,overlayClickNext:!1,overlayOpacity:.6,stagePadding:12,stageRadius:12,allowKeyboardControl:!0,popoverClass:"dashcaddy-popover",animate:!0,smoothScroll:!0,onDestroyed:()=>this.onTourComplete(),onDestroyStarted:()=>{this.progressTracker.isTourCompleted()||this.onTourSkip()}}),this.themeAdapter.applyTheme(this.driver),this.themeAdapter.onThemeChange(()=>{this.themeAdapter.applyTheme(this.driver)}),this.setupDynamicRepositioning(),!0}shouldAutoStart(){return!this.progressTracker.isInstallOnboardingCompleted()&&!this.progressTracker.isTourCompleted()&&this.progressTracker.getCurrentStep()===0}async startTour(){if(!this.driver&&!await this.initializeDriver())return;const e=v.TooltipDefinitions.getSortedTooltips(),t=this.progressTracker.getCompletedTooltips(),a=e.filter(y=>!t.includes(y.id));if(a.length===0){console.log("[TourManager] No tooltips to show"),this.progressTracker.markTourCompleted();return}const p=a.map((y,m)=>{const A=m===0,L=m===a.length-1,E={element:y.element,popover:{title:y.popover.title,description:y.popover.description,side:y.popover.position||"bottom",align:y.popover.align||"start",showButtons:this._getButtonsForStep(y,A,L),showProgress:y.popover.showProgress!==!1,onNextClick:()=>{this.progressTracker.markTooltipCompleted(y.id),this.progressTracker.setCurrentStep(m+1),this.currentStepIndex=m+1,this.driver.moveNext()},onPrevClick:()=>{this.progressTracker.setCurrentStep(Math.max(0,m-1)),this.currentStepIndex=Math.max(0,m-1),this.driver.movePrevious()},onCloseClick:()=>{this.skipTour()}}};return y.id==="dns-priority"&&this.dnsTemplateSelector&&(E.popover.onSetupNowClick=()=>{console.log("[TourManager] Opening DNS template selector"),this.dnsTemplateSelector.showTemplateSelector(),this.progressTracker.markTooltipCompleted(y.id),this.progressTracker.setCurrentStep(m+1),this.currentStepIndex=m+1,this.driver.moveNext()},E.popover.onLaterClick=()=>{console.log("[TourManager] DNS setup deferred"),this.progressTracker.markDnsSetupDeferred(),this.progressTracker.markTooltipCompleted(y.id),this.progressTracker.setCurrentStep(m+1),this.currentStepIndex=m+1,this.driver.moveNext()}),E});this.isActive=!0,this.driver.setSteps(p),this.driver.drive()}async resumeTour(){this.progressTracker.getCurrentStep()>0?await this.startTour():await this.startTour()}skipTour(){this.driver&&this.driver.destroy(),this.cleanupDynamicRepositioning(),this.isActive=!1}async restartTour(){this.progressTracker.resetProgress(),await this.startTour()}async showTooltip(e){const t=v.TooltipDefinitions.getTooltipById(e);if(!t){console.error(`[TourManager] Tooltip not found: ${e}`);return}this.driver||await this.initializeDriver();const a={element:t.element,popover:{title:t.popover.title,description:t.popover.description,side:t.popover.position||"bottom",align:t.popover.align||"start"}};this.driver.highlight(a)}async showWhatsNew(){if(!this.driver&&!await this.initializeDriver())return;const e=v.TooltipDefinitions.getNewFeatureTooltips();if(e.length===0){console.log("[TourManager] No new features to show");return}console.log(`[TourManager] Showing ${e.length} new features`);const t=e.map((a,p)=>{const y=p===0,m=p===e.length-1;return{element:a.element,popover:{title:`\u2728 NEW: ${a.popover.title}`,description:a.popover.description,side:a.popover.position||"bottom",align:a.popover.align||"start",showButtons:this._getButtonsForStep(a,y,m),showProgress:!0,onNextClick:()=>{this.driver.moveNext()},onPrevClick:()=>{this.driver.movePrevious()},onCloseClick:()=>{this.skipTour()}}}});this.isActive=!0,this.driver.setSteps(t),this.driver.drive()}setupDynamicRepositioning(){let e;this.resizeHandler=()=>{clearTimeout(e),e=setTimeout(()=>{this.isActive&&this.driver&&(console.log("[TourManager] Window resized, repositioning tooltip"),this.driver.refresh())},150)},this.layoutChangeHandler=()=>{this.isActive&&this.driver&&(console.log("[TourManager] Layout changed, repositioning tooltip"),setTimeout(()=>{this.driver&&this.driver.refresh()},100))},v.addEventListener("resize",this.resizeHandler),this.themeAdapter.onThemeChange(this.layoutChangeHandler)}cleanupDynamicRepositioning(){this.resizeHandler&&v.removeEventListener("resize",this.resizeHandler)}_getButtonsForStep(e,t,a){if(e.popover.showButtons)return e.popover.showButtons;const p=[];return t||p.push("previous"),a?p.push("close"):p.push("next"),p}onTourComplete(){this.progressTracker.markTourCompleted(),this.isActive=!1,console.log("[TourManager] Tour completed")}onTourSkip(){console.log("[TourManager] Tour skipped"),this.isActive=!1}}v.TourManager=T,console.log("[TourManager] Module loaded")})(window),(function(){"use strict";let v,T,P,e,t;async function a(){try{if(console.log("[Onboarding] Initializing system..."),window.__dashcaddySiteConfigLoaded)try{await window.__dashcaddySiteConfigLoaded}catch{}if(t=new ErrorHandler,console.log("[Onboarding] Error Handler initialized"),v=new ProgressTracker("dashcaddy_onboarding"),console.log("[Onboarding] Progress Tracker initialized"),T=new ThemeAdapter,console.log("[Onboarding] Theme Adapter initialized"),e=new DnsTemplateSelector(v),console.log("[Onboarding] DNS Template Selector initialized"),P=new TourManager(v,T,e),console.log("[Onboarding] Tour Manager initialized"),P.shouldAutoStart())console.log("[Onboarding] Auto-starting tour for first-time install"),await v.markInstallOnboardingCompleted(),setTimeout(()=>{P.startTour()},1e3);else{const A=v.isTourCompleted(),L=v.getCurrentStep();console.log(`[Onboarding] Tour not auto-starting (completed: ${A}, step: ${L})`),!A&&L>0&&console.log("[Onboarding] Tour in progress, can be resumed manually")}p(),window.DashCaddyOnboarding={startTour:()=>P.startTour(),restartTour:()=>P.restartTour(),showTooltip:A=>P.showTooltip(A),showWhatsNew:()=>P.showWhatsNew(),resetProgress:()=>v.resetProgress(),getErrors:()=>t.getErrors(),getErrorStats:()=>t.getStatistics()},console.log("[Onboarding] System initialized successfully")}catch(A){console.error("[Onboarding] Initialization error:",A),t&&t.logError("Initialization",A),console.warn("[Onboarding] System failed to initialize, dashboard will continue without onboarding")}}function p(){const A=document.querySelector(".tools-primary")||document.querySelector(".tools");if(!A)return;const L=()=>{P?(console.log("[Onboarding] Starting tour via button click"),P.restartTour()):(console.error("[Onboarding] Tour manager not initialized"),alert(`Tour is not available. Check browser console for errors.
+
+Possible issues:
+- Driver.js library failed to load
+- JavaScript errors during initialization`))},E=document.getElementById("restart-tour-btn");if(E){E.onclick=L;return}const d=document.createElement("button");d.id="restart-tour-btn",d.textContent="Help Tour",d.title="Restart the onboarding tour",d.onclick=L,A.appendChild(d)}function y(){return typeof(window.driver?.js?.driver||window.driver?.driver||window.driver)!="function"?(console.warn("[Onboarding] Driver.js not loaded yet, will retry... window.driver:",window.driver),!1):!0}function m(){let A=0;const L=10;function E(){y()?a():(A++,A