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