diff --git a/status/build.js b/status/build.js index 8713f13..4248cc9 100644 --- a/status/build.js +++ b/status/build.js @@ -11,6 +11,10 @@ const SW_JS = path.join(__dirname, 'sw.js'); // Bundle definitions — files are concatenated in order, then minified const bundles = { 'core.js': [ + // error-handler.js MUST be first — globals.js below does + // `const errorHandler = new ErrorHandler()` at top level, which throws + // ReferenceError if the ErrorHandler class isn't already on `window`. + JS('error-handler.js'), JS('globals.js'), JS('skeleton-loader.js'), JS('theme.js'), @@ -57,7 +61,8 @@ const bundles = { ], 'onboarding.js': [ JS('driver.min.js'), - JS('error-handler.js'), + // error-handler.js moved to core.js bundle; window.ErrorHandler is already + // set before this bundle runs. JS('progress-tracker.js'), JS('theme-adapter.js'), JS('tooltip-definitions.js'), diff --git a/status/dist/core.js b/status/dist/core.js index c7c4c1c..1f8d317 100644 --- a/status/dist/core.js +++ b/status/dist/core.js @@ -1,23 +1,41 @@ -const DC={NAME:"DashCaddy",POLL:{DASHBOARD:1e4,LOGS:3e3,STATS:5e3,WEATHER:6e5,HEALTH:1e3,DEPLOY_SSL:5e3},DELAYS:{BTN_RESET:2e3,RELOAD:5e3,MODAL_CLOSE:500,PORT_CHECK:500,DEPLOY_INIT:3e3},DEFAULTS:{DNS_PORT:"5380",SERVICE_PORT:"8080",TTL:300,CADDYFILE:"C:\\caddy\\Caddyfile"}},errorHandler=new ErrorHandler,_cachedCfg=JSON.parse(localStorage.getItem("dashcaddy_site_config")||"null"),SITE={tld:_cachedCfg&&_cachedCfg.tld||".home",dnsIp:"",dnsPort:DC.DEFAULTS.DNS_PORT,dnsServers:{},configurationType:_cachedCfg&&_cachedCfg.configurationType||"homelab",domain:_cachedCfg&&_cachedCfg.domain||"",defaults:_cachedCfg&&_cachedCfg.defaults||{},routingMode:_cachedCfg&&_cachedCfg.routingMode||"subdomain",onboardingCompleted:!1};window.__dashcaddySiteConfigLoaded=(async function(){try{const w=await fetch("/api/v1/config");if(w.ok){const v=await w.json();if(v.tld&&(SITE.tld=v.tld.startsWith(".")?v.tld:"."+v.tld),v.dns&&(SITE.dnsIp=v.dns.ip||"",SITE.dnsPort=v.dns.port||DC.DEFAULTS.DNS_PORT),v.dnsServers&&typeof v.dnsServers=="object")for(const[h,n]of Object.entries(v.dnsServers))h!=="__proto__"&&h!=="constructor"&&h!=="prototype"&&(SITE.dnsServers[h]=n);v.configurationType&&(SITE.configurationType=v.configurationType),v.domain&&(SITE.domain=v.domain),v.defaults&&(SITE.defaults=v.defaults),v.routingMode&&(SITE.routingMode=v.routingMode),SITE.onboardingCompleted=v.onboardingCompleted===!0,localStorage.setItem("dashcaddy_site_config",JSON.stringify({tld:SITE.tld,configurationType:SITE.configurationType,domain:SITE.domain,routingMode:SITE.routingMode})),renderDnsCards();const r=document.getElementById("manage-tokens");r&&(r.style.display=Object.keys(SITE.dnsServers).length?"":"none")}}catch{}document.querySelectorAll("[data-tld]").forEach(w=>w.textContent=SITE.tld);const m=document.getElementById("edit-tld-suffix");m&&(m.textContent=SITE.tld);const l=document.getElementById("external-proxy-ip");l&&SITE.dnsIp&&(l.value=SITE.dnsIp,l.placeholder=SITE.dnsIp)})();function buildDomain(o){return o+SITE.tld}function buildServiceUrl(o){return SITE.routingMode==="subdirectory"&&SITE.domain?"https://"+SITE.domain+"/"+o:SITE.configurationType==="public"&&SITE.domain?"https://"+o+"."+SITE.domain:"https://"+buildDomain(o)}function getDnsServerAddr(o){const m=SITE.dnsServers[o];return m?`${m.ip}:${m.port}`:buildDomain(o)}function getPrimaryDnsId(){if(!SITE.dnsIp)return null;for(const[o,m]of Object.entries(SITE.dnsServers))if(m.ip===SITE.dnsIp)return o;return null}function renderDnsCards(){const o=document.querySelector(".top");if(!o)return;const m=Object.keys(SITE.dnsServers);if(!m.length)return;const l='',w=o.firstElementChild;m.forEach(v=>{const r=escapeHtml(v),h=escapeHtml((SITE.dnsServers[v].name||v).toUpperCase()),n=document.createElement("div");n.className="card",n.setAttribute("data-app",v),n.setAttribute("data-status","off"),n.innerHTML=`
${l}
${h}OFF
--
--
`,o.insertBefore(n,w)})}window.renderDnsCards=renderDnsCards;let csrfToken=null;async function getCSRFToken(){if(csrfToken)return csrfToken;try{const o=await fetch("/api/v1/csrf-token");if(!o.ok)throw new Error("Failed to fetch CSRF token");return csrfToken=(await o.json()).token,csrfToken}catch(o){throw errorHandler.logError("[CSRF] Get Token",o,{function:"getCSRFToken"}),o}}async function secureFetch(o,m={}){const l=(m.method||"GET").toUpperCase(),w=!["GET","HEAD","OPTIONS"].includes(l);if(w)try{const r=await getCSRFToken();m.headers={...m.headers,"X-CSRF-Token":r}}catch(r){errorHandler.logError("[CSRF] Add to Request",r,{function:"secureFetch"})}m.signal||(m={...m,signal:AbortSignal.timeout(15e3)});const v=await fetch(o,m);if(w&&v.status===403)try{const r=await v.clone().json();if(r.error&&(r.error.includes("DC-100")||r.error.includes("DC-101"))){csrfToken=null;const h=await getCSRFToken();return m.headers={...m.headers,"X-CSRF-Token":h},m.signal=AbortSignal.timeout(15e3),fetch(o,m)}}catch{}return v}async function postJSON(o,m){const l=await secureFetch(o,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(m)}),w=await l.json();if(!l.ok||w.success===!1)throw new Error(w.error||`Request failed (${l.status})`);return w}async function getJSON(o){const m=await secureFetch(o);if(!m.ok){let l=`Request failed (${m.status})`;try{l=(await m.json()).error||l}catch{}throw new Error(l)}return m.json()}async function deleteAPI(o){const m=await secureFetch(o,{method:"DELETE"}),l=await m.json();if(!m.ok||l.success===!1)throw new Error(l.error||`Delete failed (${m.status})`);return l}async function withButton(o,m,l,w={}){const v=o.innerHTML,{successText:r="\u2705",resetDelay:h=DC.DELAYS.BTN_RESET}=w;o.disabled=!0,o.innerHTML=m;try{const n=await l();return o.innerHTML=r,setTimeout(()=>{o.innerHTML=v,o.disabled=!1},h),n}catch(n){throw o.innerHTML=v,o.disabled=!1,n}}function openModal(o){document.getElementById(o)?.classList.add("show")}function closeModal(o){document.getElementById(o)?.classList.remove("show")}function wireModal(o,...m){o&&(o.addEventListener("click",l=>{l.target===o&&o.classList.remove("show")}),m.forEach(l=>l?.addEventListener("click",()=>o.classList.remove("show"))))}function showNotification(o,m="info",l=3e3){const w=document.querySelector(".deploy-notification");w&&w.remove();const v={info:{bg:"#2196F3",fg:"#fff"},success:{bg:"var(--ok-bg)",fg:"var(--ok-fg)"},error:{bg:"#f44336",fg:"#fff"},warning:{bg:"#ff9800",fg:"#fff"}},r=v[m]||v.info,h=document.createElement("div");h.className="deploy-notification",h.textContent=o,h.style.cssText=` +(function(o){"use strict";class f{constructor(){this.errors=[],this.maxErrors=50}logError(g,i,s={}){const h={timestamp:new Date().toISOString(),context:g,message:i instanceof Error?i.message:i,stack:i instanceof Error?i.stack:null,metadata:s};this.errors.push(h),this.errors.length>this.maxErrors&&this.errors.shift(),console.error(`[Onboarding Error] ${g}:`,i,s)}recoverFromError(g,i){switch(this.classifyError(g)){case"ELEMENT_NOT_FOUND":return this.logError("Element Not Found",g,{currentStep:i}),{action:"SKIP_STEP",nextStep:i+1,message:"Target element not found, skipping to next step"};case"STORAGE_UNAVAILABLE":return this.logError("Storage Unavailable",g),{action:"USE_MEMORY_STORAGE",message:"Local storage unavailable, using in-memory storage"};case"DRIVER_NOT_LOADED":return this.logError("Driver.js Not Loaded",g),{action:"ABORT_TOUR",message:"Driver.js library not loaded, cannot start tour"};case"INVALID_TOOLTIP":return this.logError("Invalid Tooltip Configuration",g,{currentStep:i}),{action:"SKIP_STEP",nextStep:i+1,message:"Invalid tooltip configuration, skipping"};case"THEME_DETECTION_FAILED":return this.logError("Theme Detection Failed",g),{action:"USE_DEFAULT_THEME",message:"Using default dark theme"};default:return this.logError("Unknown Error",g,{currentStep:i}),{action:"ABORT_TOUR",message:"Unexpected error occurred, aborting tour"}}}classifyError(g){const i=g.message||g.toString();return i.includes("element")&&i.includes("not found")?"ELEMENT_NOT_FOUND":i.includes("storage")||i.includes("quota")?"STORAGE_UNAVAILABLE":i.includes("driver")||i.includes("undefined")?"DRIVER_NOT_LOADED":i.includes("invalid")||i.includes("validation")?"INVALID_TOOLTIP":i.includes("theme")?"THEME_DETECTION_FAILED":"UNKNOWN"}getErrors(){return[...this.errors]}clearErrors(){this.errors=[]}getStatistics(){const g={total:this.errors.length,byContext:{},byType:{},recent:this.errors.slice(-10)};return this.errors.forEach(i=>{g.byContext[i.context]=(g.byContext[i.context]||0)+1;const s=this.classifyError({message:i.message});g.byType[s]=(g.byType[s]||0)+1}),g}handleDriverLoadFailure(){this.logError("Driver.js Load Failure","Driver.js library failed to load");const g=document.createElement("div");return g.id="onboarding-fallback",g.style.cssText=` + position: fixed; + bottom: 20px; + right: 20px; + background: var(--card-base, #2a2a2a); + color: var(--fg, #ffffff); + padding: 15px 20px; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0,0,0,0.3); + z-index: 9999; + max-width: 300px; + font-size: 14px; + `,g.innerHTML=` + Welcome to DashCaddy!
+

+ The interactive tour is unavailable, but you can explore the dashboard freely. + Check the documentation for help getting started. +

+ `,document.body.appendChild(g),setTimeout(()=>{g.parentNode&&g.parentNode.removeChild(g)},1e4),!0}handleStorageUnavailable(){this.logError("Storage Unavailable","Local storage is not available");const g={data:{},getItem(i){return this.data[i]||null},setItem(i,s){this.data[i]=s},removeItem(i){delete this.data[i]},clear(){this.data={}}};return console.warn("[ErrorHandler] Using in-memory storage - progress will not persist"),g}sendToErrorTracking(g){}}o.ErrorHandler=f,console.log("[ErrorHandler] Module loaded")})(window);const DC={NAME:"DashCaddy",POLL:{DASHBOARD:1e4,LOGS:3e3,STATS:5e3,WEATHER:6e5,HEALTH:1e3,DEPLOY_SSL:5e3},DELAYS:{BTN_RESET:2e3,RELOAD:5e3,MODAL_CLOSE:500,PORT_CHECK:500,DEPLOY_INIT:3e3},DEFAULTS:{DNS_PORT:"5380",SERVICE_PORT:"8080",TTL:300,CADDYFILE:"C:\\caddy\\Caddyfile"}},errorHandler=new ErrorHandler,_cachedCfg=JSON.parse(localStorage.getItem("dashcaddy_site_config")||"null"),SITE={tld:_cachedCfg&&_cachedCfg.tld||".home",dnsIp:"",dnsPort:DC.DEFAULTS.DNS_PORT,dnsServers:{},configurationType:_cachedCfg&&_cachedCfg.configurationType||"homelab",domain:_cachedCfg&&_cachedCfg.domain||"",defaults:_cachedCfg&&_cachedCfg.defaults||{},routingMode:_cachedCfg&&_cachedCfg.routingMode||"subdomain",onboardingCompleted:!1};window.__dashcaddySiteConfigLoaded=(async function(){try{const g=await fetch("/api/v1/config");if(g.ok){const i=await g.json();if(i.tld&&(SITE.tld=i.tld.startsWith(".")?i.tld:"."+i.tld),i.dns&&(SITE.dnsIp=i.dns.ip||"",SITE.dnsPort=i.dns.port||DC.DEFAULTS.DNS_PORT),i.dnsServers&&typeof i.dnsServers=="object")for(const[h,n]of Object.entries(i.dnsServers))h!=="__proto__"&&h!=="constructor"&&h!=="prototype"&&(SITE.dnsServers[h]=n);i.configurationType&&(SITE.configurationType=i.configurationType),i.domain&&(SITE.domain=i.domain),i.defaults&&(SITE.defaults=i.defaults),i.routingMode&&(SITE.routingMode=i.routingMode),SITE.onboardingCompleted=i.onboardingCompleted===!0,localStorage.setItem("dashcaddy_site_config",JSON.stringify({tld:SITE.tld,configurationType:SITE.configurationType,domain:SITE.domain,routingMode:SITE.routingMode})),renderDnsCards();const s=document.getElementById("manage-tokens");s&&(s.style.display=Object.keys(SITE.dnsServers).length?"":"none")}}catch{}document.querySelectorAll("[data-tld]").forEach(g=>g.textContent=SITE.tld);const f=document.getElementById("edit-tld-suffix");f&&(f.textContent=SITE.tld);const u=document.getElementById("external-proxy-ip");u&&SITE.dnsIp&&(u.value=SITE.dnsIp,u.placeholder=SITE.dnsIp)})();function buildDomain(o){return o+SITE.tld}function buildServiceUrl(o){return SITE.routingMode==="subdirectory"&&SITE.domain?"https://"+SITE.domain+"/"+o:SITE.configurationType==="public"&&SITE.domain?"https://"+o+"."+SITE.domain:"https://"+buildDomain(o)}function getDnsServerAddr(o){const f=SITE.dnsServers[o];return f?`${f.ip}:${f.port}`:buildDomain(o)}function getPrimaryDnsId(){if(!SITE.dnsIp)return null;for(const[o,f]of Object.entries(SITE.dnsServers))if(f.ip===SITE.dnsIp)return o;return null}function renderDnsCards(){const o=document.querySelector(".top");if(!o)return;const f=Object.keys(SITE.dnsServers);if(!f.length)return;const u='',g=o.firstElementChild;f.forEach(i=>{const s=escapeHtml(i),h=escapeHtml((SITE.dnsServers[i].name||i).toUpperCase()),n=document.createElement("div");n.className="card",n.setAttribute("data-app",i),n.setAttribute("data-status","off"),n.innerHTML=`
${u}
${h}OFF
--
--
`,o.insertBefore(n,g)})}window.renderDnsCards=renderDnsCards;let csrfToken=null;async function getCSRFToken(){if(csrfToken)return csrfToken;try{const o=await fetch("/api/v1/csrf-token");if(!o.ok)throw new Error("Failed to fetch CSRF token");return csrfToken=(await o.json()).token,csrfToken}catch(o){throw errorHandler.logError("[CSRF] Get Token",o,{function:"getCSRFToken"}),o}}async function secureFetch(o,f={}){const u=(f.method||"GET").toUpperCase(),g=!["GET","HEAD","OPTIONS"].includes(u);if(g)try{const s=await getCSRFToken();f.headers={...f.headers,"X-CSRF-Token":s}}catch(s){errorHandler.logError("[CSRF] Add to Request",s,{function:"secureFetch"})}f.signal||(f={...f,signal:AbortSignal.timeout(15e3)});const i=await fetch(o,f);if(g&&i.status===403)try{const s=await i.clone().json();if(s.error&&(s.error.includes("DC-100")||s.error.includes("DC-101"))){csrfToken=null;const h=await getCSRFToken();return f.headers={...f.headers,"X-CSRF-Token":h},f.signal=AbortSignal.timeout(15e3),fetch(o,f)}}catch{}return i}async function postJSON(o,f){const u=await secureFetch(o,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(f)}),g=await u.json();if(!u.ok||g.success===!1)throw new Error(g.error||`Request failed (${u.status})`);return g}async function getJSON(o){const f=await secureFetch(o);if(!f.ok){let u=`Request failed (${f.status})`;try{u=(await f.json()).error||u}catch{}throw new Error(u)}return f.json()}async function deleteAPI(o){const f=await secureFetch(o,{method:"DELETE"}),u=await f.json();if(!f.ok||u.success===!1)throw new Error(u.error||`Delete failed (${f.status})`);return u}async function withButton(o,f,u,g={}){const i=o.innerHTML,{successText:s="\u2705",resetDelay:h=DC.DELAYS.BTN_RESET}=g;o.disabled=!0,o.innerHTML=f;try{const n=await u();return o.innerHTML=s,setTimeout(()=>{o.innerHTML=i,o.disabled=!1},h),n}catch(n){throw o.innerHTML=i,o.disabled=!1,n}}function openModal(o){document.getElementById(o)?.classList.add("show")}function closeModal(o){document.getElementById(o)?.classList.remove("show")}function wireModal(o,...f){o&&(o.addEventListener("click",u=>{u.target===o&&o.classList.remove("show")}),f.forEach(u=>u?.addEventListener("click",()=>o.classList.remove("show"))))}function showNotification(o,f="info",u=3e3){const g=document.querySelector(".deploy-notification");g&&g.remove();const i={info:{bg:"#2196F3",fg:"#fff"},success:{bg:"var(--ok-bg)",fg:"var(--ok-fg)"},error:{bg:"#f44336",fg:"#fff"},warning:{bg:"#ff9800",fg:"#fff"}},s=i[f]||i.info,h=document.createElement("div");h.className="deploy-notification",h.textContent=o,h.style.cssText=` position: fixed; top: 20px; right: 20px; - background: ${r.bg}; color: ${r.fg}; + background: ${s.bg}; color: ${s.fg}; padding: 16px 24px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,.3); z-index: 10000; animation: slideIn 0.3s ease-out; max-width: 400px; white-space: pre-line; font-size: 14px; - `,document.body.appendChild(h),l>0&&setTimeout(()=>h.remove(),l)}function timeAgo(o){const m=Date.now()-new Date(o).getTime();return m<6e4?"just now":m<36e5?Math.floor(m/6e4)+"m ago":m<864e5?Math.floor(m/36e5)+"h ago":Math.floor(m/864e5)+"d ago"}function safeGet(o,m=null){try{const l=localStorage.getItem(o);return l!==null?l:m}catch{return m}}function safeSet(o,m){try{localStorage.setItem(o,m)}catch{}}function safeRemove(o){try{localStorage.removeItem(o)}catch{}}function safeSessionGet(o,m=null){try{const l=sessionStorage.getItem(o);return l!==null?l:m}catch{return m}}function safeSessionSet(o,m){try{sessionStorage.setItem(o,m)}catch{}}function safeGetJSON(o,m=null){try{const l=localStorage.getItem(o);return l?JSON.parse(l):m}catch{return m}}function escapeHtml(o){return String(o??"").replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function injectModal(o,m){document.getElementById(o)||document.body.insertAdjacentHTML("beforeend",m)}const DC_BUS={_handlers:{},on(o,m){var l;((l=this._handlers)[o]||(l[o]=[])).push(m)},off(o,m){this._handlers[o]=this._handlers[o]?.filter(l=>l!==m)},emit(o,m){this._handlers[o]?.forEach(l=>l(m))}},AppState={_apps:[],getApps(){return this._apps},setApps(o){this._apps=o,window.APPS=o,DC_BUS.emit("apps:changed",o)},findApp(o){return this._apps.find(m=>m.id===o)},addApp(o){this._apps.push(o),window.APPS=this._apps,DC_BUS.emit("apps:changed",this._apps)},removeApp(o){const m=this._apps.findIndex(l=>l.id===o);return m>-1&&(this._apps.splice(m,1),window.APPS=this._apps,DC_BUS.emit("apps:changed",this._apps)),m>-1},updateApp(o,m){const l=this._apps.find(w=>w.id===o);if(l){for(const[w,v]of Object.entries(m))w!=="__proto__"&&w!=="constructor"&&w!=="prototype"&&(l[w]=v);DC_BUS.emit("apps:changed",this._apps)}return l}};(function(){function o(){const w=document.createElement("div");return w.className="skeleton-card",w.innerHTML='
',w}function m(w){const v=document.getElementById("cards");if(!(!v||v.querySelector(".card"))){w=w||6;for(let r=0;r.4,P={};return P.hover=C?c(y,L,.35):c(y,$,.08),P["card-hover"]=c(y,P.hover,.5),P.base=c(L,y,.6),P["fg-muted"]=c(x,L,.35),P.success=I,P.error=S,P.warning=C?"#d68a00":"#f39c12",P}function a(E,L){var $=L.lightBg||L.bg&&g(L.bg)>.4,x=L.accent||L["accent-strong"]||"#888888",y=p(x);return $?":root."+E+` body { + `,document.body.appendChild(h),u>0&&setTimeout(()=>h.remove(),u)}function timeAgo(o){const f=Date.now()-new Date(o).getTime();return f<6e4?"just now":f<36e5?Math.floor(f/6e4)+"m ago":f<864e5?Math.floor(f/36e5)+"h ago":Math.floor(f/864e5)+"d ago"}function safeGet(o,f=null){try{const u=localStorage.getItem(o);return u!==null?u:f}catch{return f}}function safeSet(o,f){try{localStorage.setItem(o,f)}catch{}}function safeRemove(o){try{localStorage.removeItem(o)}catch{}}function safeSessionGet(o,f=null){try{const u=sessionStorage.getItem(o);return u!==null?u:f}catch{return f}}function safeSessionSet(o,f){try{sessionStorage.setItem(o,f)}catch{}}function safeGetJSON(o,f=null){try{const u=localStorage.getItem(o);return u?JSON.parse(u):f}catch{return f}}function escapeHtml(o){return String(o??"").replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function injectModal(o,f){document.getElementById(o)||document.body.insertAdjacentHTML("beforeend",f)}const DC_BUS={_handlers:{},on(o,f){var u;((u=this._handlers)[o]||(u[o]=[])).push(f)},off(o,f){this._handlers[o]=this._handlers[o]?.filter(u=>u!==f)},emit(o,f){this._handlers[o]?.forEach(u=>u(f))}},AppState={_apps:[],getApps(){return this._apps},setApps(o){this._apps=o,window.APPS=o,DC_BUS.emit("apps:changed",o)},findApp(o){return this._apps.find(f=>f.id===o)},addApp(o){this._apps.push(o),window.APPS=this._apps,DC_BUS.emit("apps:changed",this._apps)},removeApp(o){const f=this._apps.findIndex(u=>u.id===o);return f>-1&&(this._apps.splice(f,1),window.APPS=this._apps,DC_BUS.emit("apps:changed",this._apps)),f>-1},updateApp(o,f){const u=this._apps.find(g=>g.id===o);if(u){for(const[g,i]of Object.entries(f))g!=="__proto__"&&g!=="constructor"&&g!=="prototype"&&(u[g]=i);DC_BUS.emit("apps:changed",this._apps)}return u}};(function(){function o(){const g=document.createElement("div");return g.className="skeleton-card",g.innerHTML='
',g}function f(g){const i=document.getElementById("cards");if(!(!i||i.querySelector(".card"))){g=g||6;for(let s=0;s.4,P={};return P.hover=C?l(b,L,.35):l(b,$,.08),P["card-hover"]=l(b,P.hover,.5),P.base=l(L,b,.6),P["fg-muted"]=l(x,L,.35),P.success=I,P.error=S,P.warning=C?"#d68a00":"#f39c12",P}function a(E,L){var $=L.lightBg||L.bg&&y(L.bg)>.4,x=L.accent||L["accent-strong"]||"#888888",b=m(x);return $?":root."+E+` body { background: - radial-gradient(1200px 800px at 10% -10%, rgba(`+y.r+","+y.g+","+y.b+`, .08), transparent 60%), - radial-gradient(1000px 700px at 110% 10%, rgba(`+y.r+","+y.g+","+y.b+`, .05), transparent 55%), + radial-gradient(1200px 800px at 10% -10%, rgba(`+b.r+","+b.g+","+b.b+`, .08), transparent 60%), + radial-gradient(1000px 700px at 110% 10%, rgba(`+b.r+","+b.g+","+b.b+`, .05), transparent 55%), var(--bg); } `:":root."+E+` body { background: - radial-gradient(1200px 900px at 8% -12%, rgba(`+y.r+","+y.g+","+y.b+`, .10), transparent 60%), - radial-gradient(1000px 700px at 110% -10%, rgba(`+y.r+","+y.g+","+y.b+`, .07), transparent 55%), + radial-gradient(1200px 900px at 8% -12%, rgba(`+b.r+","+b.g+","+b.b+`, .10), transparent 60%), + radial-gradient(1000px 700px at 110% -10%, rgba(`+b.r+","+b.g+","+b.b+`, .07), transparent 55%), var(--bg); } -`}function u(E,L){var $=L.lightBg||L.bg&&g(L.bg)>.4;return $?":root."+E+` button:hover { +`}function p(E,L){var $=L.lightBg||L.bg&&y(L.bg)>.4;return $?":root."+E+` button:hover { background: color-mix(in srgb, var(--accent-strong) 12%, white 88%); border-color: rgba(0, 0, 0, .15); box-shadow: 0 1px 6px rgba(0, 0, 0, .08), inset 0 1px 0 rgba(255, 255, 255, .8); @@ -26,10 +44,10 @@ const DC={NAME:"DashCaddy",POLL:{DASHBOARD:1e4,LOGS:3e3,STATS:5e3,WEATHER:6e5,HE background: color-mix(in srgb, var(--accent) 18%, transparent); border-color: color-mix(in srgb, var(--accent) 35%, var(--border)); } -`}function t(){return window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}function d(){r.forEach(function(E){document.documentElement.style.removeProperty("--"+E)})}function f(E,L){var $=E.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"");$||($="custom"),w.indexOf($)!==-1&&($=$+"-custom");for(var x=safeGetJSON(m,{}),y=$,I=2;x[$]&&$!==L;)$=y+"-"+I++;return $}function i(E){var L=document.getElementById("user-theme-styles");L&&L.remove(),v.length=w.length,Object.keys(b).forEach(function(S){w.indexOf(S)===-1&&delete b[S]});var $=E||safeGetJSON(m,{}),x=Object.keys($);if(x=x.filter(function(S){return w.indexOf(S)===-1}),!!x.length){var y="";x.forEach(function(S){var C=$[S];v.indexOf(S)===-1&&v.push(S);var P={};r.forEach(function(O){C[O]&&(P[O]=C[O])}),P["card-bg"]=C["card-base"]||C.bg,C.lightBg&&(P.lightBg=!0);var D=e(P);n.forEach(function(O){!P[O]&&D[O]&&(P[O]=D[O])}),b[S]=P,y+=":root."+S+` { -`,r.forEach(function(O){P[O]&&(y+=" --"+O+": "+P[O]+`; -`)}),y+=`} -`,y+=a(S,P),y+=u(S,P)});var I=document.createElement("style");I.id="user-theme-styles",I.textContent=y,document.head.appendChild(I)}}function k(){secureFetch("/api/v1/themes").then(function(E){return E.json()}).then(function(E){if(!(!E.success||!E.themes)){var L=E.themes,$=safeGetJSON(m,{});if(JSON.stringify(L)!==JSON.stringify($)){safeSet(m,JSON.stringify(L)),i(L);var x=safeGet(o);x&&v.indexOf(x)!==-1&&T(x)}}}).catch(function(){})}function B(){var E=safeGetJSON(l);if(E){var L=E.name||"Custom",$=f(L),x={name:L};r.forEach(function(S){E[S]&&(x[S]=E[S])});var y=safeGetJSON(m,{});y[$]=x,safeSet(m,JSON.stringify(y)),safeGet(o)==="custom"&&safeSet(o,$),safeRemove(l);var I={};r.forEach(function(S){x[S]&&(I[S]=x[S])}),fetch("/api/v1/themes/"+$,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:L,colors:I})}).catch(function(){})}}function T(E){document.documentElement.classList.add("theme-transitioning"),v.forEach(function(y){y!=="dark"&&document.documentElement.classList.remove(y)}),d(),E!=="dark"&&document.documentElement.classList.add(E),safeSet(o,E);var L=b[E],$=document.querySelector('meta[name="theme-color"]');$&&L&&$.setAttribute("content",L.bg);var x=L&&L.lightBg;!x&&L&&L.bg&&(x=g(L.bg)>.4),x?document.documentElement.classList.add("light-bg"):document.documentElement.classList.remove("light-bg"),setTimeout(function(){document.documentElement.classList.remove("theme-transitioning")},300)}B(),i();var A=safeGet(o);A==="red"&&(A="black",safeSet(o,"black")),A&&A!=="dark"&&v.indexOf(A)===-1&&(A=null),T(A||t()),k(),window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",function(E){safeGet(o)||T(E.matches?"dark":"light")}),window.THEMES=v,window.BUILTIN_THEMES=w,window.THEME_COLORS=b,window.THEME_PROPS=r,window.BASE_PROPS=h,window.DERIVED_PROPS=n,window.USER_THEMES_KEY=m,window.applyTheme=T,window.clearCustomProperties=d,window.injectUserThemeStyles=i,window.syncThemesFromServer=k,window.slugifyThemeName=f,window.getActiveTheme=function(){return safeGet(o)||t()},window.deriveExtendedColors=e,window.hexToRgb=p,window.rgbToHex=s,window.blendColors=c})(),(function(){function o(){const h=document.querySelector(".totp-card");if(!h)return;const b=getComputedStyle(h).backgroundColor.match(/\d+/g);if(!b)return;const p=(.299*+b[0]+.587*+b[1]+.114*+b[2])/255,s=h.querySelector(".totp-logo-dark"),c=h.querySelector(".totp-logo-light");s&&(s.style.display=p>.5?"none":""),c&&(c.style.display=p>.5?"":"none")}function m(){const h=document.getElementById("totp-overlay");if(h){h.classList.add("show"),setTimeout(o,50);const n=h.querySelector(".totp-digits input");n&&setTimeout(()=>n.focus(),100)}}function l(){const h=document.getElementById("totp-overlay");h&&h.classList.remove("show")}const w=document.getElementById("totp-digits");if(w){const h=w.querySelectorAll("input");h.forEach((n,b)=>{n.addEventListener("input",p=>{const s=p.target.value.replace(/\D/g,"");p.target.value=s.slice(0,1),s&&bg.value).join("");c.length===6&&v(c)}),n.addEventListener("keydown",p=>{p.key==="Backspace"&&!p.target.value&&b>0&&(h[b-1].focus(),h[b-1].value="")}),n.addEventListener("paste",p=>{p.preventDefault();const s=(p.clipboardData.getData("text")||"").replace(/\D/g,"");s.length>=6&&(h.forEach((c,g)=>{c.value=s[g]||""}),h[5].focus(),v(s.slice(0,6)))})})}async function v(h){const n=document.getElementById("totp-error");n.textContent="Verifying...",n.className="totp-error verifying";try{const p=await(await secureFetch("/api/v1/totp/verify",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:h})})).json();if(p.success){n.textContent="",p.csrfToken&&(csrfToken=p.csrfToken),l();const s=safeSessionGet("totp_redirect");if(s){try{sessionStorage.removeItem("totp_redirect")}catch{}window.location.href=s;return}typeof window.initializeDashboard=="function"&&window.initializeDashboard()}else{n.textContent=p.error||"Invalid code",n.className="totp-error";const s=document.querySelectorAll("#totp-digits input");s.forEach(c=>{c.value=""}),s[0]?.focus()}}catch{n.textContent="Connection error",n.className="totp-error"}}const r=new URLSearchParams(window.location.search);if(r.get("auth")==="required"){const h=r.get("return");if(h)try{const n=new URL(h,window.location.origin),b=n.hostname,p=n.origin===window.location.origin,s=SITE.tld.startsWith(".")?SITE.tld:"."+SITE.tld,c=b.endsWith(s)||b===s.substring(1);(p||c)&&safeSessionSet("totp_redirect",h)}catch{}window.history.replaceState({},"",window.location.pathname)}window._showTotpOverlay=m})(),(function(){const o=new ErrorHandler;injectModal("folder-browser-modal",`
+`}function t(){return window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}function c(){s.forEach(function(E){document.documentElement.style.removeProperty("--"+E)})}function v(E,L){var $=E.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"");$||($="custom"),g.indexOf($)!==-1&&($=$+"-custom");for(var x=safeGetJSON(f,{}),b=$,I=2;x[$]&&$!==L;)$=b+"-"+I++;return $}function d(E){var L=document.getElementById("user-theme-styles");L&&L.remove(),i.length=g.length,Object.keys(w).forEach(function(S){g.indexOf(S)===-1&&delete w[S]});var $=E||safeGetJSON(f,{}),x=Object.keys($);if(x=x.filter(function(S){return g.indexOf(S)===-1}),!!x.length){var b="";x.forEach(function(S){var C=$[S];i.indexOf(S)===-1&&i.push(S);var P={};s.forEach(function(O){C[O]&&(P[O]=C[O])}),P["card-bg"]=C["card-base"]||C.bg,C.lightBg&&(P.lightBg=!0);var D=e(P);n.forEach(function(O){!P[O]&&D[O]&&(P[O]=D[O])}),w[S]=P,b+=":root."+S+` { +`,s.forEach(function(O){P[O]&&(b+=" --"+O+": "+P[O]+`; +`)}),b+=`} +`,b+=a(S,P),b+=p(S,P)});var I=document.createElement("style");I.id="user-theme-styles",I.textContent=b,document.head.appendChild(I)}}function k(){secureFetch("/api/v1/themes").then(function(E){return E.json()}).then(function(E){if(!(!E.success||!E.themes)){var L=E.themes,$=safeGetJSON(f,{});if(JSON.stringify(L)!==JSON.stringify($)){safeSet(f,JSON.stringify(L)),d(L);var x=safeGet(o);x&&i.indexOf(x)!==-1&&T(x)}}}).catch(function(){})}function B(){var E=safeGetJSON(u);if(E){var L=E.name||"Custom",$=v(L),x={name:L};s.forEach(function(S){E[S]&&(x[S]=E[S])});var b=safeGetJSON(f,{});b[$]=x,safeSet(f,JSON.stringify(b)),safeGet(o)==="custom"&&safeSet(o,$),safeRemove(u);var I={};s.forEach(function(S){x[S]&&(I[S]=x[S])}),fetch("/api/v1/themes/"+$,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:L,colors:I})}).catch(function(){})}}function T(E){document.documentElement.classList.add("theme-transitioning"),i.forEach(function(b){b!=="dark"&&document.documentElement.classList.remove(b)}),c(),E!=="dark"&&document.documentElement.classList.add(E),safeSet(o,E);var L=w[E],$=document.querySelector('meta[name="theme-color"]');$&&L&&$.setAttribute("content",L.bg);var x=L&&L.lightBg;!x&&L&&L.bg&&(x=y(L.bg)>.4),x?document.documentElement.classList.add("light-bg"):document.documentElement.classList.remove("light-bg"),setTimeout(function(){document.documentElement.classList.remove("theme-transitioning")},300)}B(),d();var A=safeGet(o);A==="red"&&(A="black",safeSet(o,"black")),A&&A!=="dark"&&i.indexOf(A)===-1&&(A=null),T(A||t()),k(),window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",function(E){safeGet(o)||T(E.matches?"dark":"light")}),window.THEMES=i,window.BUILTIN_THEMES=g,window.THEME_COLORS=w,window.THEME_PROPS=s,window.BASE_PROPS=h,window.DERIVED_PROPS=n,window.USER_THEMES_KEY=f,window.applyTheme=T,window.clearCustomProperties=c,window.injectUserThemeStyles=d,window.syncThemesFromServer=k,window.slugifyThemeName=v,window.getActiveTheme=function(){return safeGet(o)||t()},window.deriveExtendedColors=e,window.hexToRgb=m,window.rgbToHex=r,window.blendColors=l})(),(function(){function o(){const h=document.querySelector(".totp-card");if(!h)return;const w=getComputedStyle(h).backgroundColor.match(/\d+/g);if(!w)return;const m=(.299*+w[0]+.587*+w[1]+.114*+w[2])/255,r=h.querySelector(".totp-logo-dark"),l=h.querySelector(".totp-logo-light");r&&(r.style.display=m>.5?"none":""),l&&(l.style.display=m>.5?"":"none")}function f(){const h=document.getElementById("totp-overlay");if(h){h.classList.add("show"),setTimeout(o,50);const n=h.querySelector(".totp-digits input");n&&setTimeout(()=>n.focus(),100)}}function u(){const h=document.getElementById("totp-overlay");h&&h.classList.remove("show")}const g=document.getElementById("totp-digits");if(g){const h=g.querySelectorAll("input");h.forEach((n,w)=>{n.addEventListener("input",m=>{const r=m.target.value.replace(/\D/g,"");m.target.value=r.slice(0,1),r&&wy.value).join("");l.length===6&&i(l)}),n.addEventListener("keydown",m=>{m.key==="Backspace"&&!m.target.value&&w>0&&(h[w-1].focus(),h[w-1].value="")}),n.addEventListener("paste",m=>{m.preventDefault();const r=(m.clipboardData.getData("text")||"").replace(/\D/g,"");r.length>=6&&(h.forEach((l,y)=>{l.value=r[y]||""}),h[5].focus(),i(r.slice(0,6)))})})}async function i(h){const n=document.getElementById("totp-error");n.textContent="Verifying...",n.className="totp-error verifying";try{const m=await(await secureFetch("/api/v1/totp/verify",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:h})})).json();if(m.success){n.textContent="",m.csrfToken&&(csrfToken=m.csrfToken),u();const r=safeSessionGet("totp_redirect");if(r){try{sessionStorage.removeItem("totp_redirect")}catch{}window.location.href=r;return}typeof window.initializeDashboard=="function"&&window.initializeDashboard()}else{n.textContent=m.error||"Invalid code",n.className="totp-error";const r=document.querySelectorAll("#totp-digits input");r.forEach(l=>{l.value=""}),r[0]?.focus()}}catch{n.textContent="Connection error",n.className="totp-error"}}const s=new URLSearchParams(window.location.search);if(s.get("auth")==="required"){const h=s.get("return");if(h)try{const n=new URL(h,window.location.origin),w=n.hostname,m=n.origin===window.location.origin,r=SITE.tld.startsWith(".")?SITE.tld:"."+SITE.tld,l=w.endsWith(r)||w===r.substring(1);(m||l)&&safeSessionSet("totp_redirect",h)}catch{}window.history.replaceState({},"",window.location.pathname)}window._showTotpOverlay=f})(),(function(){const o=new ErrorHandler;injectModal("folder-browser-modal",`

\u{1F4C2} Browse for Media Folders

@@ -127,7 +145,7 @@ const DC={NAME:"DashCaddy",POLL:{DASHBOARD:1e4,LOGS:3e3,STATS:5e3,WEATHER:6e5,HE
-
`);const m=document.getElementById("service-creds-modal");let l=null;const w=["sonarr","radarr","prowlarr","overseerr"],v=["sonarr","radarr"];function r(s){return s.externalUrl||s.url||""}function h(s){const c=document.getElementById("svc-creds-error");c.textContent=s,c.style.display=""}function n(){const s=document.getElementById("svc-creds-error");s.textContent="",s.style.display="none"}window.openServiceCredsModal=async function(s){l=s,n();const c=document.getElementById("svc-creds-title"),g=document.getElementById("svc-creds-desc"),e=document.getElementById("svc-creds-seedhost"),a=document.getElementById("svc-creds-apikey"),u=document.getElementById("svc-creds-basic"),t=document.getElementById("svc-creds-quality");c.textContent=s.name+" Credentials";const d=!!s.isExternal,f=w.includes(s.id)||w.includes(s.appTemplate),i=v.includes(s.id)||v.includes(s.appTemplate);e.style.display=d?"":"none",a.style.display=f?"":"none",t.style.display=i?"":"none",u.style.display=d?"none":"";const k=document.getElementById("svc-quality-select");k.innerHTML='',document.getElementById("svc-quality-status").textContent="",d?(g.textContent="Seedhost credentials auto-login past the HTTP prompt. API key bypasses the app login.",document.getElementById("svc-seedhost-pass").placeholder=`Password for ${s.name}`):f?g.textContent="API key bypasses the app login screen automatically.":g.textContent="Credentials are injected automatically when accessing this service.",await b(s),m.classList.add("show")};async function b(s){const c=document.getElementById("svc-creds-dot"),g=document.getElementById("svc-creds-status"),e=document.getElementById("svc-creds-clear");let a=!1;if(s.isExternal){try{const d=await(await fetch(`/api/v1/seedhost-creds?serviceId=${s.id}`)).json();d.success?(document.getElementById("svc-seedhost-user").value=d.username||"",d.hasCredentials&&(a=!0)):document.getElementById("svc-seedhost-user").value=""}catch{}document.getElementById("svc-seedhost-pass").value=""}try{const d=await(await fetch(`/api/v1/services/${s.id}/credentials`)).json();d.success&&(d.hasApiKey?(document.getElementById("svc-apikey-input").value="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",a=!0):document.getElementById("svc-apikey-input").value="",d.hasBasicAuth&&!s.isExternal?(document.getElementById("svc-basic-user").value=d.username||"",a=!0):document.getElementById("svc-basic-user").value="")}catch{}document.getElementById("svc-basic-pass")&&(document.getElementById("svc-basic-pass").value="");const u=s.id||s.appTemplate;if(v.includes(u)&&await p(s),a){c.style.background="var(--ok-fg, #74dfc4)",g.style.color="var(--ok-fg, #74dfc4)",g.textContent="Credentials stored",e.style.display="";const t=document.getElementById(`creds-btn-${s.id}`);t&&t.classList.add("has-creds")}else c.style.background="var(--muted)",g.style.color="var(--muted)",g.textContent="No credentials stored",e.style.display="none"}async function p(s){const c=document.getElementById("svc-quality-select"),g=document.getElementById("svc-quality-status"),e=s.id||s.appTemplate,a=r(s);if(!a){c.innerHTML='';return}c.innerHTML='',g.textContent="";try{const u=new URLSearchParams({service:e,url:a}),d=await(await fetch(`/api/v1/arr/quality-profiles?${u}`)).json();if(!d.success||!d.profiles?.length){c.innerHTML='';return}c.innerHTML="";for(const f of d.profiles){const i=document.createElement("option");i.value=f.id,i.textContent=f.name,c.appendChild(i)}if(d.storedProfileId&&(c.value=String(d.storedProfileId)),!c.value){const f=d.profiles.find(i=>/720/i.test(i.name));f&&(c.value=String(f.id))}!c.value&&d.profiles.length&&(c.value=String(d.profiles[0].id)),g.innerHTML=`${d.profiles.length} profiles loaded`}catch(u){c.innerHTML='',g.innerHTML=`Error: ${u.message}`}}document.getElementById("svc-quality-fetch")?.addEventListener("click",async()=>{if(!l)return;const s=l.id||l.appTemplate,c=r(l),e=document.getElementById("svc-apikey-input")?.value.trim(),a=document.getElementById("svc-quality-select"),u=document.getElementById("svc-quality-status");if(!c){u.innerHTML='No service URL available';return}if(!e||e==="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022"){u.innerHTML='Enter an API key first';return}a.innerHTML='',u.textContent="";try{const t=new URLSearchParams({service:s,url:c,apiKey:e}),f=await(await fetch(`/api/v1/arr/quality-profiles?${t}`)).json();if(!f.success){a.innerHTML='',u.innerHTML=`${f.error||"Failed to fetch profiles"}`;return}if(!f.profiles?.length){a.innerHTML='';return}a.innerHTML="";for(const k of f.profiles){const B=document.createElement("option");B.value=k.id,B.textContent=k.name,a.appendChild(B)}const i=f.profiles.find(k=>/720/i.test(k.name));i?a.value=String(i.id):f.profiles.length&&(a.value=String(f.profiles[0].id)),u.innerHTML=`${f.profiles.length} profiles loaded`}catch(t){a.innerHTML='',u.innerHTML=`${t.message}`}}),document.getElementById("svc-creds-save")?.addEventListener("click",async()=>{if(!l)return;const s=document.getElementById("svc-creds-save");s.textContent="Saving...",s.disabled=!0,n();try{const c=w.includes(l.id)||w.includes(l.appTemplate),g=l.id||l.appTemplate;if(l.isExternal){const u=document.getElementById("svc-seedhost-user").value.trim(),t=document.getElementById("svc-seedhost-pass").value;u&&await secureFetch("/api/v1/seedhost-creds",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:u,password:t||void 0,serviceId:l.id})})}const a=document.getElementById("svc-apikey-input")?.value.trim();if(a&&a!=="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022")if(c){const u=r(l),t=document.getElementById("svc-quality-select"),d=t?.value?parseInt(t.value):void 0,f=t?.selectedOptions?.[0]?.textContent||void 0,k=await(await secureFetch("/api/v1/arr/credentials",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({service:g,apiKey:a,url:u||void 0,qualityProfileId:d||void 0,qualityProfileName:f||void 0})})).json();if(!k.success){h(k.error||"Failed to save API key"),s.textContent="Save",s.disabled=!1;return}k.connectionTest&&!k.connectionTest.success&&h(`API key saved but connection test failed: ${k.connectionTest.error}`)}else await secureFetch(`/api/v1/services/${l.id}/credentials`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({apiKey:a})});else if(c&&v.includes(g)){const u=document.getElementById("svc-quality-select"),t=u?.value?parseInt(u.value):void 0,d=u?.selectedOptions?.[0]?.textContent||void 0;t&&await secureFetch("/api/v1/arr/quality-profiles",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({service:g,qualityProfileId:t,qualityProfileName:d})})}if(!l.isExternal){const u=document.getElementById("svc-basic-user").value.trim(),t=document.getElementById("svc-basic-pass").value;u&&t&&await secureFetch(`/api/v1/services/${l.id}/credentials`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:u,password:t})})}await b(l)}catch(c){o.logError("[ServiceCredentials] Save",c,{function:"saveCredentials"}),h("Failed to save: "+(c.message||"Unknown error"))}s.textContent="Save",s.disabled=!1}),document.getElementById("svc-creds-clear")?.addEventListener("click",async()=>{if(l&&confirm(`Remove stored credentials for ${l.name}?`)){n();try{const s=l.id||l.appTemplate,c=w.includes(s);l.isExternal&&await secureFetch(`/api/v1/seedhost-creds?serviceId=${l.id}`,{method:"DELETE"}),await secureFetch(`/api/v1/services/${l.id}/credentials`,{method:"DELETE"}),c&&await secureFetch(`/api/v1/arr/credentials/${s}`,{method:"DELETE"});const g=document.getElementById(`creds-btn-${l.id}`);g&&g.classList.remove("has-creds"),await b(l)}catch(s){o.logError("[ServiceCredentials] Clear",s,{function:"clearCredentials"}),h("Failed to clear: "+(s.message||"Unknown error"))}}}),document.getElementById("svc-creds-close")?.addEventListener("click",()=>{m.classList.remove("show"),l=null}),m?.addEventListener("click",s=>{s.target===m&&(m.classList.remove("show"),l=null)}),window.refreshCredsButtons=async function(){try{for(const s of window.APPS||[]){if(!s.isExternal&&!s.appTemplate&&!s.url)continue;let c=!1;if(s.isExternal)try{const a=await(await fetch(`/api/v1/seedhost-creds?serviceId=${s.id}`)).json();a.success&&a.hasCredentials&&(c=!0)}catch{}try{const a=await(await fetch(`/api/v1/services/${s.id}/credentials`)).json();a.success&&(a.hasApiKey||a.hasBasicAuth)&&(c=!0)}catch{}const g=document.getElementById(`creds-btn-${s.id}`);g&&g.classList.toggle("has-creds",c)}}catch{}}})(),(function(){const o=new ErrorHandler;injectModal("totp-settings-modal",`
+
`);const f=document.getElementById("service-creds-modal");let u=null;const g=["sonarr","radarr","prowlarr","overseerr"],i=["sonarr","radarr"];function s(r){return r.externalUrl||r.url||""}function h(r){const l=document.getElementById("svc-creds-error");l.textContent=r,l.style.display=""}function n(){const r=document.getElementById("svc-creds-error");r.textContent="",r.style.display="none"}window.openServiceCredsModal=async function(r){u=r,n();const l=document.getElementById("svc-creds-title"),y=document.getElementById("svc-creds-desc"),e=document.getElementById("svc-creds-seedhost"),a=document.getElementById("svc-creds-apikey"),p=document.getElementById("svc-creds-basic"),t=document.getElementById("svc-creds-quality");l.textContent=r.name+" Credentials";const c=!!r.isExternal,v=g.includes(r.id)||g.includes(r.appTemplate),d=i.includes(r.id)||i.includes(r.appTemplate);e.style.display=c?"":"none",a.style.display=v?"":"none",t.style.display=d?"":"none",p.style.display=c?"none":"";const k=document.getElementById("svc-quality-select");k.innerHTML='',document.getElementById("svc-quality-status").textContent="",c?(y.textContent="Seedhost credentials auto-login past the HTTP prompt. API key bypasses the app login.",document.getElementById("svc-seedhost-pass").placeholder=`Password for ${r.name}`):v?y.textContent="API key bypasses the app login screen automatically.":y.textContent="Credentials are injected automatically when accessing this service.",await w(r),f.classList.add("show")};async function w(r){const l=document.getElementById("svc-creds-dot"),y=document.getElementById("svc-creds-status"),e=document.getElementById("svc-creds-clear");let a=!1;if(r.isExternal){try{const c=await(await fetch(`/api/v1/seedhost-creds?serviceId=${r.id}`)).json();c.success?(document.getElementById("svc-seedhost-user").value=c.username||"",c.hasCredentials&&(a=!0)):document.getElementById("svc-seedhost-user").value=""}catch{}document.getElementById("svc-seedhost-pass").value=""}try{const c=await(await fetch(`/api/v1/services/${r.id}/credentials`)).json();c.success&&(c.hasApiKey?(document.getElementById("svc-apikey-input").value="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",a=!0):document.getElementById("svc-apikey-input").value="",c.hasBasicAuth&&!r.isExternal?(document.getElementById("svc-basic-user").value=c.username||"",a=!0):document.getElementById("svc-basic-user").value="")}catch{}document.getElementById("svc-basic-pass")&&(document.getElementById("svc-basic-pass").value="");const p=r.id||r.appTemplate;if(i.includes(p)&&await m(r),a){l.style.background="var(--ok-fg, #74dfc4)",y.style.color="var(--ok-fg, #74dfc4)",y.textContent="Credentials stored",e.style.display="";const t=document.getElementById(`creds-btn-${r.id}`);t&&t.classList.add("has-creds")}else l.style.background="var(--muted)",y.style.color="var(--muted)",y.textContent="No credentials stored",e.style.display="none"}async function m(r){const l=document.getElementById("svc-quality-select"),y=document.getElementById("svc-quality-status"),e=r.id||r.appTemplate,a=s(r);if(!a){l.innerHTML='';return}l.innerHTML='',y.textContent="";try{const p=new URLSearchParams({service:e,url:a}),c=await(await fetch(`/api/v1/arr/quality-profiles?${p}`)).json();if(!c.success||!c.profiles?.length){l.innerHTML='';return}l.innerHTML="";for(const v of c.profiles){const d=document.createElement("option");d.value=v.id,d.textContent=v.name,l.appendChild(d)}if(c.storedProfileId&&(l.value=String(c.storedProfileId)),!l.value){const v=c.profiles.find(d=>/720/i.test(d.name));v&&(l.value=String(v.id))}!l.value&&c.profiles.length&&(l.value=String(c.profiles[0].id)),y.innerHTML=`${c.profiles.length} profiles loaded`}catch(p){l.innerHTML='',y.innerHTML=`Error: ${p.message}`}}document.getElementById("svc-quality-fetch")?.addEventListener("click",async()=>{if(!u)return;const r=u.id||u.appTemplate,l=s(u),e=document.getElementById("svc-apikey-input")?.value.trim(),a=document.getElementById("svc-quality-select"),p=document.getElementById("svc-quality-status");if(!l){p.innerHTML='No service URL available';return}if(!e||e==="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022"){p.innerHTML='Enter an API key first';return}a.innerHTML='',p.textContent="";try{const t=new URLSearchParams({service:r,url:l,apiKey:e}),v=await(await fetch(`/api/v1/arr/quality-profiles?${t}`)).json();if(!v.success){a.innerHTML='',p.innerHTML=`${v.error||"Failed to fetch profiles"}`;return}if(!v.profiles?.length){a.innerHTML='';return}a.innerHTML="";for(const k of v.profiles){const B=document.createElement("option");B.value=k.id,B.textContent=k.name,a.appendChild(B)}const d=v.profiles.find(k=>/720/i.test(k.name));d?a.value=String(d.id):v.profiles.length&&(a.value=String(v.profiles[0].id)),p.innerHTML=`${v.profiles.length} profiles loaded`}catch(t){a.innerHTML='',p.innerHTML=`${t.message}`}}),document.getElementById("svc-creds-save")?.addEventListener("click",async()=>{if(!u)return;const r=document.getElementById("svc-creds-save");r.textContent="Saving...",r.disabled=!0,n();try{const l=g.includes(u.id)||g.includes(u.appTemplate),y=u.id||u.appTemplate;if(u.isExternal){const p=document.getElementById("svc-seedhost-user").value.trim(),t=document.getElementById("svc-seedhost-pass").value;p&&await secureFetch("/api/v1/seedhost-creds",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:p,password:t||void 0,serviceId:u.id})})}const a=document.getElementById("svc-apikey-input")?.value.trim();if(a&&a!=="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022")if(l){const p=s(u),t=document.getElementById("svc-quality-select"),c=t?.value?parseInt(t.value):void 0,v=t?.selectedOptions?.[0]?.textContent||void 0,k=await(await secureFetch("/api/v1/arr/credentials",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({service:y,apiKey:a,url:p||void 0,qualityProfileId:c||void 0,qualityProfileName:v||void 0})})).json();if(!k.success){h(k.error||"Failed to save API key"),r.textContent="Save",r.disabled=!1;return}k.connectionTest&&!k.connectionTest.success&&h(`API key saved but connection test failed: ${k.connectionTest.error}`)}else await secureFetch(`/api/v1/services/${u.id}/credentials`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({apiKey:a})});else if(l&&i.includes(y)){const p=document.getElementById("svc-quality-select"),t=p?.value?parseInt(p.value):void 0,c=p?.selectedOptions?.[0]?.textContent||void 0;t&&await secureFetch("/api/v1/arr/quality-profiles",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({service:y,qualityProfileId:t,qualityProfileName:c})})}if(!u.isExternal){const p=document.getElementById("svc-basic-user").value.trim(),t=document.getElementById("svc-basic-pass").value;p&&t&&await secureFetch(`/api/v1/services/${u.id}/credentials`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:p,password:t})})}await w(u)}catch(l){o.logError("[ServiceCredentials] Save",l,{function:"saveCredentials"}),h("Failed to save: "+(l.message||"Unknown error"))}r.textContent="Save",r.disabled=!1}),document.getElementById("svc-creds-clear")?.addEventListener("click",async()=>{if(u&&confirm(`Remove stored credentials for ${u.name}?`)){n();try{const r=u.id||u.appTemplate,l=g.includes(r);u.isExternal&&await secureFetch(`/api/v1/seedhost-creds?serviceId=${u.id}`,{method:"DELETE"}),await secureFetch(`/api/v1/services/${u.id}/credentials`,{method:"DELETE"}),l&&await secureFetch(`/api/v1/arr/credentials/${r}`,{method:"DELETE"});const y=document.getElementById(`creds-btn-${u.id}`);y&&y.classList.remove("has-creds"),await w(u)}catch(r){o.logError("[ServiceCredentials] Clear",r,{function:"clearCredentials"}),h("Failed to clear: "+(r.message||"Unknown error"))}}}),document.getElementById("svc-creds-close")?.addEventListener("click",()=>{f.classList.remove("show"),u=null}),f?.addEventListener("click",r=>{r.target===f&&(f.classList.remove("show"),u=null)}),window.refreshCredsButtons=async function(){try{for(const r of window.APPS||[]){if(!r.isExternal&&!r.appTemplate&&!r.url)continue;let l=!1;if(r.isExternal)try{const a=await(await fetch(`/api/v1/seedhost-creds?serviceId=${r.id}`)).json();a.success&&a.hasCredentials&&(l=!0)}catch{}try{const a=await(await fetch(`/api/v1/services/${r.id}/credentials`)).json();a.success&&(a.hasApiKey||a.hasBasicAuth)&&(l=!0)}catch{}const y=document.getElementById(`creds-btn-${r.id}`);y&&y.classList.toggle("has-creds",l)}}catch{}}})(),(function(){const o=new ErrorHandler;injectModal("totp-settings-modal",`

Authentication Settings

@@ -222,7 +240,7 @@ const DC={NAME:"DashCaddy",POLL:{DASHBOARD:1e4,LOGS:3e3,STATS:5e3,WEATHER:6e5,HE
- `);async function m(){try{const r=await(await fetch("/api/v1/totp/config")).json();if(!r.success)return;const{enabled:h,sessionDuration:n,isSetUp:b}=r.config,p=document.getElementById("totp-status-dot"),s=document.getElementById("totp-status-text"),c=document.getElementById("totp-status-banner"),g=document.getElementById("totp-setup-section"),e=document.getElementById("totp-qr-section"),a=document.getElementById("totp-duration-section"),u=document.getElementById("totp-disable-section");h&&b?(p.style.background="var(--ok-fg, #7ef2ff)",c.style.borderColor="var(--ok-fg, #7ef2ff)",c.style.background="color-mix(in srgb, var(--ok-fg) 8%, transparent)",s.textContent="TOTP is active",s.style.color="var(--ok-fg, #7ef2ff)",g.style.display="none",e.style.display="none",a.style.display="block",u.style.display="block",document.getElementById("totp-duration-select").value=n):(p.style.background="var(--muted)",c.style.borderColor="var(--border)",c.style.background="transparent",s.textContent="TOTP is not configured",s.style.color="var(--muted)",g.style.display="block",e.style.display="none",a.style.display="none",u.style.display="none"),w(h&&b,n)}catch(v){console.warn("Failed to load TOTP settings:",v)}}const l={"15m":"15 min","30m":"30 min","1h":"1 hour","2h":"2 hours","4h":"4 hours","8h":"8 hours","12h":"12 hours","24h":"24 hours",never:"Disabled"};function w(v,r){const h=document.getElementById("auth-card"),n=document.getElementById("auth-pill"),b=document.getElementById("auth-dot"),p=document.getElementById("auth-status-text");h&&(v?(h.setAttribute("data-status","on"),n.className="badge on",n.textContent="YES",b.className="dot ok at-bl",p.textContent="Session: "+(l[r]||r)):(h.setAttribute("data-status","off"),n.className="badge off",n.textContent="NO",b.className="dot bad at-bl",p.textContent="Not configured"))}document.getElementById("totp-setup-btn")?.addEventListener("click",async()=>{try{const r=await(await secureFetch("/api/v1/totp/setup",{method:"POST"})).json();r.success&&(document.getElementById("totp-qr-image").src=r.qrCode,document.getElementById("totp-manual-key").textContent=r.manualKey,document.getElementById("totp-setup-section").style.display="none",document.getElementById("totp-qr-section").style.display="block",document.getElementById("totp-setup-code").value="",document.getElementById("totp-setup-error").textContent="",document.getElementById("totp-setup-code").focus())}catch(v){o.logError("[TOTP] Setup Failed",v,{function:"setupTOTP"})}}),document.getElementById("totp-import-btn")?.addEventListener("click",async()=>{const v=document.getElementById("totp-import-key").value.trim(),r=document.getElementById("totp-import-error");if(r.textContent="",!v){r.textContent="Paste a Base32 secret key first";return}try{const n=await(await secureFetch("/api/v1/totp/setup",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({secret:v})})).json();n.success?(r.textContent="",document.getElementById("totp-qr-image").src=n.qrCode,document.getElementById("totp-manual-key").textContent=n.manualKey,document.getElementById("totp-setup-section").style.display="none",document.getElementById("totp-qr-section").style.display="block",document.getElementById("totp-setup-code").value="",document.getElementById("totp-setup-error").textContent="",document.getElementById("totp-setup-code").focus()):r.textContent=n.error||n.message||"Import failed"}catch{r.textContent="Connection error \u2014 try refreshing the page"}}),document.getElementById("totp-copy-key")?.addEventListener("click",()=>{const v=document.getElementById("totp-manual-key").textContent;navigator.clipboard.writeText(v).then(()=>{const r=document.getElementById("totp-copy-key");r.textContent="\u2705",setTimeout(()=>{r.textContent="\u{1F4CB}"},2e3)})}),document.getElementById("totp-confirm-setup")?.addEventListener("click",async()=>{const v=document.getElementById("totp-setup-code").value,r=document.getElementById("totp-setup-error");if(!/^\d{6}$/.test(v)){r.textContent="Enter a 6-digit code";return}try{const n=await(await secureFetch("/api/v1/totp/verify-setup",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:v})})).json();n.success?(r.textContent="",m()):r.textContent=n.error||"Invalid code"}catch{r.textContent="Connection error"}}),document.getElementById("totp-setup-code")?.addEventListener("keydown",v=>{v.key==="Enter"&&document.getElementById("totp-confirm-setup")?.click()}),document.getElementById("totp-duration-select")?.addEventListener("change",async v=>{try{await secureFetch("/api/v1/totp/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sessionDuration:v.target.value})}),m()}catch(r){o.logError("[TOTP] Update Session Duration",r,{function:"updateSessionDuration"})}}),document.getElementById("totp-disable-btn")?.addEventListener("click",async()=>{if(confirm("Disable TOTP authentication? All services will be accessible without a code."))try{(await(await secureFetch("/api/v1/totp/disable",{method:"POST",headers:{"Content-Type":"application/json"},body:"{}"})).json()).success&&m()}catch(v){o.logError("[TOTP] Disable Failed",v,{function:"disableTOTP"})}}),document.getElementById("auth-settings-btn")?.addEventListener("click",()=>{m(),openModal("totp-settings-modal")}),document.getElementById("totp-modal-close")?.addEventListener("click",()=>{closeModal("totp-settings-modal")}),document.getElementById("totp-settings-modal")?.addEventListener("click",v=>{v.target.id==="totp-settings-modal"&&closeModal("totp-settings-modal")}),window._updateAuthCard=w,(async()=>{try{const r=await(await fetch("/api/v1/totp/config")).json();if(r.success){const h=r.config.enabled&&r.config.isSetUp;w(h,r.config.sessionDuration)}}catch(v){o.logError("[TOTP] AuthCard Update",v,{function:"authCardUpdate"})}})()})(),(function(){injectModal("token-management-modal",` + `);async function f(){try{const s=await(await fetch("/api/v1/totp/config")).json();if(!s.success)return;const{enabled:h,sessionDuration:n,isSetUp:w}=s.config,m=document.getElementById("totp-status-dot"),r=document.getElementById("totp-status-text"),l=document.getElementById("totp-status-banner"),y=document.getElementById("totp-setup-section"),e=document.getElementById("totp-qr-section"),a=document.getElementById("totp-duration-section"),p=document.getElementById("totp-disable-section");h&&w?(m.style.background="var(--ok-fg, #7ef2ff)",l.style.borderColor="var(--ok-fg, #7ef2ff)",l.style.background="color-mix(in srgb, var(--ok-fg) 8%, transparent)",r.textContent="TOTP is active",r.style.color="var(--ok-fg, #7ef2ff)",y.style.display="none",e.style.display="none",a.style.display="block",p.style.display="block",document.getElementById("totp-duration-select").value=n):(m.style.background="var(--muted)",l.style.borderColor="var(--border)",l.style.background="transparent",r.textContent="TOTP is not configured",r.style.color="var(--muted)",y.style.display="block",e.style.display="none",a.style.display="none",p.style.display="none"),g(h&&w,n)}catch(i){console.warn("Failed to load TOTP settings:",i)}}const u={"15m":"15 min","30m":"30 min","1h":"1 hour","2h":"2 hours","4h":"4 hours","8h":"8 hours","12h":"12 hours","24h":"24 hours",never:"Disabled"};function g(i,s){const h=document.getElementById("auth-card"),n=document.getElementById("auth-pill"),w=document.getElementById("auth-dot"),m=document.getElementById("auth-status-text");h&&(i?(h.setAttribute("data-status","on"),n.className="badge on",n.textContent="YES",w.className="dot ok at-bl",m.textContent="Session: "+(u[s]||s)):(h.setAttribute("data-status","off"),n.className="badge off",n.textContent="NO",w.className="dot bad at-bl",m.textContent="Not configured"))}document.getElementById("totp-setup-btn")?.addEventListener("click",async()=>{try{const s=await(await secureFetch("/api/v1/totp/setup",{method:"POST"})).json();s.success&&(document.getElementById("totp-qr-image").src=s.qrCode,document.getElementById("totp-manual-key").textContent=s.manualKey,document.getElementById("totp-setup-section").style.display="none",document.getElementById("totp-qr-section").style.display="block",document.getElementById("totp-setup-code").value="",document.getElementById("totp-setup-error").textContent="",document.getElementById("totp-setup-code").focus())}catch(i){o.logError("[TOTP] Setup Failed",i,{function:"setupTOTP"})}}),document.getElementById("totp-import-btn")?.addEventListener("click",async()=>{const i=document.getElementById("totp-import-key").value.trim(),s=document.getElementById("totp-import-error");if(s.textContent="",!i){s.textContent="Paste a Base32 secret key first";return}try{const n=await(await secureFetch("/api/v1/totp/setup",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({secret:i})})).json();n.success?(s.textContent="",document.getElementById("totp-qr-image").src=n.qrCode,document.getElementById("totp-manual-key").textContent=n.manualKey,document.getElementById("totp-setup-section").style.display="none",document.getElementById("totp-qr-section").style.display="block",document.getElementById("totp-setup-code").value="",document.getElementById("totp-setup-error").textContent="",document.getElementById("totp-setup-code").focus()):s.textContent=n.error||n.message||"Import failed"}catch{s.textContent="Connection error \u2014 try refreshing the page"}}),document.getElementById("totp-copy-key")?.addEventListener("click",()=>{const i=document.getElementById("totp-manual-key").textContent;navigator.clipboard.writeText(i).then(()=>{const s=document.getElementById("totp-copy-key");s.textContent="\u2705",setTimeout(()=>{s.textContent="\u{1F4CB}"},2e3)})}),document.getElementById("totp-confirm-setup")?.addEventListener("click",async()=>{const i=document.getElementById("totp-setup-code").value,s=document.getElementById("totp-setup-error");if(!/^\d{6}$/.test(i)){s.textContent="Enter a 6-digit code";return}try{const n=await(await secureFetch("/api/v1/totp/verify-setup",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:i})})).json();n.success?(s.textContent="",f()):s.textContent=n.error||"Invalid code"}catch{s.textContent="Connection error"}}),document.getElementById("totp-setup-code")?.addEventListener("keydown",i=>{i.key==="Enter"&&document.getElementById("totp-confirm-setup")?.click()}),document.getElementById("totp-duration-select")?.addEventListener("change",async i=>{try{await secureFetch("/api/v1/totp/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sessionDuration:i.target.value})}),f()}catch(s){o.logError("[TOTP] Update Session Duration",s,{function:"updateSessionDuration"})}}),document.getElementById("totp-disable-btn")?.addEventListener("click",async()=>{if(confirm("Disable TOTP authentication? All services will be accessible without a code."))try{(await(await secureFetch("/api/v1/totp/disable",{method:"POST",headers:{"Content-Type":"application/json"},body:"{}"})).json()).success&&f()}catch(i){o.logError("[TOTP] Disable Failed",i,{function:"disableTOTP"})}}),document.getElementById("auth-settings-btn")?.addEventListener("click",()=>{f(),openModal("totp-settings-modal")}),document.getElementById("totp-modal-close")?.addEventListener("click",()=>{closeModal("totp-settings-modal")}),document.getElementById("totp-settings-modal")?.addEventListener("click",i=>{i.target.id==="totp-settings-modal"&&closeModal("totp-settings-modal")}),window._updateAuthCard=g,(async()=>{try{const s=await(await fetch("/api/v1/totp/config")).json();if(s.success){const h=s.config.enabled&&s.config.isSetUp;g(h,s.config.sessionDuration)}}catch(i){o.logError("[TOTP] AuthCard Update",i,{function:"authCardUpdate"})}})()})(),(function(){injectModal("token-management-modal",`

\u{1F511} DNS Credentials

@@ -240,40 +258,40 @@ const DC={NAME:"DashCaddy",POLL:{DASHBOARD:1e4,LOGS:3e3,STATS:5e3,WEATHER:6e5,HE
- `);function o(){return Object.keys(SITE.dnsServers||{})}function m(t){return(SITE.dnsServers||{})[t]?.name||t.toUpperCase()}function l(){const t=document.getElementById("dns-cred-sections");if(!t)return;t.innerHTML="";const d=o();if(d.length===0){t.innerHTML='

No DNS servers configured.

';return}for(const f of d)t.insertAdjacentHTML("beforeend",` + `);function o(){return Object.keys(SITE.dnsServers||{})}function f(t){return(SITE.dnsServers||{})[t]?.name||t.toUpperCase()}function u(){const t=document.getElementById("dns-cred-sections");if(!t)return;t.innerHTML="";const c=o();if(c.length===0){t.innerHTML='

No DNS servers configured.

';return}for(const v of c)t.insertAdjacentHTML("beforeend",`
-

${m(f)}

+

${f(v)}

- - + +
- - + +
- - + +
- - + +
-
+
- `)}function w(){let t=safeSessionGet("dashcaddy-encryption-key");if(t)return t;const d=safeGet("dashcaddy-encryption-key");if(d)return safeSessionSet("dashcaddy-encryption-key",d),safeRemove("dashcaddy-encryption-key"),d;const f=new Uint8Array(32);return crypto.getRandomValues(f),t=Array.from(f,i=>i.toString(16).padStart(2,"0")).join(""),safeSessionSet("dashcaddy-encryption-key",t),t}const v=w();function r(t,d){if(!t)return"";const f=crypto.getRandomValues(new Uint8Array(8)),i=Array.from(f,T=>T.toString(16).padStart(2,"0")).join(""),k=new TextEncoder().encode(d+i);let B="";for(let T=0;TparseInt($,16))),A=atob(t.substring(17)),E=new TextEncoder().encode(d+B);let L="";for(let $=0;${["readonly","admin"].forEach(d=>{["token","username"].forEach(f=>{safeRemove(`${t}-${d}-${f}-enc`)})}),safeRemove(`${t}-token-enc`),safeRemove(`${t}-username-enc`)})}function u(t){const d=p(t,"readonly"),f=s(t,"readonly"),i=p(t,"admin"),k=s(t,"admin"),B=h(safeGet(`${t}-token-enc`),v),T=h(safeGet(`${t}-username-enc`),v);return{username:k||f||T,token:i||d||B,readonlyToken:d||B,readonlyUsername:f||T,adminToken:i||B,adminUsername:k||T}}document.getElementById("manage-tokens")?.addEventListener("click",()=>{l();const t=document.getElementById("token-management-modal"),d=e();o().forEach(f=>{const i=d[f];document.getElementById(`${f}-readonly-username`).value=i.readonly.username,document.getElementById(`${f}-readonly-token`).value=i.readonly.token,document.getElementById(`${f}-admin-username`).value=i.admin.username,document.getElementById(`${f}-admin-token`).value=i.admin.token,document.getElementById(`${f}-token-status`).textContent=""}),t.classList.add("show")}),document.getElementById("token-management-modal")?.addEventListener("click",t=>{const d=t.target.closest(".token-toggle");if(d){const f=d.dataset.target,i=document.getElementById(f);i.type==="password"?(i.type="text",d.textContent="\u{1F648}"):(i.type="password",d.textContent="\u{1F441}");return}t.target.id==="token-management-modal"&&t.target.classList.remove("show")}),document.getElementById("token-save")?.addEventListener("click",async()=>{const t=o();t.forEach(i=>{g(i,"readonly",document.getElementById(`${i}-readonly-username`).value.trim()),c(i,"readonly",document.getElementById(`${i}-readonly-token`).value.trim()),g(i,"admin",document.getElementById(`${i}-admin-username`).value.trim()),c(i,"admin",document.getElementById(`${i}-admin-token`).value.trim())});const d={};let f=!1;if(t.forEach(i=>{const k={},B=document.getElementById(`${i}-readonly-username`).value.trim(),T=document.getElementById(`${i}-readonly-token`).value.trim(),A=document.getElementById(`${i}-admin-username`).value.trim(),E=document.getElementById(`${i}-admin-token`).value.trim();B&&T&&(k.readonly={username:B,password:T},f=!0),A&&E&&(k.admin={username:A,password:E},f=!0),Object.keys(k).length>0&&(d[i]=k)}),f){t.forEach(i=>{d[i]&&(document.getElementById(`${i}-token-status`).textContent="Verifying...",document.getElementById(`${i}-token-status`).className="token-status")});try{const k=await(await secureFetch("/api/v1/dns/credentials",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({servers:d})})).json();k.results?t.forEach(B=>{const T=document.getElementById(`${B}-token-status`);if(!d[B]){T.textContent="";return}const A=k.results[B];A?.success?(T.textContent="\u2713 Verified & saved",T.className="token-status success"):A?.partial?(T.textContent="\u2713 "+A.partial,T.className="token-status success"):(T.textContent="\u2717 "+(A?.error||"Login failed"),T.className="token-status error")}):k.success?t.forEach(B=>{d[B]&&(document.getElementById(`${B}-token-status`).textContent="\u2713 Saved",document.getElementById(`${B}-token-status`).className="token-status success")}):t.forEach(B=>{d[B]&&(document.getElementById(`${B}-token-status`).textContent="\u2717 "+(k.error||"Failed"),document.getElementById(`${B}-token-status`).className="token-status error")})}catch(i){console.error("Failed to sync DNS credentials to backend:",i),t.forEach(k=>{d[k]&&(document.getElementById(`${k}-token-status`).textContent="\u2713 Saved locally (sync failed)",document.getElementById(`${k}-token-status`).className="token-status")})}}else t.forEach(i=>{document.getElementById(`${i}-token-status`).textContent=""});setTimeout(()=>{t.every(k=>{const B=document.getElementById(`${k}-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.")){a(),o().forEach(t=>{document.getElementById(`${t}-readonly-username`).value="",document.getElementById(`${t}-readonly-token`).value="",document.getElementById(`${t}-admin-username`).value="",document.getElementById(`${t}-admin-token`).value="",document.getElementById(`${t}-token-status`).textContent="\u2713 Cleared",document.getElementById(`${t}-token-status`).className="token-status success"});try{await secureFetch("/api/v1/dns/credentials",{method:"DELETE"})}catch{}}}),window.getToken=p,window.getUsername=s,window.setToken=c,window.setUsername=g,window.getAllCredentials=e,window.getCredential=n,window.setCredential=b,window.getEncryptionKey=w,window.getDnsIds=o,window.getDnsDisplayName=m})(),(function(){function o(c,g,e=null){const a=document.getElementById(c+"-dot"),u=document.getElementById(c+"-pill"),t=document.getElementById(c+"-time"),d=document.querySelector(`[data-app="${c}"]`);a&&(a.classList.toggle("ok",g),a.classList.toggle("bad",!g)),u&&(u.textContent=g?"ON":"OFF",u.classList.toggle("on",g),u.classList.toggle("off",!g)),t&&e!==null&&(t.textContent=g?`${e}ms`:"timeout",t.className=`response-time ${m(e,g)}`),d&&d.setAttribute("data-status",g?"on":"off")}function m(c,g){return g?c<200?"excellent":c<500?"good":c<1e3?"fair":"slow":"timeout"}async function l(c){const g=performance.now();try{const e=await fetch("/probe/"+c,{cache:"no-store"}),a=performance.now(),u=Math.round(a-g);return{isUp:e.status>=200&&e.status<400||e.status===401||e.status===403,responseTime:u}}catch{const e=performance.now();return{isUp:!1,responseTime:Math.round(e-g)}}}window.APPS=[];let w=null,v=!1;async function r(){try{window.SkeletonLoader&&window.SkeletonLoader.show(6);const c=await fetch("/api/v1/services",{cache:"no-store"});c.ok?(window.APPS=await c.json(),window.SkeletonLoader&&window.SkeletonLoader.hide()):(console.error("Failed to load services:",c.status),window.SkeletonLoader&&window.SkeletonLoader.hide())}catch(c){console.error("Failed to load services:",c),window.SkeletonLoader&&window.SkeletonLoader.hide()}}function h(c){const g=window.APPS?.find(a=>a.id===c);if(g?.url)return g.url.startsWith("http")?g.url:"https://"+g.url;if(g?.isExternal&&g.externalUrl)return g.externalUrl;const e=SITE.dnsServers?.[c];return e?"http://"+e.ip+":"+(e.port||5380):buildServiceUrl(c)}function n(c,g,e){const a=document.createElement(c);return g&&(a.className=g),e&&(a.textContent=e),a}function b(){const c=document.getElementById("cards");c.innerHTML="";for(let g=0;g{D.stopPropagation(),window.openContainerLogsModal(e.containerId,e.name)},y.appendChild(S);const C=n("button","update-btn","\u2B06\uFE0F");C.title="Update container to latest version",C.id=`update-btn-${e.id}`,C.onclick=D=>{D.stopPropagation(),window.updateContainer(e.containerId,e.name,e.id)},y.appendChild(C);const P=n("button","exec-btn",">_");P.title="Open terminal",P.onclick=D=>{D.stopPropagation(),window.openExecModal&&window.openExecModal(e.containerId,e.name)},y.appendChild(P)}if(e.logPath&&!e.containerId){const S=n("button","logs-btn","\u{1F4CB}");S.title="View application logs",S.onclick=C=>{C.stopPropagation(),window.openFileLogsModal(e.logPath,e.name)},y.appendChild(S)}if(e.isExternal||e.appTemplate||e.url){const S=n("button","creds-btn","\u{1F511}");S.title="Auto-login credentials",S.id=`creds-btn-${e.id}`,S.onclick=C=>{C.stopPropagation(),window.openServiceCredsModal(e)},y.appendChild(S)}if(e.id!=="internet"){const S=n("button","options-btn","\u2699\uFE0F");S.title="Edit service settings",S.onclick=C=>{C.stopPropagation(),window.openServiceEditModal(e)},y.appendChild(S)}if(e.id!=="internet"){const S=n("button","delete-btn","\u{1F5D1}\uFE0F");S.title="Delete this service",S.onclick=C=>{C.stopPropagation(),window.deleteService(e.id,e.name)},y.appendChild(S)}const I=n("button",null,"Open");I.onclick=()=>window.open(h(e.id),"_blank","noopener"),y.appendChild(I),a.appendChild(y),a.style.transitionDelay=`${Math.min(g*45,270)}ms`,c.appendChild(a)}requestAnimationFrame(()=>{c.querySelectorAll(".card").forEach(g=>g.classList.add("loaded"))}),window.groupRecipeCards&&requestAnimationFrame(()=>window.groupRecipeCards())}function p(c,g,e=null){const a=document.getElementById("dot-"+c+"-grid"),u=document.getElementById("badge-"+c),t=document.getElementById("time-"+c),d=document.querySelector(`[data-app="${c}"]`);a&&(a.classList.toggle("ok",g),a.classList.toggle("bad",!g)),u&&(u.textContent=g?"ON":"OFF",u.classList.toggle("on",g),u.classList.toggle("off",!g)),t&&e!==null&&(t.textContent=g?`${e}ms`:"timeout",t.className=`response-time ${m(e,g)}`),d&&d.setAttribute("data-status",g?"on":"off")}async function s(){if(w)return v=!0,w;function c(a,u=new Date){const t=document.getElementById("stamp");t&&(t.textContent=`${a}: ${new Date(u).toLocaleTimeString()}`)}function g(a){Object.keys(SITE.dnsServers).forEach(t=>{const d=a[t];d&&o(t,d.isUp,d.responseTime)}),a.internet&&o("internet",a.internet.isUp,a.internet.responseTime),window.APPS.forEach(t=>{const d=a[t.id];d&&p(t.id,d.isUp,d.responseTime)})}async function e(){const a=Object.keys(SITE.dnsServers),u=a.map(i=>l(i));u.push(l("internet"));const t=await Promise.all(u);a.forEach((i,k)=>o(i,t[k].isUp,t[k].responseTime));const d=t[t.length-1];o("internet",d.isUp,d.responseTime),(await Promise.all(window.APPS.map(async i=>{const k=await l(i.id);return{id:i.id,...k}}))).forEach(i=>{p(i.id,i.isUp,i.responseTime)})}return w=(async()=>{try{const a=await fetch("/api/v1/services/status",{cache:"no-store"});if(!a.ok)throw new Error(`Status refresh failed (${a.status})`);const u=await a.json();g(u.statuses||{}),c("last check",u.checkedAt||new Date)}catch(a){console.warn("Batched status refresh failed, falling back to direct probes:",a);try{await e(),c("last check")}catch(u){console.error("Dashboard refresh failed:",u),c("last failed")}}finally{w=null,v&&(v=!1,setTimeout(()=>{window.refreshAll()},0))}})(),w}document.querySelector(".top")?.addEventListener("click",c=>{const g=c.target.closest('[id$="-open"]');if(!g)return;const e=g.id.replace("-open","");SITE.dnsServers[e]&&window.open(h(e),"_blank","noopener")}),document.getElementById("ca-open")?.addEventListener("click",()=>window.open(h("ca"),"_blank","noopener")),document.getElementById("creds-btn-ca")?.addEventListener("click",c=>{c.stopPropagation();const g=window.APPS.find(e=>e.id==="ca");g&&window.openServiceCredsModal&&window.openServiceCredsModal(g)}),document.getElementById("options-btn-ca")?.addEventListener("click",c=>{c.stopPropagation();const g=window.APPS.find(e=>e.id==="ca");g&&window.openServiceEditModal&&window.openServiceEditModal(g)}),document.getElementById("delete-btn-ca")?.addEventListener("click",c=>{c.stopPropagation(),window.deleteService&&window.deleteService("ca","DashCA")}),window.loadServices=r,window.buildGrid=b,window.refreshAll=s,window.setQuick=o,window.setBadge=p,window.getResponseTimeClass=m,window.checkServiceWithTiming=l,window.serviceUrl=h,window.el=n})(),(function(){async function o(n){const p=await(await secureFetch(`/api/v1/dns/restart/${n}`,{method:"POST"})).json();if(!p.success)throw new Error(p.error||"Restart failed");return p}document.querySelector(".top")?.addEventListener("click",async n=>{const b=n.target.closest('[id$="-restart"]');if(!b)return;const p=b.id.replace("-restart","");if(SITE.dnsServers[p]&&confirm(`Restart ${p.toUpperCase()} service?`))try{await withButton(b,"...",()=>o(p)),setTimeout(window.refreshAll,DC.DELAYS.RELOAD)}catch(s){showNotification("Restart failed: "+s.message,"error")}});async function m(n,b){const p=document.getElementById(`${n}-update`),s=p?.textContent||"\u2B06\uFE0F";try{p.textContent="\u{1F50D}",p.disabled=!0,p.title="Checking for updates...";const g=await(await fetch(`/api/v1/dns/check-update?server=${encodeURIComponent(b)}`)).json();if(!g.success)throw new Error(g.error||"Failed to check for updates");if(!g.updateAvailable){p.textContent="\u2705",p.title=`Already on latest version (${g.currentVersion})`,showNotification(`${n.toUpperCase()} is already up to date! Current version: ${g.currentVersion}`,"info"),setTimeout(()=>{p.textContent=s,p.disabled=!1,p.title="Update DNS server"},3e3);return}if(!confirm(`Update available for ${n.toUpperCase()}! + `)}function g(){let t=safeSessionGet("dashcaddy-encryption-key");if(t)return t;const c=safeGet("dashcaddy-encryption-key");if(c)return safeSessionSet("dashcaddy-encryption-key",c),safeRemove("dashcaddy-encryption-key"),c;const v=new Uint8Array(32);return crypto.getRandomValues(v),t=Array.from(v,d=>d.toString(16).padStart(2,"0")).join(""),safeSessionSet("dashcaddy-encryption-key",t),t}const i=g();function s(t,c){if(!t)return"";const v=crypto.getRandomValues(new Uint8Array(8)),d=Array.from(v,T=>T.toString(16).padStart(2,"0")).join(""),k=new TextEncoder().encode(c+d);let B="";for(let T=0;TparseInt($,16))),A=atob(t.substring(17)),E=new TextEncoder().encode(c+B);let L="";for(let $=0;${["readonly","admin"].forEach(c=>{["token","username"].forEach(v=>{safeRemove(`${t}-${c}-${v}-enc`)})}),safeRemove(`${t}-token-enc`),safeRemove(`${t}-username-enc`)})}function p(t){const c=m(t,"readonly"),v=r(t,"readonly"),d=m(t,"admin"),k=r(t,"admin"),B=h(safeGet(`${t}-token-enc`),i),T=h(safeGet(`${t}-username-enc`),i);return{username:k||v||T,token:d||c||B,readonlyToken:c||B,readonlyUsername:v||T,adminToken:d||B,adminUsername:k||T}}document.getElementById("manage-tokens")?.addEventListener("click",()=>{u();const t=document.getElementById("token-management-modal"),c=e();o().forEach(v=>{const d=c[v];document.getElementById(`${v}-readonly-username`).value=d.readonly.username,document.getElementById(`${v}-readonly-token`).value=d.readonly.token,document.getElementById(`${v}-admin-username`).value=d.admin.username,document.getElementById(`${v}-admin-token`).value=d.admin.token,document.getElementById(`${v}-token-status`).textContent=""}),t.classList.add("show")}),document.getElementById("token-management-modal")?.addEventListener("click",t=>{const c=t.target.closest(".token-toggle");if(c){const v=c.dataset.target,d=document.getElementById(v);d.type==="password"?(d.type="text",c.textContent="\u{1F648}"):(d.type="password",c.textContent="\u{1F441}");return}t.target.id==="token-management-modal"&&t.target.classList.remove("show")}),document.getElementById("token-save")?.addEventListener("click",async()=>{const t=o();t.forEach(d=>{y(d,"readonly",document.getElementById(`${d}-readonly-username`).value.trim()),l(d,"readonly",document.getElementById(`${d}-readonly-token`).value.trim()),y(d,"admin",document.getElementById(`${d}-admin-username`).value.trim()),l(d,"admin",document.getElementById(`${d}-admin-token`).value.trim())});const c={};let v=!1;if(t.forEach(d=>{const k={},B=document.getElementById(`${d}-readonly-username`).value.trim(),T=document.getElementById(`${d}-readonly-token`).value.trim(),A=document.getElementById(`${d}-admin-username`).value.trim(),E=document.getElementById(`${d}-admin-token`).value.trim();B&&T&&(k.readonly={username:B,password:T},v=!0),A&&E&&(k.admin={username:A,password:E},v=!0),Object.keys(k).length>0&&(c[d]=k)}),v){t.forEach(d=>{c[d]&&(document.getElementById(`${d}-token-status`).textContent="Verifying...",document.getElementById(`${d}-token-status`).className="token-status")});try{const k=await(await secureFetch("/api/v1/dns/credentials",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({servers:c})})).json();k.results?t.forEach(B=>{const T=document.getElementById(`${B}-token-status`);if(!c[B]){T.textContent="";return}const A=k.results[B];A?.success?(T.textContent="\u2713 Verified & saved",T.className="token-status success"):A?.partial?(T.textContent="\u2713 "+A.partial,T.className="token-status success"):(T.textContent="\u2717 "+(A?.error||"Login failed"),T.className="token-status error")}):k.success?t.forEach(B=>{c[B]&&(document.getElementById(`${B}-token-status`).textContent="\u2713 Saved",document.getElementById(`${B}-token-status`).className="token-status success")}):t.forEach(B=>{c[B]&&(document.getElementById(`${B}-token-status`).textContent="\u2717 "+(k.error||"Failed"),document.getElementById(`${B}-token-status`).className="token-status error")})}catch(d){console.error("Failed to sync DNS credentials to backend:",d),t.forEach(k=>{c[k]&&(document.getElementById(`${k}-token-status`).textContent="\u2713 Saved locally (sync failed)",document.getElementById(`${k}-token-status`).className="token-status")})}}else t.forEach(d=>{document.getElementById(`${d}-token-status`).textContent=""});setTimeout(()=>{t.every(k=>{const B=document.getElementById(`${k}-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.")){a(),o().forEach(t=>{document.getElementById(`${t}-readonly-username`).value="",document.getElementById(`${t}-readonly-token`).value="",document.getElementById(`${t}-admin-username`).value="",document.getElementById(`${t}-admin-token`).value="",document.getElementById(`${t}-token-status`).textContent="\u2713 Cleared",document.getElementById(`${t}-token-status`).className="token-status success"});try{await secureFetch("/api/v1/dns/credentials",{method:"DELETE"})}catch{}}}),window.getToken=m,window.getUsername=r,window.setToken=l,window.setUsername=y,window.getAllCredentials=e,window.getCredential=n,window.setCredential=w,window.getEncryptionKey=g,window.getDnsIds=o,window.getDnsDisplayName=f})(),(function(){function o(l,y,e=null){const a=document.getElementById(l+"-dot"),p=document.getElementById(l+"-pill"),t=document.getElementById(l+"-time"),c=document.querySelector(`[data-app="${l}"]`);a&&(a.classList.toggle("ok",y),a.classList.toggle("bad",!y)),p&&(p.textContent=y?"ON":"OFF",p.classList.toggle("on",y),p.classList.toggle("off",!y)),t&&e!==null&&(t.textContent=y?`${e}ms`:"timeout",t.className=`response-time ${f(e,y)}`),c&&c.setAttribute("data-status",y?"on":"off")}function f(l,y){return y?l<200?"excellent":l<500?"good":l<1e3?"fair":"slow":"timeout"}async function u(l){const y=performance.now();try{const e=await fetch("/probe/"+l,{cache:"no-store"}),a=performance.now(),p=Math.round(a-y);return{isUp:e.status>=200&&e.status<400||e.status===401||e.status===403,responseTime:p}}catch{const e=performance.now();return{isUp:!1,responseTime:Math.round(e-y)}}}window.APPS=[];let g=null,i=!1;async function s(){try{window.SkeletonLoader&&window.SkeletonLoader.show(6);const l=await fetch("/api/v1/services",{cache:"no-store"});l.ok?(window.APPS=await l.json(),window.SkeletonLoader&&window.SkeletonLoader.hide()):(console.error("Failed to load services:",l.status),window.SkeletonLoader&&window.SkeletonLoader.hide())}catch(l){console.error("Failed to load services:",l),window.SkeletonLoader&&window.SkeletonLoader.hide()}}function h(l){const y=window.APPS?.find(a=>a.id===l);if(y?.url)return y.url.startsWith("http")?y.url:"https://"+y.url;if(y?.isExternal&&y.externalUrl)return y.externalUrl;const e=SITE.dnsServers?.[l];return e?"http://"+e.ip+":"+(e.port||5380):buildServiceUrl(l)}function n(l,y,e){const a=document.createElement(l);return y&&(a.className=y),e&&(a.textContent=e),a}function w(){const l=document.getElementById("cards");l.innerHTML="";for(let y=0;y{D.stopPropagation(),window.openContainerLogsModal(e.containerId,e.name)},b.appendChild(S);const C=n("button","update-btn","\u2B06\uFE0F");C.title="Update container to latest version",C.id=`update-btn-${e.id}`,C.onclick=D=>{D.stopPropagation(),window.updateContainer(e.containerId,e.name,e.id)},b.appendChild(C);const P=n("button","exec-btn",">_");P.title="Open terminal",P.onclick=D=>{D.stopPropagation(),window.openExecModal&&window.openExecModal(e.containerId,e.name)},b.appendChild(P)}if(e.logPath&&!e.containerId){const S=n("button","logs-btn","\u{1F4CB}");S.title="View application logs",S.onclick=C=>{C.stopPropagation(),window.openFileLogsModal(e.logPath,e.name)},b.appendChild(S)}if(e.isExternal||e.appTemplate||e.url){const S=n("button","creds-btn","\u{1F511}");S.title="Auto-login credentials",S.id=`creds-btn-${e.id}`,S.onclick=C=>{C.stopPropagation(),window.openServiceCredsModal(e)},b.appendChild(S)}if(e.id!=="internet"){const S=n("button","options-btn","\u2699\uFE0F");S.title="Edit service settings",S.onclick=C=>{C.stopPropagation(),window.openServiceEditModal(e)},b.appendChild(S)}if(e.id!=="internet"){const S=n("button","delete-btn","\u{1F5D1}\uFE0F");S.title="Delete this service",S.onclick=C=>{C.stopPropagation(),window.deleteService(e.id,e.name)},b.appendChild(S)}const I=n("button",null,"Open");I.onclick=()=>window.open(h(e.id),"_blank","noopener"),b.appendChild(I),a.appendChild(b),a.style.transitionDelay=`${Math.min(y*45,270)}ms`,l.appendChild(a)}requestAnimationFrame(()=>{l.querySelectorAll(".card").forEach(y=>y.classList.add("loaded"))}),window.groupRecipeCards&&requestAnimationFrame(()=>window.groupRecipeCards())}function m(l,y,e=null){const a=document.getElementById("dot-"+l+"-grid"),p=document.getElementById("badge-"+l),t=document.getElementById("time-"+l),c=document.querySelector(`[data-app="${l}"]`);a&&(a.classList.toggle("ok",y),a.classList.toggle("bad",!y)),p&&(p.textContent=y?"ON":"OFF",p.classList.toggle("on",y),p.classList.toggle("off",!y)),t&&e!==null&&(t.textContent=y?`${e}ms`:"timeout",t.className=`response-time ${f(e,y)}`),c&&c.setAttribute("data-status",y?"on":"off")}async function r(){if(g)return i=!0,g;function l(a,p=new Date){const t=document.getElementById("stamp");t&&(t.textContent=`${a}: ${new Date(p).toLocaleTimeString()}`)}function y(a){Object.keys(SITE.dnsServers).forEach(t=>{const c=a[t];c&&o(t,c.isUp,c.responseTime)}),a.internet&&o("internet",a.internet.isUp,a.internet.responseTime),window.APPS.forEach(t=>{const c=a[t.id];c&&m(t.id,c.isUp,c.responseTime)})}async function e(){const a=Object.keys(SITE.dnsServers),p=a.map(d=>u(d));p.push(u("internet"));const t=await Promise.all(p);a.forEach((d,k)=>o(d,t[k].isUp,t[k].responseTime));const c=t[t.length-1];o("internet",c.isUp,c.responseTime),(await Promise.all(window.APPS.map(async d=>{const k=await u(d.id);return{id:d.id,...k}}))).forEach(d=>{m(d.id,d.isUp,d.responseTime)})}return g=(async()=>{try{const a=await fetch("/api/v1/services/status",{cache:"no-store"});if(!a.ok)throw new Error(`Status refresh failed (${a.status})`);const p=await a.json();y(p.statuses||{}),l("last check",p.checkedAt||new Date)}catch(a){console.warn("Batched status refresh failed, falling back to direct probes:",a);try{await e(),l("last check")}catch(p){console.error("Dashboard refresh failed:",p),l("last failed")}}finally{g=null,i&&(i=!1,setTimeout(()=>{window.refreshAll()},0))}})(),g}document.querySelector(".top")?.addEventListener("click",l=>{const y=l.target.closest('[id$="-open"]');if(!y)return;const e=y.id.replace("-open","");SITE.dnsServers[e]&&window.open(h(e),"_blank","noopener")}),document.getElementById("ca-open")?.addEventListener("click",()=>window.open(h("ca"),"_blank","noopener")),document.getElementById("creds-btn-ca")?.addEventListener("click",l=>{l.stopPropagation();const y=window.APPS.find(e=>e.id==="ca");y&&window.openServiceCredsModal&&window.openServiceCredsModal(y)}),document.getElementById("options-btn-ca")?.addEventListener("click",l=>{l.stopPropagation();const y=window.APPS.find(e=>e.id==="ca");y&&window.openServiceEditModal&&window.openServiceEditModal(y)}),document.getElementById("delete-btn-ca")?.addEventListener("click",l=>{l.stopPropagation(),window.deleteService&&window.deleteService("ca","DashCA")}),window.loadServices=s,window.buildGrid=w,window.refreshAll=r,window.setQuick=o,window.setBadge=m,window.getResponseTimeClass=f,window.checkServiceWithTiming=u,window.serviceUrl=h,window.el=n})(),(function(){async function o(n){const m=await(await secureFetch(`/api/v1/dns/restart/${n}`,{method:"POST"})).json();if(!m.success)throw new Error(m.error||"Restart failed");return m}document.querySelector(".top")?.addEventListener("click",async n=>{const w=n.target.closest('[id$="-restart"]');if(!w)return;const m=w.id.replace("-restart","");if(SITE.dnsServers[m]&&confirm(`Restart ${m.toUpperCase()} service?`))try{await withButton(w,"...",()=>o(m)),setTimeout(window.refreshAll,DC.DELAYS.RELOAD)}catch(r){showNotification("Restart failed: "+r.message,"error")}});async function f(n,w){const m=document.getElementById(`${n}-update`),r=m?.textContent||"\u2B06\uFE0F";try{m.textContent="\u{1F50D}",m.disabled=!0,m.title="Checking for updates...";const y=await(await fetch(`/api/v1/dns/check-update?server=${encodeURIComponent(w)}`)).json();if(!y.success)throw new Error(y.error||"Failed to check for updates");if(!y.updateAvailable){m.textContent="\u2705",m.title=`Already on latest version (${y.currentVersion})`,showNotification(`${n.toUpperCase()} is already up to date! Current version: ${y.currentVersion}`,"info"),setTimeout(()=>{m.textContent=r,m.disabled=!1,m.title="Update DNS server"},3e3);return}if(!confirm(`Update available for ${n.toUpperCase()}! -Current: ${g.currentVersion} -New: ${g.updateVersion} +Current: ${y.currentVersion} +New: ${y.updateVersion} -`+(g.updateTitle?`${g.updateTitle} +`+(y.updateTitle?`${y.updateTitle} `:"")+`The DNS server will restart during the update. -Proceed?`)){p.textContent=s,p.disabled=!1,p.title="Update DNS server";return}p.textContent="\u{1F504}",p.title="Updating...";const u=await(await secureFetch(`/api/v1/dns/update?server=${encodeURIComponent(b)}`,{method:"POST"})).json();if(!u.success)throw new Error(u.error||"Update failed");if(u.manualUpdateRequired){p.textContent="\u2B06\uFE0F",p.title=`Update available: ${u.newVersion}`;const t=u.downloadLink?` -Download: ${u.downloadLink}`:"",d=u.instructionsLink?` -Instructions: ${u.instructionsLink}`:"";showNotification(`${n.toUpperCase()} update requires manual installation. Current: ${u.previousVersion} \u2192 ${u.newVersion}. Please update manually on the host machine.`,"warning",8e3),p.disabled=!1;return}p.textContent="\u2705",p.title="Updated successfully!",showNotification(`${n.toUpperCase()} updated successfully! ${u.previousVersion} \u2192 ${u.newVersion}. Server is restarting...`,"success"),setTimeout(()=>{p.textContent=s,p.disabled=!1,p.title="Update DNS server",window.refreshAll()},1e4)}catch(c){console.error("DNS update error:",c),p.textContent="\u274C",p.title="Update failed",showNotification(`Failed to update ${n.toUpperCase()}: ${c.message}`,"error"),setTimeout(()=>{p.textContent=s,p.disabled=!1,p.title="Update DNS server"},3e3)}}document.querySelector(".top")?.addEventListener("click",n=>{const b=n.target.closest('[id$="-update"]');if(!b)return;const p=b.id.replace("-update","");SITE.dnsServers[p]&&m(p,SITE.dnsServers[p]?.ip)}),injectModal("dns-settings-modal",` +Proceed?`)){m.textContent=r,m.disabled=!1,m.title="Update DNS server";return}m.textContent="\u{1F504}",m.title="Updating...";const p=await(await secureFetch(`/api/v1/dns/update?server=${encodeURIComponent(w)}`,{method:"POST"})).json();if(!p.success)throw new Error(p.error||"Update failed");if(p.manualUpdateRequired){m.textContent="\u2B06\uFE0F",m.title=`Update available: ${p.newVersion}`;const t=p.downloadLink?` +Download: ${p.downloadLink}`:"",c=p.instructionsLink?` +Instructions: ${p.instructionsLink}`:"";showNotification(`${n.toUpperCase()} update requires manual installation. Current: ${p.previousVersion} \u2192 ${p.newVersion}. Please update manually on the host machine.`,"warning",8e3),m.disabled=!1;return}m.textContent="\u2705",m.title="Updated successfully!",showNotification(`${n.toUpperCase()} updated successfully! ${p.previousVersion} \u2192 ${p.newVersion}. Server is restarting...`,"success"),setTimeout(()=>{m.textContent=r,m.disabled=!1,m.title="Update DNS server",window.refreshAll()},1e4)}catch(l){console.error("DNS update error:",l),m.textContent="\u274C",m.title="Update failed",showNotification(`Failed to update ${n.toUpperCase()}: ${l.message}`,"error"),setTimeout(()=>{m.textContent=r,m.disabled=!1,m.title="Update DNS server"},3e3)}}document.querySelector(".top")?.addEventListener("click",n=>{const w=n.target.closest('[id$="-update"]');if(!w)return;const m=w.id.replace("-update","");SITE.dnsServers[m]&&f(m,SITE.dnsServers[m]?.ip)}),injectModal("dns-settings-modal",`

DNS Settings

@@ -300,7 +318,7 @@ Instructions: ${u.instructionsLink}`:"";showNotification(`${n.toUpperCase()} upd
- `);let l=null;function w(n){l=n;const b=SITE.dnsServers[n]||{},p=document.getElementById("dns-settings-modal");document.getElementById("dns-settings-title").textContent=`${(b.name||n).toUpperCase()} Settings`,document.getElementById("dns-edit-ip").value=b.ip||"",document.getElementById("dns-edit-port").value=b.port||DC.DEFAULTS.DNS_PORT,document.getElementById("dns-edit-name").value=b.name||"",p.classList.add("show")}async function v(){if(!l)return;const n=document.getElementById("dns-edit-ip").value.trim(),b=document.getElementById("dns-edit-port").value.trim()||DC.DEFAULTS.DNS_PORT,p=document.getElementById("dns-edit-name").value.trim();if(!n){showNotification("Server IP is required","warning");return}const s={dnsServers:{}};s.dnsServers[l]={ip:n,port:String(b)},p&&(s.dnsServers[l].name=p);try{const g=await(await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)})).json();g.success?(SITE.dnsServers[l]=s.dnsServers[l],showNotification(`${l.toUpperCase()} settings saved`,"success"),h(),window.refreshAll()):showNotification(g.error||"Failed to save settings","error")}catch(c){showNotification("Failed to save: "+c.message,"error")}}async function r(){if(l&&confirm(`Remove ${l.toUpperCase()} from dashboard? This won't affect the actual DNS server.`))try{const b=await(await secureFetch("/api/v1/config")).json();b.dnsServers&&delete b.dnsServers[l];const s=await(await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({dnsServers:b.dnsServers||{}})})).json();if(s.success){delete SITE.dnsServers[l];const c=document.querySelector(`.top [data-app="${l}"]`);c&&c.remove(),showNotification(`${l.toUpperCase()} removed from dashboard`,"success"),h()}else showNotification(s.error||"Failed to remove","error")}catch(n){showNotification("Failed to remove: "+n.message,"error")}}function h(){closeModal("dns-settings-modal"),l=null}document.getElementById("dns-settings-cancel")?.addEventListener("click",h),document.getElementById("dns-settings-save")?.addEventListener("click",v),document.getElementById("dns-settings-delete")?.addEventListener("click",r),document.getElementById("dns-settings-modal")?.addEventListener("click",n=>{n.target.id==="dns-settings-modal"&&h()}),document.querySelector(".top")?.addEventListener("click",n=>{const b=n.target.closest('[id$="-settings"]');if(!b)return;const p=b.id.replace("-settings","");SITE.dnsServers[p]&&(n.stopPropagation(),w(p))}),document.getElementById("refresh")?.addEventListener("click",window.refreshAll)})(),(function(){injectModal("logs-modal",` + `);let u=null;function g(n){u=n;const w=SITE.dnsServers[n]||{},m=document.getElementById("dns-settings-modal");document.getElementById("dns-settings-title").textContent=`${(w.name||n).toUpperCase()} Settings`,document.getElementById("dns-edit-ip").value=w.ip||"",document.getElementById("dns-edit-port").value=w.port||DC.DEFAULTS.DNS_PORT,document.getElementById("dns-edit-name").value=w.name||"",m.classList.add("show")}async function i(){if(!u)return;const n=document.getElementById("dns-edit-ip").value.trim(),w=document.getElementById("dns-edit-port").value.trim()||DC.DEFAULTS.DNS_PORT,m=document.getElementById("dns-edit-name").value.trim();if(!n){showNotification("Server IP is required","warning");return}const r={dnsServers:{}};r.dnsServers[u]={ip:n,port:String(w)},m&&(r.dnsServers[u].name=m);try{const y=await(await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)})).json();y.success?(SITE.dnsServers[u]=r.dnsServers[u],showNotification(`${u.toUpperCase()} settings saved`,"success"),h(),window.refreshAll()):showNotification(y.error||"Failed to save settings","error")}catch(l){showNotification("Failed to save: "+l.message,"error")}}async function s(){if(u&&confirm(`Remove ${u.toUpperCase()} from dashboard? This won't affect the actual DNS server.`))try{const w=await(await secureFetch("/api/v1/config")).json();w.dnsServers&&delete w.dnsServers[u];const r=await(await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({dnsServers:w.dnsServers||{}})})).json();if(r.success){delete SITE.dnsServers[u];const l=document.querySelector(`.top [data-app="${u}"]`);l&&l.remove(),showNotification(`${u.toUpperCase()} removed from dashboard`,"success"),h()}else showNotification(r.error||"Failed to remove","error")}catch(n){showNotification("Failed to remove: "+n.message,"error")}}function h(){closeModal("dns-settings-modal"),u=null}document.getElementById("dns-settings-cancel")?.addEventListener("click",h),document.getElementById("dns-settings-save")?.addEventListener("click",i),document.getElementById("dns-settings-delete")?.addEventListener("click",s),document.getElementById("dns-settings-modal")?.addEventListener("click",n=>{n.target.id==="dns-settings-modal"&&h()}),document.querySelector(".top")?.addEventListener("click",n=>{const w=n.target.closest('[id$="-settings"]');if(!w)return;const m=w.id.replace("-settings","");SITE.dnsServers[m]&&(n.stopPropagation(),g(m))}),document.getElementById("refresh")?.addEventListener("click",window.refreshAll)})(),(function(){injectModal("logs-modal",`
@@ -324,66 +342,66 @@ Instructions: ${u.instructionsLink}`:"";showNotification(`${n.toUpperCase()} upd
- `);let o=null,m=null,l=!1,w=null,v=null,r=!1,h=null,n=null,b=!1,p=null,s=!1;async function c(x,y=25){try{const I=getDnsServerAddr(x),S=await fetch(`/api/v1/dns/logs?server=${I}&limit=${y}`,{cache:"no-store",headers:{Accept:"application/json","Cache-Control":"no-cache"}});if(S.ok){const C=await S.json();return C.success&&C.logs?{logs:C.logs,count:C.count,server:C.server}:{error:C.error||"Failed to fetch logs"}}else return S.status===401?{error:"DNS auto-auth failed - check credentials in settings"}:{error:`HTTP ${S.status}`}}catch(I){return console.error("DNS logs fetch failed:",I),{error:I.message}}}function g(x){return{NoError:"var(--ok-fg)",NOERROR:"var(--ok-fg)",NxDomain:"var(--muted)",NXDOMAIN:"var(--muted)",Refused:"var(--bad-fg)",REFUSED:"var(--bad-fg)",ServerFailure:"#f39c12",SERVFAIL:"#f39c12"}[x]||"var(--fg)"}function e(x){const y=document.createElement("div");if(y.className="log-entry",y.style.cssText="display: grid; grid-template-columns: 140px 110px 1fr 70px 80px; gap: 8px; padding: 6px 10px; border-bottom: 1px solid var(--border); font-size: 0.8rem; align-items: center;",x.parsed===!1)return y.style.gridTemplateColumns="1fr",y.innerHTML=`${escapeHtml(x.raw)}`,y;const I=g(x.rcode),S=x.rcode==="Refused"||x.rcode==="REFUSED";return y.innerHTML=` + `);let o=null,f=null,u=!1,g=null,i=null,s=!1,h=null,n=null,w=!1,m=null,r=!1;async function l(x,b=25){try{const I=getDnsServerAddr(x),S=await fetch(`/api/v1/dns/logs?server=${I}&limit=${b}`,{cache:"no-store",headers:{Accept:"application/json","Cache-Control":"no-cache"}});if(S.ok){const C=await S.json();return C.success&&C.logs?{logs:C.logs,count:C.count,server:C.server}:{error:C.error||"Failed to fetch logs"}}else return S.status===401?{error:"DNS auto-auth failed - check credentials in settings"}:{error:`HTTP ${S.status}`}}catch(I){return console.error("DNS logs fetch failed:",I),{error:I.message}}}function y(x){return{NoError:"var(--ok-fg)",NOERROR:"var(--ok-fg)",NxDomain:"var(--muted)",NXDOMAIN:"var(--muted)",Refused:"var(--bad-fg)",REFUSED:"var(--bad-fg)",ServerFailure:"#f39c12",SERVFAIL:"#f39c12"}[x]||"var(--fg)"}function e(x){const b=document.createElement("div");if(b.className="log-entry",b.style.cssText="display: grid; grid-template-columns: 140px 110px 1fr 70px 80px; gap: 8px; padding: 6px 10px; border-bottom: 1px solid var(--border); font-size: 0.8rem; align-items: center;",x.parsed===!1)return b.style.gridTemplateColumns="1fr",b.innerHTML=`${escapeHtml(x.raw)}`,b;const I=y(x.rcode),S=x.rcode==="Refused"||x.rcode==="REFUSED";return b.innerHTML=` ${escapeHtml(x.timestamp)} ${escapeHtml(x.client)} ${escapeHtml(x.domain)} ${escapeHtml(x.type)} ${escapeHtml(x.rcode)} - `,y}async function a(){if(b){await L();return}if(r){await B();return}if(l||!o)return;const x=parseInt(document.getElementById("log-lines").value),y=document.getElementById("logs-content");try{const I=await c(o,x);if(I.error){y.innerHTML=` + `,b}async function a(){if(w){await L();return}if(s){await B();return}if(u||!o)return;const x=parseInt(document.getElementById("log-lines").value),b=document.getElementById("logs-content");try{const I=await l(o,x);if(I.error){b.innerHTML=`
\u26A0\uFE0F Error
${escapeHtml(I.error)}
-
`;return}y.innerHTML=` + `;return}b.innerHTML=`
Time Client Domain Type Status -
`,I.logs&&I.logs.length>0?I.logs.forEach(S=>{const C=e(S);y.appendChild(C)}):y.innerHTML+=` + `,I.logs&&I.logs.length>0?I.logs.forEach(S=>{const C=e(S);b.appendChild(C)}):b.innerHTML+=`
No DNS queries logged yet -
`}catch(I){y.innerHTML=` + `}catch(I){b.innerHTML=`
Failed to fetch logs: ${escapeHtml(I.message)} -
`}}function u(x){o=x,l=!1,r=!1;const y=document.getElementById("logs-modal"),I=document.getElementById("logs-title"),S=document.getElementById("logs-pause"),C=document.getElementById("logs-stream");I.textContent=`${x.toUpperCase()} DNS Logs`,S.textContent="\u23F8\uFE0F Pause",S.classList.remove("paused"),C&&(C.style.display="none"),y.classList.add("show"),a(),m=setInterval(a,DC.POLL.LOGS)}function t(){document.getElementById("logs-modal").classList.remove("show"),m&&(clearInterval(m),m=null),f(),o=null,r=!1,w=null,v=null,b=!1,h=null,n=null,l=!1}function d(x){p&&f();const y=document.getElementById("logs-stream"),I=document.getElementById("logs-pause"),S=document.getElementById("logs-content");m&&(clearInterval(m),m=null);try{p=new EventSource(`/api/v1/logs/stream/${x}`),s=!0,y.classList.add("active"),y.textContent="\u{1F534} Live",y.title="Streaming - click to stop",I.style.display="none";const C=document.getElementById("logs-title");C.textContent.includes("\u{1F534}")||(C.innerHTML=C.textContent.replace("\u{1F4CB}","\u{1F4CB} \u{1F534}")),p.onmessage=P=>{try{const D=JSON.parse(P.data);if(D.error){console.error("Stream error:",D.error),f();return}const O=document.createElement("div");O.className="log-entry",O.style.cssText="display: flex; gap: 12px; padding: 6px 10px; border-bottom: 1px solid var(--border); font-size: 0.8rem; align-items: flex-start; font-family: monospace;";const R=(D.stream||"stdout")==="stderr",U=R?"var(--bad-fg)":"var(--fg)",M=`${R?"STDERR":"STDOUT"}`;for(O.innerHTML=` + `}}function p(x){o=x,u=!1,s=!1;const b=document.getElementById("logs-modal"),I=document.getElementById("logs-title"),S=document.getElementById("logs-pause"),C=document.getElementById("logs-stream");I.textContent=`${x.toUpperCase()} DNS Logs`,S.textContent="\u23F8\uFE0F Pause",S.classList.remove("paused"),C&&(C.style.display="none"),b.classList.add("show"),a(),f=setInterval(a,DC.POLL.LOGS)}function t(){document.getElementById("logs-modal").classList.remove("show"),f&&(clearInterval(f),f=null),v(),o=null,s=!1,g=null,i=null,w=!1,h=null,n=null,u=!1}function c(x){m&&v();const b=document.getElementById("logs-stream"),I=document.getElementById("logs-pause"),S=document.getElementById("logs-content");f&&(clearInterval(f),f=null);try{m=new EventSource(`/api/v1/logs/stream/${x}`),r=!0,b.classList.add("active"),b.textContent="\u{1F534} Live",b.title="Streaming - click to stop",I.style.display="none";const C=document.getElementById("logs-title");C.textContent.includes("\u{1F534}")||(C.innerHTML=C.textContent.replace("\u{1F4CB}","\u{1F4CB} \u{1F534}")),m.onmessage=P=>{try{const D=JSON.parse(P.data);if(D.error){console.error("Stream error:",D.error),v();return}const O=document.createElement("div");O.className="log-entry",O.style.cssText="display: flex; gap: 12px; padding: 6px 10px; border-bottom: 1px solid var(--border); font-size: 0.8rem; align-items: flex-start; font-family: monospace;";const R=(D.stream||"stdout")==="stderr",U=R?"var(--bad-fg)":"var(--fg)",M=`${R?"STDERR":"STDOUT"}`;for(O.innerHTML=`
${M}
${escapeHtml(D.text)}
- `,S.appendChild(O),S.scrollTop=S.scrollHeight;S.children.length>500;)S.removeChild(S.firstChild)}catch(D){console.error("Error parsing stream data:",D)}},p.onerror=P=>{console.error("EventSource error:",P),f()}}catch(C){console.error("Failed to start streaming:",C),f()}}function f(){p&&(p.close(),p=null),s=!1;const x=document.getElementById("logs-stream"),y=document.getElementById("logs-pause"),I=document.getElementById("logs-title");x&&(x.classList.remove("active"),x.textContent="\u{1F4E1} Live",x.title="Enable real-time streaming"),y&&(y.style.display=""),I&&(I.textContent=I.textContent.replace(" \u{1F534}","")),r&&w&&!m&&(m=setInterval(B,DC.POLL.LOGS))}async function i(x,y=100){try{const I=`/api/v1/logs/container/${x}?tail=${y}×tamps=true`,S=await fetch(I,{cache:"no-store",headers:{Accept:"application/json","Cache-Control":"no-cache"}});if(S.ok){const C=await S.json();return C.success&&C.logs?{logs:C.logs,count:C.count,containerName:C.containerName,containerId:C.containerId}:{error:C.error||"Failed to fetch container logs"}}else return{error:`HTTP ${S.status}: ${S.statusText}`}}catch(I){return console.error("Container logs fetch failed:",I),{error:I.message}}}function k(x){const y=document.createElement("div");y.className="log-entry",y.style.cssText="display: flex; gap: 12px; padding: 6px 10px; border-bottom: 1px solid var(--border); font-size: 0.8rem; align-items: flex-start; font-family: monospace;";const I=x.stream==="stderr"?"var(--bad-fg)":"var(--fg)",S=x.stream==="stderr"?'STDERR':'STDOUT';return y.innerHTML=` + `,S.appendChild(O),S.scrollTop=S.scrollHeight;S.children.length>500;)S.removeChild(S.firstChild)}catch(D){console.error("Error parsing stream data:",D)}},m.onerror=P=>{console.error("EventSource error:",P),v()}}catch(C){console.error("Failed to start streaming:",C),v()}}function v(){m&&(m.close(),m=null),r=!1;const x=document.getElementById("logs-stream"),b=document.getElementById("logs-pause"),I=document.getElementById("logs-title");x&&(x.classList.remove("active"),x.textContent="\u{1F4E1} Live",x.title="Enable real-time streaming"),b&&(b.style.display=""),I&&(I.textContent=I.textContent.replace(" \u{1F534}","")),s&&g&&!f&&(f=setInterval(B,DC.POLL.LOGS))}async function d(x,b=100){try{const I=`/api/v1/logs/container/${x}?tail=${b}×tamps=true`,S=await fetch(I,{cache:"no-store",headers:{Accept:"application/json","Cache-Control":"no-cache"}});if(S.ok){const C=await S.json();return C.success&&C.logs?{logs:C.logs,count:C.count,containerName:C.containerName,containerId:C.containerId}:{error:C.error||"Failed to fetch container logs"}}else return{error:`HTTP ${S.status}: ${S.statusText}`}}catch(I){return console.error("Container logs fetch failed:",I),{error:I.message}}}function k(x){const b=document.createElement("div");b.className="log-entry",b.style.cssText="display: flex; gap: 12px; padding: 6px 10px; border-bottom: 1px solid var(--border); font-size: 0.8rem; align-items: flex-start; font-family: monospace;";const I=x.stream==="stderr"?"var(--bad-fg)":"var(--fg)",S=x.stream==="stderr"?'STDERR':'STDOUT';return b.innerHTML=`
${S}
${escapeHtml(x.text)}
- `,y}async function B(){if(l||!w||!r)return;const x=parseInt(document.getElementById("log-lines").value),y=document.getElementById("logs-content");try{const I=await i(w,x);if(I.error){y.innerHTML=` + `,b}async function B(){if(u||!g||!s)return;const x=parseInt(document.getElementById("log-lines").value),b=document.getElementById("logs-content");try{const I=await d(g,x);if(I.error){b.innerHTML=`
\u26A0\uFE0F Error
${escapeHtml(I.error)}
-
`;return}y.innerHTML=` + `;return}b.innerHTML=`
Stream Log Output -
`,I.logs&&I.logs.length>0?(I.logs.forEach(S=>{const C=k(S);y.appendChild(C)}),y.scrollTop=y.scrollHeight):y.innerHTML+=` + `,I.logs&&I.logs.length>0?(I.logs.forEach(S=>{const C=k(S);b.appendChild(C)}),b.scrollTop=b.scrollHeight):b.innerHTML+=`
No logs available for this container -
`}catch(I){y.innerHTML=` + `}catch(I){b.innerHTML=`
Failed to fetch logs: ${escapeHtml(I.message)} -
`}}function T(x,y){w=x,v=y,r=!0,b=!1,l=!1,f();const I=document.getElementById("logs-modal"),S=document.getElementById("logs-title"),C=document.getElementById("logs-pause"),P=document.getElementById("logs-stream");S.textContent=`\u{1F4CB} ${y} - Container Logs`,C.textContent="\u23F8\uFE0F Pause",C.classList.remove("paused"),P&&(P.style.display=""),I.classList.add("show"),B(),m=setInterval(B,DC.POLL.LOGS)}async function A(x,y=100){try{const I=`/api/v1/logs/file?path=${encodeURIComponent(x)}&tail=${y}`,S=await fetch(I,{cache:"no-store",headers:{Accept:"application/json","Cache-Control":"no-cache"}});if(S.ok){const C=await S.json();return C.success&&C.logs?{logs:C.logs,count:C.count,logPath:C.logPath,totalLines:C.totalLines}:{error:C.error||"Failed to fetch file logs"}}else return{error:(await S.json().catch(()=>({}))).error||`HTTP ${S.status}`}}catch(I){return console.error("File logs fetch failed:",I),{error:I.message}}}function E(x){const y=document.createElement("div");y.className="log-entry",y.style.cssText="display: flex; gap: 12px; padding: 6px 10px; border-bottom: 1px solid var(--border); font-size: 0.8rem; align-items: flex-start; font-family: monospace;";const I=x.text;let S="INFO",C="var(--fg)";I.match(/ERROR|FATAL|CRITICAL/i)?(S="ERROR",C="var(--bad-fg)"):I.match(/WARN|WARNING/i)?(S="WARN",C="#f39c12"):I.match(/DEBUG/i)&&(S="DEBUG",C="var(--muted)");const D=`${S}`;return y.innerHTML=` + `}}function T(x,b){g=x,i=b,s=!0,w=!1,u=!1,v();const I=document.getElementById("logs-modal"),S=document.getElementById("logs-title"),C=document.getElementById("logs-pause"),P=document.getElementById("logs-stream");S.textContent=`\u{1F4CB} ${b} - Container Logs`,C.textContent="\u23F8\uFE0F Pause",C.classList.remove("paused"),P&&(P.style.display=""),I.classList.add("show"),B(),f=setInterval(B,DC.POLL.LOGS)}async function A(x,b=100){try{const I=`/api/v1/logs/file?path=${encodeURIComponent(x)}&tail=${b}`,S=await fetch(I,{cache:"no-store",headers:{Accept:"application/json","Cache-Control":"no-cache"}});if(S.ok){const C=await S.json();return C.success&&C.logs?{logs:C.logs,count:C.count,logPath:C.logPath,totalLines:C.totalLines}:{error:C.error||"Failed to fetch file logs"}}else return{error:(await S.json().catch(()=>({}))).error||`HTTP ${S.status}`}}catch(I){return console.error("File logs fetch failed:",I),{error:I.message}}}function E(x){const b=document.createElement("div");b.className="log-entry",b.style.cssText="display: flex; gap: 12px; padding: 6px 10px; border-bottom: 1px solid var(--border); font-size: 0.8rem; align-items: flex-start; font-family: monospace;";const I=x.text;let S="INFO",C="var(--fg)";I.match(/ERROR|FATAL|CRITICAL/i)?(S="ERROR",C="var(--bad-fg)"):I.match(/WARN|WARNING/i)?(S="WARN",C="#f39c12"):I.match(/DEBUG/i)&&(S="DEBUG",C="var(--muted)");const D=`${S}`;return b.innerHTML=`
${D}
${escapeHtml(I)}
- `,y}async function L(){if(l||!h||!b)return;const x=parseInt(document.getElementById("log-lines").value),y=document.getElementById("logs-content");try{const I=await A(h,x);if(I.error){y.innerHTML=` + `,b}async function L(){if(u||!h||!w)return;const x=parseInt(document.getElementById("log-lines").value),b=document.getElementById("logs-content");try{const I=await A(h,x);if(I.error){b.innerHTML=`
\u26A0\uFE0F Error
${escapeHtml(I.error)}
-
`;return}y.innerHTML=` + `;return}b.innerHTML=`
Log Output (${I.count} of ${I.totalLines} lines) -
`,I.logs&&I.logs.length>0?(I.logs.forEach(S=>{const C=E(S);y.appendChild(C)}),y.scrollTop=y.scrollHeight):y.innerHTML+=` + `,I.logs&&I.logs.length>0?(I.logs.forEach(S=>{const C=E(S);b.appendChild(C)}),b.scrollTop=b.scrollHeight):b.innerHTML+=`
No logs available in this file -
`}catch(I){y.innerHTML=` + `}catch(I){b.innerHTML=`
Failed to fetch logs: ${escapeHtml(I.message)} -
`}}function $(x,y){h=x,n=y,b=!0,r=!1,l=!1;const I=document.getElementById("logs-modal"),S=document.getElementById("logs-title"),C=document.getElementById("logs-pause"),P=document.getElementById("logs-stream");S.textContent=`\u{1F4CB} ${y} - Application Logs`,C.textContent="\u23F8\uFE0F Pause",C.classList.remove("paused"),P&&(P.style.display="none"),I.classList.add("show"),L(),m=setInterval(L,DC.POLL.LOGS)}document.querySelector(".top")?.addEventListener("click",x=>{const y=x.target.closest('[id$="-logs"]');if(!y)return;const I=y.id.replace("-logs","");SITE.dnsServers[I]&&u(I)}),document.getElementById("logs-close")?.addEventListener("click",t),document.getElementById("logs-pause")?.addEventListener("click",()=>{l=!l;const x=document.getElementById("logs-pause");l?(x.textContent="\u25B6\uFE0F Resume",x.classList.add("paused")):(x.textContent="\u23F8\uFE0F Pause",x.classList.remove("paused"),a())}),document.getElementById("log-lines")?.addEventListener("change",()=>{l||a()}),document.getElementById("logs-stream")?.addEventListener("click",()=>{!r||!w||(s?f():d(w))}),document.getElementById("logs-modal")?.addEventListener("click",x=>{x.target.id==="logs-modal"&&t()}),document.addEventListener("keydown",x=>{x.key==="Escape"&&document.getElementById("logs-modal")?.classList.contains("show")&&t()}),window.openContainerLogsModal=T,window.openFileLogsModal=$,window.openLogsModal=u})(),(function(){injectModal("service-edit-modal",` + `}}function $(x,b){h=x,n=b,w=!0,s=!1,u=!1;const I=document.getElementById("logs-modal"),S=document.getElementById("logs-title"),C=document.getElementById("logs-pause"),P=document.getElementById("logs-stream");S.textContent=`\u{1F4CB} ${b} - Application Logs`,C.textContent="\u23F8\uFE0F Pause",C.classList.remove("paused"),P&&(P.style.display="none"),I.classList.add("show"),L(),f=setInterval(L,DC.POLL.LOGS)}document.querySelector(".top")?.addEventListener("click",x=>{const b=x.target.closest('[id$="-logs"]');if(!b)return;const I=b.id.replace("-logs","");SITE.dnsServers[I]&&p(I)}),document.getElementById("logs-close")?.addEventListener("click",t),document.getElementById("logs-pause")?.addEventListener("click",()=>{u=!u;const x=document.getElementById("logs-pause");u?(x.textContent="\u25B6\uFE0F Resume",x.classList.add("paused")):(x.textContent="\u23F8\uFE0F Pause",x.classList.remove("paused"),a())}),document.getElementById("log-lines")?.addEventListener("change",()=>{u||a()}),document.getElementById("logs-stream")?.addEventListener("click",()=>{!s||!g||(r?v():c(g))}),document.getElementById("logs-modal")?.addEventListener("click",x=>{x.target.id==="logs-modal"&&t()}),document.addEventListener("keydown",x=>{x.key==="Escape"&&document.getElementById("logs-modal")?.classList.contains("show")&&t()}),window.openContainerLogsModal=T,window.openFileLogsModal=$,window.openLogsModal=p})(),(function(){injectModal("service-edit-modal",`

Edit Service

@@ -712,44 +730,44 @@ Instructions: ${u.instructionsLink}`:"";showNotification(`${n.toUpperCase()} upd
- `)})(),(function(){async function o(r){try{const h=await fetch(`/api/v1/caddy/cas?caddyfilePath=${encodeURIComponent(r)}`);if(!h.ok)throw new Error(`Failed to load CAs: ${h.status}`);const n=await h.json();if(n.status==="success"){const b=document.getElementById("existing-ca-select");return b.innerHTML="",n.data.cas.length===0?b.innerHTML='':(b.innerHTML='',n.data.cas.forEach(p=>{const s=document.createElement("option");typeof p=="object"?(s.value=p.id,s.textContent=p.displayName||p.name):(s.value=p,s.textContent=p),b.appendChild(s)})),n.data.cas}else throw new Error(n.message)}catch(h){console.error("Error loading CAs:",h);const n=document.getElementById("existing-ca-select");return n.innerHTML='',[]}}function m(r){const{subdomain:h,port:n,ip:b,sslType:p,caName:s,existingCa:c,enableAuth:g,enableCors:e,customHeaders:a,upstreamPath:u,healthCheck:t,timeout:d,tailscaleOnly:f}=r;let i=`${buildDomain(h)} { -`;switch(f&&(i+=` @blocked not remote_ip 100.64.0.0/10 -`,i+=` respond @blocked "Access denied. Tailscale connection required." 403 -`),p){case"letsencrypt":break;case"caddy-managed":i+=` tls internal -`;break;case"existing-ca":c&&(i+=` tls { - ca ${c} + `)})(),(function(){async function o(s){try{const h=await fetch(`/api/v1/caddy/cas?caddyfilePath=${encodeURIComponent(s)}`);if(!h.ok)throw new Error(`Failed to load CAs: ${h.status}`);const n=await h.json();if(n.status==="success"){const w=document.getElementById("existing-ca-select");return w.innerHTML="",n.data.cas.length===0?w.innerHTML='':(w.innerHTML='',n.data.cas.forEach(m=>{const r=document.createElement("option");typeof m=="object"?(r.value=m.id,r.textContent=m.displayName||m.name):(r.value=m,r.textContent=m),w.appendChild(r)})),n.data.cas}else throw new Error(n.message)}catch(h){console.error("Error loading CAs:",h);const n=document.getElementById("existing-ca-select");return n.innerHTML='',[]}}function f(s){const{subdomain:h,port:n,ip:w,sslType:m,caName:r,existingCa:l,enableAuth:y,enableCors:e,customHeaders:a,upstreamPath:p,healthCheck:t,timeout:c,tailscaleOnly:v}=s;let d=`${buildDomain(h)} { +`;switch(v&&(d+=` @blocked not remote_ip 100.64.0.0/10 +`,d+=` respond @blocked "Access denied. Tailscale connection required." 403 +`),m){case"letsencrypt":break;case"caddy-managed":d+=` tls internal +`;break;case"existing-ca":l&&(d+=` tls { + ca ${l} } -`);break;case"custom-ca":s&&(i+=` tls { - ca ${s} +`);break;case"custom-ca":r&&(d+=` tls { + ca ${r} } -`);break}if(g&&(i+=` basicauth { +`);break}if(y&&(d+=` basicauth { admin $2a$14$hashed_password_here } -`),e&&(i+=` header { -`,i+=` Access-Control-Allow-Origin "*" -`,i+=` Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" -`,i+=` Access-Control-Allow-Headers "Content-Type, Authorization" -`,i+=` } -`),a)try{const k=JSON.parse(a);i+=` header { -`,Object.entries(k).forEach(([B,T])=>{i+=` ${B} "${T}" -`}),i+=` } -`}catch{console.warn("Invalid JSON in custom headers")}return t&&(i+=` health_uri ${t} -`),i+=` reverse_proxy ${b}:${n} { -`,u&&u!=="/"&&(i+=` rewrite ${u} -`),d&&d!==30&&(i+=` transport http { -`,i+=` dial_timeout ${d}s -`,i+=` response_header_timeout ${d}s -`,i+=` } -`),i+=` } -`,i+=`} -`,i}async function l(r,h,n=DC.DEFAULTS.TTL){const b=window.getToken(getPrimaryDnsId(),"admin");if(!b)throw new Error("DNS admin token not configured. Please set it in the Tokens menu.");const p=buildDomain(r),s=await secureFetch("/api/v1/dns/record",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:p,ip:h,ttl:n,token:b,server:SITE.dnsIp})});if(!s.ok){const g=await s.text();throw new Error(`DNS API Error: ${s.status} - ${g}`)}const c=await s.json();if(!c.success)throw new Error(`DNS Error: ${c.error||"Unknown error"}`);return c}async function w(r){const h={id:r.subdomain,name:r.name,logo:r.logo||`/assets/${r.subdomain}.png`};try{const n=await secureFetch("/api/v1/services",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(h)});if(!n.ok){const b=await n.json();throw new Error(b.error||"Failed to save service")}return await window.loadServices(),window.buildGrid(),h}catch(n){throw console.error("Failed to add service to config:",n),n}}async function v(r){const h=document.getElementById("service-subdomain-input").value.trim(),n=document.getElementById("service-ip-input").value.trim()||"localhost",b=document.getElementById("service-port-input").value.trim()||"80",p=await secureFetch("/api/v1/site",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:buildDomain(h),upstream:`${n}:${b}`,config:r})}),s=await p.json();if(!p.ok||!s.success)throw new Error(s.error||`Caddy API Error: ${p.status}`);return s}window.loadExistingCAs=o,window.generateCaddyConfig=m,window.createDnsRecord=l,window.addServiceToConfig=w,window.addToCaddyfile=v})(),(function(){let o=null;function m(n){o=n;const b=document.getElementById("service-edit-modal");document.getElementById("service-edit-title").textContent=`Edit ${n.name}`,document.getElementById("edit-service-name").value=n.name,document.getElementById("edit-service-url-display").textContent=n.url||buildServiceUrl(n.id),document.getElementById("edit-service-logo-preview").src=n.logo||`/assets/${n.id}.png`,document.getElementById("edit-subdomain").value=n.id,document.getElementById("edit-port").value=n.port||"",document.getElementById("edit-ip").value=n.ip||"localhost",document.getElementById("edit-tailscale-only").checked=n.tailscaleOnly||!1,document.getElementById("edit-logo-url").value=n.logo||"",b.classList.add("show")}function l(){closeModal("service-edit-modal"),o=null}async function w(){if(!o)return;const n=document.getElementById("edit-subdomain").value.trim().toLowerCase(),b=document.getElementById("edit-service-name").value.trim(),p=document.getElementById("edit-port").value.trim(),s=document.getElementById("edit-ip").value.trim()||"localhost",c=document.getElementById("edit-tailscale-only").checked,g=document.getElementById("edit-logo-url").value.trim();if(!n){showNotification("Subdomain is required","warning");return}const e=o.id,a=[];if(n!==e&&a.push("subdomain"),b&&b!==o.name&&a.push("name"),p&&p!==String(o.port)&&a.push("port"),s!==o.ip&&a.push("ip"),c!==(o.tailscaleOnly||!1)&&a.push("tailscale"),g&&g!==o.logo&&a.push("logo"),a.length===0){l();return}const u=document.getElementById("service-edit-save");u.textContent="Saving...",u.disabled=!0;try{const d=await(await secureFetch("/api/v1/services/update",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({oldSubdomain:e,newSubdomain:n,name:b||o.name,port:p||o.port,ip:s,tailscaleOnly:c,logo:g||void 0})})).json();if(!d.success)throw new Error(d.error||"Failed to update service");const f=window.APPS.findIndex(i=>i.id===e);f!==-1&&(window.APPS[f]={...window.APPS[f],id:n,name:b||window.APPS[f].name,port:p||window.APPS[f].port,ip:s,tailscaleOnly:c,logo:g||window.APPS[f].logo}),l(),window.buildGrid(),window.refreshAll()}catch(t){console.error("Error saving service changes:",t),showNotification(`Error saving changes: ${t.message}`,"error")}finally{u.textContent="Save Changes",u.disabled=!1}}document.getElementById("edit-logo-file")?.addEventListener("change",async n=>{const b=n.target.files[0];if(!b)return;if(!b.type.startsWith("image/")){showNotification("Please select an image file","warning");return}const p=new FileReader;p.onload=async s=>{const c=s.target.result;if(document.getElementById("edit-service-logo-preview").src=c,document.getElementById("edit-logo-url").value=c,o)try{const e=await(await secureFetch("/api/v1/assets/upload",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({filename:`${o.id}.png`,data:c})})).json();e.success&&e.path&&(document.getElementById("edit-logo-url").value=e.path)}catch{}},p.readAsDataURL(b)}),document.getElementById("service-edit-cancel")?.addEventListener("click",l),document.getElementById("service-edit-save")?.addEventListener("click",w),document.getElementById("service-edit-modal")?.addEventListener("click",n=>{n.target.id==="service-edit-modal"&&l()});function v(n,b,p){return new Promise(s=>{const c=document.getElementById("delete-service-modal"),g=document.getElementById("delete-modal-title"),e=document.getElementById("delete-modal-message"),a=document.getElementById("delete-modal-container-info"),u=document.getElementById("delete-modal-container-name"),t=document.getElementById("delete-modal-help"),d=document.getElementById("delete-modal-cancel"),f=document.getElementById("delete-modal-remove"),i=document.getElementById("delete-modal-delete");g.textContent=`Delete "${n}"`,b?(e.innerHTML="This service has an associated Docker container.
Choose how to proceed:",a.style.display="block",u.textContent=`Container ID: ${p?.slice(0,12)||"Unknown"}`,t.style.display="block",i.style.display="block"):(e.textContent="Remove this service from the dashboard?",a.style.display="none",t.style.display="none",i.style.display="none");const k=()=>{c.classList.remove("show"),d.removeEventListener("click",B),f.removeEventListener("click",T),i.removeEventListener("click",A),c.removeEventListener("click",E)},B=()=>{k(),s(null)},T=()=>{k(),s(!1)},A=()=>{k(),s(!0)},E=L=>{L.target===c&&(k(),s(null))};d.addEventListener("click",B),f.addEventListener("click",T),i.addEventListener("click",A),c.addEventListener("click",E),c.classList.add("show")})}async function r(n,b,p){const s=document.getElementById(`update-btn-${p}`),c=s?.textContent;if(confirm(`Update ${b} to the latest version? +`),e&&(d+=` header { +`,d+=` Access-Control-Allow-Origin "*" +`,d+=` Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" +`,d+=` Access-Control-Allow-Headers "Content-Type, Authorization" +`,d+=` } +`),a)try{const k=JSON.parse(a);d+=` header { +`,Object.entries(k).forEach(([B,T])=>{d+=` ${B} "${T}" +`}),d+=` } +`}catch{console.warn("Invalid JSON in custom headers")}return t&&(d+=` health_uri ${t} +`),d+=` reverse_proxy ${w}:${n} { +`,p&&p!=="/"&&(d+=` rewrite ${p} +`),c&&c!==30&&(d+=` transport http { +`,d+=` dial_timeout ${c}s +`,d+=` response_header_timeout ${c}s +`,d+=` } +`),d+=` } +`,d+=`} +`,d}async function u(s,h,n=DC.DEFAULTS.TTL){const w=window.getToken(getPrimaryDnsId(),"admin");if(!w)throw new Error("DNS admin token not configured. Please set it in the Tokens menu.");const m=buildDomain(s),r=await secureFetch("/api/v1/dns/record",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:m,ip:h,ttl:n,token:w,server:SITE.dnsIp})});if(!r.ok){const y=await r.text();throw new Error(`DNS API Error: ${r.status} - ${y}`)}const l=await r.json();if(!l.success)throw new Error(`DNS Error: ${l.error||"Unknown error"}`);return l}async function g(s){const h={id:s.subdomain,name:s.name,logo:s.logo||`/assets/${s.subdomain}.png`};try{const n=await secureFetch("/api/v1/services",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(h)});if(!n.ok){const w=await n.json();throw new Error(w.error||"Failed to save service")}return await window.loadServices(),window.buildGrid(),h}catch(n){throw console.error("Failed to add service to config:",n),n}}async function i(s){const h=document.getElementById("service-subdomain-input").value.trim(),n=document.getElementById("service-ip-input").value.trim()||"localhost",w=document.getElementById("service-port-input").value.trim()||"80",m=await secureFetch("/api/v1/site",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:buildDomain(h),upstream:`${n}:${w}`,config:s})}),r=await m.json();if(!m.ok||!r.success)throw new Error(r.error||`Caddy API Error: ${m.status}`);return r}window.loadExistingCAs=o,window.generateCaddyConfig=f,window.createDnsRecord=u,window.addServiceToConfig=g,window.addToCaddyfile=i})(),(function(){let o=null;function f(n){o=n;const w=document.getElementById("service-edit-modal");document.getElementById("service-edit-title").textContent=`Edit ${n.name}`,document.getElementById("edit-service-name").value=n.name,document.getElementById("edit-service-url-display").textContent=n.url||buildServiceUrl(n.id),document.getElementById("edit-service-logo-preview").src=n.logo||`/assets/${n.id}.png`,document.getElementById("edit-subdomain").value=n.id,document.getElementById("edit-port").value=n.port||"",document.getElementById("edit-ip").value=n.ip||"localhost",document.getElementById("edit-tailscale-only").checked=n.tailscaleOnly||!1,document.getElementById("edit-logo-url").value=n.logo||"",w.classList.add("show")}function u(){closeModal("service-edit-modal"),o=null}async function g(){if(!o)return;const n=document.getElementById("edit-subdomain").value.trim().toLowerCase(),w=document.getElementById("edit-service-name").value.trim(),m=document.getElementById("edit-port").value.trim(),r=document.getElementById("edit-ip").value.trim()||"localhost",l=document.getElementById("edit-tailscale-only").checked,y=document.getElementById("edit-logo-url").value.trim();if(!n){showNotification("Subdomain is required","warning");return}const e=o.id,a=[];if(n!==e&&a.push("subdomain"),w&&w!==o.name&&a.push("name"),m&&m!==String(o.port)&&a.push("port"),r!==o.ip&&a.push("ip"),l!==(o.tailscaleOnly||!1)&&a.push("tailscale"),y&&y!==o.logo&&a.push("logo"),a.length===0){u();return}const p=document.getElementById("service-edit-save");p.textContent="Saving...",p.disabled=!0;try{const c=await(await secureFetch("/api/v1/services/update",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({oldSubdomain:e,newSubdomain:n,name:w||o.name,port:m||o.port,ip:r,tailscaleOnly:l,logo:y||void 0})})).json();if(!c.success)throw new Error(c.error||"Failed to update service");const v=window.APPS.findIndex(d=>d.id===e);v!==-1&&(window.APPS[v]={...window.APPS[v],id:n,name:w||window.APPS[v].name,port:m||window.APPS[v].port,ip:r,tailscaleOnly:l,logo:y||window.APPS[v].logo}),u(),window.buildGrid(),window.refreshAll()}catch(t){console.error("Error saving service changes:",t),showNotification(`Error saving changes: ${t.message}`,"error")}finally{p.textContent="Save Changes",p.disabled=!1}}document.getElementById("edit-logo-file")?.addEventListener("change",async n=>{const w=n.target.files[0];if(!w)return;if(!w.type.startsWith("image/")){showNotification("Please select an image file","warning");return}const m=new FileReader;m.onload=async r=>{const l=r.target.result;if(document.getElementById("edit-service-logo-preview").src=l,document.getElementById("edit-logo-url").value=l,o)try{const e=await(await secureFetch("/api/v1/assets/upload",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({filename:`${o.id}.png`,data:l})})).json();e.success&&e.path&&(document.getElementById("edit-logo-url").value=e.path)}catch{}},m.readAsDataURL(w)}),document.getElementById("service-edit-cancel")?.addEventListener("click",u),document.getElementById("service-edit-save")?.addEventListener("click",g),document.getElementById("service-edit-modal")?.addEventListener("click",n=>{n.target.id==="service-edit-modal"&&u()});function i(n,w,m){return new Promise(r=>{const l=document.getElementById("delete-service-modal"),y=document.getElementById("delete-modal-title"),e=document.getElementById("delete-modal-message"),a=document.getElementById("delete-modal-container-info"),p=document.getElementById("delete-modal-container-name"),t=document.getElementById("delete-modal-help"),c=document.getElementById("delete-modal-cancel"),v=document.getElementById("delete-modal-remove"),d=document.getElementById("delete-modal-delete");y.textContent=`Delete "${n}"`,w?(e.innerHTML="This service has an associated Docker container.
Choose how to proceed:",a.style.display="block",p.textContent=`Container ID: ${m?.slice(0,12)||"Unknown"}`,t.style.display="block",d.style.display="block"):(e.textContent="Remove this service from the dashboard?",a.style.display="none",t.style.display="none",d.style.display="none");const k=()=>{l.classList.remove("show"),c.removeEventListener("click",B),v.removeEventListener("click",T),d.removeEventListener("click",A),l.removeEventListener("click",E)},B=()=>{k(),r(null)},T=()=>{k(),r(!1)},A=()=>{k(),r(!0)},E=L=>{L.target===l&&(k(),r(null))};c.addEventListener("click",B),v.addEventListener("click",T),d.addEventListener("click",A),l.addEventListener("click",E),l.classList.add("show")})}async function s(n,w,m){const r=document.getElementById(`update-btn-${m}`),l=r?.textContent;if(confirm(`Update ${w} to the latest version? This will: 1. Pull the latest image 2. Stop the container 3. Recreate with same settings -The service will be briefly unavailable.`))try{s&&(s.textContent="\u{1F504}",s.disabled=!0,s.title="Updating...");const e=await(await secureFetch(`/api/v1/containers/${n}/update`,{method:"POST"})).json();if(e.success){const a=window.APPS.find(u=>u.id===p);a&&e.newContainerId&&(a.containerId=e.newContainerId),s&&(s.textContent="\u2705",s.title="Updated successfully!",setTimeout(()=>{s.textContent=c,s.disabled=!1,s.title="Update container to latest version"},3e3)),setTimeout(()=>window.refreshAll(),2e3),showNotification(`${b} updated successfully!`,"success")}else throw new Error(e.error||"Update failed")}catch(g){console.error("Update error:",g),s&&(s.textContent="\u274C",s.title="Update failed",setTimeout(()=>{s.textContent=c,s.disabled=!1,s.title="Update container to latest version"},3e3)),showNotification(`Failed to update ${b}: ${g.message}`,"error")}}async function h(n,b){const p=window.APPS.find(i=>i.id===n),s=p?buildDomain(p.id):null,c=p?.containerId,g=await v(b||n,c,p?.containerId);if(g===null)return;let e={dashboard:!1,container:null,dns:null,caddy:null,service:null};if(g&&c)try{const i=new URLSearchParams({containerId:p.containerId,subdomain:p.id,ip:p.ip||"localhost",deleteContainer:"true"}),B=await(await secureFetch(`/api/v1/apps/${encodeURIComponent(p.id)}?${i.toString()}`,{method:"DELETE"})).json();B.success?e={...e,...B.results,dashboard:!1}:console.error("App removal failed:",B.error)}catch(i){console.error("App removal error:",i)}else if(g&&s){try{const i=p?.ip||"localhost",B=await(await secureFetch(`/api/v1/dns/record?domain=${encodeURIComponent(s)}&type=A&ipAddress=${encodeURIComponent(i)}&server=${SITE.dnsIp}`,{method:"DELETE"})).json();e.dns=B.success?"deleted":B.error||"failed"}catch(i){e.dns=i.message}try{const k=await(await secureFetch(`/api/v1/site/${encodeURIComponent(s)}`,{method:"DELETE"})).json();e.caddy=k.success||k.error&&k.error.includes("not found")?"removed":k.error||"failed"}catch(i){e.caddy=i.message}}const a=window.APPS.findIndex(i=>i.id===n);a>-1&&(window.APPS.splice(a,1),e.dashboard=!0);try{const i=safeGetJSON("custom-apps",[]),k=i.findIndex(B=>B.id===n);k>-1&&(i.splice(k,1),safeSet("custom-apps",JSON.stringify(i)))}catch{}try{const k=await(await secureFetch(`/api/v1/services/${encodeURIComponent(n)}`,{method:"DELETE"})).json();e.service=k.success?"removed":k.error||"failed"}catch(i){e.service=i.message}window.buildGrid(),window.refreshAll();let u=!1,t=[];e.dashboard||(u=!0,t.push("\u2717 Failed to remove from dashboard"));const d=["removed","already removed","not found","deleted","kept (user choice)","skipped","no such record","does not exist"],f=i=>!i||d.some(k=>i.toLowerCase().includes(k.toLowerCase()));e.container&&!f(e.container)&&(u=!0,t.push(`\u26A0 Container: ${e.container}`)),e.dns&&!f(e.dns)&&(u=!0,t.push(`\u26A0 DNS Record: ${e.dns}`)),e.caddy&&!f(e.caddy)&&(u=!0,t.push(`\u26A0 Caddy Config: ${e.caddy}`)),e.service&&!f(e.service)&&(u=!0,t.push(`\u26A0 Service File: ${e.service}`)),u&&showNotification(`Error deleting "${b||n}": ${t.join(", ")}`,"error",6e3)}window.openServiceEditModal=m,window.showDeleteModal=v,window.updateContainer=r,window.deleteService=h})(),(function(){function o(e){return e.toLowerCase().replace(/\s+/g,"-").replace(/[^a-z0-9-]/g,"").replace(/-+/g,"-").replace(/^-|-$/g,"")}function m(){return SITE.defaults?.sslType||(SITE.configurationType==="public"?"letsencrypt":"caddy-managed")}function l(){const e=document.getElementById("service-subdomain-input").value||"subdomain",a=document.getElementById("service-ip-input").value||w.lan||"localhost",u=document.getElementById("service-port-input").value||DC.DEFAULTS.SERVICE_PORT,t=document.getElementById("ssl-type-select").value,d=document.getElementById("ca-name-input").value||"sami-ca",f=document.getElementById("existing-ca-select").value,i=document.getElementById("enable-auth").checked,k=document.getElementById("enable-cors").checked,B=document.getElementById("custom-headers-input").value,T=document.getElementById("upstream-path-input").value||"/",A=document.getElementById("health-check-input").value,E=document.getElementById("timeout-input").value||30,L=document.getElementById("dns-preview");L&&(L.textContent=`${buildDomain(e)} \u2192 ${a}`);const $=document.getElementById("url-preview");$&&($.textContent=buildServiceUrl(e));const x={subdomain:e,port:u,ip:a,sslType:t,caName:d,existingCa:f,enableAuth:i,enableCors:k,customHeaders:B,upstreamPath:T,healthCheck:A,timeout:E},y=window.generateCaddyConfig(x),I=document.getElementById("caddy-config-preview");I&&(I.value=y)}const w={localhost:"127.0.0.1",lan:"",tailscale:""};async function v(){try{const t=await fetch("/api/v1/network/ips",{signal:AbortSignal.timeout(2e3)});if(t.ok){const d=await t.json();d.lan&&(w.lan=d.lan),d.tailscale&&(w.tailscale=d.tailscale)}}catch{}const e=document.getElementById("quick-ip-lan"),a=document.getElementById("quick-ip-tailscale");e&&(w.lan?(e.dataset.ip=w.lan,e.textContent=`LAN (${w.lan})`,e.title=`LAN IP: ${w.lan}`):e.style.display="none"),a&&(w.tailscale?(a.dataset.ip=w.tailscale,a.textContent=`Tailscale (${w.tailscale})`,a.title=`Tailscale IP: ${w.tailscale}`):a.style.display="none");const u=document.getElementById("service-ip-input");u&&!u.value&&w.lan&&(u.value=w.lan)}function r(){document.querySelectorAll(".quick-ip-btn").forEach(e=>{e.addEventListener("click",()=>{const a=e.dataset.ip;a&&(document.getElementById("service-ip-input").value=a,document.querySelectorAll(".quick-ip-btn").forEach(u=>u.classList.remove("active")),e.classList.add("active"),l())})}),document.getElementById("service-ip-input")?.addEventListener("input",e=>{const a=e.target.value;document.querySelectorAll(".quick-ip-btn").forEach(u=>{u.classList.toggle("active",u.dataset.ip===a)})})}async function h(){const e=document.getElementById("add-service-modal");e.classList.add("show");const a=e.querySelector(".weather-modal-content");a&&(a.scrollTop=0),document.body.style.overflow="hidden";const u=document.getElementById("ssl-type-select");u&&(u.value=m()),await v();const t=document.getElementById("caddyfile-path-input").value||DC.DEFAULTS.CADDYFILE;await window.loadExistingCAs(t);const d=document.getElementById("manual-tailscale-status"),f=document.getElementById("manual-tailscale-only");try{const k=await(await fetch("/api/v1/tailscale/status")).json();k.success&&k.installed&&k.connected?(d.innerHTML=` +The service will be briefly unavailable.`))try{r&&(r.textContent="\u{1F504}",r.disabled=!0,r.title="Updating...");const e=await(await secureFetch(`/api/v1/containers/${n}/update`,{method:"POST"})).json();if(e.success){const a=window.APPS.find(p=>p.id===m);a&&e.newContainerId&&(a.containerId=e.newContainerId),r&&(r.textContent="\u2705",r.title="Updated successfully!",setTimeout(()=>{r.textContent=l,r.disabled=!1,r.title="Update container to latest version"},3e3)),setTimeout(()=>window.refreshAll(),2e3),showNotification(`${w} updated successfully!`,"success")}else throw new Error(e.error||"Update failed")}catch(y){console.error("Update error:",y),r&&(r.textContent="\u274C",r.title="Update failed",setTimeout(()=>{r.textContent=l,r.disabled=!1,r.title="Update container to latest version"},3e3)),showNotification(`Failed to update ${w}: ${y.message}`,"error")}}async function h(n,w){const m=window.APPS.find(d=>d.id===n),r=m?buildDomain(m.id):null,l=m?.containerId,y=await i(w||n,l,m?.containerId);if(y===null)return;let e={dashboard:!1,container:null,dns:null,caddy:null,service:null};if(y&&l)try{const d=new URLSearchParams({containerId:m.containerId,subdomain:m.id,ip:m.ip||"localhost",deleteContainer:"true"}),B=await(await secureFetch(`/api/v1/apps/${encodeURIComponent(m.id)}?${d.toString()}`,{method:"DELETE"})).json();B.success?e={...e,...B.results,dashboard:!1}:console.error("App removal failed:",B.error)}catch(d){console.error("App removal error:",d)}else if(y&&r){try{const d=m?.ip||"localhost",B=await(await secureFetch(`/api/v1/dns/record?domain=${encodeURIComponent(r)}&type=A&ipAddress=${encodeURIComponent(d)}&server=${SITE.dnsIp}`,{method:"DELETE"})).json();e.dns=B.success?"deleted":B.error||"failed"}catch(d){e.dns=d.message}try{const k=await(await secureFetch(`/api/v1/site/${encodeURIComponent(r)}`,{method:"DELETE"})).json();e.caddy=k.success||k.error&&k.error.includes("not found")?"removed":k.error||"failed"}catch(d){e.caddy=d.message}}const a=window.APPS.findIndex(d=>d.id===n);a>-1&&(window.APPS.splice(a,1),e.dashboard=!0);try{const d=safeGetJSON("custom-apps",[]),k=d.findIndex(B=>B.id===n);k>-1&&(d.splice(k,1),safeSet("custom-apps",JSON.stringify(d)))}catch{}try{const k=await(await secureFetch(`/api/v1/services/${encodeURIComponent(n)}`,{method:"DELETE"})).json();e.service=k.success?"removed":k.error||"failed"}catch(d){e.service=d.message}window.buildGrid(),window.refreshAll();let p=!1,t=[];e.dashboard||(p=!0,t.push("\u2717 Failed to remove from dashboard"));const c=["removed","already removed","not found","deleted","kept (user choice)","skipped","no such record","does not exist"],v=d=>!d||c.some(k=>d.toLowerCase().includes(k.toLowerCase()));e.container&&!v(e.container)&&(p=!0,t.push(`\u26A0 Container: ${e.container}`)),e.dns&&!v(e.dns)&&(p=!0,t.push(`\u26A0 DNS Record: ${e.dns}`)),e.caddy&&!v(e.caddy)&&(p=!0,t.push(`\u26A0 Caddy Config: ${e.caddy}`)),e.service&&!v(e.service)&&(p=!0,t.push(`\u26A0 Service File: ${e.service}`)),p&&showNotification(`Error deleting "${w||n}": ${t.join(", ")}`,"error",6e3)}window.openServiceEditModal=f,window.showDeleteModal=i,window.updateContainer=s,window.deleteService=h})(),(function(){function o(e){return e.toLowerCase().replace(/\s+/g,"-").replace(/[^a-z0-9-]/g,"").replace(/-+/g,"-").replace(/^-|-$/g,"")}function f(){return SITE.defaults?.sslType||(SITE.configurationType==="public"?"letsencrypt":"caddy-managed")}function u(){const e=document.getElementById("service-subdomain-input").value||"subdomain",a=document.getElementById("service-ip-input").value||g.lan||"localhost",p=document.getElementById("service-port-input").value||DC.DEFAULTS.SERVICE_PORT,t=document.getElementById("ssl-type-select").value,c=document.getElementById("ca-name-input").value||"sami-ca",v=document.getElementById("existing-ca-select").value,d=document.getElementById("enable-auth").checked,k=document.getElementById("enable-cors").checked,B=document.getElementById("custom-headers-input").value,T=document.getElementById("upstream-path-input").value||"/",A=document.getElementById("health-check-input").value,E=document.getElementById("timeout-input").value||30,L=document.getElementById("dns-preview");L&&(L.textContent=`${buildDomain(e)} \u2192 ${a}`);const $=document.getElementById("url-preview");$&&($.textContent=buildServiceUrl(e));const x={subdomain:e,port:p,ip:a,sslType:t,caName:c,existingCa:v,enableAuth:d,enableCors:k,customHeaders:B,upstreamPath:T,healthCheck:A,timeout:E},b=window.generateCaddyConfig(x),I=document.getElementById("caddy-config-preview");I&&(I.value=b)}const g={localhost:"127.0.0.1",lan:"",tailscale:""};async function i(){try{const t=await fetch("/api/v1/network/ips",{signal:AbortSignal.timeout(2e3)});if(t.ok){const c=await t.json();c.lan&&(g.lan=c.lan),c.tailscale&&(g.tailscale=c.tailscale)}}catch{}const e=document.getElementById("quick-ip-lan"),a=document.getElementById("quick-ip-tailscale");e&&(g.lan?(e.dataset.ip=g.lan,e.textContent=`LAN (${g.lan})`,e.title=`LAN IP: ${g.lan}`):e.style.display="none"),a&&(g.tailscale?(a.dataset.ip=g.tailscale,a.textContent=`Tailscale (${g.tailscale})`,a.title=`Tailscale IP: ${g.tailscale}`):a.style.display="none");const p=document.getElementById("service-ip-input");p&&!p.value&&g.lan&&(p.value=g.lan)}function s(){document.querySelectorAll(".quick-ip-btn").forEach(e=>{e.addEventListener("click",()=>{const a=e.dataset.ip;a&&(document.getElementById("service-ip-input").value=a,document.querySelectorAll(".quick-ip-btn").forEach(p=>p.classList.remove("active")),e.classList.add("active"),u())})}),document.getElementById("service-ip-input")?.addEventListener("input",e=>{const a=e.target.value;document.querySelectorAll(".quick-ip-btn").forEach(p=>{p.classList.toggle("active",p.dataset.ip===a)})})}async function h(){const e=document.getElementById("add-service-modal");e.classList.add("show");const a=e.querySelector(".weather-modal-content");a&&(a.scrollTop=0),document.body.style.overflow="hidden";const p=document.getElementById("ssl-type-select");p&&(p.value=f()),await i();const t=document.getElementById("caddyfile-path-input").value||DC.DEFAULTS.CADDYFILE;await window.loadExistingCAs(t);const c=document.getElementById("manual-tailscale-status"),v=document.getElementById("manual-tailscale-only");try{const k=await(await fetch("/api/v1/tailscale/status")).json();k.success&&k.installed&&k.connected?(c.innerHTML=` \u2713 Connected ${k.self?.hostname} (${k.self?.ip}) - `,f.disabled=!1):k.installed?(d.innerHTML='\u26A0 Not connected',f.disabled=!0):(d.innerHTML='Not available',f.disabled=!0)}catch{d.innerHTML='Could not check',f.disabled=!0}f.checked=!1,l()}function n(){const e=document.getElementById("service-type-local"),a=document.getElementById("service-type-external"),u=document.getElementById("local-service-config"),t=document.getElementById("external-service-config"),d=document.getElementById("tab-local"),f=document.getElementById("tab-external");function i(){e.checked?(u.style.display="grid",t.style.display="none",d&&(d.style.background="var(--accent)",d.style.color="var(--bg)"),f&&(f.style.background="transparent",f.style.color="var(--muted)")):(u.style.display="none",t.style.display="block",f&&(f.style.background="var(--accent)",f.style.color="var(--bg)"),d&&(d.style.background="transparent",d.style.color="var(--muted)"))}e?.addEventListener("change",i),a?.addEventListener("change",i)}function b(){const e=document.getElementById("service-name-input"),a=document.getElementById("service-subdomain-input"),u=document.getElementById("subdomain-preview");let t=!1;e?.addEventListener("input",()=>{const T=o(e.value);!t&&a&&(a.value=T),u&&(u.textContent=T?`\u2192 ${buildDomain(T)}`:""),l()}),a?.addEventListener("input",()=>{t=a.value!==o(e?.value||"");const T=a.value.trim()||o(e?.value||"");u&&(u.textContent=T?`\u2192 ${buildDomain(T)}`:""),l()});const d=document.getElementById("external-service-name"),f=document.getElementById("external-service-subdomain"),i=document.getElementById("external-subdomain-preview"),k=document.getElementById("external-domain-preview");let B=!1;d?.addEventListener("input",()=>{const T=o(d.value);!B&&f&&(f.value=T);const A=f?.value||T;i&&(i.textContent=A?`\u2192 ${buildDomain(A)}`:""),k&&(k.textContent=A?buildDomain(A):"")}),f?.addEventListener("input",()=>{B=f.value!==o(d?.value||"");const T=f.value.trim()||o(d?.value||"");i&&(i.textContent=T?`\u2192 ${buildDomain(T)}`:""),k&&(k.textContent=T?buildDomain(T):"")})}async function p(){const e=document.getElementById("external-service-name").value.trim(),a=document.getElementById("external-service-url").value.trim(),u=(document.getElementById("external-service-subdomain").value.trim()||o(e)).toLowerCase(),t=document.getElementById("external-service-logo").value.trim(),d=document.getElementById("external-service-icon").value.trim(),f=document.getElementById("external-create-dns").checked,i=document.getElementById("external-create-caddy").checked,k=document.getElementById("external-proxy-ip").value.trim()||SITE.dnsIp||"localhost",B=document.getElementById("external-preserve-host").checked,T=document.getElementById("external-follow-redirects").checked;if(!e||!a){showNotification("Please fill in Name and External URL","warning");return}if(!u){showNotification("Could not derive subdomain from name. Please set one in Options.","warning");return}if(!a.startsWith("http://")&&!a.startsWith("https://")){showNotification("External URL must start with http:// or https://","warning");return}const A=buildDomain(u);try{const E={dns:null,caddy:null,dashboard:!1};if(f)if(window.getToken(getPrimaryDnsId(),"admin"))try{const C=await(await secureFetch("/api/v1/dns/record",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:A,ip:k,ttl:DC.DEFAULTS.TTL,server:SITE.dnsIp})})).json();E.dns=C.success?"created":C.error||"failed"}catch(S){E.dns=S.message}else E.dns="no admin token (configure in \u{1F511} Tokens)";if(i)try{const I={subdomain:u,externalUrl:a,preserveHost:B,followRedirects:T,sslType:"caddy-managed",caddyfilePath:DC.DEFAULTS.CADDYFILE,reloadCaddy:!0},C=await(await secureFetch("/api/v1/site/external",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(I)})).json();E.caddy=C.success?"created":C.error||"failed"}catch(I){E.caddy=I.message}const L={id:u,name:e,url:`https://${A}`,externalUrl:a,logo:t||d||"\u{1F310}",isExternal:!0,isCustom:!0};window.APPS.push(L),E.dashboard=!0;const $=["plex","router","chat","sync","torrent","radarr","sonarr","prowlarr","portainer","requests","jellyfin","emby"],x=window.APPS.filter(I=>!$.includes(I.id));safeSet("custom-services",JSON.stringify(x));try{await secureFetch("/api/v1/services",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(window.APPS)})}catch(I){console.warn("Failed to save to services.json:",I)}window.buildGrid(),window.refreshAll(),s();const y=[`External service "${e}" added!`];f&&y.push(`DNS: ${E.dns==="created"?"\u2713":"\u26A0 "+E.dns}`),i&&y.push(`Caddy: ${E.caddy==="created"?"\u2713":"\u26A0 "+E.caddy}`),y.push(`Access at: https://${A}`),showNotification(y.join(" | "),"success",6e3)}catch(E){console.error("Failed to create external service:",E),showNotification(`Failed to create external service: ${E.message}`,"error")}}function s(){closeModal("add-service-modal"),document.body.style.overflow="",document.getElementById("service-name-input").value="",document.getElementById("service-subdomain-input").value="",document.getElementById("service-port-input").value="",document.getElementById("service-ip-input").value=w.lan||"",document.getElementById("service-logo-input").value="",document.getElementById("dns-ttl-input").value=DC.DEFAULTS.TTL,document.getElementById("ssl-type-select").value=m(),document.getElementById("ca-name-input").value="",document.getElementById("enable-auth").checked=!1,document.getElementById("enable-cors").checked=!1,document.getElementById("custom-headers-input").value="",document.getElementById("upstream-path-input").value="/",document.getElementById("health-check-input").value="",document.getElementById("timeout-input").value="30";const e=document.getElementById("subdomain-preview");e&&(e.textContent="");const a=document.getElementById("external-subdomain-preview");a&&(a.textContent="");const u=document.getElementById("external-service-name");u&&(u.value="");const t=document.getElementById("external-service-subdomain");t&&(t.value="");const d=document.getElementById("external-service-url");d&&(d.value="");const f=document.getElementById("external-service-logo");f&&(f.value="");const i=document.getElementById("external-service-icon");i&&(i.value="");const k=document.getElementById("local-advanced-options");k&&k.removeAttribute("open");const B=document.getElementById("external-advanced-options");B&&B.removeAttribute("open");const T=document.getElementById("service-type-local");T&&(T.checked=!0);const A=document.getElementById("local-service-config"),E=document.getElementById("external-service-config");A&&(A.style.display="grid"),E&&(E.style.display="none");const L=document.getElementById("tab-local"),$=document.getElementById("tab-external");L&&(L.style.background="var(--accent)",L.style.color="var(--bg)"),$&&($.style.background="transparent",$.style.color="var(--muted)")}async function c(){const e=document.getElementById("service-name-input").value.trim(),a=(document.getElementById("service-subdomain-input").value.trim()||o(e)).toLowerCase(),u=document.getElementById("service-port-input").value.trim(),t=document.getElementById("service-ip-input").value.trim(),d=document.getElementById("service-logo-input").value.trim(),f=document.getElementById("create-dns-record").checked,i=parseInt(document.getElementById("dns-ttl-input").value)||DC.DEFAULTS.TTL,k=document.getElementById("manual-tailscale-only")?.checked||!1,B=document.getElementById("ssl-type-select")?.value||"caddy-managed",T=document.getElementById("ca-name-input")?.value||"",A=document.getElementById("existing-ca-select")?.value||"",E=document.getElementById("enable-auth")?.checked||!1,L=document.getElementById("enable-cors")?.checked||!1,$=document.getElementById("custom-headers-input")?.value||"",x=document.getElementById("upstream-path-input")?.value||"/",y=document.getElementById("health-check-input")?.value||"",I=document.getElementById("timeout-input")?.value||30,S=window.getToken(getPrimaryDnsId(),"admin");if(!e||!u||!t){showNotification("Please fill in Name, Port, and IP Address","warning");return}if(!a){showNotification("Could not derive subdomain from name. Please set one in Options.","warning");return}if(f&&!S){showNotification("DNS Admin token required. Configure it in the Tokens menu first.","warning");return}const C={dns:null,caddy:null,dashboard:!1};try{if(f)try{await window.createDnsRecord(a,t,i),C.dns="created"}catch(N){throw console.error("DNS creation failed:",N),C.dns=N.message,new Error(`DNS creation failed: ${N.message}`)}else C.dns="skipped";const P=window.generateCaddyConfig({subdomain:a,port:u,ip:t,sslType:B,caName:T,existingCa:A,enableAuth:E,enableCors:L,customHeaders:$,upstreamPath:x,healthCheck:y,timeout:I,tailscaleOnly:k});try{const R=await(await secureFetch("/api/v1/site",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:buildDomain(a),upstream:`${t}:${u}`,config:P})})).json();if(R.success)C.caddy="added & reloaded";else throw console.error("Caddy configuration failed:",R.error),C.caddy=R.error||"failed",new Error(`Caddy configuration failed: ${R.error}`)}catch(N){throw console.error("Caddy API error:",N),C.caddy=N.message,new Error(`Caddy API error: ${N.message}`)}const D={name:e,subdomain:a,port:u,ip:t,logo:d||`/assets/${a}.png`,tailscaleOnly:k||!1};await window.addServiceToConfig(D),C.dashboard=!0;const O=[`DNS: ${C.dns==="created"?"\u2713":C.dns==="skipped"?"\u25CB":"\u2717"}`,`Caddy: ${C.caddy==="added & reloaded"?"\u2713":"\u2717"}`,`Dashboard: ${C.dashboard?"\u2713":"\u2717"}`];showNotification(`Service "${e}" created! ${O.join(" | ")} \u2014 ${buildServiceUrl(a)}${k?" (Tailscale)":""}`,"success",6e3),s(),window.buildGrid(),window.refreshAll()}catch(P){console.error("Error creating service:",P),showNotification(`Error creating "${e}": ${P.message}`,"error",6e3)}}document.getElementById("add-service")?.addEventListener("click",h),document.getElementById("add-service-cancel")?.addEventListener("click",s),document.getElementById("add-service-create")?.addEventListener("click",()=>{document.querySelector('input[name="service-type"]:checked')?.value==="external"?p():c()}),n(),b(),r(),document.getElementById("ssl-type-select")?.addEventListener("change",e=>{const a=document.getElementById("existing-ca-config"),u=document.getElementById("custom-ca-config");a.style.display="none",u.style.display="none",e.target.value==="existing-ca"?a.style.display="block":e.target.value==="custom-ca"&&(u.style.display="block"),l()}),document.getElementById("refresh-cas")?.addEventListener("click",async()=>{const e=document.getElementById("refresh-cas"),a=e.textContent;e.textContent="\u231B Loading...",e.disabled=!0;try{const u=document.getElementById("caddyfile-path-input").value||DC.DEFAULTS.CADDYFILE;await window.loadExistingCAs(u),e.textContent="\u2705 Refreshed"}catch(u){e.textContent="\u274C Failed",console.error("Failed to refresh CAs:",u)}setTimeout(()=>{e.textContent=a,e.disabled=!1},2e3)}),document.getElementById("create-dns-record")?.addEventListener("change",e=>{const a=document.getElementById("dns-config");a.style.display=e.target.checked?"block":"none"}),["service-subdomain-input","service-ip-input","service-port-input","ca-name-input","existing-ca-select","enable-auth","enable-cors","custom-headers-input","upstream-path-input","health-check-input","timeout-input"].forEach(e=>{const a=document.getElementById(e);a&&(a.addEventListener("input",l),a.addEventListener("change",l))});function g(){const e=safeGet("custom-services");if(e)try{JSON.parse(e).forEach(u=>{window.APPS.find(t=>t.id===u.id)||window.APPS.push(u)})}catch(a){console.warn("Failed to load custom services:",a)}}g(),window.openAddServiceModal=h,window.closeAddServiceModal=s})(),(function(){let o=null,m=1e3;const l=3e4;function w(){if(o)try{o.close()}catch{}o=new EventSource("/api/v1/events/stream"),o.addEventListener("connected",()=>{m=1e3,debug("[SSE] Connected to event stream")}),o.addEventListener("status-change",v=>{try{const r=JSON.parse(v.data);if(r.serviceId&&typeof window.setBadge=="function"){const h=r.status==="up"||r.status==="healthy";window.setBadge(r.serviceId,h,r.responseTime||null)}}catch{}}),o.addEventListener("resource-alert",v=>{try{const r=JSON.parse(v.data),h=`${r.containerName||r.containerId}: ${r.metric} at ${r.value}% (threshold: ${r.threshold}%)`;typeof showNotification=="function"&&showNotification(h,"warning")}catch{}}),o.addEventListener("auto-restart",v=>{try{const r=JSON.parse(v.data);typeof showNotification=="function"&&showNotification(`Container "${r.containerName}" was auto-restarted`,"info")}catch{}}),o.addEventListener("update-available",v=>{try{const r=JSON.parse(v.data),h=document.getElementById("updates-btn");if(h&&!h.querySelector(".sse-dot")){const n=document.createElement("span");n.className="sse-dot",n.style.cssText="display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--accent);margin-left:6px;vertical-align:middle;",h.appendChild(n)}typeof showNotification=="function"&&showNotification(`Update available for ${r.containerName||r.containerId}`,"info")}catch{}}),o.addEventListener("update-complete",v=>{try{const r=JSON.parse(v.data);typeof showNotification=="function"&&showNotification(`Update completed: ${r.containerName||r.containerId}`,"success"),typeof window.refreshAll=="function"&&window.refreshAll()}catch{}}),o.addEventListener("update-failed",v=>{try{const r=JSON.parse(v.data);typeof showNotification=="function"&&showNotification(`Update failed: ${r.containerName||r.containerId} \u2014 ${r.error||"unknown error"}`,"error")}catch{}}),o.addEventListener("incident",v=>{try{const r=JSON.parse(v.data);typeof showNotification=="function"&&(r.type==="created"?showNotification(`Incident: ${r.message||r.serviceId}`,"error"):r.type==="resolved"&&showNotification(`Resolved: ${r.serviceId||"incident"}`,"success"))}catch{}}),o.onerror=()=>{o.close(),console.warn(`[SSE] Disconnected, reconnecting in ${m/1e3}s...`),setTimeout(w,m),m=Math.min(m*2,l)}}w(),window._sseReconnect=w})(),(function(){const o=document.getElementById("service-filter-search"),m=document.getElementById("service-filter-status"),l=document.getElementById("service-filter-count");function w(){const r=o.value.toLowerCase().trim(),h=m.value,n=document.querySelectorAll("#cards .card");let b=0;if(n.forEach(p=>{const s=p.querySelector(".name")?.textContent?.toLowerCase()||"",c=p.dataset.app?.toLowerCase()||"",g=p.dataset.status||"off";(!r||s.includes(r)||c.includes(r))&&(h==="all"||g===h)?(p.style.display="",b++):p.style.display="none"}),l){const p=n.length;l.textContent=`${b} of ${p} services`}}function v(r,h){let n;return function(...b){clearTimeout(n),n=setTimeout(()=>r.apply(this,b),h)}}o?.addEventListener("input",v(w,200)),m?.addEventListener("change",w),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>setTimeout(w,500)):setTimeout(w,500),window.refreshServiceFilter=w})(),(function(){const o=document.getElementById("batch-operations-btn"),m=document.getElementById("batch-action-bar"),l=document.getElementById("batch-selected-count"),w=document.getElementById("batch-start-btn"),v=document.getElementById("batch-stop-btn"),r=document.getElementById("batch-restart-btn"),h=document.getElementById("batch-cancel-btn");let n=!1,b=new Set;function p(){n=!0,b.clear(),m.style.display="",o.textContent="\u2713 Exit Batch Mode",c(),document.querySelectorAll("#cards .card[data-app]").forEach(a=>{const u=a.dataset.containerId;if(!u)return;const t=a.querySelector(".batch-checkbox");t&&t.remove();const d=document.createElement("input");d.type="checkbox",d.className="batch-checkbox",d.dataset.containerId=u,d.dataset.serviceName=a.querySelector(".name")?.textContent||u,d.style.cssText="position: absolute; top: 8px; left: 8px; z-index: 10; width: 18px; height: 18px; cursor: pointer;",d.addEventListener("change",f=>{f.stopPropagation(),d.checked?b.add(u):b.delete(u),c()}),a.style.position="relative",a.insertBefore(d,a.firstChild)})}function s(){n=!1,b.clear(),m.style.display="none",o.textContent="\u2630 Batch Operations",document.querySelectorAll(".batch-checkbox").forEach(e=>e.remove())}function c(){const e=b.size;l.textContent=`${e} selected`,w.disabled=e===0,v.disabled=e===0,r.disabled=e===0}async function g(e){if(b.size===0)return;const a=Array.from(b),u={start:"Starting",stop:"Stopping",restart:"Restarting"}[e];if(!confirm(`${u} ${a.length} container(s)? This cannot be undone.`))return;const t=[w,v,r];t.forEach(k=>{k.disabled=!0,k.textContent="..."});let d=0,f=0;const i=[];for(const k of a)try{const B=await fetch(`/api/v1/containers/${encodeURIComponent(k)}/${e}`,{method:"POST"});if(B.ok)d++;else{f++;const T=await B.json().catch(()=>({}));i.push(`${k}: ${T.error||B.statusText}`)}}catch(B){f++,i.push(`${k}: ${B.message}`)}t[0].textContent="\u25B6 Start All",t[1].textContent="\u2B1B Stop All",t[2].textContent="\u{1F504} Restart All",c(),f===0?typeof showNotification=="function"&&showNotification(`${u} completed: ${d} container(s)`,"success"):(typeof showNotification=="function"&&showNotification(`${u}: ${d} succeeded, ${f} failed`,"warning"),console.error("Batch operation errors:",i)),setTimeout(()=>{typeof refreshAll=="function"&&refreshAll()},1500)}o?.addEventListener("click",()=>{n?s():p()}),w?.addEventListener("click",()=>g("start")),v?.addEventListener("click",()=>g("stop")),r?.addEventListener("click",()=>g("restart")),h?.addEventListener("click",s)})(); + `,v.disabled=!1):k.installed?(c.innerHTML='\u26A0 Not connected',v.disabled=!0):(c.innerHTML='Not available',v.disabled=!0)}catch{c.innerHTML='Could not check',v.disabled=!0}v.checked=!1,u()}function n(){const e=document.getElementById("service-type-local"),a=document.getElementById("service-type-external"),p=document.getElementById("local-service-config"),t=document.getElementById("external-service-config"),c=document.getElementById("tab-local"),v=document.getElementById("tab-external");function d(){e.checked?(p.style.display="grid",t.style.display="none",c&&(c.style.background="var(--accent)",c.style.color="var(--bg)"),v&&(v.style.background="transparent",v.style.color="var(--muted)")):(p.style.display="none",t.style.display="block",v&&(v.style.background="var(--accent)",v.style.color="var(--bg)"),c&&(c.style.background="transparent",c.style.color="var(--muted)"))}e?.addEventListener("change",d),a?.addEventListener("change",d)}function w(){const e=document.getElementById("service-name-input"),a=document.getElementById("service-subdomain-input"),p=document.getElementById("subdomain-preview");let t=!1;e?.addEventListener("input",()=>{const T=o(e.value);!t&&a&&(a.value=T),p&&(p.textContent=T?`\u2192 ${buildDomain(T)}`:""),u()}),a?.addEventListener("input",()=>{t=a.value!==o(e?.value||"");const T=a.value.trim()||o(e?.value||"");p&&(p.textContent=T?`\u2192 ${buildDomain(T)}`:""),u()});const c=document.getElementById("external-service-name"),v=document.getElementById("external-service-subdomain"),d=document.getElementById("external-subdomain-preview"),k=document.getElementById("external-domain-preview");let B=!1;c?.addEventListener("input",()=>{const T=o(c.value);!B&&v&&(v.value=T);const A=v?.value||T;d&&(d.textContent=A?`\u2192 ${buildDomain(A)}`:""),k&&(k.textContent=A?buildDomain(A):"")}),v?.addEventListener("input",()=>{B=v.value!==o(c?.value||"");const T=v.value.trim()||o(c?.value||"");d&&(d.textContent=T?`\u2192 ${buildDomain(T)}`:""),k&&(k.textContent=T?buildDomain(T):"")})}async function m(){const e=document.getElementById("external-service-name").value.trim(),a=document.getElementById("external-service-url").value.trim(),p=(document.getElementById("external-service-subdomain").value.trim()||o(e)).toLowerCase(),t=document.getElementById("external-service-logo").value.trim(),c=document.getElementById("external-service-icon").value.trim(),v=document.getElementById("external-create-dns").checked,d=document.getElementById("external-create-caddy").checked,k=document.getElementById("external-proxy-ip").value.trim()||SITE.dnsIp||"localhost",B=document.getElementById("external-preserve-host").checked,T=document.getElementById("external-follow-redirects").checked;if(!e||!a){showNotification("Please fill in Name and External URL","warning");return}if(!p){showNotification("Could not derive subdomain from name. Please set one in Options.","warning");return}if(!a.startsWith("http://")&&!a.startsWith("https://")){showNotification("External URL must start with http:// or https://","warning");return}const A=buildDomain(p);try{const E={dns:null,caddy:null,dashboard:!1};if(v)if(window.getToken(getPrimaryDnsId(),"admin"))try{const C=await(await secureFetch("/api/v1/dns/record",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:A,ip:k,ttl:DC.DEFAULTS.TTL,server:SITE.dnsIp})})).json();E.dns=C.success?"created":C.error||"failed"}catch(S){E.dns=S.message}else E.dns="no admin token (configure in \u{1F511} Tokens)";if(d)try{const I={subdomain:p,externalUrl:a,preserveHost:B,followRedirects:T,sslType:"caddy-managed",caddyfilePath:DC.DEFAULTS.CADDYFILE,reloadCaddy:!0},C=await(await secureFetch("/api/v1/site/external",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(I)})).json();E.caddy=C.success?"created":C.error||"failed"}catch(I){E.caddy=I.message}const L={id:p,name:e,url:`https://${A}`,externalUrl:a,logo:t||c||"\u{1F310}",isExternal:!0,isCustom:!0};window.APPS.push(L),E.dashboard=!0;const $=["plex","router","chat","sync","torrent","radarr","sonarr","prowlarr","portainer","requests","jellyfin","emby"],x=window.APPS.filter(I=>!$.includes(I.id));safeSet("custom-services",JSON.stringify(x));try{await secureFetch("/api/v1/services",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(window.APPS)})}catch(I){console.warn("Failed to save to services.json:",I)}window.buildGrid(),window.refreshAll(),r();const b=[`External service "${e}" added!`];v&&b.push(`DNS: ${E.dns==="created"?"\u2713":"\u26A0 "+E.dns}`),d&&b.push(`Caddy: ${E.caddy==="created"?"\u2713":"\u26A0 "+E.caddy}`),b.push(`Access at: https://${A}`),showNotification(b.join(" | "),"success",6e3)}catch(E){console.error("Failed to create external service:",E),showNotification(`Failed to create external service: ${E.message}`,"error")}}function r(){closeModal("add-service-modal"),document.body.style.overflow="",document.getElementById("service-name-input").value="",document.getElementById("service-subdomain-input").value="",document.getElementById("service-port-input").value="",document.getElementById("service-ip-input").value=g.lan||"",document.getElementById("service-logo-input").value="",document.getElementById("dns-ttl-input").value=DC.DEFAULTS.TTL,document.getElementById("ssl-type-select").value=f(),document.getElementById("ca-name-input").value="",document.getElementById("enable-auth").checked=!1,document.getElementById("enable-cors").checked=!1,document.getElementById("custom-headers-input").value="",document.getElementById("upstream-path-input").value="/",document.getElementById("health-check-input").value="",document.getElementById("timeout-input").value="30";const e=document.getElementById("subdomain-preview");e&&(e.textContent="");const a=document.getElementById("external-subdomain-preview");a&&(a.textContent="");const p=document.getElementById("external-service-name");p&&(p.value="");const t=document.getElementById("external-service-subdomain");t&&(t.value="");const c=document.getElementById("external-service-url");c&&(c.value="");const v=document.getElementById("external-service-logo");v&&(v.value="");const d=document.getElementById("external-service-icon");d&&(d.value="");const k=document.getElementById("local-advanced-options");k&&k.removeAttribute("open");const B=document.getElementById("external-advanced-options");B&&B.removeAttribute("open");const T=document.getElementById("service-type-local");T&&(T.checked=!0);const A=document.getElementById("local-service-config"),E=document.getElementById("external-service-config");A&&(A.style.display="grid"),E&&(E.style.display="none");const L=document.getElementById("tab-local"),$=document.getElementById("tab-external");L&&(L.style.background="var(--accent)",L.style.color="var(--bg)"),$&&($.style.background="transparent",$.style.color="var(--muted)")}async function l(){const e=document.getElementById("service-name-input").value.trim(),a=(document.getElementById("service-subdomain-input").value.trim()||o(e)).toLowerCase(),p=document.getElementById("service-port-input").value.trim(),t=document.getElementById("service-ip-input").value.trim(),c=document.getElementById("service-logo-input").value.trim(),v=document.getElementById("create-dns-record").checked,d=parseInt(document.getElementById("dns-ttl-input").value)||DC.DEFAULTS.TTL,k=document.getElementById("manual-tailscale-only")?.checked||!1,B=document.getElementById("ssl-type-select")?.value||"caddy-managed",T=document.getElementById("ca-name-input")?.value||"",A=document.getElementById("existing-ca-select")?.value||"",E=document.getElementById("enable-auth")?.checked||!1,L=document.getElementById("enable-cors")?.checked||!1,$=document.getElementById("custom-headers-input")?.value||"",x=document.getElementById("upstream-path-input")?.value||"/",b=document.getElementById("health-check-input")?.value||"",I=document.getElementById("timeout-input")?.value||30,S=window.getToken(getPrimaryDnsId(),"admin");if(!e||!p||!t){showNotification("Please fill in Name, Port, and IP Address","warning");return}if(!a){showNotification("Could not derive subdomain from name. Please set one in Options.","warning");return}if(v&&!S){showNotification("DNS Admin token required. Configure it in the Tokens menu first.","warning");return}const C={dns:null,caddy:null,dashboard:!1};try{if(v)try{await window.createDnsRecord(a,t,d),C.dns="created"}catch(N){throw console.error("DNS creation failed:",N),C.dns=N.message,new Error(`DNS creation failed: ${N.message}`)}else C.dns="skipped";const P=window.generateCaddyConfig({subdomain:a,port:p,ip:t,sslType:B,caName:T,existingCa:A,enableAuth:E,enableCors:L,customHeaders:$,upstreamPath:x,healthCheck:b,timeout:I,tailscaleOnly:k});try{const R=await(await secureFetch("/api/v1/site",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:buildDomain(a),upstream:`${t}:${p}`,config:P})})).json();if(R.success)C.caddy="added & reloaded";else throw console.error("Caddy configuration failed:",R.error),C.caddy=R.error||"failed",new Error(`Caddy configuration failed: ${R.error}`)}catch(N){throw console.error("Caddy API error:",N),C.caddy=N.message,new Error(`Caddy API error: ${N.message}`)}const D={name:e,subdomain:a,port:p,ip:t,logo:c||`/assets/${a}.png`,tailscaleOnly:k||!1};await window.addServiceToConfig(D),C.dashboard=!0;const O=[`DNS: ${C.dns==="created"?"\u2713":C.dns==="skipped"?"\u25CB":"\u2717"}`,`Caddy: ${C.caddy==="added & reloaded"?"\u2713":"\u2717"}`,`Dashboard: ${C.dashboard?"\u2713":"\u2717"}`];showNotification(`Service "${e}" created! ${O.join(" | ")} \u2014 ${buildServiceUrl(a)}${k?" (Tailscale)":""}`,"success",6e3),r(),window.buildGrid(),window.refreshAll()}catch(P){console.error("Error creating service:",P),showNotification(`Error creating "${e}": ${P.message}`,"error",6e3)}}document.getElementById("add-service")?.addEventListener("click",h),document.getElementById("add-service-cancel")?.addEventListener("click",r),document.getElementById("add-service-create")?.addEventListener("click",()=>{document.querySelector('input[name="service-type"]:checked')?.value==="external"?m():l()}),n(),w(),s(),document.getElementById("ssl-type-select")?.addEventListener("change",e=>{const a=document.getElementById("existing-ca-config"),p=document.getElementById("custom-ca-config");a.style.display="none",p.style.display="none",e.target.value==="existing-ca"?a.style.display="block":e.target.value==="custom-ca"&&(p.style.display="block"),u()}),document.getElementById("refresh-cas")?.addEventListener("click",async()=>{const e=document.getElementById("refresh-cas"),a=e.textContent;e.textContent="\u231B Loading...",e.disabled=!0;try{const p=document.getElementById("caddyfile-path-input").value||DC.DEFAULTS.CADDYFILE;await window.loadExistingCAs(p),e.textContent="\u2705 Refreshed"}catch(p){e.textContent="\u274C Failed",console.error("Failed to refresh CAs:",p)}setTimeout(()=>{e.textContent=a,e.disabled=!1},2e3)}),document.getElementById("create-dns-record")?.addEventListener("change",e=>{const a=document.getElementById("dns-config");a.style.display=e.target.checked?"block":"none"}),["service-subdomain-input","service-ip-input","service-port-input","ca-name-input","existing-ca-select","enable-auth","enable-cors","custom-headers-input","upstream-path-input","health-check-input","timeout-input"].forEach(e=>{const a=document.getElementById(e);a&&(a.addEventListener("input",u),a.addEventListener("change",u))});function y(){const e=safeGet("custom-services");if(e)try{JSON.parse(e).forEach(p=>{window.APPS.find(t=>t.id===p.id)||window.APPS.push(p)})}catch(a){console.warn("Failed to load custom services:",a)}}y(),window.openAddServiceModal=h,window.closeAddServiceModal=r})(),(function(){let o=null,f=1e3;const u=3e4;function g(){if(o)try{o.close()}catch{}o=new EventSource("/api/v1/events/stream"),o.addEventListener("connected",()=>{f=1e3,debug("[SSE] Connected to event stream")}),o.addEventListener("status-change",i=>{try{const s=JSON.parse(i.data);if(s.serviceId&&typeof window.setBadge=="function"){const h=s.status==="up"||s.status==="healthy";window.setBadge(s.serviceId,h,s.responseTime||null)}}catch{}}),o.addEventListener("resource-alert",i=>{try{const s=JSON.parse(i.data),h=`${s.containerName||s.containerId}: ${s.metric} at ${s.value}% (threshold: ${s.threshold}%)`;typeof showNotification=="function"&&showNotification(h,"warning")}catch{}}),o.addEventListener("auto-restart",i=>{try{const s=JSON.parse(i.data);typeof showNotification=="function"&&showNotification(`Container "${s.containerName}" was auto-restarted`,"info")}catch{}}),o.addEventListener("update-available",i=>{try{const s=JSON.parse(i.data),h=document.getElementById("updates-btn");if(h&&!h.querySelector(".sse-dot")){const n=document.createElement("span");n.className="sse-dot",n.style.cssText="display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--accent);margin-left:6px;vertical-align:middle;",h.appendChild(n)}typeof showNotification=="function"&&showNotification(`Update available for ${s.containerName||s.containerId}`,"info")}catch{}}),o.addEventListener("update-complete",i=>{try{const s=JSON.parse(i.data);typeof showNotification=="function"&&showNotification(`Update completed: ${s.containerName||s.containerId}`,"success"),typeof window.refreshAll=="function"&&window.refreshAll()}catch{}}),o.addEventListener("update-failed",i=>{try{const s=JSON.parse(i.data);typeof showNotification=="function"&&showNotification(`Update failed: ${s.containerName||s.containerId} \u2014 ${s.error||"unknown error"}`,"error")}catch{}}),o.addEventListener("incident",i=>{try{const s=JSON.parse(i.data);typeof showNotification=="function"&&(s.type==="created"?showNotification(`Incident: ${s.message||s.serviceId}`,"error"):s.type==="resolved"&&showNotification(`Resolved: ${s.serviceId||"incident"}`,"success"))}catch{}}),o.onerror=()=>{o.close(),console.warn(`[SSE] Disconnected, reconnecting in ${f/1e3}s...`),setTimeout(g,f),f=Math.min(f*2,u)}}g(),window._sseReconnect=g})(),(function(){const o=document.getElementById("service-filter-search"),f=document.getElementById("service-filter-status"),u=document.getElementById("service-filter-count");function g(){const s=o.value.toLowerCase().trim(),h=f.value,n=document.querySelectorAll("#cards .card");let w=0;if(n.forEach(m=>{const r=m.querySelector(".name")?.textContent?.toLowerCase()||"",l=m.dataset.app?.toLowerCase()||"",y=m.dataset.status||"off";(!s||r.includes(s)||l.includes(s))&&(h==="all"||y===h)?(m.style.display="",w++):m.style.display="none"}),u){const m=n.length;u.textContent=`${w} of ${m} services`}}function i(s,h){let n;return function(...w){clearTimeout(n),n=setTimeout(()=>s.apply(this,w),h)}}o?.addEventListener("input",i(g,200)),f?.addEventListener("change",g),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>setTimeout(g,500)):setTimeout(g,500),window.refreshServiceFilter=g})(),(function(){const o=document.getElementById("batch-operations-btn"),f=document.getElementById("batch-action-bar"),u=document.getElementById("batch-selected-count"),g=document.getElementById("batch-start-btn"),i=document.getElementById("batch-stop-btn"),s=document.getElementById("batch-restart-btn"),h=document.getElementById("batch-cancel-btn");let n=!1,w=new Set;function m(){n=!0,w.clear(),f.style.display="",o.textContent="\u2713 Exit Batch Mode",l(),document.querySelectorAll("#cards .card[data-app]").forEach(a=>{const p=a.dataset.containerId;if(!p)return;const t=a.querySelector(".batch-checkbox");t&&t.remove();const c=document.createElement("input");c.type="checkbox",c.className="batch-checkbox",c.dataset.containerId=p,c.dataset.serviceName=a.querySelector(".name")?.textContent||p,c.style.cssText="position: absolute; top: 8px; left: 8px; z-index: 10; width: 18px; height: 18px; cursor: pointer;",c.addEventListener("change",v=>{v.stopPropagation(),c.checked?w.add(p):w.delete(p),l()}),a.style.position="relative",a.insertBefore(c,a.firstChild)})}function r(){n=!1,w.clear(),f.style.display="none",o.textContent="\u2630 Batch Operations",document.querySelectorAll(".batch-checkbox").forEach(e=>e.remove())}function l(){const e=w.size;u.textContent=`${e} selected`,g.disabled=e===0,i.disabled=e===0,s.disabled=e===0}async function y(e){if(w.size===0)return;const a=Array.from(w),p={start:"Starting",stop:"Stopping",restart:"Restarting"}[e];if(!confirm(`${p} ${a.length} container(s)? This cannot be undone.`))return;const t=[g,i,s];t.forEach(k=>{k.disabled=!0,k.textContent="..."});let c=0,v=0;const d=[];for(const k of a)try{const B=await fetch(`/api/v1/containers/${encodeURIComponent(k)}/${e}`,{method:"POST"});if(B.ok)c++;else{v++;const T=await B.json().catch(()=>({}));d.push(`${k}: ${T.error||B.statusText}`)}}catch(B){v++,d.push(`${k}: ${B.message}`)}t[0].textContent="\u25B6 Start All",t[1].textContent="\u2B1B Stop All",t[2].textContent="\u{1F504} Restart All",l(),v===0?typeof showNotification=="function"&&showNotification(`${p} completed: ${c} container(s)`,"success"):(typeof showNotification=="function"&&showNotification(`${p}: ${c} succeeded, ${v} failed`,"warning"),console.error("Batch operation errors:",d)),setTimeout(()=>{typeof refreshAll=="function"&&refreshAll()},1500)}o?.addEventListener("click",()=>{n?r():m()}),g?.addEventListener("click",()=>y("start")),i?.addEventListener("click",()=>y("stop")),s?.addEventListener("click",()=>y("restart")),h?.addEventListener("click",r)})(); diff --git a/status/dist/features.js b/status/dist/features.js index 7e4f872..a2e773a 100644 --- a/status/dist/features.js +++ b/status/dist/features.js @@ -92,7 +92,7 @@ `);const b=document.getElementById("logo-modal"),E=document.getElementById("logo-preview-dark"),N=document.getElementById("logo-preview-light"),S=document.getElementById("logo-status"),T=document.getElementById("logo-same-both"),P=document.getElementById("logo-dual-uploads"),L=document.getElementById("logo-single-upload"),H=document.getElementById("logo-upload-dark"),g=document.getElementById("logo-upload-light"),I=document.getElementById("logo-upload-single"),k=document.querySelector("#brand .brand-logo-dark"),x=document.querySelector("#brand .brand-logo-light"),$=document.querySelector(".top-row"),C=document.getElementById("dashboard-title"),R=DC.NAME;let M=null,j=null,B=null,A="left",w=R;T?.addEventListener("change",()=>{T.checked?(P.style.display="none",L.style.display="",M=null,j=null):(P.style.display="flex",L.style.display="none",B=null)});function z(a,e){if(!a||!a.type.startsWith("image/")){showNotification("Please select an image file","warning");return}const n=new FileReader;n.onload=t=>e(t.target.result),n.readAsDataURL(a)}H?.addEventListener("change",a=>{z(a.target.files[0],e=>{M=e,E.src=e,S.textContent="New dark logo ready to save"})}),g?.addEventListener("change",a=>{z(a.target.files[0],e=>{j=e,N.src=e,S.textContent="New light logo ready to save"})}),I?.addEventListener("change",a=>{z(a.target.files[0],e=>{B=e,E.src=e,N.src=e,S.textContent="New logo ready to save (both themes)"})});function f(a){$.setAttribute("data-logo-pos",a),document.querySelectorAll(".logo-pos-btn").forEach(e=>{e.style.background=e.dataset.pos===a?"var(--accent)":"var(--card-bg)",e.style.color=e.dataset.pos===a?"white":"var(--fg)"})}function p(a){w=a||R,document.title=w;const e=document.querySelector(".dashboard-title");e&&(e.textContent=w)}async function y(){try{const a=await fetch("/api/v1/logo");if(a.ok){const e=await a.json();e.customLogoDark&&(k.src=e.customLogoDark,E.src=e.customLogoDark),e.customLogoLight&&(x.src=e.customLogoLight,N.src=e.customLogoLight),!e.customLogoDark&&!e.customLogoLight&&e.customLogo&&(k.src=e.customLogo,x.src=e.customLogo,E.src=e.customLogo,N.src=e.customLogo),e.isDefault||(S.textContent="Using custom logo"),e.position&&(A=e.position,f(e.position)),e.dashboardTitle&&p(e.dashboardTitle)}}catch(a){console.warn("Could not load custom logo:",a.message)}}document.querySelectorAll(".logo-pos-btn").forEach(a=>{a.addEventListener("click",()=>{A=a.dataset.pos,f(A)})}),document.getElementById("brand")?.addEventListener("click",()=>{M=null,j=null,B=null,H&&(H.value=""),g&&(g.value=""),I&&(I.value=""),T&&(T.checked=!1),P.style.display="flex",L.style.display="none",E.src=k.src,N.src=x.src;const a=k.src.includes("custom-logo")||x.src.includes("custom-logo");S.textContent=a?"Using custom logo":"Using default logos",f(A),C.value=w,b.classList.add("show")}),document.getElementById("logo-save")?.addEventListener("click",async()=>{try{const a=C.value.trim()||R,e={position:A,dashboardTitle:a};T?.checked&&B?(e.dataDark=B,e.dataLight=B):(M&&(e.dataDark=M),j&&(e.dataLight=j));const n=await secureFetch("/api/v1/logo",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});if(n.ok){const t=await n.json(),i="?t="+Date.now();t.pathDark&&(k.src=t.pathDark+i,E.src=t.pathDark+i),t.pathLight&&(x.src=t.pathLight+i,N.src=t.pathLight+i),f(A),p(a),b.classList.remove("show")}else{const t=await n.json();showNotification("Failed to save: "+t.error,"error")}}catch(a){showNotification("Error saving: "+a.message,"error")}}),document.getElementById("logo-reset")?.addEventListener("click",async()=>{if(confirm(`Reset all branding to DashCaddy defaults? -This will reset the logo, favicon, title, and position.`))try{if((await secureFetch("/api/v1/logo",{method:"DELETE"})).ok&&(k.src="/assets/dashcaddy-logo-dark.png",x.src="/assets/dashcaddy-logo-light.png",E.src="/assets/dashcaddy-logo-dark.png",N.src="/assets/dashcaddy-logo-light.png",S.textContent="Using default logos",M=null,j=null,B=null,C.value=R,p(R),A="left",f("left")),(await secureFetch("/api/v1/favicon",{method:"DELETE"})).ok){const n=document.querySelector('link[rel="icon"]'),t=document.getElementById("favicon-preview"),i=document.getElementById("favicon-status");n&&(n.href="/assets/dashcaddy-favicon.ico?t="+Date.now()),t&&(t.src="/assets/dashcaddy-favicon.ico?t="+Date.now()),i&&(i.textContent="Using DashCaddy favicon"),s=null}}catch(a){showNotification("Error resetting branding: "+a.message,"error")}}),wireModal(b,document.getElementById("logo-cancel"));const v=document.getElementById("favicon-preview"),m=document.getElementById("favicon-status"),r=document.getElementById("favicon-upload"),c=document.querySelector('link[rel="icon"]')||document.createElement("link");let s=null;document.querySelector('link[rel="icon"]')||(c.rel="icon",c.href="/assets/dashcaddy-favicon.ico",document.head.appendChild(c));async function h(){try{const a=await fetch("/api/v1/favicon");if(a.ok){const e=await a.json();e.customFavicon&&(c.href=e.customFavicon+"?t="+Date.now(),v.src=e.customFavicon+"?t="+Date.now(),m.textContent="Using custom favicon")}}catch(a){console.warn("Could not load custom favicon:",a.message)}}r?.addEventListener("change",a=>{const e=a.target.files[0];if(!e)return;if(!e.type.match(/^image\/(png|svg\+xml)$/)){showNotification("Please select a PNG or SVG file","warning"),r.value="";return}const n=new FileReader;n.onload=t=>{s=t.target.result,v.src=s,m.textContent="New favicon ready to save"},n.readAsDataURL(e)}),document.getElementById("logo-save")?.addEventListener("click",async()=>{if(s)try{const a=await secureFetch("/api/v1/favicon",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({data:s})});if(a.ok){const e=await a.json();c.href=e.path+"?t="+Date.now(),v.src=e.path+"?t="+Date.now(),m.textContent="Using custom favicon",s=null}else{const e=await a.json();showNotification("Failed to save favicon: "+e.error,"error")}}catch(a){showNotification("Error saving favicon: "+a.message,"error")}}),h(),y();const u=document.getElementById("settings-timezone");u&&(new MutationObserver(()=>{b.classList.contains("show")&&u.options.length===0&&(async()=>{let e;try{const n=await fetch("/api/v1/config");n.ok&&(e=(await n.json()).timezone)}catch{}window.populateTimezoneSelect(u,e)})()}).observe(b,{attributes:!0,attributeFilter:["class"]}),document.getElementById("logo-save")?.addEventListener("click",async()=>{const e=u.value;if(e)try{const n=await fetch("/api/v1/config");if(!n.ok)return;const t=await n.json();t.timezone=e,t.updatedAt=new Date().toISOString(),await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})}catch(n){console.warn("Failed to save timezone:",n.message)}}))})();const errorHandler=new ErrorHandler;window.populateTimezoneSelect=function(b,E){const N=Intl.supportedValuesOf("timeZone"),S=E||Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC";b.innerHTML="";for(const T of N){const P=document.createElement("option");P.value=T,P.textContent=T.replace(/_/g," "),T===S&&(P.selected=!0),b.appendChild(P)}},(function(){let b="homelab",E=null;async function N(){try{const z=await fetch("/api/v1/config");if(z.ok&&(E=await z.json(),E&&E.setupComplete))return document.getElementById("setup-wizard").style.display="none",!0}catch(z){console.warn("Could not fetch server config, checking localStorage fallback:",z.message)}return safeGet("dashcaddy-setup")?(document.getElementById("setup-wizard").style.display="none",!0):(document.getElementById("setup-wizard").style.display="flex",!1)}N();const S=document.getElementById("setup-timezone");S&&window.populateTimezoneSelect(S);function T(w){document.querySelectorAll(".setup-step").forEach(f=>{f.style.display="none"});const z=document.getElementById(w);z&&(z.style.display="block")}function P(){const w=document.getElementById("setup-summary-content");if(!w)return;let z='
';if(b==="homelab"){const p=document.getElementById("setup-tld")?.value?.trim()||".home",y=document.getElementById("setup-ca-name")?.value?.trim()||"",v=document.getElementById("setup-dns-ip")?.value?.trim()||"",m=document.getElementById("setup-dns-port")?.value?.trim()||DC.DEFAULTS.DNS_PORT;z+=` +This will reset the logo, favicon, title, and position.`))try{if((await secureFetch("/api/v1/logo",{method:"DELETE"})).ok&&(k.src="/assets/dashcaddy-logo-dark.png",x.src="/assets/dashcaddy-logo-light.png",E.src="/assets/dashcaddy-logo-dark.png",N.src="/assets/dashcaddy-logo-light.png",S.textContent="Using default logos",M=null,j=null,B=null,C.value=R,p(R),A="left",f("left")),(await secureFetch("/api/v1/favicon",{method:"DELETE"})).ok){const n=document.querySelector('link[rel="icon"]'),t=document.getElementById("favicon-preview"),i=document.getElementById("favicon-status");n&&(n.href="/assets/dashcaddy-favicon.ico?t="+Date.now()),t&&(t.src="/assets/dashcaddy-favicon.ico?t="+Date.now()),i&&(i.textContent="Using DashCaddy favicon"),s=null}}catch(a){showNotification("Error resetting branding: "+a.message,"error")}}),wireModal(b,document.getElementById("logo-cancel"));const v=document.getElementById("favicon-preview"),m=document.getElementById("favicon-status"),r=document.getElementById("favicon-upload"),c=document.querySelector('link[rel="icon"]')||document.createElement("link");let s=null;document.querySelector('link[rel="icon"]')||(c.rel="icon",c.href="/assets/dashcaddy-favicon.ico",document.head.appendChild(c));async function h(){try{const a=await fetch("/api/v1/favicon");if(a.ok){const e=await a.json();e.customFavicon&&(c.href=e.customFavicon+"?t="+Date.now(),v.src=e.customFavicon+"?t="+Date.now(),m.textContent="Using custom favicon")}}catch(a){console.warn("Could not load custom favicon:",a.message)}}r?.addEventListener("change",a=>{const e=a.target.files[0];if(!e)return;if(!e.type.match(/^image\/(png|svg\+xml)$/)){showNotification("Please select a PNG or SVG file","warning"),r.value="";return}const n=new FileReader;n.onload=t=>{s=t.target.result,v.src=s,m.textContent="New favicon ready to save"},n.readAsDataURL(e)}),document.getElementById("logo-save")?.addEventListener("click",async()=>{if(s)try{const a=await secureFetch("/api/v1/favicon",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({data:s})});if(a.ok){const e=await a.json();c.href=e.path+"?t="+Date.now(),v.src=e.path+"?t="+Date.now(),m.textContent="Using custom favicon",s=null}else{const e=await a.json();showNotification("Failed to save favicon: "+e.error,"error")}}catch(a){showNotification("Error saving favicon: "+a.message,"error")}}),h(),y();const u=document.getElementById("settings-timezone");u&&(new MutationObserver(()=>{b.classList.contains("show")&&u.options.length===0&&(async()=>{let e;try{const n=await fetch("/api/v1/config");n.ok&&(e=(await n.json()).timezone)}catch{}window.populateTimezoneSelect(u,e)})()}).observe(b,{attributes:!0,attributeFilter:["class"]}),document.getElementById("logo-save")?.addEventListener("click",async()=>{const e=u.value;if(e)try{const n=await fetch("/api/v1/config");if(!n.ok)return;const t=await n.json();t.timezone=e,t.updatedAt=new Date().toISOString(),await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})}catch(n){console.warn("Failed to save timezone:",n.message)}}))})(),window.populateTimezoneSelect=function(b,E){const N=Intl.supportedValuesOf("timeZone"),S=E||Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC";b.innerHTML="";for(const T of N){const P=document.createElement("option");P.value=T,P.textContent=T.replace(/_/g," "),T===S&&(P.selected=!0),b.appendChild(P)}},(function(){let b="homelab",E=null;async function N(){try{const z=await fetch("/api/v1/config");if(z.ok&&(E=await z.json(),E&&E.setupComplete))return document.getElementById("setup-wizard").style.display="none",!0}catch(z){console.warn("Could not fetch server config, checking localStorage fallback:",z.message)}return safeGet("dashcaddy-setup")?(document.getElementById("setup-wizard").style.display="none",!0):(document.getElementById("setup-wizard").style.display="flex",!1)}N();const S=document.getElementById("setup-timezone");S&&window.populateTimezoneSelect(S);function T(w){document.querySelectorAll(".setup-step").forEach(f=>{f.style.display="none"});const z=document.getElementById(w);z&&(z.style.display="block")}function P(){const w=document.getElementById("setup-summary-content");if(!w)return;let z='
';if(b==="homelab"){const p=document.getElementById("setup-tld")?.value?.trim()||".home",y=document.getElementById("setup-ca-name")?.value?.trim()||"",v=document.getElementById("setup-dns-ip")?.value?.trim()||"",m=document.getElementById("setup-dns-port")?.value?.trim()||DC.DEFAULTS.DNS_PORT;z+=`

Home Lab Configuration

diff --git a/status/dist/onboarding.js b/status/dist/onboarding.js index 5771f91..6d2fd81 100644 --- a/status/dist/onboarding.js +++ b/status/dist/onboarding.js @@ -1,48 +1,30 @@ -this.driver=this.driver||{},this.driver.js=(function(v){"use strict";let b={};function k(e={}){b={animate:!0,allowClose:!0,overlayOpacity:.7,smoothScroll:!1,disableActiveInteraction:!1,showProgress:!1,stagePadding:10,stageRadius:5,popoverOffset:10,showButtons:["next","previous","close"],disableButtons:[],overlayColor:"#000",...e}}function o(e){return e?b[e]:b}function s(e,r,n,l){return(e/=l/2)<1?n/2*e*e+r:-n/2*(--e*(e-2)-1)+r}function t(e){const r='a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled])';return e.flatMap(n=>{const l=n.matches(r),i=Array.from(n.querySelectorAll(r));return[...l?[n]:[],...i]}).filter(n=>getComputedStyle(n).pointerEvents!=="none"&&T(n))}function a(e){if(!e||h(e))return;const r=o("smoothScroll");e.scrollIntoView({behavior:!r||d(e)?"auto":"smooth",inline:"center",block:"center"})}function d(e){if(!e||!e.parentElement)return;const r=e.parentElement;return r.scrollHeight>r.clientHeight}function h(e){const r=e.getBoundingClientRect();return r.top>=0&&r.left>=0&&r.bottom<=(window.innerHeight||document.documentElement.clientHeight)&&r.right<=(window.innerWidth||document.documentElement.clientWidth)}function T(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)}let c={};function x(e,r){c[e]=r}function p(e){return e?c[e]:c}function B(){c={}}let R={};function U(e,r){R[e]=r}function H(e){var r;(r=R[e])==null||r.call(R)}function se(){R={}}function ae(e,r,n,l){let i=p("__activeStagePosition");const g=i||n.getBoundingClientRect(),C=l.getBoundingClientRect(),E=s(e,g.x,C.x-g.x,r),m=s(e,g.y,C.y-g.y,r),S=s(e,g.width,C.width-g.width,r),u=s(e,g.height,C.height-g.height,r);i={x:E,y:m,width:S,height:u},J(i),x("__activeStagePosition",i)}function K(e){if(!e)return;const r=e.getBoundingClientRect(),n={x:r.x,y:r.y,width:r.width,height:r.height};x("__activeStagePosition",n),J(n)}function le(){const e=p("__activeStagePosition"),r=p("__overlaySvg");if(!e)return;if(!r){console.warn("No stage svg found.");return}const n=window.innerWidth,l=window.innerHeight;r.setAttribute("viewBox",`0 0 ${n} ${l}`)}function de(e){const r=pe(e);document.body.appendChild(r),ee(r,n=>{n.target.tagName==="path"&&H("overlayClick")}),x("__overlaySvg",r)}function J(e){const r=p("__overlaySvg");if(!r){de(e);return}const n=r.firstElementChild;if(n?.tagName!=="path")throw new Error("no path element found in stage svg");n.setAttribute("d",Q(e))}function pe(e){const r=window.innerWidth,n=window.innerHeight,l=document.createElementNS("http://www.w3.org/2000/svg","svg");l.classList.add("driver-overlay","driver-overlay-animated"),l.setAttribute("viewBox",`0 0 ${r} ${n}`),l.setAttribute("xmlSpace","preserve"),l.setAttribute("xmlnsXlink","http://www.w3.org/1999/xlink"),l.setAttribute("version","1.1"),l.setAttribute("preserveAspectRatio","xMinYMin slice"),l.style.fillRule="evenodd",l.style.clipRule="evenodd",l.style.strokeLinejoin="round",l.style.strokeMiterlimit="2",l.style.zIndex="10000",l.style.position="fixed",l.style.top="0",l.style.left="0",l.style.width="100%",l.style.height="100%";const i=document.createElementNS("http://www.w3.org/2000/svg","path");return i.setAttribute("d",Q(e)),i.style.fill=o("overlayColor")||"rgb(0,0,0)",i.style.opacity=`${o("overlayOpacity")}`,i.style.pointerEvents="auto",i.style.cursor="auto",l.appendChild(i),l}function Q(e){const r=window.innerWidth,n=window.innerHeight,l=o("stagePadding")||0,i=o("stageRadius")||0,g=e.width+l*2,C=e.height+l*2,E=Math.min(i,g/2,C/2),m=Math.floor(Math.max(E,0)),S=e.x-l+m,u=e.y-l,f=g-m*2,y=C-m*2;return`M${r},0L0,0L0,${n}L${r},${n}L${r},0Z - M${S},${u} h${f} a${m},${m} 0 0 1 ${m},${m} v${y} a${m},${m} 0 0 1 -${m},${m} h-${f} a${m},${m} 0 0 1 -${m},-${m} v-${y} a${m},${m} 0 0 1 ${m},-${m} z`}function ce(){const e=p("__overlaySvg");e&&e.remove()}function ue(){const e=document.getElementById("driver-dummy-element");if(e)return e;let r=document.createElement("div");return r.id="driver-dummy-element",r.style.width="0",r.style.height="0",r.style.pointerEvents="none",r.style.opacity="0",r.style.position="fixed",r.style.top="50%",r.style.left="50%",document.body.appendChild(r),r}function X(e){const{element:r}=e;let n=typeof r=="string"?document.querySelector(r):r;n||(n=ue()),he(n,e)}function me(){const e=p("__activeElement"),r=p("__activeStep");e&&(K(e),le(),ne(e,r))}function he(e,r){const n=Date.now(),l=p("__activeStep"),i=p("__activeElement")||e,g=!i||i===e,C=e.id==="driver-dummy-element",E=i.id==="driver-dummy-element",m=o("animate"),S=r.onHighlightStarted||o("onHighlightStarted"),u=r?.onHighlighted||o("onHighlighted"),f=l?.onDeselected||o("onDeselected"),y=o(),D=p();!g&&f&&f(E?void 0:i,l,{config:y,state:D}),S&&S(C?void 0:e,r,{config:y,state:D});const _=!g&&m;let A=!1;we(),x("previousStep",l),x("previousElement",i),x("activeStep",r),x("activeElement",e);const w=()=>{if(p("__transitionCallback")!==w)return;const P=Date.now()-n,$=400-P<=400/2;r.popover&&$&&!A&&_&&(te(e,r),A=!0),o("animate")&&P<400?ae(P,400,i,e):(K(e),u&&u(C?void 0:e,r,{config:o(),state:p()}),x("__transitionCallback",void 0),x("__previousStep",l),x("__previousElement",i),x("__activeStep",r),x("__activeElement",e)),window.requestAnimationFrame(w)};x("__transitionCallback",w),window.requestAnimationFrame(w),a(e),!_&&r.popover&&te(e,r),i.classList.remove("driver-active-element","driver-no-interaction"),i.removeAttribute("aria-haspopup"),i.removeAttribute("aria-expanded"),i.removeAttribute("aria-controls"),o("disableActiveInteraction")&&e.classList.add("driver-no-interaction"),e.classList.add("driver-active-element"),e.setAttribute("aria-haspopup","dialog"),e.setAttribute("aria-expanded","true"),e.setAttribute("aria-controls","driver-popover-content")}function ge(){var e;(e=document.getElementById("driver-dummy-element"))==null||e.remove(),document.querySelectorAll(".driver-active-element").forEach(r=>{r.classList.remove("driver-active-element","driver-no-interaction"),r.removeAttribute("aria-haspopup"),r.removeAttribute("aria-expanded"),r.removeAttribute("aria-controls")})}function F(){const e=p("__resizeTimeout");e&&window.cancelAnimationFrame(e),x("__resizeTimeout",window.requestAnimationFrame(me))}function ve(e){var r;if(!p("isInitialized")||!(e.key==="Tab"||e.keyCode===9))return;const n=p("__activeElement"),l=(r=p("popover"))==null?void 0:r.wrapper,i=t([...l?[l]:[],...n?[n]:[]]),g=i[0],C=i[i.length-1];if(e.preventDefault(),e.shiftKey){const E=i[i.indexOf(document.activeElement)-1]||C;E?.focus()}else{const E=i[i.indexOf(document.activeElement)+1]||g;E?.focus()}}function Z(e){var r;((r=o("allowKeyboardControl"))==null||r)&&(e.key==="Escape"?H("escapePress"):e.key==="ArrowRight"?H("arrowRightPress"):e.key==="ArrowLeft"&&H("arrowLeftPress"))}function ee(e,r,n){const l=(i,g)=>{const C=i.target;e.contains(C)&&((!n||n(C))&&(i.preventDefault(),i.stopPropagation(),i.stopImmediatePropagation()),g?.(i))};document.addEventListener("pointerdown",l,!0),document.addEventListener("mousedown",l,!0),document.addEventListener("pointerup",l,!0),document.addEventListener("mouseup",l,!0),document.addEventListener("click",i=>{l(i,r)},!0)}function fe(){window.addEventListener("keyup",Z,!1),window.addEventListener("keydown",ve,!1),window.addEventListener("resize",F),window.addEventListener("scroll",F)}function ye(){window.removeEventListener("keyup",Z),window.removeEventListener("resize",F),window.removeEventListener("scroll",F)}function we(){const e=p("popover");e&&(e.wrapper.style.display="none")}function te(e,r){var n,l;let i=p("popover");i&&document.body.removeChild(i.wrapper),i=Te(),document.body.appendChild(i.wrapper);const{title:g,description:C,showButtons:E,disableButtons:m,showProgress:S,nextBtnText:u=o("nextBtnText")||"Next →",prevBtnText:f=o("prevBtnText")||"← Previous",progressText:y=o("progressText")||"{current} of {total}"}=r.popover||{};i.nextButton.innerHTML=u,i.previousButton.innerHTML=f,i.progress.innerHTML=y,g?(i.title.innerHTML=g,i.title.style.display="block"):i.title.style.display="none",C?(i.description.innerHTML=C,i.description.style.display="block"):i.description.style.display="none";const D=E||o("showButtons"),_=S||o("showProgress")||!1,A=D?.includes("next")||D?.includes("previous")||_;i.closeButton.style.display=D.includes("close")?"block":"none",A?(i.footer.style.display="flex",i.progress.style.display=_?"block":"none",i.nextButton.style.display=D.includes("next")?"block":"none",i.previousButton.style.display=D.includes("previous")?"block":"none"):i.footer.style.display="none";const w=m||o("disableButtons")||[];w!=null&&w.includes("next")&&(i.nextButton.disabled=!0,i.nextButton.classList.add("driver-popover-btn-disabled")),w!=null&&w.includes("previous")&&(i.previousButton.disabled=!0,i.previousButton.classList.add("driver-popover-btn-disabled")),w!=null&&w.includes("close")&&(i.closeButton.disabled=!0,i.closeButton.classList.add("driver-popover-btn-disabled"));const P=i.wrapper;P.style.display="block",P.style.left="",P.style.top="",P.style.bottom="",P.style.right="",P.id="driver-popover-content",P.setAttribute("role","dialog"),P.setAttribute("aria-labelledby","driver-popover-title"),P.setAttribute("aria-describedby","driver-popover-description");const $=i.arrow;$.className="driver-popover-arrow";const M=((n=r.popover)==null?void 0:n.popoverClass)||o("popoverClass")||"";P.className=`driver-popover ${M}`.trim(),ee(i.wrapper,O=>{var W,j,V;const z=O.target,G=((W=r.popover)==null?void 0:W.onNextClick)||o("onNextClick"),q=((j=r.popover)==null?void 0:j.onPrevClick)||o("onPrevClick"),Y=((V=r.popover)==null?void 0:V.onCloseClick)||o("onCloseClick");if(z.classList.contains("driver-popover-next-btn"))return G?G(e,r,{config:o(),state:p()}):H("nextClick");if(z.classList.contains("driver-popover-prev-btn"))return q?q(e,r,{config:o(),state:p()}):H("prevClick");if(z.classList.contains("driver-popover-close-btn"))return Y?Y(e,r,{config:o(),state:p()}):H("closeClick")},O=>!(i!=null&&i.description.contains(O))&&!(i!=null&&i.title.contains(O))&&typeof O.className=="string"&&O.className.includes("driver-popover")),x("popover",i);const L=((l=r.popover)==null?void 0:l.onPopoverRender)||o("onPopoverRender");L&&L(i,{config:o(),state:p()}),ne(e,r),a(P);const I=e.classList.contains("driver-dummy-element"),N=t([P,...I?[]:[e]]);N.length>0&&N[0].focus()}function re(){const e=p("popover");if(!(e!=null&&e.wrapper))return;const r=e.wrapper.getBoundingClientRect(),n=o("stagePadding")||0,l=o("popoverOffset")||0;return{width:r.width+n+l,height:r.height+n+l,realWidth:r.width,realHeight:r.height}}function oe(e,r){const{elementDimensions:n,popoverDimensions:l,popoverPadding:i,popoverArrowDimensions:g}=r;return e==="start"?Math.max(Math.min(n.top-i,window.innerHeight-l.realHeight-g.width),g.width):e==="end"?Math.max(Math.min(n.top-l?.realHeight+n.height+i,window.innerHeight-l?.realHeight-g.width),g.width):e==="center"?Math.max(Math.min(n.top+n.height/2-l?.realHeight/2,window.innerHeight-l?.realHeight-g.width),g.width):0}function ie(e,r){const{elementDimensions:n,popoverDimensions:l,popoverPadding:i,popoverArrowDimensions:g}=r;return e==="start"?Math.max(Math.min(n.left-i,window.innerWidth-l.realWidth-g.width),g.width):e==="end"?Math.max(Math.min(n.left-l?.realWidth+n.width+i,window.innerWidth-l?.realWidth-g.width),g.width):e==="center"?Math.max(Math.min(n.left+n.width/2-l?.realWidth/2,window.innerWidth-l?.realWidth-g.width),g.width):0}function ne(e,r){const n=p("popover");if(!n)return;const{align:l="start",side:i="left"}=r?.popover||{},g=l,C=e.id==="driver-dummy-element"?"over":i,E=o("stagePadding")||0,m=re(),S=n.arrow.getBoundingClientRect(),u=e.getBoundingClientRect(),f=u.top-m.height;let y=f>=0;const D=window.innerHeight-(u.bottom+m.height);let _=D>=0;const A=u.left-m.width;let w=A>=0;const P=window.innerWidth-(u.right+m.width);let $=P>=0;const M=!y&&!_&&!w&&!$;let L=C;if(C==="top"&&y?$=w=_=!1:C==="bottom"&&_?$=w=y=!1:C==="left"&&w?$=y=_=!1:C==="right"&&$&&(w=y=_=!1),C==="over"){const I=window.innerWidth/2-m.realWidth/2,N=window.innerHeight/2-m.realHeight/2;n.wrapper.style.left=`${I}px`,n.wrapper.style.right="auto",n.wrapper.style.top=`${N}px`,n.wrapper.style.bottom="auto"}else if(M){const I=window.innerWidth/2-m?.realWidth/2,N=10;n.wrapper.style.left=`${I}px`,n.wrapper.style.right="auto",n.wrapper.style.bottom=`${N}px`,n.wrapper.style.top="auto"}else if(w){const I=Math.min(A,window.innerWidth-m?.realWidth-S.width),N=oe(g,{elementDimensions:u,popoverDimensions:m,popoverPadding:E,popoverArrowDimensions:S});n.wrapper.style.left=`${I}px`,n.wrapper.style.top=`${N}px`,n.wrapper.style.bottom="auto",n.wrapper.style.right="auto",L="left"}else if($){const I=Math.min(P,window.innerWidth-m?.realWidth-S.width),N=oe(g,{elementDimensions:u,popoverDimensions:m,popoverPadding:E,popoverArrowDimensions:S});n.wrapper.style.right=`${I}px`,n.wrapper.style.top=`${N}px`,n.wrapper.style.bottom="auto",n.wrapper.style.left="auto",L="right"}else if(y){const I=Math.min(f,window.innerHeight-m.realHeight-S.width);let N=ie(g,{elementDimensions:u,popoverDimensions:m,popoverPadding:E,popoverArrowDimensions:S});n.wrapper.style.top=`${I}px`,n.wrapper.style.left=`${N}px`,n.wrapper.style.bottom="auto",n.wrapper.style.right="auto",L="top"}else if(_){const I=Math.min(D,window.innerHeight-m?.realHeight-S.width);let N=ie(g,{elementDimensions:u,popoverDimensions:m,popoverPadding:E,popoverArrowDimensions:S});n.wrapper.style.left=`${N}px`,n.wrapper.style.bottom=`${I}px`,n.wrapper.style.top="auto",n.wrapper.style.right="auto",L="bottom"}M?n.arrow.classList.add("driver-popover-arrow-none"):be(g,L,e)}function be(e,r,n){const l=p("popover");if(!l)return;const i=n.getBoundingClientRect(),g=re(),C=l.arrow,E=g.width,m=window.innerWidth,S=i.width,u=i.left,f=g.height,y=window.innerHeight,D=i.top,_=i.height;C.className="driver-popover-arrow";let A=r,w=e;r==="top"?(u+S<=0?(A="right",w="end"):u+S-E<=0&&(A="top",w="start"),u>=m?(A="left",w="end"):u+E>=m&&(A="top",w="end")):r==="bottom"?(u+S<=0?(A="right",w="start"):u+S-E<=0&&(A="bottom",w="start"),u>=m?(A="left",w="start"):u+E>=m&&(A="bottom",w="end")):r==="left"?(D+_<=0?(A="bottom",w="end"):D+_-f<=0&&(A="left",w="start"),D>=y?(A="top",w="end"):D+f>=y&&(A="left",w="end")):r==="right"&&(D+_<=0?(A="bottom",w="start"):D+_-f<=0&&(A="right",w="start"),D>=y?(A="top",w="start"):D+f>=y&&(A="right",w="end")),A?(C.classList.add(`driver-popover-arrow-side-${A}`),C.classList.add(`driver-popover-arrow-align-${w}`)):C.classList.add("driver-popover-arrow-none")}function Te(){const e=document.createElement("div");e.classList.add("driver-popover");const r=document.createElement("div");r.classList.add("driver-popover-arrow");const n=document.createElement("header");n.id="driver-popover-title",n.classList.add("driver-popover-title"),n.style.display="none",n.innerText="Popover Title";const l=document.createElement("div");l.id="driver-popover-description",l.classList.add("driver-popover-description"),l.style.display="none",l.innerText="Popover description is here";const i=document.createElement("button");i.type="button",i.classList.add("driver-popover-close-btn"),i.setAttribute("aria-label","Close"),i.innerHTML="×";const g=document.createElement("footer");g.classList.add("driver-popover-footer");const C=document.createElement("span");C.classList.add("driver-popover-progress-text"),C.innerText="";const E=document.createElement("span");E.classList.add("driver-popover-navigation-btns");const m=document.createElement("button");m.type="button",m.classList.add("driver-popover-prev-btn"),m.innerHTML="← Previous";const S=document.createElement("button");return S.type="button",S.classList.add("driver-popover-next-btn"),S.innerHTML="Next →",E.appendChild(m),E.appendChild(S),g.appendChild(C),g.appendChild(E),e.appendChild(i),e.appendChild(r),e.appendChild(n),e.appendChild(l),e.appendChild(g),{wrapper:e,arrow:r,title:n,description:l,footer:g,previousButton:m,nextButton:S,closeButton:i,footerButtons:E,progress:C}}function Se(){var e;const r=p("popover");r&&((e=r.wrapper.parentElement)==null||e.removeChild(r.wrapper))}const Ee="";function Ce(e={}){k(e);function r(){o("allowClose")&&S()}function n(){const u=p("activeIndex"),f=o("steps")||[];if(typeof u>"u")return;const y=u+1;f[y]?m(y):S()}function l(){const u=p("activeIndex"),f=o("steps")||[];if(typeof u>"u")return;const y=u-1;f[y]?m(y):S()}function i(u){(o("steps")||[])[u]?m(u):S()}function g(){var u;if(p("__transitionCallback"))return;const f=p("activeIndex"),y=p("__activeStep"),D=p("__activeElement");if(typeof f>"u"||typeof y>"u"||typeof p("activeIndex")>"u")return;const _=((u=y.popover)==null?void 0:u.onPrevClick)||o("onPrevClick");if(_)return _(D,y,{config:o(),state:p()});l()}function C(){var u;if(p("__transitionCallback"))return;const f=p("activeIndex"),y=p("__activeStep"),D=p("__activeElement");if(typeof f>"u"||typeof y>"u")return;const _=((u=y.popover)==null?void 0:u.onNextClick)||o("onNextClick");if(_)return _(D,y,{config:o(),state:p()});n()}function E(){p("isInitialized")||(x("isInitialized",!0),document.body.classList.add("driver-active",o("animate")?"driver-fade":"driver-simple"),fe(),U("overlayClick",r),U("escapePress",r),U("arrowLeftPress",g),U("arrowRightPress",C))}function m(u=0){var f,y,D,_,A,w,P,$;const M=o("steps");if(!M){console.error("No steps to drive through"),S();return}if(!M[u]){S();return}x("__activeOnDestroyed",document.activeElement),x("activeIndex",u);const L=M[u],I=M[u+1],N=M[u-1],O=((f=L.popover)==null?void 0:f.doneBtnText)||o("doneBtnText")||"Done",W=o("allowClose"),j=typeof((y=L.popover)==null?void 0:y.showProgress)<"u"?(D=L.popover)==null?void 0:D.showProgress:o("showProgress"),V=(((_=L.popover)==null?void 0:_.progressText)||o("progressText")||"{{current}} of {{total}}").replace("{{current}}",`${u+1}`).replace("{{total}}",`${M.length}`),z=((A=L.popover)==null?void 0:A.showButtons)||o("showButtons"),G=["next","previous",...W?["close"]:[]].filter(ke=>!(z!=null&&z.length)||z.includes(ke)),q=((w=L.popover)==null?void 0:w.onNextClick)||o("onNextClick"),Y=((P=L.popover)==null?void 0:P.onPrevClick)||o("onPrevClick"),xe=(($=L.popover)==null?void 0:$.onCloseClick)||o("onCloseClick");X({...L,popover:{showButtons:G,nextBtnText:I?void 0:O,disableButtons:[...N?[]:["previous"]],showProgress:j,progressText:V,onNextClick:q||(()=>{I?m(u+1):S()}),onPrevClick:Y||(()=>{m(u-1)}),onCloseClick:xe||(()=>{S()}),...L?.popover||{}}})}function S(u=!0){const f=p("__activeElement"),y=p("__activeStep"),D=p("__activeOnDestroyed"),_=o("onDestroyStarted");if(u&&_){const P=!f||f?.id==="driver-dummy-element";_(P?void 0:f,y,{config:o(),state:p()});return}const A=y?.onDeselected||o("onDeselected"),w=o("onDestroyed");if(document.body.classList.remove("driver-active","driver-fade","driver-simple"),ye(),Se(),ge(),ce(),se(),B(),f&&y){const P=f.id==="driver-dummy-element";A&&A(P?void 0:f,y,{config:o(),state:p()}),w&&w(P?void 0:f,y,{config:o(),state:p()})}D&&D.focus()}return{isActive:()=>p("isInitialized")||!1,refresh:F,drive:(u=0)=>{E(),m(u)},setConfig:k,setSteps:u=>{B(),k({...o(),steps:u})},getConfig:o,getState:p,getActiveIndex:()=>p("activeIndex"),isFirstStep:()=>p("activeIndex")===0,isLastStep:()=>{const u=o("steps")||[],f=p("activeIndex");return f!==void 0&&f===u.length-1},getActiveStep:()=>p("activeStep"),getActiveElement:()=>p("activeElement"),getPreviousElement:()=>p("previousElement"),getPreviousStep:()=>p("previousStep"),moveNext:n,movePrevious:l,moveTo:i,hasNextStep:()=>{const u=o("steps")||[],f=p("activeIndex");return f!==void 0&&u[f+1]},hasPreviousStep:()=>{const u=o("steps")||[],f=p("activeIndex");return f!==void 0&&u[f-1]},highlight:u=>{E(),X({...u,popover:u.popover?{showButtons:[],showProgress:!1,progressText:"",...u.popover}:void 0})},destroy:()=>{S(!1)}}}return v.driver=Ce,Object.defineProperty(v,Symbol.toStringTag,{value:"Module"}),v})({}),(function(v){"use strict";class b{constructor(){this.errors=[],this.maxErrors=50}logError(o,s,t={}){const a={timestamp:new Date().toISOString(),context:o,message:s instanceof Error?s.message:s,stack:s instanceof Error?s.stack:null,metadata:t};this.errors.push(a),this.errors.length>this.maxErrors&&this.errors.shift(),console.error(`[Onboarding Error] ${o}:`,s,t)}recoverFromError(o,s){switch(this.classifyError(o)){case"ELEMENT_NOT_FOUND":return this.logError("Element Not Found",o,{currentStep:s}),{action:"SKIP_STEP",nextStep:s+1,message:"Target element not found, skipping to next step"};case"STORAGE_UNAVAILABLE":return this.logError("Storage Unavailable",o),{action:"USE_MEMORY_STORAGE",message:"Local storage unavailable, using in-memory storage"};case"DRIVER_NOT_LOADED":return this.logError("Driver.js Not Loaded",o),{action:"ABORT_TOUR",message:"Driver.js library not loaded, cannot start tour"};case"INVALID_TOOLTIP":return this.logError("Invalid Tooltip Configuration",o,{currentStep:s}),{action:"SKIP_STEP",nextStep:s+1,message:"Invalid tooltip configuration, skipping"};case"THEME_DETECTION_FAILED":return this.logError("Theme Detection Failed",o),{action:"USE_DEFAULT_THEME",message:"Using default dark theme"};default:return this.logError("Unknown Error",o,{currentStep:s}),{action:"ABORT_TOUR",message:"Unexpected error occurred, aborting tour"}}}classifyError(o){const s=o.message||o.toString();return s.includes("element")&&s.includes("not found")?"ELEMENT_NOT_FOUND":s.includes("storage")||s.includes("quota")?"STORAGE_UNAVAILABLE":s.includes("driver")||s.includes("undefined")?"DRIVER_NOT_LOADED":s.includes("invalid")||s.includes("validation")?"INVALID_TOOLTIP":s.includes("theme")?"THEME_DETECTION_FAILED":"UNKNOWN"}getErrors(){return[...this.errors]}clearErrors(){this.errors=[]}getStatistics(){const o={total:this.errors.length,byContext:{},byType:{},recent:this.errors.slice(-10)};return this.errors.forEach(s=>{o.byContext[s.context]=(o.byContext[s.context]||0)+1;const t=this.classifyError({message:s.message});o.byType[t]=(o.byType[t]||0)+1}),o}handleDriverLoadFailure(){this.logError("Driver.js Load Failure","Driver.js library failed to load");const o=document.createElement("div");return o.id="onboarding-fallback",o.style.cssText=` - position: fixed; - bottom: 20px; - right: 20px; - background: var(--card-base, #2a2a2a); - color: var(--fg, #ffffff); - padding: 15px 20px; - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0,0,0,0.3); - z-index: 9999; - max-width: 300px; - font-size: 14px; - `,o.innerHTML=` - Welcome to DashCaddy!
-

- The interactive tour is unavailable, but you can explore the dashboard freely. - Check the documentation for help getting started. -

- `,document.body.appendChild(o),setTimeout(()=>{o.parentNode&&o.parentNode.removeChild(o)},1e4),!0}handleStorageUnavailable(){this.logError("Storage Unavailable","Local storage is not available");const o={data:{},getItem(s){return this.data[s]||null},setItem(s,t){this.data[s]=t},removeItem(s){delete this.data[s]},clear(){this.data={}}};return console.warn("[ErrorHandler] Using in-memory storage - progress will not persist"),o}sendToErrorTracking(o){}}v.ErrorHandler=b,console.log("[ErrorHandler] Module loaded")})(window),(function(v){"use strict";const b=new ErrorHandler,k=(...s)=>{v.DASHCADDY_DEBUG&&console.log(...s)};class o{constructor(t="dashcaddy_onboarding"){this.storageKey=t,this.storageVersion="1.0",this.installOnboardingCompleted=typeof SITE<"u"&&SITE.onboardingCompleted===!0,this._initializeStorage(),this._updateLastVisit()}_initializeStorage(){const t=this._getStorage();if(!t||t.version!==this.storageVersion){const a={version:this.storageVersion,tourCompleted:!1,completedTooltips:[],currentStep:0,completionTimestamp:null,dnsSetupDeferred:!1,lastVisit:new Date().toISOString()};this._setStorage(a)}}_getStorage(){try{const t=localStorage.getItem(this.storageKey);return t?JSON.parse(t):null}catch(t){return b.logError("[ProgressTracker] Read Storage",t,{function:"_getStorage"}),null}}_setStorage(t){try{localStorage.setItem(this.storageKey,JSON.stringify(t))}catch(a){b.logError("[ProgressTracker] Write Storage",a,{function:"_setStorage"}),this._handleStorageError(a)}}_handleStorageError(t){try{sessionStorage.setItem(this.storageKey,JSON.stringify(this._getStorage())),console.warn("[ProgressTracker] Falling back to session storage")}catch(a){b.logError("[ProgressTracker] Session Storage Unavailable",a,{function:"_handleStorageError"})}}_updateLastVisit(){const t=this._getStorage();t&&(t.lastVisit=new Date().toISOString(),this._setStorage(t))}isTooltipCompleted(t){const a=this._getStorage();return a?a.completedTooltips.includes(t):!1}markTooltipCompleted(t){const a=this._getStorage();a&&(a.completedTooltips.includes(t)||(a.completedTooltips.push(t),a.tooltipTimestamps||(a.tooltipTimestamps={}),a.tooltipTimestamps[t]=new Date().toISOString(),this._setStorage(a)))}isTourCompleted(){const t=this._getStorage();return t?t.tourCompleted===!0:!1}isInstallOnboardingCompleted(){return this.installOnboardingCompleted===!0}async markInstallOnboardingCompleted(){if(!this.installOnboardingCompleted){this.installOnboardingCompleted=!0,typeof SITE<"u"&&(SITE.onboardingCompleted=!0);try{await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({onboardingCompleted:!0})})}catch(t){b.logError("[ProgressTracker] Persist Install Onboarding",t,{function:"markInstallOnboardingCompleted"})}}}markTourCompleted(){const t=this._getStorage();t&&(t.tourCompleted=!0,t.completionTimestamp=new Date().toISOString(),this._setStorage(t))}getCurrentStep(){const t=this._getStorage();return t&&t.currentStep||0}setCurrentStep(t){const a=this._getStorage();a&&(a.currentStep=t,this._setStorage(a))}resetProgress(){const t={version:this.storageVersion,tourCompleted:!1,completedTooltips:[],currentStep:0,completionTimestamp:null,dnsSetupDeferred:!1,lastVisit:new Date().toISOString()};this._setStorage(t)}getCompletionTimestamp(){const t=this._getStorage();return!t||!t.completionTimestamp?null:new Date(t.completionTimestamp)}isDnsSetupDeferred(){const t=this._getStorage();return t?t.dnsSetupDeferred===!0:!1}markDnsSetupDeferred(){const t=this._getStorage();t&&(t.dnsSetupDeferred=!0,this._setStorage(t))}getTooltipTimestamp(t){const a=this._getStorage();return!a||!a.tooltipTimestamps||!a.tooltipTimestamps[t]?null:new Date(a.tooltipTimestamps[t])}getCompletedTooltips(){const t=this._getStorage();return t?t.completedTooltips||[]:[]}getLastVisit(){const t=this._getStorage();return!t||!t.lastVisit?null:new Date(t.lastVisit)}}v.ProgressTracker=o,k("[ProgressTracker] Module loaded")})(window),(function(v){"use strict";const b=new ErrorHandler,k=(...t)=>{v.DASHCADDY_DEBUG&&console.log(...t)},o={dark:{backgroundColor:"var(--card-base)",textColor:"var(--fg)",primaryColor:"var(--accent)",overlayColor:"rgba(0, 0, 0, 0.7)",borderColor:"var(--border)",highlightColor:"var(--accent)",fontFamily:"'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"},light:{backgroundColor:"var(--card-base)",textColor:"var(--fg)",primaryColor:"var(--accent-strong)",overlayColor:"rgba(0, 0, 0, 0.5)",borderColor:"var(--border)",highlightColor:"var(--accent-strong)",fontFamily:"'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"},blue:{backgroundColor:"var(--card-base)",textColor:"var(--fg)",primaryColor:"var(--accent)",overlayColor:"rgba(25, 8, 172, 0.7)",borderColor:"var(--border)",highlightColor:"var(--accent)",fontFamily:"'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"},nord:{backgroundColor:"var(--card-base)",textColor:"var(--fg)",primaryColor:"var(--accent)",overlayColor:"rgba(46, 52, 64, 0.7)",borderColor:"var(--border)",highlightColor:"var(--accent)",fontFamily:"'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"},dracula:{backgroundColor:"var(--card-base)",textColor:"var(--fg)",primaryColor:"var(--accent)",overlayColor:"rgba(40, 42, 54, 0.7)",borderColor:"var(--border)",highlightColor:"var(--accent)",fontFamily:"'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"},"solarized-dark":{backgroundColor:"var(--card-base)",textColor:"var(--fg)",primaryColor:"var(--accent)",overlayColor:"rgba(0, 43, 54, 0.7)",borderColor:"var(--border)",highlightColor:"var(--accent)",fontFamily:"'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"},"solarized-light":{backgroundColor:"var(--card-base)",textColor:"var(--fg)",primaryColor:"var(--accent)",overlayColor:"rgba(253, 246, 227, 0.7)",borderColor:"var(--border)",highlightColor:"var(--accent)",fontFamily:"'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"}};class s{constructor(){this.currentTheme=this.getCurrentTheme(),this.themeChangeCallbacks=[],this._setupThemeChangeListener()}getCurrentTheme(){const a=document.documentElement,d=Array.from(a.classList);return(v.THEMES||[]).filter(c=>c!=="dark").find(c=>d.includes(c))||"dark"}getDriverTheme(){const a=this.getCurrentTheme(),d=o[a]||o.dark,h={};for(const[T,c]of Object.entries(d))if(typeof c=="string"&&c.startsWith("var(")){const x=c.match(/var\((--[^)]+)\)/)?.[1];if(x){const p=getComputedStyle(document.documentElement).getPropertyValue(x).trim();h[T]=p||c}else h[T]=c}else h[T]=c;return h}onThemeChange(a){typeof a=="function"&&this.themeChangeCallbacks.push(a)}_setupThemeChangeListener(){const a=document.documentElement;new MutationObserver(h=>{h.forEach(T=>{if(T.type==="attributes"&&T.attributeName==="class"){const c=this.getCurrentTheme();if(c!==this.currentTheme){const x=this.currentTheme;this.currentTheme=c,this._notifyThemeChange(c,x)}}})}).observe(a,{attributes:!0,attributeFilter:["class"]}),k("[ThemeAdapter] Theme change listener initialized")}_notifyThemeChange(a,d){k(`[ThemeAdapter] Theme changed: ${d} \u2192 ${a}`),this.themeChangeCallbacks.forEach(h=>{try{h(a,d)}catch(T){b.logError("[ThemeAdapter] Theme Change Callback",T,{function:"_notifyThemeChange"})}})}applyTheme(a){if(!a){console.warn("[ThemeAdapter] No driver instance provided");return}const d=this.getDriverTheme();this._injectDriverStyles(d),k("[ThemeAdapter] Theme applied to driver:",this.currentTheme)}_injectDriverStyles(a){const d=document.getElementById("driver-theme-styles");d&&d.remove();const h=document.createElement("style");h.id="driver-theme-styles",h.textContent=` +this.driver=this.driver||{},this.driver.js=(function(y){"use strict";let P={};function _(e={}){P={animate:!0,allowClose:!0,overlayOpacity:.7,smoothScroll:!1,disableActiveInteraction:!1,showProgress:!1,stagePadding:10,stageRadius:5,popoverOffset:10,showButtons:["next","previous","close"],disableButtons:[],overlayColor:"#000",...e}}function a(e){return e?P[e]:P}function c(e,t,i,s){return(e/=s/2)<1?i/2*e*e+t:-i/2*(--e*(e-2)-1)+t}function r(e){const t='a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled])';return e.flatMap(i=>{const s=i.matches(t),o=Array.from(i.querySelectorAll(t));return[...s?[i]:[],...o]}).filter(i=>getComputedStyle(i).pointerEvents!=="none"&&k(i))}function n(e){if(!e||A(e))return;const t=a("smoothScroll");e.scrollIntoView({behavior:!t||v(e)?"auto":"smooth",inline:"center",block:"center"})}function v(e){if(!e||!e.parentElement)return;const t=e.parentElement;return t.scrollHeight>t.clientHeight}function A(e){const t=e.getBoundingClientRect();return t.top>=0&&t.left>=0&&t.bottom<=(window.innerHeight||document.documentElement.clientHeight)&&t.right<=(window.innerWidth||document.documentElement.clientWidth)}function k(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)}let m={};function C(e,t){m[e]=t}function l(e){return e?m[e]:m}function u(){m={}}let b={};function $(e,t){b[e]=t}function L(e){var t;(t=b[e])==null||t.call(b)}function R(){b={}}function j(e,t,i,s){let o=l("__activeStagePosition");const h=o||i.getBoundingClientRect(),S=s.getBoundingClientRect(),x=c(e,h.x,S.x-h.x,t),p=c(e,h.y,S.y-h.y,t),T=c(e,h.width,S.width-h.width,t),d=c(e,h.height,S.height-h.height,t);o={x,y:p,width:T,height:d},X(o),C("__activeStagePosition",o)}function Q(e){if(!e)return;const t=e.getBoundingClientRect(),i={x:t.x,y:t.y,width:t.width,height:t.height};C("__activeStagePosition",i),X(i)}function le(){const e=l("__activeStagePosition"),t=l("__overlaySvg");if(!e)return;if(!t){console.warn("No stage svg found.");return}const i=window.innerWidth,s=window.innerHeight;t.setAttribute("viewBox",`0 0 ${i} ${s}`)}function de(e){const t=pe(e);document.body.appendChild(t),re(t,i=>{i.target.tagName==="path"&&L("overlayClick")}),C("__overlaySvg",t)}function X(e){const t=l("__overlaySvg");if(!t){de(e);return}const i=t.firstElementChild;if(i?.tagName!=="path")throw new Error("no path element found in stage svg");i.setAttribute("d",Z(e))}function pe(e){const t=window.innerWidth,i=window.innerHeight,s=document.createElementNS("http://www.w3.org/2000/svg","svg");s.classList.add("driver-overlay","driver-overlay-animated"),s.setAttribute("viewBox",`0 0 ${t} ${i}`),s.setAttribute("xmlSpace","preserve"),s.setAttribute("xmlnsXlink","http://www.w3.org/1999/xlink"),s.setAttribute("version","1.1"),s.setAttribute("preserveAspectRatio","xMinYMin slice"),s.style.fillRule="evenodd",s.style.clipRule="evenodd",s.style.strokeLinejoin="round",s.style.strokeMiterlimit="2",s.style.zIndex="10000",s.style.position="fixed",s.style.top="0",s.style.left="0",s.style.width="100%",s.style.height="100%";const o=document.createElementNS("http://www.w3.org/2000/svg","path");return o.setAttribute("d",Z(e)),o.style.fill=a("overlayColor")||"rgb(0,0,0)",o.style.opacity=`${a("overlayOpacity")}`,o.style.pointerEvents="auto",o.style.cursor="auto",s.appendChild(o),s}function Z(e){const t=window.innerWidth,i=window.innerHeight,s=a("stagePadding")||0,o=a("stageRadius")||0,h=e.width+s*2,S=e.height+s*2,x=Math.min(o,h/2,S/2),p=Math.floor(Math.max(x,0)),T=e.x-s+p,d=e.y-s,g=h-p*2,f=S-p*2;return`M${t},0L0,0L0,${i}L${t},${i}L${t},0Z + M${T},${d} h${g} a${p},${p} 0 0 1 ${p},${p} v${f} a${p},${p} 0 0 1 -${p},${p} h-${g} a${p},${p} 0 0 1 -${p},-${p} v-${f} a${p},${p} 0 0 1 ${p},-${p} z`}function ce(){const e=l("__overlaySvg");e&&e.remove()}function ue(){const e=document.getElementById("driver-dummy-element");if(e)return e;let t=document.createElement("div");return t.id="driver-dummy-element",t.style.width="0",t.style.height="0",t.style.pointerEvents="none",t.style.opacity="0",t.style.position="fixed",t.style.top="50%",t.style.left="50%",document.body.appendChild(t),t}function ee(e){const{element:t}=e;let i=typeof t=="string"?document.querySelector(t):t;i||(i=ue()),he(i,e)}function me(){const e=l("__activeElement"),t=l("__activeStep");e&&(Q(e),le(),ae(e,t))}function he(e,t){const i=Date.now(),s=l("__activeStep"),o=l("__activeElement")||e,h=!o||o===e,S=e.id==="driver-dummy-element",x=o.id==="driver-dummy-element",p=a("animate"),T=t.onHighlightStarted||a("onHighlightStarted"),d=t?.onHighlighted||a("onHighlighted"),g=s?.onDeselected||a("onDeselected"),f=a(),D=l();!h&&g&&g(x?void 0:o,s,{config:f,state:D}),T&&T(S?void 0:e,t,{config:f,state:D});const B=!h&&p;let E=!1;we(),C("previousStep",s),C("previousElement",o),C("activeStep",t),C("activeElement",e);const w=()=>{if(l("__transitionCallback")!==w)return;const I=Date.now()-i,O=400-I<=400/2;t.popover&&O&&!E&&B&&(oe(e,t),E=!0),a("animate")&&I<400?j(I,400,o,e):(Q(e),d&&d(S?void 0:e,t,{config:a(),state:l()}),C("__transitionCallback",void 0),C("__previousStep",s),C("__previousElement",o),C("__activeStep",t),C("__activeElement",e)),window.requestAnimationFrame(w)};C("__transitionCallback",w),window.requestAnimationFrame(w),n(e),!B&&t.popover&&oe(e,t),o.classList.remove("driver-active-element","driver-no-interaction"),o.removeAttribute("aria-haspopup"),o.removeAttribute("aria-expanded"),o.removeAttribute("aria-controls"),a("disableActiveInteraction")&&e.classList.add("driver-no-interaction"),e.classList.add("driver-active-element"),e.setAttribute("aria-haspopup","dialog"),e.setAttribute("aria-expanded","true"),e.setAttribute("aria-controls","driver-popover-content")}function ge(){var e;(e=document.getElementById("driver-dummy-element"))==null||e.remove(),document.querySelectorAll(".driver-active-element").forEach(t=>{t.classList.remove("driver-active-element","driver-no-interaction"),t.removeAttribute("aria-haspopup"),t.removeAttribute("aria-expanded"),t.removeAttribute("aria-controls")})}function U(){const e=l("__resizeTimeout");e&&window.cancelAnimationFrame(e),C("__resizeTimeout",window.requestAnimationFrame(me))}function ve(e){var t;if(!l("isInitialized")||!(e.key==="Tab"||e.keyCode===9))return;const i=l("__activeElement"),s=(t=l("popover"))==null?void 0:t.wrapper,o=r([...s?[s]:[],...i?[i]:[]]),h=o[0],S=o[o.length-1];if(e.preventDefault(),e.shiftKey){const x=o[o.indexOf(document.activeElement)-1]||S;x?.focus()}else{const x=o[o.indexOf(document.activeElement)+1]||h;x?.focus()}}function te(e){var t;((t=a("allowKeyboardControl"))==null||t)&&(e.key==="Escape"?L("escapePress"):e.key==="ArrowRight"?L("arrowRightPress"):e.key==="ArrowLeft"&&L("arrowLeftPress"))}function re(e,t,i){const s=(o,h)=>{const S=o.target;e.contains(S)&&((!i||i(S))&&(o.preventDefault(),o.stopPropagation(),o.stopImmediatePropagation()),h?.(o))};document.addEventListener("pointerdown",s,!0),document.addEventListener("mousedown",s,!0),document.addEventListener("pointerup",s,!0),document.addEventListener("mouseup",s,!0),document.addEventListener("click",o=>{s(o,t)},!0)}function fe(){window.addEventListener("keyup",te,!1),window.addEventListener("keydown",ve,!1),window.addEventListener("resize",U),window.addEventListener("scroll",U)}function ye(){window.removeEventListener("keyup",te),window.removeEventListener("resize",U),window.removeEventListener("scroll",U)}function we(){const e=l("popover");e&&(e.wrapper.style.display="none")}function oe(e,t){var i,s;let o=l("popover");o&&document.body.removeChild(o.wrapper),o=Te(),document.body.appendChild(o.wrapper);const{title:h,description:S,showButtons:x,disableButtons:p,showProgress:T,nextBtnText:d=a("nextBtnText")||"Next →",prevBtnText:g=a("prevBtnText")||"← Previous",progressText:f=a("progressText")||"{current} of {total}"}=t.popover||{};o.nextButton.innerHTML=d,o.previousButton.innerHTML=g,o.progress.innerHTML=f,h?(o.title.innerHTML=h,o.title.style.display="block"):o.title.style.display="none",S?(o.description.innerHTML=S,o.description.style.display="block"):o.description.style.display="none";const D=x||a("showButtons"),B=T||a("showProgress")||!1,E=D?.includes("next")||D?.includes("previous")||B;o.closeButton.style.display=D.includes("close")?"block":"none",E?(o.footer.style.display="flex",o.progress.style.display=B?"block":"none",o.nextButton.style.display=D.includes("next")?"block":"none",o.previousButton.style.display=D.includes("previous")?"block":"none"):o.footer.style.display="none";const w=p||a("disableButtons")||[];w!=null&&w.includes("next")&&(o.nextButton.disabled=!0,o.nextButton.classList.add("driver-popover-btn-disabled")),w!=null&&w.includes("previous")&&(o.previousButton.disabled=!0,o.previousButton.classList.add("driver-popover-btn-disabled")),w!=null&&w.includes("close")&&(o.closeButton.disabled=!0,o.closeButton.classList.add("driver-popover-btn-disabled"));const I=o.wrapper;I.style.display="block",I.style.left="",I.style.top="",I.style.bottom="",I.style.right="",I.id="driver-popover-content",I.setAttribute("role","dialog"),I.setAttribute("aria-labelledby","driver-popover-title"),I.setAttribute("aria-describedby","driver-popover-description");const O=o.arrow;O.className="driver-popover-arrow";const z=((i=t.popover)==null?void 0:i.popoverClass)||a("popoverClass")||"";I.className=`driver-popover ${z}`.trim(),re(o.wrapper,F=>{var V,G,q;const W=F.target,Y=((V=t.popover)==null?void 0:V.onNextClick)||a("onNextClick"),K=((G=t.popover)==null?void 0:G.onPrevClick)||a("onPrevClick"),J=((q=t.popover)==null?void 0:q.onCloseClick)||a("onCloseClick");if(W.classList.contains("driver-popover-next-btn"))return Y?Y(e,t,{config:a(),state:l()}):L("nextClick");if(W.classList.contains("driver-popover-prev-btn"))return K?K(e,t,{config:a(),state:l()}):L("prevClick");if(W.classList.contains("driver-popover-close-btn"))return J?J(e,t,{config:a(),state:l()}):L("closeClick")},F=>!(o!=null&&o.description.contains(F))&&!(o!=null&&o.title.contains(F))&&typeof F.className=="string"&&F.className.includes("driver-popover")),C("popover",o);const N=((s=t.popover)==null?void 0:s.onPopoverRender)||a("onPopoverRender");N&&N(o,{config:a(),state:l()}),ae(e,t),n(I);const M=e.classList.contains("driver-dummy-element"),H=r([I,...M?[]:[e]]);H.length>0&&H[0].focus()}function ie(){const e=l("popover");if(!(e!=null&&e.wrapper))return;const t=e.wrapper.getBoundingClientRect(),i=a("stagePadding")||0,s=a("popoverOffset")||0;return{width:t.width+i+s,height:t.height+i+s,realWidth:t.width,realHeight:t.height}}function ne(e,t){const{elementDimensions:i,popoverDimensions:s,popoverPadding:o,popoverArrowDimensions:h}=t;return e==="start"?Math.max(Math.min(i.top-o,window.innerHeight-s.realHeight-h.width),h.width):e==="end"?Math.max(Math.min(i.top-s?.realHeight+i.height+o,window.innerHeight-s?.realHeight-h.width),h.width):e==="center"?Math.max(Math.min(i.top+i.height/2-s?.realHeight/2,window.innerHeight-s?.realHeight-h.width),h.width):0}function se(e,t){const{elementDimensions:i,popoverDimensions:s,popoverPadding:o,popoverArrowDimensions:h}=t;return e==="start"?Math.max(Math.min(i.left-o,window.innerWidth-s.realWidth-h.width),h.width):e==="end"?Math.max(Math.min(i.left-s?.realWidth+i.width+o,window.innerWidth-s?.realWidth-h.width),h.width):e==="center"?Math.max(Math.min(i.left+i.width/2-s?.realWidth/2,window.innerWidth-s?.realWidth-h.width),h.width):0}function ae(e,t){const i=l("popover");if(!i)return;const{align:s="start",side:o="left"}=t?.popover||{},h=s,S=e.id==="driver-dummy-element"?"over":o,x=a("stagePadding")||0,p=ie(),T=i.arrow.getBoundingClientRect(),d=e.getBoundingClientRect(),g=d.top-p.height;let f=g>=0;const D=window.innerHeight-(d.bottom+p.height);let B=D>=0;const E=d.left-p.width;let w=E>=0;const I=window.innerWidth-(d.right+p.width);let O=I>=0;const z=!f&&!B&&!w&&!O;let N=S;if(S==="top"&&f?O=w=B=!1:S==="bottom"&&B?O=w=f=!1:S==="left"&&w?O=f=B=!1:S==="right"&&O&&(w=f=B=!1),S==="over"){const M=window.innerWidth/2-p.realWidth/2,H=window.innerHeight/2-p.realHeight/2;i.wrapper.style.left=`${M}px`,i.wrapper.style.right="auto",i.wrapper.style.top=`${H}px`,i.wrapper.style.bottom="auto"}else if(z){const M=window.innerWidth/2-p?.realWidth/2,H=10;i.wrapper.style.left=`${M}px`,i.wrapper.style.right="auto",i.wrapper.style.bottom=`${H}px`,i.wrapper.style.top="auto"}else if(w){const M=Math.min(E,window.innerWidth-p?.realWidth-T.width),H=ne(h,{elementDimensions:d,popoverDimensions:p,popoverPadding:x,popoverArrowDimensions:T});i.wrapper.style.left=`${M}px`,i.wrapper.style.top=`${H}px`,i.wrapper.style.bottom="auto",i.wrapper.style.right="auto",N="left"}else if(O){const M=Math.min(I,window.innerWidth-p?.realWidth-T.width),H=ne(h,{elementDimensions:d,popoverDimensions:p,popoverPadding:x,popoverArrowDimensions:T});i.wrapper.style.right=`${M}px`,i.wrapper.style.top=`${H}px`,i.wrapper.style.bottom="auto",i.wrapper.style.left="auto",N="right"}else if(f){const M=Math.min(g,window.innerHeight-p.realHeight-T.width);let H=se(h,{elementDimensions:d,popoverDimensions:p,popoverPadding:x,popoverArrowDimensions:T});i.wrapper.style.top=`${M}px`,i.wrapper.style.left=`${H}px`,i.wrapper.style.bottom="auto",i.wrapper.style.right="auto",N="top"}else if(B){const M=Math.min(D,window.innerHeight-p?.realHeight-T.width);let H=se(h,{elementDimensions:d,popoverDimensions:p,popoverPadding:x,popoverArrowDimensions:T});i.wrapper.style.left=`${H}px`,i.wrapper.style.bottom=`${M}px`,i.wrapper.style.top="auto",i.wrapper.style.right="auto",N="bottom"}z?i.arrow.classList.add("driver-popover-arrow-none"):be(h,N,e)}function be(e,t,i){const s=l("popover");if(!s)return;const o=i.getBoundingClientRect(),h=ie(),S=s.arrow,x=h.width,p=window.innerWidth,T=o.width,d=o.left,g=h.height,f=window.innerHeight,D=o.top,B=o.height;S.className="driver-popover-arrow";let E=t,w=e;t==="top"?(d+T<=0?(E="right",w="end"):d+T-x<=0&&(E="top",w="start"),d>=p?(E="left",w="end"):d+x>=p&&(E="top",w="end")):t==="bottom"?(d+T<=0?(E="right",w="start"):d+T-x<=0&&(E="bottom",w="start"),d>=p?(E="left",w="start"):d+x>=p&&(E="bottom",w="end")):t==="left"?(D+B<=0?(E="bottom",w="end"):D+B-g<=0&&(E="left",w="start"),D>=f?(E="top",w="end"):D+g>=f&&(E="left",w="end")):t==="right"&&(D+B<=0?(E="bottom",w="start"):D+B-g<=0&&(E="right",w="start"),D>=f?(E="top",w="start"):D+g>=f&&(E="right",w="end")),E?(S.classList.add(`driver-popover-arrow-side-${E}`),S.classList.add(`driver-popover-arrow-align-${w}`)):S.classList.add("driver-popover-arrow-none")}function Te(){const e=document.createElement("div");e.classList.add("driver-popover");const t=document.createElement("div");t.classList.add("driver-popover-arrow");const i=document.createElement("header");i.id="driver-popover-title",i.classList.add("driver-popover-title"),i.style.display="none",i.innerText="Popover Title";const s=document.createElement("div");s.id="driver-popover-description",s.classList.add("driver-popover-description"),s.style.display="none",s.innerText="Popover description is here";const o=document.createElement("button");o.type="button",o.classList.add("driver-popover-close-btn"),o.setAttribute("aria-label","Close"),o.innerHTML="×";const h=document.createElement("footer");h.classList.add("driver-popover-footer");const S=document.createElement("span");S.classList.add("driver-popover-progress-text"),S.innerText="";const x=document.createElement("span");x.classList.add("driver-popover-navigation-btns");const p=document.createElement("button");p.type="button",p.classList.add("driver-popover-prev-btn"),p.innerHTML="← Previous";const T=document.createElement("button");return T.type="button",T.classList.add("driver-popover-next-btn"),T.innerHTML="Next →",x.appendChild(p),x.appendChild(T),h.appendChild(S),h.appendChild(x),e.appendChild(o),e.appendChild(t),e.appendChild(i),e.appendChild(s),e.appendChild(h),{wrapper:e,arrow:t,title:i,description:s,footer:h,previousButton:p,nextButton:T,closeButton:o,footerButtons:x,progress:S}}function Se(){var e;const t=l("popover");t&&((e=t.wrapper.parentElement)==null||e.removeChild(t.wrapper))}const De="";function Ce(e={}){_(e);function t(){a("allowClose")&&T()}function i(){const d=l("activeIndex"),g=a("steps")||[];if(typeof d>"u")return;const f=d+1;g[f]?p(f):T()}function s(){const d=l("activeIndex"),g=a("steps")||[];if(typeof d>"u")return;const f=d-1;g[f]?p(f):T()}function o(d){(a("steps")||[])[d]?p(d):T()}function h(){var d;if(l("__transitionCallback"))return;const g=l("activeIndex"),f=l("__activeStep"),D=l("__activeElement");if(typeof g>"u"||typeof f>"u"||typeof l("activeIndex")>"u")return;const B=((d=f.popover)==null?void 0:d.onPrevClick)||a("onPrevClick");if(B)return B(D,f,{config:a(),state:l()});s()}function S(){var d;if(l("__transitionCallback"))return;const g=l("activeIndex"),f=l("__activeStep"),D=l("__activeElement");if(typeof g>"u"||typeof f>"u")return;const B=((d=f.popover)==null?void 0:d.onNextClick)||a("onNextClick");if(B)return B(D,f,{config:a(),state:l()});i()}function x(){l("isInitialized")||(C("isInitialized",!0),document.body.classList.add("driver-active",a("animate")?"driver-fade":"driver-simple"),fe(),$("overlayClick",t),$("escapePress",t),$("arrowLeftPress",h),$("arrowRightPress",S))}function p(d=0){var g,f,D,B,E,w,I,O;const z=a("steps");if(!z){console.error("No steps to drive through"),T();return}if(!z[d]){T();return}C("__activeOnDestroyed",document.activeElement),C("activeIndex",d);const N=z[d],M=z[d+1],H=z[d-1],F=((g=N.popover)==null?void 0:g.doneBtnText)||a("doneBtnText")||"Done",V=a("allowClose"),G=typeof((f=N.popover)==null?void 0:f.showProgress)<"u"?(D=N.popover)==null?void 0:D.showProgress:a("showProgress"),q=(((B=N.popover)==null?void 0:B.progressText)||a("progressText")||"{{current}} of {{total}}").replace("{{current}}",`${d+1}`).replace("{{total}}",`${z.length}`),W=((E=N.popover)==null?void 0:E.showButtons)||a("showButtons"),Y=["next","previous",...V?["close"]:[]].filter(xe=>!(W!=null&&W.length)||W.includes(xe)),K=((w=N.popover)==null?void 0:w.onNextClick)||a("onNextClick"),J=((I=N.popover)==null?void 0:I.onPrevClick)||a("onPrevClick"),ke=((O=N.popover)==null?void 0:O.onCloseClick)||a("onCloseClick");ee({...N,popover:{showButtons:Y,nextBtnText:M?void 0:F,disableButtons:[...H?[]:["previous"]],showProgress:G,progressText:q,onNextClick:K||(()=>{M?p(d+1):T()}),onPrevClick:J||(()=>{p(d-1)}),onCloseClick:ke||(()=>{T()}),...N?.popover||{}}})}function T(d=!0){const g=l("__activeElement"),f=l("__activeStep"),D=l("__activeOnDestroyed"),B=a("onDestroyStarted");if(d&&B){const I=!g||g?.id==="driver-dummy-element";B(I?void 0:g,f,{config:a(),state:l()});return}const E=f?.onDeselected||a("onDeselected"),w=a("onDestroyed");if(document.body.classList.remove("driver-active","driver-fade","driver-simple"),ye(),Se(),ge(),ce(),R(),u(),g&&f){const I=g.id==="driver-dummy-element";E&&E(I?void 0:g,f,{config:a(),state:l()}),w&&w(I?void 0:g,f,{config:a(),state:l()})}D&&D.focus()}return{isActive:()=>l("isInitialized")||!1,refresh:U,drive:(d=0)=>{x(),p(d)},setConfig:_,setSteps:d=>{u(),_({...a(),steps:d})},getConfig:a,getState:l,getActiveIndex:()=>l("activeIndex"),isFirstStep:()=>l("activeIndex")===0,isLastStep:()=>{const d=a("steps")||[],g=l("activeIndex");return g!==void 0&&g===d.length-1},getActiveStep:()=>l("activeStep"),getActiveElement:()=>l("activeElement"),getPreviousElement:()=>l("previousElement"),getPreviousStep:()=>l("previousStep"),moveNext:i,movePrevious:s,moveTo:o,hasNextStep:()=>{const d=a("steps")||[],g=l("activeIndex");return g!==void 0&&d[g+1]},hasPreviousStep:()=>{const d=a("steps")||[],g=l("activeIndex");return g!==void 0&&d[g-1]},highlight:d=>{x(),ee({...d,popover:d.popover?{showButtons:[],showProgress:!1,progressText:"",...d.popover}:void 0})},destroy:()=>{T(!1)}}}return y.driver=Ce,Object.defineProperty(y,Symbol.toStringTag,{value:"Module"}),y})({}),(function(y){"use strict";const P=new ErrorHandler,_=(...c)=>{y.DASHCADDY_DEBUG&&console.log(...c)};class a{constructor(r="dashcaddy_onboarding"){this.storageKey=r,this.storageVersion="1.0",this.installOnboardingCompleted=typeof SITE<"u"&&SITE.onboardingCompleted===!0,this._initializeStorage(),this._updateLastVisit()}_initializeStorage(){const r=this._getStorage();if(!r||r.version!==this.storageVersion){const n={version:this.storageVersion,tourCompleted:!1,completedTooltips:[],currentStep:0,completionTimestamp:null,dnsSetupDeferred:!1,lastVisit:new Date().toISOString()};this._setStorage(n)}}_getStorage(){try{const r=localStorage.getItem(this.storageKey);return r?JSON.parse(r):null}catch(r){return P.logError("[ProgressTracker] Read Storage",r,{function:"_getStorage"}),null}}_setStorage(r){try{localStorage.setItem(this.storageKey,JSON.stringify(r))}catch(n){P.logError("[ProgressTracker] Write Storage",n,{function:"_setStorage"}),this._handleStorageError(n)}}_handleStorageError(r){try{sessionStorage.setItem(this.storageKey,JSON.stringify(this._getStorage())),console.warn("[ProgressTracker] Falling back to session storage")}catch(n){P.logError("[ProgressTracker] Session Storage Unavailable",n,{function:"_handleStorageError"})}}_updateLastVisit(){const r=this._getStorage();r&&(r.lastVisit=new Date().toISOString(),this._setStorage(r))}isTooltipCompleted(r){const n=this._getStorage();return n?n.completedTooltips.includes(r):!1}markTooltipCompleted(r){const n=this._getStorage();n&&(n.completedTooltips.includes(r)||(n.completedTooltips.push(r),n.tooltipTimestamps||(n.tooltipTimestamps={}),n.tooltipTimestamps[r]=new Date().toISOString(),this._setStorage(n)))}isTourCompleted(){const r=this._getStorage();return r?r.tourCompleted===!0:!1}isInstallOnboardingCompleted(){return this.installOnboardingCompleted===!0}async markInstallOnboardingCompleted(){if(!this.installOnboardingCompleted){this.installOnboardingCompleted=!0,typeof SITE<"u"&&(SITE.onboardingCompleted=!0);try{await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({onboardingCompleted:!0})})}catch(r){P.logError("[ProgressTracker] Persist Install Onboarding",r,{function:"markInstallOnboardingCompleted"})}}}markTourCompleted(){const r=this._getStorage();r&&(r.tourCompleted=!0,r.completionTimestamp=new Date().toISOString(),this._setStorage(r))}getCurrentStep(){const r=this._getStorage();return r&&r.currentStep||0}setCurrentStep(r){const n=this._getStorage();n&&(n.currentStep=r,this._setStorage(n))}resetProgress(){const r={version:this.storageVersion,tourCompleted:!1,completedTooltips:[],currentStep:0,completionTimestamp:null,dnsSetupDeferred:!1,lastVisit:new Date().toISOString()};this._setStorage(r)}getCompletionTimestamp(){const r=this._getStorage();return!r||!r.completionTimestamp?null:new Date(r.completionTimestamp)}isDnsSetupDeferred(){const r=this._getStorage();return r?r.dnsSetupDeferred===!0:!1}markDnsSetupDeferred(){const r=this._getStorage();r&&(r.dnsSetupDeferred=!0,this._setStorage(r))}getTooltipTimestamp(r){const n=this._getStorage();return!n||!n.tooltipTimestamps||!n.tooltipTimestamps[r]?null:new Date(n.tooltipTimestamps[r])}getCompletedTooltips(){const r=this._getStorage();return r?r.completedTooltips||[]:[]}getLastVisit(){const r=this._getStorage();return!r||!r.lastVisit?null:new Date(r.lastVisit)}}y.ProgressTracker=a,_("[ProgressTracker] Module loaded")})(window),(function(y){"use strict";const P=new ErrorHandler,_=(...r)=>{y.DASHCADDY_DEBUG&&console.log(...r)},a={dark:{backgroundColor:"var(--card-base)",textColor:"var(--fg)",primaryColor:"var(--accent)",overlayColor:"rgba(0, 0, 0, 0.7)",borderColor:"var(--border)",highlightColor:"var(--accent)",fontFamily:"'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"},light:{backgroundColor:"var(--card-base)",textColor:"var(--fg)",primaryColor:"var(--accent-strong)",overlayColor:"rgba(0, 0, 0, 0.5)",borderColor:"var(--border)",highlightColor:"var(--accent-strong)",fontFamily:"'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"},blue:{backgroundColor:"var(--card-base)",textColor:"var(--fg)",primaryColor:"var(--accent)",overlayColor:"rgba(25, 8, 172, 0.7)",borderColor:"var(--border)",highlightColor:"var(--accent)",fontFamily:"'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"},nord:{backgroundColor:"var(--card-base)",textColor:"var(--fg)",primaryColor:"var(--accent)",overlayColor:"rgba(46, 52, 64, 0.7)",borderColor:"var(--border)",highlightColor:"var(--accent)",fontFamily:"'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"},dracula:{backgroundColor:"var(--card-base)",textColor:"var(--fg)",primaryColor:"var(--accent)",overlayColor:"rgba(40, 42, 54, 0.7)",borderColor:"var(--border)",highlightColor:"var(--accent)",fontFamily:"'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"},"solarized-dark":{backgroundColor:"var(--card-base)",textColor:"var(--fg)",primaryColor:"var(--accent)",overlayColor:"rgba(0, 43, 54, 0.7)",borderColor:"var(--border)",highlightColor:"var(--accent)",fontFamily:"'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"},"solarized-light":{backgroundColor:"var(--card-base)",textColor:"var(--fg)",primaryColor:"var(--accent)",overlayColor:"rgba(253, 246, 227, 0.7)",borderColor:"var(--border)",highlightColor:"var(--accent)",fontFamily:"'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"}};class c{constructor(){this.currentTheme=this.getCurrentTheme(),this.themeChangeCallbacks=[],this._setupThemeChangeListener()}getCurrentTheme(){const n=document.documentElement,v=Array.from(n.classList);return(y.THEMES||[]).filter(m=>m!=="dark").find(m=>v.includes(m))||"dark"}getDriverTheme(){const n=this.getCurrentTheme(),v=a[n]||a.dark,A={};for(const[k,m]of Object.entries(v))if(typeof m=="string"&&m.startsWith("var(")){const C=m.match(/var\((--[^)]+)\)/)?.[1];if(C){const l=getComputedStyle(document.documentElement).getPropertyValue(C).trim();A[k]=l||m}else A[k]=m}else A[k]=m;return A}onThemeChange(n){typeof n=="function"&&this.themeChangeCallbacks.push(n)}_setupThemeChangeListener(){const n=document.documentElement;new MutationObserver(A=>{A.forEach(k=>{if(k.type==="attributes"&&k.attributeName==="class"){const m=this.getCurrentTheme();if(m!==this.currentTheme){const C=this.currentTheme;this.currentTheme=m,this._notifyThemeChange(m,C)}}})}).observe(n,{attributes:!0,attributeFilter:["class"]}),_("[ThemeAdapter] Theme change listener initialized")}_notifyThemeChange(n,v){_(`[ThemeAdapter] Theme changed: ${v} \u2192 ${n}`),this.themeChangeCallbacks.forEach(A=>{try{A(n,v)}catch(k){P.logError("[ThemeAdapter] Theme Change Callback",k,{function:"_notifyThemeChange"})}})}applyTheme(n){if(!n){console.warn("[ThemeAdapter] No driver instance provided");return}const v=this.getDriverTheme();this._injectDriverStyles(v),_("[ThemeAdapter] Theme applied to driver:",this.currentTheme)}_injectDriverStyles(n){const v=document.getElementById("driver-theme-styles");v&&v.remove();const A=document.createElement("style");A.id="driver-theme-styles",A.textContent=` .driver-popover { - background: ${a.backgroundColor} !important; - color: ${a.textColor} !important; - border: 1px solid ${a.borderColor} !important; + background: ${n.backgroundColor} !important; + color: ${n.textColor} !important; + border: 1px solid ${n.borderColor} !important; border-radius: 12px !important; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4) !important; - font-family: ${a.fontFamily} !important; + font-family: ${n.fontFamily} !important; } .driver-popover-title { - color: ${a.textColor} !important; + color: ${n.textColor} !important; font-weight: 600 !important; - font-family: ${a.fontFamily} !important; + font-family: ${n.fontFamily} !important; } .driver-popover-description { - color: ${a.textColor} !important; - font-family: ${a.fontFamily} !important; + color: ${n.textColor} !important; + font-family: ${n.fontFamily} !important; } .driver-popover-footer button { - background: ${a.primaryColor} !important; - color: ${a.backgroundColor} !important; + background: ${n.primaryColor} !important; + color: ${n.backgroundColor} !important; border: none !important; - font-family: ${a.fontFamily} !important; + font-family: ${n.fontFamily} !important; font-weight: 500 !important; } @@ -51,26 +33,26 @@ this.driver=this.driver||{},this.driver.js=(function(v){"use strict";let b={};fu } .driver-popover-close-btn { - color: ${a.textColor} !important; + color: ${n.textColor} !important; } .driver-overlay { - background: ${a.overlayColor} !important; + background: ${n.overlayColor} !important; } .driver-highlighted-element { - outline: 2px solid ${a.highlightColor} !important; + outline: 2px solid ${n.highlightColor} !important; outline-offset: 4px !important; } .driver-popover-progress-text { - color: ${a.textColor} !important; + color: ${n.textColor} !important; opacity: 0.7 !important; - font-family: ${a.fontFamily} !important; + font-family: ${n.fontFamily} !important; } - `,document.head.appendChild(h)}getAvailableThemes(){return Object.keys(o)}isThemeAvailable(a){return o.hasOwnProperty(a)}}v.ThemeAdapter=s,k("[ThemeAdapter] Module loaded")})(window),(function(v){"use strict";const b=new ErrorHandler,k=(...d)=>{v.DASHCADDY_DEBUG&&console.log(...d)};function o(d){const h=[];if((!d.id||typeof d.id!="string")&&h.push("Tooltip must have a valid string id"),d.element||h.push("Tooltip must have an element selector or HTMLElement"),!d.popover||typeof d.popover!="object")h.push("Tooltip must have a popover object");else{if((!d.popover.title||typeof d.popover.title!="string")&&h.push("Tooltip popover must have a valid string title"),(!d.popover.description||typeof d.popover.description!="string")&&h.push("Tooltip popover must have a valid string description"),d.popover.position){const c=["top","bottom","left","right","center"];c.includes(d.popover.position)||h.push(`Invalid position: ${d.popover.position}. Must be one of: ${c.join(", ")}`)}if(d.popover.align){const c=["start","center","end"];c.includes(d.popover.align)||h.push(`Invalid align: ${d.popover.align}. Must be one of: ${c.join(", ")}`)}d.popover.showButtons&&!Array.isArray(d.popover.showButtons)&&h.push("showButtons must be an array"),["onNext","onPrevious","onClose","onSetupNow","onLater"].forEach(c=>{d.popover[c]&&typeof d.popover[c]!="function"&&h.push(`${c} must be a function`)})}return d.condition&&typeof d.condition!="function"&&h.push("condition must be a function"),d.priority!==void 0&&typeof d.priority!="number"&&h.push("priority must be a number"),{valid:h.length===0,errors:h}}function s(d){if(!Array.isArray(d))return{valid:!1,errors:[{tooltip:null,errors:["tooltips must be an array"]}]};const h=[],T=new Set;return d.forEach((c,x)=>{const p=o(c);p.valid||h.push({tooltip:c.id||`index ${x}`,errors:p.errors}),c.id&&(T.has(c.id)&&h.push({tooltip:c.id,errors:[`Duplicate tooltip ID: ${c.id}`]}),T.add(c.id))}),{valid:h.length===0,errors:h}}class t extends Error{constructor(h,T=null){super(h),this.name="TooltipError",this.tooltipId=T}}function a(d){if(!d.valid){const h=d.errors.map(T=>`${T.tooltip}: ${T.errors.join(", ")}`).join(` -`);throw b.logError("[TooltipDefinitions] Validation",h,{function:"validateTooltip"}),new t(`Tooltip validation failed: -${h}`)}}v.TooltipValidation={validateTooltipDefinition:o,validateTooltipDefinitions:s,handleValidationErrors:a,TooltipError:t},k("[TooltipDefinitions] Validation module loaded")})(window);const TOOLTIP_DEFINITIONS=[{id:"welcome",element:"#brand",popover:{title:"Welcome to DashCaddy!",description:` + `,document.head.appendChild(A)}getAvailableThemes(){return Object.keys(a)}isThemeAvailable(n){return a.hasOwnProperty(n)}}y.ThemeAdapter=c,_("[ThemeAdapter] Module loaded")})(window),(function(y){"use strict";const P=new ErrorHandler,_=(...u)=>{y.DASHCADDY_DEBUG&&console.log(...u)};function a(u){const b=[];if((!u.id||typeof u.id!="string")&&b.push("Tooltip must have a valid string id"),u.element||b.push("Tooltip must have an element selector or HTMLElement"),!u.popover||typeof u.popover!="object")b.push("Tooltip must have a popover object");else{if((!u.popover.title||typeof u.popover.title!="string")&&b.push("Tooltip popover must have a valid string title"),(!u.popover.description||typeof u.popover.description!="string")&&b.push("Tooltip popover must have a valid string description"),u.popover.position){const L=["top","bottom","left","right","center"];L.includes(u.popover.position)||b.push(`Invalid position: ${u.popover.position}. Must be one of: ${L.join(", ")}`)}if(u.popover.align){const L=["start","center","end"];L.includes(u.popover.align)||b.push(`Invalid align: ${u.popover.align}. Must be one of: ${L.join(", ")}`)}u.popover.showButtons&&!Array.isArray(u.popover.showButtons)&&b.push("showButtons must be an array"),["onNext","onPrevious","onClose","onSetupNow","onLater"].forEach(L=>{u.popover[L]&&typeof u.popover[L]!="function"&&b.push(`${L} must be a function`)})}return u.condition&&typeof u.condition!="function"&&b.push("condition must be a function"),u.priority!==void 0&&typeof u.priority!="number"&&b.push("priority must be a number"),{valid:b.length===0,errors:b}}function c(u){if(!Array.isArray(u))return{valid:!1,errors:[{tooltip:null,errors:["tooltips must be an array"]}]};const b=[],$=new Set;return u.forEach((L,R)=>{const j=a(L);j.valid||b.push({tooltip:L.id||`index ${R}`,errors:j.errors}),L.id&&($.has(L.id)&&b.push({tooltip:L.id,errors:[`Duplicate tooltip ID: ${L.id}`]}),$.add(L.id))}),{valid:b.length===0,errors:b}}class r extends Error{constructor(b,$=null){super(b),this.name="TooltipError",this.tooltipId=$}}function n(u){if(!u.valid){const b=u.errors.map($=>`${$.tooltip}: ${$.errors.join(", ")}`).join(` +`);throw P.logError("[TooltipDefinitions] Validation",b,{function:"validateTooltip"}),new r(`Tooltip validation failed: +${b}`)}}y.TooltipValidation={validateTooltipDefinition:a,validateTooltipDefinitions:c,handleValidationErrors:n,TooltipError:r},_("[TooltipDefinitions] Validation module loaded");const v=[{id:"welcome",element:"#brand",popover:{title:"Welcome to DashCaddy!",description:`

Your unified control panel for Docker, Caddy, and DNS — all in one place.

This tour will walk you through every section so you know exactly where everything is.

You can click your logo anytime to customize it.

@@ -172,7 +154,7 @@ ${h}`)}}v.TooltipValidation={validateTooltipDefinition:o,validateTooltipDefiniti

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(b=>b.id===v)||null}function getActiveTooltips(){return TOOLTIP_DEFINITIONS.filter(v=>{if(v.condition&&typeof v.condition=="function")try{return v.condition()}catch(b){return errorHandler.logError("[TooltipDefinitions] Condition Eval",b,{function:"evaluateCondition",tooltipId:v.id}),!1}return!0})}function getSortedTooltips(){return getActiveTooltips().sort((b,k)=>{const o=b.priority||999,s=k.priority||999;return o-s})}function getNewFeatureTooltips(){return getActiveTooltips().filter(b=>b.isNewFeature===!0).sort((b,k)=>{const o=b.priority||999,s=k.priority||999;return o-s})}window.TooltipDefinitions={TOOLTIP_DEFINITIONS,getTooltipDefinitions,getTooltipById,getActiveTooltips,getSortedTooltips,getNewFeatureTooltips},debug("[TooltipDefinitions] Definitions loaded:",TOOLTIP_DEFINITIONS.length,"tooltips"),(function(v){"use strict";const b=(...o)=>{v.DASHCADDY_DEBUG&&console.log(...o)};class k{constructor(s){this.progressTracker=s,this.modal=null,this.onTemplateSelected=null,b("[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 s=document.createElement("div");s.id="dns-template-modal",s.className="dns-template-modal",s.innerHTML=` + `,position:"bottom",align:"start",showButtons:["previous","close"],showProgress:!0},priority:13}];function A(){return v}function k(u){return v.find(b=>b.id===u)||null}function m(){return v.filter(u=>{if(u.condition&&typeof u.condition=="function")try{return u.condition()}catch(b){return P.logError("[TooltipDefinitions] Condition Eval",b,{function:"evaluateCondition",tooltipId:u.id}),!1}return!0})}function C(){return m().sort((b,$)=>{const L=b.priority||999,R=$.priority||999;return L-R})}function l(){return m().filter(b=>b.isNewFeature===!0).sort((b,$)=>{const L=b.priority||999,R=$.priority||999;return L-R})}y.TooltipDefinitions={TOOLTIP_DEFINITIONS:v,getTooltipDefinitions:A,getTooltipById:k,getActiveTooltips:m,getSortedTooltips:C,getNewFeatureTooltips:l},_("[TooltipDefinitions] Definitions loaded:",v.length,"tooltips")})(window),(function(y){"use strict";const P=(...a)=>{y.DASHCADDY_DEBUG&&console.log(...a)};class _{constructor(c){this.progressTracker=c,this.modal=null,this.onTemplateSelected=null,P("[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 c=document.createElement("div");c.id="dns-template-modal",c.className="dns-template-modal",c.innerHTML=`

\u{1F310} Choose a DNS Server

@@ -186,21 +168,21 @@ ${h}`)}}v.TooltipValidation={validateTooltipDefinition:o,validateTooltipDefiniti
- `,document.body.appendChild(s),this.modal=s,s.querySelector(".dns-template-close").addEventListener("click",()=>this.close()),s.querySelector("#dns-setup-later").addEventListener("click",()=>this.handleSetupLater()),s.addEventListener("click",t=>{t.target===s&&this.close()}),document.addEventListener("keydown",t=>{t.key==="Escape"&&s.style.display==="flex"&&this.close()})}populateTemplates(){const s=document.getElementById("dns-template-grid");if(!s)return;const t=this.getDnsTemplates();s.innerHTML="",t.forEach(a=>{const d=this.createTemplateCard(a);s.appendChild(d)})}createTemplateCard(s){const t=document.createElement("div");t.className="dns-template-card",s.recommended&&t.classList.add("recommended");const a=s.difficulty.toLowerCase();return t.innerHTML=` - ${s.recommended?'':""} -
${s.icon}
-

${s.name}

-

${s.description}

-
- ${s.difficulty} + `,document.body.appendChild(c),this.modal=c,c.querySelector(".dns-template-close").addEventListener("click",()=>this.close()),c.querySelector("#dns-setup-later").addEventListener("click",()=>this.handleSetupLater()),c.addEventListener("click",r=>{r.target===c&&this.close()}),document.addEventListener("keydown",r=>{r.key==="Escape"&&c.style.display==="flex"&&this.close()})}populateTemplates(){const c=document.getElementById("dns-template-grid");if(!c)return;const r=this.getDnsTemplates();c.innerHTML="",r.forEach(n=>{const v=this.createTemplateCard(n);c.appendChild(v)})}createTemplateCard(c){const r=document.createElement("div");r.className="dns-template-card",c.recommended&&r.classList.add("recommended");const n=c.difficulty.toLowerCase();return r.innerHTML=` + ${c.recommended?'':""} +
${c.icon}
+

${c.name}

+

${c.description}

+
+ ${c.difficulty}
    - ${s.features.slice(0,3).map(h=>`
  • ${h}
  • `).join("")} + ${c.features.slice(0,3).map(A=>`
  • ${A}
  • `).join("")}
- - `,t.querySelector(".dns-template-select-btn").addEventListener("click",()=>this.handleTemplateSelection(s)),t}handleTemplateSelection(s){b(`[DnsTemplateSelector] Template selected: ${s.id}`),this.close(),this.onTemplateSelected?this.onTemplateSelected(s):this.openAppSelector(s.id)}handleSetupLater(){b("[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(s){const t=document.querySelector('[onclick*="showAppSelector"]');t?(t.click(),setTimeout(()=>{const a=document.querySelector("#app-search");a&&(a.value=s,a.dispatchEvent(new Event("input",{bubbles:!0})))},300)):this.showNotification(`To deploy ${s}, use the App Selector and search for "${s}"`)}showNotification(s){const t=document.createElement("div");t.className="dns-template-notification",t.textContent=s,t.style.cssText=` + `,r.querySelector(".dns-template-select-btn").addEventListener("click",()=>this.handleTemplateSelection(c)),r}handleTemplateSelection(c){P(`[DnsTemplateSelector] Template selected: ${c.id}`),this.close(),this.onTemplateSelected?this.onTemplateSelected(c):this.openAppSelector(c.id)}handleSetupLater(){P("[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(c){const r=document.querySelector('[onclick*="showAppSelector"]');r?(r.click(),setTimeout(()=>{const n=document.querySelector("#app-search");n&&(n.value=c,n.dispatchEvent(new Event("input",{bubbles:!0})))},300)):this.showNotification(`To deploy ${c}, use the App Selector and search for "${c}"`)}showNotification(c){const r=document.createElement("div");r.className="dns-template-notification",r.textContent=c,r.style.cssText=` position: fixed; top: 20px; right: 20px; @@ -211,8 +193,8 @@ ${h}`)}}v.TooltipValidation={validateTooltipDefinition:o,validateTooltipDefiniti 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=k,b("[DnsTemplateSelector] Module loaded")})(window),(function(v){"use strict";const b=new ErrorHandler,k=(...s)=>{v.DASHCADDY_DEBUG&&console.log(...s)};class o{constructor(t,a,d){this.progressTracker=t,this.themeAdapter=a,this.dnsTemplateSelector=d,this.driver=null,this.currentStepIndex=0,this.isActive=!1,this.resizeHandler=null,this.layoutChangeHandler=null}async initializeDriver(){const t=v.driver?.js?.driver||v.driver?.driver||v.driver;if(typeof t!="function")return b.logError("[TourManager] Driver.js Not Loaded",new Error("Driver.js not loaded or invalid"),{windowDriver:typeof v.driver}),!1;const a=this.themeAdapter.getDriverTheme();return this.driver=t({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 t=v.TooltipDefinitions.getSortedTooltips(),a=this.progressTracker.getCompletedTooltips(),d=t.filter(T=>!a.includes(T.id));if(d.length===0){k("[TourManager] No tooltips to show"),this.progressTracker.markTourCompleted();return}const h=d.map((T,c)=>{const x=c===0,p=c===d.length-1,B={element:T.element,popover:{title:T.popover.title,description:T.popover.description,side:T.popover.position||"bottom",align:T.popover.align||"start",showButtons:this._getButtonsForStep(T,x,p),showProgress:T.popover.showProgress!==!1,onNextClick:()=>{this.progressTracker.markTooltipCompleted(T.id),this.progressTracker.setCurrentStep(c+1),this.currentStepIndex=c+1,this.driver.moveNext()},onPrevClick:()=>{this.progressTracker.setCurrentStep(Math.max(0,c-1)),this.currentStepIndex=Math.max(0,c-1),this.driver.movePrevious()},onCloseClick:()=>{this.skipTour()}}};return T.id==="dns-priority"&&this.dnsTemplateSelector&&(B.popover.onSetupNowClick=()=>{k("[TourManager] Opening DNS template selector"),this.dnsTemplateSelector.showTemplateSelector(),this.progressTracker.markTooltipCompleted(T.id),this.progressTracker.setCurrentStep(c+1),this.currentStepIndex=c+1,this.driver.moveNext()},B.popover.onLaterClick=()=>{k("[TourManager] DNS setup deferred"),this.progressTracker.markDnsSetupDeferred(),this.progressTracker.markTooltipCompleted(T.id),this.progressTracker.setCurrentStep(c+1),this.currentStepIndex=c+1,this.driver.moveNext()}),B});this.isActive=!0,this.driver.setSteps(h),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(t){const a=v.TooltipDefinitions.getTooltipById(t);if(!a){b.logError("[TourManager] Tooltip Not Found",new Error(`Tooltip not found: ${t}`),{tooltipId:t});return}this.driver||await this.initializeDriver();const d={element:a.element,popover:{title:a.popover.title,description:a.popover.description,side:a.popover.position||"bottom",align:a.popover.align||"start"}};this.driver.highlight(d)}async showWhatsNew(){if(!this.driver&&!await this.initializeDriver())return;const t=v.TooltipDefinitions.getNewFeatureTooltips();if(t.length===0){k("[TourManager] No new features to show");return}k(`[TourManager] Showing ${t.length} new features`);const a=t.map((d,h)=>{const T=h===0,c=h===t.length-1;return{element:d.element,popover:{title:`\u2728 NEW: ${d.popover.title}`,description:d.popover.description,side:d.popover.position||"bottom",align:d.popover.align||"start",showButtons:this._getButtonsForStep(d,T,c),showProgress:!0,onNextClick:()=>{this.driver.moveNext()},onPrevClick:()=>{this.driver.movePrevious()},onCloseClick:()=>{this.skipTour()}}}});this.isActive=!0,this.driver.setSteps(a),this.driver.drive()}setupDynamicRepositioning(){let t;this.resizeHandler=()=>{clearTimeout(t),t=setTimeout(()=>{this.isActive&&this.driver&&(k("[TourManager] Window resized, repositioning tooltip"),this.driver.refresh())},150)},this.layoutChangeHandler=()=>{this.isActive&&this.driver&&(k("[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(t,a,d){if(t.popover.showButtons)return t.popover.showButtons;const h=[];return a||h.push("previous"),d?h.push("close"):h.push("next"),h}onTourComplete(){this.progressTracker.markTourCompleted(),this.isActive=!1,k("[TourManager] Tour completed")}onTourSkip(){k("[TourManager] Tour skipped"),this.isActive=!1}}v.TourManager=o,k("[TourManager] Module loaded")})(window),(function(){"use strict";const v=(...c)=>{window.DASHCADDY_DEBUG&&console.log(...c)};let b,k,o,s,t;async function a(){try{if(v("[Onboarding] Initializing system..."),window.__dashcaddySiteConfigLoaded)try{await window.__dashcaddySiteConfigLoaded}catch{}if(t=new ErrorHandler,v("[Onboarding] Error Handler initialized"),b=new ProgressTracker("dashcaddy_onboarding"),v("[Onboarding] Progress Tracker initialized"),k=new ThemeAdapter,v("[Onboarding] Theme Adapter initialized"),s=new DnsTemplateSelector(b),v("[Onboarding] DNS Template Selector initialized"),o=new TourManager(b,k,s),v("[Onboarding] Tour Manager initialized"),o.shouldAutoStart())v("[Onboarding] Auto-starting tour for first-time install"),await b.markInstallOnboardingCompleted(),setTimeout(()=>{o.startTour()},1e3);else{const c=b.isTourCompleted(),x=b.getCurrentStep();v(`[Onboarding] Tour not auto-starting (completed: ${c}, step: ${x})`),!c&&x>0&&v("[Onboarding] Tour in progress, can be resumed manually")}d(),window.DashCaddyOnboarding={startTour:()=>o.startTour(),restartTour:()=>o.restartTour(),showTooltip:c=>o.showTooltip(c),showWhatsNew:()=>o.showWhatsNew(),resetProgress:()=>b.resetProgress(),getErrors:()=>t.getErrors(),getErrorStats:()=>t.getStatistics()},v("[Onboarding] System initialized successfully")}catch(c){t&&t.logError("[Onboarding] Initialization",c),console.warn("[Onboarding] System failed to initialize, dashboard will continue without onboarding")}}function d(){const c=document.querySelector(".tools-primary")||document.querySelector(".tools");if(!c)return;const x=()=>{o?(v("[Onboarding] Starting tour via button click"),o.restartTour()):(t&&t.logError("[Onboarding] Tour Manager Not Initialized",new Error("Tour manager not initialized")),alert(`Tour is not available. Check browser console for errors. + `,document.body.appendChild(r),setTimeout(()=>{r.style.opacity="0",r.style.transition="opacity 0.3s",setTimeout(()=>r.remove(),300)},3e3)}close(){this.modal&&(this.modal.style.display="none",document.body.style.overflow="")}}y.DnsTemplateSelector=_,P("[DnsTemplateSelector] Module loaded")})(window),(function(y){"use strict";const P=new ErrorHandler,_=(...c)=>{y.DASHCADDY_DEBUG&&console.log(...c)};class a{constructor(r,n,v){this.progressTracker=r,this.themeAdapter=n,this.dnsTemplateSelector=v,this.driver=null,this.currentStepIndex=0,this.isActive=!1,this.resizeHandler=null,this.layoutChangeHandler=null}async initializeDriver(){const r=y.driver?.js?.driver||y.driver?.driver||y.driver;if(typeof r!="function")return P.logError("[TourManager] Driver.js Not Loaded",new Error("Driver.js not loaded or invalid"),{windowDriver:typeof y.driver}),!1;const n=this.themeAdapter.getDriverTheme();return this.driver=r({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 r=y.TooltipDefinitions.getSortedTooltips(),n=this.progressTracker.getCompletedTooltips(),v=r.filter(k=>!n.includes(k.id));if(v.length===0){_("[TourManager] No tooltips to show"),this.progressTracker.markTourCompleted();return}const A=v.map((k,m)=>{const C=m===0,l=m===v.length-1,u={element:k.element,popover:{title:k.popover.title,description:k.popover.description,side:k.popover.position||"bottom",align:k.popover.align||"start",showButtons:this._getButtonsForStep(k,C,l),showProgress:k.popover.showProgress!==!1,onNextClick:()=>{this.progressTracker.markTooltipCompleted(k.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 k.id==="dns-priority"&&this.dnsTemplateSelector&&(u.popover.onSetupNowClick=()=>{_("[TourManager] Opening DNS template selector"),this.dnsTemplateSelector.showTemplateSelector(),this.progressTracker.markTooltipCompleted(k.id),this.progressTracker.setCurrentStep(m+1),this.currentStepIndex=m+1,this.driver.moveNext()},u.popover.onLaterClick=()=>{_("[TourManager] DNS setup deferred"),this.progressTracker.markDnsSetupDeferred(),this.progressTracker.markTooltipCompleted(k.id),this.progressTracker.setCurrentStep(m+1),this.currentStepIndex=m+1,this.driver.moveNext()}),u});this.isActive=!0,this.driver.setSteps(A),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(r){const n=y.TooltipDefinitions.getTooltipById(r);if(!n){P.logError("[TourManager] Tooltip Not Found",new Error(`Tooltip not found: ${r}`),{tooltipId:r});return}this.driver||await this.initializeDriver();const v={element:n.element,popover:{title:n.popover.title,description:n.popover.description,side:n.popover.position||"bottom",align:n.popover.align||"start"}};this.driver.highlight(v)}async showWhatsNew(){if(!this.driver&&!await this.initializeDriver())return;const r=y.TooltipDefinitions.getNewFeatureTooltips();if(r.length===0){_("[TourManager] No new features to show");return}_(`[TourManager] Showing ${r.length} new features`);const n=r.map((v,A)=>{const k=A===0,m=A===r.length-1;return{element:v.element,popover:{title:`\u2728 NEW: ${v.popover.title}`,description:v.popover.description,side:v.popover.position||"bottom",align:v.popover.align||"start",showButtons:this._getButtonsForStep(v,k,m),showProgress:!0,onNextClick:()=>{this.driver.moveNext()},onPrevClick:()=>{this.driver.movePrevious()},onCloseClick:()=>{this.skipTour()}}}});this.isActive=!0,this.driver.setSteps(n),this.driver.drive()}setupDynamicRepositioning(){let r;this.resizeHandler=()=>{clearTimeout(r),r=setTimeout(()=>{this.isActive&&this.driver&&(_("[TourManager] Window resized, repositioning tooltip"),this.driver.refresh())},150)},this.layoutChangeHandler=()=>{this.isActive&&this.driver&&(_("[TourManager] Layout changed, repositioning tooltip"),setTimeout(()=>{this.driver&&this.driver.refresh()},100))},y.addEventListener("resize",this.resizeHandler),this.themeAdapter.onThemeChange(this.layoutChangeHandler)}cleanupDynamicRepositioning(){this.resizeHandler&&y.removeEventListener("resize",this.resizeHandler)}_getButtonsForStep(r,n,v){if(r.popover.showButtons)return r.popover.showButtons;const A=[];return n||A.push("previous"),v?A.push("close"):A.push("next"),A}onTourComplete(){this.progressTracker.markTourCompleted(),this.isActive=!1,_("[TourManager] Tour completed")}onTourSkip(){_("[TourManager] Tour skipped"),this.isActive=!1}}y.TourManager=a,_("[TourManager] Module loaded")})(window),(function(){"use strict";const y=(...m)=>{window.DASHCADDY_DEBUG&&console.log(...m)};let P,_,a,c,r;async function n(){try{if(y("[Onboarding] Initializing system..."),window.__dashcaddySiteConfigLoaded)try{await window.__dashcaddySiteConfigLoaded}catch{}if(r=new ErrorHandler,y("[Onboarding] Error Handler initialized"),P=new ProgressTracker("dashcaddy_onboarding"),y("[Onboarding] Progress Tracker initialized"),_=new ThemeAdapter,y("[Onboarding] Theme Adapter initialized"),c=new DnsTemplateSelector(P),y("[Onboarding] DNS Template Selector initialized"),a=new TourManager(P,_,c),y("[Onboarding] Tour Manager initialized"),a.shouldAutoStart())y("[Onboarding] Auto-starting tour for first-time install"),await P.markInstallOnboardingCompleted(),setTimeout(()=>{a.startTour()},1e3);else{const m=P.isTourCompleted(),C=P.getCurrentStep();y(`[Onboarding] Tour not auto-starting (completed: ${m}, step: ${C})`),!m&&C>0&&y("[Onboarding] Tour in progress, can be resumed manually")}v(),window.DashCaddyOnboarding={startTour:()=>a.startTour(),restartTour:()=>a.restartTour(),showTooltip:m=>a.showTooltip(m),showWhatsNew:()=>a.showWhatsNew(),resetProgress:()=>P.resetProgress(),getErrors:()=>r.getErrors(),getErrorStats:()=>r.getStatistics()},y("[Onboarding] System initialized successfully")}catch(m){r&&r.logError("[Onboarding] Initialization",m),console.warn("[Onboarding] System failed to initialize, dashboard will continue without onboarding")}}function v(){const m=document.querySelector(".tools-primary")||document.querySelector(".tools");if(!m)return;const C=()=>{a?(y("[Onboarding] Starting tour via button click"),a.restartTour()):(r&&r.logError("[Onboarding] Tour Manager Not Initialized",new Error("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`))},p=document.getElementById("restart-tour-btn");if(p){p.onclick=x;return}const B=document.createElement("button");B.id="restart-tour-btn",B.textContent="Help Tour",B.title="Restart the onboarding tour",B.onclick=x,c.appendChild(B)}function h(){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 T(){let c=0;const x=10;function p(){h()?a():(c++,c