- Disk-space tip:
- Health-check monitoring records uptime, response-time, and incident data continuously.
- By default DashCaddy keeps 30 days of history and warns at 80% disk usage.
- You can tune the retention period, polling interval, and disk threshold later under
- Health \u2192 Configure \u2192 Global Settings.
-
-
-
- `,D+="
",g.innerHTML=D,M("setup-step-summary")}async function P(g){try{const D=await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(g)});return D.ok?(await D.json(),!0):(errorHandler.logError("[SetupWizard] Save Config",new Error(`Server returned ${D.status}`),{function:"saveConfigToServer"}),!1)}catch(D){return errorHandler.logError("[SetupWizard] Save Config",D,{function:"saveConfigToServer"}),!1}}async function A(){const g={setupComplete:!0,configurationType:y,timestamp:new Date().toISOString(),timezone:document.getElementById("setup-timezone")?.value||Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC"};if(y==="homelab"){g.tld=document.getElementById("setup-tld")?.value?.trim()||".home",g.caName=document.getElementById("setup-ca-name")?.value?.trim()||"";const I=document.getElementById("setup-dns-provider")?.value||"technitium";g.dns={provider:I,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()||""},g.defaults={dnsType:"private",sslType:"internal",targetIP:"localhost"}}else y==="simple"?(g.defaultIP=document.getElementById("setup-simple-ip")?.value?.trim()||"localhost",g.defaults={dnsType:"none",sslType:"none",targetIP:g.defaultIP}):y==="public"&&(g.domain=document.getElementById("setup-public-domain")?.value?.trim()||"",g.email=document.getElementById("setup-public-email")?.value?.trim()||"",g.routingMode=document.querySelector('input[name="routing-mode"]:checked')?.value||"subdirectory",g.defaults={dnsType:g.routingMode==="subdirectory"?"none":"public",sslType:"letsencrypt",targetIP:"localhost"});const D=await P(g);safeSet("dashcaddy-config",JSON.stringify(g)),safeSet("dashcaddy-setup","completed"),document.getElementById("setup-wizard").style.display="none";const B=y==="homelab"?"Professional Home Lab":y==="simple"?"Simple Setup":"Public Server",f=D?"server (shared across all devices)":"locally (this browser only)";showNotification(`Setup Complete! Configured for: ${B}. Settings saved to: ${f}`,"success",5e3),setTimeout(()=>location.reload(),500)}const p=document.getElementById("setup-step-1-next");p&&(p.onclick=function(g){g.preventDefault();const D=document.querySelector('input[name="config-type"]:checked');D&&(y=D.value),M(y==="homelab"?"setup-step-homelab":y==="simple"?"setup-step-simple":y==="public"?"setup-step-public":"setup-step-homelab")});const T=document.getElementById("setup-skip");T&&(T.onclick=async function(g){g.preventDefault(),confirm("Skip setup? You can run it later from Settings.")&&(await P({setupComplete:!0,skipped:!0,timestamp:new Date().toISOString()}),safeSet("dashcaddy-setup","skipped"),document.getElementById("setup-wizard").style.display="none")});const x=document.getElementById("setup-tld");x&&(x.oninput=function(g){const D=g.target.value||".home",B=document.getElementById("tld-preview"),f=document.getElementById("tld-preview-2");B&&(B.textContent=D),f&&(f.textContent=D)});const $=document.getElementById("setup-homelab-back");$&&($.onclick=function(g){g.preventDefault(),M("setup-step-1")});const k=document.getElementById("setup-homelab-next");k&&(k.onclick=function(g){g.preventDefault();const D=document.getElementById("setup-tld")?.value?.trim()||"",B=document.getElementById("setup-ca-name")?.value?.trim()||"",f=document.getElementById("setup-dns-ip")?.value?.trim()||"";if(!D||!D.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(!f){showNotification("Please enter your DNS server IP address","warning");return}R()});const H=document.getElementById("setup-simple-back");H&&(H.onclick=function(g){g.preventDefault(),M("setup-step-1")});const L=document.getElementById("setup-simple-next");L&&(L.onclick=function(g){g.preventDefault(),R()}),document.querySelectorAll('input[name="routing-mode"]').forEach(function(g){g.onchange=function(){var D=document.getElementById("dns-requirement-note");D&&(D.textContent=this.value==="subdirectory"?"Only one DNS record needed (for the main domain)":"You'll need to configure DNS manually for each subdomain")}});const u=document.getElementById("setup-public-back");u&&(u.onclick=function(g){g.preventDefault(),M("setup-step-1")});const j=document.getElementById("setup-public-next");j&&(j.onclick=function(g){g.preventDefault();const D=document.getElementById("setup-public-domain")?.value?.trim()||"",B=document.getElementById("setup-public-email")?.value?.trim()||"";if(!D){showNotification("Please enter your domain name","warning");return}if(!B||!B.includes("@")){showNotification("Please enter a valid email address","warning");return}R()});const v=document.getElementById("setup-summary-back");v&&(v.onclick=function(g){g.preventDefault(),y==="homelab"?M("setup-step-homelab"):y==="simple"?M("setup-step-simple"):y==="public"&&M("setup-step-public")});const N=document.getElementById("setup-finish");N&&(N.onclick=function(g){g.preventDefault(),A()}),window.getGlobalConfig=async function(){try{const D=await fetch("/api/v1/config");if(D.ok){const B=await D.json();if(B&&B.setupComplete)return B}}catch{console.warn("Could not fetch config from server")}const g=safeGet("dashcaddy-config");return g?JSON.parse(g):{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 y=new ErrorHandler;injectModal("app-selector-modal",`
+ `,u+="
",p.innerHTML=u,$("setup-step-summary")}async function N(p){try{const u=await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(p)});return u.ok?(await u.json(),!0):(errorHandler.logError("[SetupWizard] Save Config",new Error(`Server returned ${u.status}`),{function:"saveConfigToServer"}),!1)}catch(u){return errorHandler.logError("[SetupWizard] Save Config",u,{function:"saveConfigToServer"}),!1}}async function D(){const p={setupComplete:!0,configurationType:h,timestamp:new Date().toISOString(),timezone:document.getElementById("setup-timezone")?.value||Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC"};if(h==="homelab"){p.tld=document.getElementById("setup-tld")?.value?.trim()||".home",p.caName=document.getElementById("setup-ca-name")?.value?.trim()||"";const M=document.getElementById("setup-dns-provider")?.value||"technitium";p.dns={provider:M,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()||""},p.defaults={dnsType:"private",sslType:"internal",targetIP:"localhost"}}else h==="simple"?(p.defaultIP=document.getElementById("setup-simple-ip")?.value?.trim()||"localhost",p.defaults={dnsType:"none",sslType:"none",targetIP:p.defaultIP}):h==="public"&&(p.domain=document.getElementById("setup-public-domain")?.value?.trim()||"",p.email=document.getElementById("setup-public-email")?.value?.trim()||"",p.routingMode=document.querySelector('input[name="routing-mode"]:checked')?.value||"subdirectory",p.defaults={dnsType:p.routingMode==="subdirectory"?"none":"public",sslType:"letsencrypt",targetIP:"localhost"});const u=await N(p);safeSet("dashcaddy-config",JSON.stringify(p)),safeSet("dashcaddy-setup","completed"),document.getElementById("setup-wizard").style.display="none";const b=h==="homelab"?"Professional Home Lab":h==="simple"?"Simple Setup":"Public Server",g=u?"server (shared across all devices)":"locally (this browser only)";showNotification(`Setup Complete! Configured for: ${b}. Settings saved to: ${g}`,"success",5e3),setTimeout(()=>location.reload(),500)}const m=document.getElementById("setup-step-1-next");m&&(m.onclick=function(p){p.preventDefault();const u=document.querySelector('input[name="config-type"]:checked');u&&(h=u.value),$(h==="homelab"?"setup-step-homelab":h==="simple"?"setup-step-simple":h==="public"?"setup-step-public":"setup-step-homelab")});const T=document.getElementById("setup-skip");T&&(T.onclick=async function(p){p.preventDefault(),confirm("Skip setup? You can run it later from Settings.")&&(await N({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(p){const u=p.target.value||".home",b=document.getElementById("tld-preview"),g=document.getElementById("tld-preview-2");b&&(b.textContent=u),g&&(g.textContent=u)});const B=document.getElementById("setup-homelab-back");B&&(B.onclick=function(p){p.preventDefault(),$("setup-step-1")});const E=document.getElementById("setup-homelab-next");E&&(E.onclick=function(p){p.preventDefault();const u=document.getElementById("setup-tld")?.value?.trim()||"",b=document.getElementById("setup-ca-name")?.value?.trim()||"",g=document.getElementById("setup-dns-ip")?.value?.trim()||"";if(!u||!u.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(!g){showNotification("Please enter your DNS server IP address","warning");return}P()});const H=document.getElementById("setup-simple-back");H&&(H.onclick=function(p){p.preventDefault(),$("setup-step-1")});const C=document.getElementById("setup-simple-next");C&&(C.onclick=function(p){p.preventDefault(),P()}),document.querySelectorAll('input[name="routing-mode"]').forEach(function(p){p.onchange=function(){var u=document.getElementById("dns-requirement-note");u&&(u.textContent=this.value==="subdirectory"?"Only one DNS record needed (for the main domain)":"You'll need to configure DNS manually for each subdomain")}});const v=document.getElementById("setup-public-back");v&&(v.onclick=function(p){p.preventDefault(),$("setup-step-1")});const O=document.getElementById("setup-public-next");O&&(O.onclick=function(p){p.preventDefault();const u=document.getElementById("setup-public-domain")?.value?.trim()||"",b=document.getElementById("setup-public-email")?.value?.trim()||"";if(!u){showNotification("Please enter your domain name","warning");return}if(!b||!b.includes("@")){showNotification("Please enter a valid email address","warning");return}P()});const f=document.getElementById("setup-summary-back");f&&(f.onclick=function(p){p.preventDefault(),h==="homelab"?$("setup-step-homelab"):h==="simple"?$("setup-step-simple"):h==="public"&&$("setup-step-public")});const z=document.getElementById("setup-summary-next");z&&(z.onclick=function(p){p.preventDefault(),$("setup-step-disk-safety")});const A=document.getElementById("setup-disk-safety-back");A&&(A.onclick=function(p){p.preventDefault(),$("setup-step-summary")});const U=document.getElementById("setup-disk-safety-finish");U&&(U.onclick=function(p){p.preventDefault(),D()}),window.getGlobalConfig=async function(){try{const u=await fetch("/api/v1/config");if(u.ok){const b=await u.json();if(b&&b.setupComplete)return b}}catch{console.warn("Could not fetch config from server")}const p=safeGet("dashcaddy-config");return p?JSON.parse(p):{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 h=new ErrorHandler;injectModal("app-selector-modal",`
Choose an App
@@ -346,12 +333,12 @@ This will reset the logo, favicon, title, and position.`))try{if((await secureFe
-
`);const C="custom-apps";let O=null,E=null;const M=document.getElementById("app-selector-modal"),R=document.getElementById("app-selector-grid");async function P(){try{const c=await(await fetch("/api/v1/apps/templates")).json();if(c.success)return O=c.templates,E=c.categories,!0}catch(i){y.logError("[AppSelector] Fetch Templates",i,{function:"fetchApiTemplates"})}return!1}async function A(i){try{return await(await fetch(`/api/v1/apps/ports/${i}/check`)).json()}catch(c){return y.logError("[AppSelector] Check Port",c,{function:"checkPortAvailability"}),{available:!0}}}async function p(i){try{const l=await(await fetch(`/api/v1/apps/ports/${i}/suggest`)).json();if(l.success)return l.suggestedPort}catch(c){y.logError("[AppSelector] Get Suggested Port",c,{function:"getSuggestedPort"})}return i}async function T(){if(R.innerHTML='
Loading app templates...
',!O&&!await P()){R.innerHTML='
Failed to load app templates. Please try again.
';return}R.innerHTML="";const i={};for(const[l,w]of Object.entries(O)){const m=w.category||"Other";i[m]||(i[m]=[]),i[m].push({id:l,...w})}const c=E?Object.keys(E):Object.keys(i).sort();for(const l of c){const w=i[l];if(!w||w.length===0)continue;w.sort((e,t)=>(t.popularity||0)-(e.popularity||0));const m=document.createElement("div");m.className="app-category-header";const n=E?.[l]||{};m.innerHTML=`${escapeHtml(n.icon||"")} ${escapeHtml(l)}`,n.color&&(m.style.borderBottomColor=n.color),R.appendChild(m),w.forEach(e=>{const t=document.createElement("div");t.className="app-option";const o=e.isDashboardWidget,r=o&&safeGet("widget-"+e.id+"-enabled")!=="false",a=o?`
${r?"ON":"OFF"}
`:"",s=!o&&e.difficulty?`
${escapeHtml(e.difficulty)}
`:"";t.innerHTML=`
+ `);const L="custom-apps";let R=null,S=null;const $=document.getElementById("app-selector-modal"),P=document.getElementById("app-selector-grid");async function N(){try{const c=await(await fetch("/api/v1/apps/templates")).json();if(c.success)return R=c.templates,S=c.categories,!0}catch(r){h.logError("[AppSelector] Fetch Templates",r,{function:"fetchApiTemplates"})}return!1}async function D(r){try{return await(await fetch(`/api/v1/apps/ports/${r}/check`)).json()}catch(c){return h.logError("[AppSelector] Check Port",c,{function:"checkPortAvailability"}),{available:!0}}}async function m(r){try{const d=await(await fetch(`/api/v1/apps/ports/${r}/suggest`)).json();if(d.success)return d.suggestedPort}catch(c){h.logError("[AppSelector] Get Suggested Port",c,{function:"getSuggestedPort"})}return r}async function T(){if(P.innerHTML='
Loading app templates...
',!R&&!await N()){P.innerHTML='
Failed to load app templates. Please try again.
';return}P.innerHTML="";const r={};for(const[d,x]of Object.entries(R)){const y=x.category||"Other";r[y]||(r[y]=[]),r[y].push({id:d,...x})}const c=S?Object.keys(S):Object.keys(r).sort();for(const d of c){const x=r[d];if(!x||x.length===0)continue;x.sort((e,s)=>(s.popularity||0)-(e.popularity||0));const y=document.createElement("div");y.className="app-category-header";const t=S?.[d]||{};y.innerHTML=`${escapeHtml(t.icon||"")} ${escapeHtml(d)}`,t.color&&(y.style.borderBottomColor=t.color),P.appendChild(y),x.forEach(e=>{const s=document.createElement("div");s.className="app-option";const n=e.isDashboardWidget,i=n&&safeGet("widget-"+e.id+"-enabled")!=="false",a=n?`
${i?"ON":"OFF"}
`:"",o=!n&&e.difficulty?`
${escapeHtml(e.difficulty)}
`:"";s.innerHTML=`
${escapeHtml(e.icon||"\u{1F4E6}")}
${escapeHtml(e.name)}
${escapeHtml(e.description||"")}
- ${a}${s}
- `,o?t.onclick=()=>x(e,t):t.onclick=()=>$(e),R.appendChild(t)})}window.renderRecipeCards&&await window.renderRecipeCards(R)}function x(i,c){const l="widget-"+i.id+"-enabled",m=!(safeGet(l)!=="false");safeSet(l,String(m));const n=i.widgetSelector;if(n){const t=document.querySelector(n);t&&(t.style.display=m?"":"none")}const e=c.querySelector('div[style*="border-radius: 4px"]');e&&(e.textContent=m?"ON":"OFF",e.style.background=m?"#2ecc7130":"#e74c3c30",e.style.color=m?"#2ecc71":"#e74c3c"),showNotification(`${i.name} widget ${m?"enabled":"disabled"}`,"success",2e3)}async function $(i){const c=document.getElementById("app-deploy-modal"),l=document.getElementById("app-deploy-title"),w=document.getElementById("deploy-subdomain"),m=document.getElementById("deploy-url-preview"),n=document.getElementById("deploy-ip"),e=document.getElementById("deploy-port"),t=document.getElementById("deploy-tailscale-only"),o=document.getElementById("tailscale-status");try{const G=await(await secureFetch("/api/v1/apps/check-existing",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({appId:i.id})})).json();if(G.success&&G.exists){const V=G.container;confirm(`Found existing ${i.name} container:
+ ${a}${o}
+ `,n?s.onclick=()=>w(e,s):s.onclick=()=>B(e),P.appendChild(s)})}window.renderRecipeCards&&await window.renderRecipeCards(P)}function w(r,c){const d="widget-"+r.id+"-enabled",y=!(safeGet(d)!=="false");safeSet(d,String(y));const t=r.widgetSelector;if(t){const s=document.querySelector(t);s&&(s.style.display=y?"":"none")}const e=c.querySelector('div[style*="border-radius: 4px"]');e&&(e.textContent=y?"ON":"OFF",e.style.background=y?"#2ecc7130":"#e74c3c30",e.style.color=y?"#2ecc71":"#e74c3c"),showNotification(`${r.name} widget ${y?"enabled":"disabled"}`,"success",2e3)}async function B(r){const c=document.getElementById("app-deploy-modal"),d=document.getElementById("app-deploy-title"),x=document.getElementById("deploy-subdomain"),y=document.getElementById("deploy-url-preview"),t=document.getElementById("deploy-ip"),e=document.getElementById("deploy-port"),s=document.getElementById("deploy-tailscale-only"),n=document.getElementById("tailscale-status");try{const W=await(await secureFetch("/api/v1/apps/check-existing",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({appId:r.id})})).json();if(W.success&&W.exists){const V=W.container;confirm(`Found existing ${r.name} container:
Container: ${V.name}
Status: ${V.status}
@@ -360,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.`)&&(i._useExisting=!0,i._existingContainer=V)}}catch{}l.textContent=`Deploy ${i.name}`;const r=i.subdomain||i.id.replace(/-/g,"");w.value=r;const a=document.getElementById("subpath-compat-warning");if(a)if(SITE.routingMode==="subdirectory"){const q=i.subpathSupport||"strip";q==="none"?(a.style.display="block",a.innerHTML='⚠ '+i.name+" does not support subdirectory mode. It may not work correctly at a subpath."):q==="strip"?(a.style.display="block",a.innerHTML='ⓘ '+i.name+" has unverified subdirectory support. It may require additional configuration."):a.style.display="none"}else a.style.display="none";const s=SITE.defaults.dnsType||(SITE.configurationType==="public"?"public":"private"),d=SITE.defaults.sslType||(SITE.configurationType==="public"?"letsencrypt":"internal"),S=document.querySelector(`input[name="dns-type"][value="${s}"]`),h=document.querySelector(`input[name="ssl-type"][value="${d}"]`);S?S.checked=!0:document.querySelector('input[name="dns-type"][value="private"]').checked=!0,h?h.checked=!0:document.querySelector('input[name="ssl-type"][value="internal"]').checked=!0,n.value=SITE.defaults.targetIP||"localhost",t.checked=!1;const U=document.querySelector("#app-deploy-modal .flex-col-gap")?.closest("div"),_=document.querySelector("#app-deploy-modal details"),F=_?.querySelector("div");if(_&&F&&(SITE.configurationType==="public"||SITE.configurationType==="homelab")){const q=document.querySelectorAll('#app-deploy-modal input[name="dns-type"]')[0]?.closest("div.flex-col-gap")?.parentElement,G=document.querySelectorAll('#app-deploy-modal input[name="ssl-type"]')[0]?.closest("div.flex-col-gap")?.parentElement;q&&!q.dataset.moved&&(F.appendChild(q),q.dataset.moved="1"),G&&!G.dataset.moved&&(F.appendChild(G),G.dataset.moved="1")}const W=document.getElementById("media-path-section"),J=document.getElementById("deploy-media-path"),X=document.getElementById("media-path-description");if(i.mediaMount){W.style.display="block",J.value="",J.placeholder="/media/Movies, /media/TVShows or click Browse";const q=document.getElementById("detected-mounts-container"),G=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",G.innerHTML="";const te=[...new Set(K.mounts.map(ee=>ee.hostPath))];J.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=J.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))"),J.value=le.join(", ")},G.appendChild(Z)})}else q.style.display="none"}catch{q.style.display="none"}document.getElementById("browse-media-btn").onclick=()=>{openFolderBrowser(J)}}else W.style.display="none",J.value="",document.getElementById("detected-mounts-container").style.display="none";const Q=document.getElementById("plex-claim-section");Q&&(i.id==="plex"||i.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="",i.docker?.volumes?.length){const q=i.mediaMount?.containerPath,G=i.docker.volumes.filter(V=>!V.includes("{{MEDIA_PATH}}")&&!(q&&V.endsWith(":"+q)));G.length>0?(ne.style.display="block",G.forEach((V,K)=>{const[te,ee]=V.split(":"),Z=document.createElement("div");Z.style.cssText="display: flex; gap: 6px; align-items: center;",Z.innerHTML=`
+Click Cancel to deploy a new container.`)&&(r._useExisting=!0,r._existingContainer=V)}}catch{}d.textContent=`Deploy ${r.name}`;const i=r.subdomain||r.id.replace(/-/g,"");x.value=i;const a=document.getElementById("subpath-compat-warning");if(a)if(SITE.routingMode==="subdirectory"){const J=r.subpathSupport||"strip";J==="none"?(a.style.display="block",a.innerHTML='⚠ '+r.name+" does not support subdirectory mode. It may not work correctly at a subpath."):J==="strip"?(a.style.display="block",a.innerHTML='ⓘ '+r.name+" has unverified subdirectory support. It may require additional configuration."):a.style.display="none"}else a.style.display="none";const o=SITE.defaults.dnsType||(SITE.configurationType==="public"?"public":"private"),l=SITE.defaults.sslType||(SITE.configurationType==="public"?"letsencrypt":"internal"),k=document.querySelector(`input[name="dns-type"][value="${o}"]`),I=document.querySelector(`input[name="ssl-type"][value="${l}"]`);k?k.checked=!0:document.querySelector('input[name="dns-type"][value="private"]').checked=!0,I?I.checked=!0:document.querySelector('input[name="ssl-type"][value="internal"]').checked=!0,t.value=SITE.defaults.targetIP||"localhost",s.checked=!1;const j=document.querySelector("#app-deploy-modal .flex-col-gap")?.closest("div"),_=document.querySelector("#app-deploy-modal details"),F=_?.querySelector("div");if(_&&F&&(SITE.configurationType==="public"||SITE.configurationType==="homelab")){const J=document.querySelectorAll('#app-deploy-modal input[name="dns-type"]')[0]?.closest("div.flex-col-gap")?.parentElement,W=document.querySelectorAll('#app-deploy-modal input[name="ssl-type"]')[0]?.closest("div.flex-col-gap")?.parentElement;J&&!J.dataset.moved&&(F.appendChild(J),J.dataset.moved="1"),W&&!W.dataset.moved&&(F.appendChild(W),W.dataset.moved="1")}const q=document.getElementById("media-path-section"),G=document.getElementById("deploy-media-path"),X=document.getElementById("media-path-description");if(r.mediaMount){q.style.display="block",G.value="",G.placeholder="/media/Movies, /media/TVShows or click Browse";const J=document.getElementById("detected-mounts-container"),W=document.getElementById("detected-mounts-list");try{const K=await(await fetch("/api/v1/media/detected-mounts")).json();if(K.success&&K.mounts.length>0){J.style.display="block",W.innerHTML="";const te=[...new Set(K.mounts.map(ee=>ee.hostPath))];G.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=G.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))"),G.value=le.join(", ")},W.appendChild(Z)})}else J.style.display="none"}catch{J.style.display="none"}document.getElementById("browse-media-btn").onclick=()=>{openFolderBrowser(G)}}else q.style.display="none",G.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 J=r.mediaMount?.containerPath,W=r.docker.volumes.filter(V=>!V.includes("{{MEDIA_PATH}}")&&!(J&&V.endsWith(":"+J)));W.length>0?(ne.style.display="block",W.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}
- `,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=i.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 q=e.value||se;Y.innerHTML='Checking port...';const G=await A(q);if(G.available)Y.innerHTML=`Port ${escapeHtml(String(q))} is available`;else{const V=await p(se);Y.innerHTML=`
- Port ${escapeHtml(q)} in use by ${escapeHtml(G.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 G=await(await fetch("/api/v1/tailscale/status")).json();G.success&&G.installed&&G.connected?o.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 J=e.value||se;Y.innerHTML='Checking port...';const W=await D(J);if(W.available)Y.innerHTML=`Port ${escapeHtml(String(J))} is available`;else{const V=await m(se);Y.innerHTML=`
+ Port ${escapeHtml(J)} in use by ${escapeHtml(W.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 W=await(await fetch("/api/v1/tailscale/status")).json();W.success&&W.installed&&W.connected?n.innerHTML=`
Connected
- ${G.self?.hostname} (${G.self?.ip})
- | ${G.deviceCount} devices
- `:G.installed?o.innerHTML='Not connected':(o.innerHTML='Not available',t.disabled=!0)}catch{o.innerHTML='Could not check status'}function ae(){const q=w.value||"subdomain",G=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(G==="private")K=`${V==="none"?"http":"https"}://${buildDomain(q)}`;else if(G==="public"){const te=V==="none"?"http":"https",ee=SITE.domain||q;K=SITE.domain?`${te}://${q}.${SITE.domain}`:`${te}://${q}`}else{const te=e.value||i.defaultPort||DC.DEFAULTS.SERVICE_PORT;K=`http://${n.value}:${te}`}m.textContent=K}w.oninput=ae,n.oninput=ae,e.oninput=ae,document.querySelectorAll('input[name="dns-type"]').forEach(q=>{q.onchange=ae}),document.querySelectorAll('input[name="ssl-type"]').forEach(q=>{q.onchange=ae}),ae(),M.classList.remove("show"),c.classList.add("show"),c.dataset.appTemplate=JSON.stringify(i)}async function k(i){const c=i.appTemplate,l=safeGetJSON(C,[]),w=c._useExisting&&c._existingContainer,m=l.find(n=>n.id===i.subdomain);if(!(m&&!w&&!confirm(`An app with subdomain "${i.subdomain}" already exists. Redeploy?`))){if(m){const n=l.indexOf(m);l.splice(n,1),safeSet(C,JSON.stringify(l))}if(w)i.port=c._existingContainer.primaryPort;else{const n=i.port||c.defaultPort||8080;showNotification(`Checking port ${n} availability...`,"info",0);const e=await A(n);if(!e.available){const t=await p(c.defaultPort||8080);if(confirm(`Port ${n} is already in use by ${e.conflict?.usedBy||"another container"}.
+ ${W.self?.hostname} (${W.self?.ip})
+ | ${W.deviceCount} devices
+ `:W.installed?n.innerHTML='Not connected':(n.innerHTML='Not available',s.disabled=!0)}catch{n.innerHTML='Could not check status'}function ae(){const J=x.value||"subdomain",W=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}/${J}`;else if(W==="private")K=`${V==="none"?"http":"https"}://${buildDomain(J)}`;else if(W==="public"){const te=V==="none"?"http":"https",ee=SITE.domain||J;K=SITE.domain?`${te}://${J}.${SITE.domain}`:`${te}://${J}`}else{const te=e.value||r.defaultPort||DC.DEFAULTS.SERVICE_PORT;K=`http://${t.value}:${te}`}y.textContent=K}x.oninput=ae,t.oninput=ae,e.oninput=ae,document.querySelectorAll('input[name="dns-type"]').forEach(J=>{J.onchange=ae}),document.querySelectorAll('input[name="ssl-type"]').forEach(J=>{J.onchange=ae}),ae(),$.classList.remove("show"),c.classList.add("show"),c.dataset.appTemplate=JSON.stringify(r)}async function E(r){const c=r.appTemplate,d=safeGetJSON(L,[]),x=c._useExisting&&c._existingContainer,y=d.find(t=>t.id===r.subdomain);if(!(y&&!x&&!confirm(`An app with subdomain "${r.subdomain}" already exists. Redeploy?`))){if(y){const t=d.indexOf(y);d.splice(t,1),safeSet(L,JSON.stringify(d))}if(x)r.port=c._existingContainer.primaryPort;else{const t=r.port||c.defaultPort||8080;showNotification(`Checking port ${t} availability...`,"info",0);const e=await D(t);if(!e.available){const s=await m(c.defaultPort||8080);if(confirm(`Port ${t} is already in use by ${e.conflict?.usedBy||"another container"}.
-Would you like to use port ${t} instead?`))i.port=t;else{showNotification("Deployment cancelled - port conflict","error",5e3);return}}}showNotification(w?`Configuring ${c.name} with existing container...`:`Deploying ${c.name}...`,"info",0);try{const n={appId:c.id,config:{subdomain:i.subdomain,ip:i.ip,createDns:i.dnsType==="private",port:i.port||c.defaultPort||null,sslType:i.sslType,dnsType:i.dnsType,tailscaleOnly:i.tailscaleOnly||!1,mediaPath:i.mediaPath||null,plexClaimToken:i.plexClaimToken||null,customVolumes:i.customVolumes||null}};w&&(n.config.useExisting=!0,n.config.existingContainerId=c._existingContainer.id,n.config.existingPort=c._existingContainer.primaryPort,!i.port&&c._existingContainer.primaryPort&&(n.config.port=c._existingContainer.primaryPort));const t=await(await secureFetch("/api/v1/apps/deploy",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})).json();if(t.success){const o={id:i.subdomain,name:c.name,logo:`/assets/${c.id}.png`,containerId:t.containerId,url:t.url,ip:i.ip,appTemplate:c.id,tailscaleOnly:i.tailscaleOnly||!1};l.push(o),safeSet(C,JSON.stringify(l)),window.APPS&&!window.APPS.some(a=>a.id===c.id)&&(window.APPS.push(o),typeof window.buildGrid=="function"&&window.buildGrid(),typeof window.refreshAll=="function"&&setTimeout(()=>window.refreshAll(),500));let r=t.usedExisting?`${c.name} configured with existing container!
-URL: ${t.url}`:`${c.name} deployed successfully!
-URL: ${t.url}`;t.warning&&(r+=`
+Would you like to use port ${s} instead?`))r.port=s;else{showNotification("Deployment cancelled - port conflict","error",5e3);return}}}showNotification(x?`Configuring ${c.name} with existing container...`:`Deploying ${c.name}...`,"info",0);try{const t={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}};x&&(t.config.useExisting=!0,t.config.existingContainerId=c._existingContainer.id,t.config.existingPort=c._existingContainer.primaryPort,!r.port&&c._existingContainer.primaryPort&&(t.config.port=c._existingContainer.primaryPort));const s=await(await secureFetch("/api/v1/apps/deploy",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json();if(s.success){const n={id:r.subdomain,name:c.name,logo:`/assets/${c.id}.png`,containerId:s.containerId,url:s.url,ip:r.ip,appTemplate:c.id,tailscaleOnly:r.tailscaleOnly||!1};d.push(n),safeSet(L,JSON.stringify(d)),window.APPS&&!window.APPS.some(a=>a.id===c.id)&&(window.APPS.push(n),typeof window.buildGrid=="function"&&window.buildGrid(),typeof window.refreshAll=="function"&&setTimeout(()=>window.refreshAll(),500));let i=s.usedExisting?`${c.name} configured with existing container!
+URL: ${s.url}`:`${c.name} deployed successfully!
+URL: ${s.url}`;s.warning&&(i+=`
-\u26A0 Warning: ${t.warning}`),showNotification(r,"success",8e3),delete c._useExisting,delete c._existingContainer,t.url&&t.url.startsWith("https://")&&H(t.url,c.name),t.setupInstructions&&t.setupInstructions.length>0&&setTimeout(()=>{const a=t.setupInstructions.join(`
-`);showNotification(`Setup Instructions for ${c.name}: ${a}`,"info",1e4)},1e3)}else throw new Error(t.error||"Deployment failed")}catch(n){y.logError("[AppSelector] Deployment",n,{function:"deploy"}),showNotification(`Failed to deploy ${c.name}: ${n.message}`,"error",8e3)}}}async function H(i,c){showNotification(`\u23F3 Generating SSL certificate for ${c}...`,"warning",6e4);let l=0;const w=12,m=async()=>{l++;try{const n=await fetch(i,{method:"HEAD",mode:"no-cors"});return showNotification(`\u2705 ${c} is ready! SSL certificate generated.`,"success",5e3),!0}catch{return l{window.APPS.some(l=>l.id===c.id)||window.APPS.push(c)})}document.getElementById("add-service-btn")?.addEventListener("click",()=>{T(),M.classList.add("show")}),wireModal(M,document.getElementById("app-selector-cancel"));const u=document.getElementById("app-deploy-modal");document.getElementById("app-deploy-cancel")?.addEventListener("click",()=>{u.classList.remove("show")}),document.getElementById("app-deploy-confirm")?.addEventListener("click",()=>{const i=JSON.parse(u.dataset.appTemplate),c=document.getElementById("deploy-media-path").value.trim(),l=[];document.querySelectorAll("#volume-mounts-list .vol-host-path").forEach(m=>{l.push({hostPath:m.value.trim(),containerPath:m.dataset.containerPath})});const w={appTemplate:i,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:l.length>0?l:null,resources:{cpus:parseFloat(document.getElementById("deploy-cpu-limit").value)||0,memory:parseFloat(document.getElementById("deploy-memory-limit").value)||0}};if(!w.subdomain){showNotification("Please enter a subdomain or domain name","warning");return}if(i.mediaMount?.required&&!c){showNotification("Please enter a media library path for this application","warning");return}u.classList.remove("show"),k(w)}),wireModal(u);const j=document.getElementById("folder-browser-modal"),v=document.getElementById("folder-browser-path"),N=document.getElementById("folder-browser-list"),g=document.getElementById("folder-browser-selected"),D=document.getElementById("folder-browser-selected-list");let B="",f=[],I=null;window.openFolderBrowser=function(i){I=i,f=i.value.split(",").map(c=>c.trim()).filter(c=>c),B="",z(),b(""),j.classList.add("show")};async function b(i){v.textContent=i||"Select a drive...",N.innerHTML='
`;return}B=l.path||"",v.textContent=B||"Select a drive...";let w="";l.parent&&l.parent!==l.path&&(w+=`
+\u26A0 Warning: ${s.warning}`),showNotification(i,"success",8e3),delete c._useExisting,delete c._existingContainer,s.url&&s.url.startsWith("https://")&&H(s.url,c.name),s.setupInstructions&&s.setupInstructions.length>0&&setTimeout(()=>{const a=s.setupInstructions.join(`
+`);showNotification(`Setup Instructions for ${c.name}: ${a}`,"info",1e4)},1e3)}else throw new Error(s.error||"Deployment failed")}catch(t){h.logError("[AppSelector] Deployment",t,{function:"deploy"}),showNotification(`Failed to deploy ${c.name}: ${t.message}`,"error",8e3)}}}async function H(r,c){showNotification(`\u23F3 Generating SSL certificate for ${c}...`,"warning",6e4);let d=0;const x=12,y=async()=>{d++;try{const t=await fetch(r,{method:"HEAD",mode:"no-cors"});return showNotification(`\u2705 ${c} is ready! SSL certificate generated.`,"success",5e3),!0}catch{return d{window.APPS.some(d=>d.id===c.id)||window.APPS.push(c)})}document.getElementById("add-service-btn")?.addEventListener("click",()=>{T(),$.classList.add("show")}),wireModal($,document.getElementById("app-selector-cancel"));const v=document.getElementById("app-deploy-modal");document.getElementById("app-deploy-cancel")?.addEventListener("click",()=>{v.classList.remove("show")}),document.getElementById("app-deploy-confirm")?.addEventListener("click",()=>{const r=JSON.parse(v.dataset.appTemplate),c=document.getElementById("deploy-media-path").value.trim(),d=[];document.querySelectorAll("#volume-mounts-list .vol-host-path").forEach(y=>{d.push({hostPath:y.value.trim(),containerPath:y.dataset.containerPath})});const x={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:d.length>0?d:null,resources:{cpus:parseFloat(document.getElementById("deploy-cpu-limit").value)||0,memory:parseFloat(document.getElementById("deploy-memory-limit").value)||0}};if(!x.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}v.classList.remove("show"),E(x)}),wireModal(v);const O=document.getElementById("folder-browser-modal"),f=document.getElementById("folder-browser-path"),z=document.getElementById("folder-browser-list"),A=document.getElementById("folder-browser-selected"),U=document.getElementById("folder-browser-selected-list");let p="",u=[],b=null;window.openFolderBrowser=function(r){b=r,u=r.value.split(",").map(c=>c.trim()).filter(c=>c),p="",M(),g(""),O.classList.add("show")};async function g(r){f.textContent=r||"Select a drive...",z.innerHTML='
`:""}
+ `}function f(){const p=document.querySelectorAll("#recipe-component-list input[data-component-id]"),u=new Set;p.forEach(g=>{g.checked&&u.add(g.dataset.componentId)});const b=R.components||[];return b.filter(g=>g.required).forEach(g=>u.add(g.id)),b.filter(g=>u.has(g.id))}async function z(){const p=document.getElementById("recipe-progress-list"),u=document.getElementById("recipe-deploy-result");u.style.display="none",p.innerHTML="";const b=f();for(const c of b){const d=document.createElement("div");d.id=`recipe-progress-${c.id}`,d.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;",d.innerHTML=`
\u23F3${escapeHtml(c.role||c.id)}Queued
- `,B.appendChild(l)}const b=document.querySelectorAll("#recipe-volume-list input[data-volume-key]"),z={};b.forEach(c=>{z[c.dataset.volumeKey]=c.value});const i={selectedComponents:I.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:z},componentOverrides:{}};for(const c of I)g(c.id,"deploying","Deploying...");try{const l=await(await secureFetch("/api/v1/recipes/deploy",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({recipeId:O.id,config:i})})).json();if(l.success){for(const w of l.deployed||[])g(w.id,"success",w.url?`Running \u2192 ${w.url}`:"Running");for(const w of l.errors||[])g(w.componentId,"error",w.error);f.style.display="",f.innerHTML=`
+ `,p.appendChild(d)}const g=document.querySelectorAll("#recipe-volume-list input[data-volume-key]"),M={};g.forEach(c=>{M[c.dataset.volumeKey]=c.value});const r={selectedComponents:b.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 b)A(c.id,"deploying","Deploying...");try{const d=await(await secureFetch("/api/v1/recipes/deploy",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({recipeId:R.id,config:r})})).json();if(d.success){for(const x of d.deployed||[])A(x.id,"success",x.url?`Running \u2192 ${x.url}`:"Running");for(const x of d.errors||[])A(x.componentId,"error",x.error);u.style.display="",u.innerHTML=`
`}}function A(p,u,b){const g=document.getElementById(`recipe-progress-${p}`);if(!g)return;const M=g.querySelector(".recipe-progress-icon"),r=g.querySelector(".recipe-progress-status");u==="deploying"?(M.textContent="\u23F3",r.style.color="var(--accent)"):u==="success"?(M.textContent="\u2705",r.style.color="var(--ok-fg)"):u==="error"&&(M.textContent="\u274C",r.style.color="var(--bad-fg)"),r.textContent=b}m.addEventListener("click",()=>{if(S===3){S=4,H(),z();return}S<3&&(S++,H(),S===2&&v(),S===3&&O())}),D.addEventListener("click",()=>{S>1&&S<4&&(S--,H())}),window.groupRecipeCards=function(){const p=document.querySelectorAll(".service-card[data-recipe-id]");if(p.length===0)return;const u={};p.forEach(b=>{const g=b.dataset.recipeId;u[g]||(u[g]=[]),u[g].push(b)});for(const[b,g]of Object.entries(u))g.length<2||g.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=b.replace(/-/g," "),M.style.position="relative",M.appendChild(c))}})},window.manageRecipe=async function(p,u){const b=`/api/v1/recipes/${p}/${u}`,g=u==="remove"?"DELETE":"POST",M=u==="remove"?`/api/v1/recipes/${p}`:b;if(!(u==="remove"&&!confirm(`Remove the entire ${p} recipe? This will delete all containers and configuration.`)))try{const c=await(await secureFetch(M,{method:g})).json();c.success?(showNotification(`Recipe ${u}: ${c.results?.filter(d=>d.status!=="failed").length||0} components processed`,"success",4e3),window.loadServices&&window.loadServices()):showNotification(`Recipe ${u} failed: ${c.error}`,"error",5e3)}catch(r){showNotification(`Network error: ${r.message}`,"error",5e3)}};const U=document.createElement("style");U.textContent=`
.recipe-step {
flex: 1;
text-align: center;
@@ -563,16 +550,16 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};
.recipe-step-panel {
min-height: 180px;
}
- `,document.head.appendChild(D),x()})(),(function(){document.getElementById("reload-caddy-top")?.addEventListener("click",async()=>{const y=document.getElementById("reload-caddy-top"),C=y.textContent;try{y.textContent="\u23F3 Reloading...",y.disabled=!0;const O=await secureFetch("/api/v1/caddy/reload",{method:"POST",headers:{"Content-Type":"application/json"}}),E=await O.json();if(O.ok&&E.success)y.textContent="\u2705 Reloaded!",setTimeout(()=>{y.textContent=C,y.disabled=!1},2e3);else throw new Error(E.error||"Reload failed")}catch(O){y.textContent="\u274C Failed",showNotification(`Failed to reload Caddy: ${O.message}`,"error"),setTimeout(()=>{y.textContent=C,y.disabled=!1},2e3)}})})(),(function(){injectModal("error-log-modal",'
\u{1F4CB} Error Logs
Loading error logs...
');const y=document.getElementById("error-log-modal"),C=document.getElementById("error-log-content"),O=document.getElementById("view-error-logs"),E=document.getElementById("error-log-refresh"),M=document.getElementById("error-log-clear"),R=document.getElementById("error-log-close");async function P(){C.innerHTML='
\u2705 No errors logged! Everything is working smoothly.
':C.innerHTML=T.logs.map(x=>`
+ `,document.head.appendChild(U),w()})(),(function(){document.getElementById("reload-caddy-top")?.addEventListener("click",async()=>{const h=document.getElementById("reload-caddy-top"),L=h.textContent;try{h.textContent="\u23F3 Reloading...",h.disabled=!0;const R=await secureFetch("/api/v1/caddy/reload",{method:"POST",headers:{"Content-Type":"application/json"}}),S=await R.json();if(R.ok&&S.success)h.textContent="\u2705 Reloaded!",setTimeout(()=>{h.textContent=L,h.disabled=!1},2e3);else throw new Error(S.error||"Reload failed")}catch(R){h.textContent="\u274C Failed",showNotification(`Failed to reload Caddy: ${R.message}`,"error"),setTimeout(()=>{h.textContent=L,h.disabled=!1},2e3)}})})(),(function(){injectModal("error-log-modal",'
\u{1F4CB} Error Logs
Loading error logs...
');const h=document.getElementById("error-log-modal"),L=document.getElementById("error-log-content"),R=document.getElementById("view-error-logs"),S=document.getElementById("error-log-refresh"),$=document.getElementById("error-log-clear"),P=document.getElementById("error-log-close");async function N(){L.innerHTML='
@@ -683,7 +670,7 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};
-
`);const y=document.getElementById("snapshot-modal"),C=document.getElementById("snapshot-btn"),O=document.getElementById("snapshot-close"),E=document.getElementById("snapshot-container-select"),M=document.getElementById("snapshot-details"),R=document.getElementById("snapshot-create-btn"),P=document.getElementById("snapshot-create-status");let A=null;async function p(){try{const L=await(await fetch("/api/v1/containers")).json();if(!L.success||!L.containers)return;E.innerHTML='';for(const u of L.containers){const j=document.createElement("option");j.value=u.id,j.textContent=`${u.name||u.id} (${u.image||"unknown"})`,j.dataset.name=u.name,j.dataset.image=u.image,j.dataset.status=u.status,j.dataset.created=u.created,E.appendChild(j)}}catch(H){console.error("Failed to load containers:",H)}}function T(H){if(!H||!H.value){M.style.display="none",A=null;return}A=H.value,document.getElementById("snapshot-image").textContent=H.dataset.image||"-",document.getElementById("snapshot-status").textContent=H.dataset.status||"-",document.getElementById("snapshot-created").textContent=H.dataset.created?new Date(H.dataset.created*1e3).toLocaleString():"-",document.getElementById("snapshot-id").textContent=H.value.substring(0,12),M.style.display=""}async function x(){if(!A){P.textContent="Please select a container first",P.style.color="var(--bad-fg)";return}const H=document.getElementById("snapshot-name").value.trim();if(!H){P.textContent="Please enter a snapshot name",P.style.color="var(--bad-fg)";return}const L=document.getElementById("snapshot-leave-running").checked;R.disabled=!0,R.textContent="Creating...",P.textContent="";try{const j=await(await fetch(`/api/v1/containers/${encodeURIComponent(A)}/checkpoint`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:H,leaveRunning:L})})).json();j.success?(P.textContent=`\u2713 Snapshot "${H}" created successfully`,P.style.color="var(--ok-fg)",document.getElementById("snapshot-name").value=""):(P.textContent=`\u2717 Failed: ${j.error||"Unknown error"}`,P.style.color="var(--bad-fg)")}catch(u){P.textContent=`\u2717 Error: ${u.message}`,P.style.color="var(--bad-fg)"}finally{R.disabled=!1,R.textContent="\u{1F4BE} Create Snapshot"}}function $(){y.classList.add("show"),p()}function k(){y.classList.remove("show"),M.style.display="none",A=null,E.selectedIndex=0}C?.addEventListener("click",$),O?.addEventListener("click",k),wireModal(y,O),E?.addEventListener("change",H=>{const L=E.options[E.selectedIndex];T(L)}),R?.addEventListener("click",x),y?.querySelectorAll(".panel-tab").forEach(H=>{H.addEventListener("click",()=>{y.querySelectorAll(".panel-tab").forEach(L=>L.classList.remove("active")),y.querySelectorAll(".panel-section").forEach(L=>L.classList.remove("active")),H.classList.add("active"),y.querySelector(`#${H.dataset.panel}`).classList.add("active")})})})(),(function(){injectModal("arr-setup-modal",`
+
`);const h=document.getElementById("snapshot-modal"),L=document.getElementById("snapshot-btn"),R=document.getElementById("snapshot-close"),S=document.getElementById("snapshot-container-select"),$=document.getElementById("snapshot-details"),P=document.getElementById("snapshot-create-btn"),N=document.getElementById("snapshot-create-status");let D=null;async function m(){try{const C=await(await fetch("/api/v1/containers")).json();if(!C.success||!C.containers)return;S.innerHTML='';for(const v of C.containers){const O=document.createElement("option");O.value=v.id,O.textContent=`${v.name||v.id} (${v.image||"unknown"})`,O.dataset.name=v.name,O.dataset.image=v.image,O.dataset.status=v.status,O.dataset.created=v.created,S.appendChild(O)}}catch(H){console.error("Failed to load containers:",H)}}function T(H){if(!H||!H.value){$.style.display="none",D=null;return}D=H.value,document.getElementById("snapshot-image").textContent=H.dataset.image||"-",document.getElementById("snapshot-status").textContent=H.dataset.status||"-",document.getElementById("snapshot-created").textContent=H.dataset.created?new Date(H.dataset.created*1e3).toLocaleString():"-",document.getElementById("snapshot-id").textContent=H.value.substring(0,12),$.style.display=""}async function w(){if(!D){N.textContent="Please select a container first",N.style.color="var(--bad-fg)";return}const H=document.getElementById("snapshot-name").value.trim();if(!H){N.textContent="Please enter a snapshot name",N.style.color="var(--bad-fg)";return}const C=document.getElementById("snapshot-leave-running").checked;P.disabled=!0,P.textContent="Creating...",N.textContent="";try{const O=await(await fetch(`/api/v1/containers/${encodeURIComponent(D)}/checkpoint`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:H,leaveRunning:C})})).json();O.success?(N.textContent=`\u2713 Snapshot "${H}" created successfully`,N.style.color="var(--ok-fg)",document.getElementById("snapshot-name").value=""):(N.textContent=`\u2717 Failed: ${O.error||"Unknown error"}`,N.style.color="var(--bad-fg)")}catch(v){N.textContent=`\u2717 Error: ${v.message}`,N.style.color="var(--bad-fg)"}finally{P.disabled=!1,P.textContent="\u{1F4BE} Create Snapshot"}}function B(){h.classList.add("show"),m()}function E(){h.classList.remove("show"),$.style.display="none",D=null,S.selectedIndex=0}L?.addEventListener("click",B),R?.addEventListener("click",E),wireModal(h,R),S?.addEventListener("change",H=>{const C=S.options[S.selectedIndex];T(C)}),P?.addEventListener("click",w),h?.querySelectorAll(".panel-tab").forEach(H=>{H.addEventListener("click",()=>{h.querySelectorAll(".panel-tab").forEach(C=>C.classList.remove("active")),h.querySelectorAll(".panel-section").forEach(C=>C.classList.remove("active")),H.classList.add("active"),h.querySelector(`#${H.dataset.panel}`).classList.add("active")})})})(),(function(){injectModal("arr-setup-modal",`
\u{1F3AC} Smart Arr Connect
@@ -762,73 +749,73 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};
-
`);const y=document.getElementById("arr-setup-modal"),C=document.getElementById("arr-setup-btn"),O=document.getElementById("arr-setup-cancel"),E=document.getElementById("smart-connect-btn"),M=document.getElementById("smart-phase-detect"),R=document.getElementById("smart-phase-credentials"),P=document.getElementById("smart-phase-progress"),A=document.getElementById("smart-phase-results"),p=document.getElementById("smart-detect-results"),T=document.getElementById("smart-credential-inputs"),x=document.getElementById("smart-progress-steps"),$=document.getElementById("smart-results-content"),k=document.getElementById("smart-plex-libraries"),H=document.getElementById("smart-retry-btn");let L=null;const u={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 v(b){M.style.display=b==="detect"?"block":"none",R.style.display=b==="credentials"?"block":"none",P.style.display=b==="progress"?"block":"none",A.style.display=b==="results"?"block":"none"}function N(b){const z={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"}},i=z[b]||z.not_found;return`${i.icon} ${i.text}`}async function g(){v("detect"),p.style.display="none";try{if(L=await(await fetch("/api/v1/arr/smart-detect")).json(),!L.success){p.innerHTML=`
`,p.style.display="block"}}function D(b){let z="";const i=b.services,c=["radarr","sonarr","prowlarr"];for(const m of c){const n=i[m];if(!n||n.status==="not_found"&&!n.url)continue;const e=u[m],t=j[m],o=n.status==="connected";z+=`
`,m.style.display="block"}}function U(g){let M="";const r=g.services,c=["radarr","sonarr","prowlarr"];for(const y of c){const t=r[y];if(!t||t.status==="not_found"&&!t.url)continue;const e=v[y],s=O[y],n=t.status==="connected";M+=`
`}T.innerHTML=z}window.smartTestConnection=async function(b){const z=document.getElementById(`smart-${b}-url`),i=document.getElementById(`smart-${b}-key`),c=document.getElementById(`smart-${b}-status`),l=z?.value.trim(),w=i?.value.trim();if(!l||!w){c.innerHTML='Enter URL and API key';return}c.innerHTML='';try{const n=await(await secureFetch("/api/v1/arr/test-connection",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({service:b,url:l,apiKey:w})})).json();n.success?c.innerHTML=`✓ ${escapeHtml(n.appName||"Connected")} v${escapeHtml(n.version||"")}`:c.innerHTML=`✗ ${escapeHtml(n.error)}`}catch(m){c.innerHTML=`✗ ${escapeHtml(m.message)}`}};async function B(){v("progress"),x.innerHTML='
Connecting services...
';const b={};for(const i of["radarr","sonarr","prowlarr"]){const c=document.getElementById(`smart-${i}-url`)?.value.trim(),l=document.getElementById(`smart-${i}-key`)?.value.trim();l&&c?b[i]={apiKey:l,url:c}:l&&(b[i]={apiKey:l})}const z={services:Object.keys(b).length>0?b: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(z)})).json();let l="";for(const w of c.steps||[]){const m=w.status==="success"?'✓':'✗',n=w.status==="success"?"var(--muted)":"var(--bad-fg)";l+=`
",$.innerHTML=m,H.style.display=z.failed>0?"block":"none",b.steps?.some(n=>n.step.includes("Plex")&&n.status==="success")&&I()}async function I(){try{const z=await(await fetch("/api/v1/plex/libraries")).json();if(z.success&&z.libraries?.length>0){let i=`
-
\u{1F3AC} ${escapeHtml(z.serverName)} Libraries
-
`;for(const c of z.libraries){const l=c.type==="movie"?"\u{1F3AC}":c.type==="show"?"\u{1F4FA}":"\u{1F3B5}";i+=`
- ${l} ${escapeHtml(c.title)}
+ ${t}
+
`}T.innerHTML=M}window.smartTestConnection=async function(g){const M=document.getElementById(`smart-${g}-url`),r=document.getElementById(`smart-${g}-key`),c=document.getElementById(`smart-${g}-status`),d=M?.value.trim(),x=r?.value.trim();if(!d||!x){c.innerHTML='Enter URL and API key';return}c.innerHTML='';try{const t=await(await secureFetch("/api/v1/arr/test-connection",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({service:g,url:d,apiKey:x})})).json();t.success?c.innerHTML=`✓ ${escapeHtml(t.appName||"Connected")} v${escapeHtml(t.version||"")}`:c.innerHTML=`✗ ${escapeHtml(t.error)}`}catch(y){c.innerHTML=`✗ ${escapeHtml(y.message)}`}};async function p(){f("progress"),w.innerHTML='
Connecting services...
';const g={};for(const r of["radarr","sonarr","prowlarr"]){const c=document.getElementById(`smart-${r}-url`)?.value.trim(),d=document.getElementById(`smart-${r}-key`)?.value.trim();d&&c?g[r]={apiKey:d,url:c}:d&&(g[r]={apiKey:d})}const M={services:Object.keys(g).length>0?g: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 d="";for(const x of c.steps||[]){const y=x.status==="success"?'✓':'✗',t=x.status==="success"?"var(--muted)":"var(--bad-fg)";d+=`
"}}}async function f(e,t){try{var o=await secureFetch("/api/v1/backups/schedule",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({appId:e,...t})}),r=await o.json();r.success?showNotification("Schedule updated for "+e,"success"):(showNotification("Update failed: "+(r.error||"Unknown"),"error"),B())}catch(a){showNotification("Error: "+a.message,"error")}}async function I(e){try{var t=await secureFetch("/api/v1/backups/backup/"+encodeURIComponent(e),{method:"POST",headers:{"Content-Type":"application/json"}}),o=await t.json();o.success?showNotification("Backup started for "+e+"!","success"):showNotification("Backup failed: "+(o.error||"Unknown"),"error")}catch(r){showNotification("Error: "+r.message,"error")}}async function b(e){if(confirm("Remove backup schedule for "+e+"?"))try{var t=await secureFetch("/api/v1/backups/schedule/"+encodeURIComponent(e),{method:"DELETE"}),o=await t.json();o.success?(showNotification("Schedule removed for "+e,"success"),B()):showNotification("Delete failed: "+(o.error||"Unknown"),"error")}catch(r){showNotification("Error: "+r.message,"error")}}async function z(){var e=document.getElementById("new-schedule-appid")?.value?.trim(),t=document.getElementById("new-schedule-interval")?.value||"daily",o=parseInt(document.getElementById("new-schedule-retention")?.value)||7;if(!e){showNotification("Please enter an App ID","warning");return}try{var r=await secureFetch("/api/v1/backups/schedule",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({appId:e,schedule:t,retention:{keep:o},enabled:!0})}),a=await r.json();if(a.success){showNotification("Schedule created for "+e,"success"),B();var s=document.getElementById("new-schedule-appid");s&&(s.value="")}else showNotification("Failed: "+(a.error||"Unknown"),"error")}catch(d){showNotification("Error: "+d.message,"error")}}async function i(){if(N){N.innerHTML='
Loading...
';try{var e=await fetch("/api/v1/backups/files"),t=await e.json();if(!t.success)throw new Error(t.error||"Failed to load");var o=t.files||[];if(o.length===0){N.innerHTML='
"}}}window.__restoreServerBackup=async function(e){if(confirm("Restore from this server backup? This will overwrite current configuration."))try{var t=await secureFetch("/api/v1/backups/restore/"+e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({restoreServices:!0,restoreConfig:!0})}),o=await t.json();o.success?(showNotification("Restore completed successfully!","success"),location.reload()):showNotification("Restore failed: "+(o.error||"Unknown error"),"error")}catch(r){showNotification("Restore error: "+r.message,"error")}},document.querySelector('[data-panel="backup-schedules-tab"]')?.addEventListener("click",B),document.querySelector('[data-panel="backup-disk-tab"]')?.addEventListener("click",i),document.querySelector('[data-panel="backup-pointintime-tab"]')?.addEventListener("click",l),document.querySelector('[data-panel="backup-history-tab"]')?.addEventListener("click",c);async function l(){if(g){try{var e=await fetch("/api/v1/license/status"),t=await e.json();if(t.tier!=="premium"){g.innerHTML=`
\u2B50
Premium Feature
Point-in-time restore requires DashCaddy Premium with auto-backup enabled.
"}}}async function u(e,s){try{var n=await secureFetch("/api/v1/backups/schedule",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({appId:e,...s})}),i=await n.json();i.success?showNotification("Schedule updated for "+e,"success"):(showNotification("Update failed: "+(i.error||"Unknown"),"error"),p())}catch(a){showNotification("Error: "+a.message,"error")}}async function b(e){try{var s=await secureFetch("/api/v1/backups/backup/"+encodeURIComponent(e),{method:"POST",headers:{"Content-Type":"application/json"}}),n=await s.json();n.success?showNotification("Backup started for "+e+"!","success"):showNotification("Backup failed: "+(n.error||"Unknown"),"error")}catch(i){showNotification("Error: "+i.message,"error")}}async function g(e){if(confirm("Remove backup schedule for "+e+"?"))try{var s=await secureFetch("/api/v1/backups/schedule/"+encodeURIComponent(e),{method:"DELETE"}),n=await s.json();n.success?(showNotification("Schedule removed for "+e,"success"),p()):showNotification("Delete failed: "+(n.error||"Unknown"),"error")}catch(i){showNotification("Error: "+i.message,"error")}}async function M(){var e=document.getElementById("new-schedule-appid")?.value?.trim(),s=document.getElementById("new-schedule-interval")?.value||"daily",n=parseInt(document.getElementById("new-schedule-retention")?.value)||7;if(!e){showNotification("Please enter an App ID","warning");return}try{var i=await secureFetch("/api/v1/backups/schedule",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({appId:e,schedule:s,retention:{keep:n},enabled:!0})}),a=await i.json();if(a.success){showNotification("Schedule created for "+e,"success"),p();var o=document.getElementById("new-schedule-appid");o&&(o.value="")}else showNotification("Failed: "+(a.error||"Unknown"),"error")}catch(l){showNotification("Error: "+l.message,"error")}}async function r(){if(z){z.innerHTML='
Loading...
';try{var e=await fetch("/api/v1/backups/files"),s=await e.json();if(!s.success)throw new Error(s.error||"Failed to load");var n=s.files||[];if(n.length===0){z.innerHTML='
"}}}window.__restoreServerBackup=async function(e){if(confirm("Restore from this server backup? This will overwrite current configuration."))try{var s=await secureFetch("/api/v1/backups/restore/"+e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({restoreServices:!0,restoreConfig:!0})}),n=await s.json();n.success?(showNotification("Restore completed successfully!","success"),location.reload()):showNotification("Restore failed: "+(n.error||"Unknown error"),"error")}catch(i){showNotification("Restore error: "+i.message,"error")}},document.querySelector('[data-panel="backup-schedules-tab"]')?.addEventListener("click",p),document.querySelector('[data-panel="backup-disk-tab"]')?.addEventListener("click",r),document.querySelector('[data-panel="backup-pointintime-tab"]')?.addEventListener("click",d),document.querySelector('[data-panel="backup-history-tab"]')?.addEventListener("click",c);async function d(){if(A){try{var e=await fetch("/api/v1/license/status"),s=await e.json();if(s.tier!=="premium"){A.innerHTML=`
\u2B50
Premium Feature
Point-in-time restore requires DashCaddy Premium with auto-backup enabled.
Configuration differs \u2014 restoring will replace current settings
':o+='
No changes
',o+="
"}o+='
',document.body.insertAdjacentHTML("beforeend",o),document.getElementById("compare-close-btn")?.addEventListener("click",function(){document.getElementById("compare-overlay")?.remove()}),document.getElementById("compare-overlay")?.addEventListener("click",function(I){I.target===this&&this.remove()})}catch(I){showNotification("Compare error: "+I.message,"error")}}async function t(e,s){if(confirm("Restore "+s+" for "+e+`?
-This will replace current configuration, credentials, and data. Containers will be restarted.`))try{var o=await secureFetch("/api/v1/apps/"+encodeURIComponent(e)+"/revert/"+encodeURIComponent(t),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({restartContainers:!0})}),r=await o.json();r.success?(showNotification(e+" restored to "+t,"success"),setTimeout(function(){location.reload()},1500)):showNotification("Restore failed: "+(r.error||"Unknown"),"error")}catch(a){showNotification("Restore error: "+a.message,"error")}}})(),(function(){injectModal("stats-modal",`
+This will replace current configuration, credentials, and data. Containers will be restarted.`))try{var n=await secureFetch("/api/v1/apps/"+encodeURIComponent(e)+"/revert/"+encodeURIComponent(s),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({restartContainers:!0})}),i=await n.json();i.success?(showNotification(e+" restored to "+s,"success"),setTimeout(function(){location.reload()},1500)):showNotification("Restore failed: "+(i.error||"Unknown"),"error")}catch(a){showNotification("Restore error: "+a.message,"error")}}})(),(function(){injectModal("stats-modal",`
\u{1F4CA} Resource Monitor
@@ -1209,10 +1196,10 @@ This will replace current configuration, credentials, and data. Containers will
-
`);const y=document.getElementById("stats-modal"),C=document.getElementById("container-stats-btn"),O=document.getElementById("stats-cancel"),E=document.getElementById("stats-refresh-btn"),M=document.getElementById("stats-auto-refresh"),R=document.getElementById("stats-container"),P=document.getElementById("stats-aggregated-container"),A=document.getElementById("stats-alerts-container"),p=document.getElementById("stats-last-update");let T=null,x=null;function $(l){if(l===0||!l)return"0 B";const w=1024,m=["B","KB","MB","GB"],n=Math.floor(Math.log(l)/Math.log(w));return parseFloat((l/Math.pow(w,n)).toFixed(1))+" "+m[n]}function k(l){return l<30?"#2ecc71":l<70?"#f39c12":"#e74c3c"}function H(l){return l<50?"#2ecc71":l<80?"#f39c12":"#e74c3c"}async function L(){try{let l=null,w=!1;try{const e=await(await fetch("/api/v1/monitoring/stats")).json();e.success&&e.stats&&(l=e.stats,w=!0,x=e.stats)}catch{}if(!w){const e=await(await fetch("/api/v1/stats/containers")).json();if(e.success&&e.stats){l={};for(const t of e.stats)l[t.name]={name:t.name,current:{cpu:t.cpu,memory:{percent:t.memory.percent,usage:t.memory.used,limit:t.memory.limit,usageMB:Math.round(t.memory.used/1048576),limitMB:Math.round(t.memory.limit/1048576)},network:{rxBytes:t.network.rx,txBytes:t.network.tx,rxMB:(t.network.rx/1048576).toFixed(1),txMB:(t.network.tx/1048576).toFixed(1)},disk:{readMB:0,writeMB:0}},status:t.status};x=l}}if(!l||Object.keys(l).length===0){R.innerHTML='
';try{const e=await(await fetch(`/api/v1/monitoring/history/${encodeURIComponent(l)}?startTime=${m}&endTime=${w}`)).json();if(!e.success)throw new Error(e.error||"Failed to load history");const t=e.samples||[],o=e.tier||"raw";if(t.length===0){D.innerHTML=`
\u{1F4CA}No data for the last ${f}. Tier: ${b(o)}.
`;return}const r=o==="raw",a=r?U=>U.cpu?.percent:U=>U.cpu?.avg,s=r?U=>U.memory?.percent:U=>U.memory?.avgPercent,d=r?U=>U.network?.rxMB||0:U=>U.network?.rxMB||0,S=r?U=>U.network?.txMB||0:U=>U.network?.txMB||0;let h=`
+ `}function r(){if(!A)return;const d=w||{},x=A.value,y=Object.entries(d);if(y.length===0){A.innerHTML='';return}A.innerHTML=y.map(([t,e])=>``).join(""),x&&d[x]&&(A.value=x)}async function c(){if(!U||!A)return;const d=A.value;if(!d){U.innerHTML='
';try{const e=await(await fetch(`/api/v1/monitoring/history/${encodeURIComponent(d)}?startTime=${y}&endTime=${x}`)).json();if(!e.success)throw new Error(e.error||"Failed to load history");const s=e.samples||[],n=e.tier||"raw";if(s.length===0){U.innerHTML=`
\u{1F4CA}No data for the last ${u}. Tier: ${g(n)}.
@@ -1378,7 +1365,7 @@ This will replace current configuration, credentials, and data. Containers will
\u{1F30D} Global Settings
Applies to all health checks. Settings are stored locally in this browser.
-
+
@@ -1389,6 +1376,11 @@ This will replace current configuration, credentials, and data. Containers will
How often each service is checked.
+
+
+
+
Cap on stored history records per service.
+
@@ -1455,24 +1447,24 @@ This will replace current configuration, credentials, and data. Containers will
-
`);const y=document.getElementById("health-modal"),C=document.getElementById("health-check-btn"),O=document.getElementById("health-cancel"),E=document.getElementById("health-refresh-btn"),M=document.getElementById("health-status-container"),R=document.getElementById("health-incidents-container"),P=document.getElementById("health-config-container"),A=document.getElementById("health-last-update"),p=document.getElementById("health-add-btn"),T=document.getElementById("health-config-form"),x=document.getElementById("health-form-title"),$=document.getElementById("health-form-cancel"),k=document.getElementById("health-form-save"),H="dashcaddy-health-settings",L={retentionDays:30,pollingInterval:30,diskUsageThreshold:80},u=document.getElementById("health-global-save"),j=document.getElementById("health-global-reset"),v=document.getElementById("health-global-status"),N=document.getElementById("health-setting-retention"),g=document.getElementById("health-setting-interval"),D=document.getElementById("health-setting-disk-threshold");function B(){try{const t=safeGet(H),o=t?JSON.parse(t):{};return Object.assign({},L,o)}catch{return Object.assign({},L)}}function f(){const t=B();N&&(N.value=t.retentionDays),g&&(g.value=t.pollingInterval),D&&(D.value=t.diskUsageThreshold)}function I(){const t={retentionDays:Math.max(1,Math.min(3650,parseInt(N?.value)||L.retentionDays)),pollingInterval:Math.max(5,Math.min(3600,parseInt(g?.value)||L.pollingInterval)),diskUsageThreshold:Math.max(50,Math.min(99,parseInt(D?.value)||L.diskUsageThreshold))};try{safeSet(H,JSON.stringify(t)),f(),v&&(v.textContent="Saved \u2713",v.style.color="var(--ok-fg)",setTimeout(()=>{v&&(v.textContent="")},2500)),typeof showNotification=="function"&&showNotification("Global health settings saved","success")}catch(o){v&&(v.textContent="Save failed",v.style.color="var(--bad-fg)"),typeof showNotification=="function"&&showNotification("Failed to save settings: "+o.message,"error")}}function b(){try{safeSet(H,JSON.stringify(L))}catch{}f(),v&&(v.textContent="Reset to defaults \u2713",v.style.color="var(--ok-fg)",setTimeout(()=>{v&&(v.textContent="")},2500))}f(),u?.addEventListener("click",I),j?.addEventListener("click",b);let z=null;function i(t){return t>=99.9?"var(--ok-fg)":t>=95?"#f39c12":"var(--bad-fg)"}function c(t){const o={critical:"var(--bad-fg)",high:"#ff6b6b",medium:"#f39c12",low:"var(--muted)"};return`${t}`}async function l(){try{const o=await(await fetch("/api/v1/health-checks/status")).json();if(!o.success||!o.status||Object.keys(o.status).length===0){M.innerHTML='
\u{1F3E5}No health checks configured. Go to the Configure tab to add services.
';return}const r=Object.values(o.status);let a='
';a+='
',a+='
Service
Status
',a+='
Uptime 24h
Uptime 7d
',a+='
Avg Response
Last Check
';for(const s of r){const d=s.status==="up",S=d?"var(--dot-ok)":"var(--dot-bad)",h=s.uptime?.["24h"]??"-",U=s.uptime?.["7d"]??"-",_=s.avgResponseTime!=null?Math.round(s.avgResponseTime)+"ms":"-",F=s.timestamp?timeAgo(s.timestamp):"-";a+=`
`);const h=document.getElementById("health-modal"),L=document.getElementById("health-check-btn"),R=document.getElementById("health-cancel"),S=document.getElementById("health-refresh-btn"),$=document.getElementById("health-status-container"),P=document.getElementById("health-incidents-container"),N=document.getElementById("health-config-container"),D=document.getElementById("health-last-update"),m=document.getElementById("health-add-btn"),T=document.getElementById("health-config-form"),w=document.getElementById("health-form-title"),B=document.getElementById("health-form-cancel"),E=document.getElementById("health-form-save"),H="dashcaddy-health-settings",C={retentionDays:30,pollingInterval:30,maxEntriesPerService:500,diskUsageThreshold:80},v=document.getElementById("health-global-save"),O=document.getElementById("health-global-reset"),f=document.getElementById("health-global-status"),z=document.getElementById("health-setting-retention"),A=document.getElementById("health-setting-interval"),U=document.getElementById("health-setting-max-entries"),p=document.getElementById("health-setting-disk-threshold");function u(){try{const n=safeGet(H),i=n?JSON.parse(n):{};return Object.assign({},C,i)}catch{return Object.assign({},C)}}function b(){const n=u();z&&(z.value=n.retentionDays),A&&(A.value=n.pollingInterval),U&&(U.value=n.maxEntriesPerService),p&&(p.value=n.diskUsageThreshold)}function g(){const n={retentionDays:Math.max(1,Math.min(3650,parseInt(z?.value)||C.retentionDays)),pollingInterval:Math.max(5,Math.min(3600,parseInt(A?.value)||C.pollingInterval)),maxEntriesPerService:Math.max(10,Math.min(1e5,parseInt(U?.value)||C.maxEntriesPerService)),diskUsageThreshold:Math.max(50,Math.min(99,parseInt(p?.value)||C.diskUsageThreshold))};try{safeSet(H,JSON.stringify(n)),b(),f&&(f.textContent="Saved \u2713",f.style.color="var(--ok-fg)",setTimeout(()=>{f&&(f.textContent="")},2500)),typeof showNotification=="function"&&showNotification("Global health settings saved","success")}catch(i){f&&(f.textContent="Save failed",f.style.color="var(--bad-fg)"),typeof showNotification=="function"&&showNotification("Failed to save settings: "+i.message,"error")}}function M(){try{safeSet(H,JSON.stringify(C))}catch{}b(),f&&(f.textContent="Reset to defaults \u2713",f.style.color="var(--ok-fg)",setTimeout(()=>{f&&(f.textContent="")},2500))}b(),v?.addEventListener("click",g),O?.addEventListener("click",M);let r=null;function c(n){return n>=99.9?"var(--ok-fg)":n>=95?"#f39c12":"var(--bad-fg)"}function d(n){const i={critical:"var(--bad-fg)",high:"#ff6b6b",medium:"#f39c12",low:"var(--muted)"};return`${n}`}async function x(){try{const i=await(await fetch("/api/v1/health-checks/status")).json();if(!i.success||!i.status||Object.keys(i.status).length===0){$.innerHTML='
\u{1F3E5}No health checks configured. Go to the Configure tab to add services.
';return}const a=Object.values(i.status);let o='
';o+='
',o+='
Service
Status
',o+='
Uptime 24h
Uptime 7d
',o+='
Avg Response
Last Check
';for(const l of a){const k=l.status==="up",I=k?"var(--dot-ok)":"var(--dot-bad)",j=l.uptime?.["24h"]??"-",_=l.uptime?.["7d"]??"-",F=l.avgResponseTime!=null?Math.round(l.avgResponseTime)+"ms":"-",q=l.timestamp?timeAgo(l.timestamp):"-";o+=`
';for(const h of S){const U=h.status==="resolved",_=U&&h.duration?h.duration<6e4?Math.round(h.duration/1e3)+"s":Math.round(h.duration/6e4)+"m":"-";s+='
',s+=`
${escapeHtml(h.serviceId)}
`,s+=`
${escapeHtml(h.type)}
`,s+=`
${c(h.severity)}
`,s+=`
${h.status}
`,s+=`
${_}
`,s+=`
${timeAgo(h.createdAt)}
`,s+="
"}s+="
"}R.innerHTML=s||'
\u{1F6A8}No incidents recorded yet.
'}catch(t){R.innerHTML=`
Failed: ${escapeHtml(t.message)}
`}}async function m(){try{const o=await(await fetch("/api/v1/health-checks/status")).json(),r=o.success&&o.status?Object.values(o.status):[];if(r.length===0){P.innerHTML='
\u2699\uFE0FNo health checks configured yet. Click "Add Health Check" below.
';return}let a='
';a+='
Service
Status
SLA Target
Actions
';for(const s of r){const d=s.status==="up";a+='
',a+=`
${escapeHtml(s.name||s.serviceId)}
`,a+=`
${d?"Up":"Down"}
`,a+=`
${s.sla?.target?s.sla.target+"%":"-"}
`,a+='
',a+=``,a+=``,a+="
"}a+="
",P.innerHTML=a}catch(t){P.innerHTML=`
Failed: ${escapeHtml(t.message)}
`}}function n(t,o,r,a,s,d,S){z=t||null,x.textContent=t?"Edit Health Check":"Add Health Check",document.getElementById("health-form-id").value=t||"",document.getElementById("health-form-id").disabled=!!t,document.getElementById("health-form-name").value=o||"",document.getElementById("health-form-url").value=r||"",document.getElementById("health-form-timeout").value=a||1e4,document.getElementById("health-form-codes").value=s||"200",document.getElementById("health-form-sla").value=d||99.9,document.getElementById("health-form-slow").value=S||5e3,T.style.display="",p.style.display="none"}function e(){T.style.display="none",p.style.display="",z=null}p?.addEventListener("click",()=>n("","","",1e4,"200",99.9,5e3)),$?.addEventListener("click",e),k?.addEventListener("click",async()=>{const t=z||document.getElementById("health-form-id").value.trim();if(!t)return showNotification("Service ID is required","warning");const o=document.getElementById("health-form-url").value.trim();if(!o)return showNotification("URL is required","warning");const r=document.getElementById("health-form-codes").value.split(",").map(s=>parseInt(s.trim())).filter(Boolean),a={name:document.getElementById("health-form-name").value.trim()||t,url:o,timeout:parseInt(document.getElementById("health-form-timeout").value)||1e4,expectedStatusCodes:r.length?r:[200],sla:{target:parseFloat(document.getElementById("health-form-sla").value)||99.9},slowResponseThreshold:parseInt(document.getElementById("health-form-slow").value)||5e3};try{k.textContent="Saving...",k.disabled=!0;const d=await(await secureFetch(`/api/v1/health-checks/${encodeURIComponent(t)}/configure`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(a)})).json();if(!d.success)throw new Error(d.error||"Save failed");e(),m(),l()}catch(s){showNotification("Error: "+s.message,"error")}finally{k.textContent="Save",k.disabled=!1}}),document.addEventListener("health-edit",async t=>{const o=t.detail;n(o,"","",1e4,"200",99.9,5e3)}),document.addEventListener("health-delete",async t=>{const o=t.detail;if(confirm(`Delete health check for "${o}"?`))try{const a=await(await secureFetch(`/api/v1/health-checks/${encodeURIComponent(o)}/configure`,{method:"DELETE"})).json();if(!a.success)throw new Error(a.error);m(),l()}catch(r){showNotification("Error: "+r.message,"error")}}),C?.addEventListener("click",()=>{y?.classList.add("show"),l()}),wireModal(y,O),E?.addEventListener("click",l),document.querySelector('[data-panel="health-incidents"]')?.addEventListener("click",w),document.querySelector('[data-panel="health-config"]')?.addEventListener("click",m)})(),(function(){injectModal("updates-modal",`
+
${escapeHtml(j.message)}
+
Started ${timeAgo(j.createdAt)} \xB7 ${j.occurrences||1} occurrence(s)
';for(const j of I){const _=j.status==="resolved",F=_&&j.duration?j.duration<6e4?Math.round(j.duration/1e3)+"s":Math.round(j.duration/6e4)+"m":"-";l+='
',l+=`
${escapeHtml(j.serviceId)}
`,l+=`
${escapeHtml(j.type)}
`,l+=`
${d(j.severity)}
`,l+=`
${j.status}
`,l+=`
${F}
`,l+=`
${timeAgo(j.createdAt)}
`,l+="
"}l+="
"}P.innerHTML=l||'
\u{1F6A8}No incidents recorded yet.
'}catch(n){P.innerHTML=`
Failed: ${escapeHtml(n.message)}
`}}async function t(){try{const i=await(await fetch("/api/v1/health-checks/status")).json(),a=i.success&&i.status?Object.values(i.status):[];if(a.length===0){N.innerHTML='
\u2699\uFE0FNo health checks configured yet. Click "Add Health Check" below.
';return}let o='
';o+='
Service
Status
SLA Target
Actions
';for(const l of a){const k=l.status==="up";o+='
',o+=`
${escapeHtml(l.name||l.serviceId)}
`,o+=`
${k?"Up":"Down"}
`,o+=`
${l.sla?.target?l.sla.target+"%":"-"}
`,o+='
',o+=``,o+=``,o+="
"}o+="
",N.innerHTML=o}catch(n){N.innerHTML=`
Failed: ${escapeHtml(n.message)}
`}}function e(n,i,a,o,l,k,I){r=n||null,w.textContent=n?"Edit Health Check":"Add Health Check",document.getElementById("health-form-id").value=n||"",document.getElementById("health-form-id").disabled=!!n,document.getElementById("health-form-name").value=i||"",document.getElementById("health-form-url").value=a||"",document.getElementById("health-form-timeout").value=o||1e4,document.getElementById("health-form-codes").value=l||"200",document.getElementById("health-form-sla").value=k||99.9,document.getElementById("health-form-slow").value=I||5e3,T.style.display="",m.style.display="none"}function s(){T.style.display="none",m.style.display="",r=null}m?.addEventListener("click",()=>e("","","",1e4,"200",99.9,5e3)),B?.addEventListener("click",s),E?.addEventListener("click",async()=>{const n=r||document.getElementById("health-form-id").value.trim();if(!n)return showNotification("Service ID is required","warning");const i=document.getElementById("health-form-url").value.trim();if(!i)return showNotification("URL is required","warning");const a=document.getElementById("health-form-codes").value.split(",").map(l=>parseInt(l.trim())).filter(Boolean),o={name:document.getElementById("health-form-name").value.trim()||n,url:i,timeout:parseInt(document.getElementById("health-form-timeout").value)||1e4,expectedStatusCodes:a.length?a:[200],sla:{target:parseFloat(document.getElementById("health-form-sla").value)||99.9},slowResponseThreshold:parseInt(document.getElementById("health-form-slow").value)||5e3};try{E.textContent="Saving...",E.disabled=!0;const k=await(await secureFetch(`/api/v1/health-checks/${encodeURIComponent(n)}/configure`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)})).json();if(!k.success)throw new Error(k.error||"Save failed");s(),t(),x()}catch(l){showNotification("Error: "+l.message,"error")}finally{E.textContent="Save",E.disabled=!1}}),document.addEventListener("health-edit",async n=>{const i=n.detail;e(i,"","",1e4,"200",99.9,5e3)}),document.addEventListener("health-delete",async n=>{const i=n.detail;if(confirm(`Delete health check for "${i}"?`))try{const o=await(await secureFetch(`/api/v1/health-checks/${encodeURIComponent(i)}/configure`,{method:"DELETE"})).json();if(!o.success)throw new Error(o.error);t(),x()}catch(a){showNotification("Error: "+a.message,"error")}}),L?.addEventListener("click",()=>{h?.classList.add("show"),x()}),wireModal(h,R),S?.addEventListener("click",x),document.querySelector('[data-panel="health-incidents"]')?.addEventListener("click",y),document.querySelector('[data-panel="health-config"]')?.addEventListener("click",t)})(),(function(){injectModal("updates-modal",`
\u2B06\uFE0F Update Management
@@ -1546,17 +1538,17 @@ This will replace current configuration, credentials, and data. Containers will
-
`);const y=document.getElementById("updates-modal"),C=document.getElementById("updates-btn"),O=document.getElementById("updates-cancel"),E=document.getElementById("updates-check-btn"),M=document.getElementById("updates-available-container"),R=document.getElementById("updates-history-container"),P=document.getElementById("updates-auto-container"),A=document.getElementById("updates-last-check");async function p(){try{const n=await(await fetch("/api/v1/updates/available")).json();if(!n.success)throw new Error(n.error);const e=n.updates||[];if(e.length===0){M.innerHTML='
';for(const a of e){const s=(()=>{const d=window.APPS||[];for(const S of d)if(S.containerId===a.containerId||S.name===a.containerName||S.id===a.containerName)return S.id;return a.containerName})();t+=`
`,t+=`
${escapeHtml(a.containerName)}
`,t+=`
${escapeHtml(a.imageName)}
`,t+=`
${escapeHtml(a.currentDigest)}
`,t+=`
${escapeHtml(a.latestDigest)}
`,t+='
',t+=``,t+=``,t+="
"}t+="
",M.innerHTML=t,A.textContent=e.length+" update(s) available";const o=document.getElementById("updates-count-badge"),r=document.getElementById("updates-update-all-btn");o&&(o.textContent=e.length+" pending",o.style.display=""),r&&e.length>0&&(r.style.display=""),window._pendingUpdates=e,M.querySelectorAll(".update-now-btn").forEach(a=>{a.addEventListener("click",async()=>{const s=a.dataset.id,d=a.dataset.name;if(confirm(`Update "${d}" 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(s)}`,{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(()=>p(),2e3);else throw new Error(h.error||"Update failed")}catch(S){a.textContent="Failed",a.style.color="var(--bad-fg)",showNotification("Update error: "+S.message,"error"),setTimeout(()=>{a.textContent="Update",a.disabled=!1,a.style.color="",a.style.background=""},3e3)}}})}),M.querySelectorAll(".rollback-btn").forEach(a=>{a.addEventListener("click",async()=>{const s=a.dataset.id,d=a.dataset.name;if(confirm(`Rollback "${d}" to its previous version?`)){a.textContent="Rolling back...",a.disabled=!0;try{const h=await(await secureFetch(`/api/v1/updates/rollback/${encodeURIComponent(s)}`,{method:"POST"})).json();if(h.success)a.textContent="Rolled back!",setTimeout(()=>p(),2e3);else throw new Error(h.error||"Rollback failed")}catch(S){a.textContent="Failed",showNotification("Rollback error: "+S.message,"error"),setTimeout(()=>{a.textContent="Rollback",a.disabled=!1},3e3)}}})})}catch(m){M.innerHTML=`
Failed: ${escapeHtml(m.message)}
`}}async function T(){const m=window._pendingUpdates||[];if(!m.length)return;const n=document.getElementById("updates-update-all-btn");if(!confirm(`Update all ${m.length} containers? Each will restart.`))return;n.textContent="\u23F3 Updating...",n.disabled=!0;let e=0,t=0;for(const o of m)try{(await(await secureFetch(`/api/v1/updates/update/${encodeURIComponent(o.containerId)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({autoRollback:!0})})).json()).success?e++:t++}catch{t++}n.textContent="\u2705 Done",showNotification(`Update all: ${e} succeeded, ${t} failed.`,e>0&&t===0?"success":"error"),setTimeout(()=>{n.textContent="\u2B06\uFE0F Update All",n.disabled=!1,p()},3e3)}document.getElementById("updates-update-all-btn")?.addEventListener("click",T);async function x(){E.textContent="\u{1F50D} Checking...",E.disabled=!0;try{const n=await(await secureFetch("/api/v1/updates/check",{method:"POST"})).json();if(!n.success)throw new Error(n.error);E.textContent="\u2705 Done!",await p()}catch(m){E.textContent="\u274C Failed",showNotification("Check error: "+m.message,"error")}setTimeout(()=>{E.textContent="\u{1F50D} Check for Updates",E.disabled=!1},3e3)}async function $(){try{R.innerHTML='
Auto-updates run during maintenance window (default 2AM-4AM). Daily = every day, Weekly = Sundays, Monthly = 1st of month.
';a+='
',a+='
Container
Schedule
Window
Rollback
Last Run
Actions
';for(const s of o){const d=s.name||s.Names?.[0]?.replace(/^\//,"")||s.Id?.substring(0,12),S=s.containerId||s.Id,h=r[S]||{},U=h.enabled?h.schedule||"weekly":"",_=h.autoRollback!==!1,F=h.maintenanceWindow||"",W=h.lastAutoUpdate?timeAgo(h.lastAutoUpdate):"Never";a+=`
`}}const H=document.getElementById("dashcaddy-current-version"),L=document.getElementById("dashcaddy-update-badge"),u=document.getElementById("dashcaddy-update-details"),j=document.getElementById("dashcaddy-new-version"),v=document.getElementById("dashcaddy-changelog"),N=document.getElementById("dashcaddy-apply-btn"),g=document.getElementById("dashcaddy-check-btn"),D=document.getElementById("dashcaddy-rollback-btn"),B=document.getElementById("dashcaddy-status-bar"),f=document.getElementById("dashcaddy-history-container");let I=null;function b(m,n){B&&(B.style.display="block",B.style.background=n==="error"?"var(--bad-bg)":n==="success"?"var(--ok-bg)":"var(--bg)",B.style.color=n==="error"?"var(--bad-fg)":n==="success"?"var(--ok-fg)":"var(--fg)",B.textContent=m)}async function z(){try{const n=await(await fetch("/api/v1/system/version")).json();if(n.success){const e=n.commit&&n.commit!=="unknown"?n.commit:null;H.textContent="v"+n.version+(e?" ("+e.substring(0,7)+")":"")}}catch{H.textContent="Unable to fetch version"}}async function i(m){m||(g.textContent="Checking...",g.disabled=!0);try{const e=await(await fetch("/api/v1/system/update-check")).json();if(I=e,e.success&&e.available&&e.remote){L.style.display="",u.style.display="",j.textContent="v"+e.remote.version,v.textContent=e.remote.changelog||"No changelog available.";const t=document.getElementById("updates-btn");if(t&&!t.querySelector(".update-dot")){const r=document.createElement("span");r.className="update-dot",r.style.cssText="position:absolute;top:2px;right:2px;width:8px;height:8px;border-radius:50%;background:var(--accent);",t.style.position="relative",t.appendChild(r)}const o=document.getElementById("updates-dashcaddy-tab");if(o&&!o.querySelector(".update-dot")){const r=document.createElement("span");r.className="update-dot",r.style.cssText="display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--accent);margin-left:4px;vertical-align:middle;",o.appendChild(r)}}else L.style.display="none",u.style.display="none",await z(),m||b("You are running the latest version.","success");m||(g.textContent="Check for Updates",g.disabled=!1)}catch(n){m||(b("Failed to check: "+n.message,"error"),g.textContent="Check for Updates",g.disabled=!1)}}async function c(){if(!confirm("Apply DashCaddy update? The API container will restart."))return!1;N.textContent="Updating...",N.disabled=!0,b("Downloading and applying update...","info");try{const n=await(await secureFetch("/api/v1/system/update-apply",{method:"POST"})).json();if(n.success)return b("Update initiated: v"+(n.fromVersion||"?")+" \u2192 v"+(n.toVersion||"?")+". The container will restart shortly.","success"),N.textContent="Applied!",document.querySelectorAll(".update-dot").forEach(e=>e.remove()),!0;throw new Error(n.error||"Update failed")}catch(m){throw b("Update failed: "+m.message,"error"),N.textContent="Update Now",N.disabled=!1,m}}async function l(){try{const n=await(await fetch("/api/v1/system/update-history")).json(),e=n.success&&n.history?n.history:[];if(e.length===0){f.innerHTML='
\u{1F4E6}No self-update history.
';return}let t='
';t+='
When
Version
From
Status
';for(const o of e){const r=o.status==="success"?"\u2713 success":o.status==="pending"?"\u23F3 pending":o.status==="partial"?"\u26A0 partial":"\u2717 "+o.status,a=o.status==="success"?"var(--ok-fg)":o.status==="pending"?"var(--muted)":"var(--bad-fg)";t+='
`);const h=document.getElementById("updates-modal"),L=document.getElementById("updates-btn"),R=document.getElementById("updates-cancel"),S=document.getElementById("updates-check-btn"),$=document.getElementById("updates-available-container"),P=document.getElementById("updates-history-container"),N=document.getElementById("updates-auto-container"),D=document.getElementById("updates-last-check");async function m(){try{const t=await(await fetch("/api/v1/updates/available")).json();if(!t.success)throw new Error(t.error);const e=t.updates||[];if(e.length===0){$.innerHTML='
';for(const a of e){const o=(()=>{const l=window.APPS||[];for(const k of l)if(k.containerId===a.containerId||k.name===a.containerName||k.id===a.containerName)return k.id;return a.containerName})();s+=`
`,s+=`
${escapeHtml(a.containerName)}
`,s+=`
${escapeHtml(a.imageName)}
`,s+=`
${escapeHtml(a.currentDigest)}
`,s+=`
${escapeHtml(a.latestDigest)}
`,s+='
',s+=``,s+=``,s+="
"}s+="
",$.innerHTML=s,D.textContent=e.length+" update(s) available";const n=document.getElementById("updates-count-badge"),i=document.getElementById("updates-update-all-btn");n&&(n.textContent=e.length+" pending",n.style.display=""),i&&e.length>0&&(i.style.display=""),window._pendingUpdates=e,$.querySelectorAll(".update-now-btn").forEach(a=>{a.addEventListener("click",async()=>{const o=a.dataset.id,l=a.dataset.name;if(confirm(`Update "${l}" to the latest version? The container will restart.`)){a.textContent="Updating...",a.disabled=!0;try{const I=await(await secureFetch(`/api/v1/updates/update/${encodeURIComponent(o)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({autoRollback:!0})})).json();if(I.success)a.textContent="Done!",a.style.background="var(--ok-fg)",setTimeout(()=>m(),2e3);else throw new Error(I.error||"Update failed")}catch(k){a.textContent="Failed",a.style.color="var(--bad-fg)",showNotification("Update error: "+k.message,"error"),setTimeout(()=>{a.textContent="Update",a.disabled=!1,a.style.color="",a.style.background=""},3e3)}}})}),$.querySelectorAll(".rollback-btn").forEach(a=>{a.addEventListener("click",async()=>{const o=a.dataset.id,l=a.dataset.name;if(confirm(`Rollback "${l}" to its previous version?`)){a.textContent="Rolling back...",a.disabled=!0;try{const I=await(await secureFetch(`/api/v1/updates/rollback/${encodeURIComponent(o)}`,{method:"POST"})).json();if(I.success)a.textContent="Rolled back!",setTimeout(()=>m(),2e3);else throw new Error(I.error||"Rollback failed")}catch(k){a.textContent="Failed",showNotification("Rollback error: "+k.message,"error"),setTimeout(()=>{a.textContent="Rollback",a.disabled=!1},3e3)}}})})}catch(y){$.innerHTML=`
Failed: ${escapeHtml(y.message)}
`}}async function T(){const y=window._pendingUpdates||[];if(!y.length)return;const t=document.getElementById("updates-update-all-btn");if(!confirm(`Update all ${y.length} containers? Each will restart.`))return;t.textContent="\u23F3 Updating...",t.disabled=!0;let e=0,s=0;for(const n of y)try{(await(await secureFetch(`/api/v1/updates/update/${encodeURIComponent(n.containerId)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({autoRollback:!0})})).json()).success?e++:s++}catch{s++}t.textContent="\u2705 Done",showNotification(`Update all: ${e} succeeded, ${s} failed.`,e>0&&s===0?"success":"error"),setTimeout(()=>{t.textContent="\u2B06\uFE0F Update All",t.disabled=!1,m()},3e3)}document.getElementById("updates-update-all-btn")?.addEventListener("click",T);async function w(){S.textContent="\u{1F50D} Checking...",S.disabled=!0;try{const t=await(await secureFetch("/api/v1/updates/check",{method:"POST"})).json();if(!t.success)throw new Error(t.error);S.textContent="\u2705 Done!",await m()}catch(y){S.textContent="\u274C Failed",showNotification("Check error: "+y.message,"error")}setTimeout(()=>{S.textContent="\u{1F50D} Check for Updates",S.disabled=!1},3e3)}async function B(){try{P.innerHTML='
Auto-updates run during maintenance window (default 2AM-4AM). Daily = every day, Weekly = Sundays, Monthly = 1st of month.
';a+='
',a+='
Container
Schedule
Window
Rollback
Last Run
Actions
';for(const o of n){const l=o.name||o.Names?.[0]?.replace(/^\//,"")||o.Id?.substring(0,12),k=o.containerId||o.Id,I=i[k]||{},j=I.enabled?I.schedule||"weekly":"",_=I.autoRollback!==!1,F=I.maintenanceWindow||"",q=I.lastAutoUpdate?timeAgo(I.lastAutoUpdate):"Never";a+=`
`}}const H=document.getElementById("dashcaddy-current-version"),C=document.getElementById("dashcaddy-update-badge"),v=document.getElementById("dashcaddy-update-details"),O=document.getElementById("dashcaddy-new-version"),f=document.getElementById("dashcaddy-changelog"),z=document.getElementById("dashcaddy-apply-btn"),A=document.getElementById("dashcaddy-check-btn"),U=document.getElementById("dashcaddy-rollback-btn"),p=document.getElementById("dashcaddy-status-bar"),u=document.getElementById("dashcaddy-history-container");let b=null;function g(y,t){p&&(p.style.display="block",p.style.background=t==="error"?"var(--bad-bg)":t==="success"?"var(--ok-bg)":"var(--bg)",p.style.color=t==="error"?"var(--bad-fg)":t==="success"?"var(--ok-fg)":"var(--fg)",p.textContent=y)}async function M(){try{const t=await(await fetch("/api/v1/system/version")).json();if(t.success){const e=t.commit&&t.commit!=="unknown"?t.commit:null;H.textContent="v"+t.version+(e?" ("+e.substring(0,7)+")":"")}}catch{H.textContent="Unable to fetch version"}}async function r(y){y||(A.textContent="Checking...",A.disabled=!0);try{const e=await(await fetch("/api/v1/system/update-check")).json();if(b=e,e.success&&e.available&&e.remote){C.style.display="",v.style.display="",O.textContent="v"+e.remote.version,f.textContent=e.remote.changelog||"No changelog available.";const s=document.getElementById("updates-btn");if(s&&!s.querySelector(".update-dot")){const i=document.createElement("span");i.className="update-dot",i.style.cssText="position:absolute;top:2px;right:2px;width:8px;height:8px;border-radius:50%;background:var(--accent);",s.style.position="relative",s.appendChild(i)}const n=document.getElementById("updates-dashcaddy-tab");if(n&&!n.querySelector(".update-dot")){const i=document.createElement("span");i.className="update-dot",i.style.cssText="display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--accent);margin-left:4px;vertical-align:middle;",n.appendChild(i)}}else C.style.display="none",v.style.display="none",await M(),y||g("You are running the latest version.","success");y||(A.textContent="Check for Updates",A.disabled=!1)}catch(t){y||(g("Failed to check: "+t.message,"error"),A.textContent="Check for Updates",A.disabled=!1)}}async function c(){if(!confirm("Apply DashCaddy update? The API container will restart."))return!1;z.textContent="Updating...",z.disabled=!0,g("Downloading and applying update...","info");try{const t=await(await secureFetch("/api/v1/system/update-apply",{method:"POST"})).json();if(t.success)return g("Update initiated: v"+(t.fromVersion||"?")+" \u2192 v"+(t.toVersion||"?")+". The container will restart shortly.","success"),z.textContent="Applied!",document.querySelectorAll(".update-dot").forEach(e=>e.remove()),!0;throw new Error(t.error||"Update failed")}catch(y){throw g("Update failed: "+y.message,"error"),z.textContent="Update Now",z.disabled=!1,y}}async function d(){try{const t=await(await fetch("/api/v1/system/update-history")).json(),e=t.success&&t.history?t.history:[];if(e.length===0){u.innerHTML='
\u{1F4E6}No self-update history.
';return}let s='
';s+='
When
Version
From
Status
';for(const n of e){const i=n.status==="success"?"\u2713 success":n.status==="pending"?"\u23F3 pending":n.status==="partial"?"\u26A0 partial":"\u2717 "+n.status,a=n.status==="success"?"var(--ok-fg)":n.status==="pending"?"var(--muted)":"var(--bad-fg)";s+='
"}}async function x(){try{const t=await(await fetch("/api/v1/system/rollback-versions")).json(),e=t.success&&t.versions?t.versions:[];if(e.length===0){showNotification("No rollback versions available.","info");return}const s=prompt(`Available rollback versions:
`+e.join(`
`)+`
-Enter version to rollback to:`);if(!t)return;if(!e.includes(t)){showNotification("Invalid version: "+t,"error");return}if(!confirm("Rollback DashCaddy to v"+t+"? The container will restart."))return;b("Rolling back to v"+t+"...","info");const r=await(await secureFetch("/api/v1/system/rollback",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({version:t})})).json();if(r.success)b("Rollback to v"+t+" initiated. Container will restart.","success");else throw new Error(r.error||"Rollback failed")}catch(m){b("Rollback failed: "+m.message,"error")}}g?.addEventListener("click",()=>i(!1)),N?.addEventListener("click",()=>c().catch(()=>{})),D?.addEventListener("click",w),E?.addEventListener("click",x),C?.addEventListener("click",()=>{y?.classList.add("show"),p()}),wireModal(y,O),window.openUpdateModal=function(m){y?.classList.add("show"),p().then(()=>{if(!m)return;const n=M.querySelector(`[data-app-id="${m}"]`);n&&(n.scrollIntoView({behavior:"smooth",block:"center"}),n.style.background="rgba(249,115,22,0.15)",setTimeout(()=>{n.style.background=""},3e3))})},document.querySelector('[data-panel="updates-history"]')?.addEventListener("click",$),document.querySelector('[data-panel="updates-auto"]')?.addEventListener("click",k),document.querySelector('[data-panel="updates-dashcaddy"]')?.addEventListener("click",()=>{z(),l(),I||i(!0)}),window.dcApplyUpdate=c,window.dcCheckForUpdate=i,setTimeout(()=>i(!0),5e3)})(),(function(){injectModal("docker-resources-modal",`
+Enter version to rollback to:`);if(!s)return;if(!e.includes(s)){showNotification("Invalid version: "+s,"error");return}if(!confirm("Rollback DashCaddy to v"+s+"? The container will restart."))return;g("Rolling back to v"+s+"...","info");const i=await(await secureFetch("/api/v1/system/rollback",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({version:s})})).json();if(i.success)g("Rollback to v"+s+" initiated. Container will restart.","success");else throw new Error(i.error||"Rollback failed")}catch(y){g("Rollback failed: "+y.message,"error")}}A?.addEventListener("click",()=>r(!1)),z?.addEventListener("click",()=>c().catch(()=>{})),U?.addEventListener("click",x),S?.addEventListener("click",w),L?.addEventListener("click",()=>{h?.classList.add("show"),m()}),wireModal(h,R),window.openUpdateModal=function(y){h?.classList.add("show"),m().then(()=>{if(!y)return;const t=$.querySelector(`[data-app-id="${y}"]`);t&&(t.scrollIntoView({behavior:"smooth",block:"center"}),t.style.background="rgba(249,115,22,0.15)",setTimeout(()=>{t.style.background=""},3e3))})},document.querySelector('[data-panel="updates-history"]')?.addEventListener("click",B),document.querySelector('[data-panel="updates-auto"]')?.addEventListener("click",E),document.querySelector('[data-panel="updates-dashcaddy"]')?.addEventListener("click",()=>{M(),d(),b||r(!0)}),window.dcApplyUpdate=c,window.dcCheckForUpdate=r,setTimeout(()=>r(!0),5e3)})(),(function(){injectModal("docker-resources-modal",`
\u{1F433} Docker Resources
Manage volumes, networks, and view disk usage.
@@ -1605,7 +1597,7 @@ Enter version to rollback to:`);if(!t)return;if(!e.includes(t)){showNotification
",A.innerHTML=x,A.querySelectorAll(".dr-vol-del").forEach($=>{$.addEventListener("click",async()=>{if(confirm(`Delete volume "${$.dataset.name}"? Data will be lost.`)){$.textContent="...",$.disabled=!0;try{await deleteAPI(`/api/v1/docker/volumes/${encodeURIComponent($.dataset.name)}?force=true`),M()}catch(k){showNotification("Delete failed: "+k.message,"error"),$.textContent="Delete",$.disabled=!1}}})})}catch(p){A.innerHTML=`
Failed: ${escapeHtml(p.message)}
`}}document.getElementById("dr-vol-create")?.addEventListener("click",async()=>{const A=document.getElementById("dr-vol-name"),p=A.value.trim();if(!p){showNotification("Enter a volume name","warning");return}try{await postJSON("/api/v1/docker/volumes",{name:p}),A.value="",showNotification(`Volume "${p}" created`,"success"),M()}catch(T){showNotification("Create failed: "+T.message,"error")}});async function R(){const A=document.getElementById("dr-net-list");try{const T=(await getJSON("/api/v1/docker/networks")).networks||[];if(T.length===0){A.innerHTML='
\u{1F310}No networks found.
';return}let x='
';x+='
Name
Driver
Scope
Containers
Actions
';for(const $ of T){const k=["bridge","host","none"].includes($.name);x+='
",D.innerHTML=w,D.querySelectorAll(".dr-vol-del").forEach(B=>{B.addEventListener("click",async()=>{if(confirm(`Delete volume "${B.dataset.name}"? Data will be lost.`)){B.textContent="...",B.disabled=!0;try{await deleteAPI(`/api/v1/docker/volumes/${encodeURIComponent(B.dataset.name)}?force=true`),$()}catch(E){showNotification("Delete failed: "+E.message,"error"),B.textContent="Delete",B.disabled=!1}}})})}catch(m){D.innerHTML=`
Failed: ${escapeHtml(m.message)}
`}}document.getElementById("dr-vol-create")?.addEventListener("click",async()=>{const D=document.getElementById("dr-vol-name"),m=D.value.trim();if(!m){showNotification("Enter a volume name","warning");return}try{await postJSON("/api/v1/docker/volumes",{name:m}),D.value="",showNotification(`Volume "${m}" created`,"success"),$()}catch(T){showNotification("Create failed: "+T.message,"error")}});async function P(){const D=document.getElementById("dr-net-list");try{const T=(await getJSON("/api/v1/docker/networks")).networks||[];if(T.length===0){D.innerHTML='
\u{1F310}No networks found.
';return}let w='
';w+='
Name
Driver
Scope
Containers
Actions
';for(const B of T){const E=["bridge","host","none"].includes(B.name);w+='
';for(const x of p.results){const $=x.status==="deployed"||x.status==="created"?"\u2705":x.status==="exists"?"\u26A1":x.status==="skipped"?"\u23ED":"\u274C";T+='
';for(const w of m.results){const B=w.status==="deployed"||w.status==="created"?"\u2705":w.status==="exists"?"\u26A1":w.status==="skipped"?"\u23ED":"\u274C";T+='
`}}C?.addEventListener("click",()=>{y?.classList.add("show"),x(!1)}),wireModal(y,O),E?.addEventListener("click",()=>x(!1)),R?.addEventListener("change",()=>x(!1)),A?.addEventListener("click",()=>x(!0)),M?.addEventListener("click",async()=>{if(confirm("Clear the entire audit log? This cannot be undone."))try{const k=await(await secureFetch("/api/v1/audit-logs",{method:"DELETE"})).json();k.success?x(!1):showNotification("Error: "+(k.error||"Clear failed"),"error")}catch($){showNotification("Error: "+$.message,"error")}})})(),(function(){injectModal("security-modal",`
+
`);const h=document.getElementById("audit-modal"),L=document.getElementById("audit-log-btn"),R=document.getElementById("audit-cancel"),S=document.getElementById("audit-refresh-btn"),$=document.getElementById("audit-clear-btn"),P=document.getElementById("audit-filter"),N=document.getElementById("audit-log-container"),D=document.getElementById("audit-load-more");let m=0;const T=50;async function w(B){try{B||(m=0,N.innerHTML='
`}}L?.addEventListener("click",()=>{h?.classList.add("show"),w(!1)}),wireModal(h,R),S?.addEventListener("click",()=>w(!1)),P?.addEventListener("change",()=>w(!1)),D?.addEventListener("click",()=>w(!0)),$?.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?w(!1):showNotification("Error: "+(E.error||"Clear failed"),"error")}catch(B){showNotification("Error: "+B.message,"error")}})})(),(function(){injectModal("security-modal",`
\u{1F6E1}\uFE0F Security Center
@@ -1774,39 +1766,39 @@ Enter version to rollback to:`);if(!t)return;if(!e.includes(t)){showNotification
`}async function B(){try{const c=(await(await fetch("/api/v1/security/hosts")).json()).data?.hosts||[],l=u.value;u.innerHTML=''+c.map(w=>``).join(""),l&&(u.value=l)}catch{}}document.getElementById("sec-host-register-btn").addEventListener("click",I),document.getElementById("sec-hosts-refresh").addEventListener("click",f);async function f(){try{const c=(await(await fetch("/api/v1/security/hosts")).json()).data?.hosts||[];P=c;const l=document.getElementById("sec-hosts-container");if(!c.length){l.innerHTML='
No hosts registered. Click \u2795 Register Host to add one.
`}async function p(){try{const c=(await(await fetch("/api/v1/security/hosts")).json()).data?.hosts||[],d=v.value;v.innerHTML=''+c.map(x=>``).join(""),d&&(v.value=d)}catch{}}document.getElementById("sec-host-register-btn").addEventListener("click",b),document.getElementById("sec-hosts-refresh").addEventListener("click",u);async function u(){try{const c=(await(await fetch("/api/v1/security/hosts")).json()).data?.hosts||[];N=c;const d=document.getElementById("sec-hosts-container");if(!c.length){d.innerHTML='
No hosts registered. Click \u2795 Register Host to add one.
`}).join(""),l.querySelectorAll(".sec-host-del").forEach(w=>{w.addEventListener("click",async()=>{confirm(`Remove host ${w.dataset.id}? Events already received will remain in the store.`)&&(await fetch(`/api/v1/security/hosts/${encodeURIComponent(w.dataset.id)}`,{method:"DELETE"}),f())})})}catch(i){document.getElementById("sec-hosts-container").innerHTML='
Load failed: '+b(i.message)+"
"}}async function I(){const i=prompt("Host id (lowercase, no spaces):");if(!i)return;const c=prompt("Display label:",i)||i,l=prompt('Type ("dashcaddy", "service", or "agent"):',"agent")||"agent";try{const w=await fetch("/api/v1/security/hosts",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:i,label:c,type:l})}),m=await w.json();if(!w.ok){alert("Failed: "+(m?.error?.message||w.statusText));return}alert(`\u2705 Host registered!
+ `}).join(""),d.querySelectorAll(".sec-host-del").forEach(x=>{x.addEventListener("click",async()=>{confirm(`Remove host ${x.dataset.id}? Events already received will remain in the store.`)&&(await fetch(`/api/v1/security/hosts/${encodeURIComponent(x.dataset.id)}`,{method:"DELETE"}),u())})})}catch(r){document.getElementById("sec-hosts-container").innerHTML='
Load failed: '+g(r.message)+"
"}}async function b(){const r=prompt("Host id (lowercase, no spaces):");if(!r)return;const c=prompt("Display label:",r)||r,d=prompt('Type ("dashcaddy", "service", or "agent"):',"agent")||"agent";try{const x=await fetch("/api/v1/security/hosts",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:r,label:c,type:d})}),y=await x.json();if(!x.ok){alert("Failed: "+(y?.error?.message||x.statusText));return}alert(`\u2705 Host registered!
-id: ${m.data.host.id}
-label: ${m.data.host.label}
-type: ${m.data.host.type}
+id: ${y.data.host.id}
+label: ${y.data.host.label}
+type: ${y.data.host.type}
\u{1F511} API KEY (save this NOW \u2014 won't be shown again):
-${m.data.api_key}
+${y.data.api_key}
Send this key as: Authorization: Bearer
-To endpoint: POST /api/v1/security/events/ingest or /events/batch`),f()}catch(w){alert("Failed: "+w.message)}}function b(i){return String(i).replace(/[&<>"']/g,c=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[c])}function z(i,c){let l;return function(){clearTimeout(l),l=setTimeout(()=>i.apply(this,arguments),c)}}})(),(function(){const y=new ErrorHandler;injectModal("weather-modal",`
Weather Settings
+To endpoint: POST /api/v1/security/events/ingest or /events/batch`),u()}catch(x){alert("Failed: "+x.message)}}function g(r){return String(r).replace(/[&<>"']/g,c=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[c])}function M(r,c){let d;return function(){clearTimeout(d),d=setTimeout(()=>r.apply(this,arguments),c)}}})(),(function(){const h=new ErrorHandler;injectModal("weather-modal",`
Weather Settings
Enter a city name, postal code, or “City, Country”
@@ -1817,23 +1809,23 @@ To endpoint: POST /api/v1/security/events/ingest or /events/batch`),f()}catch(w)
-
`);const C="weather-location",O="weather-zip",E="weather-geo",M="weather-unit";!safeGet(C)&&safeGet(O)&&safeSet(C,safeGet(O));function R(){return safeGet(M)||"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 A={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"},p={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"},T=["N","NNE","NE","ENE","E","ESE","SE","SSE","S","SSW","SW","WSW","W","WNW","NW","NNW"];function x(v){return T[Math.round(v/22.5)%16]}async function $(v){const N=safeGet(E);if(N)try{const I=JSON.parse(N);if(I.query===v)return I}catch{}const g=await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(v)}&count=1&language=en&format=json`);if(!g.ok)throw new Error("Geocoding failed");const D=await g.json();if(!D.results||!D.results.length)throw new Error("Location not found");const B=D.results[0],f={query:v,lat:B.latitude,lon:B.longitude,city:B.name,state:B.admin1||"",country:B.country||"",countryCode:B.country_code||""};return safeSet(E,JSON.stringify(f)),f}function k(v){return v.countryCode==="US"&&v.state?`${v.city}, ${v.state}`:v.country?`${v.city}, ${v.country}`:v.city}async function H(v){try{const N=await $(v),g=R(),D=g==="metric"?"celsius":"fahrenheit",B=g==="metric"?"kmh":"mph",f=`https://api.open-meteo.com/v1/forecast?latitude=${N.lat}&longitude=${N.lon}¤t=temperature_2m,weather_code,wind_speed_10m,wind_direction_10m&temperature_unit=${D}&wind_speed_unit=${B}`,I=await fetch(f);if(!I.ok)throw new Error("Weather fetch failed");const z=(await I.json()).current,i=z.weather_code;return{temp:Math.round(z.temperature_2m),condition:A[i]||"Unknown",icon:p[i]||"\u{1F324}\uFE0F",locationStr:k(N),windSpeed:Math.round(z.wind_speed_10m),windDir:x(z.wind_direction_10m),unit:g}}catch(N){return console.warn("Weather fetch failed:",N),null}}async function L(){const v=P();if(!v.icon||!v.temp||!v.condition||!v.location||!v.wind){console.warn("Weather widget elements not found");return}const N=safeGet(C);if(!N){v.location.textContent="Set Location",v.temp.textContent="--\xB0",v.condition.textContent="Click \u2699\uFE0F to configure",v.wind.textContent="--",v.icon.innerHTML='\u{1F324}\uFE0F';return}try{const g=await H(N);if(g){const D=g.unit==="metric"?"\xB0C":"\xB0F",B=g.unit==="metric"?"km/h":"mph";v.location.textContent=g.locationStr,v.temp.textContent=`${g.temp}${D}`,v.condition.textContent=g.condition,v.wind.textContent=`Wind: ${g.windSpeed} ${B} ${g.windDir}`,v.icon.innerHTML=`${escapeHtml(g.icon)}`}}catch(g){y.logError("[Weather] Update Error",g,{function:"updateWeather"}),v.location.textContent="Weather Error",v.temp.textContent="Error",v.condition.textContent="Failed to load",v.wind.textContent="--"}}const u=document.getElementById("weather-modal"),j=document.getElementById("weather-location-input");document.getElementById("weather-settings")?.addEventListener("click",()=>{j.value=safeGet(C)||"";const v=R(),N=u.querySelector(`input[name="weather-unit-radio"][value="${v}"]`);N&&(N.checked=!0),u.classList.add("show"),j.focus()}),document.getElementById("weather-cancel")?.addEventListener("click",()=>{u.classList.remove("show")}),document.getElementById("weather-save")?.addEventListener("click",()=>{const v=j.value.trim();if(v){safeGet(C)!==v&&safeSet(E,""),safeSet(C,v);const g=u.querySelector('input[name="weather-unit-radio"]:checked'),D=g?g.value:"imperial",B=R();safeSet(M,D),B!==D&&safeSet(E,""),u.classList.remove("show"),L()}else showNotification("Please enter a location (e.g., Hamburg, London, 90210)","warning")}),wireModal(u),document.addEventListener("keydown",v=>{v.key==="Escape"&&u.classList.contains("show")&&u.classList.remove("show")}),L(),setInterval(L,DC.POLL.WEATHER)})(),(function(){const y=document.getElementById("clock-widget"),C=document.getElementById("clock-render");if(!y||!C)return;const O=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],E=["January","February","March","April","May","June","July","August","September","October","November","December"],M=["XII","I","II","III","IV","V","VI","VII","VIII","IX","X","XI"];let R=safeGet("clock-style")||"default",P=-1,A=!1,p="",T="",x=null,$=null;function k(n){if(A||safeGet("clock-chimes")!=="true")return;A=!0;const e=parseInt(safeGet("clock-chime-volume")||"50",10)/100;let t=0;function o(){if(t>=n){A=!1;return}const r=new Audio("/assets/sounds/church-bell.mp3");r.volume=e,r.play().catch(()=>{}),t++,t{A=!1},2500)}o()}function H(n){return O[n.getDay()]+", "+E[n.getMonth()]+" "+n.getDate()+", "+n.getFullYear()}function L(){T="",x=null}function u(){return T!=="digital"&&(C.innerHTML='
`).join("")}async function v(){try{const b=await(await fetch("/api/v1/license/status")).json();b.success&&(j(b.license),D(b.license))}catch(I){console.warn("Failed to load license status:",I.message)}}async function N(){const I=C.value.trim();if(!I){L("Please enter a license code.");return}H(),O.disabled=!0,O.textContent="Activating...";try{const z=await(await secureFetch("/api/v1/license/activate",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:I})})).json();z.success?(u(z.message),C.value="",j(z.license),showNotification("License activated! Premium features unlocked.","success",5e3),D(z.license)):L(z.error||"Activation failed")}catch(b){L("Network error: "+b.message)}finally{O.disabled=!1,O.textContent="Activate"}}async function g(){if(confirm("Deactivate your license? You can reuse the code on another machine.")){E.disabled=!0,E.textContent="Deactivating...";try{const b=await(await secureFetch("/api/v1/license/deactivate",{method:"POST"})).json();b.success?(u(b.message),await v(),showNotification("License deactivated.","info",3e3),D({active:!1})):L(b.error||"Deactivation failed")}catch(I){L("Network error: "+I.message)}finally{E.disabled=!1,E.textContent="Deactivate"}}}function D(I){const b=document.getElementById("license-status-topbar"),z=document.getElementById("license-topbar-icon"),i=document.getElementById("license-topbar-text"),c=document.getElementById("license-topbar-time");if(b)if(b.className="license-status-topbar "+(I.active?"premium":"free"),I.active)if(z.textContent="\u2605",i.textContent="PREMIUM",I.lifetime)c.textContent="\xB7 LIFETIME";else{const l=I.daysRemaining;c.textContent=l!=null?"\xB7 "+l+"d remaining":""}else z.textContent="\u2606",i.textContent=I.expired?"EXPIRED":"FREE TIER",c.textContent=""}function B(){H(),v(),y.classList.add("show")}C.addEventListener("input",function(){let I=this.value.toUpperCase().replace(/[^A-Z0-9-]/g,"");if(I.length>this._prevLength&&(I=I.replace(/-/g,""),I.length>2&&!I.startsWith("DC")&&(I="DC"+I),I.startsWith("DC")&&I.length>2)){const b=["DC"],z=I.substring(2);for(let i=0;i{I.key==="Enter"&&N()}),wireModal(y,document.getElementById("license-cancel"));const f=document.getElementById("license-status-topbar");f&&f.addEventListener("click",()=>window.openLicenseModal&&window.openLicenseModal()),window.openLicenseModal=B,window.checkPremiumFeature=async function(I){try{return(await(await fetch(`/api/v1/license/feature/${I}`)).json()).available}catch{return!1}},v().then(I=>{k&&D(k)})})(),(function(){"use strict";if(window.__dc_058_share_modal_loaded)return;window.__dc_058_share_modal_loaded=!0;const y=[{ms:3600*1e3,label:"1 hour"},{ms:1440*60*1e3,label:"24 hours"},{ms:10080*60*1e3,label:"7 days"}];injectModal("share-modal",`
+
`).join("")}async function f(){try{const g=await(await fetch("/api/v1/license/status")).json();g.success&&(O(g.license),U(g.license))}catch(b){console.warn("Failed to load license status:",b.message)}}async function z(){const b=L.value.trim();if(!b){C("Please enter a license code.");return}H(),R.disabled=!0,R.textContent="Activating...";try{const M=await(await secureFetch("/api/v1/license/activate",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:b})})).json();M.success?(v(M.message),L.value="",O(M.license),showNotification("License activated! Premium features unlocked.","success",5e3),U(M.license)):C(M.error||"Activation failed")}catch(g){C("Network error: "+g.message)}finally{R.disabled=!1,R.textContent="Activate"}}async function A(){if(confirm("Deactivate your license? You can reuse the code on another machine.")){S.disabled=!0,S.textContent="Deactivating...";try{const g=await(await secureFetch("/api/v1/license/deactivate",{method:"POST"})).json();g.success?(v(g.message),await f(),showNotification("License deactivated.","info",3e3),U({active:!1})):C(g.error||"Deactivation failed")}catch(b){C("Network error: "+b.message)}finally{S.disabled=!1,S.textContent="Deactivate"}}}function U(b){const g=document.getElementById("license-status-topbar"),M=document.getElementById("license-topbar-icon"),r=document.getElementById("license-topbar-text"),c=document.getElementById("license-topbar-time");if(g)if(g.className="license-status-topbar "+(b.active?"premium":"free"),b.active)if(M.textContent="\u2605",r.textContent="PREMIUM",b.lifetime)c.textContent="\xB7 LIFETIME";else{const d=b.daysRemaining;c.textContent=d!=null?"\xB7 "+d+"d remaining":""}else M.textContent="\u2606",r.textContent=b.expired?"EXPIRED":"FREE TIER",c.textContent=""}function p(){H(),f(),h.classList.add("show")}L.addEventListener("input",function(){let b=this.value.toUpperCase().replace(/[^A-Z0-9-]/g,"");if(b.length>this._prevLength&&(b=b.replace(/-/g,""),b.length>2&&!b.startsWith("DC")&&(b="DC"+b),b.startsWith("DC")&&b.length>2)){const g=["DC"],M=b.substring(2);for(let r=0;r{b.key==="Enter"&&z()}),wireModal(h,document.getElementById("license-cancel"));const u=document.getElementById("license-status-topbar");u&&u.addEventListener("click",()=>window.openLicenseModal&&window.openLicenseModal()),window.openLicenseModal=p,window.checkPremiumFeature=async function(b){try{return(await(await fetch(`/api/v1/license/feature/${b}`)).json()).available}catch{return!1}},f().then(b=>{E&&U(E)})})(),(function(){"use strict";if(window.__dc_058_share_modal_loaded)return;window.__dc_058_share_modal_loaded=!0;const h=[{ms:3600*1e3,label:"1 hour"},{ms:1440*60*1e3,label:"24 hours"},{ms:10080*60*1e3,label:"7 days"}];injectModal("share-modal",`
Share \u2026
@@ -1925,7 +1917,7 @@ To endpoint: POST /api/v1/security/events/ingest or /events/batch`),f()}catch(w)