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