",u.innerHTML=m,$("setup-step-summary")}async function N(u){try{const m=await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(u)});return m.ok?(await m.json(),!0):(errorHandler.logError("[SetupWizard] Save Config",new Error(`Server returned ${m.status}`),{function:"saveConfigToServer"}),!1)}catch(m){return errorHandler.logError("[SetupWizard] Save Config",m,{function:"saveConfigToServer"}),!1}}async function D(){const u={setupComplete:!0,configurationType:b,timestamp:new Date().toISOString(),timezone:document.getElementById("setup-timezone")?.value||Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC"};if(b==="homelab"){u.tld=document.getElementById("setup-tld")?.value?.trim()||".home",u.caName=document.getElementById("setup-ca-name")?.value?.trim()||"";const M=document.getElementById("setup-dns-provider")?.value||"technitium";u.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()||""},u.defaults={dnsType:"private",sslType:"internal",targetIP:"localhost"}}else b==="simple"?(u.defaultIP=document.getElementById("setup-simple-ip")?.value?.trim()||"localhost",u.defaults={dnsType:"none",sslType:"none",targetIP:u.defaultIP}):b==="public"&&(u.domain=document.getElementById("setup-public-domain")?.value?.trim()||"",u.email=document.getElementById("setup-public-email")?.value?.trim()||"",u.routingMode=document.querySelector('input[name="routing-mode"]:checked')?.value||"subdirectory",u.defaults={dnsType:u.routingMode==="subdirectory"?"none":"public",sslType:"letsencrypt",targetIP:"localhost"});const m=await N(u);safeSet("dashcaddy-config",JSON.stringify(u)),safeSet("dashcaddy-setup","completed"),document.getElementById("setup-wizard").style.display="none";const x=b==="homelab"?"Professional Home Lab":b==="simple"?"Simple Setup":"Public Server",g=m?"server (shared across all devices)":"locally (this browser only)";showNotification(`Setup Complete! Configured for: ${x}. Settings saved to: ${g}`,"success",5e3),setTimeout(()=>location.reload(),500)}const v=document.getElementById("setup-step-1-next");v&&(v.onclick=function(u){u.preventDefault();const m=document.querySelector('input[name="config-type"]:checked');m&&(b=m.value),$(b==="homelab"?"setup-step-homelab":b==="simple"?"setup-step-simple":b==="public"?"setup-step-public":"setup-step-homelab")});const T=document.getElementById("setup-skip");T&&(T.onclick=async function(u){u.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(u){const m=u.target.value||".home",x=document.getElementById("tld-preview"),g=document.getElementById("tld-preview-2");x&&(x.textContent=m),g&&(g.textContent=m)});const B=document.getElementById("setup-homelab-back");B&&(B.onclick=function(u){u.preventDefault(),$("setup-step-1")});const S=document.getElementById("setup-homelab-next");S&&(S.onclick=function(u){u.preventDefault();const m=document.getElementById("setup-tld")?.value?.trim()||"",x=document.getElementById("setup-ca-name")?.value?.trim()||"",g=document.getElementById("setup-dns-ip")?.value?.trim()||"";if(!m||!m.startsWith(".")){showNotification("Please enter a valid TLD starting with a dot (e.g., .home)","warning");return}if(!x){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(u){u.preventDefault(),$("setup-step-1")});const C=document.getElementById("setup-simple-next");C&&(C.onclick=function(u){u.preventDefault(),P()}),document.querySelectorAll('input[name="routing-mode"]').forEach(function(u){u.onchange=function(){var m=document.getElementById("dns-requirement-note");m&&(m.textContent=this.value==="subdirectory"?"Only one DNS record needed (for the main domain)":"You'll need to configure DNS manually for each subdomain")}});const y=document.getElementById("setup-public-back");y&&(y.onclick=function(u){u.preventDefault(),$("setup-step-1")});const O=document.getElementById("setup-public-next");O&&(O.onclick=function(u){u.preventDefault();const m=document.getElementById("setup-public-domain")?.value?.trim()||"",x=document.getElementById("setup-public-email")?.value?.trim()||"";if(!m){showNotification("Please enter your domain name","warning");return}if(!x||!x.includes("@")){showNotification("Please enter a valid email address","warning");return}P()});const h=document.getElementById("setup-summary-back");h&&(h.onclick=function(u){u.preventDefault(),b==="homelab"?$("setup-step-homelab"):b==="simple"?$("setup-step-simple"):b==="public"&&$("setup-step-public")});const z=document.getElementById("setup-summary-next");z&&(z.onclick=function(u){u.preventDefault(),$("setup-step-disk-safety")});const A=document.getElementById("setup-disk-safety-back");A&&(A.onclick=function(u){u.preventDefault(),$("setup-step-summary")});const j=document.getElementById("setup-disk-safety-finish");j&&(j.onclick=function(u){u.preventDefault(),D()}),window.getGlobalConfig=async function(){try{const m=await fetch("/api/v1/config");if(m.ok){const x=await m.json();if(x&&x.setupComplete)return x}}catch{console.warn("Could not fetch config from server")}const u=safeGet("dashcaddy-config");return u?JSON.parse(u):{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 b=new ErrorHandler;injectModal("app-selector-modal",`
+ `,u+="
",p.innerHTML=u,D("setup-step-summary")}async function H(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 P(){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 z=document.getElementById("setup-dns-provider")?.value||"technitium";p.dns={provider:z,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 H(p);safeSet("dashcaddy-config",JSON.stringify(p)),safeSet("dashcaddy-setup","completed"),document.getElementById("setup-wizard").style.display="none";const f=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: ${f}. Settings saved to: ${g}`,"success",5e3),setTimeout(()=>location.reload(),500)}const v=document.getElementById("setup-step-1-next");v&&(v.onclick=function(p){p.preventDefault();const u=document.querySelector('input[name="config-type"]:checked');u&&(h=u.value),D(h==="homelab"?"setup-step-homelab":h==="simple"?"setup-step-simple":h==="public"?"setup-step-public":"setup-step-homelab")});const C=document.getElementById("setup-skip");C&&(C.onclick=async function(p){p.preventDefault(),confirm("Skip setup? You can run it later from Settings.")&&(await H({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(p){const u=p.target.value||".home",f=document.getElementById("tld-preview"),g=document.getElementById("tld-preview-2");f&&(f.textContent=u),g&&(g.textContent=u)});const $=document.getElementById("setup-homelab-back");$&&($.onclick=function(p){p.preventDefault(),D("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()||"",f=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(!f){showNotification("Please enter a Certificate Authority name","warning");return}if(!g){showNotification("Please enter your DNS server IP address","warning");return}R()});const M=document.getElementById("setup-simple-back");M&&(M.onclick=function(p){p.preventDefault(),D("setup-step-1")});const S=document.getElementById("setup-simple-next");S&&(S.onclick=function(p){p.preventDefault(),R()}),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 B=document.getElementById("setup-public-back");B&&(B.onclick=function(p){p.preventDefault(),D("setup-step-1")});const U=document.getElementById("setup-public-next");U&&(U.onclick=function(p){p.preventDefault();const u=document.getElementById("setup-public-domain")?.value?.trim()||"",f=document.getElementById("setup-public-email")?.value?.trim()||"";if(!u){showNotification("Please enter your domain name","warning");return}if(!f||!f.includes("@")){showNotification("Please enter a valid email address","warning");return}R()});const T=document.getElementById("setup-summary-back");T&&(T.onclick=function(p){p.preventDefault(),h==="homelab"?D("setup-step-homelab"):h==="simple"?D("setup-step-simple"):h==="public"&&D("setup-step-public")});const b=document.getElementById("setup-summary-next");b&&(b.onclick=function(p){p.preventDefault(),D("setup-step-disk-safety")});const N=document.getElementById("setup-disk-safety-back");N&&(N.onclick=function(p){p.preventDefault(),D("setup-step-summary")});const O=document.getElementById("setup-disk-safety-finish");O&&(O.onclick=function(p){p.preventDefault(),P()}),window.getGlobalConfig=async function(){try{const u=await fetch("/api/v1/config");if(u.ok){const f=await u.json();if(f&&f.setupComplete)return f}}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
@@ -333,12 +333,12 @@ This will reset the logo, favicon, title, and position.`))try{if((await secureFe
-
`);const L="custom-apps";let R=null,I=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,I=c.categories,!0}catch(r){b.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 b.logError("[AppSelector] Check Port",c,{function:"checkPortAvailability"}),{available:!0}}}async function v(r){try{const d=await(await fetch(`/api/v1/apps/ports/${r}/suggest`)).json();if(d.success)return d.suggestedPort}catch(c){b.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,k]of Object.entries(R)){const f=k.category||"Other";r[f]||(r[f]=[]),r[f].push({id:d,...k})}const c=I?Object.keys(I):Object.keys(r).sort();for(const d of c){const k=r[d];if(!k||k.length===0)continue;k.sort((e,a)=>(a.popularity||0)-(e.popularity||0));const f=document.createElement("div");f.className="app-category-header";const t=I?.[d]||{};f.innerHTML=`${escapeHtml(t.icon||"")} ${escapeHtml(d)}`,t.color&&(f.style.borderBottomColor=t.color),P.appendChild(f),k.forEach(e=>{const a=document.createElement("div");a.className="app-option";const o=e.isDashboardWidget,i=o&&safeGet("widget-"+e.id+"-enabled")!=="false",n=o?`
${i?"ON":"OFF"}
`:"",s=!o&&e.difficulty?`
${escapeHtml(e.difficulty)}
`:"";a.innerHTML=`
+ `);const L="custom-apps";let A=null,I=null;const D=document.getElementById("app-selector-modal"),R=document.getElementById("app-selector-grid");async function H(){try{const c=await(await fetch("/api/v1/apps/templates")).json();if(c.success)return A=c.templates,I=c.categories,!0}catch(r){h.logError("[AppSelector] Fetch Templates",r,{function:"fetchApiTemplates"})}return!1}async function P(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 v(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 C(){if(R.innerHTML='
Loading app templates...
',!A&&!await H()){R.innerHTML='
Failed to load app templates. Please try again.
';return}R.innerHTML="";const r={};for(const[d,w]of Object.entries(A)){const y=w.category||"Other";r[y]||(r[y]=[]),r[y].push({id:d,...w})}const c=I?Object.keys(I):Object.keys(r).sort();for(const d of c){const w=r[d];if(!w||w.length===0)continue;w.sort((e,a)=>(a.popularity||0)-(e.popularity||0));const y=document.createElement("div");y.className="app-category-header";const t=I?.[d]||{};y.innerHTML=`${escapeHtml(t.icon||"")} ${escapeHtml(d)}`,t.color&&(y.style.borderBottomColor=t.color),R.appendChild(y),w.forEach(e=>{const a=document.createElement("div");a.className="app-option";const o=e.isDashboardWidget,i=o&&safeGet("widget-"+e.id+"-enabled")!=="false",n=o?`
${i?"ON":"OFF"}
`:"",s=!o&&e.difficulty?`
${escapeHtml(e.difficulty)}
`:"";a.innerHTML=`
${escapeHtml(e.icon||"\u{1F4E6}")}
${escapeHtml(e.name)}
${escapeHtml(e.description||"")}
${n}${s}
- `,o?a.onclick=()=>w(e,a):a.onclick=()=>B(e),P.appendChild(a)})}window.renderRecipeCards&&await window.renderRecipeCards(P)}function w(r,c){const d="widget-"+r.id+"-enabled",f=!(safeGet(d)!=="false");safeSet(d,String(f));const t=r.widgetSelector;if(t){const a=document.querySelector(t);a&&(a.style.display=f?"":"none")}const e=c.querySelector('div[style*="border-radius: 4px"]');e&&(e.textContent=f?"ON":"OFF",e.style.background=f?"#2ecc7130":"#e74c3c30",e.style.color=f?"#2ecc71":"#e74c3c"),showNotification(`${r.name} widget ${f?"enabled":"disabled"}`,"success",2e3)}async function B(r){const c=document.getElementById("app-deploy-modal"),d=document.getElementById("app-deploy-title"),k=document.getElementById("deploy-subdomain"),f=document.getElementById("deploy-url-preview"),t=document.getElementById("deploy-ip"),e=document.getElementById("deploy-port"),a=document.getElementById("deploy-tailscale-only"),o=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:
+ `,o?a.onclick=()=>x(e,a):a.onclick=()=>$(e),R.appendChild(a)})}window.renderRecipeCards&&await window.renderRecipeCards(R)}function x(r,c){const d="widget-"+r.id+"-enabled",y=!(safeGet(d)!=="false");safeSet(d,String(y));const t=r.widgetSelector;if(t){const a=document.querySelector(t);a&&(a.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 $(r){const c=document.getElementById("app-deploy-modal"),d=document.getElementById("app-deploy-title"),w=document.getElementById("deploy-subdomain"),y=document.getElementById("deploy-url-preview"),t=document.getElementById("deploy-ip"),e=document.getElementById("deploy-port"),a=document.getElementById("deploy-tailscale-only"),o=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}
@@ -347,38 +347,38 @@ Port: ${V.primaryPort||"N/A"}
Would you like to use this existing container?
Click OK to configure DNS/Caddy for the existing container.
-Click Cancel to deploy a new container.`)&&(r._useExisting=!0,r._existingContainer=V)}}catch{}d.textContent=`Deploy ${r.name}`;const i=r.subdomain||r.id.replace(/-/g,"");k.value=i;const n=document.getElementById("subpath-compat-warning");if(n)if(SITE.routingMode==="subdirectory"){const G=r.subpathSupport||"strip";G==="none"?(n.style.display="block",n.innerHTML='⚠ '+r.name+" does not support subdirectory mode. It may not work correctly at a subpath."):G==="strip"?(n.style.display="block",n.innerHTML='ⓘ '+r.name+" has unverified subdirectory support. It may require additional configuration."):n.style.display="none"}else n.style.display="none";const s=SITE.defaults.dnsType||(SITE.configurationType==="public"?"public":"private"),l=SITE.defaults.sslType||(SITE.configurationType==="public"?"letsencrypt":"internal"),p=document.querySelector(`input[name="dns-type"][value="${s}"]`),E=document.querySelector(`input[name="ssl-type"][value="${l}"]`);p?p.checked=!0:document.querySelector('input[name="dns-type"][value="private"]').checked=!0,E?E.checked=!0:document.querySelector('input[name="ssl-type"][value="internal"]').checked=!0,t.value=SITE.defaults.targetIP||"localhost",a.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 G=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;G&&!G.dataset.moved&&(F.appendChild(G),G.dataset.moved="1"),W&&!W.dataset.moved&&(F.appendChild(W),W.dataset.moved="1")}const q=document.getElementById("media-path-section"),J=document.getElementById("deploy-media-path"),X=document.getElementById("media-path-description");if(r.mediaMount){q.style.display="block",J.value="",J.placeholder="/media/Movies, /media/TVShows or click Browse";const G=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){G.style.display="block",W.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(", ")},W.appendChild(Z)})}else G.style.display="none"}catch{G.style.display="none"}document.getElementById("browse-media-btn").onclick=()=>{openFolderBrowser(J)}}else q.style.display="none",J.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 G=r.mediaMount?.containerPath,W=r.docker.volumes.filter(V=>!V.includes("{{MEDIA_PATH}}")&&!(G&&V.endsWith(":"+G)));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=`
+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,"");w.value=i;const n=document.getElementById("subpath-compat-warning");if(n)if(SITE.routingMode==="subdirectory"){const G=r.subpathSupport||"strip";G==="none"?(n.style.display="block",n.innerHTML='⚠ '+r.name+" does not support subdirectory mode. It may not work correctly at a subpath."):G==="strip"?(n.style.display="block",n.innerHTML='ⓘ '+r.name+" has unverified subdirectory support. It may require additional configuration."):n.style.display="none"}else n.style.display="none";const s=SITE.defaults.dnsType||(SITE.configurationType==="public"?"public":"private"),l=SITE.defaults.sslType||(SITE.configurationType==="public"?"letsencrypt":"internal"),m=document.querySelector(`input[name="dns-type"][value="${s}"]`),k=document.querySelector(`input[name="ssl-type"][value="${l}"]`);m?m.checked=!0:document.querySelector('input[name="dns-type"][value="private"]').checked=!0,k?k.checked=!0:document.querySelector('input[name="ssl-type"][value="internal"]').checked=!0,t.value=SITE.defaults.targetIP||"localhost",a.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 G=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;G&&!G.dataset.moved&&(F.appendChild(G),G.dataset.moved="1"),W&&!W.dataset.moved&&(F.appendChild(W),W.dataset.moved="1")}const q=document.getElementById("media-path-section"),J=document.getElementById("deploy-media-path"),X=document.getElementById("media-path-description");if(r.mediaMount){q.style.display="block",J.value="",J.placeholder="/media/Movies, /media/TVShows or click Browse";const G=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){G.style.display="block",W.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(", ")},W.appendChild(Z)})}else G.style.display="none"}catch{G.style.display="none"}document.getElementById("browse-media-btn").onclick=()=>{openFolderBrowser(J)}}else q.style.display="none",J.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 G=r.mediaMount?.containerPath,W=r.docker.volumes.filter(V=>!V.includes("{{MEDIA_PATH}}")&&!(G&&V.endsWith(":"+G)));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=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 G=e.value||se;Y.innerHTML='Checking port...';const W=await D(G);if(W.available)Y.innerHTML=`Port ${escapeHtml(String(G))} is available`;else{const V=await v(se);Y.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 G=e.value||se;Y.innerHTML='Checking port...';const W=await P(G);if(W.available)Y.innerHTML=`Port ${escapeHtml(String(G))} is available`;else{const V=await v(se);Y.innerHTML=`
Port ${escapeHtml(G)} 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?o.innerHTML=`
Connected${W.self?.hostname} (${W.self?.ip})| ${W.deviceCount} devices
- `:W.installed?o.innerHTML='Not connected':(o.innerHTML='Not available',a.disabled=!0)}catch{o.innerHTML='Could not check status'}function ae(){const G=k.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}/${G}`;else if(W==="private")K=`${V==="none"?"http":"https"}://${buildDomain(G)}`;else if(W==="public"){const te=V==="none"?"http":"https",ee=SITE.domain||G;K=SITE.domain?`${te}://${G}.${SITE.domain}`:`${te}://${G}`}else{const te=e.value||r.defaultPort||DC.DEFAULTS.SERVICE_PORT;K=`http://${t.value}:${te}`}f.textContent=K}k.oninput=ae,t.oninput=ae,e.oninput=ae,document.querySelectorAll('input[name="dns-type"]').forEach(G=>{G.onchange=ae}),document.querySelectorAll('input[name="ssl-type"]').forEach(G=>{G.onchange=ae}),ae(),$.classList.remove("show"),c.classList.add("show"),c.dataset.appTemplate=JSON.stringify(r)}async function S(r){const c=r.appTemplate,d=safeGetJSON(L,[]),k=c._useExisting&&c._existingContainer,f=d.find(t=>t.id===r.subdomain);if(!(f&&!k&&!confirm(`An app with subdomain "${r.subdomain}" already exists. Redeploy?`))){if(f){const t=d.indexOf(f);d.splice(t,1),safeSet(L,JSON.stringify(d))}if(k)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 a=await v(c.defaultPort||8080);if(confirm(`Port ${t} is already in use by ${e.conflict?.usedBy||"another container"}.
+ `:W.installed?o.innerHTML='Not connected':(o.innerHTML='Not available',a.disabled=!0)}catch{o.innerHTML='Could not check status'}function ae(){const G=w.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}/${G}`;else if(W==="private")K=`${V==="none"?"http":"https"}://${buildDomain(G)}`;else if(W==="public"){const te=V==="none"?"http":"https",ee=SITE.domain||G;K=SITE.domain?`${te}://${G}.${SITE.domain}`:`${te}://${G}`}else{const te=e.value||r.defaultPort||DC.DEFAULTS.SERVICE_PORT;K=`http://${t.value}:${te}`}y.textContent=K}w.oninput=ae,t.oninput=ae,e.oninput=ae,document.querySelectorAll('input[name="dns-type"]').forEach(G=>{G.onchange=ae}),document.querySelectorAll('input[name="ssl-type"]').forEach(G=>{G.onchange=ae}),ae(),D.classList.remove("show"),c.classList.add("show"),c.dataset.appTemplate=JSON.stringify(r)}async function E(r){const c=r.appTemplate,d=safeGetJSON(L,[]),w=c._useExisting&&c._existingContainer,y=d.find(t=>t.id===r.subdomain);if(!(y&&!w&&!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(w)r.port=c._existingContainer.primaryPort;else{const t=r.port||c.defaultPort||8080;showNotification(`Checking port ${t} availability...`,"info",0);const e=await P(t);if(!e.available){const a=await v(c.defaultPort||8080);if(confirm(`Port ${t} is already in use by ${e.conflict?.usedBy||"another container"}.
-Would you like to use port ${a} instead?`))r.port=a;else{showNotification("Deployment cancelled - port conflict","error",5e3);return}}}showNotification(k?`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}};k&&(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 a=await(await secureFetch("/api/v1/apps/deploy",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json();if(a.success){const o={id:r.subdomain,name:c.name,logo:`/assets/${c.id}.png`,containerId:a.containerId,url:a.url,ip:r.ip,appTemplate:c.id,tailscaleOnly:r.tailscaleOnly||!1};d.push(o),safeSet(L,JSON.stringify(d)),window.APPS&&!window.APPS.some(n=>n.id===c.id)&&(window.APPS.push(o),typeof window.buildGrid=="function"&&window.buildGrid(),typeof window.refreshAll=="function"&&setTimeout(()=>window.refreshAll(),500));let i=a.usedExisting?`${c.name} configured with existing container!
+Would you like to use port ${a} instead?`))r.port=a;else{showNotification("Deployment cancelled - port conflict","error",5e3);return}}}showNotification(w?`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}};w&&(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 a=await(await secureFetch("/api/v1/apps/deploy",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json();if(a.success){const o={id:r.subdomain,name:c.name,logo:`/assets/${c.id}.png`,containerId:a.containerId,url:a.url,ip:r.ip,appTemplate:c.id,tailscaleOnly:r.tailscaleOnly||!1};d.push(o),safeSet(L,JSON.stringify(d)),window.APPS&&!window.APPS.some(n=>n.id===c.id)&&(window.APPS.push(o),typeof window.buildGrid=="function"&&window.buildGrid(),typeof window.refreshAll=="function"&&setTimeout(()=>window.refreshAll(),500));let i=a.usedExisting?`${c.name} configured with existing container!
URL: ${a.url}`:`${c.name} deployed successfully!
URL: ${a.url}`;a.warning&&(i+=`
-\u26A0 Warning: ${a.warning}`),showNotification(i,"success",8e3),delete c._useExisting,delete c._existingContainer,a.url&&a.url.startsWith("https://")&&H(a.url,c.name),a.setupInstructions&&a.setupInstructions.length>0&&setTimeout(()=>{const n=a.setupInstructions.join(`
-`);showNotification(`Setup Instructions for ${c.name}: ${n}`,"info",1e4)},1e3)}else throw new Error(a.error||"Deployment failed")}catch(t){b.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 k=12,f=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 y=document.getElementById("app-deploy-modal");document.getElementById("app-deploy-cancel")?.addEventListener("click",()=>{y.classList.remove("show")}),document.getElementById("app-deploy-confirm")?.addEventListener("click",()=>{const r=JSON.parse(y.dataset.appTemplate),c=document.getElementById("deploy-media-path").value.trim(),d=[];document.querySelectorAll("#volume-mounts-list .vol-host-path").forEach(f=>{d.push({hostPath:f.value.trim(),containerPath:f.dataset.containerPath})});const k={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(!k.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}y.classList.remove("show"),S(k)}),wireModal(y);const O=document.getElementById("folder-browser-modal"),h=document.getElementById("folder-browser-path"),z=document.getElementById("folder-browser-list"),A=document.getElementById("folder-browser-selected"),j=document.getElementById("folder-browser-selected-list");let u="",m=[],x=null;window.openFolderBrowser=function(r){x=r,m=r.value.split(",").map(c=>c.trim()).filter(c=>c),u="",M(),g(""),O.classList.add("show")};async function g(r){h.textContent=r||"Select a drive...",z.innerHTML='
`;return}u=d.path||"",h.textContent=u||"Select a drive...";let k="";d.parent&&d.parent!==d.path&&(k+=`
+\u26A0 Warning: ${a.warning}`),showNotification(i,"success",8e3),delete c._useExisting,delete c._existingContainer,a.url&&a.url.startsWith("https://")&&M(a.url,c.name),a.setupInstructions&&a.setupInstructions.length>0&&setTimeout(()=>{const n=a.setupInstructions.join(`
+`);showNotification(`Setup Instructions for ${c.name}: ${n}`,"info",1e4)},1e3)}else throw new Error(a.error||"Deployment failed")}catch(t){h.logError("[AppSelector] Deployment",t,{function:"deploy"}),showNotification(`Failed to deploy ${c.name}: ${t.message}`,"error",8e3)}}}async function M(r,c){showNotification(`\u23F3 Generating SSL certificate for ${c}...`,"warning",6e4);let d=0;const w=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",()=>{C(),D.classList.add("show")}),wireModal(D,document.getElementById("app-selector-cancel"));const B=document.getElementById("app-deploy-modal");document.getElementById("app-deploy-cancel")?.addEventListener("click",()=>{B.classList.remove("show")}),document.getElementById("app-deploy-confirm")?.addEventListener("click",()=>{const r=JSON.parse(B.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 w={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(!w.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}B.classList.remove("show"),E(w)}),wireModal(B);const U=document.getElementById("folder-browser-modal"),T=document.getElementById("folder-browser-path"),b=document.getElementById("folder-browser-list"),N=document.getElementById("folder-browser-selected"),O=document.getElementById("folder-browser-selected-list");let p="",u=[],f=null;window.openFolderBrowser=function(r){f=r,u=r.value.split(",").map(c=>c.trim()).filter(c=>c),p="",z(),g(""),U.classList.add("show")};async function g(r){T.textContent=r||"Select a drive...",b.innerHTML='
`:""}
+ `}function T(){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 f=A.components||[];return f.filter(g=>g.required).forEach(g=>u.add(g.id)),f.filter(g=>u.has(g.id))}async function b(){const p=document.getElementById("recipe-progress-list"),u=document.getElementById("recipe-deploy-result");u.style.display="none",p.innerHTML="";const f=T();for(const c of f){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
- `,u.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:x.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 x)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 k of d.deployed||[])A(k.id,"success",k.url?`Running \u2192 ${k.url}`:"Running");for(const k of d.errors||[])A(k.componentId,"error",k.error);m.style.display="",m.innerHTML=`
+ `,p.appendChild(d)}const g=document.querySelectorAll("#recipe-volume-list input[data-volume-key]"),z={};g.forEach(c=>{z[c.dataset.volumeKey]=c.value});const r={selectedComponents:f.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 f)N(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:A.id,config:r})})).json();if(d.success){for(const w of d.deployed||[])N(w.id,"success",w.url?`Running \u2192 ${w.url}`:"Running");for(const w of d.errors||[])N(w.componentId,"error",w.error);u.style.display="",u.innerHTML=`
`}}function N(p,u,f){const g=document.getElementById(`recipe-progress-${p}`);if(!g)return;const z=g.querySelector(".recipe-progress-icon"),r=g.querySelector(".recipe-progress-status");u==="deploying"?(z.textContent="\u23F3",r.style.color="var(--accent)"):u==="success"?(z.textContent="\u2705",r.style.color="var(--ok-fg)"):u==="error"&&(z.textContent="\u274C",r.style.color="var(--bad-fg)"),r.textContent=f}v.addEventListener("click",()=>{if(I===3){I=4,M(),b();return}I<3&&(I++,M(),I===2&&B(),I===3&&U())}),P.addEventListener("click",()=>{I>1&&I<4&&(I--,M())}),window.groupRecipeCards=function(){const p=document.querySelectorAll(".service-card[data-recipe-id]");if(p.length===0)return;const u={};p.forEach(f=>{const g=f.dataset.recipeId;u[g]||(u[g]=[]),u[g].push(f)});for(const[f,g]of Object.entries(u))g.length<2||g.forEach((z,r)=>{if(z.style.borderLeft="3px solid rgba(142,68,173,0.5)",r===0){let c=z.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=f.replace(/-/g," "),z.style.position="relative",z.appendChild(c))}})},window.manageRecipe=async function(p,u){const f=`/api/v1/recipes/${p}/${u}`,g=u==="remove"?"DELETE":"POST",z=u==="remove"?`/api/v1/recipes/${p}`:f;if(!(u==="remove"&&!confirm(`Remove the entire ${p} recipe? This will delete all containers and configuration.`)))try{const c=await(await secureFetch(z,{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 O=document.createElement("style");O.textContent=`
.recipe-step {
flex: 1;
text-align: center;
@@ -550,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(j),w()})(),(function(){document.getElementById("reload-caddy-top")?.addEventListener("click",async()=>{const b=document.getElementById("reload-caddy-top"),L=b.textContent;try{b.textContent="\u23F3 Reloading...",b.disabled=!0;const R=await secureFetch("/api/v1/caddy/reload",{method:"POST",headers:{"Content-Type":"application/json"}}),I=await R.json();if(R.ok&&I.success)b.textContent="\u2705 Reloaded!",setTimeout(()=>{b.textContent=L,b.disabled=!1},2e3);else throw new Error(I.error||"Reload failed")}catch(R){b.textContent="\u274C Failed",showNotification(`Failed to reload Caddy: ${R.message}`,"error"),setTimeout(()=>{b.textContent=L,b.disabled=!1},2e3)}})})(),(function(){injectModal("error-log-modal",'
\u{1F4CB} Error Logs
Loading error logs...
');const b=document.getElementById("error-log-modal"),L=document.getElementById("error-log-content"),R=document.getElementById("view-error-logs"),I=document.getElementById("error-log-refresh"),$=document.getElementById("error-log-clear"),P=document.getElementById("error-log-close");async function N(){L.innerHTML='
\u2705 No errors logged! Everything is working smoothly.
':L.innerHTML=T.logs.map(w=>`
+ `,document.head.appendChild(O),x()})(),(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 A=await secureFetch("/api/v1/caddy/reload",{method:"POST",headers:{"Content-Type":"application/json"}}),I=await A.json();if(A.ok&&I.success)h.textContent="\u2705 Reloaded!",setTimeout(()=>{h.textContent=L,h.disabled=!1},2e3);else throw new Error(I.error||"Reload failed")}catch(A){h.textContent="\u274C Failed",showNotification(`Failed to reload Caddy: ${A.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"),A=document.getElementById("view-error-logs"),I=document.getElementById("error-log-refresh"),D=document.getElementById("error-log-clear"),R=document.getElementById("error-log-close");async function H(){L.innerHTML='
@@ -670,7 +670,7 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};
-
`);const b=document.getElementById("snapshot-modal"),L=document.getElementById("snapshot-btn"),R=document.getElementById("snapshot-close"),I=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 v(){try{const C=await(await fetch("/api/v1/containers")).json();if(!C.success||!C.containers)return;I.innerHTML='';for(const y of C.containers){const O=document.createElement("option");O.value=y.id,O.textContent=`${y.name||y.id} (${y.image||"unknown"})`,O.dataset.name=y.name,O.dataset.image=y.image,O.dataset.status=y.status,O.dataset.created=y.created,I.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(y){N.textContent=`\u2717 Error: ${y.message}`,N.style.color="var(--bad-fg)"}finally{P.disabled=!1,P.textContent="\u{1F4BE} Create Snapshot"}}function B(){b.classList.add("show"),v()}function S(){b.classList.remove("show"),$.style.display="none",D=null,I.selectedIndex=0}L?.addEventListener("click",B),R?.addEventListener("click",S),wireModal(b,R),I?.addEventListener("change",H=>{const C=I.options[I.selectedIndex];T(C)}),P?.addEventListener("click",w),b?.querySelectorAll(".panel-tab").forEach(H=>{H.addEventListener("click",()=>{b.querySelectorAll(".panel-tab").forEach(C=>C.classList.remove("active")),b.querySelectorAll(".panel-section").forEach(C=>C.classList.remove("active")),H.classList.add("active"),b.querySelector(`#${H.dataset.panel}`).classList.add("active")})})})(),(function(){injectModal("arr-setup-modal",`
+
`);const h=document.getElementById("snapshot-modal"),L=document.getElementById("snapshot-btn"),A=document.getElementById("snapshot-close"),I=document.getElementById("snapshot-container-select"),D=document.getElementById("snapshot-details"),R=document.getElementById("snapshot-create-btn"),H=document.getElementById("snapshot-create-status");let P=null;async function v(){try{const S=await(await fetch("/api/v1/containers")).json();if(!S.success||!S.containers)return;I.innerHTML='';for(const B of S.containers){const U=document.createElement("option");U.value=B.id,U.textContent=`${B.name||B.id} (${B.image||"unknown"})`,U.dataset.name=B.name,U.dataset.image=B.image,U.dataset.status=B.status,U.dataset.created=B.created,I.appendChild(U)}}catch(M){console.error("Failed to load containers:",M)}}function C(M){if(!M||!M.value){D.style.display="none",P=null;return}P=M.value,document.getElementById("snapshot-image").textContent=M.dataset.image||"-",document.getElementById("snapshot-status").textContent=M.dataset.status||"-",document.getElementById("snapshot-created").textContent=M.dataset.created?new Date(M.dataset.created*1e3).toLocaleString():"-",document.getElementById("snapshot-id").textContent=M.value.substring(0,12),D.style.display=""}async function x(){if(!P){H.textContent="Please select a container first",H.style.color="var(--bad-fg)";return}const M=document.getElementById("snapshot-name").value.trim();if(!M){H.textContent="Please enter a snapshot name",H.style.color="var(--bad-fg)";return}const S=document.getElementById("snapshot-leave-running").checked;R.disabled=!0,R.textContent="Creating...",H.textContent="";try{const U=await(await fetch(`/api/v1/containers/${encodeURIComponent(P)}/checkpoint`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:M,leaveRunning:S})})).json();U.success?(H.textContent=`\u2713 Snapshot "${M}" created successfully`,H.style.color="var(--ok-fg)",document.getElementById("snapshot-name").value=""):(H.textContent=`\u2717 Failed: ${U.error||"Unknown error"}`,H.style.color="var(--bad-fg)")}catch(B){H.textContent=`\u2717 Error: ${B.message}`,H.style.color="var(--bad-fg)"}finally{R.disabled=!1,R.textContent="\u{1F4BE} Create Snapshot"}}function $(){h.classList.add("show"),v()}function E(){h.classList.remove("show"),D.style.display="none",P=null,I.selectedIndex=0}L?.addEventListener("click",$),A?.addEventListener("click",E),wireModal(h,A),I?.addEventListener("change",M=>{const S=I.options[I.selectedIndex];C(S)}),R?.addEventListener("click",x),h?.querySelectorAll(".panel-tab").forEach(M=>{M.addEventListener("click",()=>{h.querySelectorAll(".panel-tab").forEach(S=>S.classList.remove("active")),h.querySelectorAll(".panel-section").forEach(S=>S.classList.remove("active")),M.classList.add("active"),h.querySelector(`#${M.dataset.panel}`).classList.add("active")})})})(),(function(){injectModal("arr-setup-modal",`
\u{1F3AC} Smart Arr Connect
@@ -749,73 +749,73 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};
-
`);const b=document.getElementById("arr-setup-modal"),L=document.getElementById("arr-setup-btn"),R=document.getElementById("arr-setup-cancel"),I=document.getElementById("smart-connect-btn"),$=document.getElementById("smart-phase-detect"),P=document.getElementById("smart-phase-credentials"),N=document.getElementById("smart-phase-progress"),D=document.getElementById("smart-phase-results"),v=document.getElementById("smart-detect-results"),T=document.getElementById("smart-credential-inputs"),w=document.getElementById("smart-progress-steps"),B=document.getElementById("smart-results-content"),S=document.getElementById("smart-plex-libraries"),H=document.getElementById("smart-retry-btn");let C=null;const y={plex:"\u{1F3AC}",radarr:"\u{1F3AC}",sonarr:"\u{1F4FA}",prowlarr:"\u{1F50D}",seerr:"\u{1F4CB}"},O={plex:"Plex",radarr:"Radarr (Movies)",sonarr:"Sonarr (TV)",prowlarr:"Prowlarr (Indexers)",seerr:"Seerr"};function h(g){$.style.display=g==="detect"?"block":"none",P.style.display=g==="credentials"?"block":"none",N.style.display=g==="progress"?"block":"none",D.style.display=g==="results"?"block":"none"}function z(g){const M={connected:{bg:"var(--ok-fg)",icon:"✓",text:"Connected"},needs_key:{bg:"#f39c12",icon:"🔑",text:"Needs API Key"},not_found:{bg:"var(--muted)",icon:"—",text:"Not Found"},error:{bg:"var(--bad-fg)",icon:"✗",text:"Error"}},r=M[g]||M.not_found;return`${r.icon} ${r.text}`}async function A(){h("detect"),v.style.display="none";try{if(C=await(await fetch("/api/v1/arr/smart-detect")).json(),!C.success){v.innerHTML=`
`,v.style.display="block"}}function j(g){let M="";const r=g.services,c=["radarr","sonarr","prowlarr"];for(const f of c){const t=r[f];if(!t||t.status==="not_found"&&!t.url)continue;const e=y[f],a=O[f],o=t.status==="connected";M+=`
`,v.style.display="block"}}function O(g){let z="";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=B[y],a=U[y],o=t.status==="connected";z+=`
`}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(),k=r?.value.trim();if(!d||!k){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:k})})).json();t.success?c.innerHTML=`✓ ${escapeHtml(t.appName||"Connected")} v${escapeHtml(t.version||"")}`:c.innerHTML=`✗ ${escapeHtml(t.error)}`}catch(f){c.innerHTML=`✗ ${escapeHtml(f.message)}`}};async function u(){h("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 k of c.steps||[]){const f=k.status==="success"?'✓':'✗',t=k.status==="success"?"var(--muted)":"var(--bad-fg)";d+=`
`}C.innerHTML=z}window.smartTestConnection=async function(g){const z=document.getElementById(`smart-${g}-url`),r=document.getElementById(`smart-${g}-key`),c=document.getElementById(`smart-${g}-status`),d=z?.value.trim(),w=r?.value.trim();if(!d||!w){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:w})})).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(){T("progress"),x.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 z={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(z)})).json();let d="";for(const w of c.steps||[]){const y=w.status==="success"?'✓':'✗',t=w.status==="success"?"var(--muted)":"var(--bad-fg)";d+=`
"}}}async function m(e,a){try{var o=await secureFetch("/api/v1/backups/schedule",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({appId:e,...a})}),i=await o.json();i.success?showNotification("Schedule updated for "+e,"success"):(showNotification("Update failed: "+(i.error||"Unknown"),"error"),u())}catch(n){showNotification("Error: "+n.message,"error")}}async function x(e){try{var a=await secureFetch("/api/v1/backups/backup/"+encodeURIComponent(e),{method:"POST",headers:{"Content-Type":"application/json"}}),o=await a.json();o.success?showNotification("Backup started for "+e+"!","success"):showNotification("Backup failed: "+(o.error||"Unknown"),"error")}catch(i){showNotification("Error: "+i.message,"error")}}async function g(e){if(confirm("Remove backup schedule for "+e+"?"))try{var a=await secureFetch("/api/v1/backups/schedule/"+encodeURIComponent(e),{method:"DELETE"}),o=await a.json();o.success?(showNotification("Schedule removed for "+e,"success"),u()):showNotification("Delete failed: "+(o.error||"Unknown"),"error")}catch(i){showNotification("Error: "+i.message,"error")}}async function M(){var e=document.getElementById("new-schedule-appid")?.value?.trim(),a=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 i=await secureFetch("/api/v1/backups/schedule",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({appId:e,schedule:a,retention:{keep:o},enabled:!0})}),n=await i.json();if(n.success){showNotification("Schedule created for "+e,"success"),u();var s=document.getElementById("new-schedule-appid");s&&(s.value="")}else showNotification("Failed: "+(n.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"),a=await e.json();if(!a.success)throw new Error(a.error||"Failed to load");var o=a.files||[];if(o.length===0){z.innerHTML='
"}}}window.__restoreServerBackup=async function(e){if(confirm("Restore from this server backup? This will overwrite current configuration."))try{var a=await secureFetch("/api/v1/backups/restore/"+e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({restoreServices:!0,restoreConfig:!0})}),o=await a.json();o.success?(showNotification("Restore completed successfully!","success"),location.reload()):showNotification("Restore failed: "+(o.error||"Unknown error"),"error")}catch(i){showNotification("Restore error: "+i.message,"error")}},document.querySelector('[data-panel="backup-schedules-tab"]')?.addEventListener("click",u),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"),a=await e.json();if(a.tier!=="premium"){A.innerHTML=`
\u2B50
Premium Feature
Point-in-time restore requires DashCaddy Premium with auto-backup enabled.
"}}}async function u(e,a){try{var o=await secureFetch("/api/v1/backups/schedule",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({appId:e,...a})}),i=await o.json();i.success?showNotification("Schedule updated for "+e,"success"):(showNotification("Update failed: "+(i.error||"Unknown"),"error"),p())}catch(n){showNotification("Error: "+n.message,"error")}}async function f(e){try{var a=await secureFetch("/api/v1/backups/backup/"+encodeURIComponent(e),{method:"POST",headers:{"Content-Type":"application/json"}}),o=await a.json();o.success?showNotification("Backup started for "+e+"!","success"):showNotification("Backup failed: "+(o.error||"Unknown"),"error")}catch(i){showNotification("Error: "+i.message,"error")}}async function g(e){if(confirm("Remove backup schedule for "+e+"?"))try{var a=await secureFetch("/api/v1/backups/schedule/"+encodeURIComponent(e),{method:"DELETE"}),o=await a.json();o.success?(showNotification("Schedule removed for "+e,"success"),p()):showNotification("Delete failed: "+(o.error||"Unknown"),"error")}catch(i){showNotification("Error: "+i.message,"error")}}async function z(){var e=document.getElementById("new-schedule-appid")?.value?.trim(),a=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 i=await secureFetch("/api/v1/backups/schedule",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({appId:e,schedule:a,retention:{keep:o},enabled:!0})}),n=await i.json();if(n.success){showNotification("Schedule created for "+e,"success"),p();var s=document.getElementById("new-schedule-appid");s&&(s.value="")}else showNotification("Failed: "+(n.error||"Unknown"),"error")}catch(l){showNotification("Error: "+l.message,"error")}}async function r(){if(b){b.innerHTML='
Loading...
';try{var e=await fetch("/api/v1/backups/files"),a=await e.json();if(!a.success)throw new Error(a.error||"Failed to load");var o=a.files||[];if(o.length===0){b.innerHTML='
"}}}window.__restoreServerBackup=async function(e){if(confirm("Restore from this server backup? This will overwrite current configuration."))try{var a=await secureFetch("/api/v1/backups/restore/"+e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({restoreServices:!0,restoreConfig:!0})}),o=await a.json();o.success?(showNotification("Restore completed successfully!","success"),location.reload()):showNotification("Restore failed: "+(o.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(N){try{var e=await fetch("/api/v1/license/status"),a=await e.json();if(a.tier!=="premium"){N.innerHTML=`
\u2B50
Premium Feature
Point-in-time restore requires DashCaddy Premium with auto-backup enabled.
Configuration differs \u2014 restoring will replace current settings
':s+='
No changes
',s+="
"}s+='
',document.body.insertAdjacentHTML("beforeend",s),document.getElementById("compare-close-btn")?.addEventListener("click",function(){document.getElementById("compare-overlay")?.remove()}),document.getElementById("compare-overlay")?.addEventListener("click",function(k){k.target===this&&this.remove()})}catch(k){showNotification("Compare error: "+k.message,"error")}}async function t(e,a){if(confirm("Restore "+a+" 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(a),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({restartContainers:!0})}),i=await o.json();i.success?(showNotification(e+" restored to "+a,"success"),setTimeout(function(){location.reload()},1500)):showNotification("Restore failed: "+(i.error||"Unknown"),"error")}catch(n){showNotification("Restore error: "+n.message,"error")}}})(),(function(){injectModal("stats-modal",`
@@ -1196,7 +1196,7 @@ This will replace current configuration, credentials, and data. Containers will
- `);const b=document.getElementById("stats-modal"),L=document.getElementById("container-stats-btn"),R=document.getElementById("stats-cancel"),I=document.getElementById("stats-refresh-btn"),$=document.getElementById("stats-auto-refresh"),P=document.getElementById("stats-container"),N=document.getElementById("stats-aggregated-container"),D=document.getElementById("stats-alerts-container"),v=document.getElementById("stats-last-update");let T=null,w=null;function B(d){if(d===0||!d)return"0 B";const k=1024,f=["B","KB","MB","GB"],t=Math.floor(Math.log(d)/Math.log(k));return parseFloat((d/Math.pow(k,t)).toFixed(1))+" "+f[t]}function S(d){return d<30?"#2ecc71":d<70?"#f39c12":"#e74c3c"}function H(d){return d<50?"#2ecc71":d<80?"#f39c12":"#e74c3c"}async function C(){try{let d=null,k=!1;try{const e=await(await fetch("/api/v1/monitoring/stats")).json();e.success&&e.stats&&(d=e.stats,k=!0,w=e.stats)}catch{}if(!k){const e=await(await fetch("/api/v1/stats/containers")).json();if(e.success&&e.stats){d={};for(const a of e.stats)d[a.name]={name:a.name,current:{cpu:a.cpu,memory:{percent:a.memory.percent,usage:a.memory.used,limit:a.memory.limit,usageMB:Math.round(a.memory.used/1048576),limitMB:Math.round(a.memory.limit/1048576)},network:{rxBytes:a.network.rx,txBytes:a.network.tx,rxMB:(a.network.rx/1048576).toFixed(1),txMB:(a.network.tx/1048576).toFixed(1)},disk:{readMB:0,writeMB:0}},status:a.status};w=d}}if(!d||Object.keys(d).length===0){P.innerHTML='
';try{const e=await(await fetch(`/api/v1/monitoring/history/${encodeURIComponent(d)}?startTime=${f}&endTime=${k}`)).json();if(!e.success)throw new Error(e.error||"Failed to load history");const a=e.samples||[],o=e.tier||"raw";if(a.length===0){j.innerHTML=`
\u{1F4CA}No data for the last ${m}. Tier: ${g(o)}.
`;return}const i=o==="raw",n=i?U=>U.cpu?.percent:U=>U.cpu?.avg,s=i?U=>U.memory?.percent:U=>U.memory?.avgPercent,l=i?U=>U.network?.rxMB||0:U=>U.network?.rxMB||0,p=i?U=>U.network?.txMB||0:U=>U.network?.txMB||0;let E=`
+ `}function r(){if(!N)return;const d=x||{},w=N.value,y=Object.entries(d);if(y.length===0){N.innerHTML='';return}N.innerHTML=y.map(([t,e])=>``).join(""),w&&d[w]&&(N.value=w)}async function c(){if(!O||!N)return;const d=N.value;if(!d){O.innerHTML='
';try{const e=await(await fetch(`/api/v1/monitoring/history/${encodeURIComponent(d)}?startTime=${y}&endTime=${w}`)).json();if(!e.success)throw new Error(e.error||"Failed to load history");const a=e.samples||[],o=e.tier||"raw";if(a.length===0){O.innerHTML=`
\u{1F4CA}No data for the last ${u}. Tier: ${g(o)}.
@@ -1452,7 +1452,7 @@ This will replace current configuration, credentials, and data. Containers will
-
`);const b=document.getElementById("health-modal"),L=document.getElementById("health-check-btn"),R=document.getElementById("health-cancel"),I=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"),v=document.getElementById("health-add-btn"),T=document.getElementById("health-config-form"),w=document.getElementById("health-form-title"),B=document.getElementById("health-form-cancel"),S=document.getElementById("health-form-save"),H="dashcaddy-health-settings",C={retentionDays:30,pollingInterval:60,statsPollingInterval:30,maxEntriesPerService:500,diskUsageThreshold:80},y=document.getElementById("health-global-save"),O=document.getElementById("health-global-reset"),h=document.getElementById("health-global-status"),z=document.getElementById("health-setting-retention"),A=document.getElementById("health-setting-interval"),j=document.getElementById("health-setting-stats-interval"),u=document.getElementById("health-setting-max-entries"),m=document.getElementById("health-setting-disk-threshold");function x(){try{const i=safeGet(H),n=i?JSON.parse(i):{};return Object.assign({},C,n)}catch{return Object.assign({},C)}}function g(){const i=x();z&&(z.value=i.retentionDays),A&&(A.value=i.pollingInterval),j&&(j.value=i.statsPollingInterval),u&&(u.value=i.maxEntriesPerService),m&&(m.value=i.diskUsageThreshold)}function M(){const i={retentionDays:Math.max(1,Math.min(3650,parseInt(z?.value)||C.retentionDays)),pollingInterval:Math.max(5,Math.min(3600,parseInt(A?.value)||C.pollingInterval)),statsPollingInterval:Math.max(5,Math.min(3600,parseInt(j?.value)||C.statsPollingInterval)),maxEntriesPerService:Math.max(10,Math.min(1e5,parseInt(u?.value)||C.maxEntriesPerService)),diskUsageThreshold:Math.max(50,Math.min(99,parseInt(m?.value)||C.diskUsageThreshold))};try{safeSet(H,JSON.stringify(i)),g(),h&&(h.textContent="Saved \u2713",h.style.color="var(--ok-fg)",setTimeout(()=>{h&&(h.textContent="")},2500)),typeof showNotification=="function"&&showNotification("Global health settings saved","success")}catch(n){h&&(h.textContent="Save failed",h.style.color="var(--bad-fg)"),typeof showNotification=="function"&&showNotification("Failed to save settings: "+n.message,"error")}}function r(){try{safeSet(H,JSON.stringify(C))}catch{}g(),h&&(h.textContent="Reset to defaults \u2713",h.style.color="var(--ok-fg)",setTimeout(()=>{h&&(h.textContent="")},2500))}g(),y?.addEventListener("click",M),O?.addEventListener("click",r);let c=null;function d(i){return i>=99.9?"var(--ok-fg)":i>=95?"#f39c12":"var(--bad-fg)"}function k(i){const n={critical:"var(--bad-fg)",high:"#ff6b6b",medium:"#f39c12",low:"var(--muted)"};return`${i}`}async function f(){try{const n=await(await fetch("/api/v1/health-checks/status")).json();if(!n.success||!n.status||Object.keys(n.status).length===0){$.innerHTML='
\u{1F3E5}No health checks configured. Go to the Configure tab to add services.
';return}const s=Object.values(n.status);let l='
';l+='
',l+='
Service
Status
',l+='
Uptime 24h
Uptime 7d
',l+='
Avg Response
Last Check
';for(const p of s){const E=p.status==="up",U=E?"var(--dot-ok)":"var(--dot-bad)",_=p.uptime?.["24h"]??"-",F=p.uptime?.["7d"]??"-",q=p.avgResponseTime!=null?Math.round(p.avgResponseTime)+"ms":"-",J=p.timestamp?timeAgo(p.timestamp):"-";l+=`
`,l+=`
${escapeHtml(p.name||p.serviceId)}
`,l+=`
${E?"Up":"Down"}
`,l+=`
${typeof _=="number"?_.toFixed(1)+"%":_}
`,l+=`
${typeof F=="number"?F.toFixed(1)+"%":F}
`,l+=`
${q}
`,l+=`
${J}
`,l+="
",l+=`
Loading details...
`}l+="
",$.innerHTML=l,D.textContent="Updated "+new Date().toLocaleTimeString(),$.querySelectorAll("tr[data-health-id]").forEach(p=>{p.addEventListener("click",async()=>{const E=p.dataset.healthId,U=document.getElementById("health-detail-"+E);if(U){if(U.style.display!=="none"){U.style.display="none";return}U.style.display="";try{const F=await(await fetch(`/api/v1/health-checks/${E}/stats?hours=24`)).json();if(F.success&&F.stats){const q=F.stats,J=q.responseTime||{};U.querySelector("td").innerHTML=`
+ `);const h=document.getElementById("health-modal"),L=document.getElementById("health-check-btn"),A=document.getElementById("health-cancel"),I=document.getElementById("health-refresh-btn"),D=document.getElementById("health-status-container"),R=document.getElementById("health-incidents-container"),H=document.getElementById("health-config-container"),P=document.getElementById("health-last-update"),v=document.getElementById("health-add-btn"),C=document.getElementById("health-config-form"),x=document.getElementById("health-form-title"),$=document.getElementById("health-form-cancel"),E=document.getElementById("health-form-save"),M="dashcaddy-health-settings",S={retentionDays:30,pollingInterval:60,statsPollingInterval:30,maxEntriesPerService:500,diskUsageThreshold:80},B=document.getElementById("health-global-save"),U=document.getElementById("health-global-reset"),T=document.getElementById("health-global-status"),b=document.getElementById("health-setting-retention"),N=document.getElementById("health-setting-interval"),O=document.getElementById("health-setting-stats-interval"),p=document.getElementById("health-setting-max-entries"),u=document.getElementById("health-setting-disk-threshold");function f(){try{const i=safeGet(M),n=i?JSON.parse(i):{};return Object.assign({},S,n)}catch{return Object.assign({},S)}}function g(){const i=f();b&&(b.value=i.retentionDays),N&&(N.value=i.pollingInterval),O&&(O.value=i.statsPollingInterval),p&&(p.value=i.maxEntriesPerService),u&&(u.value=i.diskUsageThreshold)}function z(){const i={retentionDays:Math.max(1,Math.min(3650,parseInt(b?.value)||S.retentionDays)),pollingInterval:Math.max(5,Math.min(3600,parseInt(N?.value)||S.pollingInterval)),statsPollingInterval:Math.max(5,Math.min(3600,parseInt(O?.value)||S.statsPollingInterval)),maxEntriesPerService:Math.max(10,Math.min(1e5,parseInt(p?.value)||S.maxEntriesPerService)),diskUsageThreshold:Math.max(50,Math.min(99,parseInt(u?.value)||S.diskUsageThreshold))};try{safeSet(M,JSON.stringify(i)),g(),T&&(T.textContent="Saved \u2713",T.style.color="var(--ok-fg)",setTimeout(()=>{T&&(T.textContent="")},2500)),typeof showNotification=="function"&&showNotification("Global health settings saved","success")}catch(n){T&&(T.textContent="Save failed",T.style.color="var(--bad-fg)"),typeof showNotification=="function"&&showNotification("Failed to save settings: "+n.message,"error")}}function r(){try{safeSet(M,JSON.stringify(S))}catch{}g(),T&&(T.textContent="Reset to defaults \u2713",T.style.color="var(--ok-fg)",setTimeout(()=>{T&&(T.textContent="")},2500))}g(),B?.addEventListener("click",z),U?.addEventListener("click",r);let c=null;function d(i){return i>=99.9?"var(--ok-fg)":i>=95?"#f39c12":"var(--bad-fg)"}function w(i){const n={critical:"var(--bad-fg)",high:"#ff6b6b",medium:"#f39c12",low:"var(--muted)"};return`${i}`}async function y(){try{const n=await(await fetch("/api/v1/health-checks/status")).json();if(!n.success||!n.status||Object.keys(n.status).length===0){D.innerHTML='
\u{1F3E5}No health checks configured. Go to the Configure tab to add services.
';return}const s=Object.values(n.status);let l='
';l+='
',l+='
Service
Status
',l+='
Uptime 24h
Uptime 7d
',l+='
Avg Response
Last Check
';for(const m of s){const k=m.status==="up",j=k?"var(--dot-ok)":"var(--dot-bad)",_=m.uptime?.["24h"]??"-",F=m.uptime?.["7d"]??"-",q=m.avgResponseTime!=null?Math.round(m.avgResponseTime)+"ms":"-",J=m.timestamp?timeAgo(m.timestamp):"-";l+=`
';for(const _ of U){const F=_.status==="resolved",q=F&&_.duration?_.duration<6e4?Math.round(_.duration/1e3)+"s":Math.round(_.duration/6e4)+"m":"-";p+='
',p+=`
${escapeHtml(_.serviceId)}
`,p+=`
${escapeHtml(_.type)}
`,p+=`
${k(_.severity)}
`,p+=`
${_.status}
`,p+=`
${q}
`,p+=`
${timeAgo(_.createdAt)}
`,p+="
"}p+="
"}P.innerHTML=p||'
\u{1F6A8}No incidents recorded yet.
'}catch(i){P.innerHTML=`
Failed: ${escapeHtml(i.message)}
`}}async function e(){try{const n=await(await fetch("/api/v1/health-checks/status")).json(),s=n.success&&n.status?Object.values(n.status):[];if(s.length===0){N.innerHTML='
\u2699\uFE0FNo health checks configured yet. Click "Add Health Check" below.
';return}let l='
';l+='
Service
Status
SLA Target
Actions
';for(const p of s){const E=p.status==="up";l+='
',l+=`
${escapeHtml(p.name||p.serviceId)}
`,l+=`
${E?"Up":"Down"}
`,l+=`
${p.sla?.target?p.sla.target+"%":"-"}
`,l+='
',l+=``,l+=``,l+="
"}l+="
",N.innerHTML=l}catch(i){N.innerHTML=`
Failed: ${escapeHtml(i.message)}
`}}function a(i,n,s,l,p,E,U){c=i||null,w.textContent=i?"Edit Health Check":"Add Health Check",document.getElementById("health-form-id").value=i||"",document.getElementById("health-form-id").disabled=!!i,document.getElementById("health-form-name").value=n||"",document.getElementById("health-form-url").value=s||"",document.getElementById("health-form-timeout").value=l||1e4,document.getElementById("health-form-codes").value=p||"200",document.getElementById("health-form-sla").value=E||99.9,document.getElementById("health-form-slow").value=U||5e3,T.style.display="",v.style.display="none"}function o(){T.style.display="none",v.style.display="",c=null}v?.addEventListener("click",()=>a("","","",1e4,"200",99.9,5e3)),B?.addEventListener("click",o),S?.addEventListener("click",async()=>{const i=c||document.getElementById("health-form-id").value.trim();if(!i)return showNotification("Service ID is required","warning");const n=document.getElementById("health-form-url").value.trim();if(!n)return showNotification("URL is required","warning");const s=document.getElementById("health-form-codes").value.split(",").map(p=>parseInt(p.trim())).filter(Boolean),l={name:document.getElementById("health-form-name").value.trim()||i,url:n,timeout:parseInt(document.getElementById("health-form-timeout").value)||1e4,expectedStatusCodes:s.length?s:[200],sla:{target:parseFloat(document.getElementById("health-form-sla").value)||99.9},slowResponseThreshold:parseInt(document.getElementById("health-form-slow").value)||5e3};try{S.textContent="Saving...",S.disabled=!0;const E=await(await secureFetch(`/api/v1/health-checks/${encodeURIComponent(i)}/configure`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(l)})).json();if(!E.success)throw new Error(E.error||"Save failed");o(),e(),f()}catch(p){showNotification("Error: "+p.message,"error")}finally{S.textContent="Save",S.disabled=!1}}),document.addEventListener("health-edit",async i=>{const n=i.detail;a(n,"","",1e4,"200",99.9,5e3)}),document.addEventListener("health-delete",async i=>{const n=i.detail;if(confirm(`Delete health check for "${n}"?`))try{const l=await(await secureFetch(`/api/v1/health-checks/${encodeURIComponent(n)}/configure`,{method:"DELETE"})).json();if(!l.success)throw new Error(l.error);e(),f()}catch(s){showNotification("Error: "+s.message,"error")}}),L?.addEventListener("click",()=>{b?.classList.add("show"),f()}),wireModal(b,R),I?.addEventListener("click",f),document.querySelector('[data-panel="health-incidents"]')?.addEventListener("click",t),document.querySelector('[data-panel="health-config"]')?.addEventListener("click",e)})(),(function(){injectModal("updates-modal",`
';for(const _ of j){const F=_.status==="resolved",q=F&&_.duration?_.duration<6e4?Math.round(_.duration/1e3)+"s":Math.round(_.duration/6e4)+"m":"-";m+='
',m+=`
${escapeHtml(_.serviceId)}
`,m+=`
${escapeHtml(_.type)}
`,m+=`
${w(_.severity)}
`,m+=`
${_.status}
`,m+=`
${q}
`,m+=`
${timeAgo(_.createdAt)}
`,m+="
"}m+="
"}R.innerHTML=m||'
\u{1F6A8}No incidents recorded yet.
'}catch(i){R.innerHTML=`
Failed: ${escapeHtml(i.message)}
`}}async function e(){try{const n=await(await fetch("/api/v1/health-checks/status")).json(),s=n.success&&n.status?Object.values(n.status):[];if(s.length===0){H.innerHTML='
\u2699\uFE0FNo health checks configured yet. Click "Add Health Check" below.
';return}let l='
';l+='
Service
Status
SLA Target
Actions
';for(const m of s){const k=m.status==="up";l+='
',l+=`
${escapeHtml(m.name||m.serviceId)}
`,l+=`
${k?"Up":"Down"}
`,l+=`
${m.sla?.target?m.sla.target+"%":"-"}
`,l+='
',l+=``,l+=``,l+="
"}l+="
",H.innerHTML=l}catch(i){H.innerHTML=`
Failed: ${escapeHtml(i.message)}
`}}function a(i,n,s,l,m,k,j){c=i||null,x.textContent=i?"Edit Health Check":"Add Health Check",document.getElementById("health-form-id").value=i||"",document.getElementById("health-form-id").disabled=!!i,document.getElementById("health-form-name").value=n||"",document.getElementById("health-form-url").value=s||"",document.getElementById("health-form-timeout").value=l||1e4,document.getElementById("health-form-codes").value=m||"200",document.getElementById("health-form-sla").value=k||99.9,document.getElementById("health-form-slow").value=j||5e3,C.style.display="",v.style.display="none"}function o(){C.style.display="none",v.style.display="",c=null}v?.addEventListener("click",()=>a("","","",1e4,"200",99.9,5e3)),$?.addEventListener("click",o),E?.addEventListener("click",async()=>{const i=c||document.getElementById("health-form-id").value.trim();if(!i)return showNotification("Service ID is required","warning");const n=document.getElementById("health-form-url").value.trim();if(!n)return showNotification("URL is required","warning");const s=document.getElementById("health-form-codes").value.split(",").map(m=>parseInt(m.trim())).filter(Boolean),l={name:document.getElementById("health-form-name").value.trim()||i,url:n,timeout:parseInt(document.getElementById("health-form-timeout").value)||1e4,expectedStatusCodes:s.length?s:[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(i)}/configure`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(l)})).json();if(!k.success)throw new Error(k.error||"Save failed");o(),e(),y()}catch(m){showNotification("Error: "+m.message,"error")}finally{E.textContent="Save",E.disabled=!1}}),document.addEventListener("health-edit",async i=>{const n=i.detail;a(n,"","",1e4,"200",99.9,5e3)}),document.addEventListener("health-delete",async i=>{const n=i.detail;if(confirm(`Delete health check for "${n}"?`))try{const l=await(await secureFetch(`/api/v1/health-checks/${encodeURIComponent(n)}/configure`,{method:"DELETE"})).json();if(!l.success)throw new Error(l.error);e(),y()}catch(s){showNotification("Error: "+s.message,"error")}}),L?.addEventListener("click",()=>{h?.classList.add("show"),y()}),wireModal(h,A),I?.addEventListener("click",y),document.querySelector('[data-panel="health-incidents"]')?.addEventListener("click",t),document.querySelector('[data-panel="health-config"]')?.addEventListener("click",e)})(),(function(){injectModal("updates-modal",`
\u2B06\uFE0F Update Management
@@ -1543,17 +1543,17 @@ This will replace current configuration, credentials, and data. Containers will
- `);const b=document.getElementById("updates-modal"),L=document.getElementById("updates-btn"),R=document.getElementById("updates-cancel"),I=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 v(){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 n of e){const s=(()=>{const l=window.APPS||[];for(const p of l)if(p.containerId===n.containerId||p.name===n.containerName||p.id===n.containerName)return p.id;return n.containerName})();a+=`
`,a+=`
${escapeHtml(n.containerName)}
`,a+=`
${escapeHtml(n.imageName)}
`,a+=`
${escapeHtml(n.currentDigest)}
`,a+=`
${escapeHtml(n.latestDigest)}
`,a+='
',a+=``,a+=``,a+="
"}a+="
",$.innerHTML=a,D.textContent=e.length+" update(s) available";const o=document.getElementById("updates-count-badge"),i=document.getElementById("updates-update-all-btn");o&&(o.textContent=e.length+" pending",o.style.display=""),i&&e.length>0&&(i.style.display=""),window._pendingUpdates=e,$.querySelectorAll(".update-now-btn").forEach(n=>{n.addEventListener("click",async()=>{const s=n.dataset.id,l=n.dataset.name;if(confirm(`Update "${l}" to the latest version? The container will restart.`)){n.textContent="Updating...",n.disabled=!0;try{const E=await(await secureFetch(`/api/v1/updates/update/${encodeURIComponent(s)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({autoRollback:!0})})).json();if(E.success)n.textContent="Done!",n.style.background="var(--ok-fg)",setTimeout(()=>v(),2e3);else throw new Error(E.error||"Update failed")}catch(p){n.textContent="Failed",n.style.color="var(--bad-fg)",showNotification("Update error: "+p.message,"error"),setTimeout(()=>{n.textContent="Update",n.disabled=!1,n.style.color="",n.style.background=""},3e3)}}})}),$.querySelectorAll(".rollback-btn").forEach(n=>{n.addEventListener("click",async()=>{const s=n.dataset.id,l=n.dataset.name;if(confirm(`Rollback "${l}" to its previous version?`)){n.textContent="Rolling back...",n.disabled=!0;try{const E=await(await secureFetch(`/api/v1/updates/rollback/${encodeURIComponent(s)}`,{method:"POST"})).json();if(E.success)n.textContent="Rolled back!",setTimeout(()=>v(),2e3);else throw new Error(E.error||"Rollback failed")}catch(p){n.textContent="Failed",showNotification("Rollback error: "+p.message,"error"),setTimeout(()=>{n.textContent="Rollback",n.disabled=!1},3e3)}}})})}catch(f){$.innerHTML=`
Failed: ${escapeHtml(f.message)}
`}}async function T(){const f=window._pendingUpdates||[];if(!f.length)return;const t=document.getElementById("updates-update-all-btn");if(!confirm(`Update all ${f.length} containers? Each will restart.`))return;t.textContent="\u23F3 Updating...",t.disabled=!0;let e=0,a=0;for(const o of f)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++:a++}catch{a++}t.textContent="\u2705 Done",showNotification(`Update all: ${e} succeeded, ${a} failed.`,e>0&&a===0?"success":"error"),setTimeout(()=>{t.textContent="\u2B06\uFE0F Update All",t.disabled=!1,v()},3e3)}document.getElementById("updates-update-all-btn")?.addEventListener("click",T);async function w(){I.textContent="\u{1F50D} Checking...",I.disabled=!0;try{const t=await(await secureFetch("/api/v1/updates/check",{method:"POST"})).json();if(!t.success)throw new Error(t.error);I.textContent="\u2705 Done!",await v()}catch(f){I.textContent="\u274C Failed",showNotification("Check error: "+f.message,"error")}setTimeout(()=>{I.textContent="\u{1F50D} Check for Updates",I.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.
';n+='
',n+='
Container
Schedule
Window
Rollback
Last Run
Actions
';for(const s of o){const l=s.name||s.Names?.[0]?.replace(/^\//,"")||s.Id?.substring(0,12),p=s.containerId||s.Id,E=i[p]||{},U=E.enabled?E.schedule||"weekly":"",_=E.autoRollback!==!1,F=E.maintenanceWindow||"",q=E.lastAutoUpdate?timeAgo(E.lastAutoUpdate):"Never";n+=`
`}}const H=document.getElementById("dashcaddy-current-version"),C=document.getElementById("dashcaddy-update-badge"),y=document.getElementById("dashcaddy-update-details"),O=document.getElementById("dashcaddy-new-version"),h=document.getElementById("dashcaddy-changelog"),z=document.getElementById("dashcaddy-apply-btn"),A=document.getElementById("dashcaddy-check-btn"),j=document.getElementById("dashcaddy-rollback-btn"),u=document.getElementById("dashcaddy-status-bar"),m=document.getElementById("dashcaddy-history-container");let x=null;function g(f,t){u&&(u.style.display="block",u.style.background=t==="error"?"var(--bad-bg)":t==="success"?"var(--ok-bg)":"var(--bg)",u.style.color=t==="error"?"var(--bad-fg)":t==="success"?"var(--ok-fg)":"var(--fg)",u.textContent=f)}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(f){f||(A.textContent="Checking...",A.disabled=!0);try{const e=await(await fetch("/api/v1/system/update-check")).json();if(x=e,e.success&&e.available&&e.remote){C.style.display="",y.style.display="",O.textContent="v"+e.remote.version,h.textContent=e.remote.changelog||"No changelog available.";const a=document.getElementById("updates-btn");if(a&&!a.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);",a.style.position="relative",a.appendChild(i)}const o=document.getElementById("updates-dashcaddy-tab");if(o&&!o.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;",o.appendChild(i)}}else C.style.display="none",y.style.display="none",await M(),f||g("You are running the latest version.","success");f||(A.textContent="Check for Updates",A.disabled=!1)}catch(t){f||(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(f){throw g("Update failed: "+f.message,"error"),z.textContent="Update Now",z.disabled=!1,f}}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){m.innerHTML='
\u{1F4E6}No self-update history.
';return}let a='
';a+='
When
Version
From
Status
';for(const o of e){const i=o.status==="success"?"\u2713 success":o.status==="pending"?"\u23F3 pending":o.status==="partial"?"\u26A0 partial":"\u2717 "+o.status,n=o.status==="success"?"var(--ok-fg)":o.status==="pending"?"var(--muted)":"var(--bad-fg)";a+='
';for(const n of e){const s=(()=>{const l=window.APPS||[];for(const m of l)if(m.containerId===n.containerId||m.name===n.containerName||m.id===n.containerName)return m.id;return n.containerName})();a+=`
`,a+=`
${escapeHtml(n.containerName)}
`,a+=`
${escapeHtml(n.imageName)}
`,a+=`
${escapeHtml(n.currentDigest)}
`,a+=`
${escapeHtml(n.latestDigest)}
`,a+='
',a+=``,a+=``,a+="
"}a+="
",D.innerHTML=a,P.textContent=e.length+" update(s) available";const o=document.getElementById("updates-count-badge"),i=document.getElementById("updates-update-all-btn");o&&(o.textContent=e.length+" pending",o.style.display=""),i&&e.length>0&&(i.style.display=""),window._pendingUpdates=e,D.querySelectorAll(".update-now-btn").forEach(n=>{n.addEventListener("click",async()=>{const s=n.dataset.id,l=n.dataset.name;if(confirm(`Update "${l}" to the latest version? The container will restart.`)){n.textContent="Updating...",n.disabled=!0;try{const k=await(await secureFetch(`/api/v1/updates/update/${encodeURIComponent(s)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({autoRollback:!0})})).json();if(k.success)n.textContent="Done!",n.style.background="var(--ok-fg)",setTimeout(()=>v(),2e3);else throw new Error(k.error||"Update failed")}catch(m){n.textContent="Failed",n.style.color="var(--bad-fg)",showNotification("Update error: "+m.message,"error"),setTimeout(()=>{n.textContent="Update",n.disabled=!1,n.style.color="",n.style.background=""},3e3)}}})}),D.querySelectorAll(".rollback-btn").forEach(n=>{n.addEventListener("click",async()=>{const s=n.dataset.id,l=n.dataset.name;if(confirm(`Rollback "${l}" to its previous version?`)){n.textContent="Rolling back...",n.disabled=!0;try{const k=await(await secureFetch(`/api/v1/updates/rollback/${encodeURIComponent(s)}`,{method:"POST"})).json();if(k.success)n.textContent="Rolled back!",setTimeout(()=>v(),2e3);else throw new Error(k.error||"Rollback failed")}catch(m){n.textContent="Failed",showNotification("Rollback error: "+m.message,"error"),setTimeout(()=>{n.textContent="Rollback",n.disabled=!1},3e3)}}})})}catch(y){D.innerHTML=`
Failed: ${escapeHtml(y.message)}
`}}async function C(){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,a=0;for(const o of y)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++:a++}catch{a++}t.textContent="\u2705 Done",showNotification(`Update all: ${e} succeeded, ${a} failed.`,e>0&&a===0?"success":"error"),setTimeout(()=>{t.textContent="\u2B06\uFE0F Update All",t.disabled=!1,v()},3e3)}document.getElementById("updates-update-all-btn")?.addEventListener("click",C);async function x(){I.textContent="\u{1F50D} Checking...",I.disabled=!0;try{const t=await(await secureFetch("/api/v1/updates/check",{method:"POST"})).json();if(!t.success)throw new Error(t.error);I.textContent="\u2705 Done!",await v()}catch(y){I.textContent="\u274C Failed",showNotification("Check error: "+y.message,"error")}setTimeout(()=>{I.textContent="\u{1F50D} Check for Updates",I.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.
';n+='
',n+='
Container
Schedule
Window
Rollback
Last Run
Actions
';for(const s of o){const l=s.name||s.Names?.[0]?.replace(/^\//,"")||s.Id?.substring(0,12),m=s.containerId||s.Id,k=i[m]||{},j=k.enabled?k.schedule||"weekly":"",_=k.autoRollback!==!1,F=k.maintenanceWindow||"",q=k.lastAutoUpdate?timeAgo(k.lastAutoUpdate):"Never";n+=`
`}}const M=document.getElementById("dashcaddy-current-version"),S=document.getElementById("dashcaddy-update-badge"),B=document.getElementById("dashcaddy-update-details"),U=document.getElementById("dashcaddy-new-version"),T=document.getElementById("dashcaddy-changelog"),b=document.getElementById("dashcaddy-apply-btn"),N=document.getElementById("dashcaddy-check-btn"),O=document.getElementById("dashcaddy-rollback-btn"),p=document.getElementById("dashcaddy-status-bar"),u=document.getElementById("dashcaddy-history-container");let f=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 z(){try{const t=await(await fetch("/api/v1/system/version")).json();if(t.success){const e=t.commit&&t.commit!=="unknown"?t.commit:null;M.textContent="v"+t.version+(e?" ("+e.substring(0,7)+")":"")}}catch{M.textContent="Unable to fetch version"}}async function r(y){y||(N.textContent="Checking...",N.disabled=!0);try{const e=await(await fetch("/api/v1/system/update-check")).json();if(f=e,e.success&&e.available&&e.remote){S.style.display="",B.style.display="",U.textContent="v"+e.remote.version,T.textContent=e.remote.changelog||"No changelog available.";const a=document.getElementById("updates-btn");if(a&&!a.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);",a.style.position="relative",a.appendChild(i)}const o=document.getElementById("updates-dashcaddy-tab");if(o&&!o.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;",o.appendChild(i)}}else S.style.display="none",B.style.display="none",await z(),y||g("You are running the latest version.","success");y||(N.textContent="Check for Updates",N.disabled=!1)}catch(t){y||(g("Failed to check: "+t.message,"error"),N.textContent="Check for Updates",N.disabled=!1)}}async function c(){if(!confirm("Apply DashCaddy update? The API container will restart."))return!1;b.textContent="Updating...",b.disabled=!0,g("Downloading and applying update...","info");try{const 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"),b.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"),b.textContent="Update Now",b.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 a='
';a+='
When
Version
From
Status
';for(const o of e){const i=o.status==="success"?"\u2713 success":o.status==="pending"?"\u23F3 pending":o.status==="partial"?"\u26A0 partial":"\u2717 "+o.status,n=o.status==="success"?"var(--ok-fg)":o.status==="pending"?"var(--muted)":"var(--bad-fg)";a+='
"}}async function w(){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 a=prompt(`Available rollback versions:
`+e.join(`
`)+`
-Enter version to rollback to:`);if(!a)return;if(!e.includes(a)){showNotification("Invalid version: "+a,"error");return}if(!confirm("Rollback DashCaddy to v"+a+"? The container will restart."))return;g("Rolling back to v"+a+"...","info");const i=await(await secureFetch("/api/v1/system/rollback",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({version:a})})).json();if(i.success)g("Rollback to v"+a+" initiated. Container will restart.","success");else throw new Error(i.error||"Rollback failed")}catch(f){g("Rollback failed: "+f.message,"error")}}A?.addEventListener("click",()=>r(!1)),z?.addEventListener("click",()=>c().catch(()=>{})),j?.addEventListener("click",k),I?.addEventListener("click",w),L?.addEventListener("click",()=>{b?.classList.add("show"),v()}),wireModal(b,R),window.openUpdateModal=function(f){b?.classList.add("show"),v().then(()=>{if(!f)return;const t=$.querySelector(`[data-app-id="${f}"]`);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",S),document.querySelector('[data-panel="updates-dashcaddy"]')?.addEventListener("click",()=>{M(),d(),x||r(!0)}),window.dcApplyUpdate=c,window.dcCheckForUpdate=r,setTimeout(()=>r(!0),5e3)})(),(function(){injectModal("docker-resources-modal",`
+Enter version to rollback to:`);if(!a)return;if(!e.includes(a)){showNotification("Invalid version: "+a,"error");return}if(!confirm("Rollback DashCaddy to v"+a+"? The container will restart."))return;g("Rolling back to v"+a+"...","info");const i=await(await secureFetch("/api/v1/system/rollback",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({version:a})})).json();if(i.success)g("Rollback to v"+a+" initiated. Container will restart.","success");else throw new Error(i.error||"Rollback failed")}catch(y){g("Rollback failed: "+y.message,"error")}}N?.addEventListener("click",()=>r(!1)),b?.addEventListener("click",()=>c().catch(()=>{})),O?.addEventListener("click",w),I?.addEventListener("click",x),L?.addEventListener("click",()=>{h?.classList.add("show"),v()}),wireModal(h,A),window.openUpdateModal=function(y){h?.classList.add("show"),v().then(()=>{if(!y)return;const t=D.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",$),document.querySelector('[data-panel="updates-auto"]')?.addEventListener("click",E),document.querySelector('[data-panel="updates-dashcaddy"]')?.addEventListener("click",()=>{z(),d(),f||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.
@@ -1602,7 +1602,7 @@ Enter version to rollback to:`);if(!a)return;if(!e.includes(a)){showNotification
",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(S){showNotification("Delete failed: "+S.message,"error"),B.textContent="Delete",B.disabled=!1}}})})}catch(v){D.innerHTML=`
Failed: ${escapeHtml(v.message)}
`}}document.getElementById("dr-vol-create")?.addEventListener("click",async()=>{const D=document.getElementById("dr-vol-name"),v=D.value.trim();if(!v){showNotification("Enter a volume name","warning");return}try{await postJSON("/api/v1/docker/volumes",{name:v}),D.value="",showNotification(`Volume "${v}" 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 S=["bridge","host","none"].includes(B.name);w+='
",P.innerHTML=x,P.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`),D()}catch(E){showNotification("Delete failed: "+E.message,"error"),$.textContent="Delete",$.disabled=!1}}})})}catch(v){P.innerHTML=`
Failed: ${escapeHtml(v.message)}
`}}document.getElementById("dr-vol-create")?.addEventListener("click",async()=>{const P=document.getElementById("dr-vol-name"),v=P.value.trim();if(!v){showNotification("Enter a volume name","warning");return}try{await postJSON("/api/v1/docker/volumes",{name:v}),P.value="",showNotification(`Volume "${v}" created`,"success"),D()}catch(C){showNotification("Create failed: "+C.message,"error")}});async function R(){const P=document.getElementById("dr-net-list");try{const C=(await getJSON("/api/v1/docker/networks")).networks||[];if(C.length===0){P.innerHTML='
\u{1F310}No networks found.
';return}let x='
';x+='
Name
Driver
Scope
Containers
Actions
';for(const $ of C){const E=["bridge","host","none"].includes($.name);x+='
';for(const w of v.results){const B=w.status==="deployed"||w.status==="created"?"\u2705":w.status==="exists"?"\u26A1":w.status==="skipped"?"\u23ED":"\u274C";T+='
';for(const x of v.results){const $=x.status==="deployed"||x.status==="created"?"\u2705":x.status==="exists"?"\u26A1":x.status==="skipped"?"\u23ED":"\u274C";C+='
`}}L?.addEventListener("click",()=>{b?.classList.add("show"),w(!1)}),wireModal(b,R),I?.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 S=await(await secureFetch("/api/v1/audit-logs",{method:"DELETE"})).json();S.success?w(!1):showNotification("Error: "+(S.error||"Clear failed"),"error")}catch(B){showNotification("Error: "+B.message,"error")}})})(),(function(){injectModal("security-modal",`
+
`);const h=document.getElementById("audit-modal"),L=document.getElementById("audit-log-btn"),A=document.getElementById("audit-cancel"),I=document.getElementById("audit-refresh-btn"),D=document.getElementById("audit-clear-btn"),R=document.getElementById("audit-filter"),H=document.getElementById("audit-log-container"),P=document.getElementById("audit-load-more");let v=0;const C=50;async function x($){try{$||(v=0,H.innerHTML='
`}}L?.addEventListener("click",()=>{h?.classList.add("show"),x(!1)}),wireModal(h,A),I?.addEventListener("click",()=>x(!1)),R?.addEventListener("change",()=>x(!1)),P?.addEventListener("click",()=>x(!0)),D?.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?x(!1):showNotification("Error: "+(E.error||"Clear failed"),"error")}catch($){showNotification("Error: "+$.message,"error")}})})(),(function(){injectModal("security-modal",`
\u{1F6E1}\uFE0F Security Center
@@ -1771,39 +1771,39 @@ Enter version to rollback to:`);if(!a)return;if(!e.includes(a)){showNotification
`}async function u(){try{const c=(await(await fetch("/api/v1/security/hosts")).json()).data?.hosts||[],d=y.value;y.innerHTML=''+c.map(k=>``).join(""),d&&(y.value=d)}catch{}}document.getElementById("sec-host-register-btn").addEventListener("click",x),document.getElementById("sec-hosts-refresh").addEventListener("click",m);async function m(){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.
`}async function p(){try{const c=(await(await fetch("/api/v1/security/hosts")).json()).data?.hosts||[],d=B.value;B.innerHTML=''+c.map(w=>``).join(""),d&&(B.value=d)}catch{}}document.getElementById("sec-host-register-btn").addEventListener("click",f),document.getElementById("sec-hosts-refresh").addEventListener("click",u);async function u(){try{const c=(await(await fetch("/api/v1/security/hosts")).json()).data?.hosts||[];H=c;const d=document.getElementById("sec-hosts-container");if(!c.length){d.innerHTML='
No hosts registered. Click \u2795 Register Host to add one.
`}).join(""),d.querySelectorAll(".sec-host-del").forEach(k=>{k.addEventListener("click",async()=>{confirm(`Remove host ${k.dataset.id}? Events already received will remain in the store.`)&&(await fetch(`/api/v1/security/hosts/${encodeURIComponent(k.dataset.id)}`,{method:"DELETE"}),m())})})}catch(r){document.getElementById("sec-hosts-container").innerHTML='
Load failed: '+g(r.message)+"
"}}async function x(){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 k=await fetch("/api/v1/security/hosts",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:r,label:c,type:d})}),f=await k.json();if(!k.ok){alert("Failed: "+(f?.error?.message||k.statusText));return}alert(`\u2705 Host registered!
+ `}).join(""),d.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"}),u())})})}catch(r){document.getElementById("sec-hosts-container").innerHTML='
Load failed: '+g(r.message)+"
"}}async function f(){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 w=await fetch("/api/v1/security/hosts",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:r,label:c,type:d})}),y=await w.json();if(!w.ok){alert("Failed: "+(y?.error?.message||w.statusText));return}alert(`\u2705 Host registered!
-id: ${f.data.host.id}
-label: ${f.data.host.label}
-type: ${f.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):
-${f.data.api_key}
+${y.data.api_key}
Send this key as: Authorization: Bearer
-To endpoint: POST /api/v1/security/events/ingest or /events/batch`),m()}catch(k){alert("Failed: "+k.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 b=new ErrorHandler;injectModal("weather-modal",`
Weather Settings
+To endpoint: POST /api/v1/security/events/ingest or /events/batch`),u()}catch(w){alert("Failed: "+w.message)}}function g(r){return String(r).replace(/[&<>"']/g,c=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[c])}function z(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”
@@ -1814,23 +1814,23 @@ To endpoint: POST /api/v1/security/events/ingest or /events/batch`),m()}catch(k)
-
`);const L="weather-location",R="weather-zip",I="weather-geo",$="weather-unit";!safeGet(L)&&safeGet(R)&&safeSet(L,safeGet(R));function P(){return safeGet($)||"imperial"}function N(){return{icon:document.querySelector(".weather-icon"),temp:document.querySelector(".weather-temp"),condition:document.querySelector(".weather-condition"),location:document.querySelector(".weather-location"),wind:document.querySelector(".weather-wind")}}const D={0:"Clear sky",1:"Mainly clear",2:"Partly cloudy",3:"Overcast",45:"Fog",48:"Rime fog",51:"Light drizzle",53:"Drizzle",55:"Dense drizzle",56:"Freezing drizzle",57:"Dense freezing drizzle",61:"Light rain",63:"Moderate rain",65:"Heavy rain",66:"Light freezing rain",67:"Heavy freezing rain",71:"Light snow",73:"Moderate snow",75:"Heavy snow",77:"Snow grains",80:"Light showers",81:"Moderate showers",82:"Violent showers",85:"Light snow showers",86:"Heavy snow showers",95:"Thunderstorm",96:"Thunderstorm with hail",99:"Severe thunderstorm"},v={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 w(h){return T[Math.round(h/22.5)%16]}async function B(h){const z=safeGet(I);if(z)try{const x=JSON.parse(z);if(x.query===h)return x}catch{}const A=await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(h)}&count=1&language=en&format=json`);if(!A.ok)throw new Error("Geocoding failed");const j=await A.json();if(!j.results||!j.results.length)throw new Error("Location not found");const u=j.results[0],m={query:h,lat:u.latitude,lon:u.longitude,city:u.name,state:u.admin1||"",country:u.country||"",countryCode:u.country_code||""};return safeSet(I,JSON.stringify(m)),m}function S(h){return h.countryCode==="US"&&h.state?`${h.city}, ${h.state}`:h.country?`${h.city}, ${h.country}`:h.city}async function H(h){try{const z=await B(h),A=P(),j=A==="metric"?"celsius":"fahrenheit",u=A==="metric"?"kmh":"mph",m=`https://api.open-meteo.com/v1/forecast?latitude=${z.lat}&longitude=${z.lon}¤t=temperature_2m,weather_code,wind_speed_10m,wind_direction_10m&temperature_unit=${j}&wind_speed_unit=${u}`,x=await fetch(m);if(!x.ok)throw new Error("Weather fetch failed");const M=(await x.json()).current,r=M.weather_code;return{temp:Math.round(M.temperature_2m),condition:D[r]||"Unknown",icon:v[r]||"\u{1F324}\uFE0F",locationStr:S(z),windSpeed:Math.round(M.wind_speed_10m),windDir:w(M.wind_direction_10m),unit:A}}catch(z){return console.warn("Weather fetch failed:",z),null}}async function C(){const h=N();if(!h.icon||!h.temp||!h.condition||!h.location||!h.wind){console.warn("Weather widget elements not found");return}const z=safeGet(L);if(!z){h.location.textContent="Set Location",h.temp.textContent="--\xB0",h.condition.textContent="Click \u2699\uFE0F to configure",h.wind.textContent="--",h.icon.innerHTML='\u{1F324}\uFE0F';return}try{const A=await H(z);if(A){const j=A.unit==="metric"?"\xB0C":"\xB0F",u=A.unit==="metric"?"km/h":"mph";h.location.textContent=A.locationStr,h.temp.textContent=`${A.temp}${j}`,h.condition.textContent=A.condition,h.wind.textContent=`Wind: ${A.windSpeed} ${u} ${A.windDir}`,h.icon.innerHTML=`${escapeHtml(A.icon)}`}}catch(A){b.logError("[Weather] Update Error",A,{function:"updateWeather"}),h.location.textContent="Weather Error",h.temp.textContent="Error",h.condition.textContent="Failed to load",h.wind.textContent="--"}}const y=document.getElementById("weather-modal"),O=document.getElementById("weather-location-input");document.getElementById("weather-settings")?.addEventListener("click",()=>{O.value=safeGet(L)||"";const h=P(),z=y.querySelector(`input[name="weather-unit-radio"][value="${h}"]`);z&&(z.checked=!0),y.classList.add("show"),O.focus()}),document.getElementById("weather-cancel")?.addEventListener("click",()=>{y.classList.remove("show")}),document.getElementById("weather-save")?.addEventListener("click",()=>{const h=O.value.trim();if(h){safeGet(L)!==h&&safeSet(I,""),safeSet(L,h);const A=y.querySelector('input[name="weather-unit-radio"]:checked'),j=A?A.value:"imperial",u=P();safeSet($,j),u!==j&&safeSet(I,""),y.classList.remove("show"),C()}else showNotification("Please enter a location (e.g., Hamburg, London, 90210)","warning")}),wireModal(y),document.addEventListener("keydown",h=>{h.key==="Escape"&&y.classList.contains("show")&&y.classList.remove("show")}),C(),setInterval(C,DC.POLL.WEATHER)})(),(function(){const b=document.getElementById("clock-widget"),L=document.getElementById("clock-render");if(!b||!L)return;const R=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],I=["January","February","March","April","May","June","July","August","September","October","November","December"],$=["XII","I","II","III","IV","V","VI","VII","VIII","IX","X","XI"];let P=safeGet("clock-style")||"default",N=-1,D=!1,v="",T="",w=null,B=null;function S(t){if(D||safeGet("clock-chimes")!=="true")return;D=!0;const e=parseInt(safeGet("clock-chime-volume")||"50",10)/100;let a=0;function o(){if(a>=t){D=!1;return}const i=new Audio("/assets/sounds/church-bell.mp3");i.volume=e,i.play().catch(()=>{}),a++,a{D=!1},2500)}o()}function H(t){return R[t.getDay()]+", "+I[t.getMonth()]+" "+t.getDate()+", "+t.getFullYear()}function C(){T="",w=null}function y(){return T!=="digital"&&(L.innerHTML='