From c50b106faf295584630c78a38c9fc95cf06616ed Mon Sep 17 00:00:00 2001 From: Krystie Date: Fri, 15 May 2026 01:27:56 -0700 Subject: [PATCH] Add Container Log Viewer UI with streaming, search, and download MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New container-logs.js module for viewing Docker container logs - Integrated with existing API endpoints (/logs/containers, /logs/container/:id, /logs/stream/:id) - Features: - Select container from dropdown - View logs with stdout/stderr color coding - Real-time log streaming via SSE - Search/filter within logs - Download logs as text file - Line count and filter indicators - Added '📜 Container Logs' button to Tools section in index.html - Added to features.js bundle via build.js - Rebuilt dist files --- status/build.js | 1 + status/dist/core.js | 176 +++++++------- status/dist/features.js | 471 ++++++++++++++++++++---------------- status/dist/init.js | 14 +- status/dist/onboarding.js | 72 +++--- status/index.html | 3 +- status/js/container-logs.js | 403 ++++++++++++++++++++++++++++++ 7 files changed, 801 insertions(+), 339 deletions(-) create mode 100644 status/js/container-logs.js diff --git a/status/build.js b/status/build.js index 22c67f7..1bce0c3 100644 --- a/status/build.js +++ b/status/build.js @@ -33,6 +33,7 @@ const bundles = { JS('recipes.js'), JS('import-export.js'), JS('error-logs.js'), + JS('container-logs.js'), JS('smart-arr-connect.js'), JS('notification-settings.js'), JS('panel-tabs.js'), diff --git a/status/dist/core.js b/status/dist/core.js index 05f88bd..e234bd8 100644 --- a/status/dist/core.js +++ b/status/dist/core.js @@ -1,35 +1,35 @@ -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"}},_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 f=await fetch("/api/v1/config");if(f.ok){const m=await f.json();if(m.tld&&(SITE.tld=m.tld.startsWith(".")?m.tld:"."+m.tld),m.dns&&(SITE.dnsIp=m.dns.ip||"",SITE.dnsPort=m.dns.port||DC.DEFAULTS.DNS_PORT),m.dnsServers&&typeof m.dnsServers=="object")for(const[b,a]of Object.entries(m.dnsServers))b!=="__proto__"&&b!=="constructor"&&b!=="prototype"&&(SITE.dnsServers[b]=a);m.configurationType&&(SITE.configurationType=m.configurationType),m.domain&&(SITE.domain=m.domain),m.defaults&&(SITE.defaults=m.defaults),m.routingMode&&(SITE.routingMode=m.routingMode),SITE.onboardingCompleted=m.onboardingCompleted===!0,localStorage.setItem("dashcaddy_site_config",JSON.stringify({tld:SITE.tld,configurationType:SITE.configurationType,domain:SITE.domain,routingMode:SITE.routingMode})),renderDnsCards();const l=document.getElementById("manage-tokens");l&&(l.style.display=Object.keys(SITE.dnsServers).length?"":"none")}}catch{}document.querySelectorAll("[data-tld]").forEach(f=>f.textContent=SITE.tld);const i=document.getElementById("edit-tld-suffix");i&&(i.textContent=SITE.tld);const v=document.getElementById("external-proxy-ip");v&&SITE.dnsIp&&(v.value=SITE.dnsIp,v.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 i=SITE.dnsServers[o];return i?`${i.ip}:${i.port}`:buildDomain(o)}function getPrimaryDnsId(){if(!SITE.dnsIp)return null;for(const[o,i]of Object.entries(SITE.dnsServers))if(i.ip===SITE.dnsIp)return o;return null}function renderDnsCards(){const o=document.querySelector(".top");if(!o)return;const i=Object.keys(SITE.dnsServers);if(!i.length)return;const v='',f=o.firstElementChild;i.forEach(m=>{const l=escapeHtml(m),b=escapeHtml((SITE.dnsServers[m].name||m).toUpperCase()),a=document.createElement("div");a.className="card",a.setAttribute("data-app",m),a.setAttribute("data-status","off"),a.innerHTML=`
${v}
${b}OFF
--
--
`,o.insertBefore(a,f)})}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 console.error("Failed to get CSRF token:",o),o}}async function secureFetch(o,i={}){const v=(i.method||"GET").toUpperCase(),f=!["GET","HEAD","OPTIONS"].includes(v);if(f)try{const l=await getCSRFToken();i.headers={...i.headers,"X-CSRF-Token":l}}catch(l){console.error("Failed to add CSRF token to request:",l)}i.signal||(i={...i,signal:AbortSignal.timeout(15e3)});const m=await fetch(o,i);if(f&&m.status===403)try{const l=await m.clone().json();if(l.error&&(l.error.includes("DC-100")||l.error.includes("DC-101"))){csrfToken=null;const b=await getCSRFToken();return i.headers={...i.headers,"X-CSRF-Token":b},i.signal=AbortSignal.timeout(15e3),fetch(o,i)}}catch{}return m}async function postJSON(o,i){const v=await secureFetch(o,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)}),f=await v.json();if(!v.ok||f.success===!1)throw new Error(f.error||`Request failed (${v.status})`);return f}async function getJSON(o){const i=await secureFetch(o);if(!i.ok){let v=`Request failed (${i.status})`;try{v=(await i.json()).error||v}catch{}throw new Error(v)}return i.json()}async function deleteAPI(o){const i=await secureFetch(o,{method:"DELETE"}),v=await i.json();if(!i.ok||v.success===!1)throw new Error(v.error||`Delete failed (${i.status})`);return v}async function withButton(o,i,v,f={}){const m=o.innerHTML,{successText:l="\u2705",resetDelay:b=DC.DELAYS.BTN_RESET}=f;o.disabled=!0,o.innerHTML=i;try{const a=await v();return o.innerHTML=l,setTimeout(()=>{o.innerHTML=m,o.disabled=!1},b),a}catch(a){throw o.innerHTML=m,o.disabled=!1,a}}function openModal(o){document.getElementById(o)?.classList.add("show")}function closeModal(o){document.getElementById(o)?.classList.remove("show")}function wireModal(o,...i){o&&(o.addEventListener("click",v=>{v.target===o&&o.classList.remove("show")}),i.forEach(v=>v?.addEventListener("click",()=>o.classList.remove("show"))))}function showNotification(o,i="info",v=3e3){const f=document.querySelector(".deploy-notification");f&&f.remove();const m={info:{bg:"#2196F3",fg:"#fff"},success:{bg:"var(--ok-bg)",fg:"var(--ok-fg)"},error:{bg:"#f44336",fg:"#fff"},warning:{bg:"#ff9800",fg:"#fff"}},l=m[i]||m.info,b=document.createElement("div");b.className="deploy-notification",b.textContent=o,b.style.cssText=` +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 k=await fetch("/api/v1/config");if(k.ok){const v=await k.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 i=document.getElementById("manage-tokens");i&&(i.style.display=Object.keys(SITE.dnsServers).length?"":"none")}}catch{}document.querySelectorAll("[data-tld]").forEach(k=>k.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='',k=o.firstElementChild;m.forEach(v=>{const i=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,k)})}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(),k=!["GET","HEAD","OPTIONS"].includes(l);if(k)try{const i=await getCSRFToken();m.headers={...m.headers,"X-CSRF-Token":i}}catch(i){errorHandler.logError("[CSRF] Add to Request",i,{function:"secureFetch"})}m.signal||(m={...m,signal:AbortSignal.timeout(15e3)});const v=await fetch(o,m);if(k&&v.status===403)try{const i=await v.clone().json();if(i.error&&(i.error.includes("DC-100")||i.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)}),k=await l.json();if(!l.ok||k.success===!1)throw new Error(k.error||`Request failed (${l.status})`);return k}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,k={}){const v=o.innerHTML,{successText:i="\u2705",resetDelay:h=DC.DELAYS.BTN_RESET}=k;o.disabled=!0,o.innerHTML=m;try{const n=await l();return o.innerHTML=i,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 k=document.querySelector(".deploy-notification");k&&k.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"}},i=v[m]||v.info,h=document.createElement("div");h.className="deploy-notification",h.textContent=o,h.style.cssText=` position: fixed; top: 20px; right: 20px; - background: ${l.bg}; color: ${l.fg}; + background: ${i.bg}; color: ${i.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(b),v>0&&setTimeout(()=>b.remove(),v)}function timeAgo(o){const i=Date.now()-new Date(o).getTime();return i<6e4?"just now":i<36e5?Math.floor(i/6e4)+"m ago":i<864e5?Math.floor(i/36e5)+"h ago":Math.floor(i/864e5)+"d ago"}function safeGet(o,i=null){try{const v=localStorage.getItem(o);return v!==null?v:i}catch{return i}}function safeSet(o,i){try{localStorage.setItem(o,i)}catch{}}function safeRemove(o){try{localStorage.removeItem(o)}catch{}}function safeSessionGet(o,i=null){try{const v=sessionStorage.getItem(o);return v!==null?v:i}catch{return i}}function safeSessionSet(o,i){try{sessionStorage.setItem(o,i)}catch{}}function safeGetJSON(o,i=null){try{const v=localStorage.getItem(o);return v?JSON.parse(v):i}catch{return i}}function escapeHtml(o){return String(o??"").replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function injectModal(o,i){document.getElementById(o)||document.body.insertAdjacentHTML("beforeend",i)}const DC_BUS={_handlers:{},on(o,i){var v;((v=this._handlers)[o]||(v[o]=[])).push(i)},off(o,i){this._handlers[o]=this._handlers[o]?.filter(v=>v!==i)},emit(o,i){this._handlers[o]?.forEach(v=>v(i))}},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(i=>i.id===o)},addApp(o){this._apps.push(o),window.APPS=this._apps,DC_BUS.emit("apps:changed",this._apps)},removeApp(o){const i=this._apps.findIndex(v=>v.id===o);return i>-1&&(this._apps.splice(i,1),window.APPS=this._apps,DC_BUS.emit("apps:changed",this._apps)),i>-1},updateApp(o,i){const v=this._apps.find(f=>f.id===o);if(v){for(const[f,m]of Object.entries(i))f!=="__proto__"&&f!=="constructor"&&f!=="prototype"&&(v[f]=m);DC_BUS.emit("apps:changed",this._apps)}return v}};(function(){function o(){const f=document.createElement("div");return f.className="skeleton-card",f.innerHTML='
',f}function i(f){const m=document.getElementById("cards");if(!(!m||m.querySelector(".card"))){f=f||6;for(let l=0;l.4,P={};return P.hover=S?u(g,T,.35):u(g,$,.08),P["card-hover"]=u(g,P.hover,.5),P.base=u(T,g,.6),P["fg-muted"]=u(w,T,.35),P.success=C,P.error=E,P.warning=S?"#d68a00":"#f39c12",P}function s(x,T){var $=T.lightBg||T.bg&&h(T.bg)>.4,w=T.accent||T["accent-strong"]||"#888888",g=t(w);return $?":root."+x+` body { + `,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(k=>k.id===o);if(l){for(const[k,v]of Object.entries(m))k!=="__proto__"&&k!=="constructor"&&k!=="prototype"&&(l[k]=v);DC_BUS.emit("apps:changed",this._apps)}return l}};(function(){function o(){const k=document.createElement("div");return k.className="skeleton-card",k.innerHTML='
',k}function m(k){const v=document.getElementById("cards");if(!(!v||v.querySelector(".card"))){k=k||6;for(let i=0;i.4,P={};return P.hover=C?c(y,T,.35):c(y,$,.08),P["card-hover"]=c(y,P.hover,.5),P.base=c(T,y,.6),P["fg-muted"]=c(b,T,.35),P.success=I,P.error=E,P.warning=C?"#d68a00":"#f39c12",P}function s(w,T){var $=T.lightBg||T.bg&&g(T.bg)>.4,b=T.accent||T["accent-strong"]||"#888888",y=p(b);return $?":root."+w+` body { background: - radial-gradient(1200px 800px at 10% -10%, rgba(`+g.r+","+g.g+","+g.b+`, .08), transparent 60%), - radial-gradient(1000px 700px at 110% 10%, rgba(`+g.r+","+g.g+","+g.b+`, .05), transparent 55%), + 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%), var(--bg); } -`:":root."+x+` body { +`:":root."+w+` body { background: - radial-gradient(1200px 900px at 8% -12%, rgba(`+g.r+","+g.g+","+g.b+`, .10), transparent 60%), - radial-gradient(1000px 700px at 110% -10%, rgba(`+g.r+","+g.g+","+g.b+`, .07), transparent 55%), + 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%), var(--bg); } -`}function p(x,T){var $=T.lightBg||T.bg&&h(T.bg)>.4;return $?":root."+x+` button:hover { +`}function u(w,T){var $=T.lightBg||T.bg&&g(T.bg)>.4;return $?":root."+w+` 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); } -`:":root."+x+` button:hover { +`:":root."+w+` button:hover { background: color-mix(in srgb, var(--accent) 18%, transparent); border-color: color-mix(in srgb, var(--accent) 35%, var(--border)); } -`}function n(){return window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}function c(){l.forEach(function(x){document.documentElement.style.removeProperty("--"+x)})}function y(x,T){var $=x.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"");$||($="custom"),f.indexOf($)!==-1&&($=$+"-custom");for(var w=safeGetJSON(i,{}),g=$,C=2;w[$]&&$!==T;)$=g+"-"+C++;return $}function r(x){var T=document.getElementById("user-theme-styles");T&&T.remove(),m.length=f.length,Object.keys(k).forEach(function(E){f.indexOf(E)===-1&&delete k[E]});var $=x||safeGetJSON(i,{}),w=Object.keys($);if(w=w.filter(function(E){return f.indexOf(E)===-1}),!!w.length){var g="";w.forEach(function(E){var S=$[E];m.indexOf(E)===-1&&m.push(E);var P={};l.forEach(function(O){S[O]&&(P[O]=S[O])}),P["card-bg"]=S["card-base"]||S.bg,S.lightBg&&(P.lightBg=!0);var N=e(P);a.forEach(function(O){!P[O]&&N[O]&&(P[O]=N[O])}),k[E]=P,g+=":root."+E+` { -`,l.forEach(function(O){P[O]&&(g+=" --"+O+": "+P[O]+`; -`)}),g+=`} -`,g+=s(E,P),g+=p(E,P)});var C=document.createElement("style");C.id="user-theme-styles",C.textContent=g,document.head.appendChild(C)}}function I(){secureFetch("/api/v1/themes").then(function(x){return x.json()}).then(function(x){if(!(!x.success||!x.themes)){var T=x.themes,$=safeGetJSON(i,{});if(JSON.stringify(T)!==JSON.stringify($)){safeSet(i,JSON.stringify(T)),r(T);var w=safeGet(o);w&&m.indexOf(w)!==-1&&L(w)}}}).catch(function(){})}function B(){var x=safeGetJSON(v);if(x){var T=x.name||"Custom",$=y(T),w={name:T};l.forEach(function(E){x[E]&&(w[E]=x[E])});var g=safeGetJSON(i,{});g[$]=w,safeSet(i,JSON.stringify(g)),safeGet(o)==="custom"&&safeSet(o,$),safeRemove(v);var C={};l.forEach(function(E){w[E]&&(C[E]=w[E])}),fetch("/api/v1/themes/"+$,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:T,colors:C})}).catch(function(){})}}function L(x){document.documentElement.classList.add("theme-transitioning"),m.forEach(function(g){g!=="dark"&&document.documentElement.classList.remove(g)}),c(),x!=="dark"&&document.documentElement.classList.add(x),safeSet(o,x);var T=k[x],$=document.querySelector('meta[name="theme-color"]');$&&T&&$.setAttribute("content",T.bg);var w=T&&T.lightBg;!w&&T&&T.bg&&(w=h(T.bg)>.4),w?document.documentElement.classList.add("light-bg"):document.documentElement.classList.remove("light-bg"),setTimeout(function(){document.documentElement.classList.remove("theme-transitioning")},300)}B(),r();var A=safeGet(o);A==="red"&&(A="black",safeSet(o,"black")),A&&A!=="dark"&&m.indexOf(A)===-1&&(A=null),L(A||n()),I(),window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",function(x){safeGet(o)||L(x.matches?"dark":"light")}),window.THEMES=m,window.BUILTIN_THEMES=f,window.THEME_COLORS=k,window.THEME_PROPS=l,window.BASE_PROPS=b,window.DERIVED_PROPS=a,window.USER_THEMES_KEY=i,window.applyTheme=L,window.clearCustomProperties=c,window.injectUserThemeStyles=r,window.syncThemesFromServer=I,window.slugifyThemeName=y,window.getActiveTheme=function(){return safeGet(o)||n()},window.deriveExtendedColors=e,window.hexToRgb=t,window.rgbToHex=d,window.blendColors=u})(),(function(){function o(){const b=document.querySelector(".totp-card");if(!b)return;const k=getComputedStyle(b).backgroundColor.match(/\d+/g);if(!k)return;const t=(.299*+k[0]+.587*+k[1]+.114*+k[2])/255,d=b.querySelector(".totp-logo-dark"),u=b.querySelector(".totp-logo-light");d&&(d.style.display=t>.5?"none":""),u&&(u.style.display=t>.5?"":"none")}function i(){const b=document.getElementById("totp-overlay");if(b){b.classList.add("show"),setTimeout(o,50);const a=b.querySelector(".totp-digits input");a&&setTimeout(()=>a.focus(),100)}}function v(){const b=document.getElementById("totp-overlay");b&&b.classList.remove("show")}const f=document.getElementById("totp-digits");if(f){const b=f.querySelectorAll("input");b.forEach((a,k)=>{a.addEventListener("input",t=>{const d=t.target.value.replace(/\D/g,"");t.target.value=d.slice(0,1),d&&kh.value).join("");u.length===6&&m(u)}),a.addEventListener("keydown",t=>{t.key==="Backspace"&&!t.target.value&&k>0&&(b[k-1].focus(),b[k-1].value="")}),a.addEventListener("paste",t=>{t.preventDefault();const d=(t.clipboardData.getData("text")||"").replace(/\D/g,"");d.length>=6&&(b.forEach((u,h)=>{u.value=d[h]||""}),b[5].focus(),m(d.slice(0,6)))})})}async function m(b){const a=document.getElementById("totp-error");a.textContent="Verifying...",a.className="totp-error verifying";try{const t=await(await secureFetch("/api/v1/totp/verify",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:b})})).json();if(t.success){a.textContent="",t.csrfToken&&(csrfToken=t.csrfToken),v();const d=safeSessionGet("totp_redirect");if(d){try{sessionStorage.removeItem("totp_redirect")}catch{}window.location.href=d;return}typeof window.initializeDashboard=="function"&&window.initializeDashboard()}else{a.textContent=t.error||"Invalid code",a.className="totp-error";const d=document.querySelectorAll("#totp-digits input");d.forEach(u=>{u.value=""}),d[0]?.focus()}}catch{a.textContent="Connection error",a.className="totp-error"}}const l=new URLSearchParams(window.location.search);if(l.get("auth")==="required"){const b=l.get("return");if(b)try{const a=new URL(b,window.location.origin),k=a.hostname,t=a.origin===window.location.origin,d=SITE.tld.startsWith(".")?SITE.tld:"."+SITE.tld,u=k.endsWith(d)||k===d.substring(1);(t||u)&&safeSessionSet("totp_redirect",b)}catch{}window.history.replaceState({},"",window.location.pathname)}window._showTotpOverlay=i})(),(function(){injectModal("folder-browser-modal",`
+`}function t(){return window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}function d(){i.forEach(function(w){document.documentElement.style.removeProperty("--"+w)})}function f(w,T){var $=w.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"");$||($="custom"),k.indexOf($)!==-1&&($=$+"-custom");for(var b=safeGetJSON(m,{}),y=$,I=2;b[$]&&$!==T;)$=y+"-"+I++;return $}function r(w){var T=document.getElementById("user-theme-styles");T&&T.remove(),v.length=k.length,Object.keys(x).forEach(function(E){k.indexOf(E)===-1&&delete x[E]});var $=w||safeGetJSON(m,{}),b=Object.keys($);if(b=b.filter(function(E){return k.indexOf(E)===-1}),!!b.length){var y="";b.forEach(function(E){var C=$[E];v.indexOf(E)===-1&&v.push(E);var P={};i.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])}),x[E]=P,y+=":root."+E+` { +`,i.forEach(function(O){P[O]&&(y+=" --"+O+": "+P[O]+`; +`)}),y+=`} +`,y+=s(E,P),y+=u(E,P)});var I=document.createElement("style");I.id="user-theme-styles",I.textContent=y,document.head.appendChild(I)}}function S(){secureFetch("/api/v1/themes").then(function(w){return w.json()}).then(function(w){if(!(!w.success||!w.themes)){var T=w.themes,$=safeGetJSON(m,{});if(JSON.stringify(T)!==JSON.stringify($)){safeSet(m,JSON.stringify(T)),r(T);var b=safeGet(o);b&&v.indexOf(b)!==-1&&L(b)}}}).catch(function(){})}function B(){var w=safeGetJSON(l);if(w){var T=w.name||"Custom",$=f(T),b={name:T};i.forEach(function(E){w[E]&&(b[E]=w[E])});var y=safeGetJSON(m,{});y[$]=b,safeSet(m,JSON.stringify(y)),safeGet(o)==="custom"&&safeSet(o,$),safeRemove(l);var I={};i.forEach(function(E){b[E]&&(I[E]=b[E])}),fetch("/api/v1/themes/"+$,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:T,colors:I})}).catch(function(){})}}function L(w){document.documentElement.classList.add("theme-transitioning"),v.forEach(function(y){y!=="dark"&&document.documentElement.classList.remove(y)}),d(),w!=="dark"&&document.documentElement.classList.add(w),safeSet(o,w);var T=x[w],$=document.querySelector('meta[name="theme-color"]');$&&T&&$.setAttribute("content",T.bg);var b=T&&T.lightBg;!b&&T&&T.bg&&(b=g(T.bg)>.4),b?document.documentElement.classList.add("light-bg"):document.documentElement.classList.remove("light-bg"),setTimeout(function(){document.documentElement.classList.remove("theme-transitioning")},300)}B(),r();var A=safeGet(o);A==="red"&&(A="black",safeSet(o,"black")),A&&A!=="dark"&&v.indexOf(A)===-1&&(A=null),L(A||t()),S(),window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",function(w){safeGet(o)||L(w.matches?"dark":"light")}),window.THEMES=v,window.BUILTIN_THEMES=k,window.THEME_COLORS=x,window.THEME_PROPS=i,window.BASE_PROPS=h,window.DERIVED_PROPS=n,window.USER_THEMES_KEY=m,window.applyTheme=L,window.clearCustomProperties=d,window.injectUserThemeStyles=r,window.syncThemesFromServer=S,window.slugifyThemeName=f,window.getActiveTheme=function(){return safeGet(o)||t()},window.deriveExtendedColors=e,window.hexToRgb=p,window.rgbToHex=a,window.blendColors=c})(),(function(){function o(){const h=document.querySelector(".totp-card");if(!h)return;const x=getComputedStyle(h).backgroundColor.match(/\d+/g);if(!x)return;const p=(.299*+x[0]+.587*+x[1]+.114*+x[2])/255,a=h.querySelector(".totp-logo-dark"),c=h.querySelector(".totp-logo-light");a&&(a.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 k=document.getElementById("totp-digits");if(k){const h=k.querySelectorAll("input");h.forEach((n,x)=>{n.addEventListener("input",p=>{const a=p.target.value.replace(/\D/g,"");p.target.value=a.slice(0,1),a&&xg.value).join("");c.length===6&&v(c)}),n.addEventListener("keydown",p=>{p.key==="Backspace"&&!p.target.value&&x>0&&(h[x-1].focus(),h[x-1].value="")}),n.addEventListener("paste",p=>{p.preventDefault();const a=(p.clipboardData.getData("text")||"").replace(/\D/g,"");a.length>=6&&(h.forEach((c,g)=>{c.value=a[g]||""}),h[5].focus(),v(a.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 a=safeSessionGet("totp_redirect");if(a){try{sessionStorage.removeItem("totp_redirect")}catch{}window.location.href=a;return}typeof window.initializeDashboard=="function"&&window.initializeDashboard()}else{n.textContent=p.error||"Invalid code",n.className="totp-error";const a=document.querySelectorAll("#totp-digits input");a.forEach(c=>{c.value=""}),a[0]?.focus()}}catch{n.textContent="Connection error",n.className="totp-error"}}const i=new URLSearchParams(window.location.search);if(i.get("auth")==="required"){const h=i.get("return");if(h)try{const n=new URL(h,window.location.origin),x=n.hostname,p=n.origin===window.location.origin,a=SITE.tld.startsWith(".")?SITE.tld:"."+SITE.tld,c=x.endsWith(a)||x===a.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",`

\u{1F4C2} Browse for Media Folders

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

Authentication Settings

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

\u{1F511} DNS Credentials

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

No DNS servers configured.

';return}for(const y of c)n.insertAdjacentHTML("beforeend",` + `);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",`
-

${i(y)}

+

${m(f)}

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

DNS Settings

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

Edit Service

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

Home Lab Configuration

-
TLD: ${u}
-
Certificate Authority: ${y}
-
DNS Server: ${c}:${o}
-
Example URLs: https://uptime${u}, https://nextcloud${u}
+
TLD: ${p}
+
Certificate Authority: ${g}
+
DNS Server: ${v}:${m}
+
Example URLs: https://uptime${p}, https://nextcloud${p}
- `}else if(w==="simple"){const u=document.getElementById("setup-simple-ip")?.value?.trim()||"localhost";T+=` + `}else if(k==="simple"){const p=document.getElementById("setup-simple-ip")?.value?.trim()||"localhost";H+=`

Simple Setup

Access Method: IP:Port only
-
Default IP: ${u}
+
Default IP: ${p}
SSL: None (HTTP only)
-
Example URLs: http://${u}:8080, http://${u}:3000
+
Example URLs: http://${p}:8080, http://${p}:3000
- `}else if(w==="public"){const u=document.getElementById("setup-public-domain")?.value?.trim()||"",y=document.getElementById("setup-public-email")?.value?.trim()||"",c=document.querySelector('input[name="routing-mode"]:checked')?.value||"subdirectory",o=c==="subdirectory"?`https://${u}/sonarr, https://${u}/grafana`:`https://sonarr.${u}, https://grafana.${u}`;T+=` + `}else if(k==="public"){const p=document.getElementById("setup-public-domain")?.value?.trim()||"",g=document.getElementById("setup-public-email")?.value?.trim()||"",v=document.querySelector('input[name="routing-mode"]:checked')?.value||"subdirectory",m=v==="subdirectory"?`https://${p}/sonarr, https://${p}/grafana`:`https://sonarr.${p}, https://grafana.${p}`;H+=`

Public Server

-
Domain: ${u}
+
Domain: ${p}
SSL: Let's Encrypt
-
Email: ${y}
-
Routing: ${c==="subdirectory"?"Subdirectory (domain.com/app)":"Subdomain (app.domain.com)"}
-
Example URLs: ${o}
+
Email: ${g}
+
Routing: ${v==="subdirectory"?"Subdirectory (domain.com/app)":"Subdomain (app.domain.com)"}
+
Example URLs: ${m}
- `}const b=document.getElementById("setup-timezone")?.value||Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC";T+=` + `}const f=document.getElementById("setup-timezone")?.value||Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC";H+=`
-
Timezone: ${b.replace(/_/g," ")}
+
Timezone: ${f.replace(/_/g," ")}
- `,T+="
",S.innerHTML=T,C("setup-step-summary")}async function $(S){try{const T=await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(S)});return T.ok?(await T.json(),!0):(console.error("Failed to save config to server:",T.status),!1)}catch(T){return console.error("Error saving config to server:",T),!1}}async function L(){const S={setupComplete:!0,configurationType:w,timestamp:new Date().toISOString(),timezone:document.getElementById("setup-timezone")?.value||Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC"};w==="homelab"?(S.tld=document.getElementById("setup-tld")?.value?.trim()||".home",S.caName=document.getElementById("setup-ca-name")?.value?.trim()||"",S.dns={provider:"technitium",ip:document.getElementById("setup-dns-ip")?.value?.trim()||"",port:document.getElementById("setup-dns-port")?.value?.trim()||DC.DEFAULTS.DNS_PORT,token:document.getElementById("setup-dns-token")?.value?.trim()||""},S.defaults={dnsType:"private",sslType:"internal",targetIP:"localhost"}):w==="simple"?(S.defaultIP=document.getElementById("setup-simple-ip")?.value?.trim()||"localhost",S.defaults={dnsType:"none",sslType:"none",targetIP:S.defaultIP}):w==="public"&&(S.domain=document.getElementById("setup-public-domain")?.value?.trim()||"",S.email=document.getElementById("setup-public-email")?.value?.trim()||"",S.routingMode=document.querySelector('input[name="routing-mode"]:checked')?.value||"subdirectory",S.defaults={dnsType:S.routingMode==="subdirectory"?"none":"public",sslType:"letsencrypt",targetIP:"localhost"});const T=await $(S);safeSet("dashcaddy-config",JSON.stringify(S)),safeSet("dashcaddy-setup","completed"),document.getElementById("setup-wizard").style.display="none";const b=w==="homelab"?"Professional Home Lab":w==="simple"?"Simple Setup":"Public Server",u=T?"server (shared across all devices)":"locally (this browser only)";showNotification(`Setup Complete! Configured for: ${b}. Settings saved to: ${u}`,"success",5e3),setTimeout(()=>location.reload(),500)}const g=document.getElementById("setup-step-1-next");g&&(g.onclick=function(S){S.preventDefault();const T=document.querySelector('input[name="config-type"]:checked');T&&(w=T.value),C(w==="homelab"?"setup-step-homelab":w==="simple"?"setup-step-simple":w==="public"?"setup-step-public":"setup-step-homelab")});const k=document.getElementById("setup-skip");k&&(k.onclick=async function(S){S.preventDefault(),confirm("Skip setup? You can run it later from Settings.")&&(await $({setupComplete:!0,skipped:!0,timestamp:new Date().toISOString()}),safeSet("dashcaddy-setup","skipped"),document.getElementById("setup-wizard").style.display="none")});const f=document.getElementById("setup-tld");f&&(f.oninput=function(S){const T=S.target.value||".home",b=document.getElementById("tld-preview"),u=document.getElementById("tld-preview-2");b&&(b.textContent=T),u&&(u.textContent=T)});const x=document.getElementById("setup-homelab-back");x&&(x.onclick=function(S){S.preventDefault(),C("setup-step-1")});const E=document.getElementById("setup-homelab-next");E&&(E.onclick=function(S){S.preventDefault();const T=document.getElementById("setup-tld")?.value?.trim()||"",b=document.getElementById("setup-ca-name")?.value?.trim()||"",u=document.getElementById("setup-dns-ip")?.value?.trim()||"";if(!T||!T.startsWith(".")){showNotification("Please enter a valid TLD starting with a dot (e.g., .home)","warning");return}if(!b){showNotification("Please enter a Certificate Authority name","warning");return}if(!u){showNotification("Please enter your DNS server IP address","warning");return}P()});const R=document.getElementById("setup-simple-back");R&&(R.onclick=function(S){S.preventDefault(),C("setup-step-1")});const O=document.getElementById("setup-simple-next");O&&(O.onclick=function(S){S.preventDefault(),P()}),document.querySelectorAll('input[name="routing-mode"]').forEach(function(S){S.onchange=function(){var T=document.getElementById("dns-requirement-note");T&&(T.textContent=this.value==="subdirectory"?"Only one DNS record needed (for the main domain)":"You'll need to configure DNS manually for each subdomain")}});const D=document.getElementById("setup-public-back");D&&(D.onclick=function(S){S.preventDefault(),C("setup-step-1")});const N=document.getElementById("setup-public-next");N&&(N.onclick=function(S){S.preventDefault();const T=document.getElementById("setup-public-domain")?.value?.trim()||"",b=document.getElementById("setup-public-email")?.value?.trim()||"";if(!T){showNotification("Please enter your domain name","warning");return}if(!b||!b.includes("@")){showNotification("Please enter a valid email address","warning");return}P()});const M=document.getElementById("setup-summary-back");M&&(M.onclick=function(S){S.preventDefault(),w==="homelab"?C("setup-step-homelab"):w==="simple"?C("setup-step-simple"):w==="public"&&C("setup-step-public")});const H=document.getElementById("setup-finish");H&&(H.onclick=function(S){S.preventDefault(),L()}),window.getGlobalConfig=async function(){try{const T=await fetch("/api/v1/config");if(T.ok){const b=await T.json();if(b&&b.setupComplete)return b}}catch{console.warn("Could not fetch config from server")}const S=safeGet("dashcaddy-config");return S?JSON.parse(S):{setupComplete:!1,configurationType:"homelab",tld:".home",caName:"",defaults:{dnsType:"private",sslType:"internal",targetIP:"localhost"}}},window.resetSetupWizard=async function(){if(confirm("Reset DashCaddy configuration? This will show the setup wizard again.")){try{await secureFetch("/api/v1/config",{method:"DELETE"})}catch{console.warn("Could not delete server config")}safeRemove("dashcaddy-setup"),safeRemove("dashcaddy-config"),location.reload()}}})(),(function(){injectModal("app-selector-modal",`
+ `,H+="
",b.innerHTML=H,L("setup-step-summary")}async function T(b){try{const H=await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(b)});return H.ok?(await H.json(),!0):(errorHandler.logError("[SetupWizard] Save Config",new Error(`Server returned ${H.status}`),{function:"saveConfigToServer"}),!1)}catch(H){return errorHandler.logError("[SetupWizard] Save Config",H,{function:"saveConfigToServer"}),!1}}async function $(){const b={setupComplete:!0,configurationType:k,timestamp:new Date().toISOString(),timezone:document.getElementById("setup-timezone")?.value||Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC"};k==="homelab"?(b.tld=document.getElementById("setup-tld")?.value?.trim()||".home",b.caName=document.getElementById("setup-ca-name")?.value?.trim()||"",b.dns={provider:"technitium",ip:document.getElementById("setup-dns-ip")?.value?.trim()||"",port:document.getElementById("setup-dns-port")?.value?.trim()||DC.DEFAULTS.DNS_PORT,token:document.getElementById("setup-dns-token")?.value?.trim()||""},b.defaults={dnsType:"private",sslType:"internal",targetIP:"localhost"}):k==="simple"?(b.defaultIP=document.getElementById("setup-simple-ip")?.value?.trim()||"localhost",b.defaults={dnsType:"none",sslType:"none",targetIP:b.defaultIP}):k==="public"&&(b.domain=document.getElementById("setup-public-domain")?.value?.trim()||"",b.email=document.getElementById("setup-public-email")?.value?.trim()||"",b.routingMode=document.querySelector('input[name="routing-mode"]:checked')?.value||"subdirectory",b.defaults={dnsType:b.routingMode==="subdirectory"?"none":"public",sslType:"letsencrypt",targetIP:"localhost"});const H=await T(b);safeSet("dashcaddy-config",JSON.stringify(b)),safeSet("dashcaddy-setup","completed"),document.getElementById("setup-wizard").style.display="none";const f=k==="homelab"?"Professional Home Lab":k==="simple"?"Simple Setup":"Public Server",p=H?"server (shared across all devices)":"locally (this browser only)";showNotification(`Setup Complete! Configured for: ${f}. Settings saved to: ${p}`,"success",5e3),setTimeout(()=>location.reload(),500)}const y=document.getElementById("setup-step-1-next");y&&(y.onclick=function(b){b.preventDefault();const H=document.querySelector('input[name="config-type"]:checked');H&&(k=H.value),L(k==="homelab"?"setup-step-homelab":k==="simple"?"setup-step-simple":k==="public"?"setup-step-public":"setup-step-homelab")});const S=document.getElementById("setup-skip");S&&(S.onclick=async function(b){b.preventDefault(),confirm("Skip setup? You can run it later from Settings.")&&(await T({setupComplete:!0,skipped:!0,timestamp:new Date().toISOString()}),safeSet("dashcaddy-setup","skipped"),document.getElementById("setup-wizard").style.display="none")});const w=document.getElementById("setup-tld");w&&(w.oninput=function(b){const H=b.target.value||".home",f=document.getElementById("tld-preview"),p=document.getElementById("tld-preview-2");f&&(f.textContent=H),p&&(p.textContent=H)});const x=document.getElementById("setup-homelab-back");x&&(x.onclick=function(b){b.preventDefault(),L("setup-step-1")});const C=document.getElementById("setup-homelab-next");C&&(C.onclick=function(b){b.preventDefault();const H=document.getElementById("setup-tld")?.value?.trim()||"",f=document.getElementById("setup-ca-name")?.value?.trim()||"",p=document.getElementById("setup-dns-ip")?.value?.trim()||"";if(!H||!H.startsWith(".")){showNotification("Please enter a valid TLD starting with a dot (e.g., .home)","warning");return}if(!f){showNotification("Please enter a Certificate Authority name","warning");return}if(!p){showNotification("Please enter your DNS server IP address","warning");return}A()});const M=document.getElementById("setup-simple-back");M&&(M.onclick=function(b){b.preventDefault(),L("setup-step-1")});const R=document.getElementById("setup-simple-next");R&&(R.onclick=function(b){b.preventDefault(),A()}),document.querySelectorAll('input[name="routing-mode"]').forEach(function(b){b.onchange=function(){var H=document.getElementById("dns-requirement-note");H&&(H.textContent=this.value==="subdirectory"?"Only one DNS record needed (for the main domain)":"You'll need to configure DNS manually for each subdomain")}});const D=document.getElementById("setup-public-back");D&&(D.onclick=function(b){b.preventDefault(),L("setup-step-1")});const j=document.getElementById("setup-public-next");j&&(j.onclick=function(b){b.preventDefault();const H=document.getElementById("setup-public-domain")?.value?.trim()||"",f=document.getElementById("setup-public-email")?.value?.trim()||"";if(!H){showNotification("Please enter your domain name","warning");return}if(!f||!f.includes("@")){showNotification("Please enter a valid email address","warning");return}A()});const B=document.getElementById("setup-summary-back");B&&(B.onclick=function(b){b.preventDefault(),k==="homelab"?L("setup-step-homelab"):k==="simple"?L("setup-step-simple"):k==="public"&&L("setup-step-public")});const P=document.getElementById("setup-finish");P&&(P.onclick=function(b){b.preventDefault(),$()}),window.getGlobalConfig=async function(){try{const H=await fetch("/api/v1/config");if(H.ok){const f=await H.json();if(f&&f.setupComplete)return f}}catch{console.warn("Could not fetch config from server")}const b=safeGet("dashcaddy-config");return b?JSON.parse(b):{setupComplete:!1,configurationType:"homelab",tld:".home",caName:"",defaults:{dnsType:"private",sslType:"internal",targetIP:"localhost"}}},window.resetSetupWizard=async function(){if(confirm("Reset DashCaddy configuration? This will show the setup wizard again.")){try{await secureFetch("/api/v1/config",{method:"DELETE"})}catch{console.warn("Could not delete server config")}safeRemove("dashcaddy-setup"),safeRemove("dashcaddy-config"),location.reload()}}})(),(function(){const k=new ErrorHandler;injectModal("app-selector-modal",`

Choose an App

@@ -333,12 +333,12 @@ This will reset the logo, favicon, title, and position.`))try{if((await secureFe
-
`);const w="custom-apps";let B=null,z=null;const I=document.getElementById("app-selector-modal"),C=document.getElementById("app-selector-grid");async function P(){try{const l=await(await fetch("/api/v1/apps/templates")).json();if(l.success)return B=l.templates,z=l.categories,!0}catch(o){console.error("Failed to fetch app templates:",o)}return!1}async function $(o){try{return await(await fetch(`/api/v1/apps/ports/${o}/check`)).json()}catch(l){return console.error("Failed to check port:",l),{available:!0}}}async function L(o){try{const v=await(await fetch(`/api/v1/apps/ports/${o}/suggest`)).json();if(v.success)return v.suggestedPort}catch(l){console.error("Failed to get suggested port:",l)}return o}async function g(){if(C.innerHTML='
Loading app templates...
',!B&&!await P()){C.innerHTML='
Failed to load app templates. Please try again.
';return}C.innerHTML="";const o={};for(const[v,r]of Object.entries(B)){const p=r.category||"Other";o[p]||(o[p]=[]),o[p].push({id:v,...r})}const l=z?Object.keys(z):Object.keys(o).sort();for(const v of l){const r=o[v];if(!r||r.length===0)continue;r.sort((n,e)=>(e.popularity||0)-(n.popularity||0));const p=document.createElement("div");p.className="app-category-header";const m=z?.[v]||{};p.innerHTML=`${escapeHtml(m.icon||"")} ${escapeHtml(v)}`,m.color&&(p.style.borderBottomColor=m.color),C.appendChild(p),r.forEach(n=>{const e=document.createElement("div");e.className="app-option";const a=n.isDashboardWidget,t=a&&safeGet("widget-"+n.id+"-enabled")!=="false",s=a?`
${t?"ON":"OFF"}
`:"",i=!a&&n.difficulty?`
${escapeHtml(n.difficulty)}
`:"";e.innerHTML=` -
${escapeHtml(n.icon||"\u{1F4E6}")}
-
${escapeHtml(n.name)}
-
${escapeHtml(n.description||"")}
- ${s}${i} - `,a?e.onclick=()=>k(n,e):e.onclick=()=>f(n),C.appendChild(e)})}window.renderRecipeCards&&await window.renderRecipeCards(C)}function k(o,l){const v="widget-"+o.id+"-enabled",p=!(safeGet(v)!=="false");safeSet(v,String(p));const m=o.widgetSelector;if(m){const e=document.querySelector(m);e&&(e.style.display=p?"":"none")}const n=l.querySelector('div[style*="border-radius: 4px"]');n&&(n.textContent=p?"ON":"OFF",n.style.background=p?"#2ecc7130":"#e74c3c30",n.style.color=p?"#2ecc71":"#e74c3c"),showNotification(`${o.name} widget ${p?"enabled":"disabled"}`,"success",2e3)}async function f(o){const l=document.getElementById("app-deploy-modal"),v=document.getElementById("app-deploy-title"),r=document.getElementById("deploy-subdomain"),p=document.getElementById("deploy-url-preview"),m=document.getElementById("deploy-ip"),n=document.getElementById("deploy-port"),e=document.getElementById("deploy-tailscale-only"),a=document.getElementById("tailscale-status");try{const J=await(await secureFetch("/api/v1/apps/check-existing",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({appId:o.id})})).json();if(J.success&&J.exists){const V=J.container;confirm(`Found existing ${o.name} container: + `);const E="custom-apps";let z=null,I=null;const L=document.getElementById("app-selector-modal"),A=document.getElementById("app-selector-grid");async function T(){try{const c=await(await fetch("/api/v1/apps/templates")).json();if(c.success)return z=c.templates,I=c.categories,!0}catch(r){k.logError("[AppSelector] Fetch Templates",r,{function:"fetchApiTemplates"})}return!1}async function $(r){try{return await(await fetch(`/api/v1/apps/ports/${r}/check`)).json()}catch(c){return k.logError("[AppSelector] Check Port",c,{function:"checkPortAvailability"}),{available:!0}}}async function y(r){try{const s=await(await fetch(`/api/v1/apps/ports/${r}/suggest`)).json();if(s.success)return s.suggestedPort}catch(c){k.logError("[AppSelector] Get Suggested Port",c,{function:"getSuggestedPort"})}return r}async function S(){if(A.innerHTML='
Loading app templates...
',!z&&!await T()){A.innerHTML='
Failed to load app templates. Please try again.
';return}A.innerHTML="";const r={};for(const[s,h]of Object.entries(z)){const u=h.category||"Other";r[u]||(r[u]=[]),r[u].push({id:s,...h})}const c=I?Object.keys(I):Object.keys(r).sort();for(const s of c){const h=r[s];if(!h||h.length===0)continue;h.sort((e,n)=>(n.popularity||0)-(e.popularity||0));const u=document.createElement("div");u.className="app-category-header";const a=I?.[s]||{};u.innerHTML=`${escapeHtml(a.icon||"")} ${escapeHtml(s)}`,a.color&&(u.style.borderBottomColor=a.color),A.appendChild(u),h.forEach(e=>{const n=document.createElement("div");n.className="app-option";const t=e.isDashboardWidget,i=t&&safeGet("widget-"+e.id+"-enabled")!=="false",o=t?`
${i?"ON":"OFF"}
`:"",d=!t&&e.difficulty?`
${escapeHtml(e.difficulty)}
`:"";n.innerHTML=` +
${escapeHtml(e.icon||"\u{1F4E6}")}
+
${escapeHtml(e.name)}
+
${escapeHtml(e.description||"")}
+ ${o}${d} + `,t?n.onclick=()=>w(e,n):n.onclick=()=>x(e),A.appendChild(n)})}window.renderRecipeCards&&await window.renderRecipeCards(A)}function w(r,c){const s="widget-"+r.id+"-enabled",u=!(safeGet(s)!=="false");safeSet(s,String(u));const a=r.widgetSelector;if(a){const n=document.querySelector(a);n&&(n.style.display=u?"":"none")}const e=c.querySelector('div[style*="border-radius: 4px"]');e&&(e.textContent=u?"ON":"OFF",e.style.background=u?"#2ecc7130":"#e74c3c30",e.style.color=u?"#2ecc71":"#e74c3c"),showNotification(`${r.name} widget ${u?"enabled":"disabled"}`,"success",2e3)}async function x(r){const c=document.getElementById("app-deploy-modal"),s=document.getElementById("app-deploy-title"),h=document.getElementById("deploy-subdomain"),u=document.getElementById("deploy-url-preview"),a=document.getElementById("deploy-ip"),e=document.getElementById("deploy-port"),n=document.getElementById("deploy-tailscale-only"),t=document.getElementById("tailscale-status");try{const J=await(await secureFetch("/api/v1/apps/check-existing",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({appId:r.id})})).json();if(J.success&&J.exists){const V=J.container;confirm(`Found existing ${r.name} container: Container: ${V.name} Status: ${V.status} @@ -347,38 +347,38 @@ Port: ${V.primaryPort||"N/A"} Would you like to use this existing container? Click OK to configure DNS/Caddy for the existing container. -Click Cancel to deploy a new container.`)&&(o._useExisting=!0,o._existingContainer=V)}}catch{}v.textContent=`Deploy ${o.name}`;const t=o.subdomain||o.id.replace(/-/g,"");r.value=t;const s=document.getElementById("subpath-compat-warning");if(s)if(SITE.routingMode==="subdirectory"){const q=o.subpathSupport||"strip";q==="none"?(s.style.display="block",s.innerHTML=''+o.name+" does not support subdirectory mode. It may not work correctly at a subpath."):q==="strip"?(s.style.display="block",s.innerHTML='ⓘ '+o.name+" has unverified subdirectory support. It may require additional configuration."):s.style.display="none"}else s.style.display="none";const i=SITE.defaults.dnsType||(SITE.configurationType==="public"?"public":"private"),h=SITE.defaults.sslType||(SITE.configurationType==="public"?"letsencrypt":"internal"),d=document.querySelector(`input[name="dns-type"][value="${i}"]`),A=document.querySelector(`input[name="ssl-type"][value="${h}"]`);d?d.checked=!0:document.querySelector('input[name="dns-type"][value="private"]').checked=!0,A?A.checked=!0:document.querySelector('input[name="ssl-type"][value="internal"]').checked=!0,m.value=SITE.defaults.targetIP||"localhost",e.checked=!1;const F=document.querySelector("#app-deploy-modal .flex-col-gap")?.closest("div"),j=document.querySelector("#app-deploy-modal details"),_=j?.querySelector("div");if(j&&_&&(SITE.configurationType==="public"||SITE.configurationType==="homelab")){const q=document.querySelectorAll('#app-deploy-modal input[name="dns-type"]')[0]?.closest("div.flex-col-gap")?.parentElement,J=document.querySelectorAll('#app-deploy-modal input[name="ssl-type"]')[0]?.closest("div.flex-col-gap")?.parentElement;q&&!q.dataset.moved&&(_.appendChild(q),q.dataset.moved="1"),J&&!J.dataset.moved&&(_.appendChild(J),J.dataset.moved="1")}const U=document.getElementById("media-path-section"),W=document.getElementById("deploy-media-path"),Z=document.getElementById("media-path-description");if(o.mediaMount){U.style.display="block",W.value="",W.placeholder="/media/Movies, /media/TVShows or click Browse";const q=document.getElementById("detected-mounts-container"),J=document.getElementById("detected-mounts-list");try{const K=await(await fetch("/api/v1/media/detected-mounts")).json();if(K.success&&K.mounts.length>0){q.style.display="block",J.innerHTML="";const te=[...new Set(K.mounts.map(ee=>ee.hostPath))];W.value=te.join(", "),K.mounts.forEach(ee=>{const Y=document.createElement("button");Y.type="button";const le=te.includes(ee.hostPath);Y.style.cssText=`padding: 8px 14px; font-size: 0.85rem; background: color-mix(in srgb, var(--success) ${le?"40%":"15%"}, var(--card-bg)); border: 1px solid var(--success); border-radius: 6px; cursor: pointer; color: var(--fg);`,Y.innerHTML=`${escapeHtml(ee.folderName)}
from ${escapeHtml(ee.sourceImage)}`,Y.title=`${ee.hostPath} (from ${ee.sourceContainer})`,Y.onclick=()=>{const re=W.value.split(",").map(de=>de.trim()).filter(de=>de),ce=re.indexOf(ee.hostPath);ce>=0?(re.splice(ce,1),Y.style.background="color-mix(in srgb, var(--success) 15%, var(--card-bg))"):(re.push(ee.hostPath),Y.style.background="color-mix(in srgb, var(--success) 40%, var(--card-bg))"),W.value=re.join(", ")},J.appendChild(Y)})}else q.style.display="none"}catch{q.style.display="none"}document.getElementById("browse-media-btn").onclick=()=>{openFolderBrowser(W)}}else U.style.display="none",W.value="",document.getElementById("detected-mounts-container").style.display="none";const G=document.getElementById("plex-claim-section");G&&(o.id==="plex"||o.claimToken?(G.style.display="block",document.getElementById("deploy-plex-claim").value=""):G.style.display="none");const Q=document.getElementById("volume-mounts-section"),ae=document.getElementById("volume-mounts-list");if(ae.innerHTML="",o.docker?.volumes?.length){const q=o.mediaMount?.containerPath,J=o.docker.volumes.filter(V=>!V.includes("{{MEDIA_PATH}}")&&!(q&&V.endsWith(":"+q)));J.length>0?(Q.style.display="block",J.forEach((V,K)=>{const[te,ee]=V.split(":"),Y=document.createElement("div");Y.style.cssText="display: flex; gap: 6px; align-items: center;",Y.innerHTML=` +Click Cancel to deploy a new container.`)&&(r._useExisting=!0,r._existingContainer=V)}}catch{}s.textContent=`Deploy ${r.name}`;const i=r.subdomain||r.id.replace(/-/g,"");h.value=i;const o=document.getElementById("subpath-compat-warning");if(o)if(SITE.routingMode==="subdirectory"){const _=r.subpathSupport||"strip";_==="none"?(o.style.display="block",o.innerHTML=''+r.name+" does not support subdirectory mode. It may not work correctly at a subpath."):_==="strip"?(o.style.display="block",o.innerHTML='ⓘ '+r.name+" has unverified subdirectory support. It may require additional configuration."):o.style.display="none"}else o.style.display="none";const d=SITE.defaults.dnsType||(SITE.configurationType==="public"?"public":"private"),l=SITE.defaults.sslType||(SITE.configurationType==="public"?"letsencrypt":"internal"),N=document.querySelector(`input[name="dns-type"][value="${d}"]`),O=document.querySelector(`input[name="ssl-type"][value="${l}"]`);N?N.checked=!0:document.querySelector('input[name="dns-type"][value="private"]').checked=!0,O?O.checked=!0:document.querySelector('input[name="ssl-type"][value="internal"]').checked=!0,a.value=SITE.defaults.targetIP||"localhost",n.checked=!1;const F=document.querySelector("#app-deploy-modal .flex-col-gap")?.closest("div"),q=document.querySelector("#app-deploy-modal details"),U=q?.querySelector("div");if(q&&U&&(SITE.configurationType==="public"||SITE.configurationType==="homelab")){const _=document.querySelectorAll('#app-deploy-modal input[name="dns-type"]')[0]?.closest("div.flex-col-gap")?.parentElement,J=document.querySelectorAll('#app-deploy-modal input[name="ssl-type"]')[0]?.closest("div.flex-col-gap")?.parentElement;_&&!_.dataset.moved&&(U.appendChild(_),_.dataset.moved="1"),J&&!J.dataset.moved&&(U.appendChild(J),J.dataset.moved="1")}const G=document.getElementById("media-path-section"),W=document.getElementById("deploy-media-path"),X=document.getElementById("media-path-description");if(r.mediaMount){G.style.display="block",W.value="",W.placeholder="/media/Movies, /media/TVShows or click Browse";const _=document.getElementById("detected-mounts-container"),J=document.getElementById("detected-mounts-list");try{const K=await(await fetch("/api/v1/media/detected-mounts")).json();if(K.success&&K.mounts.length>0){_.style.display="block",J.innerHTML="";const te=[...new Set(K.mounts.map(ee=>ee.hostPath))];W.value=te.join(", "),K.mounts.forEach(ee=>{const Z=document.createElement("button");Z.type="button";const de=te.includes(ee.hostPath);Z.style.cssText=`padding: 8px 14px; font-size: 0.85rem; background: color-mix(in srgb, var(--success) ${de?"40%":"15%"}, var(--card-bg)); border: 1px solid var(--success); border-radius: 6px; cursor: pointer; color: var(--fg);`,Z.innerHTML=`${escapeHtml(ee.folderName)}
from ${escapeHtml(ee.sourceImage)}`,Z.title=`${ee.hostPath} (from ${ee.sourceContainer})`,Z.onclick=()=>{const le=W.value.split(",").map(ce=>ce.trim()).filter(ce=>ce),pe=le.indexOf(ee.hostPath);pe>=0?(le.splice(pe,1),Z.style.background="color-mix(in srgb, var(--success) 15%, var(--card-bg))"):(le.push(ee.hostPath),Z.style.background="color-mix(in srgb, var(--success) 40%, var(--card-bg))"),W.value=le.join(", ")},J.appendChild(Z)})}else _.style.display="none"}catch{_.style.display="none"}document.getElementById("browse-media-btn").onclick=()=>{openFolderBrowser(W)}}else G.style.display="none",W.value="",document.getElementById("detected-mounts-container").style.display="none";const Q=document.getElementById("plex-claim-section");Q&&(r.id==="plex"||r.claimToken?(Q.style.display="block",document.getElementById("deploy-plex-claim").value=""):Q.style.display="none");const ne=document.getElementById("volume-mounts-section"),oe=document.getElementById("volume-mounts-list");if(oe.innerHTML="",r.docker?.volumes?.length){const _=r.mediaMount?.containerPath,J=r.docker.volumes.filter(V=>!V.includes("{{MEDIA_PATH}}")&&!(_&&V.endsWith(":"+_)));J.length>0?(ne.style.display="block",J.forEach((V,K)=>{const[te,ee]=V.split(":"),Z=document.createElement("div");Z.style.cssText="display: flex; gap: 6px; align-items: center;",Z.innerHTML=` \u2192 ${ee} - `,ae.appendChild(Y),Y.querySelector(".vol-browse-btn").onclick=()=>{const le=Y.querySelector(".vol-host-path");openFolderBrowser(le)}})):Q.style.display="none"}else Q.style.display="none";const ne=o.defaultPort||8080;n.value="",n.placeholder=`Default: ${ne}`;let X=document.getElementById("deploy-port-status");X||(X=document.createElement("div"),X.id="deploy-port-status",X.style.cssText="font-size: 0.8rem; margin-top: 4px;",n.parentNode.appendChild(X));async function se(){const q=n.value||ne;X.innerHTML='Checking port...';const J=await $(q);if(J.available)X.innerHTML=`Port ${escapeHtml(String(q))} is available`;else{const V=await L(ne);X.innerHTML=` - Port ${escapeHtml(q)} in use by ${escapeHtml(J.conflict?.usedBy||"unknown")} - `;const K=document.createElement("button");K.type="button",K.textContent=`Use ${V}`,K.style.cssText="margin-left: 8px; padding: 2px 8px; font-size: 0.75rem; cursor: pointer;",K.onclick=()=>{document.getElementById("deploy-port").value=V,X.innerHTML=`Using suggested port ${escapeHtml(String(V))}`},X.appendChild(K)}}let ie;n.oninput=function(){clearTimeout(ie),ie=setTimeout(se,500)},se();try{const J=await(await fetch("/api/v1/tailscale/status")).json();J.success&&J.installed&&J.connected?a.innerHTML=` + `,oe.appendChild(Z),Z.querySelector(".vol-browse-btn").onclick=()=>{const de=Z.querySelector(".vol-host-path");openFolderBrowser(de)}})):ne.style.display="none"}else ne.style.display="none";const se=r.defaultPort||8080;e.value="",e.placeholder=`Default: ${se}`;let Y=document.getElementById("deploy-port-status");Y||(Y=document.createElement("div"),Y.id="deploy-port-status",Y.style.cssText="font-size: 0.8rem; margin-top: 4px;",e.parentNode.appendChild(Y));async function ie(){const _=e.value||se;Y.innerHTML='Checking port...';const J=await $(_);if(J.available)Y.innerHTML=`Port ${escapeHtml(String(_))} is available`;else{const V=await y(se);Y.innerHTML=` + Port ${escapeHtml(_)} in use by ${escapeHtml(J.conflict?.usedBy||"unknown")} + `;const K=document.createElement("button");K.type="button",K.textContent=`Use ${V}`,K.style.cssText="margin-left: 8px; padding: 2px 8px; font-size: 0.75rem; cursor: pointer;",K.onclick=()=>{document.getElementById("deploy-port").value=V,Y.innerHTML=`Using suggested port ${escapeHtml(String(V))}`},Y.appendChild(K)}}let re;e.oninput=function(){clearTimeout(re),re=setTimeout(ie,500)},ie();try{const J=await(await fetch("/api/v1/tailscale/status")).json();J.success&&J.installed&&J.connected?t.innerHTML=` Connected ${J.self?.hostname} (${J.self?.ip}) | ${J.deviceCount} devices - `:J.installed?a.innerHTML='Not connected':(a.innerHTML='Not available',e.disabled=!0)}catch{a.innerHTML='Could not check status'}function oe(){const q=r.value||"subdomain",J=document.querySelector('input[name="dns-type"]:checked').value,V=document.querySelector('input[name="ssl-type"]:checked').value;let K="";if(SITE.routingMode==="subdirectory"&&SITE.domain)K=`https://${SITE.domain}/${q}`;else if(J==="private")K=`${V==="none"?"http":"https"}://${buildDomain(q)}`;else if(J==="public"){const te=V==="none"?"http":"https",ee=SITE.domain||q;K=SITE.domain?`${te}://${q}.${SITE.domain}`:`${te}://${q}`}else{const te=n.value||o.defaultPort||DC.DEFAULTS.SERVICE_PORT;K=`http://${m.value}:${te}`}p.textContent=K}r.oninput=oe,m.oninput=oe,n.oninput=oe,document.querySelectorAll('input[name="dns-type"]').forEach(q=>{q.onchange=oe}),document.querySelectorAll('input[name="ssl-type"]').forEach(q=>{q.onchange=oe}),oe(),I.classList.remove("show"),l.classList.add("show"),l.dataset.appTemplate=JSON.stringify(o)}async function x(o){const l=o.appTemplate,v=safeGetJSON(w,[]),r=l._useExisting&&l._existingContainer,p=v.find(m=>m.id===o.subdomain);if(!(p&&!r&&!confirm(`An app with subdomain "${o.subdomain}" already exists. Redeploy?`))){if(p){const m=v.indexOf(p);v.splice(m,1),safeSet(w,JSON.stringify(v))}if(r)o.port=l._existingContainer.primaryPort;else{const m=o.port||l.defaultPort||8080;showNotification(`Checking port ${m} availability...`,"info",0);const n=await $(m);if(!n.available){const e=await L(l.defaultPort||8080);if(confirm(`Port ${m} is already in use by ${n.conflict?.usedBy||"another container"}. + `:J.installed?t.innerHTML='Not connected':(t.innerHTML='Not available',n.disabled=!0)}catch{t.innerHTML='Could not check status'}function ae(){const _=h.value||"subdomain",J=document.querySelector('input[name="dns-type"]:checked').value,V=document.querySelector('input[name="ssl-type"]:checked').value;let K="";if(SITE.routingMode==="subdirectory"&&SITE.domain)K=`https://${SITE.domain}/${_}`;else if(J==="private")K=`${V==="none"?"http":"https"}://${buildDomain(_)}`;else if(J==="public"){const te=V==="none"?"http":"https",ee=SITE.domain||_;K=SITE.domain?`${te}://${_}.${SITE.domain}`:`${te}://${_}`}else{const te=e.value||r.defaultPort||DC.DEFAULTS.SERVICE_PORT;K=`http://${a.value}:${te}`}u.textContent=K}h.oninput=ae,a.oninput=ae,e.oninput=ae,document.querySelectorAll('input[name="dns-type"]').forEach(_=>{_.onchange=ae}),document.querySelectorAll('input[name="ssl-type"]').forEach(_=>{_.onchange=ae}),ae(),L.classList.remove("show"),c.classList.add("show"),c.dataset.appTemplate=JSON.stringify(r)}async function C(r){const c=r.appTemplate,s=safeGetJSON(E,[]),h=c._useExisting&&c._existingContainer,u=s.find(a=>a.id===r.subdomain);if(!(u&&!h&&!confirm(`An app with subdomain "${r.subdomain}" already exists. Redeploy?`))){if(u){const a=s.indexOf(u);s.splice(a,1),safeSet(E,JSON.stringify(s))}if(h)r.port=c._existingContainer.primaryPort;else{const a=r.port||c.defaultPort||8080;showNotification(`Checking port ${a} availability...`,"info",0);const e=await $(a);if(!e.available){const n=await y(c.defaultPort||8080);if(confirm(`Port ${a} is already in use by ${e.conflict?.usedBy||"another container"}. -Would you like to use port ${e} instead?`))o.port=e;else{showNotification("Deployment cancelled - port conflict","error",5e3);return}}}showNotification(r?`Configuring ${l.name} with existing container...`:`Deploying ${l.name}...`,"info",0);try{const m={appId:l.id,config:{subdomain:o.subdomain,ip:o.ip,createDns:o.dnsType==="private",port:o.port||l.defaultPort||null,sslType:o.sslType,dnsType:o.dnsType,tailscaleOnly:o.tailscaleOnly||!1,mediaPath:o.mediaPath||null,plexClaimToken:o.plexClaimToken||null,customVolumes:o.customVolumes||null}};r&&(m.config.useExisting=!0,m.config.existingContainerId=l._existingContainer.id,m.config.existingPort=l._existingContainer.primaryPort,!o.port&&l._existingContainer.primaryPort&&(m.config.port=l._existingContainer.primaryPort));const e=await(await secureFetch("/api/v1/apps/deploy",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(m)})).json();if(e.success){const a={id:o.subdomain,name:l.name,logo:`/assets/${l.id}.png`,containerId:e.containerId,url:e.url,ip:o.ip,appTemplate:l.id,tailscaleOnly:o.tailscaleOnly||!1};v.push(a),safeSet(w,JSON.stringify(v)),window.APPS&&!window.APPS.some(s=>s.id===l.id)&&(window.APPS.push(a),typeof window.buildGrid=="function"&&window.buildGrid(),typeof window.refreshAll=="function"&&setTimeout(()=>window.refreshAll(),500));let t=e.usedExisting?`${l.name} configured with existing container! -URL: ${e.url}`:`${l.name} deployed successfully! -URL: ${e.url}`;e.warning&&(t+=` +Would you like to use port ${n} instead?`))r.port=n;else{showNotification("Deployment cancelled - port conflict","error",5e3);return}}}showNotification(h?`Configuring ${c.name} with existing container...`:`Deploying ${c.name}...`,"info",0);try{const a={appId:c.id,config:{subdomain:r.subdomain,ip:r.ip,createDns:r.dnsType==="private",port:r.port||c.defaultPort||null,sslType:r.sslType,dnsType:r.dnsType,tailscaleOnly:r.tailscaleOnly||!1,mediaPath:r.mediaPath||null,plexClaimToken:r.plexClaimToken||null,customVolumes:r.customVolumes||null}};h&&(a.config.useExisting=!0,a.config.existingContainerId=c._existingContainer.id,a.config.existingPort=c._existingContainer.primaryPort,!r.port&&c._existingContainer.primaryPort&&(a.config.port=c._existingContainer.primaryPort));const n=await(await secureFetch("/api/v1/apps/deploy",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(a)})).json();if(n.success){const t={id:r.subdomain,name:c.name,logo:`/assets/${c.id}.png`,containerId:n.containerId,url:n.url,ip:r.ip,appTemplate:c.id,tailscaleOnly:r.tailscaleOnly||!1};s.push(t),safeSet(E,JSON.stringify(s)),window.APPS&&!window.APPS.some(o=>o.id===c.id)&&(window.APPS.push(t),typeof window.buildGrid=="function"&&window.buildGrid(),typeof window.refreshAll=="function"&&setTimeout(()=>window.refreshAll(),500));let i=n.usedExisting?`${c.name} configured with existing container! +URL: ${n.url}`:`${c.name} deployed successfully! +URL: ${n.url}`;n.warning&&(i+=` -\u26A0 Warning: ${e.warning}`),showNotification(t,"success",8e3),delete l._useExisting,delete l._existingContainer,e.url&&e.url.startsWith("https://")&&E(e.url,l.name),e.setupInstructions&&e.setupInstructions.length>0&&setTimeout(()=>{const s=e.setupInstructions.join(` -`);showNotification(`Setup Instructions for ${l.name}: ${s}`,"info",1e4)},1e3)}else throw new Error(e.error||"Deployment failed")}catch(m){console.error("Deployment error:",m),showNotification(`Failed to deploy ${l.name}: ${m.message}`,"error",8e3)}}}async function E(o,l){showNotification(`\u23F3 Generating SSL certificate for ${l}...`,"warning",6e4);let v=0;const r=12,p=async()=>{v++;try{const m=await fetch(o,{method:"HEAD",mode:"no-cors"});return showNotification(`\u2705 ${l} is ready! SSL certificate generated.`,"success",5e3),!0}catch{return v{window.APPS.some(v=>v.id===l.id)||window.APPS.push(l)})}document.getElementById("add-service-btn")?.addEventListener("click",()=>{g(),I.classList.add("show")}),wireModal(I,document.getElementById("app-selector-cancel"));const O=document.getElementById("app-deploy-modal");document.getElementById("app-deploy-cancel")?.addEventListener("click",()=>{O.classList.remove("show")}),document.getElementById("app-deploy-confirm")?.addEventListener("click",()=>{const o=JSON.parse(O.dataset.appTemplate),l=document.getElementById("deploy-media-path").value.trim(),v=[];document.querySelectorAll("#volume-mounts-list .vol-host-path").forEach(p=>{v.push({hostPath:p.value.trim(),containerPath:p.dataset.containerPath})});const r={appTemplate:o,subdomain:document.getElementById("deploy-subdomain").value.trim(),dnsType:document.querySelector('input[name="dns-type"]:checked').value,sslType:document.querySelector('input[name="ssl-type"]:checked').value,ip:document.getElementById("deploy-ip").value.trim(),port:document.getElementById("deploy-port").value.trim(),tailscaleOnly:document.getElementById("deploy-tailscale-only").checked,mediaPath:l||null,plexClaimToken:document.getElementById("deploy-plex-claim")?.value.trim()||null,customVolumes:v.length>0?v:null,resources:{cpus:parseFloat(document.getElementById("deploy-cpu-limit").value)||0,memory:parseFloat(document.getElementById("deploy-memory-limit").value)||0}};if(!r.subdomain){showNotification("Please enter a subdomain or domain name","warning");return}if(o.mediaMount?.required&&!l){showNotification("Please enter a media library path for this application","warning");return}O.classList.remove("show"),x(r)}),wireModal(O);const D=document.getElementById("folder-browser-modal"),N=document.getElementById("folder-browser-path"),M=document.getElementById("folder-browser-list"),H=document.getElementById("folder-browser-selected"),S=document.getElementById("folder-browser-selected-list");let T="",b=[],u=null;window.openFolderBrowser=function(o){u=o,b=o.value.split(",").map(l=>l.trim()).filter(l=>l),T="",c(),y(""),D.classList.add("show")};async function y(o){N.textContent=o||"Select a drive...",M.innerHTML='
Loading...
';try{const v=await(await fetch(`/api/v1/browse/directories?path=${encodeURIComponent(o)}`)).json();if(!v.success){M.innerHTML=`
Error: ${escapeHtml(v.error)}
`;return}T=v.path||"",N.textContent=T||"Select a drive...";let r="";v.parent&&v.parent!==v.path&&(r+=`
+\u26A0 Warning: ${n.warning}`),showNotification(i,"success",8e3),delete c._useExisting,delete c._existingContainer,n.url&&n.url.startsWith("https://")&&M(n.url,c.name),n.setupInstructions&&n.setupInstructions.length>0&&setTimeout(()=>{const o=n.setupInstructions.join(` +`);showNotification(`Setup Instructions for ${c.name}: ${o}`,"info",1e4)},1e3)}else throw new Error(n.error||"Deployment failed")}catch(a){k.logError("[AppSelector] Deployment",a,{function:"deploy"}),showNotification(`Failed to deploy ${c.name}: ${a.message}`,"error",8e3)}}}async function M(r,c){showNotification(`\u23F3 Generating SSL certificate for ${c}...`,"warning",6e4);let s=0;const h=12,u=async()=>{s++;try{const a=await fetch(r,{method:"HEAD",mode:"no-cors"});return showNotification(`\u2705 ${c} is ready! SSL certificate generated.`,"success",5e3),!0}catch{return s{window.APPS.some(s=>s.id===c.id)||window.APPS.push(c)})}document.getElementById("add-service-btn")?.addEventListener("click",()=>{S(),L.classList.add("show")}),wireModal(L,document.getElementById("app-selector-cancel"));const D=document.getElementById("app-deploy-modal");document.getElementById("app-deploy-cancel")?.addEventListener("click",()=>{D.classList.remove("show")}),document.getElementById("app-deploy-confirm")?.addEventListener("click",()=>{const r=JSON.parse(D.dataset.appTemplate),c=document.getElementById("deploy-media-path").value.trim(),s=[];document.querySelectorAll("#volume-mounts-list .vol-host-path").forEach(u=>{s.push({hostPath:u.value.trim(),containerPath:u.dataset.containerPath})});const h={appTemplate:r,subdomain:document.getElementById("deploy-subdomain").value.trim(),dnsType:document.querySelector('input[name="dns-type"]:checked').value,sslType:document.querySelector('input[name="ssl-type"]:checked').value,ip:document.getElementById("deploy-ip").value.trim(),port:document.getElementById("deploy-port").value.trim(),tailscaleOnly:document.getElementById("deploy-tailscale-only").checked,mediaPath:c||null,plexClaimToken:document.getElementById("deploy-plex-claim")?.value.trim()||null,customVolumes:s.length>0?s:null,resources:{cpus:parseFloat(document.getElementById("deploy-cpu-limit").value)||0,memory:parseFloat(document.getElementById("deploy-memory-limit").value)||0}};if(!h.subdomain){showNotification("Please enter a subdomain or domain name","warning");return}if(r.mediaMount?.required&&!c){showNotification("Please enter a media library path for this application","warning");return}D.classList.remove("show"),C(h)}),wireModal(D);const j=document.getElementById("folder-browser-modal"),B=document.getElementById("folder-browser-path"),P=document.getElementById("folder-browser-list"),b=document.getElementById("folder-browser-selected"),H=document.getElementById("folder-browser-selected-list");let f="",p=[],g=null;window.openFolderBrowser=function(r){g=r,p=r.value.split(",").map(c=>c.trim()).filter(c=>c),f="",m(),v(""),j.classList.add("show")};async function v(r){B.textContent=r||"Select a drive...",P.innerHTML='
Loading...
';try{const s=await(await fetch(`/api/v1/browse/directories?path=${encodeURIComponent(r)}`)).json();if(!s.success){P.innerHTML=`
Error: ${escapeHtml(s.error)}
`;return}f=s.path||"",B.textContent=f||"Select a drive...";let h="";s.parent&&s.parent!==s.path&&(h+=`
\u2B06\uFE0F .. Parent Directory -
`),v.items.length===0&&!v.parent?r+='
No browseable drives configured. Check your docker-compose.yml volume mounts.
':v.items.length===0?r+='
No subfolders found
':v.items.forEach(p=>{const m=p.type==="drive"?"\u{1F4BE}":"\u{1F4C1}",n=b.includes(p.path),e=n?"background: color-mix(in srgb, var(--success) 20%, transparent);":"";r+=`
- ${m} - ${escapeHtml(p.name)} - ${n?'\u2713':""} -
`}),M.innerHTML=r,M.querySelectorAll(".folder-item").forEach(p=>{p.addEventListener("click",()=>{y(p.dataset.path)}),p.addEventListener("mouseenter",()=>{p.style.background="var(--card-bg)"}),p.addEventListener("mouseleave",()=>{const m=b.includes(p.dataset.path);p.style.background=m?"color-mix(in srgb, var(--success) 20%, transparent)":""})})}catch(l){M.innerHTML=`
Failed to load: ${escapeHtml(l.message)}
`}}function c(){if(b.length===0){H.style.display="none";return}H.style.display="block",S.innerHTML=b.map(o=>` +
`),s.items.length===0&&!s.parent?h+='
No browseable drives configured. Check your docker-compose.yml volume mounts.
':s.items.length===0?h+='
No subfolders found
':s.items.forEach(u=>{const a=u.type==="drive"?"\u{1F4BE}":"\u{1F4C1}",e=p.includes(u.path),n=e?"background: color-mix(in srgb, var(--success) 20%, transparent);":"";h+=`
+ ${a} + ${escapeHtml(u.name)} + ${e?'\u2713':""} +
`}),P.innerHTML=h,P.querySelectorAll(".folder-item").forEach(u=>{u.addEventListener("click",()=>{v(u.dataset.path)}),u.addEventListener("mouseenter",()=>{u.style.background="var(--card-bg)"}),u.addEventListener("mouseleave",()=>{const a=p.includes(u.dataset.path);u.style.background=a?"color-mix(in srgb, var(--success) 20%, transparent)":""})})}catch(c){P.innerHTML=`
Failed to load: ${escapeHtml(c.message)}
`}}function m(){if(p.length===0){b.style.display="none";return}b.style.display="block",H.innerHTML=p.map(r=>` - ${escapeHtml(o)} - + ${escapeHtml(r)} + - `).join("")}window.removeSelectedFolder=function(o){b=b.filter(l=>l!==o),c(),y(T)},document.getElementById("folder-browser-select-current").addEventListener("click",()=>{T&&!b.includes(T)&&(b.push(T),c(),y(T))}),wireModal(D,document.getElementById("folder-browser-cancel")),document.getElementById("folder-browser-done").addEventListener("click",()=>{u&&(u.value=b.join(", ")),D.classList.remove("show")}),R()})(),(function(){injectModal("recipe-deploy-modal",`
+ `).join("")}window.removeSelectedFolder=function(r){p=p.filter(c=>c!==r),m(),v(f)},document.getElementById("folder-browser-select-current").addEventListener("click",()=>{f&&!p.includes(f)&&(p.push(f),m(),v(f))}),wireModal(j,document.getElementById("folder-browser-cancel")),document.getElementById("folder-browser-done").addEventListener("click",()=>{g&&(g.value=p.join(", ")),j.classList.remove("show")}),R()})(),(function(){injectModal("recipe-deploy-modal",`

Deploy Recipe

@@ -445,70 +445,70 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};
-
`);let w=null,B=null,z=null,I=1,C=!1;const P=document.getElementById("recipe-deploy-modal"),$=document.getElementById("recipe-cancel"),L=document.getElementById("recipe-prev"),g=document.getElementById("recipe-next");wireModal(P,$);async function k(){try{const b=await fetch("/api/v1/recipes/templates"),u=await b.json();if(u.success)return w=u.templates,B=u.categories,!0;if(b.status===403)return C=!1,!1}catch(b){console.warn("Failed to fetch recipe templates:",b.message)}return!1}async function f(){try{C=(await(await fetch("/api/v1/license/feature/recipes")).json()).available}catch{C=!1}return C}window.renderRecipeCards=async function(b){await f();let u;if(C&&w?u=w:u=x(),!u||u.length===0)return;const y=document.createElement("div");y.className="app-category-header",y.innerHTML="\u{1F9EA} Recipes",y.style.borderBottomColor="#8e44ad",b.appendChild(y);const c=Array.isArray(u)?u:Object.values(u);c.sort((o,l)=>(l.popularity||0)-(o.popularity||0));for(const o of c){const l=document.createElement("div");l.className="app-option",l.style.position="relative";const v=`
${o.componentCount||o.components?.length||"?"} apps
`,r=C?"":'
PREMIUM
';l.innerHTML=` - ${r} -
${escapeHtml(o.icon||"\u{1F9EA}")}
-
${escapeHtml(o.name)}
-
${escapeHtml(o.description||"")}
- ${v} - `,l.onclick=()=>{if(!C){showNotification("Recipes require a DashCaddy Premium license. Click the License button to activate.","warning",5e3),window.openLicenseModal&&window.openLicenseModal();return}E(o)},b.appendChild(l)}};function x(){return[{id:"htpc-suite",name:"HTPC Suite",icon:"\u{1F3AC}",description:"Complete media automation: find, download, organize, and stream",componentCount:6,popularity:98},{id:"nextcloud-complete",name:"Nextcloud Complete",icon:"\u2601\uFE0F",description:"Full productivity suite: cloud storage, office editing, and collaboration",componentCount:4,popularity:90},{id:"smart-home",name:"Smart Home Hub",icon:"\u{1F3E0}",description:"Home automation: control, automate, and monitor IoT devices",componentCount:4,popularity:88},{id:"dev-environment",name:"Dev Environment",icon:"\u{1F4BB}",description:"Self-hosted development workflow: Git, CI/CD, IDE, and database",componentCount:4,popularity:82}]}function E(b){z=b,I=1;const u=document.getElementById("app-selector-modal");u&&u.classList.remove("show"),document.getElementById("recipe-deploy-title").textContent=`Deploy ${b.name}`,R(),O(),P.classList.add("show")}function R(){document.querySelectorAll("#recipe-steps .recipe-step").forEach(b=>{const u=parseInt(b.dataset.step);b.classList.toggle("active",u===I),b.classList.toggle("completed",u1&&I<4?"":"none",I===4?(g.style.display="none",$.textContent="Close"):I===3?(g.textContent="\u{1F680} Deploy",g.style.display="",$.textContent="Cancel"):(g.textContent="Next",g.style.display="",$.textContent="Cancel")}function O(){const b=document.getElementById("recipe-component-list");b.innerHTML="";const u=z.components||[];for(const y of u){const c=document.createElement("div");c.style.cssText="display: flex; align-items: center; gap: 12px; padding: 12px; border-radius: 8px; background: var(--card-bg); border: 1px solid var(--border);";const o=y.required,l=y.internal;c.innerHTML=` - `);let k=null,E=null,z=null,I=1,L=!1;const A=document.getElementById("recipe-deploy-modal"),T=document.getElementById("recipe-cancel"),$=document.getElementById("recipe-prev"),y=document.getElementById("recipe-next");wireModal(A,T);async function S(){try{const f=await fetch("/api/v1/recipes/templates"),p=await f.json();if(p.success)return k=p.templates,E=p.categories,!0;if(f.status===403)return L=!1,!1}catch(f){console.warn("Failed to fetch recipe templates:",f.message)}return!1}async function w(){try{L=(await(await fetch("/api/v1/license/feature/recipes")).json()).available}catch{L=!1}return L}window.renderRecipeCards=async function(f){await w();let p;if(L&&k?p=k:p=x(),!p||p.length===0)return;const g=document.createElement("div");g.className="app-category-header",g.innerHTML="\u{1F9EA} Recipes",g.style.borderBottomColor="#8e44ad",f.appendChild(g);const v=Array.isArray(p)?p:Object.values(p);v.sort((m,r)=>(r.popularity||0)-(m.popularity||0));for(const m of v){const r=document.createElement("div");r.className="app-option",r.style.position="relative";const c=`
${m.componentCount||m.components?.length||"?"} apps
`,s=L?"":'
PREMIUM
';r.innerHTML=` + ${s} +
${escapeHtml(m.icon||"\u{1F9EA}")}
+
${escapeHtml(m.name)}
+
${escapeHtml(m.description||"")}
+ ${c} + `,r.onclick=()=>{if(!L){showNotification("Recipes require a DashCaddy Premium license. Click the License button to activate.","warning",5e3),window.openLicenseModal&&window.openLicenseModal();return}C(m)},f.appendChild(r)}};function x(){return[{id:"htpc-suite",name:"HTPC Suite",icon:"\u{1F3AC}",description:"Complete media automation: find, download, organize, and stream",componentCount:6,popularity:98},{id:"nextcloud-complete",name:"Nextcloud Complete",icon:"\u2601\uFE0F",description:"Full productivity suite: cloud storage, office editing, and collaboration",componentCount:4,popularity:90},{id:"smart-home",name:"Smart Home Hub",icon:"\u{1F3E0}",description:"Home automation: control, automate, and monitor IoT devices",componentCount:4,popularity:88},{id:"dev-environment",name:"Dev Environment",icon:"\u{1F4BB}",description:"Self-hosted development workflow: Git, CI/CD, IDE, and database",componentCount:4,popularity:82}]}function C(f){z=f,I=1;const p=document.getElementById("app-selector-modal");p&&p.classList.remove("show"),document.getElementById("recipe-deploy-title").textContent=`Deploy ${f.name}`,M(),R(),A.classList.add("show")}function M(){document.querySelectorAll("#recipe-steps .recipe-step").forEach(f=>{const p=parseInt(f.dataset.step);f.classList.toggle("active",p===I),f.classList.toggle("completed",p1&&I<4?"":"none",I===4?(y.style.display="none",T.textContent="Close"):I===3?(y.textContent="\u{1F680} Deploy",y.style.display="",T.textContent="Cancel"):(y.textContent="Next",y.style.display="",T.textContent="Cancel")}function R(){const f=document.getElementById("recipe-component-list");f.innerHTML="";const p=z.components||[];for(const g of p){const v=document.createElement("div");v.style.cssText="display: flex; align-items: center; gap: 12px; padding: 12px; border-radius: 8px; background: var(--card-bg); border: 1px solid var(--border);";const m=g.required,r=g.internal;v.innerHTML=` +
-
${escapeHtml(y.role||y.id)}
+
${escapeHtml(g.role||g.id)}
- ${y.templateRef?escapeHtml(y.templateRef):"Built-in"} - ${o?'Required':'Optional'} - ${l?'(Internal)':""} + ${g.templateRef?escapeHtml(g.templateRef):"Built-in"} + ${m?'Required':'Optional'} + ${r?'(Internal)':""}
- ${y.note?`
\u26A0 ${escapeHtml(y.note)}
`:""} + ${g.note?`
\u26A0 ${escapeHtml(g.note)}
`:""}
- `,b.appendChild(c)}}function D(){const b=document.getElementById("recipe-volumes-section"),u=document.getElementById("recipe-volume-list"),y=z.sharedVolumes;if(y&&Object.keys(y).length>0){b.style.display="",u.innerHTML="";for(const[c,o]of Object.entries(y)){const l=document.createElement("div");l.style.cssText="display: grid; gap: 4px;",l.innerHTML=` - - 0){f.style.display="",p.innerHTML="";for(const[v,m]of Object.entries(g)){const r=document.createElement("div");r.style.cssText="display: grid; gap: 4px;",r.innerHTML=` + + -
${escapeHtml(o.description||"")}
- `,u.appendChild(l)}}else b.style.display="none"}function N(){const b=document.getElementById("recipe-review-content"),u=M(),y=document.querySelectorAll("#recipe-volume-list input[data-volume-key]"),c={};y.forEach(r=>{c[r.dataset.volumeKey]=r.value});const o=document.getElementById("recipe-timezone").value||"UTC",l=document.getElementById("recipe-ip").value||"host.docker.internal",v=document.getElementById("recipe-tailscale").checked;b.innerHTML=` +
${escapeHtml(m.description||"")}
+ `,p.appendChild(r)}}else f.style.display="none"}function j(){const f=document.getElementById("recipe-review-content"),p=B(),g=document.querySelectorAll("#recipe-volume-list input[data-volume-key]"),v={};g.forEach(s=>{v[s.dataset.volumeKey]=s.value});const m=document.getElementById("recipe-timezone").value||"UTC",r=document.getElementById("recipe-ip").value||"host.docker.internal",c=document.getElementById("recipe-tailscale").checked;f.innerHTML=`
${escapeHtml(z.name)}
${escapeHtml(z.description||"")}
- Components (${u.length}): + Components (${p.length}):
- ${u.map(r=>`
- \u2022 ${escapeHtml(r.role||r.id)} ${r.internal?'(internal)':""} + ${p.map(s=>`
+ \u2022 ${escapeHtml(s.role||s.id)} ${s.internal?'(internal)':""}
`).join("")}
- ${Object.keys(c).length>0?`
+ ${Object.keys(v).length>0?`
Volumes: - ${Object.entries(c).map(([r,p])=>`
${r}: ${escapeHtml(p)}
`).join("")} + ${Object.entries(v).map(([s,h])=>`
${s}: ${escapeHtml(h)}
`).join("")}
`:""}
- Timezone: ${escapeHtml(o)} • IP: ${escapeHtml(l)} ${v?"• Tailscale only":""} + Timezone: ${escapeHtml(m)} • IP: ${escapeHtml(r)} ${c?"• Tailscale only":""}
${z.network?`
Docker network: ${escapeHtml(z.network.name)}
`:""} - `}function M(){const b=document.querySelectorAll("#recipe-component-list input[data-component-id]"),u=new Set;b.forEach(c=>{c.checked&&u.add(c.dataset.componentId)});const y=z.components||[];return y.filter(c=>c.required).forEach(c=>u.add(c.id)),y.filter(c=>u.has(c.id))}async function H(){const b=document.getElementById("recipe-progress-list"),u=document.getElementById("recipe-deploy-result");u.style.display="none",b.innerHTML="";const y=M();for(const v of y){const r=document.createElement("div");r.id=`recipe-progress-${v.id}`,r.style.cssText="display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 6px; background: var(--card-bg); border: 1px solid var(--border); font-size: 0.85rem;",r.innerHTML=` + `}function B(){const f=document.querySelectorAll("#recipe-component-list input[data-component-id]"),p=new Set;f.forEach(v=>{v.checked&&p.add(v.dataset.componentId)});const g=z.components||[];return g.filter(v=>v.required).forEach(v=>p.add(v.id)),g.filter(v=>p.has(v.id))}async function P(){const f=document.getElementById("recipe-progress-list"),p=document.getElementById("recipe-deploy-result");p.style.display="none",f.innerHTML="";const g=B();for(const c of g){const s=document.createElement("div");s.id=`recipe-progress-${c.id}`,s.style.cssText="display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 6px; background: var(--card-bg); border: 1px solid var(--border); font-size: 0.85rem;",s.innerHTML=` \u23F3 - ${escapeHtml(v.role||v.id)} + ${escapeHtml(c.role||c.id)} Queued - `,b.appendChild(r)}const c=document.querySelectorAll("#recipe-volume-list input[data-volume-key]"),o={};c.forEach(v=>{o[v.dataset.volumeKey]=v.value});const l={selectedComponents:y.map(v=>v.id),sharedConfig:{ip:document.getElementById("recipe-ip").value||"host.docker.internal",timezone:document.getElementById("recipe-timezone").value||"UTC",tailscaleOnly:document.getElementById("recipe-tailscale").checked,volumes:o},componentOverrides:{}};for(const v of y)S(v.id,"deploying","Deploying...");try{const r=await(await secureFetch("/api/v1/recipes/deploy",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({recipeId:z.id,config:l})})).json();if(r.success){for(const p of r.deployed||[])S(p.id,"success",p.url?`Running \u2192 ${p.url}`:"Running");for(const p of r.errors||[])S(p.componentId,"error",p.error);u.style.display="",u.innerHTML=` + `,f.appendChild(s)}const v=document.querySelectorAll("#recipe-volume-list input[data-volume-key]"),m={};v.forEach(c=>{m[c.dataset.volumeKey]=c.value});const r={selectedComponents:g.map(c=>c.id),sharedConfig:{ip:document.getElementById("recipe-ip").value||"host.docker.internal",timezone:document.getElementById("recipe-timezone").value||"UTC",tailscaleOnly:document.getElementById("recipe-tailscale").checked,volumes:m},componentOverrides:{}};for(const c of g)b(c.id,"deploying","Deploying...");try{const s=await(await secureFetch("/api/v1/recipes/deploy",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({recipeId:z.id,config:r})})).json();if(s.success){for(const h of s.deployed||[])b(h.id,"success",h.url?`Running \u2192 ${h.url}`:"Running");for(const h of s.errors||[])b(h.componentId,"error",h.error);p.style.display="",p.innerHTML=`
-
${escapeHtml(r.message||"Deployed!")}
- ${r.setupInstructions?`
+
${escapeHtml(s.message||"Deployed!")}
+ ${s.setupInstructions?`
Setup tips: -
    ${r.setupInstructions.map(p=>`
  • ${escapeHtml(p)}
  • `).join("")}
+
    ${s.setupInstructions.map(h=>`
  • ${escapeHtml(h)}
  • `).join("")}
`:""}
- `,showNotification(`${z.name} recipe deployed successfully!`,"success",5e3),window.loadServices&&window.loadServices()}else u.style.display="",u.innerHTML=`
- Deployment failed: ${escapeHtml(r.error||"Unknown error")} -
`,showNotification(`Recipe deployment failed: ${r.error}`,"error",5e3)}catch(v){u.style.display="",u.innerHTML=`
- Network error: ${escapeHtml(v.message)} -
`}}function S(b,u,y){const c=document.getElementById(`recipe-progress-${b}`);if(!c)return;const o=c.querySelector(".recipe-progress-icon"),l=c.querySelector(".recipe-progress-status");u==="deploying"?(o.textContent="\u23F3",l.style.color="var(--accent)"):u==="success"?(o.textContent="\u2705",l.style.color="var(--ok-fg)"):u==="error"&&(o.textContent="\u274C",l.style.color="var(--bad-fg)"),l.textContent=y}g.addEventListener("click",()=>{if(I===3){I=4,R(),H();return}I<3&&(I++,R(),I===2&&D(),I===3&&N())}),L.addEventListener("click",()=>{I>1&&I<4&&(I--,R())}),window.groupRecipeCards=function(){const b=document.querySelectorAll(".service-card[data-recipe-id]");if(b.length===0)return;const u={};b.forEach(y=>{const c=y.dataset.recipeId;u[c]||(u[c]=[]),u[c].push(y)});for(const[y,c]of Object.entries(u))c.length<2||c.forEach((o,l)=>{if(o.style.borderLeft="3px solid rgba(142,68,173,0.5)",l===0){let v=o.querySelector(".recipe-group-label");v||(v=document.createElement("div"),v.className="recipe-group-label",v.style.cssText="position: absolute; top: -8px; left: 12px; font-size: 0.6rem; padding: 1px 8px; border-radius: 8px; background: rgba(142,68,173,0.3); color: #d4a5ff; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;",v.textContent=y.replace(/-/g," "),o.style.position="relative",o.appendChild(v))}})},window.manageRecipe=async function(b,u){const y=`/api/v1/recipes/${b}/${u}`,c=u==="remove"?"DELETE":"POST",o=u==="remove"?`/api/v1/recipes/${b}`:y;if(!(u==="remove"&&!confirm(`Remove the entire ${b} recipe? This will delete all containers and configuration.`)))try{const v=await(await secureFetch(o,{method:c})).json();v.success?(showNotification(`Recipe ${u}: ${v.results?.filter(r=>r.status!=="failed").length||0} components processed`,"success",4e3),window.loadServices&&window.loadServices()):showNotification(`Recipe ${u} failed: ${v.error}`,"error",5e3)}catch(l){showNotification(`Network error: ${l.message}`,"error",5e3)}};const T=document.createElement("style");T.textContent=` + `,showNotification(`${z.name} recipe deployed successfully!`,"success",5e3),window.loadServices&&window.loadServices()}else p.style.display="",p.innerHTML=`
+ Deployment failed: ${escapeHtml(s.error||"Unknown error")} +
`,showNotification(`Recipe deployment failed: ${s.error}`,"error",5e3)}catch(c){p.style.display="",p.innerHTML=`
+ Network error: ${escapeHtml(c.message)} +
`}}function b(f,p,g){const v=document.getElementById(`recipe-progress-${f}`);if(!v)return;const m=v.querySelector(".recipe-progress-icon"),r=v.querySelector(".recipe-progress-status");p==="deploying"?(m.textContent="\u23F3",r.style.color="var(--accent)"):p==="success"?(m.textContent="\u2705",r.style.color="var(--ok-fg)"):p==="error"&&(m.textContent="\u274C",r.style.color="var(--bad-fg)"),r.textContent=g}y.addEventListener("click",()=>{if(I===3){I=4,M(),P();return}I<3&&(I++,M(),I===2&&D(),I===3&&j())}),$.addEventListener("click",()=>{I>1&&I<4&&(I--,M())}),window.groupRecipeCards=function(){const f=document.querySelectorAll(".service-card[data-recipe-id]");if(f.length===0)return;const p={};f.forEach(g=>{const v=g.dataset.recipeId;p[v]||(p[v]=[]),p[v].push(g)});for(const[g,v]of Object.entries(p))v.length<2||v.forEach((m,r)=>{if(m.style.borderLeft="3px solid rgba(142,68,173,0.5)",r===0){let c=m.querySelector(".recipe-group-label");c||(c=document.createElement("div"),c.className="recipe-group-label",c.style.cssText="position: absolute; top: -8px; left: 12px; font-size: 0.6rem; padding: 1px 8px; border-radius: 8px; background: rgba(142,68,173,0.3); color: #d4a5ff; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;",c.textContent=g.replace(/-/g," "),m.style.position="relative",m.appendChild(c))}})},window.manageRecipe=async function(f,p){const g=`/api/v1/recipes/${f}/${p}`,v=p==="remove"?"DELETE":"POST",m=p==="remove"?`/api/v1/recipes/${f}`:g;if(!(p==="remove"&&!confirm(`Remove the entire ${f} recipe? This will delete all containers and configuration.`)))try{const c=await(await secureFetch(m,{method:v})).json();c.success?(showNotification(`Recipe ${p}: ${c.results?.filter(s=>s.status!=="failed").length||0} components processed`,"success",4e3),window.loadServices&&window.loadServices()):showNotification(`Recipe ${p} failed: ${c.error}`,"error",5e3)}catch(r){showNotification(`Network error: ${r.message}`,"error",5e3)}};const H=document.createElement("style");H.textContent=` .recipe-step { flex: 1; text-align: center; @@ -550,16 +550,73 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}}; .recipe-step-panel { min-height: 180px; } - `,document.head.appendChild(T),f()})(),(function(){document.getElementById("reload-caddy-top")?.addEventListener("click",async()=>{const w=document.getElementById("reload-caddy-top"),B=w.textContent;try{w.textContent="\u23F3 Reloading...",w.disabled=!0;const z=await secureFetch("/api/v1/caddy/reload",{method:"POST",headers:{"Content-Type":"application/json"}}),I=await z.json();if(z.ok&&I.success)w.textContent="\u2705 Reloaded!",setTimeout(()=>{w.textContent=B,w.disabled=!1},2e3);else throw new Error(I.error||"Reload failed")}catch(z){w.textContent="\u274C Failed",showNotification(`Failed to reload Caddy: ${z.message}`,"error"),setTimeout(()=>{w.textContent=B,w.disabled=!1},2e3)}})})(),(function(){injectModal("error-log-modal",'

\u{1F4CB} Error Logs

Loading error logs...
');const w=document.getElementById("error-log-modal"),B=document.getElementById("error-log-content"),z=document.getElementById("view-error-logs"),I=document.getElementById("error-log-refresh"),C=document.getElementById("error-log-clear"),P=document.getElementById("error-log-close");async function $(){B.innerHTML='
Loading error logs...
';try{const k=await(await fetch("/api/v1/error-logs")).json();k.success&&k.logs?k.logs.length===0?B.innerHTML='
\u2705 No errors logged! Everything is working smoothly.
':B.innerHTML=k.logs.map(f=>` + `,document.head.appendChild(H),w()})(),(function(){document.getElementById("reload-caddy-top")?.addEventListener("click",async()=>{const k=document.getElementById("reload-caddy-top"),E=k.textContent;try{k.textContent="\u23F3 Reloading...",k.disabled=!0;const z=await secureFetch("/api/v1/caddy/reload",{method:"POST",headers:{"Content-Type":"application/json"}}),I=await z.json();if(z.ok&&I.success)k.textContent="\u2705 Reloaded!",setTimeout(()=>{k.textContent=E,k.disabled=!1},2e3);else throw new Error(I.error||"Reload failed")}catch(z){k.textContent="\u274C Failed",showNotification(`Failed to reload Caddy: ${z.message}`,"error"),setTimeout(()=>{k.textContent=E,k.disabled=!1},2e3)}})})(),(function(){injectModal("error-log-modal",'

\u{1F4CB} Error Logs

Loading error logs...
');const k=document.getElementById("error-log-modal"),E=document.getElementById("error-log-content"),z=document.getElementById("view-error-logs"),I=document.getElementById("error-log-refresh"),L=document.getElementById("error-log-clear"),A=document.getElementById("error-log-close");async function T(){E.innerHTML='
Loading error logs...
';try{const S=await(await fetch("/api/v1/error-logs")).json();S.success&&S.logs?S.logs.length===0?E.innerHTML='
\u2705 No errors logged! Everything is working smoothly.
':E.innerHTML=S.logs.map(w=>`
- ${new Date(f.timestamp).toLocaleString()} + ${new Date(w.timestamp).toLocaleString()} ERROR
- ${escapeHtml(f.context)}: ${escapeHtml(f.error)} - ${f.details?`
${escapeHtml(f.details)}`:""} + ${escapeHtml(w.context)}: ${escapeHtml(w.error)} + ${w.details?`
${escapeHtml(w.details)}`:""}
- `).join(""):B.innerHTML='
\u274C Failed to load error logs
'}catch(g){B.innerHTML=`
\u274C Error loading logs: ${escapeHtml(g.message)}
`}}async function L(){if(confirm("Clear all error logs?"))try{(await(await secureFetch("/api/v1/error-logs",{method:"DELETE"})).json()).success?(showNotification("\u2705 Error logs cleared","success",3e3),$()):showNotification("\u274C Failed to clear logs","error",3e3)}catch(g){showNotification(`\u274C Error: ${g.message}`,"error",3e3)}}z?.addEventListener("click",()=>{w.classList.add("show"),$()}),I?.addEventListener("click",$),C?.addEventListener("click",L),wireModal(w,P)})(),(function(){injectModal("arr-setup-modal",`
+ `).join(""):E.innerHTML='
\u274C Failed to load error logs
'}catch(y){E.innerHTML=`
\u274C Error loading logs: ${escapeHtml(y.message)}
`}}async function $(){if(confirm("Clear all error logs?"))try{(await(await secureFetch("/api/v1/error-logs",{method:"DELETE"})).json()).success?(showNotification("\u2705 Error logs cleared","success",3e3),T()):showNotification("\u274C Failed to clear logs","error",3e3)}catch(y){showNotification(`\u274C Error: ${y.message}`,"error",3e3)}}z?.addEventListener("click",()=>{k.classList.add("show"),T()}),I?.addEventListener("click",T),L?.addEventListener("click",$),wireModal(k,A)})(),(function(){injectModal("container-logs-modal",`
+
+
+
+

\u{1F4DC} Container Logs

+ +
+
+ + + + + + + + +
+
+ +
+ Image: - + Status: - + Created: - +
+ +
+
Select a container to view logs
+
+ + + + +
+
`);const k=document.getElementById("container-logs-modal"),E=document.getElementById("cl-container-select"),z=document.getElementById("cl-log-content"),I=document.getElementById("cl-log-search"),L=document.getElementById("cl-log-tail"),A=document.getElementById("cl-refresh"),T=document.getElementById("cl-stream"),$=document.getElementById("cl-download"),y=document.getElementById("cl-clear-search"),S=document.getElementById("cl-close"),w=document.getElementById("cl-close-btn"),x=document.getElementById("cl-stream-status"),C=document.getElementById("cl-stream-indicator"),M=document.getElementById("cl-stream-text"),R=document.getElementById("cl-line-count"),D=document.getElementById("cl-filter-count"),j=document.getElementById("cl-image"),B=document.getElementById("cl-status"),P=document.getElementById("cl-created");let b=null,H=[],f=[],p=null,g=!1,v=null;function m(d){if(!d)return"-";const l=new Date(d);return isNaN(l.getTime())?d:l.toLocaleString()}function r(d){if(!d)return"";const l=document.createElement("div");return l.textContent=d,l.innerHTML}function c(d,l){const N=d.stream==="stderr"?"log-stderr":"log-stdout",O=d.stream==="stderr"?"\u26A0\uFE0F":"\u{1F4E4}";return` +
+ ${l+1} + ${O} + ${r(d.text)} +
+ `}function s(d,l=""){if(!d||d.length===0){z.innerHTML='
No logs available
',R.textContent="0 lines",D.textContent="0 filtered";return}if(H=d,f=l?d.filter(N=>N.text&&N.text.toLowerCase().includes(l.toLowerCase())):d,R.textContent=`${d.length} lines`,D.textContent=l?`${f.length} of ${d.length} shown`:`${d.length} shown`,f.length===0){z.innerHTML=`
No logs match "${r(l)}"
`;return}z.innerHTML=f.map((N,O)=>c(N,O)).join(""),z.scrollTop=z.scrollHeight}async function h(){try{const l=(await getJSON("/api/v1/logs/containers")).containers||[],N=E.value;E.innerHTML='',l.forEach(O=>{const F=document.createElement("option");F.value=O.id,F.textContent=`${O.name} (${O.image.split(":")[0]}) - ${O.status}`,F.dataset.name=O.name,F.dataset.image=O.image,F.dataset.status=O.status,F.dataset.created=O.created,E.appendChild(F)}),N&&E.querySelector(`option[value="${N}"]`)&&(E.value=N,u(N))}catch(d){console.error("Failed to load containers:",d)}}function u(d){const l=E.querySelector(`option[value="${d}"]`);l&&(j.textContent=l.dataset.image||"-",B.textContent=l.dataset.status||"-",B.style.color=l.dataset.status==="running"?"var(--ok-fg, #4ade80)":"var(--bad-fg, #ef4444)",P.textContent=m(l.dataset.created))}async function a(){const d=E.value;if(!d){z.innerHTML='
Select a container to view logs
';return}n(),b=d,u(d);const l=L.value,N=I.value.trim();z.innerHTML='
Loading logs...
';try{const O=`/api/v1/logs/container/${d}${l!=="all"?`?tail=${l}`:""}`,F=await getJSON(O);F.logs&&F.logs.length>0?s(F.logs,N):(z.innerHTML='
No logs found for this container
',R.textContent="0 lines",D.textContent="0 filtered")}catch(O){z.innerHTML=`
Error loading logs: ${r(O.message)}
`}}function e(){const d=E.value;if(!d)return;n(),g=!0,T.textContent="\u23F9 Stop",x.style.display="flex",C.textContent="\u{1F7E2}",M.textContent="Connecting...";const l=`/api/v1/logs/stream/${d}`;p=new EventSource(l),p.onopen=()=>{C.textContent="\u{1F7E2}",M.textContent="Connected - streaming logs"},p.onmessage=N=>{try{const O=JSON.parse(N.data);if(O.error){C.textContent="\u{1F534}",M.textContent=`Error: ${O.error}`;return}H.push(O),f.push(O),R.textContent=`${H.length} lines`,D.textContent=`${f.length} shown`;const F=I.value.trim();if(!F||O.text&&O.text.toLowerCase().includes(F.toLowerCase())){const q=document.createElement("div");q.innerHTML=c(O,f.length-1);const U=q.firstElementChild;U.style.background="#1a3a1a",z.appendChild(U),z.scrollTop=z.scrollHeight}}catch(O){console.error("Error parsing log:",O)}},p.onerror=()=>{C.textContent="\u{1F534}",M.textContent="Disconnected",g=!1,T.textContent="\u25B6 Stream"},k._eventSource=p}function n(){p&&(p.close(),p=null),k._eventSource&&(k._eventSource.close(),k._eventSource=null),g=!1,T.textContent="\u25B6 Stream",x.style.display="none"}function t(){if(!H||H.length===0){showNotification("No logs to download","error");return}const d=E.querySelector(`option[value="${b}"]`)?.dataset.name||b,l=new Date().toISOString().replace(/[:.]/g,"-"),N=`${d}-logs-${l}.txt`,O=H.map(G=>{const W=G.timestamp||"",X=G.stream==="stderr"?"[ERR]":"[OUT]";return`${W?W+" ":""}${X} ${G.text}`}).join(` +`),F=new Blob([O],{type:"text/plain"}),q=URL.createObjectURL(F),U=document.createElement("a");U.href=q,U.download=N,document.body.appendChild(U),U.click(),document.body.removeChild(U),URL.revokeObjectURL(q),showNotification(`Downloaded ${H.length} log lines`,"success")}E?.addEventListener("change",()=>{a()}),L?.addEventListener("change",()=>{a()}),A?.addEventListener("click",()=>{a()}),T?.addEventListener("click",()=>{g?n():e()}),$?.addEventListener("click",()=>{t()}),y?.addEventListener("click",()=>{I.value="",s(H,"")}),I?.addEventListener("input",()=>{clearTimeout(v),v=setTimeout(()=>{s(H,I.value.trim())},300)}),I?.addEventListener("keydown",d=>{d.key==="Escape"&&(I.value="",s(H,""))}),document.getElementById("view-container-logs")?.addEventListener("click",()=>{k.classList.add("show"),h()});function o(){n(),k.classList.remove("show")}S?.addEventListener("click",o),w?.addEventListener("click",o),document.addEventListener("keydown",d=>{d.key==="Escape"&&k.classList.contains("show")&&o()}),wireModal(k,null,o)})(),(function(){injectModal("arr-setup-modal",`

\u{1F3AC} Smart Arr Connect

@@ -638,73 +695,73 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};

-
`);const w=document.getElementById("arr-setup-modal"),B=document.getElementById("arr-setup-btn"),z=document.getElementById("arr-setup-cancel"),I=document.getElementById("smart-connect-btn"),C=document.getElementById("smart-phase-detect"),P=document.getElementById("smart-phase-credentials"),$=document.getElementById("smart-phase-progress"),L=document.getElementById("smart-phase-results"),g=document.getElementById("smart-detect-results"),k=document.getElementById("smart-credential-inputs"),f=document.getElementById("smart-progress-steps"),x=document.getElementById("smart-results-content"),E=document.getElementById("smart-plex-libraries"),R=document.getElementById("smart-retry-btn");let O=null;const D={plex:"\u{1F3AC}",radarr:"\u{1F3AC}",sonarr:"\u{1F4FA}",prowlarr:"\u{1F50D}",seerr:"\u{1F4CB}"},N={plex:"Plex",radarr:"Radarr (Movies)",sonarr:"Sonarr (TV)",prowlarr:"Prowlarr (Indexers)",seerr:"Seerr"};function M(c){C.style.display=c==="detect"?"block":"none",P.style.display=c==="credentials"?"block":"none",$.style.display=c==="progress"?"block":"none",L.style.display=c==="results"?"block":"none"}function H(c){const o={connected:{bg:"var(--ok-fg)",icon:"✓",text:"Connected"},needs_key:{bg:"#f39c12",icon:"🔑",text:"Needs API Key"},not_found:{bg:"var(--muted)",icon:"—",text:"Not Found"},error:{bg:"var(--bad-fg)",icon:"✗",text:"Error"}},l=o[c]||o.not_found;return`${l.icon} ${l.text}`}async function S(){M("detect"),g.style.display="none";try{if(O=await(await fetch("/api/v1/arr/smart-detect")).json(),!O.success){g.innerHTML=`
Detection failed: ${escapeHtml(O.error)}
`,g.style.display="block";return}let o='
';for(const[v,r]of Object.entries(O.services)){const p=D[v]||"\u{1F4E6}",m=N[v]||v,n=r.source?`${escapeHtml(r.source)}`:"",e=r.version?`v${escapeHtml(r.version)}`:"",a=(r.hasApiKey||r.hasToken)&&r.status==="connected"?'Key saved':"";o+=`
- ${p} +
`);const k=document.getElementById("arr-setup-modal"),E=document.getElementById("arr-setup-btn"),z=document.getElementById("arr-setup-cancel"),I=document.getElementById("smart-connect-btn"),L=document.getElementById("smart-phase-detect"),A=document.getElementById("smart-phase-credentials"),T=document.getElementById("smart-phase-progress"),$=document.getElementById("smart-phase-results"),y=document.getElementById("smart-detect-results"),S=document.getElementById("smart-credential-inputs"),w=document.getElementById("smart-progress-steps"),x=document.getElementById("smart-results-content"),C=document.getElementById("smart-plex-libraries"),M=document.getElementById("smart-retry-btn");let R=null;const D={plex:"\u{1F3AC}",radarr:"\u{1F3AC}",sonarr:"\u{1F4FA}",prowlarr:"\u{1F50D}",seerr:"\u{1F4CB}"},j={plex:"Plex",radarr:"Radarr (Movies)",sonarr:"Sonarr (TV)",prowlarr:"Prowlarr (Indexers)",seerr:"Seerr"};function B(v){L.style.display=v==="detect"?"block":"none",A.style.display=v==="credentials"?"block":"none",T.style.display=v==="progress"?"block":"none",$.style.display=v==="results"?"block":"none"}function P(v){const m={connected:{bg:"var(--ok-fg)",icon:"✓",text:"Connected"},needs_key:{bg:"#f39c12",icon:"🔑",text:"Needs API Key"},not_found:{bg:"var(--muted)",icon:"—",text:"Not Found"},error:{bg:"var(--bad-fg)",icon:"✗",text:"Error"}},r=m[v]||m.not_found;return`${r.icon} ${r.text}`}async function b(){B("detect"),y.style.display="none";try{if(R=await(await fetch("/api/v1/arr/smart-detect")).json(),!R.success){y.innerHTML=`
Detection failed: ${escapeHtml(R.error)}
`,y.style.display="block";return}let m='
';for(const[c,s]of Object.entries(R.services)){const h=D[c]||"\u{1F4E6}",u=j[c]||c,a=s.source?`${escapeHtml(s.source)}`:"",e=s.version?`v${escapeHtml(s.version)}`:"",n=(s.hasApiKey||s.hasToken)&&s.status==="connected"?'Key saved':"";m+=`
+ ${h}
-
${m}
+
${u}
- ${n} ${e} ${a} + ${a} ${e} ${n}
- ${H(r.status)} -
`}o+="
";const l=O.summary;o+=`
- ${escapeHtml(String(l.fullyConnected))}/${escapeHtml(String(l.totalDetected+(5-l.totalDetected)))} services detected · - ${escapeHtml(String(l.fullyConnected))} connected${l.needsApiKey>0?` · ${escapeHtml(String(l.needsApiKey))} needs API key`:""} -
`,g.innerHTML=o,g.style.display="block",T(O),setTimeout(()=>{M("credentials")},800)}catch(c){g.innerHTML=`
Error: ${escapeHtml(c.message)}
`,g.style.display="block"}}function T(c){let o="";const l=c.services,v=["radarr","sonarr","prowlarr"];for(const m of v){const n=l[m];if(!n||n.status==="not_found"&&!n.url)continue;const e=D[m],a=N[m],t=n.status==="connected";o+=`
+ ${P(s.status)} +
`}m+="
";const r=R.summary;m+=`
+ ${escapeHtml(String(r.fullyConnected))}/${escapeHtml(String(r.totalDetected+(5-r.totalDetected)))} services detected · + ${escapeHtml(String(r.fullyConnected))} connected${r.needsApiKey>0?` · ${escapeHtml(String(r.needsApiKey))} needs API key`:""} +
`,y.innerHTML=m,y.style.display="block",H(R),setTimeout(()=>{B("credentials")},800)}catch(v){y.innerHTML=`
Error: ${escapeHtml(v.message)}
`,y.style.display="block"}}function H(v){let m="";const r=v.services,c=["radarr","sonarr","prowlarr"];for(const u of c){const a=r[u];if(!a||a.status==="not_found"&&!a.url)continue;const e=D[u],n=j[u],t=a.status==="connected";m+=`
${e} - ${a} - + ${n} + ${t?'✓ Connected':""}
-
-
- -
`}const r=l.plex;if(r){const m=r.status==="connected";o+=`
+ +
`}const s=r.plex;if(s){const u=s.status==="connected";m+=`
\u{1F3AC} Plex - ${H(r.status)} - ${escapeHtml(r.source||"")} + ${P(s.status)} + ${escapeHtml(s.source||"")}
-
`}const p=l.seerr;if(p){const m=p.status==="connected";let n="";if(p.configuredServices){const e=p.configuredServices;n=`
+
`}const h=r.seerr;if(h){const u=h.status==="connected";let a="";if(h.configuredServices){const e=h.configuredServices;a=`
Configured: ${e.radarr?"✓ Radarr":"✗ Radarr"} · ${e.sonarr?"✓ Sonarr":"✗ Sonarr"} · ${e.plex?"✓ Plex":"✗ Plex"} -
`}o+=`
+
`}m+=`
\u{1F4CB} Seerr - ${H(p.status)} + ${P(h.status)}
- ${n} -
`}k.innerHTML=o}window.smartTestConnection=async function(c){const o=document.getElementById(`smart-${c}-url`),l=document.getElementById(`smart-${c}-key`),v=document.getElementById(`smart-${c}-status`),r=o?.value.trim(),p=l?.value.trim();if(!r||!p){v.innerHTML='Enter URL and API key';return}v.innerHTML='';try{const n=await(await secureFetch("/api/v1/arr/test-connection",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({service:c,url:r,apiKey:p})})).json();n.success?v.innerHTML=`✓ ${escapeHtml(n.appName||"Connected")} v${escapeHtml(n.version||"")}`:v.innerHTML=`✗ ${escapeHtml(n.error)}`}catch(m){v.innerHTML=`✗ ${escapeHtml(m.message)}`}};async function b(){M("progress"),f.innerHTML='
Connecting services...
';const c={};for(const l of["radarr","sonarr","prowlarr"]){const v=document.getElementById(`smart-${l}-url`)?.value.trim(),r=document.getElementById(`smart-${l}-key`)?.value.trim();r&&v?c[l]={apiKey:r,url:v}:r&&(c[l]={apiKey:r})}const o={services:Object.keys(c).length>0?c:void 0,configurePlex:document.getElementById("smart-opt-plex")?.checked,configureProwlarr:document.getElementById("smart-opt-prowlarr")?.checked,configureSeerr:document.getElementById("smart-opt-seerr")?.checked,saveCredentials:document.getElementById("smart-opt-save")?.checked};try{const v=await(await secureFetch("/api/v1/arr/smart-connect",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)})).json();let r="";for(const p of v.steps||[]){const m=p.status==="success"?'':'',n=p.status==="success"?"var(--muted)":"var(--bad-fg)";r+=`
- ${m} - ${escapeHtml(p.step)} - ${escapeHtml(p.details||"")} -
`}f.innerHTML=r,setTimeout(()=>u(v),500)}catch(l){f.innerHTML=`
Connection error: ${escapeHtml(l.message)}
`}}function u(c){M("results");const o=c.summary||{},l=o.failed===0&&o.succeeded>0,v=l?"var(--ok-fg)":"#f39c12",r=l?"✓":"⚠",p=l?"All Connected!":`${escapeHtml(String(o.succeeded))}/${escapeHtml(String(o.totalSteps))} Steps Succeeded`;let m=`
-
${r}
-
${p}
-
${escapeHtml(String(o.succeeded))} succeeded, ${escapeHtml(String(o.failed))} failed
-
`;m+='
';for(const n of c.steps||[]){const e=n.status==="success"?'':'';m+=`
- ${e} ${escapeHtml(n.step)} ${escapeHtml(n.details||"")} -
`}m+="
",x.innerHTML=m,R.style.display=o.failed>0?"block":"none",c.steps?.some(n=>n.step.includes("Plex")&&n.status==="success")&&y()}async function y(){try{const o=await(await fetch("/api/v1/plex/libraries")).json();if(o.success&&o.libraries?.length>0){let l=`
-

\u{1F3AC} ${escapeHtml(o.serverName)} Libraries

-
`;for(const v of o.libraries){const r=v.type==="movie"?"\u{1F3AC}":v.type==="show"?"\u{1F4FA}":"\u{1F3B5}";l+=`
- ${r} ${escapeHtml(v.title)} - ${escapeHtml(String(v.count))} items -
`}l+="
",E.innerHTML=l,E.style.display="block"}}catch{}}B?.addEventListener("click",()=>{w.classList.add("show"),E.style.display="none",S()}),wireModal(w,z),I?.addEventListener("click",b),R?.addEventListener("click",b)})(),(function(){injectModal("notifications-modal",`
+ ${a} +
`}S.innerHTML=m}window.smartTestConnection=async function(v){const m=document.getElementById(`smart-${v}-url`),r=document.getElementById(`smart-${v}-key`),c=document.getElementById(`smart-${v}-status`),s=m?.value.trim(),h=r?.value.trim();if(!s||!h){c.innerHTML='Enter URL and API key';return}c.innerHTML='';try{const a=await(await secureFetch("/api/v1/arr/test-connection",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({service:v,url:s,apiKey:h})})).json();a.success?c.innerHTML=`✓ ${escapeHtml(a.appName||"Connected")} v${escapeHtml(a.version||"")}`:c.innerHTML=`✗ ${escapeHtml(a.error)}`}catch(u){c.innerHTML=`✗ ${escapeHtml(u.message)}`}};async function f(){B("progress"),w.innerHTML='
Connecting services...
';const v={};for(const r of["radarr","sonarr","prowlarr"]){const c=document.getElementById(`smart-${r}-url`)?.value.trim(),s=document.getElementById(`smart-${r}-key`)?.value.trim();s&&c?v[r]={apiKey:s,url:c}:s&&(v[r]={apiKey:s})}const m={services:Object.keys(v).length>0?v:void 0,configurePlex:document.getElementById("smart-opt-plex")?.checked,configureProwlarr:document.getElementById("smart-opt-prowlarr")?.checked,configureSeerr:document.getElementById("smart-opt-seerr")?.checked,saveCredentials:document.getElementById("smart-opt-save")?.checked};try{const c=await(await secureFetch("/api/v1/arr/smart-connect",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(m)})).json();let s="";for(const h of c.steps||[]){const u=h.status==="success"?'':'',a=h.status==="success"?"var(--muted)":"var(--bad-fg)";s+=`
+ ${u} + ${escapeHtml(h.step)} + ${escapeHtml(h.details||"")} +
`}w.innerHTML=s,setTimeout(()=>p(c),500)}catch(r){w.innerHTML=`
Connection error: ${escapeHtml(r.message)}
`}}function p(v){B("results");const m=v.summary||{},r=m.failed===0&&m.succeeded>0,c=r?"var(--ok-fg)":"#f39c12",s=r?"✓":"⚠",h=r?"All Connected!":`${escapeHtml(String(m.succeeded))}/${escapeHtml(String(m.totalSteps))} Steps Succeeded`;let u=`
+
${s}
+
${h}
+
${escapeHtml(String(m.succeeded))} succeeded, ${escapeHtml(String(m.failed))} failed
+
`;u+='
';for(const a of v.steps||[]){const e=a.status==="success"?'':'';u+=`
+ ${e} ${escapeHtml(a.step)} ${escapeHtml(a.details||"")} +
`}u+="
",x.innerHTML=u,M.style.display=m.failed>0?"block":"none",v.steps?.some(a=>a.step.includes("Plex")&&a.status==="success")&&g()}async function g(){try{const m=await(await fetch("/api/v1/plex/libraries")).json();if(m.success&&m.libraries?.length>0){let r=`
+

\u{1F3AC} ${escapeHtml(m.serverName)} Libraries

+
`;for(const c of m.libraries){const s=c.type==="movie"?"\u{1F3AC}":c.type==="show"?"\u{1F4FA}":"\u{1F3B5}";r+=`
+ ${s} ${escapeHtml(c.title)} + ${escapeHtml(String(c.count))} items +
`}r+="
",C.innerHTML=r,C.style.display="block"}}catch{}}E?.addEventListener("click",()=>{k.classList.add("show"),C.style.display="none",b()}),wireModal(k,z),I?.addEventListener("click",f),M?.addEventListener("click",f)})(),(function(){const k=new ErrorHandler;injectModal("notifications-modal",`

\u{1F514} Notification Settings

@@ -884,15 +941,15 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};
-
`);const w=document.getElementById("notifications-modal"),B=document.getElementById("manage-notifications"),z=document.getElementById("notifications-save"),I=document.getElementById("notifications-cancel");["discord","telegram","ntfy","email"].forEach(f=>{const x=document.getElementById(`${f}-enabled`),E=document.getElementById(`${f}-config`);x?.addEventListener("change",()=>{E.style.display=x.checked?"block":"none"})});const C=document.getElementById("health-check-enabled"),P=document.getElementById("health-check-config");C?.addEventListener("change",()=>{P.style.opacity=C.checked?"1":"0.5"});async function $(){try{const x=await(await fetch("/api/v1/notifications/config")).json();if(x.success){const E=x.config;document.getElementById("notifications-enabled").checked=E.enabled,document.getElementById("discord-enabled").checked=E.providers?.discord?.enabled||!1,document.getElementById("telegram-enabled").checked=E.providers?.telegram?.enabled||!1,document.getElementById("ntfy-enabled").checked=E.providers?.ntfy?.enabled||!1,document.getElementById("email-enabled").checked=E.providers?.email?.enabled||!1,document.getElementById("discord-config").style.display=E.providers?.discord?.enabled?"block":"none",document.getElementById("telegram-config").style.display=E.providers?.telegram?.enabled?"block":"none",document.getElementById("ntfy-config").style.display=E.providers?.ntfy?.enabled?"block":"none",document.getElementById("email-config").style.display=E.providers?.email?.enabled?"block":"none",E.providers?.ntfy?.serverUrl&&(document.getElementById("ntfy-server").value=E.providers.ntfy.serverUrl),E.providers?.email?.host&&(document.getElementById("email-host").value=E.providers.email.host),E.providers?.email?.from&&(document.getElementById("email-from").value=E.providers.email.from),document.getElementById("health-check-enabled").checked=E.healthCheck?.enabled||!1,E.healthCheck?.intervalMinutes&&(document.getElementById("health-check-interval").value=E.healthCheck.intervalMinutes),E.healthCheck?.lastCheck&&(document.getElementById("health-check-status").textContent=`Last check: ${new Date(E.healthCheck.lastCheck).toLocaleString()}`),document.getElementById("event-container-down").checked=E.events?.containerDown!==!1,document.getElementById("event-container-up").checked=E.events?.containerUp!==!1,document.getElementById("event-deploy-success").checked=E.events?.deploymentSuccess!==!1,document.getElementById("event-deploy-failed").checked=E.events?.deploymentFailed!==!1}}catch(f){console.error("Failed to load notification config:",f)}}async function L(){try{const x=await(await fetch("/api/v1/notifications/history?limit=10")).json(),E=document.getElementById("notification-history");x.success&&x.history?.length>0?E.innerHTML=x.history.map(R=>{const O=new Date(R.timestamp).toLocaleString();return` +
`);const E=document.getElementById("notifications-modal"),z=document.getElementById("manage-notifications"),I=document.getElementById("notifications-save"),L=document.getElementById("notifications-cancel");["discord","telegram","ntfy","email"].forEach(x=>{const C=document.getElementById(`${x}-enabled`),M=document.getElementById(`${x}-config`);C?.addEventListener("change",()=>{M.style.display=C.checked?"block":"none"})});const A=document.getElementById("health-check-enabled"),T=document.getElementById("health-check-config");A?.addEventListener("change",()=>{T.style.opacity=A.checked?"1":"0.5"});async function $(){try{const C=await(await fetch("/api/v1/notifications/config")).json();if(C.success){const M=C.config;document.getElementById("notifications-enabled").checked=M.enabled,document.getElementById("discord-enabled").checked=M.providers?.discord?.enabled||!1,document.getElementById("telegram-enabled").checked=M.providers?.telegram?.enabled||!1,document.getElementById("ntfy-enabled").checked=M.providers?.ntfy?.enabled||!1,document.getElementById("email-enabled").checked=M.providers?.email?.enabled||!1,document.getElementById("discord-config").style.display=M.providers?.discord?.enabled?"block":"none",document.getElementById("telegram-config").style.display=M.providers?.telegram?.enabled?"block":"none",document.getElementById("ntfy-config").style.display=M.providers?.ntfy?.enabled?"block":"none",document.getElementById("email-config").style.display=M.providers?.email?.enabled?"block":"none",M.providers?.ntfy?.serverUrl&&(document.getElementById("ntfy-server").value=M.providers.ntfy.serverUrl),M.providers?.email?.host&&(document.getElementById("email-host").value=M.providers.email.host),M.providers?.email?.from&&(document.getElementById("email-from").value=M.providers.email.from),document.getElementById("health-check-enabled").checked=M.healthCheck?.enabled||!1,M.healthCheck?.intervalMinutes&&(document.getElementById("health-check-interval").value=M.healthCheck.intervalMinutes),M.healthCheck?.lastCheck&&(document.getElementById("health-check-status").textContent=`Last check: ${new Date(M.healthCheck.lastCheck).toLocaleString()}`),document.getElementById("event-container-down").checked=M.events?.containerDown!==!1,document.getElementById("event-container-up").checked=M.events?.containerUp!==!1,document.getElementById("event-deploy-success").checked=M.events?.deploymentSuccess!==!1,document.getElementById("event-deploy-failed").checked=M.events?.deploymentFailed!==!1}}catch(x){k.logError("[Notifications] Load Config",x,{function:"loadConfig"})}}async function y(){try{const C=await(await fetch("/api/v1/notifications/history?limit=10")).json(),M=document.getElementById("notification-history");C.success&&C.history?.length>0?M.innerHTML=C.history.map(R=>{const D=new Date(R.timestamp).toLocaleString();return`
${R.type==="success"?"\u2713":R.type==="error"?"\u2717":"\u2139"}
${escapeHtml(R.title)}
-
${O}
+
${D}
- `}).join(""):E.innerHTML='
No notifications yet
'}catch(f){console.error("Failed to load notification history:",f)}}async function g(){try{const f={enabled:document.getElementById("notifications-enabled").checked,providers:{discord:{enabled:document.getElementById("discord-enabled").checked,webhookUrl:document.getElementById("discord-webhook").value.trim()},telegram:{enabled:document.getElementById("telegram-enabled").checked,botToken:document.getElementById("telegram-bot-token").value.trim(),chatId:document.getElementById("telegram-chat-id").value.trim()},ntfy:{enabled:document.getElementById("ntfy-enabled").checked,serverUrl:document.getElementById("ntfy-server").value.trim()||"https://ntfy.sh",topic:document.getElementById("ntfy-topic").value.trim()},email:{enabled:document.getElementById("email-enabled").checked,host:document.getElementById("email-host").value.trim(),port:parseInt(document.getElementById("email-port").value)||587,secure:document.getElementById("email-secure").checked,user:document.getElementById("email-user").value.trim(),pass:document.getElementById("email-pass").value.trim(),from:document.getElementById("email-from").value.trim(),to:document.getElementById("email-to").value.trim()}},events:{containerDown:document.getElementById("event-container-down").checked,containerUp:document.getElementById("event-container-up").checked,deploymentSuccess:document.getElementById("event-deploy-success").checked,deploymentFailed:document.getElementById("event-deploy-failed").checked},healthCheck:{enabled:document.getElementById("health-check-enabled").checked,intervalMinutes:parseInt(document.getElementById("health-check-interval").value)||5}},E=await(await secureFetch("/api/v1/notifications/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(f)})).json();E.success?(showNotification("Notification settings saved","success",3e3),w.classList.remove("show")):showNotification(`Failed to save: ${E.error}`,"error",3e3)}catch(f){showNotification(`Error: ${f.message}`,"error",3e3)}}async function k(f){try{const E=await(await secureFetch("/api/v1/notifications/test",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({provider:f})})).json();E.success?showNotification(`Test ${f} notification sent!`,"success",3e3):showNotification(`Test failed: ${E.error}`,"error",3e3)}catch(x){showNotification(`Error: ${x.message}`,"error",3e3)}}document.getElementById("discord-test")?.addEventListener("click",()=>k("discord")),document.getElementById("telegram-test")?.addEventListener("click",()=>k("telegram")),document.getElementById("ntfy-test")?.addEventListener("click",()=>k("ntfy")),document.getElementById("email-test")?.addEventListener("click",()=>k("email")),document.getElementById("health-check-now")?.addEventListener("click",async()=>{try{const x=await(await secureFetch("/api/v1/notifications/health-check",{method:"POST"})).json();x.success&&(document.getElementById("health-check-status").textContent=`Last check: ${new Date(x.lastCheck).toLocaleString()} (${x.containersMonitored} containers)`,showNotification("Health check completed","success",2e3))}catch(f){showNotification(`Error: ${f.message}`,"error",3e3)}}),B?.addEventListener("click",()=>{w.classList.add("show"),$(),L()}),z?.addEventListener("click",g),wireModal(w,I)})(),(function(){document.addEventListener("click",w=>{const B=w.target.closest(".panel-tab");if(!B)return;const z=B.dataset.panel;if(!z)return;const I=B.closest(".panel-tabs"),C=I.closest(".weather-modal-content");I.querySelectorAll(".panel-tab").forEach($=>$.classList.remove("active")),B.classList.add("active"),C.querySelectorAll(".panel-section").forEach($=>$.classList.remove("active"));const P=C.querySelector("#"+z);P&&P.classList.add("active")})})(),(function(){var w=["dashcaddy_site_config","dashcaddy_onboarding","dashcaddy-encryption-key","dashcaddy-setup","dashcaddy-config","theme","user-themes","custom-theme","custom-apps","custom-services","toolbar-sections","weather-location","weather-zip","weather-geo","weather-unit","clock-style","clock-chimes","clock-chime-volume"];function B(){for(var e={},a=0;a + `}).join(""):M.innerHTML='
No notifications yet
'}catch(x){k.logError("[Notifications] Load History",x,{function:"loadHistory"})}}async function S(){try{const x={enabled:document.getElementById("notifications-enabled").checked,providers:{discord:{enabled:document.getElementById("discord-enabled").checked,webhookUrl:document.getElementById("discord-webhook").value.trim()},telegram:{enabled:document.getElementById("telegram-enabled").checked,botToken:document.getElementById("telegram-bot-token").value.trim(),chatId:document.getElementById("telegram-chat-id").value.trim()},ntfy:{enabled:document.getElementById("ntfy-enabled").checked,serverUrl:document.getElementById("ntfy-server").value.trim()||"https://ntfy.sh",topic:document.getElementById("ntfy-topic").value.trim()},email:{enabled:document.getElementById("email-enabled").checked,host:document.getElementById("email-host").value.trim(),port:parseInt(document.getElementById("email-port").value)||587,secure:document.getElementById("email-secure").checked,user:document.getElementById("email-user").value.trim(),pass:document.getElementById("email-pass").value.trim(),from:document.getElementById("email-from").value.trim(),to:document.getElementById("email-to").value.trim()}},events:{containerDown:document.getElementById("event-container-down").checked,containerUp:document.getElementById("event-container-up").checked,deploymentSuccess:document.getElementById("event-deploy-success").checked,deploymentFailed:document.getElementById("event-deploy-failed").checked},healthCheck:{enabled:document.getElementById("health-check-enabled").checked,intervalMinutes:parseInt(document.getElementById("health-check-interval").value)||5}},M=await(await secureFetch("/api/v1/notifications/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(x)})).json();M.success?(showNotification("Notification settings saved","success",3e3),E.classList.remove("show")):showNotification(`Failed to save: ${M.error}`,"error",3e3)}catch(x){showNotification(`Error: ${x.message}`,"error",3e3)}}async function w(x){try{const M=await(await secureFetch("/api/v1/notifications/test",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({provider:x})})).json();M.success?showNotification(`Test ${x} notification sent!`,"success",3e3):showNotification(`Test failed: ${M.error}`,"error",3e3)}catch(C){showNotification(`Error: ${C.message}`,"error",3e3)}}document.getElementById("discord-test")?.addEventListener("click",()=>w("discord")),document.getElementById("telegram-test")?.addEventListener("click",()=>w("telegram")),document.getElementById("ntfy-test")?.addEventListener("click",()=>w("ntfy")),document.getElementById("email-test")?.addEventListener("click",()=>w("email")),document.getElementById("health-check-now")?.addEventListener("click",async()=>{try{const C=await(await secureFetch("/api/v1/notifications/health-check",{method:"POST"})).json();C.success&&(document.getElementById("health-check-status").textContent=`Last check: ${new Date(C.lastCheck).toLocaleString()} (${C.containersMonitored} containers)`,showNotification("Health check completed","success",2e3))}catch(x){showNotification(`Error: ${x.message}`,"error",3e3)}}),z?.addEventListener("click",()=>{E.classList.add("show"),$(),y()}),I?.addEventListener("click",S),wireModal(E,L)})(),(function(){document.addEventListener("click",k=>{const E=k.target.closest(".panel-tab");if(!E)return;const z=E.dataset.panel;if(!z)return;const I=E.closest(".panel-tabs"),L=I.closest(".weather-modal-content");I.querySelectorAll(".panel-tab").forEach(T=>T.classList.remove("active")),E.classList.add("active"),L.querySelectorAll(".panel-section").forEach(T=>T.classList.remove("active"));const A=L.querySelector("#"+z);A&&A.classList.add("active")})})(),(function(){var k=["dashcaddy_site_config","dashcaddy_onboarding","dashcaddy-encryption-key","dashcaddy-setup","dashcaddy-config","theme","user-themes","custom-theme","custom-apps","custom-services","toolbar-sections","weather-location","weather-zip","weather-geo","weather-unit","clock-style","clock-chimes","clock-chime-volume"];function E(){for(var e={},n=0;n

\u{1F4BE} Backup & Restore

- `);var P=document.getElementById("backup-modal"),$=document.getElementById("backup-restore-btn"),L=document.getElementById("backup-cancel"),g=document.getElementById("backup-export-btn"),k=document.getElementById("backup-select-file"),f=document.getElementById("backup-file-input"),x=document.getElementById("backup-file-name"),E=document.getElementById("backup-preview"),R=document.getElementById("backup-preview-content"),O=document.getElementById("backup-do-restore-btn"),D=document.getElementById("backup-result"),N=document.getElementById("backup-schedule-container"),M=document.getElementById("backup-history-container"),H=null;$?.addEventListener("click",function(){P.classList.add("show"),D&&(D.style.display="none"),E&&(E.style.display="none"),x&&(x.style.display="none"),H=null}),wireModal(P,L),g?.addEventListener("click",async function(){g.disabled=!0,g.innerHTML=' Exporting...';try{var e=await fetch("/api/v1/backup/export"),a=await e.json();a.browserState=B();var t=new Blob([JSON.stringify(a,null,2)],{type:"application/json"}),s=URL.createObjectURL(t),i=document.createElement("a");i.href=s,i.download="dashcaddy-backup-"+new Date().toISOString().split("T")[0]+".json",document.body.appendChild(i),i.click(),document.body.removeChild(i),URL.revokeObjectURL(s);var h=Object.keys(a.browserState).length,d=a.themes?Object.keys(a.themes).length:0;D.innerHTML="\u2705 Full backup downloaded \u2014 server config + "+h+" browser settings"+(d?" + "+d+" themes":""),D.style.display="block",D.style.background="color-mix(in srgb, var(--ok-fg) 15%, transparent)",D.style.border="1px solid var(--ok-fg)"}catch(A){D.innerHTML="\u274C Export failed: "+escapeHtml(A.message),D.style.display="block",D.style.background="color-mix(in srgb, var(--bad-fg) 15%, transparent)",D.style.border="1px solid var(--bad-fg)"}g.disabled=!1,g.innerHTML="\u2B07\uFE0F Download Full Backup"}),k?.addEventListener("click",function(){f.click()}),f?.addEventListener("change",async function(e){var a=e.target.files[0];if(a){x.textContent="\u{1F4C4} "+a.name,x.style.display="block",D.style.display="none";try{var t=await a.text(),s=JSON.parse(t);if(I(s)){H=s;var i='
Legacy format (v'+escapeHtml(s.version)+")
";i+='
',s.services?.length&&(i+='\u{1F4CB} '+s.services.length+" services"),s.customApps?.length&&(i+='\u{1F4E6} '+s.customApps.length+" custom apps"),s.theme&&(i+='\u{1F3A8} Theme: '+escapeHtml(s.theme)+""),s.userThemes&&(i+='\u{1F3A8} '+Object.keys(s.userThemes).length+" custom themes"),i+="
",R.innerHTML=i,E.style.display="block";return}var h=await secureFetch("/api/v1/backup/preview",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)}),d=await h.json();if(d.success){H=s;var i='
Exported: '+new Date(s.exportedAt).toLocaleString()+" (v"+escapeHtml(s.version)+")
";i+='
Server Config
',i+='
';for(var A in d.preview.files){var F=d.preview.files[A],j=F.action==="create"?"\u{1F195}":"\u{1F4DD}";i+=''+j+" "+escapeHtml(F.description)+""}i+="
",d.preview.serviceCount&&(i+='
'+d.preview.serviceCount+" services
"),d.preview.themeCount&&(i+='
\u{1F3A8} '+d.preview.themeCount+" custom themes
"),d.preview.browserStateCount&&(i+='
Browser Preferences
',i+='
\u{1F5A5}\uFE0F '+d.preview.browserStateCount+" saved settings (theme, weather, clock, widgets, etc.)
"),R.innerHTML=i,E.style.display="block"}else D.innerHTML="\u26A0\uFE0F Invalid backup file: "+escapeHtml(d.error),D.style.display="block",D.style.background="color-mix(in srgb, #f39c12 15%, transparent)",D.style.border="1px solid #f39c12",E.style.display="none"}catch(_){D.innerHTML="\u274C Could not read file: "+escapeHtml(_.message),D.style.display="block",D.style.background="color-mix(in srgb, var(--bad-fg) 15%, transparent)",D.style.border="1px solid var(--bad-fg)",E.style.display="none"}}}),O?.addEventListener("click",async function(){if(H&&confirm("This will overwrite your current configuration and browser preferences. Continue?")){O.disabled=!0,O.innerHTML=' Restoring...';try{if(I(H)){C(H),D.innerHTML="\u2705 Legacy backup restored \u2014 browser settings and services imported.",D.style.background="color-mix(in srgb, var(--ok-fg) 15%, transparent)",D.style.border="1px solid var(--ok-fg)",D.style.display="block",setTimeout(function(){location.reload()},2e3),O.disabled=!1,O.innerHTML="\u26A1 Restore Everything";return}var e=document.getElementById("backup-reload-caddy")?.checked??!0,a=await secureFetch("/api/v1/backup/restore",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({backup:H,options:{reloadCaddy:e}})}),t=await a.json(),s=0;if(H.browserState&&(s=z(H.browserState)),t.success){var i="\u2705 "+t.message;s>0&&(i+='
'+s+" browser settings restored"),t.results.caddyReloaded&&(i+='
Caddy configuration reloaded'),D.innerHTML=i,D.style.background="color-mix(in srgb, var(--ok-fg) 15%, transparent)",D.style.border="1px solid var(--ok-fg)",setTimeout(function(){location.reload()},2e3)}else D.innerHTML="\u26A0\uFE0F "+escapeHtml(t.message),s>0&&(D.innerHTML+='
'+s+" browser settings were restored"),t.results?.errors?.length>0&&(D.innerHTML+="
"+t.results.errors.map(function(h){return escapeHtml(h.file)+": "+escapeHtml(h.error)}).join(", ")+""),D.style.background="color-mix(in srgb, #f39c12 15%, transparent)",D.style.border="1px solid #f39c12";D.style.display="block"}catch(h){D.innerHTML="\u274C Restore failed: "+escapeHtml(h.message),D.style.display="block",D.style.background="color-mix(in srgb, var(--bad-fg) 15%, transparent)",D.style.border="1px solid var(--bad-fg)"}O.disabled=!1,O.innerHTML="\u26A1 Restore Everything"}});var S={type:"local"};async function T(){if(N)try{var e=await fetch("/api/v1/backups/config"),a=await e.json();if(!a.success)throw new Error(a.error||"Failed to load config");var t=a.config?.backups||{},s=Object.keys(t)[0],i=s?t[s]:null,h=i?.destinations&&i.destinations[0]||{type:"local"};S=JSON.parse(JSON.stringify(h));var d='
';d+='

\u23F0 Backup Schedule

',d+='
',d+='
',d+='
",d+='
',d+='
",d+="
",d+='
',d+='
",d+='
',d+=' ',d+=' ',d+="
",d+="
",d+='
',d+='

\u2601\uFE0F Backup Destination

',d+='
',d+='
",d+='
',d+='',d+="
",d+='',N.innerHTML=d,document.getElementById("backup-save-schedule")?.addEventListener("click",p),document.getElementById("backup-run-now")?.addEventListener("click",m);var A=document.getElementById("backup-dest-type");A?.addEventListener("change",function(){S={type:A.value},b(A.value)}),b(S.type)}catch(F){N.innerHTML='
Failed to load schedule: '+escapeHtml(F.message)+"
"}}async function b(e){var a=document.getElementById("backup-dest-form");if(a){if(e==="local"){a.innerHTML='
Backups are stored on the host filesystem. No additional configuration required.
';return}var t="";if(e==="dropbox"?(t+='',t+='',t+='',t+='',t+='
Generate a token at Dropbox App Console with files.content.write + files.content.read scopes.
'):e==="webdav"?(t+='',t+='',t+='
',t+='
',t+='
',t+='
',t+='
',t+="
",t+='',t+=''):e==="sftp"&&(t+='
',t+='
',t+='
',t+='
',t+='
',t+="
",t+='',t+='',t+='',t+='",t+='
',t+='
',t+='',t+='',t+=''),t+='
',t+=' ',t+=' ',t+=' ',t+="
",a.innerHTML=t,e==="sftp"){var s=document.getElementById("dest-sftp-authtype"),i=document.getElementById("dest-sftp-password-row"),h=document.getElementById("dest-sftp-key-row");s?.addEventListener("change",function(){s.value==="key"?(i.style.display="none",h.style.display=""):(i.style.display="",h.style.display="none")})}document.getElementById("dest-save-creds")?.addEventListener("click",function(){o(e)}),document.getElementById("dest-test-conn")?.addEventListener("click",function(){r(e)}),document.getElementById("dest-clear-creds")?.addEventListener("click",function(){l(e)}),await y(e)}}function u(e,a){var t=document.getElementById("backup-dest-result");t&&(t.innerHTML=e,t.style.display="block",t.style.background=a?"color-mix(in srgb, var(--ok-fg) 15%, transparent)":"color-mix(in srgb, var(--bad-fg) 15%, transparent)",t.style.border=a?"1px solid var(--ok-fg)":"1px solid var(--bad-fg)")}async function y(e){try{var a=await fetch("/api/v1/backups/credentials/"+e),t=await a.json();if(!t.success||!t.credentials)return;var s=t.credentials;if(e==="dropbox"){var i=document.getElementById("dest-dropbox-token");i&&s.token&&(i.value=s.token)}else if(e==="webdav"){var h=document.getElementById("dest-webdav-url");h&&s.url&&(h.value=s.url);var d=document.getElementById("dest-webdav-username");d&&s.username&&(d.value=s.username);var A=document.getElementById("dest-webdav-password");A&&s.password&&(A.value=s.password)}else if(e==="sftp"){var F=document.getElementById("dest-sftp-host");F&&s.host&&(F.value=s.host);var j=document.getElementById("dest-sftp-port");j&&s.port&&(j.value=s.port);var _=document.getElementById("dest-sftp-username");_&&s.username&&(_.value=s.username);var U=document.getElementById("dest-sftp-password");U&&s.password&&(U.value=s.password);var W=document.getElementById("dest-sftp-privatekey");if(W&&s.privateKey&&(W.value=s.privateKey),s.privateKey){var Z=document.getElementById("dest-sftp-authtype");Z&&(Z.value="key",Z.dispatchEvent(new Event("change")))}}}catch{}}function c(e){if(e==="dropbox")return{token:document.getElementById("dest-dropbox-token")?.value};if(e==="webdav")return{url:document.getElementById("dest-webdav-url")?.value,username:document.getElementById("dest-webdav-username")?.value,password:document.getElementById("dest-webdav-password")?.value};if(e==="sftp"){var a=document.getElementById("dest-sftp-authtype")?.value,t={host:document.getElementById("dest-sftp-host")?.value,port:parseInt(document.getElementById("dest-sftp-port")?.value)||22,username:document.getElementById("dest-sftp-username")?.value};return a==="key"?t.privateKey=document.getElementById("dest-sftp-privatekey")?.value:t.password=document.getElementById("dest-sftp-password")?.value,t}return{}}async function o(e){try{var a=c(e),t=await secureFetch("/api/v1/backups/credentials/"+e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(a)}),s=await t.json();u(s.success?"\u2705 Credentials saved":"\u26A0\uFE0F "+escapeHtml(s.error||"Failed"),s.success)}catch(i){u("\u274C "+escapeHtml(i.message),!1)}}async function l(e){if(confirm("Delete saved "+e+" credentials?"))try{var a=await secureFetch("/api/v1/backups/credentials/"+e,{method:"DELETE"}),t=await a.json();t.success?(u("\u2705 Credentials cleared",!0),b(e)):u("\u26A0\uFE0F "+escapeHtml(t.error||"Failed"),!1)}catch(s){u("\u274C "+escapeHtml(s.message),!1)}}function v(e){var a={type:e};return e==="local"||(e==="dropbox"?a.path=document.getElementById("dest-dropbox-path")?.value||"/dashcaddy-backups":e==="webdav"?a.path=document.getElementById("dest-webdav-path")?.value||"/dashcaddy-backups":e==="sftp"&&(a.path=document.getElementById("dest-sftp-path")?.value||"/dashcaddy-backups")),a}async function r(e){u(' Testing connection...',!0);try{var a=v(e),t=await secureFetch("/api/v1/backups/test-destination",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(a)}),s=await t.json();if(s.success){var i=s.elapsedMs?" ("+s.elapsedMs+"ms)":"";u("\u2705 Connection OK"+i+" \u2014 write/read/delete probe succeeded",!0)}else u("\u274C "+escapeHtml(s.error||"Connection failed"),!1)}catch(h){u("\u274C "+escapeHtml(h.message),!1)}}async function p(){var e=document.getElementById("backup-schedule-select")?.value,a=parseInt(document.getElementById("backup-retention-select")?.value)||5,t=document.getElementById("backup-encrypt-toggle")?.checked??!0,s=document.getElementById("backup-dest-type")?.value||"local",i=document.getElementById("backup-schedule-result");try{var h=await secureFetch("/api/v1/backups/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({backups:{auto:{enabled:e!=="disabled",schedule:e==="disabled"?"daily":e,include:["all"],encrypt:t,verify:!0,retention:{keep:a},destinations:[v(s)]}}})}),d=await h.json();i&&(i.innerHTML=d.success?"\u2705 Schedule saved":"\u26A0\uFE0F "+escapeHtml(d.error),i.style.display="block",i.style.background=d.success?"color-mix(in srgb, var(--ok-fg) 15%, transparent)":"color-mix(in srgb, var(--bad-fg) 15%, transparent)",i.style.border=d.success?"1px solid var(--ok-fg)":"1px solid var(--bad-fg)",setTimeout(function(){i&&(i.style.display="none")},3e3))}catch(A){i&&(i.innerHTML="\u274C "+escapeHtml(A.message),i.style.display="block",i.style.background="color-mix(in srgb, var(--bad-fg) 15%, transparent)",i.style.border="1px solid var(--bad-fg)")}}async function m(){var e=document.getElementById("backup-run-now"),a=document.getElementById("backup-schedule-result"),t=document.getElementById("backup-dest-type")?.value||"local";e&&(e.disabled=!0,e.innerHTML=' Running...');try{var s=await secureFetch("/api/v1/backups/execute",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({include:["all"],destinations:[v(t)]})}),i=await s.json();if(a){if(i.success){var h=i.backup?.size?(i.backup.size/1024/1024).toFixed(2):"?";a.innerHTML="\u2705 Backup complete ("+h+" MB)",a.style.background="color-mix(in srgb, var(--ok-fg) 15%, transparent)",a.style.border="1px solid var(--ok-fg)"}else a.innerHTML="\u26A0\uFE0F "+escapeHtml(i.error),a.style.background="color-mix(in srgb, var(--bad-fg) 15%, transparent)",a.style.border="1px solid var(--bad-fg)";a.style.display="block"}n()}catch(d){a&&(a.innerHTML="\u274C "+escapeHtml(d.message),a.style.display="block",a.style.background="color-mix(in srgb, var(--bad-fg) 15%, transparent)",a.style.border="1px solid var(--bad-fg)")}e&&(e.disabled=!1,e.innerHTML="\u25B6\uFE0F Run Backup Now")}async function n(){if(M){M.innerHTML='
Loading...
';try{var e=await fetch("/api/v1/backups/history?limit=50"),a=await e.json();if(!a.success||!a.history?.length){M.innerHTML='
\u{1F4CB} No backup history yet
';return}for(var t='
',s=0;s',t+='
',t+=' '+escapeHtml(i.name||"backup")+"",t+='
',t+=' '+escapeHtml(i.status)+"",i.status==="success"&&(t+=' '),t+="
",t+="
",t+='
',t+=" "+new Date(i.timestamp).toLocaleString()+" | "+h+" MB | "+(i.duration?(i.duration/1e3).toFixed(1)+"s":"--"),i.encrypted&&(t+=" | \u{1F512}"),t+="
",t+="
"}t+="",M.innerHTML=t,M.querySelectorAll(".backup-restore-btn").forEach(function(d){d.addEventListener("click",function(){window.__restoreServerBackup(d.dataset.backupId)})})}catch(d){M.innerHTML='
Failed: '+escapeHtml(d.message)+"
"}}}window.__restoreServerBackup=async function(e){if(confirm("Restore from this server backup? This will overwrite current configuration."))try{var a=await secureFetch("/api/v1/backups/restore/"+e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({restoreServices:!0,restoreConfig:!0})}),t=await a.json();t.success?(showNotification("Restore completed successfully!","success"),location.reload()):showNotification("Restore failed: "+(t.error||"Unknown error"),"error")}catch(s){showNotification("Restore error: "+s.message,"error")}},document.querySelector('[data-panel="backup-automated"]')?.addEventListener("click",T),document.querySelector('[data-panel="backup-history-tab"]')?.addEventListener("click",n)})(),(function(){injectModal("stats-modal",`
+
`);var A=document.getElementById("backup-modal"),T=document.getElementById("backup-restore-btn"),$=document.getElementById("backup-cancel"),y=document.getElementById("backup-export-btn"),S=document.getElementById("backup-select-file"),w=document.getElementById("backup-file-input"),x=document.getElementById("backup-file-name"),C=document.getElementById("backup-preview"),M=document.getElementById("backup-preview-content"),R=document.getElementById("backup-do-restore-btn"),D=document.getElementById("backup-result"),j=document.getElementById("backup-schedule-container"),B=document.getElementById("backup-history-container"),P=null;T?.addEventListener("click",function(){A.classList.add("show"),D&&(D.style.display="none"),C&&(C.style.display="none"),x&&(x.style.display="none"),P=null}),wireModal(A,$),y?.addEventListener("click",async function(){y.disabled=!0,y.innerHTML=' Exporting...';try{var e=await fetch("/api/v1/backup/export"),n=await e.json();n.browserState=E();var t=new Blob([JSON.stringify(n,null,2)],{type:"application/json"}),i=URL.createObjectURL(t),o=document.createElement("a");o.href=i,o.download="dashcaddy-backup-"+new Date().toISOString().split("T")[0]+".json",document.body.appendChild(o),o.click(),document.body.removeChild(o),URL.revokeObjectURL(i);var d=Object.keys(n.browserState).length,l=n.themes?Object.keys(n.themes).length:0;D.innerHTML="\u2705 Full backup downloaded \u2014 server config + "+d+" browser settings"+(l?" + "+l+" themes":""),D.style.display="block",D.style.background="color-mix(in srgb, var(--ok-fg) 15%, transparent)",D.style.border="1px solid var(--ok-fg)"}catch(N){D.innerHTML="\u274C Export failed: "+escapeHtml(N.message),D.style.display="block",D.style.background="color-mix(in srgb, var(--bad-fg) 15%, transparent)",D.style.border="1px solid var(--bad-fg)"}y.disabled=!1,y.innerHTML="\u2B07\uFE0F Download Full Backup"}),S?.addEventListener("click",function(){w.click()}),w?.addEventListener("change",async function(e){var n=e.target.files[0];if(n){x.textContent="\u{1F4C4} "+n.name,x.style.display="block",D.style.display="none";try{var t=await n.text(),i=JSON.parse(t);if(I(i)){P=i;var o='
Legacy format (v'+escapeHtml(i.version)+")
";o+='
',i.services?.length&&(o+='\u{1F4CB} '+i.services.length+" services"),i.customApps?.length&&(o+='\u{1F4E6} '+i.customApps.length+" custom apps"),i.theme&&(o+='\u{1F3A8} Theme: '+escapeHtml(i.theme)+""),i.userThemes&&(o+='\u{1F3A8} '+Object.keys(i.userThemes).length+" custom themes"),o+="
",M.innerHTML=o,C.style.display="block";return}var d=await secureFetch("/api/v1/backup/preview",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)}),l=await d.json();if(l.success){P=i;var o='
Exported: '+new Date(i.exportedAt).toLocaleString()+" (v"+escapeHtml(i.version)+")
";o+='
Server Config
',o+='
';for(var N in l.preview.files){var O=l.preview.files[N],F=O.action==="create"?"\u{1F195}":"\u{1F4DD}";o+=''+F+" "+escapeHtml(O.description)+""}o+="
",l.preview.serviceCount&&(o+='
'+l.preview.serviceCount+" services
"),l.preview.themeCount&&(o+='
\u{1F3A8} '+l.preview.themeCount+" custom themes
"),l.preview.browserStateCount&&(o+='
Browser Preferences
',o+='
\u{1F5A5}\uFE0F '+l.preview.browserStateCount+" saved settings (theme, weather, clock, widgets, etc.)
"),M.innerHTML=o,C.style.display="block"}else D.innerHTML="\u26A0\uFE0F Invalid backup file: "+escapeHtml(l.error),D.style.display="block",D.style.background="color-mix(in srgb, #f39c12 15%, transparent)",D.style.border="1px solid #f39c12",C.style.display="none"}catch(q){D.innerHTML="\u274C Could not read file: "+escapeHtml(q.message),D.style.display="block",D.style.background="color-mix(in srgb, var(--bad-fg) 15%, transparent)",D.style.border="1px solid var(--bad-fg)",C.style.display="none"}}}),R?.addEventListener("click",async function(){if(P&&confirm("This will overwrite your current configuration and browser preferences. Continue?")){R.disabled=!0,R.innerHTML=' Restoring...';try{if(I(P)){L(P),D.innerHTML="\u2705 Legacy backup restored \u2014 browser settings and services imported.",D.style.background="color-mix(in srgb, var(--ok-fg) 15%, transparent)",D.style.border="1px solid var(--ok-fg)",D.style.display="block",setTimeout(function(){location.reload()},2e3),R.disabled=!1,R.innerHTML="\u26A1 Restore Everything";return}var e=document.getElementById("backup-reload-caddy")?.checked??!0,n=await secureFetch("/api/v1/backup/restore",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({backup:P,options:{reloadCaddy:e}})}),t=await n.json(),i=0;if(P.browserState&&(i=z(P.browserState)),t.success){var o="\u2705 "+t.message;i>0&&(o+='
'+i+" browser settings restored"),t.results.caddyReloaded&&(o+='
Caddy configuration reloaded'),D.innerHTML=o,D.style.background="color-mix(in srgb, var(--ok-fg) 15%, transparent)",D.style.border="1px solid var(--ok-fg)",setTimeout(function(){location.reload()},2e3)}else D.innerHTML="\u26A0\uFE0F "+escapeHtml(t.message),i>0&&(D.innerHTML+='
'+i+" browser settings were restored"),t.results?.errors?.length>0&&(D.innerHTML+="
"+t.results.errors.map(function(d){return escapeHtml(d.file)+": "+escapeHtml(d.error)}).join(", ")+""),D.style.background="color-mix(in srgb, #f39c12 15%, transparent)",D.style.border="1px solid #f39c12";D.style.display="block"}catch(d){D.innerHTML="\u274C Restore failed: "+escapeHtml(d.message),D.style.display="block",D.style.background="color-mix(in srgb, var(--bad-fg) 15%, transparent)",D.style.border="1px solid var(--bad-fg)"}R.disabled=!1,R.innerHTML="\u26A1 Restore Everything"}});var b={type:"local"};async function H(){if(j)try{var e=await fetch("/api/v1/backups/config"),n=await e.json();if(!n.success)throw new Error(n.error||"Failed to load config");var t=n.config?.backups||{},i=Object.keys(t)[0],o=i?t[i]:null,d=o?.destinations&&o.destinations[0]||{type:"local"};b=JSON.parse(JSON.stringify(d));var l='
';l+='

\u23F0 Backup Schedule

',l+='
',l+='
',l+='
",l+='
',l+='
",l+="
",l+='
',l+='
",l+='
',l+=' ',l+=' ',l+="
",l+="
",l+='
',l+='

\u2601\uFE0F Backup Destination

',l+='
',l+='
",l+='
',l+='',l+="
",l+='',j.innerHTML=l,document.getElementById("backup-save-schedule")?.addEventListener("click",h),document.getElementById("backup-run-now")?.addEventListener("click",u);var N=document.getElementById("backup-dest-type");N?.addEventListener("change",function(){b={type:N.value},f(N.value)}),f(b.type)}catch(O){j.innerHTML='
Failed to load schedule: '+escapeHtml(O.message)+"
"}}async function f(e){var n=document.getElementById("backup-dest-form");if(n){if(e==="local"){n.innerHTML='
Backups are stored on the host filesystem. No additional configuration required.
';return}var t="";if(e==="dropbox"?(t+='',t+='',t+='',t+='',t+='
Generate a token at Dropbox App Console with files.content.write + files.content.read scopes.
'):e==="webdav"?(t+='',t+='',t+='
',t+='
',t+='
',t+='
',t+='
',t+="
",t+='',t+=''):e==="sftp"&&(t+='
',t+='
',t+='
',t+='
',t+='
',t+="
",t+='',t+='',t+='',t+='",t+='
',t+='
',t+='',t+='',t+=''),t+='
',t+=' ',t+=' ',t+=' ',t+="
",n.innerHTML=t,e==="sftp"){var i=document.getElementById("dest-sftp-authtype"),o=document.getElementById("dest-sftp-password-row"),d=document.getElementById("dest-sftp-key-row");i?.addEventListener("change",function(){i.value==="key"?(o.style.display="none",d.style.display=""):(o.style.display="",d.style.display="none")})}document.getElementById("dest-save-creds")?.addEventListener("click",function(){m(e)}),document.getElementById("dest-test-conn")?.addEventListener("click",function(){s(e)}),document.getElementById("dest-clear-creds")?.addEventListener("click",function(){r(e)}),await g(e)}}function p(e,n){var t=document.getElementById("backup-dest-result");t&&(t.innerHTML=e,t.style.display="block",t.style.background=n?"color-mix(in srgb, var(--ok-fg) 15%, transparent)":"color-mix(in srgb, var(--bad-fg) 15%, transparent)",t.style.border=n?"1px solid var(--ok-fg)":"1px solid var(--bad-fg)")}async function g(e){try{var n=await fetch("/api/v1/backups/credentials/"+e),t=await n.json();if(!t.success||!t.credentials)return;var i=t.credentials;if(e==="dropbox"){var o=document.getElementById("dest-dropbox-token");o&&i.token&&(o.value=i.token)}else if(e==="webdav"){var d=document.getElementById("dest-webdav-url");d&&i.url&&(d.value=i.url);var l=document.getElementById("dest-webdav-username");l&&i.username&&(l.value=i.username);var N=document.getElementById("dest-webdav-password");N&&i.password&&(N.value=i.password)}else if(e==="sftp"){var O=document.getElementById("dest-sftp-host");O&&i.host&&(O.value=i.host);var F=document.getElementById("dest-sftp-port");F&&i.port&&(F.value=i.port);var q=document.getElementById("dest-sftp-username");q&&i.username&&(q.value=i.username);var U=document.getElementById("dest-sftp-password");U&&i.password&&(U.value=i.password);var G=document.getElementById("dest-sftp-privatekey");if(G&&i.privateKey&&(G.value=i.privateKey),i.privateKey){var W=document.getElementById("dest-sftp-authtype");W&&(W.value="key",W.dispatchEvent(new Event("change")))}}}catch{}}function v(e){if(e==="dropbox")return{token:document.getElementById("dest-dropbox-token")?.value};if(e==="webdav")return{url:document.getElementById("dest-webdav-url")?.value,username:document.getElementById("dest-webdav-username")?.value,password:document.getElementById("dest-webdav-password")?.value};if(e==="sftp"){var n=document.getElementById("dest-sftp-authtype")?.value,t={host:document.getElementById("dest-sftp-host")?.value,port:parseInt(document.getElementById("dest-sftp-port")?.value)||22,username:document.getElementById("dest-sftp-username")?.value};return n==="key"?t.privateKey=document.getElementById("dest-sftp-privatekey")?.value:t.password=document.getElementById("dest-sftp-password")?.value,t}return{}}async function m(e){try{var n=v(e),t=await secureFetch("/api/v1/backups/credentials/"+e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)}),i=await t.json();p(i.success?"\u2705 Credentials saved":"\u26A0\uFE0F "+escapeHtml(i.error||"Failed"),i.success)}catch(o){p("\u274C "+escapeHtml(o.message),!1)}}async function r(e){if(confirm("Delete saved "+e+" credentials?"))try{var n=await secureFetch("/api/v1/backups/credentials/"+e,{method:"DELETE"}),t=await n.json();t.success?(p("\u2705 Credentials cleared",!0),f(e)):p("\u26A0\uFE0F "+escapeHtml(t.error||"Failed"),!1)}catch(i){p("\u274C "+escapeHtml(i.message),!1)}}function c(e){var n={type:e};return e==="local"||(e==="dropbox"?n.path=document.getElementById("dest-dropbox-path")?.value||"/dashcaddy-backups":e==="webdav"?n.path=document.getElementById("dest-webdav-path")?.value||"/dashcaddy-backups":e==="sftp"&&(n.path=document.getElementById("dest-sftp-path")?.value||"/dashcaddy-backups")),n}async function s(e){p(' Testing connection...',!0);try{var n=c(e),t=await secureFetch("/api/v1/backups/test-destination",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)}),i=await t.json();if(i.success){var o=i.elapsedMs?" ("+i.elapsedMs+"ms)":"";p("\u2705 Connection OK"+o+" \u2014 write/read/delete probe succeeded",!0)}else p("\u274C "+escapeHtml(i.error||"Connection failed"),!1)}catch(d){p("\u274C "+escapeHtml(d.message),!1)}}async function h(){var e=document.getElementById("backup-schedule-select")?.value,n=parseInt(document.getElementById("backup-retention-select")?.value)||5,t=document.getElementById("backup-encrypt-toggle")?.checked??!0,i=document.getElementById("backup-dest-type")?.value||"local",o=document.getElementById("backup-schedule-result");try{var d=await secureFetch("/api/v1/backups/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({backups:{auto:{enabled:e!=="disabled",schedule:e==="disabled"?"daily":e,include:["all"],encrypt:t,verify:!0,retention:{keep:n},destinations:[c(i)]}}})}),l=await d.json();o&&(o.innerHTML=l.success?"\u2705 Schedule saved":"\u26A0\uFE0F "+escapeHtml(l.error),o.style.display="block",o.style.background=l.success?"color-mix(in srgb, var(--ok-fg) 15%, transparent)":"color-mix(in srgb, var(--bad-fg) 15%, transparent)",o.style.border=l.success?"1px solid var(--ok-fg)":"1px solid var(--bad-fg)",setTimeout(function(){o&&(o.style.display="none")},3e3))}catch(N){o&&(o.innerHTML="\u274C "+escapeHtml(N.message),o.style.display="block",o.style.background="color-mix(in srgb, var(--bad-fg) 15%, transparent)",o.style.border="1px solid var(--bad-fg)")}}async function u(){var e=document.getElementById("backup-run-now"),n=document.getElementById("backup-schedule-result"),t=document.getElementById("backup-dest-type")?.value||"local";e&&(e.disabled=!0,e.innerHTML=' Running...');try{var i=await secureFetch("/api/v1/backups/execute",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({include:["all"],destinations:[c(t)]})}),o=await i.json();if(n){if(o.success){var d=o.backup?.size?(o.backup.size/1024/1024).toFixed(2):"?";n.innerHTML="\u2705 Backup complete ("+d+" MB)",n.style.background="color-mix(in srgb, var(--ok-fg) 15%, transparent)",n.style.border="1px solid var(--ok-fg)"}else n.innerHTML="\u26A0\uFE0F "+escapeHtml(o.error),n.style.background="color-mix(in srgb, var(--bad-fg) 15%, transparent)",n.style.border="1px solid var(--bad-fg)";n.style.display="block"}a()}catch(l){n&&(n.innerHTML="\u274C "+escapeHtml(l.message),n.style.display="block",n.style.background="color-mix(in srgb, var(--bad-fg) 15%, transparent)",n.style.border="1px solid var(--bad-fg)")}e&&(e.disabled=!1,e.innerHTML="\u25B6\uFE0F Run Backup Now")}async function a(){if(B){B.innerHTML='
Loading...
';try{var e=await fetch("/api/v1/backups/history?limit=50"),n=await e.json();if(!n.success||!n.history?.length){B.innerHTML='
\u{1F4CB} No backup history yet
';return}for(var t='
',i=0;i',t+='
',t+=' '+escapeHtml(o.name||"backup")+"",t+='
',t+=' '+escapeHtml(o.status)+"",o.status==="success"&&(t+=' '),t+="
",t+="
",t+='
',t+=" "+new Date(o.timestamp).toLocaleString()+" | "+d+" MB | "+(o.duration?(o.duration/1e3).toFixed(1)+"s":"--"),o.encrypted&&(t+=" | \u{1F512}"),t+="
",t+="
"}t+="",B.innerHTML=t,B.querySelectorAll(".backup-restore-btn").forEach(function(l){l.addEventListener("click",function(){window.__restoreServerBackup(l.dataset.backupId)})})}catch(l){B.innerHTML='
Failed: '+escapeHtml(l.message)+"
"}}}window.__restoreServerBackup=async function(e){if(confirm("Restore from this server backup? This will overwrite current configuration."))try{var n=await secureFetch("/api/v1/backups/restore/"+e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({restoreServices:!0,restoreConfig:!0})}),t=await n.json();t.success?(showNotification("Restore completed successfully!","success"),location.reload()):showNotification("Restore failed: "+(t.error||"Unknown error"),"error")}catch(i){showNotification("Restore error: "+i.message,"error")}},document.querySelector('[data-panel="backup-automated"]')?.addEventListener("click",H),document.querySelector('[data-panel="backup-history-tab"]')?.addEventListener("click",a)})(),(function(){injectModal("stats-modal",`

\u{1F4CA} Resource Monitor

- `);const w=document.getElementById("stats-modal"),B=document.getElementById("container-stats-btn"),z=document.getElementById("stats-cancel"),I=document.getElementById("stats-refresh-btn"),C=document.getElementById("stats-auto-refresh"),P=document.getElementById("stats-container"),$=document.getElementById("stats-aggregated-container"),L=document.getElementById("stats-alerts-container"),g=document.getElementById("stats-last-update");let k=null,f=null;function x(r){if(r===0||!r)return"0 B";const p=1024,m=["B","KB","MB","GB"],n=Math.floor(Math.log(r)/Math.log(p));return parseFloat((r/Math.pow(p,n)).toFixed(1))+" "+m[n]}function E(r){return r<30?"#2ecc71":r<70?"#f39c12":"#e74c3c"}function R(r){return r<50?"#2ecc71":r<80?"#f39c12":"#e74c3c"}async function O(){try{let r=null,p=!1;try{const e=await(await fetch("/api/v1/monitoring/stats")).json();e.success&&e.stats&&(r=e.stats,p=!0,f=e.stats)}catch{}if(!p){const e=await(await fetch("/api/v1/stats/containers")).json();if(e.success&&e.stats){r={};for(const a of e.stats)r[a.name]={name:a.name,current:{cpu:a.cpu,memory:{percent:a.memory.percent,usage:a.memory.used,limit:a.memory.limit,usageMB:Math.round(a.memory.used/1048576),limitMB:Math.round(a.memory.limit/1048576)},network:{rxBytes:a.network.rx,txBytes:a.network.tx,rxMB:(a.network.rx/1048576).toFixed(1),txMB:(a.network.tx/1048576).toFixed(1)},disk:{readMB:0,writeMB:0}},status:a.status};f=r}}if(!r||Object.keys(r).length===0){P.innerHTML='
No running containers found
';return}let m='
';for(const[n,e]of Object.entries(r)){const a=e.current||e,t=a.cpu?.percent||0,s=a.memory?.percent||0,i=E(t),h=R(s),d=a.memory?.usage||a.memory?.used||0,A=a.memory?.limit||0,F=a.network?.rxBytes||a.network?.rx||0,j=a.network?.txBytes||a.network?.tx||0,_=e.aggregated;m+=` +
`);const k=document.getElementById("stats-modal"),E=document.getElementById("container-stats-btn"),z=document.getElementById("stats-cancel"),I=document.getElementById("stats-refresh-btn"),L=document.getElementById("stats-auto-refresh"),A=document.getElementById("stats-container"),T=document.getElementById("stats-aggregated-container"),$=document.getElementById("stats-alerts-container"),y=document.getElementById("stats-last-update");let S=null,w=null;function x(s){if(s===0||!s)return"0 B";const h=1024,u=["B","KB","MB","GB"],a=Math.floor(Math.log(s)/Math.log(h));return parseFloat((s/Math.pow(h,a)).toFixed(1))+" "+u[a]}function C(s){return s<30?"#2ecc71":s<70?"#f39c12":"#e74c3c"}function M(s){return s<50?"#2ecc71":s<80?"#f39c12":"#e74c3c"}async function R(){try{let s=null,h=!1;try{const e=await(await fetch("/api/v1/monitoring/stats")).json();e.success&&e.stats&&(s=e.stats,h=!0,w=e.stats)}catch{}if(!h){const e=await(await fetch("/api/v1/stats/containers")).json();if(e.success&&e.stats){s={};for(const n of e.stats)s[n.name]={name:n.name,current:{cpu:n.cpu,memory:{percent:n.memory.percent,usage:n.memory.used,limit:n.memory.limit,usageMB:Math.round(n.memory.used/1048576),limitMB:Math.round(n.memory.limit/1048576)},network:{rxBytes:n.network.rx,txBytes:n.network.tx,rxMB:(n.network.rx/1048576).toFixed(1),txMB:(n.network.tx/1048576).toFixed(1)},disk:{readMB:0,writeMB:0}},status:n.status};w=s}}if(!s||Object.keys(s).length===0){A.innerHTML='
No running containers found
';return}let u='
';for(const[a,e]of Object.entries(s)){const n=e.current||e,t=n.cpu?.percent||0,i=n.memory?.percent||0,o=C(t),d=M(i),l=n.memory?.usage||n.memory?.used||0,N=n.memory?.limit||0,O=n.network?.rxBytes||n.network?.rx||0,F=n.network?.txBytes||n.network?.tx||0,q=e.aggregated;u+=`
- ${e.name||n} - ${_?`avg ${_.cpu?.avg?.toFixed(0)||0}% cpu`:""} + ${e.name||a} + ${q?`avg ${q.cpu?.avg?.toFixed(0)||0}% cpu`:""} ${e.status||"running"}
@@ -1068,32 +1125,32 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};
CPU
-
+
- ${t.toFixed(1)}% + ${t.toFixed(1)}%
Memory
-
+
- ${s.toFixed(1)}% + ${i.toFixed(1)}%
-
${x(d)} / ${x(A)}
+
${x(l)} / ${x(N)}
Network
- \u2193 ${x(F)} + \u2193 ${x(O)} / - \u2191 ${x(j)} + \u2191 ${x(F)}
-
`}m+="",P.innerHTML=m,g.textContent="Updated: "+new Date().toLocaleTimeString()}catch(r){P.innerHTML=`
\u274C Failed to load stats: ${escapeHtml(r.message)}
`}}async function D(){if(!$)return;const r=f;if(!r||Object.keys(r).length===0){$.innerHTML='
\u{1F4C8}No monitoring data available. Open the Live Stats tab first.
';return}let p='
';for(const[m,n]of Object.entries(r)){const e=n.aggregated;e&&(p+=`
-
${n.name||m}
+
`}u+="
",A.innerHTML=u,y.textContent="Updated: "+new Date().toLocaleTimeString()}catch(s){A.innerHTML=`
\u274C Failed to load stats: ${escapeHtml(s.message)}
`}}async function D(){if(!T)return;const s=w;if(!s||Object.keys(s).length===0){T.innerHTML='
\u{1F4C8}No monitoring data available. Open the Live Stats tab first.
';return}let h='
';for(const[u,a]of Object.entries(s)){const e=a.aggregated;e&&(h+=`
+
${a.name||u}
${e.cpu?.avg?.toFixed(1)||0}%Avg CPU
${e.cpu?.max?.toFixed(1)||0}%Max CPU
@@ -1101,49 +1158,49 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};
${e.memory?.max?.toFixed(1)||0}%Max Mem
${e.dataPoints?`
${e.dataPoints} data points over ${e.timeRange||24}h
`:""} -
`)}p+="
",$.innerHTML=p}async function N(){if(!L)return;L.innerHTML='
Loading alerts...
';const r=f;if(!r||Object.keys(r).length===0){L.innerHTML='
\u{1F514}No containers found. Open the Live Stats tab first.
';return}let p='
';for(const[m,n]of Object.entries(r)){const e=n.alertConfig||{};p+=`
+
`)}h+="
",T.innerHTML=h}async function j(){if(!$)return;$.innerHTML='
Loading alerts...
';const s=w;if(!s||Object.keys(s).length===0){$.innerHTML='
\u{1F514}No containers found. Open the Live Stats tab first.
';return}let h='
';for(const[u,a]of Object.entries(s)){const e=a.alertConfig||{};h+=`
- ${n.name||m} + ${a.name||u}
- +
- +
- +
- +
-
`}p+="
",L.innerHTML=p,L.querySelectorAll(".alert-save-btn").forEach(m=>{m.addEventListener("click",async()=>{const n=m.dataset.container,e=L.querySelector(`.alert-enabled[data-container="${n}"]`)?.checked||!1,a=parseInt(L.querySelector(`.alert-cpu[data-container="${n}"]`)?.value)||80,t=parseInt(L.querySelector(`.alert-mem[data-container="${n}"]`)?.value)||85,s=parseInt(L.querySelector(`.alert-cooldown[data-container="${n}"]`)?.value)||15,i=L.querySelector(`.alert-autorestart[data-container="${n}"]`)?.checked||!1;try{const d=await(await secureFetch(`/api/v1/monitoring/alerts/${n}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({enabled:e,cpuThreshold:a,memoryThreshold:t,cooldownMinutes:s,autoRestart:i})})).json();m.textContent=d.success?"\u2705 Saved":"\u26A0\uFE0F Failed",setTimeout(()=>{m.textContent="Save"},2e3)}catch{m.textContent="\u274C Error",setTimeout(()=>{m.textContent="Save"},2e3)}})})}function M(){k&&clearInterval(k),C?.checked&&(k=setInterval(O,DC.POLL.STATS))}function H(){k&&(clearInterval(k),k=null)}B?.addEventListener("click",()=>{w.classList.add("show"),O(),M()}),z?.addEventListener("click",()=>{w.classList.remove("show"),H()}),w?.addEventListener("click",r=>{r.target===w&&(w.classList.remove("show"),H())}),I?.addEventListener("click",O),C?.addEventListener("change",()=>{C.checked?M():H()}),document.querySelector('[data-panel="stats-aggregated"]')?.addEventListener("click",D),document.querySelector('[data-panel="stats-alerts"]')?.addEventListener("click",N);const S=document.getElementById("stats-history-container"),T=document.getElementById("stats-history-container-area"),b=document.querySelectorAll(".stats-range-btn");let u="1h";function y(r){switch(r){case"1h":return 3600*1e3;case"24h":return 1440*60*1e3;case"7d":return 10080*60*1e3;case"30d":return 720*60*60*1e3;case"1y":return 365*24*60*60*1e3;default:return 3600*1e3}}function c(r){return r==="raw"?"live (10s samples)":r==="hourly"?"hourly average":r==="daily"?"daily average":r}function o(r,p,m,n,e){if(!r||r.length===0)return`
No data for ${escapeHtml(n)}
`;const a=r.map(p).filter(W=>W!=null);if(a.length===0)return`
No data for ${escapeHtml(n)}
`;const t=Math.max(...a,1),s=Math.min(...a,0),i=t-s||1,h=600,d=80,A=4,F=(h-A*2)/Math.max(a.length-1,1),j=a.map((W,Z)=>{const G=A+Z*F,Q=d-A-(W-s)/i*(d-A*2);return`${G.toFixed(1)},${Q.toFixed(1)}`}).join(" "),_=a[a.length-1],U=a.reduce((W,Z)=>W+Z,0)/a.length;return` + `}h+="",$.innerHTML=h,$.querySelectorAll(".alert-save-btn").forEach(u=>{u.addEventListener("click",async()=>{const a=u.dataset.container,e=$.querySelector(`.alert-enabled[data-container="${a}"]`)?.checked||!1,n=parseInt($.querySelector(`.alert-cpu[data-container="${a}"]`)?.value)||80,t=parseInt($.querySelector(`.alert-mem[data-container="${a}"]`)?.value)||85,i=parseInt($.querySelector(`.alert-cooldown[data-container="${a}"]`)?.value)||15,o=$.querySelector(`.alert-autorestart[data-container="${a}"]`)?.checked||!1;try{const l=await(await secureFetch(`/api/v1/monitoring/alerts/${a}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({enabled:e,cpuThreshold:n,memoryThreshold:t,cooldownMinutes:i,autoRestart:o})})).json();u.textContent=l.success?"\u2705 Saved":"\u26A0\uFE0F Failed",setTimeout(()=>{u.textContent="Save"},2e3)}catch{u.textContent="\u274C Error",setTimeout(()=>{u.textContent="Save"},2e3)}})})}function B(){S&&clearInterval(S),L?.checked&&(S=setInterval(R,DC.POLL.STATS))}function P(){S&&(clearInterval(S),S=null)}E?.addEventListener("click",()=>{k.classList.add("show"),R(),B()}),z?.addEventListener("click",()=>{k.classList.remove("show"),P()}),k?.addEventListener("click",s=>{s.target===k&&(k.classList.remove("show"),P())}),I?.addEventListener("click",R),L?.addEventListener("change",()=>{L.checked?B():P()}),document.querySelector('[data-panel="stats-aggregated"]')?.addEventListener("click",D),document.querySelector('[data-panel="stats-alerts"]')?.addEventListener("click",j);const b=document.getElementById("stats-history-container"),H=document.getElementById("stats-history-container-area"),f=document.querySelectorAll(".stats-range-btn");let p="1h";function g(s){switch(s){case"1h":return 3600*1e3;case"24h":return 1440*60*1e3;case"7d":return 10080*60*1e3;case"30d":return 720*60*60*1e3;case"1y":return 365*24*60*60*1e3;default:return 3600*1e3}}function v(s){return s==="raw"?"live (10s samples)":s==="hourly"?"hourly average":s==="daily"?"daily average":s}function m(s,h,u,a,e){if(!s||s.length===0)return`
No data for ${escapeHtml(a)}
`;const n=s.map(h).filter(G=>G!=null);if(n.length===0)return`
No data for ${escapeHtml(a)}
`;const t=Math.max(...n,1),i=Math.min(...n,0),o=t-i||1,d=600,l=80,N=4,O=(d-N*2)/Math.max(n.length-1,1),F=n.map((G,W)=>{const X=N+W*O,Q=l-N-(G-i)/o*(l-N*2);return`${X.toFixed(1)},${Q.toFixed(1)}`}).join(" "),q=n[n.length-1],U=n.reduce((G,W)=>G+W,0)/n.length;return`
- ${escapeHtml(n)} - last ${_.toFixed(1)}${e} \xB7 avg ${U.toFixed(1)}${e} \xB7 max ${t.toFixed(1)}${e} + ${escapeHtml(a)} + last ${q.toFixed(1)}${e} \xB7 avg ${U.toFixed(1)}${e} \xB7 max ${t.toFixed(1)}${e}
- - + +
- `}function l(){if(!S)return;const r=f||{},p=S.value,m=Object.entries(r);if(m.length===0){S.innerHTML='';return}S.innerHTML=m.map(([n,e])=>``).join(""),p&&r[p]&&(S.value=p)}async function v(){if(!T||!S)return;const r=S.value;if(!r){T.innerHTML='
\u{1F4CA}No container selected.
';return}const p=Date.now(),m=p-y(u);T.innerHTML='
Loading history...
';try{const e=await(await fetch(`/api/v1/monitoring/history/${encodeURIComponent(r)}?startTime=${m}&endTime=${p}`)).json();if(!e.success)throw new Error(e.error||"Failed to load history");const a=e.samples||[],t=e.tier||"raw";if(a.length===0){T.innerHTML=`
\u{1F4CA}No data for the last ${u}. Tier: ${c(t)}.
`;return}const s=t==="raw",i=s?j=>j.cpu?.percent:j=>j.cpu?.avg,h=s?j=>j.memory?.percent:j=>j.memory?.avgPercent,d=s?j=>j.network?.rxMB||0:j=>j.network?.rxMB||0,A=s?j=>j.network?.txMB||0:j=>j.network?.txMB||0;let F=` + `}function r(){if(!b)return;const s=w||{},h=b.value,u=Object.entries(s);if(u.length===0){b.innerHTML='';return}b.innerHTML=u.map(([a,e])=>``).join(""),h&&s[h]&&(b.value=h)}async function c(){if(!H||!b)return;const s=b.value;if(!s){H.innerHTML='
\u{1F4CA}No container selected.
';return}const h=Date.now(),u=h-g(p);H.innerHTML='
Loading history...
';try{const e=await(await fetch(`/api/v1/monitoring/history/${encodeURIComponent(s)}?startTime=${u}&endTime=${h}`)).json();if(!e.success)throw new Error(e.error||"Failed to load history");const n=e.samples||[],t=e.tier||"raw";if(n.length===0){H.innerHTML=`
\u{1F4CA}No data for the last ${p}. Tier: ${v(t)}.
`;return}const i=t==="raw",o=i?F=>F.cpu?.percent:F=>F.cpu?.avg,d=i?F=>F.memory?.percent:F=>F.memory?.avgPercent,l=i?F=>F.network?.rxMB||0:F=>F.network?.rxMB||0,N=i?F=>F.network?.txMB||0:F=>F.network?.txMB||0;let O=`
- ${a.length} samples \xB7 ${escapeHtml(c(t))} \xB7 ${new Date(m).toLocaleString()} \u2192 ${new Date(p).toLocaleString()} + ${n.length} samples \xB7 ${escapeHtml(v(t))} \xB7 ${new Date(u).toLocaleString()} \u2192 ${new Date(h).toLocaleString()}
- `;F+=o(a,i,"#2ecc71","CPU","%"),F+=o(a,h,"#3498db","Memory","%"),F+=o(a,d,"#9b59b6","Network RX"," MB"),F+=o(a,A,"#e67e22","Network TX"," MB"),T.innerHTML=F}catch(n){T.innerHTML=`
\u26A0\uFE0FFailed to load history: ${escapeHtml(n.message)}
`}}b.forEach(r=>{r.addEventListener("click",()=>{b.forEach(p=>p.classList.remove("active")),r.classList.add("active"),u=r.dataset.range,v()})}),S?.addEventListener("change",v),document.querySelector('[data-panel="stats-history"]')?.addEventListener("click",()=>{l(),v()})})(),(function(){injectModal("health-modal",`
+ `;O+=m(n,o,"#2ecc71","CPU","%"),O+=m(n,d,"#3498db","Memory","%"),O+=m(n,l,"#9b59b6","Network RX"," MB"),O+=m(n,N,"#e67e22","Network TX"," MB"),H.innerHTML=O}catch(a){H.innerHTML=`
\u26A0\uFE0FFailed to load history: ${escapeHtml(a.message)}
`}}f.forEach(s=>{s.addEventListener("click",()=>{f.forEach(h=>h.classList.remove("active")),s.classList.add("active"),p=s.dataset.range,c()})}),b?.addEventListener("change",c),document.querySelector('[data-panel="stats-history"]')?.addEventListener("click",()=>{r(),c()})})(),(function(){injectModal("health-modal",`

\u{1F3E5} Health Check Dashboard

-
`);const w=document.getElementById("health-modal"),B=document.getElementById("health-check-btn"),z=document.getElementById("health-cancel"),I=document.getElementById("health-refresh-btn"),C=document.getElementById("health-status-container"),P=document.getElementById("health-incidents-container"),$=document.getElementById("health-config-container"),L=document.getElementById("health-last-update"),g=document.getElementById("health-add-btn"),k=document.getElementById("health-config-form"),f=document.getElementById("health-form-title"),x=document.getElementById("health-form-cancel"),E=document.getElementById("health-form-save");let R=null;function O(b){return b>=99.9?"var(--ok-fg)":b>=95?"#f39c12":"var(--bad-fg)"}function D(b){const u={critical:"var(--bad-fg)",high:"#ff6b6b",medium:"#f39c12",low:"var(--muted)"};return`${b}`}async function N(){try{const u=await(await fetch("/api/v1/health-checks/status")).json();if(!u.success||!u.status||Object.keys(u.status).length===0){C.innerHTML='
\u{1F3E5}No health checks configured. Go to the Configure tab to add services.
';return}const y=Object.values(u.status);let c='';c+='',c+='',c+='',c+='';for(const o of y){const l=o.status==="up",v=l?"var(--dot-ok)":"var(--dot-bad)",r=o.uptime?.["24h"]??"-",p=o.uptime?.["7d"]??"-",m=o.avgResponseTime!=null?Math.round(o.avgResponseTime)+"ms":"-",n=o.timestamp?timeAgo(o.timestamp):"-";c+=``,c+=``,c+=``,c+=``,c+=``,c+=``,c+=``,c+="",c+=``}c+="
ServiceStatusUptime 24hUptime 7dAvg ResponseLast Check
${escapeHtml(o.name||o.serviceId)}${l?"Up":"Down"}${typeof r=="number"?r.toFixed(1)+"%":r}${typeof p=="number"?p.toFixed(1)+"%":p}${m}${n}
",C.innerHTML=c,L.textContent="Updated "+new Date().toLocaleTimeString(),C.querySelectorAll("tr[data-health-id]").forEach(o=>{o.addEventListener("click",async()=>{const l=o.dataset.healthId,v=document.getElementById("health-detail-"+l);if(v){if(v.style.display!=="none"){v.style.display="none";return}v.style.display="";try{const p=await(await fetch(`/api/v1/health-checks/${l}/stats?hours=24`)).json();if(p.success&&p.stats){const m=p.stats,n=m.responseTime||{};v.querySelector("td").innerHTML=` + `);const k=document.getElementById("health-modal"),E=document.getElementById("health-check-btn"),z=document.getElementById("health-cancel"),I=document.getElementById("health-refresh-btn"),L=document.getElementById("health-status-container"),A=document.getElementById("health-incidents-container"),T=document.getElementById("health-config-container"),$=document.getElementById("health-last-update"),y=document.getElementById("health-add-btn"),S=document.getElementById("health-config-form"),w=document.getElementById("health-form-title"),x=document.getElementById("health-form-cancel"),C=document.getElementById("health-form-save");let M=null;function R(f){return f>=99.9?"var(--ok-fg)":f>=95?"#f39c12":"var(--bad-fg)"}function D(f){const p={critical:"var(--bad-fg)",high:"#ff6b6b",medium:"#f39c12",low:"var(--muted)"};return`${f}`}async function j(){try{const p=await(await fetch("/api/v1/health-checks/status")).json();if(!p.success||!p.status||Object.keys(p.status).length===0){L.innerHTML='
\u{1F3E5}No health checks configured. Go to the Configure tab to add services.
';return}const g=Object.values(p.status);let v='';v+='',v+='',v+='',v+='';for(const m of g){const r=m.status==="up",c=r?"var(--dot-ok)":"var(--dot-bad)",s=m.uptime?.["24h"]??"-",h=m.uptime?.["7d"]??"-",u=m.avgResponseTime!=null?Math.round(m.avgResponseTime)+"ms":"-",a=m.timestamp?timeAgo(m.timestamp):"-";v+=``,v+=``,v+=``,v+=``,v+=``,v+=``,v+=``,v+="",v+=``}v+="
ServiceStatusUptime 24hUptime 7dAvg ResponseLast Check
${escapeHtml(m.name||m.serviceId)}${r?"Up":"Down"}${typeof s=="number"?s.toFixed(1)+"%":s}${typeof h=="number"?h.toFixed(1)+"%":h}${u}${a}
",L.innerHTML=v,$.textContent="Updated "+new Date().toLocaleTimeString(),L.querySelectorAll("tr[data-health-id]").forEach(m=>{m.addEventListener("click",async()=>{const r=m.dataset.healthId,c=document.getElementById("health-detail-"+r);if(c){if(c.style.display!=="none"){c.style.display="none";return}c.style.display="";try{const h=await(await fetch(`/api/v1/health-checks/${r}/stats?hours=24`)).json();if(h.success&&h.stats){const u=h.stats,a=u.responseTime||{};c.querySelector("td").innerHTML=`
-
Total Checks
${m.totalChecks||0}
-
Uptime
${(m.uptime||0).toFixed(2)}%
-
Avg Response
${Math.round(n.avg||0)}ms
-
P95 / P99
${Math.round(n.p95||0)}ms / ${Math.round(n.p99||0)}ms
-
Min Response
${Math.round(n.min||0)}ms
-
Max Response
${Math.round(n.max||0)}ms
-
Up Checks
${m.upChecks||0}
-
Down Checks
${m.downChecks||0}
-
`}else v.querySelector("td").innerHTML='
No detailed stats available for this period.
'}catch(r){v.querySelector("td").innerHTML=`
Failed: ${escapeHtml(r.message)}
`}}})})}catch(b){C.innerHTML=`
Failed to load health status: ${escapeHtml(b.message)}
`}}async function M(){try{const[b,u]=await Promise.all([fetch("/api/v1/health-checks/incidents"),fetch("/api/v1/health-checks/incidents/history?limit=50")]),y=await b.json(),c=await u.json();let o="";const l=y.success&&y.incidents?y.incidents:[];if(l.length>0){o+='

Open Incidents ('+l.length+")

";for(const r of l)o+=`
+
Total Checks
${u.totalChecks||0}
+
Uptime
${(u.uptime||0).toFixed(2)}%
+
Avg Response
${Math.round(a.avg||0)}ms
+
P95 / P99
${Math.round(a.p95||0)}ms / ${Math.round(a.p99||0)}ms
+
Min Response
${Math.round(a.min||0)}ms
+
Max Response
${Math.round(a.max||0)}ms
+
Up Checks
${u.upChecks||0}
+
Down Checks
${u.downChecks||0}
+
`}else c.querySelector("td").innerHTML='
No detailed stats available for this period.
'}catch(s){c.querySelector("td").innerHTML=`
Failed: ${escapeHtml(s.message)}
`}}})})}catch(f){L.innerHTML=`
Failed to load health status: ${escapeHtml(f.message)}
`}}async function B(){try{const[f,p]=await Promise.all([fetch("/api/v1/health-checks/incidents"),fetch("/api/v1/health-checks/incidents/history?limit=50")]),g=await f.json(),v=await p.json();let m="";const r=g.success&&g.incidents?g.incidents:[];if(r.length>0){m+='

Open Incidents ('+r.length+")

";for(const s of r)m+=`
- ${escapeHtml(r.serviceId)} - ${D(r.severity)} + ${escapeHtml(s.serviceId)} + ${D(s.severity)}
-
${escapeHtml(r.message)}
-
Started ${timeAgo(r.createdAt)} \xB7 ${r.occurrences||1} occurrence(s)
-
`;o+="
"}else o+='
All services operational \u2014 no open incidents
';const v=c.success&&c.history?c.history:[];if(v.length>0){o+='

Incident History

',o+='',o+='';for(const r of v){const p=r.status==="resolved",m=p&&r.duration?r.duration<6e4?Math.round(r.duration/1e3)+"s":Math.round(r.duration/6e4)+"m":"-";o+='',o+=``,o+=``,o+=``,o+=``,o+=``,o+=``,o+=""}o+="
ServiceTypeSeverityStatusDurationWhen
${escapeHtml(r.serviceId)}${escapeHtml(r.type)}${D(r.severity)}${r.status}${m}${timeAgo(r.createdAt)}
"}P.innerHTML=o||'
\u{1F6A8}No incidents recorded yet.
'}catch(b){P.innerHTML=`
Failed: ${escapeHtml(b.message)}
`}}async function H(){try{const u=await(await fetch("/api/v1/health-checks/status")).json(),y=u.success&&u.status?Object.values(u.status):[];if(y.length===0){$.innerHTML='
\u2699\uFE0FNo health checks configured yet. Click "Add Health Check" below.
';return}let c='';c+='';for(const o of y){const l=o.status==="up";c+='',c+=``,c+=``,c+=``,c+='"}c+="
ServiceStatusSLA TargetActions
${escapeHtml(o.name||o.serviceId)}${l?"Up":"Down"}${o.sla?.target?o.sla.target+"%":"-"}',c+=``,c+=``,c+="
",$.innerHTML=c}catch(b){$.innerHTML=`
Failed: ${escapeHtml(b.message)}
`}}function S(b,u,y,c,o,l,v){R=b||null,f.textContent=b?"Edit Health Check":"Add Health Check",document.getElementById("health-form-id").value=b||"",document.getElementById("health-form-id").disabled=!!b,document.getElementById("health-form-name").value=u||"",document.getElementById("health-form-url").value=y||"",document.getElementById("health-form-timeout").value=c||1e4,document.getElementById("health-form-codes").value=o||"200",document.getElementById("health-form-sla").value=l||99.9,document.getElementById("health-form-slow").value=v||5e3,k.style.display="",g.style.display="none"}function T(){k.style.display="none",g.style.display="",R=null}g?.addEventListener("click",()=>S("","","",1e4,"200",99.9,5e3)),x?.addEventListener("click",T),E?.addEventListener("click",async()=>{const b=R||document.getElementById("health-form-id").value.trim();if(!b)return showNotification("Service ID is required","warning");const u=document.getElementById("health-form-url").value.trim();if(!u)return showNotification("URL is required","warning");const y=document.getElementById("health-form-codes").value.split(",").map(o=>parseInt(o.trim())).filter(Boolean),c={name:document.getElementById("health-form-name").value.trim()||b,url:u,timeout:parseInt(document.getElementById("health-form-timeout").value)||1e4,expectedStatusCodes:y.length?y:[200],sla:{target:parseFloat(document.getElementById("health-form-sla").value)||99.9},slowResponseThreshold:parseInt(document.getElementById("health-form-slow").value)||5e3};try{E.textContent="Saving...",E.disabled=!0;const l=await(await secureFetch(`/api/v1/health-checks/${encodeURIComponent(b)}/configure`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(c)})).json();if(!l.success)throw new Error(l.error||"Save failed");T(),H(),N()}catch(o){showNotification("Error: "+o.message,"error")}finally{E.textContent="Save",E.disabled=!1}}),document.addEventListener("health-edit",async b=>{const u=b.detail;S(u,"","",1e4,"200",99.9,5e3)}),document.addEventListener("health-delete",async b=>{const u=b.detail;if(confirm(`Delete health check for "${u}"?`))try{const c=await(await secureFetch(`/api/v1/health-checks/${encodeURIComponent(u)}/configure`,{method:"DELETE"})).json();if(!c.success)throw new Error(c.error);H(),N()}catch(y){showNotification("Error: "+y.message,"error")}}),B?.addEventListener("click",()=>{w?.classList.add("show"),N()}),wireModal(w,z),I?.addEventListener("click",N),document.querySelector('[data-panel="health-incidents"]')?.addEventListener("click",M),document.querySelector('[data-panel="health-config"]')?.addEventListener("click",H)})(),(function(){injectModal("updates-modal",`
+
${escapeHtml(s.message)}
+
Started ${timeAgo(s.createdAt)} \xB7 ${s.occurrences||1} occurrence(s)
+
`;m+="
"}else m+='
All services operational \u2014 no open incidents
';const c=v.success&&v.history?v.history:[];if(c.length>0){m+='

Incident History

',m+='',m+='';for(const s of c){const h=s.status==="resolved",u=h&&s.duration?s.duration<6e4?Math.round(s.duration/1e3)+"s":Math.round(s.duration/6e4)+"m":"-";m+='',m+=``,m+=``,m+=``,m+=``,m+=``,m+=``,m+=""}m+="
ServiceTypeSeverityStatusDurationWhen
${escapeHtml(s.serviceId)}${escapeHtml(s.type)}${D(s.severity)}${s.status}${u}${timeAgo(s.createdAt)}
"}A.innerHTML=m||'
\u{1F6A8}No incidents recorded yet.
'}catch(f){A.innerHTML=`
Failed: ${escapeHtml(f.message)}
`}}async function P(){try{const p=await(await fetch("/api/v1/health-checks/status")).json(),g=p.success&&p.status?Object.values(p.status):[];if(g.length===0){T.innerHTML='
\u2699\uFE0FNo health checks configured yet. Click "Add Health Check" below.
';return}let v='';v+='';for(const m of g){const r=m.status==="up";v+='',v+=``,v+=``,v+=``,v+='"}v+="
ServiceStatusSLA TargetActions
${escapeHtml(m.name||m.serviceId)}${r?"Up":"Down"}${m.sla?.target?m.sla.target+"%":"-"}',v+=``,v+=``,v+="
",T.innerHTML=v}catch(f){T.innerHTML=`
Failed: ${escapeHtml(f.message)}
`}}function b(f,p,g,v,m,r,c){M=f||null,w.textContent=f?"Edit Health Check":"Add Health Check",document.getElementById("health-form-id").value=f||"",document.getElementById("health-form-id").disabled=!!f,document.getElementById("health-form-name").value=p||"",document.getElementById("health-form-url").value=g||"",document.getElementById("health-form-timeout").value=v||1e4,document.getElementById("health-form-codes").value=m||"200",document.getElementById("health-form-sla").value=r||99.9,document.getElementById("health-form-slow").value=c||5e3,S.style.display="",y.style.display="none"}function H(){S.style.display="none",y.style.display="",M=null}y?.addEventListener("click",()=>b("","","",1e4,"200",99.9,5e3)),x?.addEventListener("click",H),C?.addEventListener("click",async()=>{const f=M||document.getElementById("health-form-id").value.trim();if(!f)return showNotification("Service ID is required","warning");const p=document.getElementById("health-form-url").value.trim();if(!p)return showNotification("URL is required","warning");const g=document.getElementById("health-form-codes").value.split(",").map(m=>parseInt(m.trim())).filter(Boolean),v={name:document.getElementById("health-form-name").value.trim()||f,url:p,timeout:parseInt(document.getElementById("health-form-timeout").value)||1e4,expectedStatusCodes:g.length?g:[200],sla:{target:parseFloat(document.getElementById("health-form-sla").value)||99.9},slowResponseThreshold:parseInt(document.getElementById("health-form-slow").value)||5e3};try{C.textContent="Saving...",C.disabled=!0;const r=await(await secureFetch(`/api/v1/health-checks/${encodeURIComponent(f)}/configure`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(v)})).json();if(!r.success)throw new Error(r.error||"Save failed");H(),P(),j()}catch(m){showNotification("Error: "+m.message,"error")}finally{C.textContent="Save",C.disabled=!1}}),document.addEventListener("health-edit",async f=>{const p=f.detail;b(p,"","",1e4,"200",99.9,5e3)}),document.addEventListener("health-delete",async f=>{const p=f.detail;if(confirm(`Delete health check for "${p}"?`))try{const v=await(await secureFetch(`/api/v1/health-checks/${encodeURIComponent(p)}/configure`,{method:"DELETE"})).json();if(!v.success)throw new Error(v.error);P(),j()}catch(g){showNotification("Error: "+g.message,"error")}}),E?.addEventListener("click",()=>{k?.classList.add("show"),j()}),wireModal(k,z),I?.addEventListener("click",j),document.querySelector('[data-panel="health-incidents"]')?.addEventListener("click",B),document.querySelector('[data-panel="health-config"]')?.addEventListener("click",P)})(),(function(){injectModal("updates-modal",`

\u2B06\uFE0F Update Management

- `);const w=document.getElementById("updates-modal"),B=document.getElementById("updates-btn"),z=document.getElementById("updates-cancel"),I=document.getElementById("updates-check-btn"),C=document.getElementById("updates-available-container"),P=document.getElementById("updates-history-container"),$=document.getElementById("updates-auto-container"),L=document.getElementById("updates-last-check");async function g(){try{const m=await(await fetch("/api/v1/updates/available")).json();if(!m.success)throw new Error(m.error);const n=m.updates||[];if(n.length===0){C.innerHTML='
\u2705All containers are up to date.
',L.textContent="";return}let e='';e+='';for(const a of n)e+='',e+=``,e+=``,e+=``,e+=``,e+='";e+="
ContainerImageCurrentLatestActions
${escapeHtml(a.containerName)}${escapeHtml(a.imageName)}${escapeHtml(a.currentDigest)}${escapeHtml(a.latestDigest)}',e+=``,e+=``,e+="
",C.innerHTML=e,L.textContent=n.length+" update(s) available",C.querySelectorAll(".update-now-btn").forEach(a=>{a.addEventListener("click",async()=>{const t=a.dataset.id,s=a.dataset.name;if(confirm(`Update "${s}" to the latest version? The container will restart.`)){a.textContent="Updating...",a.disabled=!0;try{const h=await(await secureFetch(`/api/v1/updates/update/${encodeURIComponent(t)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({autoRollback:!0})})).json();if(h.success)a.textContent="Done!",a.style.background="var(--ok-fg)",setTimeout(()=>g(),2e3);else throw new Error(h.error||"Update failed")}catch(i){a.textContent="Failed",a.style.color="var(--bad-fg)",showNotification("Update error: "+i.message,"error"),setTimeout(()=>{a.textContent="Update",a.disabled=!1,a.style.color="",a.style.background=""},3e3)}}})}),C.querySelectorAll(".rollback-btn").forEach(a=>{a.addEventListener("click",async()=>{const t=a.dataset.id,s=a.dataset.name;if(confirm(`Rollback "${s}" to its previous version?`)){a.textContent="Rolling back...",a.disabled=!0;try{const h=await(await secureFetch(`/api/v1/updates/rollback/${encodeURIComponent(t)}`,{method:"POST"})).json();if(h.success)a.textContent="Rolled back!",setTimeout(()=>g(),2e3);else throw new Error(h.error||"Rollback failed")}catch(i){a.textContent="Failed",showNotification("Rollback error: "+i.message,"error"),setTimeout(()=>{a.textContent="Rollback",a.disabled=!1},3e3)}}})})}catch(p){C.innerHTML=`
Failed: ${escapeHtml(p.message)}
`}}async function k(){I.textContent="\u{1F50D} Checking...",I.disabled=!0;try{const m=await(await secureFetch("/api/v1/updates/check",{method:"POST"})).json();if(!m.success)throw new Error(m.error);I.textContent="\u2705 Done!",await g()}catch(p){I.textContent="\u274C Failed",showNotification("Check error: "+p.message,"error")}setTimeout(()=>{I.textContent="\u{1F50D} Check for Updates",I.disabled=!1},3e3)}async function f(){try{P.innerHTML='
Loading...
';const m=await(await fetch("/api/v1/updates/history?limit=50")).json(),n=m.success&&m.history?m.history:[];if(n.length===0){P.innerHTML='
\u{1F4CB}No update history yet.
';return}let e='';e+='';for(const a of n){const t=a.status==="success",s=a.duration?a.duration<1e3?a.duration+"ms":Math.round(a.duration/1e3)+"s":"-";e+='',e+=``,e+=``,e+=``,e+=``,e+=``,e+="",!t&&a.error&&(e+=``)}e+="
WhenContainerImageDurationStatus
${timeAgo(a.timestamp)}${escapeHtml(a.containerName)}${escapeHtml(a.imageName)}${s}${t?"\u2713 success":"\u2717 failed"}
${escapeHtml(a.error)}
",P.innerHTML=e}catch(p){P.innerHTML=`
Failed: ${escapeHtml(p.message)}
`}}async function x(){try{$.innerHTML='
Loading...
';const[p,m]=await Promise.all([fetch("/api/v1/stats/containers"),fetch("/api/v1/updates/auto-update")]),n=await p.json(),e=await m.json(),a=n.success&&n.stats?n.stats:[],t=e.success&&e.config?e.config:{};if(a.length===0){$.innerHTML='
\u{1F916}No running containers found.
';return}let s='
Auto-updates run during maintenance window (default 2AM-4AM). Daily = every day, Weekly = Sundays, Monthly = 1st of month.
';s+='',s+='';for(const i of a){const h=i.name||i.Names?.[0]?.replace(/^\//,"")||i.Id?.substring(0,12),d=i.containerId||i.Id,A=t[d]||{},F=A.enabled?A.schedule||"weekly":"",j=A.autoRollback!==!1,_=A.maintenanceWindow||"",U=A.lastAutoUpdate?timeAgo(A.lastAutoUpdate):"Never";s+=``,s+=``,s+=``,s+=``,s+=``,s+=``,s+=``,s+=""}s+="
ContainerScheduleWindowRollbackLast RunActions
${escapeHtml(h)} - ${U}
",$.innerHTML=s,$.querySelectorAll(".save-auto-btn").forEach(i=>{i.addEventListener("click",async()=>{const h=i.dataset.id,d=i.closest("tr"),A=d.querySelector(".auto-schedule").value,F=d.querySelector(".auto-rollback").checked,j=d.querySelector(".auto-window").value.trim();i.textContent="Saving...",i.disabled=!0;try{const U=await(await secureFetch(`/api/v1/updates/auto-update/${encodeURIComponent(h)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({enabled:!!A,schedule:A||"weekly",autoRollback:F,maintenanceWindow:j||void 0})})).json();if(U.success)i.textContent="\u2713 Saved";else throw new Error(U.error)}catch(_){i.textContent="\u2717 Error",showNotification("Save error: "+_.message,"error")}setTimeout(()=>{i.textContent="Save",i.disabled=!1},2e3)})})}catch(p){$.innerHTML=`
Failed: ${escapeHtml(p.message)}
`}}const E=document.getElementById("dashcaddy-current-version"),R=document.getElementById("dashcaddy-update-badge"),O=document.getElementById("dashcaddy-update-details"),D=document.getElementById("dashcaddy-new-version"),N=document.getElementById("dashcaddy-changelog"),M=document.getElementById("dashcaddy-apply-btn"),H=document.getElementById("dashcaddy-check-btn"),S=document.getElementById("dashcaddy-rollback-btn"),T=document.getElementById("dashcaddy-status-bar"),b=document.getElementById("dashcaddy-history-container");let u=null;function y(p,m){T&&(T.style.display="block",T.style.background=m==="error"?"var(--bad-bg)":m==="success"?"var(--ok-bg)":"var(--bg)",T.style.color=m==="error"?"var(--bad-fg)":m==="success"?"var(--ok-fg)":"var(--fg)",T.textContent=p)}async function c(){try{const m=await(await fetch("/api/v1/system/version")).json();m.success&&(E.textContent="v"+m.version+(m.commit?" ("+m.commit.substring(0,7)+")":""))}catch{E.textContent="Unable to fetch version"}}async function o(p){p||(H.textContent="Checking...",H.disabled=!0);try{const n=await(await fetch("/api/v1/system/update-check")).json();if(u=n,n.success&&n.available&&n.remote){R.style.display="",O.style.display="",D.textContent="v"+n.remote.version,N.textContent=n.remote.changelog||"No changelog available.";const e=document.getElementById("updates-btn");if(e&&!e.querySelector(".update-dot")){const t=document.createElement("span");t.className="update-dot",t.style.cssText="position:absolute;top:2px;right:2px;width:8px;height:8px;border-radius:50%;background:var(--accent);",e.style.position="relative",e.appendChild(t)}const a=document.getElementById("updates-dashcaddy-tab");if(a&&!a.querySelector(".update-dot")){const t=document.createElement("span");t.className="update-dot",t.style.cssText="display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--accent);margin-left:4px;vertical-align:middle;",a.appendChild(t)}}else R.style.display="none",O.style.display="none",await c(),p||y("You are running the latest version.","success");p||(H.textContent="Check for Updates",H.disabled=!1)}catch(m){p||(y("Failed to check: "+m.message,"error"),H.textContent="Check for Updates",H.disabled=!1)}}async function l(){if(confirm("Apply DashCaddy update? The API container will restart.")){M.textContent="Updating...",M.disabled=!0,y("Downloading and applying update...","info");try{const m=await(await secureFetch("/api/v1/system/update-apply",{method:"POST"})).json();if(m.success)y("Update initiated: v"+(m.fromVersion||"?")+" \u2192 v"+(m.toVersion||"?")+". The container will restart shortly.","success"),M.textContent="Applied!",document.querySelectorAll(".update-dot").forEach(n=>n.remove());else throw new Error(m.error||"Update failed")}catch(p){y("Update failed: "+p.message,"error"),M.textContent="Update Now",M.disabled=!1}}}async function v(){try{const m=await(await fetch("/api/v1/system/update-history")).json(),n=m.success&&m.history?m.history:[];if(n.length===0){b.innerHTML='
\u{1F4E6}No self-update history.
';return}let e='';e+='';for(const a of n){const t=a.status==="success"?"\u2713 success":a.status==="pending"?"\u23F3 pending":a.status==="partial"?"\u26A0 partial":"\u2717 "+a.status,s=a.status==="success"?"var(--ok-fg)":a.status==="pending"?"var(--muted)":"var(--bad-fg)";e+='',e+='",e+='",e+='",e+='",e+="",a.error&&(e+='"),a.note&&(e+='")}e+="
WhenVersionFromStatus
'+timeAgo(a.timestamp)+"v'+escapeHtml(a.version)+(a.rollback?" (rollback)":"")+"v'+escapeHtml(a.fromVersion||"?")+"'+t+"
'+escapeHtml(a.error)+"
'+escapeHtml(a.note)+"
",b.innerHTML=e}catch(p){b.innerHTML='
Failed: '+escapeHtml(p.message)+"
"}}async function r(){try{const m=await(await fetch("/api/v1/system/rollback-versions")).json(),n=m.success&&m.versions?m.versions:[];if(n.length===0){showNotification("No rollback versions available.","info");return}const e=prompt(`Available rollback versions: -`+n.join(` + `);const k=document.getElementById("updates-modal"),E=document.getElementById("updates-btn"),z=document.getElementById("updates-cancel"),I=document.getElementById("updates-check-btn"),L=document.getElementById("updates-available-container"),A=document.getElementById("updates-history-container"),T=document.getElementById("updates-auto-container"),$=document.getElementById("updates-last-check");async function y(){try{const u=await(await fetch("/api/v1/updates/available")).json();if(!u.success)throw new Error(u.error);const a=u.updates||[];if(a.length===0){L.innerHTML='
\u2705All containers are up to date.
',$.textContent="";return}let e='';e+='';for(const n of a)e+='',e+=``,e+=``,e+=``,e+=``,e+='";e+="
ContainerImageCurrentLatestActions
${escapeHtml(n.containerName)}${escapeHtml(n.imageName)}${escapeHtml(n.currentDigest)}${escapeHtml(n.latestDigest)}',e+=``,e+=``,e+="
",L.innerHTML=e,$.textContent=a.length+" update(s) available",L.querySelectorAll(".update-now-btn").forEach(n=>{n.addEventListener("click",async()=>{const t=n.dataset.id,i=n.dataset.name;if(confirm(`Update "${i}" to the latest version? The container will restart.`)){n.textContent="Updating...",n.disabled=!0;try{const d=await(await secureFetch(`/api/v1/updates/update/${encodeURIComponent(t)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({autoRollback:!0})})).json();if(d.success)n.textContent="Done!",n.style.background="var(--ok-fg)",setTimeout(()=>y(),2e3);else throw new Error(d.error||"Update failed")}catch(o){n.textContent="Failed",n.style.color="var(--bad-fg)",showNotification("Update error: "+o.message,"error"),setTimeout(()=>{n.textContent="Update",n.disabled=!1,n.style.color="",n.style.background=""},3e3)}}})}),L.querySelectorAll(".rollback-btn").forEach(n=>{n.addEventListener("click",async()=>{const t=n.dataset.id,i=n.dataset.name;if(confirm(`Rollback "${i}" to its previous version?`)){n.textContent="Rolling back...",n.disabled=!0;try{const d=await(await secureFetch(`/api/v1/updates/rollback/${encodeURIComponent(t)}`,{method:"POST"})).json();if(d.success)n.textContent="Rolled back!",setTimeout(()=>y(),2e3);else throw new Error(d.error||"Rollback failed")}catch(o){n.textContent="Failed",showNotification("Rollback error: "+o.message,"error"),setTimeout(()=>{n.textContent="Rollback",n.disabled=!1},3e3)}}})})}catch(h){L.innerHTML=`
Failed: ${escapeHtml(h.message)}
`}}async function S(){I.textContent="\u{1F50D} Checking...",I.disabled=!0;try{const u=await(await secureFetch("/api/v1/updates/check",{method:"POST"})).json();if(!u.success)throw new Error(u.error);I.textContent="\u2705 Done!",await y()}catch(h){I.textContent="\u274C Failed",showNotification("Check error: "+h.message,"error")}setTimeout(()=>{I.textContent="\u{1F50D} Check for Updates",I.disabled=!1},3e3)}async function w(){try{A.innerHTML='
Loading...
';const u=await(await fetch("/api/v1/updates/history?limit=50")).json(),a=u.success&&u.history?u.history:[];if(a.length===0){A.innerHTML='
\u{1F4CB}No update history yet.
';return}let e='';e+='';for(const n of a){const t=n.status==="success",i=n.duration?n.duration<1e3?n.duration+"ms":Math.round(n.duration/1e3)+"s":"-";e+='',e+=``,e+=``,e+=``,e+=``,e+=``,e+="",!t&&n.error&&(e+=``)}e+="
WhenContainerImageDurationStatus
${timeAgo(n.timestamp)}${escapeHtml(n.containerName)}${escapeHtml(n.imageName)}${i}${t?"\u2713 success":"\u2717 failed"}
${escapeHtml(n.error)}
",A.innerHTML=e}catch(h){A.innerHTML=`
Failed: ${escapeHtml(h.message)}
`}}async function x(){try{T.innerHTML='
Loading...
';const[h,u]=await Promise.all([fetch("/api/v1/stats/containers"),fetch("/api/v1/updates/auto-update")]),a=await h.json(),e=await u.json(),n=a.success&&a.stats?a.stats:[],t=e.success&&e.config?e.config:{};if(n.length===0){T.innerHTML='
\u{1F916}No running containers found.
';return}let i='
Auto-updates run during maintenance window (default 2AM-4AM). Daily = every day, Weekly = Sundays, Monthly = 1st of month.
';i+='',i+='';for(const o of n){const d=o.name||o.Names?.[0]?.replace(/^\//,"")||o.Id?.substring(0,12),l=o.containerId||o.Id,N=t[l]||{},O=N.enabled?N.schedule||"weekly":"",F=N.autoRollback!==!1,q=N.maintenanceWindow||"",U=N.lastAutoUpdate?timeAgo(N.lastAutoUpdate):"Never";i+=``,i+=``,i+=``,i+=``,i+=``,i+=``,i+=``,i+=""}i+="
ContainerScheduleWindowRollbackLast RunActions
${escapeHtml(d)} + ${U}
",T.innerHTML=i,T.querySelectorAll(".save-auto-btn").forEach(o=>{o.addEventListener("click",async()=>{const d=o.dataset.id,l=o.closest("tr"),N=l.querySelector(".auto-schedule").value,O=l.querySelector(".auto-rollback").checked,F=l.querySelector(".auto-window").value.trim();o.textContent="Saving...",o.disabled=!0;try{const U=await(await secureFetch(`/api/v1/updates/auto-update/${encodeURIComponent(d)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({enabled:!!N,schedule:N||"weekly",autoRollback:O,maintenanceWindow:F||void 0})})).json();if(U.success)o.textContent="\u2713 Saved";else throw new Error(U.error)}catch(q){o.textContent="\u2717 Error",showNotification("Save error: "+q.message,"error")}setTimeout(()=>{o.textContent="Save",o.disabled=!1},2e3)})})}catch(h){T.innerHTML=`
Failed: ${escapeHtml(h.message)}
`}}const C=document.getElementById("dashcaddy-current-version"),M=document.getElementById("dashcaddy-update-badge"),R=document.getElementById("dashcaddy-update-details"),D=document.getElementById("dashcaddy-new-version"),j=document.getElementById("dashcaddy-changelog"),B=document.getElementById("dashcaddy-apply-btn"),P=document.getElementById("dashcaddy-check-btn"),b=document.getElementById("dashcaddy-rollback-btn"),H=document.getElementById("dashcaddy-status-bar"),f=document.getElementById("dashcaddy-history-container");let p=null;function g(h,u){H&&(H.style.display="block",H.style.background=u==="error"?"var(--bad-bg)":u==="success"?"var(--ok-bg)":"var(--bg)",H.style.color=u==="error"?"var(--bad-fg)":u==="success"?"var(--ok-fg)":"var(--fg)",H.textContent=h)}async function v(){try{const u=await(await fetch("/api/v1/system/version")).json();if(u.success){const a=u.commit&&u.commit!=="unknown"?u.commit:null;C.textContent="v"+u.version+(a?" ("+a.substring(0,7)+")":"")}}catch{C.textContent="Unable to fetch version"}}async function m(h){h||(P.textContent="Checking...",P.disabled=!0);try{const a=await(await fetch("/api/v1/system/update-check")).json();if(p=a,a.success&&a.available&&a.remote){M.style.display="",R.style.display="",D.textContent="v"+a.remote.version,j.textContent=a.remote.changelog||"No changelog available.";const e=document.getElementById("updates-btn");if(e&&!e.querySelector(".update-dot")){const t=document.createElement("span");t.className="update-dot",t.style.cssText="position:absolute;top:2px;right:2px;width:8px;height:8px;border-radius:50%;background:var(--accent);",e.style.position="relative",e.appendChild(t)}const n=document.getElementById("updates-dashcaddy-tab");if(n&&!n.querySelector(".update-dot")){const t=document.createElement("span");t.className="update-dot",t.style.cssText="display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--accent);margin-left:4px;vertical-align:middle;",n.appendChild(t)}}else M.style.display="none",R.style.display="none",await v(),h||g("You are running the latest version.","success");h||(P.textContent="Check for Updates",P.disabled=!1)}catch(u){h||(g("Failed to check: "+u.message,"error"),P.textContent="Check for Updates",P.disabled=!1)}}async function r(){if(!confirm("Apply DashCaddy update? The API container will restart."))return!1;B.textContent="Updating...",B.disabled=!0,g("Downloading and applying update...","info");try{const u=await(await secureFetch("/api/v1/system/update-apply",{method:"POST"})).json();if(u.success)return g("Update initiated: v"+(u.fromVersion||"?")+" \u2192 v"+(u.toVersion||"?")+". The container will restart shortly.","success"),B.textContent="Applied!",document.querySelectorAll(".update-dot").forEach(a=>a.remove()),!0;throw new Error(u.error||"Update failed")}catch(h){throw g("Update failed: "+h.message,"error"),B.textContent="Update Now",B.disabled=!1,h}}async function c(){try{const u=await(await fetch("/api/v1/system/update-history")).json(),a=u.success&&u.history?u.history:[];if(a.length===0){f.innerHTML='
\u{1F4E6}No self-update history.
';return}let e='';e+='';for(const n of a){const t=n.status==="success"?"\u2713 success":n.status==="pending"?"\u23F3 pending":n.status==="partial"?"\u26A0 partial":"\u2717 "+n.status,i=n.status==="success"?"var(--ok-fg)":n.status==="pending"?"var(--muted)":"var(--bad-fg)";e+='',e+='",e+='",e+='",e+='",e+="",n.error&&(e+='"),n.note&&(e+='")}e+="
WhenVersionFromStatus
'+timeAgo(n.timestamp)+"v'+escapeHtml(n.version)+(n.rollback?" (rollback)":"")+"v'+escapeHtml(n.fromVersion||"?")+"'+t+"
'+escapeHtml(n.error)+"
'+escapeHtml(n.note)+"
",f.innerHTML=e}catch(h){f.innerHTML='
Failed: '+escapeHtml(h.message)+"
"}}async function s(){try{const u=await(await fetch("/api/v1/system/rollback-versions")).json(),a=u.success&&u.versions?u.versions:[];if(a.length===0){showNotification("No rollback versions available.","info");return}const e=prompt(`Available rollback versions: +`+a.join(` `)+` -Enter version to rollback to:`);if(!e)return;if(!n.includes(e)){showNotification("Invalid version: "+e,"error");return}if(!confirm("Rollback DashCaddy to v"+e+"? The container will restart."))return;y("Rolling back to v"+e+"...","info");const t=await(await secureFetch("/api/v1/system/rollback",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({version:e})})).json();if(t.success)y("Rollback to v"+e+" initiated. Container will restart.","success");else throw new Error(t.error||"Rollback failed")}catch(p){y("Rollback failed: "+p.message,"error")}}H?.addEventListener("click",()=>o(!1)),M?.addEventListener("click",l),S?.addEventListener("click",r),I?.addEventListener("click",k),B?.addEventListener("click",()=>{w?.classList.add("show"),g()}),wireModal(w,z),document.querySelector('[data-panel="updates-history"]')?.addEventListener("click",f),document.querySelector('[data-panel="updates-auto"]')?.addEventListener("click",x),document.querySelector('[data-panel="updates-dashcaddy"]')?.addEventListener("click",()=>{c(),v(),u||o(!0)}),setTimeout(()=>o(!0),5e3)})(),(function(){injectModal("docker-resources-modal",`
+Enter version to rollback to:`);if(!e)return;if(!a.includes(e)){showNotification("Invalid version: "+e,"error");return}if(!confirm("Rollback DashCaddy to v"+e+"? The container will restart."))return;g("Rolling back to v"+e+"...","info");const t=await(await secureFetch("/api/v1/system/rollback",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({version:e})})).json();if(t.success)g("Rollback to v"+e+" initiated. Container will restart.","success");else throw new Error(t.error||"Rollback failed")}catch(h){g("Rollback failed: "+h.message,"error")}}P?.addEventListener("click",()=>m(!1)),B?.addEventListener("click",()=>r().catch(()=>{})),b?.addEventListener("click",s),I?.addEventListener("click",S),E?.addEventListener("click",()=>{k?.classList.add("show"),y()}),wireModal(k,z),document.querySelector('[data-panel="updates-history"]')?.addEventListener("click",w),document.querySelector('[data-panel="updates-auto"]')?.addEventListener("click",x),document.querySelector('[data-panel="updates-dashcaddy"]')?.addEventListener("click",()=>{v(),c(),p||m(!0)}),window.dcApplyUpdate=r,window.dcCheckForUpdate=m,setTimeout(()=>m(!0),5e3)})(),(function(){injectModal("docker-resources-modal",`

\u{1F433} Docker Resources

@@ -1377,7 +1434,7 @@ Enter version to rollback to:`);if(!e)return;if(!n.includes(e)){showNotification
-
`);const w=document.getElementById("docker-resources-modal"),B=document.getElementById("docker-resources-btn"),z=document.getElementById("dr-close");function I(L){if(!L||L===0)return"0 B";const g=["B","KB","MB","GB","TB"],k=Math.floor(Math.log(Math.abs(L))/Math.log(1024));return(L/Math.pow(1024,k)).toFixed(1)+" "+g[k]}async function C(){const L=document.getElementById("dr-vol-list");try{const k=(await getJSON("/api/v1/docker/volumes")).volumes||[];if(k.length===0){L.innerHTML='
\u{1F4E6}No volumes found.
';return}let f='';f+='';for(const x of k){const E=x.name==="buildkit"||x.name.length===64;f+='',f+=``,f+=``,f+=``,f+='"}f+="
NameDriverScopeActions
${escapeHtml(x.name.length>40?x.name.substring(0,37)+"...":x.name)}${escapeHtml(x.driver)}${escapeHtml(x.scope)}',E||(f+=``),f+="
",L.innerHTML=f,L.querySelectorAll(".dr-vol-del").forEach(x=>{x.addEventListener("click",async()=>{if(confirm(`Delete volume "${x.dataset.name}"? Data will be lost.`)){x.textContent="...",x.disabled=!0;try{await deleteAPI(`/api/v1/docker/volumes/${encodeURIComponent(x.dataset.name)}?force=true`),C()}catch(E){showNotification("Delete failed: "+E.message,"error"),x.textContent="Delete",x.disabled=!1}}})})}catch(g){L.innerHTML=`
Failed: ${escapeHtml(g.message)}
`}}document.getElementById("dr-vol-create")?.addEventListener("click",async()=>{const L=document.getElementById("dr-vol-name"),g=L.value.trim();if(!g){showNotification("Enter a volume name","warning");return}try{await postJSON("/api/v1/docker/volumes",{name:g}),L.value="",showNotification(`Volume "${g}" created`,"success"),C()}catch(k){showNotification("Create failed: "+k.message,"error")}});async function P(){const L=document.getElementById("dr-net-list");try{const k=(await getJSON("/api/v1/docker/networks")).networks||[];if(k.length===0){L.innerHTML='
\u{1F310}No networks found.
';return}let f='';f+='';for(const x of k){const E=["bridge","host","none"].includes(x.name);f+='',f+=``,f+=``,f+=``,f+=``,f+='"}f+="
NameDriverScopeContainersActions
${escapeHtml(x.name)}${escapeHtml(x.driver)}${escapeHtml(x.scope)}${x.containers}',E||(f+=``),f+="
",L.innerHTML=f,L.querySelectorAll(".dr-net-del").forEach(x=>{x.addEventListener("click",async()=>{if(confirm(`Delete network "${x.dataset.name}"?`)){x.textContent="...",x.disabled=!0;try{await deleteAPI(`/api/v1/docker/networks/${encodeURIComponent(x.dataset.id)}`),P()}catch(E){showNotification("Delete failed: "+E.message,"error"),x.textContent="Delete",x.disabled=!1}}})})}catch(g){L.innerHTML=`
Failed: ${escapeHtml(g.message)}
`}}document.getElementById("dr-net-create")?.addEventListener("click",async()=>{const L=document.getElementById("dr-net-name"),g=document.getElementById("dr-net-driver"),k=L.value.trim();if(!k){showNotification("Enter a network name","warning");return}try{await postJSON("/api/v1/docker/networks",{name:k,driver:g.value}),L.value="",showNotification(`Network "${k}" created`,"success"),P()}catch(f){showNotification("Create failed: "+f.message,"error")}});async function $(){const L=document.getElementById("dr-disk-content");try{const g=await getJSON("/api/v1/docker/disk-usage"),k=[{label:"Images",icon:"\u{1F4C0}",count:g.images.count,size:g.images.size,reclaimable:g.images.reclaimable},{label:"Containers",icon:"\u{1F4E6}",count:g.containers.count,size:g.containers.size,extra:`${g.containers.running} running`},{label:"Volumes",icon:"\u{1F4BE}",count:g.volumes.count,size:g.volumes.size,reclaimable:g.volumes.reclaimable},{label:"Build Cache",icon:"\u{1F527}",count:g.buildCache.count,size:g.buildCache.size,reclaimable:g.buildCache.reclaimable}];let f=`
Total: ${I(g.totalSize)}
`;f+='
';for(const x of k)f+='
',f+=`
${x.icon} ${x.label} (${x.count})
`,f+=`
${I(x.size)}
`,x.reclaimable>0&&(f+=`
Reclaimable: ${I(x.reclaimable)}
`),x.extra&&(f+=`
${x.extra}
`),f+="
";f+="
",L.innerHTML=f}catch(g){L.innerHTML=`
Failed: ${escapeHtml(g.message)}
`}}B?.addEventListener("click",()=>{w?.classList.add("show"),C()}),wireModal(w,z),document.querySelector('[data-panel="dr-networks"]')?.addEventListener("click",P),document.querySelector('[data-panel="dr-disk"]')?.addEventListener("click",$)})(),(function(){injectModal("compose-import-modal",`
+
`);const k=document.getElementById("docker-resources-modal"),E=document.getElementById("docker-resources-btn"),z=document.getElementById("dr-close");function I($){if(!$||$===0)return"0 B";const y=["B","KB","MB","GB","TB"],S=Math.floor(Math.log(Math.abs($))/Math.log(1024));return($/Math.pow(1024,S)).toFixed(1)+" "+y[S]}async function L(){const $=document.getElementById("dr-vol-list");try{const S=(await getJSON("/api/v1/docker/volumes")).volumes||[];if(S.length===0){$.innerHTML='
\u{1F4E6}No volumes found.
';return}let w='';w+='';for(const x of S){const C=x.name==="buildkit"||x.name.length===64;w+='',w+=``,w+=``,w+=``,w+='"}w+="
NameDriverScopeActions
${escapeHtml(x.name.length>40?x.name.substring(0,37)+"...":x.name)}${escapeHtml(x.driver)}${escapeHtml(x.scope)}',C||(w+=``),w+="
",$.innerHTML=w,$.querySelectorAll(".dr-vol-del").forEach(x=>{x.addEventListener("click",async()=>{if(confirm(`Delete volume "${x.dataset.name}"? Data will be lost.`)){x.textContent="...",x.disabled=!0;try{await deleteAPI(`/api/v1/docker/volumes/${encodeURIComponent(x.dataset.name)}?force=true`),L()}catch(C){showNotification("Delete failed: "+C.message,"error"),x.textContent="Delete",x.disabled=!1}}})})}catch(y){$.innerHTML=`
Failed: ${escapeHtml(y.message)}
`}}document.getElementById("dr-vol-create")?.addEventListener("click",async()=>{const $=document.getElementById("dr-vol-name"),y=$.value.trim();if(!y){showNotification("Enter a volume name","warning");return}try{await postJSON("/api/v1/docker/volumes",{name:y}),$.value="",showNotification(`Volume "${y}" created`,"success"),L()}catch(S){showNotification("Create failed: "+S.message,"error")}});async function A(){const $=document.getElementById("dr-net-list");try{const S=(await getJSON("/api/v1/docker/networks")).networks||[];if(S.length===0){$.innerHTML='
\u{1F310}No networks found.
';return}let w='';w+='';for(const x of S){const C=["bridge","host","none"].includes(x.name);w+='',w+=``,w+=``,w+=``,w+=``,w+='"}w+="
NameDriverScopeContainersActions
${escapeHtml(x.name)}${escapeHtml(x.driver)}${escapeHtml(x.scope)}${x.containers}',C||(w+=``),w+="
",$.innerHTML=w,$.querySelectorAll(".dr-net-del").forEach(x=>{x.addEventListener("click",async()=>{if(confirm(`Delete network "${x.dataset.name}"?`)){x.textContent="...",x.disabled=!0;try{await deleteAPI(`/api/v1/docker/networks/${encodeURIComponent(x.dataset.id)}`),A()}catch(C){showNotification("Delete failed: "+C.message,"error"),x.textContent="Delete",x.disabled=!1}}})})}catch(y){$.innerHTML=`
Failed: ${escapeHtml(y.message)}
`}}document.getElementById("dr-net-create")?.addEventListener("click",async()=>{const $=document.getElementById("dr-net-name"),y=document.getElementById("dr-net-driver"),S=$.value.trim();if(!S){showNotification("Enter a network name","warning");return}try{await postJSON("/api/v1/docker/networks",{name:S,driver:y.value}),$.value="",showNotification(`Network "${S}" created`,"success"),A()}catch(w){showNotification("Create failed: "+w.message,"error")}});async function T(){const $=document.getElementById("dr-disk-content");try{const y=await getJSON("/api/v1/docker/disk-usage"),S=[{label:"Images",icon:"\u{1F4C0}",count:y.images.count,size:y.images.size,reclaimable:y.images.reclaimable},{label:"Containers",icon:"\u{1F4E6}",count:y.containers.count,size:y.containers.size,extra:`${y.containers.running} running`},{label:"Volumes",icon:"\u{1F4BE}",count:y.volumes.count,size:y.volumes.size,reclaimable:y.volumes.reclaimable},{label:"Build Cache",icon:"\u{1F527}",count:y.buildCache.count,size:y.buildCache.size,reclaimable:y.buildCache.reclaimable}];let w=`
Total: ${I(y.totalSize)}
`;w+='
';for(const x of S)w+='
',w+=`
${x.icon} ${x.label} (${x.count})
`,w+=`
${I(x.size)}
`,x.reclaimable>0&&(w+=`
Reclaimable: ${I(x.reclaimable)}
`),x.extra&&(w+=`
${x.extra}
`),w+="
";w+="
",$.innerHTML=w}catch(y){$.innerHTML=`
Failed: ${escapeHtml(y.message)}
`}}E?.addEventListener("click",()=>{k?.classList.add("show"),L()}),wireModal(k,z),document.querySelector('[data-panel="dr-networks"]')?.addEventListener("click",A),document.querySelector('[data-panel="dr-disk"]')?.addEventListener("click",T)})(),(function(){injectModal("compose-import-modal",`

\u{1F4E6} Import Docker Compose

@@ -1418,8 +1475,8 @@ Enter version to rollback to:`);if(!e)return;if(!n.includes(e)){showNotification
- `);const w=document.getElementById("compose-import-modal"),B=document.getElementById("compose-import-btn"),z=document.getElementById("compose-cancel");wireModal(w,z);let I=null;function C($){document.getElementById("compose-step-paste").style.display=$==="paste"?"":"none",document.getElementById("compose-step-preview").style.display=$==="preview"?"":"none",document.getElementById("compose-step-progress").style.display=$==="progress"?"":"none"}B?.addEventListener("click",()=>{C("paste"),I=null,document.getElementById("compose-yaml").value="",document.getElementById("compose-stack-name").value="",w?.classList.add("show")}),document.getElementById("compose-file-upload")?.addEventListener("change",$=>{const L=$.target.files[0];if(!L)return;const g=new FileReader;g.onload=()=>{document.getElementById("compose-yaml").value=g.result},g.readAsText(L)}),document.getElementById("compose-parse-btn")?.addEventListener("click",async()=>{const $=document.getElementById("compose-yaml").value.trim(),L=document.getElementById("compose-stack-name").value.trim()||"stack";if(!$){showNotification("Paste a docker-compose.yml","warning");return}const g=document.getElementById("compose-parse-btn"),k=g.textContent;g.textContent="Parsing...",g.disabled=!0;try{const f=await postJSON("/api/v1/apps/import-compose",{yaml:$,stackName:L});I=f,I.stackName=L,P(f),C("preview")}catch(f){showNotification("Parse failed: "+f.message,"error")}finally{g.textContent=k,g.disabled=!1}});function P($){const L=document.getElementById("compose-preview-content");let g="";$.networks&&$.networks.length>0&&(g+=`
Networks: ${$.networks.map(k=>`${escapeHtml(k)}`).join(", ")}
`),$.volumes&&$.volumes.length>0&&(g+=`
Volumes: ${$.volumes.map(k=>`${escapeHtml(k)}`).join(", ")}
`),g+=`
${$.services.length} service(s)
`,g+='
';for(const k of $.services){const f=k.skip?"var(--bad-fg)":"var(--border)";if(g+=`
`,g+=`
${escapeHtml(k.name)}`,k.skip&&(g+=` \u2014 skipped: ${escapeHtml(k.reason)}`),g+="
",!k.skip&&(g+=`
Image: ${escapeHtml(k.image)}
`,k.ports?.length&&(g+=`
Ports: ${k.ports.map(x=>`${x.host}:${x.container}`).join(", ")}
`),k.volumes?.length&&(g+=`
Volumes: ${k.volumes.length}
`),Object.keys(k.environment||{}).length&&(g+=`
Env vars: ${Object.keys(k.environment).length}
`),k.envFileWarning&&(g+=`
\u26A0 ${escapeHtml(k.envFileWarning)}
`),k.resources?.cpus||k.resources?.memory)){const x=[];k.resources.cpus&&x.push(`CPU: ${k.resources.cpus}`),k.resources.memory&&x.push(`Mem: ${k.resources.memory}MB`),g+=`
Limits: ${x.join(", ")}
`}g+="
"}g+="
",L.innerHTML=g}document.getElementById("compose-back-btn")?.addEventListener("click",()=>C("paste")),document.getElementById("compose-deploy-btn")?.addEventListener("click",async()=>{if(!I)return;const $=document.getElementById("compose-deploy-btn");$.textContent="Deploying...",$.disabled=!0,C("progress");const L=document.getElementById("compose-progress-content");L.innerHTML='
Deploying services...
';try{const g=await postJSON("/api/v1/apps/deploy-compose",{services:I.services,networks:I.networks,stackName:I.stackName});let k=`
Stack "${escapeHtml(g.stackName)}" \u2014 Deployment Complete
`;k+='
';for(const f of g.results){const x=f.status==="deployed"||f.status==="created"?"\u2705":f.status==="exists"?"\u26A1":f.status==="skipped"?"\u23ED":"\u274C";k+='
',k+=`${x} ${escapeHtml(f.name)} (${f.type}) \u2014 ${escapeHtml(f.status)}`,f.error&&(k+=` ${escapeHtml(f.error)}`),f.subdomain&&(k+=` \u2192 ${escapeHtml(f.subdomain)}`),f.reason&&(k+=` (${escapeHtml(f.reason)})`),k+="
"}k+="
",k+='',L.innerHTML=k,document.getElementById("compose-done-btn")?.addEventListener("click",()=>{w?.classList.remove("show"),typeof window.loadServices=="function"&&window.loadServices().then(()=>{typeof window.buildGrid=="function"&&window.buildGrid()})}),showNotification(`Stack "${g.stackName}" deployed`,"success")}catch(g){L.innerHTML=`
Deployment failed: ${escapeHtml(g.message)}
- `,document.getElementById("compose-retry-btn")?.addEventListener("click",()=>C("paste"))}finally{$.textContent="Deploy All",$.disabled=!1}})})(),(function(){injectModal("exec-modal",`
+
`);const k=document.getElementById("compose-import-modal"),E=document.getElementById("compose-import-btn"),z=document.getElementById("compose-cancel");wireModal(k,z);let I=null;function L(T){document.getElementById("compose-step-paste").style.display=T==="paste"?"":"none",document.getElementById("compose-step-preview").style.display=T==="preview"?"":"none",document.getElementById("compose-step-progress").style.display=T==="progress"?"":"none"}E?.addEventListener("click",()=>{L("paste"),I=null,document.getElementById("compose-yaml").value="",document.getElementById("compose-stack-name").value="",k?.classList.add("show")}),document.getElementById("compose-file-upload")?.addEventListener("change",T=>{const $=T.target.files[0];if(!$)return;const y=new FileReader;y.onload=()=>{document.getElementById("compose-yaml").value=y.result},y.readAsText($)}),document.getElementById("compose-parse-btn")?.addEventListener("click",async()=>{const T=document.getElementById("compose-yaml").value.trim(),$=document.getElementById("compose-stack-name").value.trim()||"stack";if(!T){showNotification("Paste a docker-compose.yml","warning");return}const y=document.getElementById("compose-parse-btn"),S=y.textContent;y.textContent="Parsing...",y.disabled=!0;try{const w=await postJSON("/api/v1/apps/import-compose",{yaml:T,stackName:$});I=w,I.stackName=$,A(w),L("preview")}catch(w){showNotification("Parse failed: "+w.message,"error")}finally{y.textContent=S,y.disabled=!1}});function A(T){const $=document.getElementById("compose-preview-content");let y="";T.networks&&T.networks.length>0&&(y+=`
Networks: ${T.networks.map(S=>`${escapeHtml(S)}`).join(", ")}
`),T.volumes&&T.volumes.length>0&&(y+=`
Volumes: ${T.volumes.map(S=>`${escapeHtml(S)}`).join(", ")}
`),y+=`
${T.services.length} service(s)
`,y+='
';for(const S of T.services){const w=S.skip?"var(--bad-fg)":"var(--border)";if(y+=`
`,y+=`
${escapeHtml(S.name)}`,S.skip&&(y+=` \u2014 skipped: ${escapeHtml(S.reason)}`),y+="
",!S.skip&&(y+=`
Image: ${escapeHtml(S.image)}
`,S.ports?.length&&(y+=`
Ports: ${S.ports.map(x=>`${x.host}:${x.container}`).join(", ")}
`),S.volumes?.length&&(y+=`
Volumes: ${S.volumes.length}
`),Object.keys(S.environment||{}).length&&(y+=`
Env vars: ${Object.keys(S.environment).length}
`),S.envFileWarning&&(y+=`
\u26A0 ${escapeHtml(S.envFileWarning)}
`),S.resources?.cpus||S.resources?.memory)){const x=[];S.resources.cpus&&x.push(`CPU: ${S.resources.cpus}`),S.resources.memory&&x.push(`Mem: ${S.resources.memory}MB`),y+=`
Limits: ${x.join(", ")}
`}y+="
"}y+="
",$.innerHTML=y}document.getElementById("compose-back-btn")?.addEventListener("click",()=>L("paste")),document.getElementById("compose-deploy-btn")?.addEventListener("click",async()=>{if(!I)return;const T=document.getElementById("compose-deploy-btn");T.textContent="Deploying...",T.disabled=!0,L("progress");const $=document.getElementById("compose-progress-content");$.innerHTML='
Deploying services...
';try{const y=await postJSON("/api/v1/apps/deploy-compose",{services:I.services,networks:I.networks,stackName:I.stackName});let S=`
Stack "${escapeHtml(y.stackName)}" \u2014 Deployment Complete
`;S+='
';for(const w of y.results){const x=w.status==="deployed"||w.status==="created"?"\u2705":w.status==="exists"?"\u26A1":w.status==="skipped"?"\u23ED":"\u274C";S+='
',S+=`${x} ${escapeHtml(w.name)} (${w.type}) \u2014 ${escapeHtml(w.status)}`,w.error&&(S+=` ${escapeHtml(w.error)}`),w.subdomain&&(S+=` \u2192 ${escapeHtml(w.subdomain)}`),w.reason&&(S+=` (${escapeHtml(w.reason)})`),S+="
"}S+="
",S+='',$.innerHTML=S,document.getElementById("compose-done-btn")?.addEventListener("click",()=>{k?.classList.remove("show"),typeof window.loadServices=="function"&&window.loadServices().then(()=>{typeof window.buildGrid=="function"&&window.buildGrid()})}),showNotification(`Stack "${y.stackName}" deployed`,"success")}catch(y){$.innerHTML=`
Deployment failed: ${escapeHtml(y.message)}
+ `,document.getElementById("compose-retry-btn")?.addEventListener("click",()=>L("paste"))}finally{T.textContent="Deploy All",T.disabled=!1}})})(),(function(){injectModal("exec-modal",`

Terminal

@@ -1427,11 +1484,11 @@ Enter version to rollback to:`);if(!e)return;if(!n.includes(e)){showNotification
- `);const w=document.getElementById("exec-modal"),B=document.getElementById("exec-terminal"),z=document.getElementById("exec-close");let I=null,C=null,P=null;function $(){if(C){try{C.close()}catch{}C=null}if(I){try{I.dispose()}catch{}I=null}P=null,B.innerHTML=""}function L(g,k){if($(),document.getElementById("exec-title").textContent=`Terminal \u2014 ${k||g}`,w?.classList.add("show"),typeof Terminal>"u"){B.innerHTML='
xterm.js not loaded
';return}I=new Terminal({cursorBlink:!0,fontSize:14,fontFamily:"'Cascadia Code', 'Fira Code', 'Consolas', monospace",theme:{background:"#1e1e1e",foreground:"#d4d4d4",cursor:"#aeafad",selectionBackground:"#264f78"},scrollback:5e3}),typeof FitAddon<"u"&&(P=new FitAddon.FitAddon,I.loadAddon(P)),I.open(B),P&&setTimeout(()=>P.fit(),50);const f=location.protocol==="https:"?"wss:":"ws:";C=new WebSocket(`${f}//${location.host}/ws/exec/${encodeURIComponent(g)}`),C.binaryType="arraybuffer",C.onopen=()=>{if(I.writeln("\x1B[32mConnecting...\x1B[0m"),P){const E=P.proposeDimensions();E&&C.send(JSON.stringify({type:"resize",cols:E.cols,rows:E.rows}))}},C.onmessage=E=>{if(typeof E.data=="string"){try{const R=JSON.parse(E.data);if(R.type==="connected"){I.writeln(`\x1B[32mConnected (${R.shell})\x1B[0m\r -`);return}if(R.type==="error"){I.writeln(`\x1B[31mError: ${R.message}\x1B[0m`);return}if(R.type==="exit"){I.writeln(`\r -\x1B[33mSession ended.\x1B[0m`);return}}catch{}I.write(E.data)}else I.write(new Uint8Array(E.data))},C.onclose=()=>{I&&I.writeln(`\r -\x1B[33mDisconnected.\x1B[0m`)},C.onerror=()=>{I&&I.writeln(`\r -\x1B[31mConnection error.\x1B[0m`)},I.onData(E=>{C&&C.readyState===WebSocket.OPEN&&C.send(E)}),I.onResize(({cols:E,rows:R})=>{C&&C.readyState===WebSocket.OPEN&&C.send(JSON.stringify({type:"resize",cols:E,rows:R}))});const x=()=>{P&&P.fit()};window.addEventListener("resize",x),w._resizeHandler=x}z?.addEventListener("click",()=>{$(),w._resizeHandler&&window.removeEventListener("resize",w._resizeHandler),w?.classList.remove("show")}),w?.addEventListener("click",g=>{g.target===w&&($(),w._resizeHandler&&window.removeEventListener("resize",w._resizeHandler),w?.classList.remove("show"))}),window.openExecModal=L})(),(function(){injectModal("audit-modal",`
+
`);const k=document.getElementById("exec-modal"),E=document.getElementById("exec-terminal"),z=document.getElementById("exec-close");let I=null,L=null,A=null;function T(){if(L){try{L.close()}catch{}L=null}if(I){try{I.dispose()}catch{}I=null}A=null,E.innerHTML=""}function $(y,S){if(T(),document.getElementById("exec-title").textContent=`Terminal \u2014 ${S||y}`,k?.classList.add("show"),typeof Terminal>"u"){E.innerHTML='
xterm.js not loaded
';return}I=new Terminal({cursorBlink:!0,fontSize:14,fontFamily:"'Cascadia Code', 'Fira Code', 'Consolas', monospace",theme:{background:"#1e1e1e",foreground:"#d4d4d4",cursor:"#aeafad",selectionBackground:"#264f78"},scrollback:5e3}),typeof FitAddon<"u"&&(A=new FitAddon.FitAddon,I.loadAddon(A)),I.open(E),A&&setTimeout(()=>A.fit(),50);const w=location.protocol==="https:"?"wss:":"ws:";L=new WebSocket(`${w}//${location.host}/ws/exec/${encodeURIComponent(y)}`),L.binaryType="arraybuffer",L.onopen=()=>{if(I.writeln("\x1B[32mConnecting...\x1B[0m"),A){const C=A.proposeDimensions();C&&L.send(JSON.stringify({type:"resize",cols:C.cols,rows:C.rows}))}},L.onmessage=C=>{if(typeof C.data=="string"){try{const M=JSON.parse(C.data);if(M.type==="connected"){I.writeln(`\x1B[32mConnected (${M.shell})\x1B[0m\r +`);return}if(M.type==="error"){I.writeln(`\x1B[31mError: ${M.message}\x1B[0m`);return}if(M.type==="exit"){I.writeln(`\r +\x1B[33mSession ended.\x1B[0m`);return}}catch{}I.write(C.data)}else I.write(new Uint8Array(C.data))},L.onclose=()=>{I&&I.writeln(`\r +\x1B[33mDisconnected.\x1B[0m`)},L.onerror=()=>{I&&I.writeln(`\r +\x1B[31mConnection error.\x1B[0m`)},I.onData(C=>{L&&L.readyState===WebSocket.OPEN&&L.send(C)}),I.onResize(({cols:C,rows:M})=>{L&&L.readyState===WebSocket.OPEN&&L.send(JSON.stringify({type:"resize",cols:C,rows:M}))});const x=()=>{A&&A.fit()};window.addEventListener("resize",x),k._resizeHandler=x}z?.addEventListener("click",()=>{T(),k._resizeHandler&&window.removeEventListener("resize",k._resizeHandler),k?.classList.remove("show")}),k?.addEventListener("click",y=>{y.target===k&&(T(),k._resizeHandler&&window.removeEventListener("resize",k._resizeHandler),k?.classList.remove("show"))}),window.openExecModal=$})(),(function(){injectModal("audit-modal",`

\u{1F4DC} Audit Log

- `);const w=document.getElementById("audit-modal"),B=document.getElementById("audit-log-btn"),z=document.getElementById("audit-cancel"),I=document.getElementById("audit-refresh-btn"),C=document.getElementById("audit-clear-btn"),P=document.getElementById("audit-filter"),$=document.getElementById("audit-log-container"),L=document.getElementById("audit-load-more");let g=0;const k=50;async function f(x){try{x||(g=0,$.innerHTML='
Loading...
');const E=P.value;let R=`/api/v1/audit-logs?limit=${k}&offset=${g}`;E&&(R+=`&action=${encodeURIComponent(E)}`);const D=await(await fetch(R)).json(),N=D.success&&D.entries?D.entries:[];if(N.length===0&&!x){$.innerHTML='
\u{1F4DC}No audit log entries yet. Actions will be logged automatically.
',L.style.display="none";return}let M="";x||(M='',M+='');for(const H of N){const S=H.outcome==="success";M+='',M+=``,M+=``,M+=``,M+=``,M+=``,M+="",H.details&&Object.keys(H.details).length>0&&(M+=``)}if(!x)M+="
WhenIPActionResourceResult
${timeAgo(H.timestamp)}${escapeHtml(H.ip||"-")}${escapeHtml(H.action||"-")}${escapeHtml(H.resource||"-")}${S?"\u2713":"\u2717"}
",$.innerHTML=M;else{const H=$.querySelector("table");H&&H.insertAdjacentHTML("beforeend",M)}g+=N.length,L.style.display=N.length>=k?"":"none",$.querySelectorAll(".audit-row").forEach(H=>{H.dataset.wired||(H.dataset.wired="true",H.addEventListener("click",()=>{const S=H.nextElementSibling;S&&S.classList.contains("audit-detail")&&(S.style.display=S.style.display==="none"?"":"none")}))})}catch(E){$.innerHTML=`
Failed: ${escapeHtml(E.message)}
`}}B?.addEventListener("click",()=>{w?.classList.add("show"),f(!1)}),wireModal(w,z),I?.addEventListener("click",()=>f(!1)),P?.addEventListener("change",()=>f(!1)),L?.addEventListener("click",()=>f(!0)),C?.addEventListener("click",async()=>{if(confirm("Clear the entire audit log? This cannot be undone."))try{const E=await(await secureFetch("/api/v1/audit-logs",{method:"DELETE"})).json();E.success?f(!1):showNotification("Error: "+(E.error||"Clear failed"),"error")}catch(x){showNotification("Error: "+x.message,"error")}})})(),(function(){injectModal("weather-modal",`

Weather Settings

+
`);const k=document.getElementById("audit-modal"),E=document.getElementById("audit-log-btn"),z=document.getElementById("audit-cancel"),I=document.getElementById("audit-refresh-btn"),L=document.getElementById("audit-clear-btn"),A=document.getElementById("audit-filter"),T=document.getElementById("audit-log-container"),$=document.getElementById("audit-load-more");let y=0;const S=50;async function w(x){try{x||(y=0,T.innerHTML='
Loading...
');const C=A.value;let M=`/api/v1/audit-logs?limit=${S}&offset=${y}`;C&&(M+=`&action=${encodeURIComponent(C)}`);const D=await(await fetch(M)).json(),j=D.success&&D.entries?D.entries:[];if(j.length===0&&!x){T.innerHTML='
\u{1F4DC}No audit log entries yet. Actions will be logged automatically.
',$.style.display="none";return}let B="";x||(B='',B+='');for(const P of j){const b=P.outcome==="success";B+='',B+=``,B+=``,B+=``,B+=``,B+=``,B+="",P.details&&Object.keys(P.details).length>0&&(B+=``)}if(!x)B+="
WhenIPActionResourceResult
${timeAgo(P.timestamp)}${escapeHtml(P.ip||"-")}${escapeHtml(P.action||"-")}${escapeHtml(P.resource||"-")}${b?"\u2713":"\u2717"}
",T.innerHTML=B;else{const P=T.querySelector("table");P&&P.insertAdjacentHTML("beforeend",B)}y+=j.length,$.style.display=j.length>=S?"":"none",T.querySelectorAll(".audit-row").forEach(P=>{P.dataset.wired||(P.dataset.wired="true",P.addEventListener("click",()=>{const b=P.nextElementSibling;b&&b.classList.contains("audit-detail")&&(b.style.display=b.style.display==="none"?"":"none")}))})}catch(C){T.innerHTML=`
Failed: ${escapeHtml(C.message)}
`}}E?.addEventListener("click",()=>{k?.classList.add("show"),w(!1)}),wireModal(k,z),I?.addEventListener("click",()=>w(!1)),A?.addEventListener("change",()=>w(!1)),$?.addEventListener("click",()=>w(!0)),L?.addEventListener("click",async()=>{if(confirm("Clear the entire audit log? This cannot be undone."))try{const C=await(await secureFetch("/api/v1/audit-logs",{method:"DELETE"})).json();C.success?w(!1):showNotification("Error: "+(C.error||"Clear failed"),"error")}catch(x){showNotification("Error: "+x.message,"error")}})})(),(function(){const k=new ErrorHandler;injectModal("weather-modal",`

Weather Settings

Enter a city name, postal code, or “City, Country”
@@ -1478,23 +1535,23 @@ Enter version to rollback to:`);if(!e)return;if(!n.includes(e)){showNotification
-
`);const w="weather-location",B="weather-zip",z="weather-geo",I="weather-unit";!safeGet(w)&&safeGet(B)&&safeSet(w,safeGet(B));function C(){return safeGet(I)||"imperial"}function P(){return{icon:document.querySelector(".weather-icon"),temp:document.querySelector(".weather-temp"),condition:document.querySelector(".weather-condition"),location:document.querySelector(".weather-location"),wind:document.querySelector(".weather-wind")}}const $={0:"Clear sky",1:"Mainly clear",2:"Partly cloudy",3:"Overcast",45:"Fog",48:"Rime fog",51:"Light drizzle",53:"Drizzle",55:"Dense drizzle",56:"Freezing drizzle",57:"Dense freezing drizzle",61:"Light rain",63:"Moderate rain",65:"Heavy rain",66:"Light freezing rain",67:"Heavy freezing rain",71:"Light snow",73:"Moderate snow",75:"Heavy snow",77:"Snow grains",80:"Light showers",81:"Moderate showers",82:"Violent showers",85:"Light snow showers",86:"Heavy snow showers",95:"Thunderstorm",96:"Thunderstorm with hail",99:"Severe thunderstorm"},L={0:"\u2600\uFE0F",1:"\u{1F324}\uFE0F",2:"\u26C5",3:"\u2601\uFE0F",45:"\u{1F32B}\uFE0F",48:"\u{1F32B}\uFE0F",51:"\u{1F326}\uFE0F",53:"\u{1F326}\uFE0F",55:"\u{1F326}\uFE0F",56:"\u{1F328}\uFE0F",57:"\u{1F328}\uFE0F",61:"\u{1F326}\uFE0F",63:"\u{1F327}\uFE0F",65:"\u{1F327}\uFE0F",66:"\u{1F328}\uFE0F",67:"\u{1F328}\uFE0F",71:"\u{1F328}\uFE0F",73:"\u2744\uFE0F",75:"\u2744\uFE0F",77:"\u2744\uFE0F",80:"\u{1F326}\uFE0F",81:"\u{1F327}\uFE0F",82:"\u{1F327}\uFE0F",85:"\u{1F328}\uFE0F",86:"\u2744\uFE0F",95:"\u26C8\uFE0F",96:"\u26C8\uFE0F",99:"\u26C8\uFE0F"},g=["N","NNE","NE","ENE","E","ESE","SE","SSE","S","SSW","SW","WSW","W","WNW","NW","NNW"];function k(N){return g[Math.round(N/22.5)%16]}async function f(N){const M=safeGet(z);if(M)try{const u=JSON.parse(M);if(u.query===N)return u}catch{}const H=await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(N)}&count=1&language=en&format=json`);if(!H.ok)throw new Error("Geocoding failed");const S=await H.json();if(!S.results||!S.results.length)throw new Error("Location not found");const T=S.results[0],b={query:N,lat:T.latitude,lon:T.longitude,city:T.name,state:T.admin1||"",country:T.country||"",countryCode:T.country_code||""};return safeSet(z,JSON.stringify(b)),b}function x(N){return N.countryCode==="US"&&N.state?`${N.city}, ${N.state}`:N.country?`${N.city}, ${N.country}`:N.city}async function E(N){try{const M=await f(N),H=C(),S=H==="metric"?"celsius":"fahrenheit",T=H==="metric"?"kmh":"mph",b=`https://api.open-meteo.com/v1/forecast?latitude=${M.lat}&longitude=${M.lon}¤t=temperature_2m,weather_code,wind_speed_10m,wind_direction_10m&temperature_unit=${S}&wind_speed_unit=${T}`,u=await fetch(b);if(!u.ok)throw new Error("Weather fetch failed");const c=(await u.json()).current,o=c.weather_code;return{temp:Math.round(c.temperature_2m),condition:$[o]||"Unknown",icon:L[o]||"\u{1F324}\uFE0F",locationStr:x(M),windSpeed:Math.round(c.wind_speed_10m),windDir:k(c.wind_direction_10m),unit:H}}catch(M){return console.warn("Weather fetch failed:",M),null}}async function R(){const N=P();if(!N.icon||!N.temp||!N.condition||!N.location||!N.wind){console.warn("Weather widget elements not found");return}const M=safeGet(w);if(!M){N.location.textContent="Set Location",N.temp.textContent="--\xB0",N.condition.textContent="Click \u2699\uFE0F to configure",N.wind.textContent="--",N.icon.innerHTML='\u{1F324}\uFE0F';return}try{const H=await E(M);if(H){const S=H.unit==="metric"?"\xB0C":"\xB0F",T=H.unit==="metric"?"km/h":"mph";N.location.textContent=H.locationStr,N.temp.textContent=`${H.temp}${S}`,N.condition.textContent=H.condition,N.wind.textContent=`Wind: ${H.windSpeed} ${T} ${H.windDir}`,N.icon.innerHTML=`${escapeHtml(H.icon)}`}}catch(H){console.error("Weather update error:",H),N.location.textContent="Weather Error",N.temp.textContent="Error",N.condition.textContent="Failed to load",N.wind.textContent="--"}}const O=document.getElementById("weather-modal"),D=document.getElementById("weather-location-input");document.getElementById("weather-settings")?.addEventListener("click",()=>{D.value=safeGet(w)||"";const N=C(),M=O.querySelector(`input[name="weather-unit-radio"][value="${N}"]`);M&&(M.checked=!0),O.classList.add("show"),D.focus()}),document.getElementById("weather-cancel")?.addEventListener("click",()=>{O.classList.remove("show")}),document.getElementById("weather-save")?.addEventListener("click",()=>{const N=D.value.trim();if(N){safeGet(w)!==N&&safeSet(z,""),safeSet(w,N);const H=O.querySelector('input[name="weather-unit-radio"]:checked'),S=H?H.value:"imperial",T=C();safeSet(I,S),T!==S&&safeSet(z,""),O.classList.remove("show"),R()}else showNotification("Please enter a location (e.g., Hamburg, London, 90210)","warning")}),wireModal(O),document.addEventListener("keydown",N=>{N.key==="Escape"&&O.classList.contains("show")&&O.classList.remove("show")}),R(),setInterval(R,DC.POLL.WEATHER)})(),(function(){const w=document.getElementById("clock-widget"),B=document.getElementById("clock-render");if(!w||!B)return;const z=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],I=["January","February","March","April","May","June","July","August","September","October","November","December"],C=["XII","I","II","III","IV","V","VI","VII","VIII","IX","X","XI"];let P=safeGet("clock-style")||"default",$=-1,L=!1,g="",k="",f=null,x=null;function E(n){if(L||safeGet("clock-chimes")!=="true")return;L=!0;const e=parseInt(safeGet("clock-chime-volume")||"50",10)/100;let a=0;function t(){if(a>=n){L=!1;return}const s=new Audio("/assets/sounds/church-bell.mp3");s.volume=e,s.play().catch(()=>{}),a++,a{L=!1},2500)}t()}function R(n){return z[n.getDay()]+", "+I[n.getMonth()]+" "+n.getDate()+", "+n.getFullYear()}function O(){k="",f=null}function D(){return k!=="digital"&&(B.innerHTML='
',f={main:B.querySelector(".clock-main"),seconds:B.querySelector(".clock-seconds"),ampm:B.querySelector(".clock-ampm"),date:B.querySelector(".clock-date")},k="digital"),f}function N(n){const e=n.getHours(),a=n.getMinutes(),t=n.getSeconds(),s=e>=12?"PM":"AM",i=e%12||12,h=D();h.main.textContent=`${i}:${String(a).padStart(2,"0")}`,h.seconds.textContent=`:${String(t).padStart(2,"0")}`,h.ampm.textContent=s,h.date.textContent=R(n)}function M(n,e){const a=n.getHours(),t=n.getMinutes(),s=n.getSeconds(),i=a>=12?"PM":"AM",h=a%12||12,d=D();d.main.textContent=`${String(h).padStart(2,"0")}:${String(t).padStart(2,"0")}`,d.seconds.textContent=`:${String(s).padStart(2,"0")}`,d.ampm.textContent=i,d.date.textContent=R(n)}function H(n){const e=n.getHours(),a=n.getMinutes(),t=n.getSeconds(),s=e>=12?"PM":"AM",i=e%12||12,h=String(i).padStart(2," ")+String(a).padStart(2,"0")+String(t).padStart(2,"0");let d='
';if(d+=S(h[0],0),d+=S(h[1],1),d+=':',d+=S(h[2],2),d+=S(h[3],3),d+=':',d+=S(h[4],4),d+=S(h[5],5),d+=`${s}`,d+="
",d+=`
${R(n)}
`,B.innerHTML=d,k="flip",g){for(let A=0;A<6;A++)if(h[A]!==g[A]){const F=B.querySelector(`.flip-card[data-idx="${A}"]`);F&&F.classList.add("flipping")}}g=h}function S(n,e){const a=n===" "?"":n;return`
${a}
${a}
`}function T(n){const e=n.getHours(),a=n.getMinutes(),t=n.getSeconds(),s=e%12||12,i=e>=12?"PM":"AM",h=[Math.floor(s/10),s%10,Math.floor(a/10),a%10,Math.floor(t/10),t%10];let d='
';d+='
HHMMSS
';for(let A=3;A>=0;A--){d+='
';for(let F=0;F<6;F++){const j=h[F]>>A&1;d+=`
`}d+="
"}d+='
';for(let A=0;A<6;A++)d+=`${h[A]}`;d+="
",d+=`
${i}
`,d+="
",d+=`
${R(n)}
`,B.innerHTML=d,k="binary"}function b(n,e){const a=n.getHours(),t=n.getMinutes(),s=n.getSeconds(),i=120,h=i/2,d=i/2,A=s/60*360-90,F=(t+s/60)/60*360-90,j=(a%12+t/60)/12*360-90;let _="";for(let G=1;G<=12;G++){const Q=G/12*2*Math.PI-Math.PI/2,ae=47,ne=h+ae*Math.cos(Q),X=d+ae*Math.sin(Q),se=e?C[G%12]:G;_+=`${se}`}let U="";for(let G=0;G<60;G++){const Q=G/60*2*Math.PI-Math.PI/2,ae=56,ne=G%5===0?52:54,X=h+ne*Math.cos(Q),se=d+ne*Math.sin(Q),ie=h+ae*Math.cos(Q),oe=d+ae*Math.sin(Q),q=G%5===0?1.5:.5;U+=``}const W=` - +
`);const E="weather-location",z="weather-zip",I="weather-geo",L="weather-unit";!safeGet(E)&&safeGet(z)&&safeSet(E,safeGet(z));function A(){return safeGet(L)||"imperial"}function T(){return{icon:document.querySelector(".weather-icon"),temp:document.querySelector(".weather-temp"),condition:document.querySelector(".weather-condition"),location:document.querySelector(".weather-location"),wind:document.querySelector(".weather-wind")}}const $={0:"Clear sky",1:"Mainly clear",2:"Partly cloudy",3:"Overcast",45:"Fog",48:"Rime fog",51:"Light drizzle",53:"Drizzle",55:"Dense drizzle",56:"Freezing drizzle",57:"Dense freezing drizzle",61:"Light rain",63:"Moderate rain",65:"Heavy rain",66:"Light freezing rain",67:"Heavy freezing rain",71:"Light snow",73:"Moderate snow",75:"Heavy snow",77:"Snow grains",80:"Light showers",81:"Moderate showers",82:"Violent showers",85:"Light snow showers",86:"Heavy snow showers",95:"Thunderstorm",96:"Thunderstorm with hail",99:"Severe thunderstorm"},y={0:"\u2600\uFE0F",1:"\u{1F324}\uFE0F",2:"\u26C5",3:"\u2601\uFE0F",45:"\u{1F32B}\uFE0F",48:"\u{1F32B}\uFE0F",51:"\u{1F326}\uFE0F",53:"\u{1F326}\uFE0F",55:"\u{1F326}\uFE0F",56:"\u{1F328}\uFE0F",57:"\u{1F328}\uFE0F",61:"\u{1F326}\uFE0F",63:"\u{1F327}\uFE0F",65:"\u{1F327}\uFE0F",66:"\u{1F328}\uFE0F",67:"\u{1F328}\uFE0F",71:"\u{1F328}\uFE0F",73:"\u2744\uFE0F",75:"\u2744\uFE0F",77:"\u2744\uFE0F",80:"\u{1F326}\uFE0F",81:"\u{1F327}\uFE0F",82:"\u{1F327}\uFE0F",85:"\u{1F328}\uFE0F",86:"\u2744\uFE0F",95:"\u26C8\uFE0F",96:"\u26C8\uFE0F",99:"\u26C8\uFE0F"},S=["N","NNE","NE","ENE","E","ESE","SE","SSE","S","SSW","SW","WSW","W","WNW","NW","NNW"];function w(B){return S[Math.round(B/22.5)%16]}async function x(B){const P=safeGet(I);if(P)try{const g=JSON.parse(P);if(g.query===B)return g}catch{}const b=await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(B)}&count=1&language=en&format=json`);if(!b.ok)throw new Error("Geocoding failed");const H=await b.json();if(!H.results||!H.results.length)throw new Error("Location not found");const f=H.results[0],p={query:B,lat:f.latitude,lon:f.longitude,city:f.name,state:f.admin1||"",country:f.country||"",countryCode:f.country_code||""};return safeSet(I,JSON.stringify(p)),p}function C(B){return B.countryCode==="US"&&B.state?`${B.city}, ${B.state}`:B.country?`${B.city}, ${B.country}`:B.city}async function M(B){try{const P=await x(B),b=A(),H=b==="metric"?"celsius":"fahrenheit",f=b==="metric"?"kmh":"mph",p=`https://api.open-meteo.com/v1/forecast?latitude=${P.lat}&longitude=${P.lon}¤t=temperature_2m,weather_code,wind_speed_10m,wind_direction_10m&temperature_unit=${H}&wind_speed_unit=${f}`,g=await fetch(p);if(!g.ok)throw new Error("Weather fetch failed");const m=(await g.json()).current,r=m.weather_code;return{temp:Math.round(m.temperature_2m),condition:$[r]||"Unknown",icon:y[r]||"\u{1F324}\uFE0F",locationStr:C(P),windSpeed:Math.round(m.wind_speed_10m),windDir:w(m.wind_direction_10m),unit:b}}catch(P){return console.warn("Weather fetch failed:",P),null}}async function R(){const B=T();if(!B.icon||!B.temp||!B.condition||!B.location||!B.wind){console.warn("Weather widget elements not found");return}const P=safeGet(E);if(!P){B.location.textContent="Set Location",B.temp.textContent="--\xB0",B.condition.textContent="Click \u2699\uFE0F to configure",B.wind.textContent="--",B.icon.innerHTML='\u{1F324}\uFE0F';return}try{const b=await M(P);if(b){const H=b.unit==="metric"?"\xB0C":"\xB0F",f=b.unit==="metric"?"km/h":"mph";B.location.textContent=b.locationStr,B.temp.textContent=`${b.temp}${H}`,B.condition.textContent=b.condition,B.wind.textContent=`Wind: ${b.windSpeed} ${f} ${b.windDir}`,B.icon.innerHTML=`${escapeHtml(b.icon)}`}}catch(b){k.logError("[Weather] Update Error",b,{function:"updateWeather"}),B.location.textContent="Weather Error",B.temp.textContent="Error",B.condition.textContent="Failed to load",B.wind.textContent="--"}}const D=document.getElementById("weather-modal"),j=document.getElementById("weather-location-input");document.getElementById("weather-settings")?.addEventListener("click",()=>{j.value=safeGet(E)||"";const B=A(),P=D.querySelector(`input[name="weather-unit-radio"][value="${B}"]`);P&&(P.checked=!0),D.classList.add("show"),j.focus()}),document.getElementById("weather-cancel")?.addEventListener("click",()=>{D.classList.remove("show")}),document.getElementById("weather-save")?.addEventListener("click",()=>{const B=j.value.trim();if(B){safeGet(E)!==B&&safeSet(I,""),safeSet(E,B);const b=D.querySelector('input[name="weather-unit-radio"]:checked'),H=b?b.value:"imperial",f=A();safeSet(L,H),f!==H&&safeSet(I,""),D.classList.remove("show"),R()}else showNotification("Please enter a location (e.g., Hamburg, London, 90210)","warning")}),wireModal(D),document.addEventListener("keydown",B=>{B.key==="Escape"&&D.classList.contains("show")&&D.classList.remove("show")}),R(),setInterval(R,DC.POLL.WEATHER)})(),(function(){const k=document.getElementById("clock-widget"),E=document.getElementById("clock-render");if(!k||!E)return;const z=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],I=["January","February","March","April","May","June","July","August","September","October","November","December"],L=["XII","I","II","III","IV","V","VI","VII","VIII","IX","X","XI"];let A=safeGet("clock-style")||"default",T=-1,$=!1,y="",S="",w=null,x=null;function C(a){if($||safeGet("clock-chimes")!=="true")return;$=!0;const e=parseInt(safeGet("clock-chime-volume")||"50",10)/100;let n=0;function t(){if(n>=a){$=!1;return}const i=new Audio("/assets/sounds/church-bell.mp3");i.volume=e,i.play().catch(()=>{}),n++,n{$=!1},2500)}t()}function M(a){return z[a.getDay()]+", "+I[a.getMonth()]+" "+a.getDate()+", "+a.getFullYear()}function R(){S="",w=null}function D(){return S!=="digital"&&(E.innerHTML='
',w={main:E.querySelector(".clock-main"),seconds:E.querySelector(".clock-seconds"),ampm:E.querySelector(".clock-ampm"),date:E.querySelector(".clock-date")},S="digital"),w}function j(a){const e=a.getHours(),n=a.getMinutes(),t=a.getSeconds(),i=e>=12?"PM":"AM",o=e%12||12,d=D();d.main.textContent=`${o}:${String(n).padStart(2,"0")}`,d.seconds.textContent=`:${String(t).padStart(2,"0")}`,d.ampm.textContent=i,d.date.textContent=M(a)}function B(a,e){const n=a.getHours(),t=a.getMinutes(),i=a.getSeconds(),o=n>=12?"PM":"AM",d=n%12||12,l=D();l.main.textContent=`${String(d).padStart(2,"0")}:${String(t).padStart(2,"0")}`,l.seconds.textContent=`:${String(i).padStart(2,"0")}`,l.ampm.textContent=o,l.date.textContent=M(a)}function P(a){const e=a.getHours(),n=a.getMinutes(),t=a.getSeconds(),i=e>=12?"PM":"AM",o=e%12||12,d=String(o).padStart(2," ")+String(n).padStart(2,"0")+String(t).padStart(2,"0");let l='
';if(l+=b(d[0],0),l+=b(d[1],1),l+=':',l+=b(d[2],2),l+=b(d[3],3),l+=':',l+=b(d[4],4),l+=b(d[5],5),l+=`${i}`,l+="
",l+=`
${M(a)}
`,E.innerHTML=l,S="flip",y){for(let N=0;N<6;N++)if(d[N]!==y[N]){const O=E.querySelector(`.flip-card[data-idx="${N}"]`);O&&O.classList.add("flipping")}}y=d}function b(a,e){const n=a===" "?"":a;return`
${n}
${n}
`}function H(a){const e=a.getHours(),n=a.getMinutes(),t=a.getSeconds(),i=e%12||12,o=e>=12?"PM":"AM",d=[Math.floor(i/10),i%10,Math.floor(n/10),n%10,Math.floor(t/10),t%10];let l='
';l+='
HHMMSS
';for(let N=3;N>=0;N--){l+='
';for(let O=0;O<6;O++){const F=d[O]>>N&1;l+=`
`}l+="
"}l+='
';for(let N=0;N<6;N++)l+=`${d[N]}`;l+="
",l+=`
${o}
`,l+="
",l+=`
${M(a)}
`,E.innerHTML=l,S="binary"}function f(a,e){const n=a.getHours(),t=a.getMinutes(),i=a.getSeconds(),o=120,d=o/2,l=o/2,N=i/60*360-90,O=(t+i/60)/60*360-90,F=(n%12+t/60)/12*360-90;let q="";for(let X=1;X<=12;X++){const Q=X/12*2*Math.PI-Math.PI/2,ne=47,oe=d+ne*Math.cos(Q),se=l+ne*Math.sin(Q),Y=e?L[X%12]:X;q+=`${Y}`}let U="";for(let X=0;X<60;X++){const Q=X/60*2*Math.PI-Math.PI/2,ne=56,oe=X%5===0?52:54,se=d+oe*Math.cos(Q),Y=l+oe*Math.sin(Q),ie=d+ne*Math.cos(Q),re=l+ne*Math.sin(Q),ae=X%5===0?1.5:.5;U+=``}const G=` + ${U} - ${_} - - - - - `,Z=n.getHours()>=12?"PM":"AM";B.innerHTML=`
${W}
${n.getHours()%12||12}:${String(t).padStart(2,"0")} ${Z}${R(n)}
`,k="analog"}function u(){const n=new Date,e=n.getHours()%12||12,a=n.getMinutes(),t=n.getSeconds(),s="clock-widget"+(P!=="default"?" "+P:"");switch(w.className!==s&&(w.className=s),P){case"lcd":M(n);break;case"lcd-blue":M(n);break;case"lcd-amber":M(n);break;case"lcd-retro":M(n);break;case"lcd-taxi":M(n);break;case"flip":H(n);break;case"binary":T(n);break;case"analog":b(n,!1);break;case"roman":b(n,!0);break;default:N(n)}a===0&&t===0&&e!==$&&($=e,E(e)),a!==0&&($=-1)}function y(){clearTimeout(x);const n=document.hidden?6e4:1e3,e=n-Date.now()%n+25;x=setTimeout(()=>{u(),y()},e)}document.addEventListener("visibilitychange",()=>{g="",O(),u(),y()}),u(),y();const c=[{id:"default",label:"Default",icon:"\u{1F550}"},{id:"lcd",label:"LCD Green",icon:"\u{1F49A}"},{id:"lcd-blue",label:"LCD Blue",icon:"\u{1F499}"},{id:"lcd-amber",label:"LCD Amber",icon:"\u{1F7E0}"},{id:"lcd-retro",label:"LCD Retro",icon:"\u{1F7E9}"},{id:"lcd-taxi",label:"LCD Taxi",icon:"\u{1F7E1}"},{id:"flip",label:"Flip Clock",icon:"\u{1F4DF}"},{id:"binary",label:"Binary",icon:"\u{1F4BB}"},{id:"analog",label:"Analog",icon:"\u23F0"},{id:"roman",label:"Roman",icon:"\u{1F3DB}\uFE0F"}];let o='
';c.forEach(n=>{o+=``}),o+="
",injectModal("clock-settings-modal",`
+ ${q} + + + + + `,W=a.getHours()>=12?"PM":"AM";E.innerHTML=`
${G}
${a.getHours()%12||12}:${String(t).padStart(2,"0")} ${W}${M(a)}
`,S="analog"}function p(){const a=new Date,e=a.getHours()%12||12,n=a.getMinutes(),t=a.getSeconds(),i="clock-widget"+(A!=="default"?" "+A:"");switch(k.className!==i&&(k.className=i),A){case"lcd":B(a);break;case"lcd-blue":B(a);break;case"lcd-amber":B(a);break;case"lcd-retro":B(a);break;case"lcd-taxi":B(a);break;case"flip":P(a);break;case"binary":H(a);break;case"analog":f(a,!1);break;case"roman":f(a,!0);break;default:j(a)}n===0&&t===0&&e!==T&&(T=e,C(e)),n!==0&&(T=-1)}function g(){clearTimeout(x);const a=document.hidden?6e4:1e3,e=a-Date.now()%a+25;x=setTimeout(()=>{p(),g()},e)}document.addEventListener("visibilitychange",()=>{y="",R(),p(),g()}),p(),g();const v=[{id:"default",label:"Default",icon:"\u{1F550}"},{id:"lcd",label:"LCD Green",icon:"\u{1F49A}"},{id:"lcd-blue",label:"LCD Blue",icon:"\u{1F499}"},{id:"lcd-amber",label:"LCD Amber",icon:"\u{1F7E0}"},{id:"lcd-retro",label:"LCD Retro",icon:"\u{1F7E9}"},{id:"lcd-taxi",label:"LCD Taxi",icon:"\u{1F7E1}"},{id:"flip",label:"Flip Clock",icon:"\u{1F4DF}"},{id:"binary",label:"Binary",icon:"\u{1F4BB}"},{id:"analog",label:"Analog",icon:"\u23F0"},{id:"roman",label:"Roman",icon:"\u{1F3DB}\uFE0F"}];let m='
';v.forEach(a=>{m+=``}),m+="
",injectModal("clock-settings-modal",`

Clock Settings

- ${o} + ${m}
-
`);const l=document.getElementById("clock-settings-modal"),v=document.getElementById("clock-chimes-toggle"),r=document.getElementById("clock-chime-volume"),p=document.getElementById("clock-volume-section");function m(){const n=safeGet("clock-style")||"default",e=l.querySelector(`input[value="${n}"]`);e&&(e.checked=!0),v.checked=safeGet("clock-chimes")==="true",r.value=safeGet("clock-chime-volume")||"50",p.style.opacity=v.checked?"1":"0.4"}v?.addEventListener("change",()=>{p.style.opacity=v.checked?"1":"0.4"}),document.getElementById("clock-settings")?.addEventListener("click",()=>{m(),l.classList.add("show")}),document.getElementById("clock-chime-test")?.addEventListener("click",()=>{const n=parseInt(r.value,10)/100,e=new Audio("/assets/sounds/church-bell.mp3");e.volume=n,e.play().catch(()=>{})}),document.getElementById("clock-settings-save")?.addEventListener("click",()=>{const n=l.querySelector('input[name="clock-style-radio"]:checked'),e=n?n.value:"default";safeSet("clock-style",e),safeSet("clock-chimes",String(v.checked)),safeSet("clock-chime-volume",r.value),P=e,g="",O(),u(),y(),l.classList.remove("show"),showNotification("Clock settings saved","success",2e3)}),document.getElementById("clock-settings-cancel")?.addEventListener("click",()=>{l.classList.remove("show")}),wireModal(l),l?.querySelectorAll('input[name="clock-style-radio"]').forEach(n=>{n.addEventListener("change",()=>{P=n.value,g="",O(),u()})})})(),(function(){async function w(){try{const $=await(await fetch("/api/v1/health-checks/status")).json();if(!$.success||!$.status)return;for(const[L,g]of Object.entries($.status)){const k=document.getElementById("uptime-"+L),f=document.getElementById("uptime-bar-"+L);if(!k)continue;const x=g.uptime?.["24h"];if(x!=null){const E=x.toFixed(1);k.textContent=`${E}% uptime`,k.className="uptime-chip",x>=99.9?k.classList.add("excellent"):x>=99?k.classList.add("good"):x>=95?k.classList.add("degraded"):k.classList.add("poor"),f&&(f.style.width=E+"%")}}}catch{console.warn("[Card Badges] Health check API unavailable")}}let B;try{B=new Set(JSON.parse(safeSessionGet("dismissed-updates")||"[]"))}catch{B=new Set}async function z(){try{const $=await(await fetch("/api/v1/updates/available")).json();if(!$.success||(document.querySelectorAll(".update-available-badge").forEach(L=>L.classList.remove("visible")),!$.updates?.length))return;for(const L of $.updates){const g=window.APPS||[];for(const k of g)if(k.containerId===L.containerId||k.id===L.containerName||k.name===L.containerName){if(B.has(k.id))break;const f=document.getElementById("update-badge-"+k.id);f&&(f.classList.add("visible"),f.title=`Image digest changed. Click to dismiss if already up to date. -${L.imageName||""}`,f.style.cursor="pointer",f.onclick=x=>{x.stopPropagation(),f.classList.remove("visible"),B.add(k.id),safeSessionSet("dismissed-updates",JSON.stringify([...B]))});break}}}catch{console.warn("[Card Badges] Updates API unavailable")}}function I(){setTimeout(()=>{w(),z()},5e3),setInterval(()=>{w(),z()},6e4)}const C=window.refreshAll;C&&(window.refreshAll=async function(){try{await C(),setTimeout(w,1e3)}catch(P){console.warn("[Card Badges] Error in refreshAll hook:",P.message)}}),I()})(),(function(){var w=null,B=null,z={},I={dark:"Dark",light:"Light",blue:"Blue",black:"Black",nord:"Nord",dracula:"Dracula","solarized-dark":"Solarized Dark","solarized-light":"Solarized Light",taxi:"Taxi",ocean:"Ocean"},C=[["bg","Background","base"],["card-base","Card","base"],["fg","Text","base"],["muted","Muted Text","base"],["border","Border","base"],["accent","Accent","accent"],["accent-strong","Accent Strong","accent"],["ok-bg","OK Background","status"],["ok-fg","OK Text","status"],["bad-bg","Error Bg","status"],["bad-fg","Error Text","status"],["dot-ok","Dot OK","status"],["dot-bad","Dot Error","status"],["uptime","Uptime Bar","status"],["hover","Hover","advanced"],["card-hover","Card Hover","advanced"],["base","Tags/Badges","advanced"],["fg-muted","Dim Text","advanced"],["success","Success","advanced"],["error","Error","advanced"],["warning","Warning","advanced"]],P=document.getElementById("theme");if(!P)return;var $=document.getElementById("theme-label");function L(t){if(I[t])return I[t];var s=safeGetJSON(window.USER_THEMES_KEY,{});return s[t]&&s[t].name||t}function g(){$&&($.textContent=L(window.getActiveTheme()))}P.addEventListener("click",function(){var t=window.THEMES.slice(),s=window.getActiveTheme(),i=t.indexOf(s),h=t[(i+1)%t.length];window.applyTheme(h),g()}),g();function k(){var t={base:"Base Colors",accent:"Accent",status:"Status",advanced:"Advanced (auto-derived)"},s={};C.forEach(function(h){s[h[2]]||(s[h[2]]=[]),s[h[2]].push(h)});var i="";return Object.keys(t).forEach(function(h){h==="advanced"?(i+='
Show advanced colors ▼
',i+='`).join("")}async function B(){try{const v=await(await fetch("/api/v1/license/status")).json();v.success&&(j(v.license),H(v.license))}catch(g){console.warn("Failed to load license status:",g.message)}}async function P(){const g=E.value.trim();if(!g){R("Please enter a license code.");return}M(),z.disabled=!0,z.textContent="Activating...";try{const m=await(await secureFetch("/api/v1/license/activate",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:g})})).json();m.success?(D(m.message),E.value="",j(m.license),showNotification("License activated! Premium features unlocked.","success",5e3),H(m.license)):R(m.error||"Activation failed")}catch(v){R("Network error: "+v.message)}finally{z.disabled=!1,z.textContent="Activate"}}async function b(){if(confirm("Deactivate your license? You can reuse the code on another machine.")){I.disabled=!0,I.textContent="Deactivating...";try{const v=await(await secureFetch("/api/v1/license/deactivate",{method:"POST"})).json();v.success?(D(v.message),await B(),showNotification("License deactivated.","info",3e3),H({active:!1})):R(v.error||"Deactivation failed")}catch(g){R("Network error: "+g.message)}finally{I.disabled=!1,I.textContent="Deactivate"}}}function H(g){const v=document.getElementById("license-status-topbar"),m=document.getElementById("license-topbar-icon"),r=document.getElementById("license-topbar-text"),c=document.getElementById("license-topbar-time");if(v)if(v.className="license-status-topbar "+(g.active?"premium":"free"),g.active)if(m.textContent="\u2605",r.textContent="PREMIUM",g.lifetime)c.textContent="\xB7 LIFETIME";else{const s=g.daysRemaining;c.textContent=s!=null?"\xB7 "+s+"d remaining":""}else m.textContent="\u2606",r.textContent=g.expired?"EXPIRED":"FREE TIER",c.textContent=""}function f(){M(),B(),k.classList.add("show")}E.addEventListener("input",function(){let g=this.value.toUpperCase().replace(/[^A-Z0-9-]/g,"");if(g.length>this._prevLength&&(g=g.replace(/-/g,""),g.length>2&&!g.startsWith("DC")&&(g="DC"+g),g.startsWith("DC")&&g.length>2)){const v=["DC"],m=g.substring(2);for(let r=0;r{g.key==="Enter"&&P()}),wireModal(k,document.getElementById("license-cancel"));const p=document.getElementById("license-status-topbar");p&&p.addEventListener("click",()=>window.openLicenseModal&&window.openLicenseModal()),window.openLicenseModal=f,window.checkPremiumFeature=async function(g){try{return(await(await fetch(`/api/v1/license/feature/${g}`)).json()).available}catch{return!1}},B().then(g=>{C&&H(C)})})(); diff --git a/status/dist/init.js b/status/dist/init.js index 48b8d44..28fd97a 100644 --- a/status/dist/init.js +++ b/status/dist/init.js @@ -1,4 +1,4 @@ -(function(){function k(){const a=safeGet("custom-services");if(a)try{JSON.parse(a).forEach(i=>{window.APPS.find(r=>r.id===i.id)||window.APPS.push(i)})}catch(c){console.warn("Failed to load custom services:",c)}}k();function n(){const a=document.querySelectorAll(".top .card");a.forEach((c,i)=>{c.style.transitionDelay=`${Math.min(i*60,300)}ms`}),requestAnimationFrame(()=>{a.forEach(c=>c.classList.add("loaded"))})}function u(){if(!("serviceWorker"in navigator)||!window.isSecureContext&&location.hostname!=="localhost"&&location.hostname!=="127.0.0.1")return;const a=()=>{navigator.serviceWorker.register("/sw.js",{updateViaCache:"none"}).catch(c=>{console.warn("[init] Service worker registration failed:",c)})};document.readyState==="complete"?a():window.addEventListener("load",a,{once:!0})}let l=!1;async function m(){if(l){console.warn("[init] initializeDashboard called again, skipping duplicate");return}if(l=!0,await window.loadServices(),window.buildGrid(),n(),window.refreshAll(),setInterval(window.refreshAll,DC.POLL.DASHBOARD),typeof window.refreshCredsButtons=="function"&&window.refreshCredsButtons(),typeof window._updateAuthCard=="function")try{const c=await(await fetch("/api/v1/totp/config",{cache:"no-store"})).json();c.success&&window._updateAuthCard(c.config.enabled&&c.config.isSetUp,c.config.sessionDuration)}catch{}if(window.__dashcaddySiteConfigLoaded)try{await window.__dashcaddySiteConfigLoaded}catch{}p(),q()&&v()}function v(){if(document.querySelector('script[src="/dist/onboarding.js"]'))return;const a=document.createElement("script");if(a.src="/dist/onboarding.js",a.defer=!0,document.head.appendChild(a),!document.querySelector('link[href="/css/driver.min.css"]')){const c=document.createElement("link");c.rel="stylesheet",c.href="/css/driver.min.css",document.head.appendChild(c)}if(!document.querySelector('link[href="/css/onboarding.css"]')){const c=document.createElement("link");c.rel="stylesheet",c.href="/css/onboarding.css",document.head.appendChild(c)}}function q(){if(typeof SITE<"u"&&SITE.onboardingCompleted)return!1;try{const a=JSON.parse(localStorage.getItem("dashcaddy_onboarding"));return!a||!a.tourCompleted&&a.currentStep===0}catch{return!0}}function g(){const a=document.querySelectorAll(".tools-section");if(!a.length)return;let c={};try{c=JSON.parse(localStorage.getItem("toolbar-sections")||"{}")}catch{}a.forEach(i=>{const r=i.dataset.section,h=i.querySelector(".tools-section-header");h&&(c[r]&&(i.classList.add("open"),h.setAttribute("aria-expanded","true")),h.addEventListener("click",S=>{S.preventDefault();const y=i.classList.toggle("open");h.setAttribute("aria-expanded",y?"true":"false");const w={};document.querySelectorAll(".tools-section").forEach(f=>{w[f.dataset.section]=f.classList.contains("open")}),localStorage.setItem("toolbar-sections",JSON.stringify(w))}))})}g();function p(){if(document.getElementById("restart-tour-btn"))return;let a=typeof SITE<"u"&&SITE.onboardingCompleted;try{const r=JSON.parse(localStorage.getItem("dashcaddy_onboarding"));a=a||!!(r&&r.tourCompleted)}catch{}const c=a?document.querySelector('.tools-section[data-section="admin"] .tools-section-items'):document.querySelector(".tools-primary");if(!c)return;const i=document.createElement("button");i.id="restart-tour-btn",i.textContent=a?"Help Tour":"\u{1F393} Help Tour",i.title="Restart the onboarding tour",i.onclick=()=>{if(window.DashCaddyOnboarding)window.DashCaddyOnboarding.restartTour();else{v();const r=setInterval(()=>{window.DashCaddyOnboarding&&(clearInterval(r),window.DashCaddyOnboarding.restartTour())},100);setTimeout(()=>clearInterval(r),5e3)}},c.appendChild(i)}window.initializeDashboard=m,window.loadCustomServices=k,u(),(async()=>{try{const c=await(await fetch("/api/v1/totp/config",{cache:"no-store"})).json();if(c.success&&c.config.enabled&&(await fetch("/api/v1/totp/check-session",{cache:"no-store"})).status===401){window._showTotpOverlay();return}}catch(a){console.warn("TOTP check failed, proceeding normally:",a)}m()})()})(),(function(){"use strict";const k=["#app-selector-modal","#app-deploy-modal","#weather-modal","#token-management-modal","#service-edit-modal","#notifications-modal","#backup-modal","#stats-modal","#arr-setup-modal","#add-service-modal","#error-log-modal","#logs-modal","#dns-template-modal"];let n=null,u=null,l=null;function m(){try{v(),document.addEventListener("keydown",q),console.log("[Keyboard Shortcuts] Initialized"),console.log("[Keyboard Shortcuts] Press Ctrl+K to open quick search"),console.log("[Keyboard Shortcuts] Press Escape to close modals")}catch(t){console.warn("[Keyboard Shortcuts] Failed to initialize:",t.message)}}function v(){n=document.createElement("div"),n.id="quick-search-modal",n.className="quick-search-modal",n.innerHTML=` +(function(){function p(){const a=safeGet("custom-services");if(a)try{JSON.parse(a).forEach(i=>{window.APPS.find(r=>r.id===i.id)||window.APPS.push(i)})}catch(c){console.warn("Failed to load custom services:",c)}}p();function y(){const a=document.querySelectorAll(".top .card");a.forEach((c,i)=>{c.style.transitionDelay=`${Math.min(i*60,300)}ms`}),requestAnimationFrame(()=>{a.forEach(c=>c.classList.add("loaded"))})}function n(){if(!("serviceWorker"in navigator)||!window.isSecureContext&&location.hostname!=="localhost"&&location.hostname!=="127.0.0.1")return;const a=()=>{navigator.serviceWorker.register("/sw.js",{updateViaCache:"none"}).catch(c=>{console.warn("[init] Service worker registration failed:",c)})};document.readyState==="complete"?a():window.addEventListener("load",a,{once:!0})}let u=!1;async function l(){if(u){console.warn("[init] initializeDashboard called again, skipping duplicate");return}if(u=!0,await window.loadServices(),window.buildGrid(),y(),window.refreshAll(),setInterval(window.refreshAll,DC.POLL.DASHBOARD),typeof window.refreshCredsButtons=="function"&&window.refreshCredsButtons(),typeof window._updateAuthCard=="function")try{const c=await(await fetch("/api/v1/totp/config",{cache:"no-store"})).json();c.success&&window._updateAuthCard(c.config.enabled&&c.config.isSetUp,c.config.sessionDuration)}catch{}if(window.__dashcaddySiteConfigLoaded)try{await window.__dashcaddySiteConfigLoaded}catch{}k(),w()&&m()}function m(){if(document.querySelector('script[src="/dist/onboarding.js"]'))return;const a=document.createElement("script");if(a.src="/dist/onboarding.js",a.defer=!0,document.head.appendChild(a),!document.querySelector('link[href="/css/driver.min.css"]')){const c=document.createElement("link");c.rel="stylesheet",c.href="/css/driver.min.css",document.head.appendChild(c)}if(!document.querySelector('link[href="/css/onboarding.css"]')){const c=document.createElement("link");c.rel="stylesheet",c.href="/css/onboarding.css",document.head.appendChild(c)}}function w(){if(typeof SITE<"u"&&SITE.onboardingCompleted)return!1;try{const a=JSON.parse(localStorage.getItem("dashcaddy_onboarding"));return!a||!a.tourCompleted&&a.currentStep===0}catch{return!0}}function b(){const a=document.querySelectorAll(".tools-section");if(!a.length)return;let c={};try{c=JSON.parse(localStorage.getItem("toolbar-sections")||"{}")}catch{}a.forEach(i=>{const r=i.dataset.section,h=i.querySelector(".tools-section-header");h&&(c[r]&&(i.classList.add("open"),h.setAttribute("aria-expanded","true")),h.addEventListener("click",q=>{q.preventDefault();const S=i.classList.toggle("open");h.setAttribute("aria-expanded",S?"true":"false");const f={};document.querySelectorAll(".tools-section").forEach(v=>{f[v.dataset.section]=v.classList.contains("open")}),localStorage.setItem("toolbar-sections",JSON.stringify(f))}))})}b();function k(){if(document.getElementById("restart-tour-btn"))return;let a=typeof SITE<"u"&&SITE.onboardingCompleted;try{const r=JSON.parse(localStorage.getItem("dashcaddy_onboarding"));a=a||!!(r&&r.tourCompleted)}catch{}const c=a?document.querySelector('.tools-section[data-section="admin"] .tools-section-items'):document.querySelector(".tools-primary");if(!c)return;const i=document.createElement("button");i.id="restart-tour-btn",i.textContent=a?"Help Tour":"\u{1F393} Help Tour",i.title="Restart the onboarding tour",i.onclick=()=>{if(window.DashCaddyOnboarding)window.DashCaddyOnboarding.restartTour();else{m();const r=setInterval(()=>{window.DashCaddyOnboarding&&(clearInterval(r),window.DashCaddyOnboarding.restartTour())},100);setTimeout(()=>clearInterval(r),5e3)}},c.appendChild(i)}window.initializeDashboard=l,window.loadCustomServices=p,n(),(async()=>{try{const c=await(await fetch("/api/v1/totp/config",{cache:"no-store"})).json();if(c.success&&c.config.enabled&&(await fetch("/api/v1/totp/check-session",{cache:"no-store"})).status===401){window._showTotpOverlay();return}}catch(a){console.warn("TOTP check failed, proceeding normally:",a)}l()})()})(),(function(){"use strict";const p=(...t)=>{window.DASHCADDY_DEBUG&&console.log(...t)},y=["#app-selector-modal","#app-deploy-modal","#weather-modal","#token-management-modal","#service-edit-modal","#notifications-modal","#backup-modal","#stats-modal","#arr-setup-modal","#add-service-modal","#error-log-modal","#logs-modal","#dns-template-modal"];let n=null,u=null,l=null;function m(){try{w(),document.addEventListener("keydown",b),p("[Keyboard Shortcuts] Initialized"),p("[Keyboard Shortcuts] Press Ctrl+K to open quick search"),p("[Keyboard Shortcuts] Press Escape to close modals")}catch(t){console.warn("[Keyboard Shortcuts] Failed to initialize:",t.message)}}function w(){n=document.createElement("div"),n.id="quick-search-modal",n.className="quick-search-modal",n.innerHTML=`
\u{1F50D} @@ -160,7 +160,7 @@ font-family: monospace; margin-right: 4px; } - `,document.head.appendChild(t),document.body.appendChild(n),u=document.getElementById("quick-search-input"),l=document.getElementById("quick-search-results"),u.addEventListener("input",r),u.addEventListener("keydown",w),n.addEventListener("click",e=>{e.target===n&&p()})}function q(t){try{if((t.ctrlKey||t.metaKey)&&t.key==="k"){t.preventDefault(),g();return}if(t.key==="Escape"){if(n&&n.classList.contains("show")){p();return}a()}}catch(e){console.warn("[Keyboard Shortcuts] Error handling keydown:",e.message)}}function g(){try{n.classList.add("show"),u.value="",u.focus(),c()}catch(t){console.warn("[Keyboard Shortcuts] Error opening quick search:",t.message)}}function p(){try{n.classList.remove("show"),u.value="",l.innerHTML=""}catch(t){console.warn("[Keyboard Shortcuts] Error closing quick search:",t.message)}}function a(){for(const t of k){const e=document.querySelector(t);if(e&&(e.classList.contains("show")||e.style.display==="flex"))return e.classList.remove("show"),e.style.display="none",!0}return!1}function c(){const t=` + `,document.head.appendChild(t),document.body.appendChild(n),u=document.getElementById("quick-search-input"),l=document.getElementById("quick-search-results"),u.addEventListener("input",h),u.addEventListener("keydown",v),n.addEventListener("click",e=>{e.target===n&&a()})}function b(t){try{if((t.ctrlKey||t.metaKey)&&t.key==="k"){t.preventDefault(),k();return}if(t.key==="Escape"){if(n&&n.classList.contains("show")){a();return}c()}}catch(e){console.warn("[Keyboard Shortcuts] Error handling keydown:",e.message)}}function k(){try{n.classList.add("show"),u.value="",u.focus(),i()}catch(t){console.warn("[Keyboard Shortcuts] Error opening quick search:",t.message)}}function a(){try{n.classList.remove("show"),u.value="",l.innerHTML=""}catch(t){console.warn("[Keyboard Shortcuts] Error closing quick search:",t.message)}}function c(){for(const t of y){const e=document.querySelector(t);if(e&&(e.classList.contains("show")||e.style.display==="flex"))return e.classList.remove("show"),e.style.display="none",!0}return!1}function i(){const t=`
Quick Actions
\u{1F504} @@ -192,9 +192,9 @@
Services
- ${i()} - `;l.innerHTML=t,y()}function i(){const t=document.querySelectorAll(".card[data-app], #cards .card");let e="";return t.forEach(s=>{const d=s.querySelector(".name")?.textContent||"Unknown",o=s.dataset.status||"unknown",b=s.dataset.app||"";d&&d!=="--"&&(e+=` -
+ ${r()} + `;l.innerHTML=t,f()}function r(){const t=document.querySelectorAll(".card[data-app], #cards .card");let e="";return t.forEach(s=>{const d=s.querySelector(".name")?.textContent||"Unknown",o=s.dataset.status||"unknown",g=s.dataset.app||"";d&&d!=="--"&&(e+=` +
${o==="on"?"\u{1F7E2}":"\u{1F534}"}
${d}
@@ -202,7 +202,7 @@
${o.toUpperCase()}
- `)}),e||'
No services found
'}function r(t){try{const e=t.target.value.toLowerCase().trim();if(!e){c();return}const s=h(e);S(s)}catch(e){console.warn("[Keyboard Shortcuts] Error handling search input:",e.message)}}function h(t){const e={actions:[],services:[]};return[{id:"refresh",title:"Refresh Dashboard",icon:"\u{1F504}",keywords:"refresh reload update status"},{id:"reload-caddy",title:"Reload Caddy",icon:"\u26A1",keywords:"reload caddy proxy config"},{id:"add-service",title:"Add Service",icon:"\u2795",keywords:"add new service create"},{id:"app-selector",title:"App Selector",icon:"\u{1F4F1}",keywords:"app deploy install docker container"},{id:"backup",title:"Backup & Restore",icon:"\u{1F4BE}",keywords:"backup restore export import"},{id:"stats",title:"Container Stats",icon:"\u{1F4CA}",keywords:"stats resources cpu memory"},{id:"logs",title:"View Logs",icon:"\u{1F4CB}",keywords:"logs error debug"},{id:"tokens",title:"Manage Tokens",icon:"\u{1F511}",keywords:"tokens api keys credentials"},{id:"notifications",title:"Notifications",icon:"\u{1F514}",keywords:"alerts notifications discord telegram"},{id:"theme",title:"Change Theme",icon:"\u{1F3A8}",keywords:"theme dark light appearance"},{id:"tour",title:"Help Tour",icon:"\u{1F393}",keywords:"help tour guide onboarding"}].forEach(o=>{(o.title.toLowerCase().includes(t)||o.keywords.includes(t))&&e.actions.push(o)}),document.querySelectorAll(".card[data-app], #cards .card").forEach(o=>{const b=o.querySelector(".name")?.textContent||"",x=o.dataset.app||"",E=o.dataset.status||"unknown";(b.toLowerCase().includes(t)||x.toLowerCase().includes(t))&&e.services.push({id:x,title:b,status:E,icon:E==="on"?"\u{1F7E2}":"\u{1F534}"})}),e}function S(t){let e="";t.actions.length>0&&(e+='
Actions
',t.actions.forEach(s=>{e+=` + `)}),e||'
No services found
'}function h(t){try{const e=t.target.value.toLowerCase().trim();if(!e){i();return}const s=q(e);S(s)}catch(e){console.warn("[Keyboard Shortcuts] Error handling search input:",e.message)}}function q(t){const e={actions:[],services:[]};return[{id:"refresh",title:"Refresh Dashboard",icon:"\u{1F504}",keywords:"refresh reload update status"},{id:"reload-caddy",title:"Reload Caddy",icon:"\u26A1",keywords:"reload caddy proxy config"},{id:"add-service",title:"Add Service",icon:"\u2795",keywords:"add new service create"},{id:"app-selector",title:"App Selector",icon:"\u{1F4F1}",keywords:"app deploy install docker container"},{id:"backup",title:"Backup & Restore",icon:"\u{1F4BE}",keywords:"backup restore export import"},{id:"stats",title:"Container Stats",icon:"\u{1F4CA}",keywords:"stats resources cpu memory"},{id:"logs",title:"View Logs",icon:"\u{1F4CB}",keywords:"logs error debug"},{id:"tokens",title:"Manage Tokens",icon:"\u{1F511}",keywords:"tokens api keys credentials"},{id:"notifications",title:"Notifications",icon:"\u{1F514}",keywords:"alerts notifications discord telegram"},{id:"theme",title:"Change Theme",icon:"\u{1F3A8}",keywords:"theme dark light appearance"},{id:"tour",title:"Help Tour",icon:"\u{1F393}",keywords:"help tour guide onboarding"}].forEach(o=>{(o.title.toLowerCase().includes(t)||o.keywords.includes(t))&&e.actions.push(o)}),document.querySelectorAll(".card[data-app], #cards .card").forEach(o=>{const g=o.querySelector(".name")?.textContent||"",E=o.dataset.app||"",C=o.dataset.status||"unknown";(g.toLowerCase().includes(t)||E.toLowerCase().includes(t))&&e.services.push({id:E,title:g,status:C,icon:C==="on"?"\u{1F7E2}":"\u{1F534}"})}),e}function S(t){let e="";t.actions.length>0&&(e+='
Actions
',t.actions.forEach(s=>{e+=`
${s.icon}
@@ -217,4 +217,4 @@
${s.status.toUpperCase()}
- `})),e||(e='
No results found
'),l.innerHTML=e,y()}function y(){l.querySelectorAll(".quick-search-item").forEach((e,s)=>{e.addEventListener("click",()=>f(e)),s===0&&e.classList.add("selected")})}function w(t){try{const e=l.querySelectorAll(".quick-search-item"),s=l.querySelector(".quick-search-item.selected"),d=Array.from(e).indexOf(s);if(t.key==="ArrowDown"){t.preventDefault(),s&&s.classList.remove("selected");const o=(d+1)%e.length;e[o]?.classList.add("selected"),e[o]?.scrollIntoView({block:"nearest"})}else if(t.key==="ArrowUp"){t.preventDefault(),s&&s.classList.remove("selected");const o=d<=0?e.length-1:d-1;e[o]?.classList.add("selected"),e[o]?.scrollIntoView({block:"nearest"})}else t.key==="Enter"&&(t.preventDefault(),s&&f(s))}catch(e){console.warn("[Keyboard Shortcuts] Error handling search navigation:",e.message)}}function f(t){try{const e=t.dataset.action,s=t.dataset.service;switch(p(),e){case"refresh":document.getElementById("refresh")?.click();break;case"reload-caddy":document.getElementById("reload-caddy-top")?.click();break;case"add-service":document.getElementById("add-service")?.click();break;case"app-selector":document.getElementById("add-service-btn")?.click();break;case"backup":document.getElementById("backup-restore-btn")?.click();break;case"stats":document.getElementById("container-stats-btn")?.click();break;case"logs":document.getElementById("view-error-logs")?.click();break;case"tokens":document.getElementById("manage-tokens")?.click();break;case"notifications":document.getElementById("manage-notifications")?.click();break;case"theme":document.getElementById("theme")?.click();break;case"tour":document.getElementById("restart-tour-btn")?.click();break;case"open-service":if(s){const d=document.querySelector(`[data-app="${s}"] [id$="-open"], [data-app="${s}"] button:not(.restart-btn):not(.logs-btn):not(.settings-btn)`);if(d)d.click();else{const o=document.querySelector(`[data-app="${s}"]`);o&&o.click()}}break;default:console.log("[Keyboard Shortcuts] Unknown action:",e)}}catch(e){console.warn("[Keyboard Shortcuts] Error executing action:",e.message)}}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",m):m(),window.DashCaddyKeyboardShortcuts={openQuickSearch:g,closeQuickSearch:p}})(); + `})),e||(e='
No results found
'),l.innerHTML=e,f()}function f(){l.querySelectorAll(".quick-search-item").forEach((e,s)=>{e.addEventListener("click",()=>x(e)),s===0&&e.classList.add("selected")})}function v(t){try{const e=l.querySelectorAll(".quick-search-item"),s=l.querySelector(".quick-search-item.selected"),d=Array.from(e).indexOf(s);if(t.key==="ArrowDown"){t.preventDefault(),s&&s.classList.remove("selected");const o=(d+1)%e.length;e[o]?.classList.add("selected"),e[o]?.scrollIntoView({block:"nearest"})}else if(t.key==="ArrowUp"){t.preventDefault(),s&&s.classList.remove("selected");const o=d<=0?e.length-1:d-1;e[o]?.classList.add("selected"),e[o]?.scrollIntoView({block:"nearest"})}else t.key==="Enter"&&(t.preventDefault(),s&&x(s))}catch(e){console.warn("[Keyboard Shortcuts] Error handling search navigation:",e.message)}}function x(t){try{const e=t.dataset.action,s=t.dataset.service;switch(a(),e){case"refresh":document.getElementById("refresh")?.click();break;case"reload-caddy":document.getElementById("reload-caddy-top")?.click();break;case"add-service":document.getElementById("add-service")?.click();break;case"app-selector":document.getElementById("add-service-btn")?.click();break;case"backup":document.getElementById("backup-restore-btn")?.click();break;case"stats":document.getElementById("container-stats-btn")?.click();break;case"logs":document.getElementById("view-error-logs")?.click();break;case"tokens":document.getElementById("manage-tokens")?.click();break;case"notifications":document.getElementById("manage-notifications")?.click();break;case"theme":document.getElementById("theme")?.click();break;case"tour":document.getElementById("restart-tour-btn")?.click();break;case"open-service":if(s){const d=document.querySelector(`[data-app="${s}"] [id$="-open"], [data-app="${s}"] button:not(.restart-btn):not(.logs-btn):not(.settings-btn)`);if(d)d.click();else{const o=document.querySelector(`[data-app="${s}"]`);o&&o.click()}}break;default:p("[Keyboard Shortcuts] Unknown action:",e)}}catch(e){console.warn("[Keyboard Shortcuts] Error executing action:",e.message)}}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",m):m(),window.DashCaddyKeyboardShortcuts={openQuickSearch:k,closeQuickSearch:a}})(); diff --git a/status/dist/onboarding.js b/status/dist/onboarding.js index eca6a29..5771f91 100644 --- a/status/dist/onboarding.js +++ b/status/dist/onboarding.js @@ -1,5 +1,5 @@ -this.driver=this.driver||{},this.driver.js=(function(v){"use strict";let T={};function P(o={}){T={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",...o}}function e(o){return o?T[o]:T}function t(o,r,n,s){return(o/=s/2)<1?n/2*o*o+r:-n/2*(--o*(o-2)-1)+r}function a(o){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 o.flatMap(n=>{const s=n.matches(r),i=Array.from(n.querySelectorAll(r));return[...s?[n]:[],...i]}).filter(n=>getComputedStyle(n).pointerEvents!=="none"&&A(n))}function p(o){if(!o||m(o))return;const r=e("smoothScroll");o.scrollIntoView({behavior:!r||y(o)?"auto":"smooth",inline:"center",block:"center"})}function y(o){if(!o||!o.parentElement)return;const r=o.parentElement;return r.scrollHeight>r.clientHeight}function m(o){const r=o.getBoundingClientRect();return r.top>=0&&r.left>=0&&r.bottom<=(window.innerHeight||document.documentElement.clientHeight)&&r.right<=(window.innerWidth||document.documentElement.clientWidth)}function A(o){return!!(o.offsetWidth||o.offsetHeight||o.getClientRects().length)}let L={};function E(o,r){L[o]=r}function d(o){return o?L[o]:L}function K(){L={}}let F={};function R(o,r){F[o]=r}function O(o){var r;(r=F[o])==null||r.call(F)}function se(){F={}}function ae(o,r,n,s){let i=d("__activeStagePosition");const u=i||n.getBoundingClientRect(),b=s.getBoundingClientRect(),S=t(o,u.x,b.x-u.x,r),c=t(o,u.y,b.y-u.y,r),w=t(o,u.width,b.width-u.width,r),l=t(o,u.height,b.height-u.height,r);i={x:S,y:c,width:w,height:l},J(i),E("__activeStagePosition",i)}function Y(o){if(!o)return;const r=o.getBoundingClientRect(),n={x:r.x,y:r.y,width:r.width,height:r.height};E("__activeStagePosition",n),J(n)}function le(){const o=d("__activeStagePosition"),r=d("__overlaySvg");if(!o)return;if(!r){console.warn("No stage svg found.");return}const n=window.innerWidth,s=window.innerHeight;r.setAttribute("viewBox",`0 0 ${n} ${s}`)}function de(o){const r=ce(o);document.body.appendChild(r),ee(r,n=>{n.target.tagName==="path"&&O("overlayClick")}),E("__overlaySvg",r)}function J(o){const r=d("__overlaySvg");if(!r){de(o);return}const n=r.firstElementChild;if(n?.tagName!=="path")throw new Error("no path element found in stage svg");n.setAttribute("d",Q(o))}function ce(o){const r=window.innerWidth,n=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 ${r} ${n}`),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 i=document.createElementNS("http://www.w3.org/2000/svg","path");return i.setAttribute("d",Q(o)),i.style.fill=e("overlayColor")||"rgb(0,0,0)",i.style.opacity=`${e("overlayOpacity")}`,i.style.pointerEvents="auto",i.style.cursor="auto",s.appendChild(i),s}function Q(o){const r=window.innerWidth,n=window.innerHeight,s=e("stagePadding")||0,i=e("stageRadius")||0,u=o.width+s*2,b=o.height+s*2,S=Math.min(i,u/2,b/2),c=Math.floor(Math.max(S,0)),w=o.x-s+c,l=o.y-s,h=u-c*2,g=b-c*2;return`M${r},0L0,0L0,${n}L${r},${n}L${r},0Z - M${w},${l} h${h} a${c},${c} 0 0 1 ${c},${c} v${g} a${c},${c} 0 0 1 -${c},${c} h-${h} a${c},${c} 0 0 1 -${c},-${c} v-${g} a${c},${c} 0 0 1 ${c},-${c} z`}function pe(){const o=d("__overlaySvg");o&&o.remove()}function ue(){const o=document.getElementById("driver-dummy-element");if(o)return o;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(o){const{element:r}=o;let n=typeof r=="string"?document.querySelector(r):r;n||(n=ue()),he(n,o)}function me(){const o=d("__activeElement"),r=d("__activeStep");o&&(Y(o),le(),ne(o,r))}function he(o,r){const n=Date.now(),s=d("__activeStep"),i=d("__activeElement")||o,u=!i||i===o,b=o.id==="driver-dummy-element",S=i.id==="driver-dummy-element",c=e("animate"),w=r.onHighlightStarted||e("onHighlightStarted"),l=r?.onHighlighted||e("onHighlighted"),h=s?.onDeselected||e("onDeselected"),g=e(),C=d();!u&&h&&h(S?void 0:i,s,{config:g,state:C}),w&&w(b?void 0:o,r,{config:g,state:C});const k=!u&&c;let x=!1;we(),E("previousStep",s),E("previousElement",i),E("activeStep",r),E("activeElement",o);const f=()=>{if(d("__transitionCallback")!==f)return;const D=Date.now()-n,B=400-D<=400/2;r.popover&&B&&!x&&k&&(te(o,r),x=!0),e("animate")&&D<400?ae(D,400,i,o):(Y(o),l&&l(b?void 0:o,r,{config:e(),state:d()}),E("__transitionCallback",void 0),E("__previousStep",s),E("__previousElement",i),E("__activeStep",r),E("__activeElement",o)),window.requestAnimationFrame(f)};E("__transitionCallback",f),window.requestAnimationFrame(f),p(o),!k&&r.popover&&te(o,r),i.classList.remove("driver-active-element","driver-no-interaction"),i.removeAttribute("aria-haspopup"),i.removeAttribute("aria-expanded"),i.removeAttribute("aria-controls"),e("disableActiveInteraction")&&o.classList.add("driver-no-interaction"),o.classList.add("driver-active-element"),o.setAttribute("aria-haspopup","dialog"),o.setAttribute("aria-expanded","true"),o.setAttribute("aria-controls","driver-popover-content")}function ge(){var o;(o=document.getElementById("driver-dummy-element"))==null||o.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 z(){const o=d("__resizeTimeout");o&&window.cancelAnimationFrame(o),E("__resizeTimeout",window.requestAnimationFrame(me))}function ve(o){var r;if(!d("isInitialized")||!(o.key==="Tab"||o.keyCode===9))return;const n=d("__activeElement"),s=(r=d("popover"))==null?void 0:r.wrapper,i=a([...s?[s]:[],...n?[n]:[]]),u=i[0],b=i[i.length-1];if(o.preventDefault(),o.shiftKey){const S=i[i.indexOf(document.activeElement)-1]||b;S?.focus()}else{const S=i[i.indexOf(document.activeElement)+1]||u;S?.focus()}}function Z(o){var r;((r=e("allowKeyboardControl"))==null||r)&&(o.key==="Escape"?O("escapePress"):o.key==="ArrowRight"?O("arrowRightPress"):o.key==="ArrowLeft"&&O("arrowLeftPress"))}function ee(o,r,n){const s=(i,u)=>{const b=i.target;o.contains(b)&&((!n||n(b))&&(i.preventDefault(),i.stopPropagation(),i.stopImmediatePropagation()),u?.(i))};document.addEventListener("pointerdown",s,!0),document.addEventListener("mousedown",s,!0),document.addEventListener("pointerup",s,!0),document.addEventListener("mouseup",s,!0),document.addEventListener("click",i=>{s(i,r)},!0)}function fe(){window.addEventListener("keyup",Z,!1),window.addEventListener("keydown",ve,!1),window.addEventListener("resize",z),window.addEventListener("scroll",z)}function ye(){window.removeEventListener("keyup",Z),window.removeEventListener("resize",z),window.removeEventListener("scroll",z)}function we(){const o=d("popover");o&&(o.wrapper.style.display="none")}function te(o,r){var n,s;let i=d("popover");i&&document.body.removeChild(i.wrapper),i=Te(),document.body.appendChild(i.wrapper);const{title:u,description:b,showButtons:S,disableButtons:c,showProgress:w,nextBtnText:l=e("nextBtnText")||"Next →",prevBtnText:h=e("prevBtnText")||"← Previous",progressText:g=e("progressText")||"{current} of {total}"}=r.popover||{};i.nextButton.innerHTML=l,i.previousButton.innerHTML=h,i.progress.innerHTML=g,u?(i.title.innerHTML=u,i.title.style.display="block"):i.title.style.display="none",b?(i.description.innerHTML=b,i.description.style.display="block"):i.description.style.display="none";const C=S||e("showButtons"),k=w||e("showProgress")||!1,x=C?.includes("next")||C?.includes("previous")||k;i.closeButton.style.display=C.includes("close")?"block":"none",x?(i.footer.style.display="flex",i.progress.style.display=k?"block":"none",i.nextButton.style.display=C.includes("next")?"block":"none",i.previousButton.style.display=C.includes("previous")?"block":"none"):i.footer.style.display="none";const f=c||e("disableButtons")||[];f!=null&&f.includes("next")&&(i.nextButton.disabled=!0,i.nextButton.classList.add("driver-popover-btn-disabled")),f!=null&&f.includes("previous")&&(i.previousButton.disabled=!0,i.previousButton.classList.add("driver-popover-btn-disabled")),f!=null&&f.includes("close")&&(i.closeButton.disabled=!0,i.closeButton.classList.add("driver-popover-btn-disabled"));const D=i.wrapper;D.style.display="block",D.style.left="",D.style.top="",D.style.bottom="",D.style.right="",D.id="driver-popover-content",D.setAttribute("role","dialog"),D.setAttribute("aria-labelledby","driver-popover-title"),D.setAttribute("aria-describedby","driver-popover-description");const B=i.arrow;B.className="driver-popover-arrow";const $=((n=r.popover)==null?void 0:n.popoverClass)||e("popoverClass")||"";D.className=`driver-popover ${$}`.trim(),ee(i.wrapper,M=>{var W,U,j;const H=M.target,V=((W=r.popover)==null?void 0:W.onNextClick)||e("onNextClick"),q=((U=r.popover)==null?void 0:U.onPrevClick)||e("onPrevClick"),G=((j=r.popover)==null?void 0:j.onCloseClick)||e("onCloseClick");if(H.classList.contains("driver-popover-next-btn"))return V?V(o,r,{config:e(),state:d()}):O("nextClick");if(H.classList.contains("driver-popover-prev-btn"))return q?q(o,r,{config:e(),state:d()}):O("prevClick");if(H.classList.contains("driver-popover-close-btn"))return G?G(o,r,{config:e(),state:d()}):O("closeClick")},M=>!(i!=null&&i.description.contains(M))&&!(i!=null&&i.title.contains(M))&&typeof M.className=="string"&&M.className.includes("driver-popover")),E("popover",i);const _=((s=r.popover)==null?void 0:s.onPopoverRender)||e("onPopoverRender");_&&_(i,{config:e(),state:d()}),ne(o,r),p(D);const I=o.classList.contains("driver-dummy-element"),N=a([D,...I?[]:[o]]);N.length>0&&N[0].focus()}function oe(){const o=d("popover");if(!(o!=null&&o.wrapper))return;const r=o.wrapper.getBoundingClientRect(),n=e("stagePadding")||0,s=e("popoverOffset")||0;return{width:r.width+n+s,height:r.height+n+s,realWidth:r.width,realHeight:r.height}}function re(o,r){const{elementDimensions:n,popoverDimensions:s,popoverPadding:i,popoverArrowDimensions:u}=r;return o==="start"?Math.max(Math.min(n.top-i,window.innerHeight-s.realHeight-u.width),u.width):o==="end"?Math.max(Math.min(n.top-s?.realHeight+n.height+i,window.innerHeight-s?.realHeight-u.width),u.width):o==="center"?Math.max(Math.min(n.top+n.height/2-s?.realHeight/2,window.innerHeight-s?.realHeight-u.width),u.width):0}function ie(o,r){const{elementDimensions:n,popoverDimensions:s,popoverPadding:i,popoverArrowDimensions:u}=r;return o==="start"?Math.max(Math.min(n.left-i,window.innerWidth-s.realWidth-u.width),u.width):o==="end"?Math.max(Math.min(n.left-s?.realWidth+n.width+i,window.innerWidth-s?.realWidth-u.width),u.width):o==="center"?Math.max(Math.min(n.left+n.width/2-s?.realWidth/2,window.innerWidth-s?.realWidth-u.width),u.width):0}function ne(o,r){const n=d("popover");if(!n)return;const{align:s="start",side:i="left"}=r?.popover||{},u=s,b=o.id==="driver-dummy-element"?"over":i,S=e("stagePadding")||0,c=oe(),w=n.arrow.getBoundingClientRect(),l=o.getBoundingClientRect(),h=l.top-c.height;let g=h>=0;const C=window.innerHeight-(l.bottom+c.height);let k=C>=0;const x=l.left-c.width;let f=x>=0;const D=window.innerWidth-(l.right+c.width);let B=D>=0;const $=!g&&!k&&!f&&!B;let _=b;if(b==="top"&&g?B=f=k=!1:b==="bottom"&&k?B=f=g=!1:b==="left"&&f?B=g=k=!1:b==="right"&&B&&(f=g=k=!1),b==="over"){const I=window.innerWidth/2-c.realWidth/2,N=window.innerHeight/2-c.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($){const I=window.innerWidth/2-c?.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(f){const I=Math.min(x,window.innerWidth-c?.realWidth-w.width),N=re(u,{elementDimensions:l,popoverDimensions:c,popoverPadding:S,popoverArrowDimensions:w});n.wrapper.style.left=`${I}px`,n.wrapper.style.top=`${N}px`,n.wrapper.style.bottom="auto",n.wrapper.style.right="auto",_="left"}else if(B){const I=Math.min(D,window.innerWidth-c?.realWidth-w.width),N=re(u,{elementDimensions:l,popoverDimensions:c,popoverPadding:S,popoverArrowDimensions:w});n.wrapper.style.right=`${I}px`,n.wrapper.style.top=`${N}px`,n.wrapper.style.bottom="auto",n.wrapper.style.left="auto",_="right"}else if(g){const I=Math.min(h,window.innerHeight-c.realHeight-w.width);let N=ie(u,{elementDimensions:l,popoverDimensions:c,popoverPadding:S,popoverArrowDimensions:w});n.wrapper.style.top=`${I}px`,n.wrapper.style.left=`${N}px`,n.wrapper.style.bottom="auto",n.wrapper.style.right="auto",_="top"}else if(k){const I=Math.min(C,window.innerHeight-c?.realHeight-w.width);let N=ie(u,{elementDimensions:l,popoverDimensions:c,popoverPadding:S,popoverArrowDimensions:w});n.wrapper.style.left=`${N}px`,n.wrapper.style.bottom=`${I}px`,n.wrapper.style.top="auto",n.wrapper.style.right="auto",_="bottom"}$?n.arrow.classList.add("driver-popover-arrow-none"):be(u,_,o)}function be(o,r,n){const s=d("popover");if(!s)return;const i=n.getBoundingClientRect(),u=oe(),b=s.arrow,S=u.width,c=window.innerWidth,w=i.width,l=i.left,h=u.height,g=window.innerHeight,C=i.top,k=i.height;b.className="driver-popover-arrow";let x=r,f=o;r==="top"?(l+w<=0?(x="right",f="end"):l+w-S<=0&&(x="top",f="start"),l>=c?(x="left",f="end"):l+S>=c&&(x="top",f="end")):r==="bottom"?(l+w<=0?(x="right",f="start"):l+w-S<=0&&(x="bottom",f="start"),l>=c?(x="left",f="start"):l+S>=c&&(x="bottom",f="end")):r==="left"?(C+k<=0?(x="bottom",f="end"):C+k-h<=0&&(x="left",f="start"),C>=g?(x="top",f="end"):C+h>=g&&(x="left",f="end")):r==="right"&&(C+k<=0?(x="bottom",f="start"):C+k-h<=0&&(x="right",f="start"),C>=g?(x="top",f="start"):C+h>=g&&(x="right",f="end")),x?(b.classList.add(`driver-popover-arrow-side-${x}`),b.classList.add(`driver-popover-arrow-align-${f}`)):b.classList.add("driver-popover-arrow-none")}function Te(){const o=document.createElement("div");o.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 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 i=document.createElement("button");i.type="button",i.classList.add("driver-popover-close-btn"),i.setAttribute("aria-label","Close"),i.innerHTML="×";const u=document.createElement("footer");u.classList.add("driver-popover-footer");const b=document.createElement("span");b.classList.add("driver-popover-progress-text"),b.innerText="";const S=document.createElement("span");S.classList.add("driver-popover-navigation-btns");const c=document.createElement("button");c.type="button",c.classList.add("driver-popover-prev-btn"),c.innerHTML="← Previous";const w=document.createElement("button");return w.type="button",w.classList.add("driver-popover-next-btn"),w.innerHTML="Next →",S.appendChild(c),S.appendChild(w),u.appendChild(b),u.appendChild(S),o.appendChild(i),o.appendChild(r),o.appendChild(n),o.appendChild(s),o.appendChild(u),{wrapper:o,arrow:r,title:n,description:s,footer:u,previousButton:c,nextButton:w,closeButton:i,footerButtons:S,progress:b}}function Se(){var o;const r=d("popover");r&&((o=r.wrapper.parentElement)==null||o.removeChild(r.wrapper))}const Ee="";function Ce(o={}){P(o);function r(){e("allowClose")&&w()}function n(){const l=d("activeIndex"),h=e("steps")||[];if(typeof l>"u")return;const g=l+1;h[g]?c(g):w()}function s(){const l=d("activeIndex"),h=e("steps")||[];if(typeof l>"u")return;const g=l-1;h[g]?c(g):w()}function i(l){(e("steps")||[])[l]?c(l):w()}function u(){var l;if(d("__transitionCallback"))return;const h=d("activeIndex"),g=d("__activeStep"),C=d("__activeElement");if(typeof h>"u"||typeof g>"u"||typeof d("activeIndex")>"u")return;const k=((l=g.popover)==null?void 0:l.onPrevClick)||e("onPrevClick");if(k)return k(C,g,{config:e(),state:d()});s()}function b(){var l;if(d("__transitionCallback"))return;const h=d("activeIndex"),g=d("__activeStep"),C=d("__activeElement");if(typeof h>"u"||typeof g>"u")return;const k=((l=g.popover)==null?void 0:l.onNextClick)||e("onNextClick");if(k)return k(C,g,{config:e(),state:d()});n()}function S(){d("isInitialized")||(E("isInitialized",!0),document.body.classList.add("driver-active",e("animate")?"driver-fade":"driver-simple"),fe(),R("overlayClick",r),R("escapePress",r),R("arrowLeftPress",u),R("arrowRightPress",b))}function c(l=0){var h,g,C,k,x,f,D,B;const $=e("steps");if(!$){console.error("No steps to drive through"),w();return}if(!$[l]){w();return}E("__activeOnDestroyed",document.activeElement),E("activeIndex",l);const _=$[l],I=$[l+1],N=$[l-1],M=((h=_.popover)==null?void 0:h.doneBtnText)||e("doneBtnText")||"Done",W=e("allowClose"),U=typeof((g=_.popover)==null?void 0:g.showProgress)<"u"?(C=_.popover)==null?void 0:C.showProgress:e("showProgress"),j=(((k=_.popover)==null?void 0:k.progressText)||e("progressText")||"{{current}} of {{total}}").replace("{{current}}",`${l+1}`).replace("{{total}}",`${$.length}`),H=((x=_.popover)==null?void 0:x.showButtons)||e("showButtons"),V=["next","previous",...W?["close"]:[]].filter(ke=>!(H!=null&&H.length)||H.includes(ke)),q=((f=_.popover)==null?void 0:f.onNextClick)||e("onNextClick"),G=((D=_.popover)==null?void 0:D.onPrevClick)||e("onPrevClick"),xe=((B=_.popover)==null?void 0:B.onCloseClick)||e("onCloseClick");X({..._,popover:{showButtons:V,nextBtnText:I?void 0:M,disableButtons:[...N?[]:["previous"]],showProgress:U,progressText:j,onNextClick:q||(()=>{I?c(l+1):w()}),onPrevClick:G||(()=>{c(l-1)}),onCloseClick:xe||(()=>{w()}),..._?.popover||{}}})}function w(l=!0){const h=d("__activeElement"),g=d("__activeStep"),C=d("__activeOnDestroyed"),k=e("onDestroyStarted");if(l&&k){const D=!h||h?.id==="driver-dummy-element";k(D?void 0:h,g,{config:e(),state:d()});return}const x=g?.onDeselected||e("onDeselected"),f=e("onDestroyed");if(document.body.classList.remove("driver-active","driver-fade","driver-simple"),ye(),Se(),ge(),pe(),se(),K(),h&&g){const D=h.id==="driver-dummy-element";x&&x(D?void 0:h,g,{config:e(),state:d()}),f&&f(D?void 0:h,g,{config:e(),state:d()})}C&&C.focus()}return{isActive:()=>d("isInitialized")||!1,refresh:z,drive:(l=0)=>{S(),c(l)},setConfig:P,setSteps:l=>{K(),P({...e(),steps:l})},getConfig:e,getState:d,getActiveIndex:()=>d("activeIndex"),isFirstStep:()=>d("activeIndex")===0,isLastStep:()=>{const l=e("steps")||[],h=d("activeIndex");return h!==void 0&&h===l.length-1},getActiveStep:()=>d("activeStep"),getActiveElement:()=>d("activeElement"),getPreviousElement:()=>d("previousElement"),getPreviousStep:()=>d("previousStep"),moveNext:n,movePrevious:s,moveTo:i,hasNextStep:()=>{const l=e("steps")||[],h=d("activeIndex");return h!==void 0&&l[h+1]},hasPreviousStep:()=>{const l=e("steps")||[],h=d("activeIndex");return h!==void 0&&l[h-1]},highlight:l=>{S(),X({...l,popover:l.popover?{showButtons:[],showProgress:!1,progressText:"",...l.popover}:void 0})},destroy:()=>{w(!1)}}}return v.driver=Ce,Object.defineProperty(v,Symbol.toStringTag,{value:"Module"}),v})({}),(function(v){"use strict";class T{constructor(){this.errors=[],this.maxErrors=50}logError(e,t,a={}){const p={timestamp:new Date().toISOString(),context:e,message:t instanceof Error?t.message:t,stack:t instanceof Error?t.stack:null,metadata:a};this.errors.push(p),this.errors.length>this.maxErrors&&this.errors.shift(),console.error(`[Onboarding Error] ${e}:`,t,a)}recoverFromError(e,t){switch(this.classifyError(e)){case"ELEMENT_NOT_FOUND":return this.logError("Element Not Found",e,{currentStep:t}),{action:"SKIP_STEP",nextStep:t+1,message:"Target element not found, skipping to next step"};case"STORAGE_UNAVAILABLE":return this.logError("Storage Unavailable",e),{action:"USE_MEMORY_STORAGE",message:"Local storage unavailable, using in-memory storage"};case"DRIVER_NOT_LOADED":return this.logError("Driver.js Not Loaded",e),{action:"ABORT_TOUR",message:"Driver.js library not loaded, cannot start tour"};case"INVALID_TOOLTIP":return this.logError("Invalid Tooltip Configuration",e,{currentStep:t}),{action:"SKIP_STEP",nextStep:t+1,message:"Invalid tooltip configuration, skipping"};case"THEME_DETECTION_FAILED":return this.logError("Theme Detection Failed",e),{action:"USE_DEFAULT_THEME",message:"Using default dark theme"};default:return this.logError("Unknown Error",e,{currentStep:t}),{action:"ABORT_TOUR",message:"Unexpected error occurred, aborting tour"}}}classifyError(e){const t=e.message||e.toString();return t.includes("element")&&t.includes("not found")?"ELEMENT_NOT_FOUND":t.includes("storage")||t.includes("quota")?"STORAGE_UNAVAILABLE":t.includes("driver")||t.includes("undefined")?"DRIVER_NOT_LOADED":t.includes("invalid")||t.includes("validation")?"INVALID_TOOLTIP":t.includes("theme")?"THEME_DETECTION_FAILED":"UNKNOWN"}getErrors(){return[...this.errors]}clearErrors(){this.errors=[]}getStatistics(){const e={total:this.errors.length,byContext:{},byType:{},recent:this.errors.slice(-10)};return this.errors.forEach(t=>{e.byContext[t.context]=(e.byContext[t.context]||0)+1;const a=this.classifyError({message:t.message});e.byType[a]=(e.byType[a]||0)+1}),e}handleDriverLoadFailure(){this.logError("Driver.js Load Failure","Driver.js library failed to load");const e=document.createElement("div");return e.id="onboarding-fallback",e.style.cssText=` +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; @@ -11,38 +11,38 @@ this.driver=this.driver||{},this.driver.js=(function(v){"use strict";let T={};fu z-index: 9999; max-width: 300px; font-size: 14px; - `,e.innerHTML=` + `,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(e),setTimeout(()=>{e.parentNode&&e.parentNode.removeChild(e)},1e4),!0}handleStorageUnavailable(){this.logError("Storage Unavailable","Local storage is not available");const e={data:{},getItem(t){return this.data[t]||null},setItem(t,a){this.data[t]=a},removeItem(t){delete this.data[t]},clear(){this.data={}}};return console.warn("[ErrorHandler] Using in-memory storage - progress will not persist"),e}sendToErrorTracking(e){}}v.ErrorHandler=T,console.log("[ErrorHandler] Module loaded")})(window),(function(v){"use strict";class T{constructor(e="dashcaddy_onboarding"){this.storageKey=e,this.storageVersion="1.0",this.installOnboardingCompleted=typeof SITE<"u"&&SITE.onboardingCompleted===!0,this._initializeStorage(),this._updateLastVisit()}_initializeStorage(){const e=this._getStorage();if(!e||e.version!==this.storageVersion){const t={version:this.storageVersion,tourCompleted:!1,completedTooltips:[],currentStep:0,completionTimestamp:null,dnsSetupDeferred:!1,lastVisit:new Date().toISOString()};this._setStorage(t)}}_getStorage(){try{const e=localStorage.getItem(this.storageKey);return e?JSON.parse(e):null}catch(e){return console.error("[ProgressTracker] Error reading from storage:",e),null}}_setStorage(e){try{localStorage.setItem(this.storageKey,JSON.stringify(e))}catch(t){console.error("[ProgressTracker] Error writing to storage:",t),this._handleStorageError(t)}}_handleStorageError(e){try{sessionStorage.setItem(this.storageKey,JSON.stringify(this._getStorage())),console.warn("[ProgressTracker] Falling back to session storage")}catch(t){console.error("[ProgressTracker] Session storage also unavailable:",t)}}_updateLastVisit(){const e=this._getStorage();e&&(e.lastVisit=new Date().toISOString(),this._setStorage(e))}isTooltipCompleted(e){const t=this._getStorage();return t?t.completedTooltips.includes(e):!1}markTooltipCompleted(e){const t=this._getStorage();t&&(t.completedTooltips.includes(e)||(t.completedTooltips.push(e),t.tooltipTimestamps||(t.tooltipTimestamps={}),t.tooltipTimestamps[e]=new Date().toISOString(),this._setStorage(t)))}isTourCompleted(){const e=this._getStorage();return e?e.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(e){console.error("[ProgressTracker] Failed to persist install onboarding state:",e)}}}markTourCompleted(){const e=this._getStorage();e&&(e.tourCompleted=!0,e.completionTimestamp=new Date().toISOString(),this._setStorage(e))}getCurrentStep(){const e=this._getStorage();return e&&e.currentStep||0}setCurrentStep(e){const t=this._getStorage();t&&(t.currentStep=e,this._setStorage(t))}resetProgress(){const e={version:this.storageVersion,tourCompleted:!1,completedTooltips:[],currentStep:0,completionTimestamp:null,dnsSetupDeferred:!1,lastVisit:new Date().toISOString()};this._setStorage(e)}getCompletionTimestamp(){const e=this._getStorage();return!e||!e.completionTimestamp?null:new Date(e.completionTimestamp)}isDnsSetupDeferred(){const e=this._getStorage();return e?e.dnsSetupDeferred===!0:!1}markDnsSetupDeferred(){const e=this._getStorage();e&&(e.dnsSetupDeferred=!0,this._setStorage(e))}getTooltipTimestamp(e){const t=this._getStorage();return!t||!t.tooltipTimestamps||!t.tooltipTimestamps[e]?null:new Date(t.tooltipTimestamps[e])}getCompletedTooltips(){const e=this._getStorage();return e?e.completedTooltips||[]:[]}getLastVisit(){const e=this._getStorage();return!e||!e.lastVisit?null:new Date(e.lastVisit)}}v.ProgressTracker=T,console.log("[ProgressTracker] Module loaded")})(window),(function(v){"use strict";const T={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 P{constructor(){this.currentTheme=this.getCurrentTheme(),this.themeChangeCallbacks=[],this._setupThemeChangeListener()}getCurrentTheme(){const t=document.documentElement,a=Array.from(t.classList);return(v.THEMES||[]).filter(m=>m!=="dark").find(m=>a.includes(m))||"dark"}getDriverTheme(){const t=this.getCurrentTheme(),a=T[t]||T.dark,p={};for(const[y,m]of Object.entries(a))if(typeof m=="string"&&m.startsWith("var(")){const A=m.match(/var\((--[^)]+)\)/)?.[1];if(A){const L=getComputedStyle(document.documentElement).getPropertyValue(A).trim();p[y]=L||m}else p[y]=m}else p[y]=m;return p}onThemeChange(t){typeof t=="function"&&this.themeChangeCallbacks.push(t)}_setupThemeChangeListener(){const t=document.documentElement;new MutationObserver(p=>{p.forEach(y=>{if(y.type==="attributes"&&y.attributeName==="class"){const m=this.getCurrentTheme();if(m!==this.currentTheme){const A=this.currentTheme;this.currentTheme=m,this._notifyThemeChange(m,A)}}})}).observe(t,{attributes:!0,attributeFilter:["class"]}),console.log("[ThemeAdapter] Theme change listener initialized")}_notifyThemeChange(t,a){console.log(`[ThemeAdapter] Theme changed: ${a} \u2192 ${t}`),this.themeChangeCallbacks.forEach(p=>{try{p(t,a)}catch(y){console.error("[ThemeAdapter] Error in theme change callback:",y)}})}applyTheme(t){if(!t){console.warn("[ThemeAdapter] No driver instance provided");return}const a=this.getDriverTheme();this._injectDriverStyles(a),console.log("[ThemeAdapter] Theme applied to driver:",this.currentTheme)}_injectDriverStyles(t){const a=document.getElementById("driver-theme-styles");a&&a.remove();const p=document.createElement("style");p.id="driver-theme-styles",p.textContent=` + `,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=` .driver-popover { - background: ${t.backgroundColor} !important; - color: ${t.textColor} !important; - border: 1px solid ${t.borderColor} !important; + background: ${a.backgroundColor} !important; + color: ${a.textColor} !important; + border: 1px solid ${a.borderColor} !important; border-radius: 12px !important; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4) !important; - font-family: ${t.fontFamily} !important; + font-family: ${a.fontFamily} !important; } .driver-popover-title { - color: ${t.textColor} !important; + color: ${a.textColor} !important; font-weight: 600 !important; - font-family: ${t.fontFamily} !important; + font-family: ${a.fontFamily} !important; } .driver-popover-description { - color: ${t.textColor} !important; - font-family: ${t.fontFamily} !important; + color: ${a.textColor} !important; + font-family: ${a.fontFamily} !important; } .driver-popover-footer button { - background: ${t.primaryColor} !important; - color: ${t.backgroundColor} !important; + background: ${a.primaryColor} !important; + color: ${a.backgroundColor} !important; border: none !important; - font-family: ${t.fontFamily} !important; + font-family: ${a.fontFamily} !important; font-weight: 500 !important; } @@ -51,26 +51,26 @@ this.driver=this.driver||{},this.driver.js=(function(v){"use strict";let T={};fu } .driver-popover-close-btn { - color: ${t.textColor} !important; + color: ${a.textColor} !important; } .driver-overlay { - background: ${t.overlayColor} !important; + background: ${a.overlayColor} !important; } .driver-highlighted-element { - outline: 2px solid ${t.highlightColor} !important; + outline: 2px solid ${a.highlightColor} !important; outline-offset: 4px !important; } .driver-popover-progress-text { - color: ${t.textColor} !important; + color: ${a.textColor} !important; opacity: 0.7 !important; - font-family: ${t.fontFamily} !important; + font-family: ${a.fontFamily} !important; } - `,document.head.appendChild(p)}getAvailableThemes(){return Object.keys(T)}isThemeAvailable(t){return T.hasOwnProperty(t)}}v.ThemeAdapter=P,console.log("[ThemeAdapter] Module loaded")})(window),(function(v){"use strict";function T(a){const p=[];if((!a.id||typeof a.id!="string")&&p.push("Tooltip must have a valid string id"),a.element||p.push("Tooltip must have an element selector or HTMLElement"),!a.popover||typeof a.popover!="object")p.push("Tooltip must have a popover object");else{if((!a.popover.title||typeof a.popover.title!="string")&&p.push("Tooltip popover must have a valid string title"),(!a.popover.description||typeof a.popover.description!="string")&&p.push("Tooltip popover must have a valid string description"),a.popover.position){const m=["top","bottom","left","right","center"];m.includes(a.popover.position)||p.push(`Invalid position: ${a.popover.position}. Must be one of: ${m.join(", ")}`)}if(a.popover.align){const m=["start","center","end"];m.includes(a.popover.align)||p.push(`Invalid align: ${a.popover.align}. Must be one of: ${m.join(", ")}`)}a.popover.showButtons&&!Array.isArray(a.popover.showButtons)&&p.push("showButtons must be an array"),["onNext","onPrevious","onClose","onSetupNow","onLater"].forEach(m=>{a.popover[m]&&typeof a.popover[m]!="function"&&p.push(`${m} must be a function`)})}return a.condition&&typeof a.condition!="function"&&p.push("condition must be a function"),a.priority!==void 0&&typeof a.priority!="number"&&p.push("priority must be a number"),{valid:p.length===0,errors:p}}function P(a){if(!Array.isArray(a))return{valid:!1,errors:[{tooltip:null,errors:["tooltips must be an array"]}]};const p=[],y=new Set;return a.forEach((m,A)=>{const L=T(m);L.valid||p.push({tooltip:m.id||`index ${A}`,errors:L.errors}),m.id&&(y.has(m.id)&&p.push({tooltip:m.id,errors:[`Duplicate tooltip ID: ${m.id}`]}),y.add(m.id))}),{valid:p.length===0,errors:p}}class e extends Error{constructor(p,y=null){super(p),this.name="TooltipError",this.tooltipId=y}}function t(a){if(!a.valid){const p=a.errors.map(y=>`${y.tooltip}: ${y.errors.join(", ")}`).join(` -`);throw console.error("[TooltipDefinitions] Validation errors:",p),new e(`Tooltip validation failed: -${p}`)}}v.TooltipValidation={validateTooltipDefinition:T,validateTooltipDefinitions:P,handleValidationErrors:t,TooltipError:e},console.log("[TooltipDefinitions] Validation module loaded")})(window);const TOOLTIP_DEFINITIONS=[{id:"welcome",element:"#brand",popover:{title:"Welcome to DashCaddy!",description:` + `,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:`

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 +172,7 @@ ${p}`)}}v.TooltipValidation={validateTooltipDefinition:T,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(T=>T.id===v)||null}function getActiveTooltips(){return TOOLTIP_DEFINITIONS.filter(v=>{if(v.condition&&typeof v.condition=="function")try{return v.condition()}catch(T){return console.error(`[TooltipDefinitions] Error evaluating condition for ${v.id}:`,T),!1}return!0})}function getSortedTooltips(){return getActiveTooltips().sort((T,P)=>{const e=T.priority||999,t=P.priority||999;return e-t})}function getNewFeatureTooltips(){return getActiveTooltips().filter(T=>T.isNewFeature===!0).sort((T,P)=>{const e=T.priority||999,t=P.priority||999;return e-t})}window.TooltipDefinitions={TOOLTIP_DEFINITIONS,getTooltipDefinitions,getTooltipById,getActiveTooltips,getSortedTooltips,getNewFeatureTooltips},console.log("[TooltipDefinitions] Definitions loaded:",TOOLTIP_DEFINITIONS.length,"tooltips"),(function(v){"use strict";class T{constructor(e){this.progressTracker=e,this.modal=null,this.onTemplateSelected=null,console.log("[DnsTemplateSelector] Module loaded")}getDnsTemplates(){return[{id:"technitium",name:"Technitium DNS Server",description:"Modern DNS server with web UI for managing private zones",icon:"\u{1F310}",difficulty:"Easy",features:["Web-based management interface","Private zone management for .sami domain","DHCP server integration","DNS-over-HTTPS and DNS-over-TLS support"],recommended:!0},{id:"bind9",name:"BIND9 DNS Server",description:"Industry-standard DNS server - powerful and flexible",icon:"\u{1F527}",difficulty:"Advanced",features:["Industry standard DNS server","Full RFC compliance","Advanced zone management","DNSSEC support"],recommended:!1},{id:"pihole",name:"Pi-hole",description:"Network-wide ad blocker with DNS capabilities",icon:"\u{1F6E1}\uFE0F",difficulty:"Intermediate",features:["Ad blocking at DNS level","Web interface for management","DHCP server included","Query logging and statistics"],recommended:!1},{id:"powerdns",name:"PowerDNS",description:"High-performance DNS server with SQL backend",icon:"\u26A1",difficulty:"Intermediate",features:["SQL database backend","RESTful API for automation","Geographic load balancing","DNSSEC support"],recommended:!1},{id:"coredns",name:"CoreDNS",description:"Cloud-native DNS server - lightweight and flexible",icon:"\u2601\uFE0F",difficulty:"Intermediate",features:["Plugin-based architecture","Kubernetes-native","Lightweight and fast","Prometheus metrics"],recommended:!1}]}showTemplateSelector(){this.modal||this.createModal(),this.populateTemplates(),this.modal.style.display="flex",document.body.style.overflow="hidden"}createModal(){const e=document.createElement("div");e.id="dns-template-modal",e.className="dns-template-modal",e.innerHTML=` + `,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=`

\u{1F310} Choose a DNS Server

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

${e.name}

-

${e.description}

+ `,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}

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