From c90851f25f79fa8d71c61b6276ce66c226e566fd Mon Sep 17 00:00:00 2001 From: DashCaddy Polish Loop Date: Mon, 14 Sep 2026 03:49:35 -0700 Subject: [PATCH] =?UTF-8?q?DC-130:=20deploys=20panel=20frontend=20?= =?UTF-8?q?=E2=80=94=20modal=20with=20services/deploy/journal=20tabs,=20ro?= =?UTF-8?q?llback=20+=20status=20actions=20(bundle=20+=20cache=20tag=20reb?= =?UTF-8?q?uilt)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- status/dist/features.js | 368 +++++++++++++++++++++++----------------- status/sw.js | 2 +- 2 files changed, 211 insertions(+), 159 deletions(-) diff --git a/status/dist/features.js b/status/dist/features.js index 8a196ef..412a0ae 100644 --- a/status/dist/features.js +++ b/status/dist/features.js @@ -90,44 +90,44 @@ - `);const k=document.getElementById("logo-modal"),B=document.getElementById("logo-preview-dark"),A=document.getElementById("logo-preview-light"),I=document.getElementById("logo-status"),z=document.getElementById("logo-same-both"),P=document.getElementById("logo-dual-uploads"),M=document.getElementById("logo-single-upload"),D=document.getElementById("logo-upload-dark"),b=document.getElementById("logo-upload-light"),T=document.getElementById("logo-upload-single"),h=document.querySelector("#brand .brand-logo-dark"),H=document.querySelector("#brand .brand-logo-light"),S=document.querySelector(".top-row"),$=document.getElementById("dashboard-title"),f=DC.NAME;let E=null,O=null,L=null,j="left",N=f;z?.addEventListener("change",()=>{z.checked?(P.style.display="none",M.style.display="",E=null,O=null):(P.style.display="flex",M.style.display="none",L=null)});function R(t,e){if(!t||!t.type.startsWith("image/")){showNotification("Please select an image file","warning");return}const a=new FileReader;a.onload=o=>e(o.target.result),a.readAsDataURL(t)}D?.addEventListener("change",t=>{R(t.target.files[0],e=>{E=e,B.src=e,I.textContent="New dark logo ready to save"})}),b?.addEventListener("change",t=>{R(t.target.files[0],e=>{O=e,A.src=e,I.textContent="New light logo ready to save"})}),T?.addEventListener("change",t=>{R(t.target.files[0],e=>{L=e,B.src=e,A.src=e,I.textContent="New logo ready to save (both themes)"})});function u(t){S.setAttribute("data-logo-pos",t),document.querySelectorAll(".logo-pos-btn").forEach(e=>{e.style.background=e.dataset.pos===t?"var(--accent)":"var(--card-bg)",e.style.color=e.dataset.pos===t?"white":"var(--fg)"})}function v(t){N=t||f,document.title=N;const e=document.querySelector(".dashboard-title");e&&(e.textContent=N)}async function w(){try{const t=await fetch("/api/v1/logo");if(t.ok){const e=await t.json();e.customLogoDark&&(h.src=e.customLogoDark,B.src=e.customLogoDark),e.customLogoLight&&(H.src=e.customLogoLight,A.src=e.customLogoLight),!e.customLogoDark&&!e.customLogoLight&&e.customLogo&&(h.src=e.customLogo,H.src=e.customLogo,B.src=e.customLogo,A.src=e.customLogo),e.isDefault||(I.textContent="Using custom logo"),e.position&&(j=e.position,u(e.position)),e.dashboardTitle&&v(e.dashboardTitle)}}catch(t){console.warn("Could not load custom logo:",t.message)}}document.querySelectorAll(".logo-pos-btn").forEach(t=>{t.addEventListener("click",()=>{j=t.dataset.pos,u(j)})}),document.getElementById("brand")?.addEventListener("click",()=>{E=null,O=null,L=null,D&&(D.value=""),b&&(b.value=""),T&&(T.value=""),z&&(z.checked=!1),P.style.display="flex",M.style.display="none",B.src=h.src,A.src=H.src;const t=h.src.includes("custom-logo")||H.src.includes("custom-logo");I.textContent=t?"Using custom logo":"Using default logos",u(j),$.value=N,k.classList.add("show")}),document.getElementById("logo-save")?.addEventListener("click",async()=>{try{const t=$.value.trim()||f,e={position:j,dashboardTitle:t};z?.checked&&L?(e.dataDark=L,e.dataLight=L):(E&&(e.dataDark=E),O&&(e.dataLight=O));const a=await secureFetch("/api/v1/logo",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});if(a.ok){const o=await a.json(),i="?t="+Date.now();o.pathDark&&(h.src=o.pathDark+i,B.src=o.pathDark+i),o.pathLight&&(H.src=o.pathLight+i,A.src=o.pathLight+i),u(j),v(t),k.classList.remove("show")}else{const o=await a.json();showNotification("Failed to save: "+o.error,"error")}}catch(t){showNotification("Error saving: "+t.message,"error")}}),document.getElementById("logo-reset")?.addEventListener("click",async()=>{if(confirm(`Reset all branding to DashCaddy defaults? + `);const b=document.getElementById("logo-modal"),C=document.getElementById("logo-preview-dark"),j=document.getElementById("logo-preview-light"),k=document.getElementById("logo-status"),z=document.getElementById("logo-same-both"),P=document.getElementById("logo-dual-uploads"),H=document.getElementById("logo-single-upload"),A=document.getElementById("logo-upload-dark"),x=document.getElementById("logo-upload-light"),B=document.getElementById("logo-upload-single"),w=document.querySelector("#brand .brand-logo-dark"),M=document.querySelector("#brand .brand-logo-light"),L=document.querySelector(".top-row"),$=document.getElementById("dashboard-title"),u=DC.NAME;let v=null,D=null,I=null,R="left",N=u;z?.addEventListener("change",()=>{z.checked?(P.style.display="none",H.style.display="",v=null,D=null):(P.style.display="flex",H.style.display="none",I=null)});function O(t,e){if(!t||!t.type.startsWith("image/")){showNotification("Please select an image file","warning");return}const a=new FileReader;a.onload=o=>e(o.target.result),a.readAsDataURL(t)}A?.addEventListener("change",t=>{O(t.target.files[0],e=>{v=e,C.src=e,k.textContent="New dark logo ready to save"})}),x?.addEventListener("change",t=>{O(t.target.files[0],e=>{D=e,j.src=e,k.textContent="New light logo ready to save"})}),B?.addEventListener("change",t=>{O(t.target.files[0],e=>{I=e,C.src=e,j.src=e,k.textContent="New logo ready to save (both themes)"})});function m(t){L.setAttribute("data-logo-pos",t),document.querySelectorAll(".logo-pos-btn").forEach(e=>{e.style.background=e.dataset.pos===t?"var(--accent)":"var(--card-bg)",e.style.color=e.dataset.pos===t?"white":"var(--fg)"})}function y(t){N=t||u,document.title=N;const e=document.querySelector(".dashboard-title");e&&(e.textContent=N)}async function S(){try{const t=await fetch("/api/v1/logo");if(t.ok){const e=await t.json();e.customLogoDark&&(w.src=e.customLogoDark,C.src=e.customLogoDark),e.customLogoLight&&(M.src=e.customLogoLight,j.src=e.customLogoLight),!e.customLogoDark&&!e.customLogoLight&&e.customLogo&&(w.src=e.customLogo,M.src=e.customLogo,C.src=e.customLogo,j.src=e.customLogo),e.isDefault||(k.textContent="Using custom logo"),e.position&&(R=e.position,m(e.position)),e.dashboardTitle&&y(e.dashboardTitle)}}catch(t){console.warn("Could not load custom logo:",t.message)}}document.querySelectorAll(".logo-pos-btn").forEach(t=>{t.addEventListener("click",()=>{R=t.dataset.pos,m(R)})}),document.getElementById("brand")?.addEventListener("click",()=>{v=null,D=null,I=null,A&&(A.value=""),x&&(x.value=""),B&&(B.value=""),z&&(z.checked=!1),P.style.display="flex",H.style.display="none",C.src=w.src,j.src=M.src;const t=w.src.includes("custom-logo")||M.src.includes("custom-logo");k.textContent=t?"Using custom logo":"Using default logos",m(R),$.value=N,b.classList.add("show")}),document.getElementById("logo-save")?.addEventListener("click",async()=>{try{const t=$.value.trim()||u,e={position:R,dashboardTitle:t};z?.checked&&I?(e.dataDark=I,e.dataLight=I):(v&&(e.dataDark=v),D&&(e.dataLight=D));const a=await secureFetch("/api/v1/logo",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});if(a.ok){const o=await a.json(),i="?t="+Date.now();o.pathDark&&(w.src=o.pathDark+i,C.src=o.pathDark+i),o.pathLight&&(M.src=o.pathLight+i,j.src=o.pathLight+i),m(R),y(t),b.classList.remove("show")}else{const o=await a.json();showNotification("Failed to save: "+o.error,"error")}}catch(t){showNotification("Error saving: "+t.message,"error")}}),document.getElementById("logo-reset")?.addEventListener("click",async()=>{if(confirm(`Reset all branding to DashCaddy defaults? -This will reset the logo, favicon, title, and position.`))try{if((await secureFetch("/api/v1/logo",{method:"DELETE"})).ok&&(h.src="/assets/dashcaddy-logo-dark.png",H.src="/assets/dashcaddy-logo-light.png",B.src="/assets/dashcaddy-logo-dark.png",A.src="/assets/dashcaddy-logo-light.png",I.textContent="Using default logos",E=null,O=null,L=null,$.value=f,v(f),j="left",u("left")),(await secureFetch("/api/v1/favicon",{method:"DELETE"})).ok){const a=document.querySelector('link[rel="icon"]'),o=document.getElementById("favicon-preview"),i=document.getElementById("favicon-status");a&&(a.href="/assets/dashcaddy-favicon.ico?t="+Date.now()),o&&(o.src="/assets/dashcaddy-favicon.ico?t="+Date.now()),i&&(i.textContent="Using DashCaddy favicon"),l=null}}catch(t){showNotification("Error resetting branding: "+t.message,"error")}}),wireModal(k,document.getElementById("logo-cancel"));const g=document.getElementById("favicon-preview"),x=document.getElementById("favicon-status"),s=document.getElementById("favicon-upload"),p=document.querySelector('link[rel="icon"]')||document.createElement("link");let l=null;document.querySelector('link[rel="icon"]')||(p.rel="icon",p.href="/assets/dashcaddy-favicon.ico",document.head.appendChild(p));async function m(){try{const t=await fetch("/api/v1/favicon");if(t.ok){const e=await t.json();e.customFavicon&&(p.href=e.customFavicon+"?t="+Date.now(),g.src=e.customFavicon+"?t="+Date.now(),x.textContent="Using custom favicon")}}catch(t){console.warn("Could not load custom favicon:",t.message)}}s?.addEventListener("change",t=>{const e=t.target.files[0];if(!e)return;if(!e.type.match(/^image\/(png|svg\+xml)$/)){showNotification("Please select a PNG or SVG file","warning"),s.value="";return}const a=new FileReader;a.onload=o=>{l=o.target.result,g.src=l,x.textContent="New favicon ready to save"},a.readAsDataURL(e)}),document.getElementById("logo-save")?.addEventListener("click",async()=>{if(l)try{const t=await secureFetch("/api/v1/favicon",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({data:l})});if(t.ok){const e=await t.json();p.href=e.path+"?t="+Date.now(),g.src=e.path+"?t="+Date.now(),x.textContent="Using custom favicon",l=null}else{const e=await t.json();showNotification("Failed to save favicon: "+e.error,"error")}}catch(t){showNotification("Error saving favicon: "+t.message,"error")}}),m(),w();const c=document.getElementById("settings-timezone");c&&(new MutationObserver(()=>{k.classList.contains("show")&&c.options.length===0&&(async()=>{let e;try{const a=await fetch("/api/v1/config");a.ok&&(e=(await a.json()).timezone)}catch{}window.populateTimezoneSelect(c,e)})()}).observe(k,{attributes:!0,attributeFilter:["class"]}),document.getElementById("logo-save")?.addEventListener("click",async()=>{const e=c.value;if(e)try{const a=await fetch("/api/v1/config");if(!a.ok)return;const o=await a.json();o.timezone=e,o.updatedAt=new Date().toISOString(),await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)})}catch(a){console.warn("Failed to save timezone:",a.message)}}))})(),window.populateTimezoneSelect=function(k,B){const A=Intl.supportedValuesOf("timeZone"),I=B||Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC";k.innerHTML="";for(const z of A){const P=document.createElement("option");P.value=z,P.textContent=z.replace(/_/g," "),z===I&&(P.selected=!0),k.appendChild(P)}},(function(){let k="homelab",B=null;async function A(){try{const v=await fetch("/api/v1/config");if(v.ok&&(B=await v.json(),B&&B.setupComplete))return document.getElementById("setup-wizard").style.display="none",!0}catch(v){console.warn("Could not fetch server config, checking localStorage fallback:",v.message)}return safeGet("dashcaddy-setup")?(document.getElementById("setup-wizard").style.display="none",!0):(document.getElementById("setup-wizard").style.display="flex",!1)}A();const I=document.getElementById("setup-timezone");I&&window.populateTimezoneSelect(I);function z(u){document.querySelectorAll(".setup-step").forEach(w=>{w.style.display="none"});const v=document.getElementById(u);v&&(v.style.display="block")}function P(){const u=document.getElementById("setup-summary-content");if(!u)return;let v='
';if(k==="homelab"){const g=document.getElementById("setup-tld")?.value?.trim()||".home",x=document.getElementById("setup-ca-name")?.value?.trim()||"",s=document.getElementById("setup-dns-ip")?.value?.trim()||"",p=document.getElementById("setup-dns-port")?.value?.trim()||DC.DEFAULTS.DNS_PORT;v+=` +This will reset the logo, favicon, title, and position.`))try{if((await secureFetch("/api/v1/logo",{method:"DELETE"})).ok&&(w.src="/assets/dashcaddy-logo-dark.png",M.src="/assets/dashcaddy-logo-light.png",C.src="/assets/dashcaddy-logo-dark.png",j.src="/assets/dashcaddy-logo-light.png",k.textContent="Using default logos",v=null,D=null,I=null,$.value=u,y(u),R="left",m("left")),(await secureFetch("/api/v1/favicon",{method:"DELETE"})).ok){const a=document.querySelector('link[rel="icon"]'),o=document.getElementById("favicon-preview"),i=document.getElementById("favicon-status");a&&(a.href="/assets/dashcaddy-favicon.ico?t="+Date.now()),o&&(o.src="/assets/dashcaddy-favicon.ico?t="+Date.now()),i&&(i.textContent="Using DashCaddy favicon"),l=null}}catch(t){showNotification("Error resetting branding: "+t.message,"error")}}),wireModal(b,document.getElementById("logo-cancel"));const f=document.getElementById("favicon-preview"),E=document.getElementById("favicon-status"),s=document.getElementById("favicon-upload"),p=document.querySelector('link[rel="icon"]')||document.createElement("link");let l=null;document.querySelector('link[rel="icon"]')||(p.rel="icon",p.href="/assets/dashcaddy-favicon.ico",document.head.appendChild(p));async function g(){try{const t=await fetch("/api/v1/favicon");if(t.ok){const e=await t.json();e.customFavicon&&(p.href=e.customFavicon+"?t="+Date.now(),f.src=e.customFavicon+"?t="+Date.now(),E.textContent="Using custom favicon")}}catch(t){console.warn("Could not load custom favicon:",t.message)}}s?.addEventListener("change",t=>{const e=t.target.files[0];if(!e)return;if(!e.type.match(/^image\/(png|svg\+xml)$/)){showNotification("Please select a PNG or SVG file","warning"),s.value="";return}const a=new FileReader;a.onload=o=>{l=o.target.result,f.src=l,E.textContent="New favicon ready to save"},a.readAsDataURL(e)}),document.getElementById("logo-save")?.addEventListener("click",async()=>{if(l)try{const t=await secureFetch("/api/v1/favicon",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({data:l})});if(t.ok){const e=await t.json();p.href=e.path+"?t="+Date.now(),f.src=e.path+"?t="+Date.now(),E.textContent="Using custom favicon",l=null}else{const e=await t.json();showNotification("Failed to save favicon: "+e.error,"error")}}catch(t){showNotification("Error saving favicon: "+t.message,"error")}}),g(),S();const c=document.getElementById("settings-timezone");c&&(new MutationObserver(()=>{b.classList.contains("show")&&c.options.length===0&&(async()=>{let e;try{const a=await fetch("/api/v1/config");a.ok&&(e=(await a.json()).timezone)}catch{}window.populateTimezoneSelect(c,e)})()}).observe(b,{attributes:!0,attributeFilter:["class"]}),document.getElementById("logo-save")?.addEventListener("click",async()=>{const e=c.value;if(e)try{const a=await fetch("/api/v1/config");if(!a.ok)return;const o=await a.json();o.timezone=e,o.updatedAt=new Date().toISOString(),await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)})}catch(a){console.warn("Failed to save timezone:",a.message)}}))})(),window.populateTimezoneSelect=function(b,C){const j=Intl.supportedValuesOf("timeZone"),k=C||Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC";b.innerHTML="";for(const z of j){const P=document.createElement("option");P.value=z,P.textContent=z.replace(/_/g," "),z===k&&(P.selected=!0),b.appendChild(P)}},(function(){let b="homelab",C=null;async function j(){try{const y=await fetch("/api/v1/config");if(y.ok&&(C=await y.json(),C&&C.setupComplete))return document.getElementById("setup-wizard").style.display="none",!0}catch(y){console.warn("Could not fetch server config, checking localStorage fallback:",y.message)}return safeGet("dashcaddy-setup")?(document.getElementById("setup-wizard").style.display="none",!0):(document.getElementById("setup-wizard").style.display="flex",!1)}j();const k=document.getElementById("setup-timezone");k&&window.populateTimezoneSelect(k);function z(m){document.querySelectorAll(".setup-step").forEach(S=>{S.style.display="none"});const y=document.getElementById(m);y&&(y.style.display="block")}function P(){const m=document.getElementById("setup-summary-content");if(!m)return;let y='
';if(b==="homelab"){const f=document.getElementById("setup-tld")?.value?.trim()||".home",E=document.getElementById("setup-ca-name")?.value?.trim()||"",s=document.getElementById("setup-dns-ip")?.value?.trim()||"",p=document.getElementById("setup-dns-port")?.value?.trim()||DC.DEFAULTS.DNS_PORT;y+=`

Home Lab Configuration

-
TLD: ${g}
-
Certificate Authority: ${x}
+
TLD: ${f}
+
Certificate Authority: ${E}
DNS Server: ${s}:${p}
-
Example URLs: https://uptime${g}, https://nextcloud${g}
+
Example URLs: https://uptime${f}, https://nextcloud${f}
- `}else if(k==="simple"){const g=document.getElementById("setup-simple-ip")?.value?.trim()||"localhost";v+=` + `}else if(b==="simple"){const f=document.getElementById("setup-simple-ip")?.value?.trim()||"localhost";y+=`

Simple Setup

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

Public Server

-
Domain: ${g}
+
Domain: ${f}
SSL: Let's Encrypt
-
Email: ${x}
+
Email: ${E}
Routing: ${s==="subdirectory"?"Subdirectory (domain.com/app)":"Subdomain (app.domain.com)"}
Example URLs: ${p}
- `}const w=document.getElementById("setup-timezone")?.value||Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC";v+=` + `}const S=document.getElementById("setup-timezone")?.value||Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC";y+=`
-
Timezone: ${w.replace(/_/g," ")}
+
Timezone: ${S.replace(/_/g," ")}
- `,v+="
",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='
Loading...
';try{const l=await(await fetch(`/api/v1/browse/directories?path=${encodeURIComponent(s)}`)).json();if(!l.success){j.innerHTML=`
Error: ${escapeHtml(l.error)}
`;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='
Loading...
';try{const l=await(await fetch(`/api/v1/browse/directories?path=${encodeURIComponent(s)}`)).json();if(!l.success){R.innerHTML=`
Error: ${escapeHtml(l.error)}
`;return}m=l.path||"",I.textContent=m||"Select a drive...";let g="";l.parent&&l.parent!==l.path&&(g+=`
\u2B06\uFE0F .. Parent Directory -
`),l.items.length===0&&!l.parent?m+='
No browseable drives configured. Check your docker-compose.yml volume mounts.
':l.items.length===0?m+='
No subfolders found
':l.items.forEach(c=>{const t=c.type==="drive"?"\u{1F4BE}":"\u{1F4C1}",e=v.includes(c.path),a=e?"background: color-mix(in srgb, var(--success) 20%, transparent);":"";m+=`
+
`),l.items.length===0&&!l.parent?g+='
No browseable drives configured. Check your docker-compose.yml volume mounts.
':l.items.length===0?g+='
No subfolders found
':l.items.forEach(c=>{const t=c.type==="drive"?"\u{1F4BE}":"\u{1F4C1}",e=y.includes(c.path),a=e?"background: color-mix(in srgb, var(--success) 20%, transparent);":"";g+=`
${t} ${escapeHtml(c.name)} ${e?'\u2713':""} -
`}),j.innerHTML=m,j.querySelectorAll(".folder-item").forEach(c=>{c.addEventListener("click",()=>{g(c.dataset.path)}),c.addEventListener("mouseenter",()=>{c.style.background="var(--card-bg)"}),c.addEventListener("mouseleave",()=>{const t=v.includes(c.dataset.path);c.style.background=t?"color-mix(in srgb, var(--success) 20%, transparent)":""})})}catch(p){j.innerHTML=`
Failed to load: ${escapeHtml(p.message)}
`}}function x(){if(v.length===0){N.style.display="none";return}N.style.display="block",R.innerHTML=v.map(s=>` +
`}),R.innerHTML=g,R.querySelectorAll(".folder-item").forEach(c=>{c.addEventListener("click",()=>{f(c.dataset.path)}),c.addEventListener("mouseenter",()=>{c.style.background="var(--card-bg)"}),c.addEventListener("mouseleave",()=>{const t=y.includes(c.dataset.path);c.style.background=t?"color-mix(in srgb, var(--success) 20%, transparent)":""})})}catch(p){R.innerHTML=`
Failed to load: ${escapeHtml(p.message)}
`}}function E(){if(y.length===0){N.style.display="none";return}N.style.display="block",O.innerHTML=y.map(s=>` ${escapeHtml(s)} - `).join("")}window.removeSelectedFolder=function(s){v=v.filter(p=>p!==s),x(),g(u)},document.getElementById("folder-browser-select-current").addEventListener("click",()=>{u&&!v.includes(u)&&(v.push(u),x(),g(u))}),wireModal(O,document.getElementById("folder-browser-cancel")),document.getElementById("folder-browser-done").addEventListener("click",()=>{w&&(w.value=v.join(", ")),O.classList.remove("show")}),f()})(),(function(){injectModal("recipe-deploy-modal",`
+ `).join("")}window.removeSelectedFolder=function(s){y=y.filter(p=>p!==s),E(),f(m)},document.getElementById("folder-browser-select-current").addEventListener("click",()=>{m&&!y.includes(m)&&(y.push(m),E(),f(m))}),wireModal(D,document.getElementById("folder-browser-cancel")),document.getElementById("folder-browser-done").addEventListener("click",()=>{S&&(S.value=y.join(", ")),D.classList.remove("show")}),u()})(),(function(){injectModal("recipe-deploy-modal",`

Deploy Recipe

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

\u{1F4CB} Error Logs

-
`);const k=document.getElementById("error-log-modal"),B=document.getElementById("view-error-logs"),A=document.getElementById("error-log-refresh"),I=document.getElementById("error-log-clear"),z=document.getElementById("error-log-close"),P=document.getElementById("error-log-level"),M=document.getElementById("error-log-context"),D=document.getElementById("error-log-search"),b=document.getElementById("error-log-since"),T=document.getElementById("error-log-until"),h=document.getElementById("error-log-container"),H=document.getElementById("error-log-load-more"),S=document.getElementById("error-log-total"),$=50;let f=0,E=null,O=0,L=[];function j(x){if(!x)return null;const s=new Date(x);return isNaN(s.getTime())?null:s.toISOString()}async function N(){try{const x=await fetch("/api/v1/error-logs/contexts");if(!x.ok)return;const s=await x.json();if(!s.success||!Array.isArray(s.contexts))return;L=s.contexts;const p=M.value;M.innerHTML='';for(const l of s.contexts){const m=document.createElement("option");m.value=l.name,m.textContent=`${l.name} (${l.count})`,M.appendChild(m)}p&&s.contexts.some(l=>l.name===p)&&(M.value=p)}catch{}}function R(){const x=new URLSearchParams;x.set("limit",String($)),x.set("offset",String(f)),P.value&&x.set("level",P.value),M.value&&x.set("context",M.value);const s=j(b.value),p=j(T.value);s&&x.set("since",s),p&&x.set("until",p);const l=(D.value||"").trim();return l&&x.set("search",l),x}async function u(x){try{x?(E&&E.abort(),E=new AbortController):(E&&E.abort(),E=new AbortController,f=0,O++,h.innerHTML='
Loading...
');const s=O,p=R(),l=await fetch("/api/v1/error-logs?"+p.toString(),{signal:E.signal});if(!l.ok){h.innerHTML=`
Failed: HTTP ${l.status}
`,H.style.display="none",S.textContent="";return}const m=await l.json();if(!m.success){h.innerHTML=`
Failed: ${escapeHtml(m.error||"unknown")}
`,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=`
\u{1F4CB}${escapeHtml(e)}
`,H.style.display="none",S.textContent=m.total?`${m.total} total`:"";return}let t="";x||(t='',t+='',t+='',t+='',t+='',t+='',t+='',t+="");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+='',t+=``,t+=``,t+=``,t+=``,t+=``,t+="",e.detail&&(t+=``)}if(!x)t+="
WhenLevelContextMessageIP
${escapeHtml(i)}${escapeHtml(a)}${escapeHtml(n)}${escapeHtml(r)}${escapeHtml(d)}
",h.innerHTML=t;else{const e=h.querySelector("table");e&&e.insertAdjacentHTML("beforeend",t)}f+=c.length,H.style.display=m.hasMore?"":"none",S.textContent=`${m.total} total${m.hasMore?" (showing "+f+")":""}`,h.querySelectorAll(".error-log-row").forEach(e=>{e.dataset.wired||(e.dataset.wired="true",e.addEventListener("click",()=>{const a=e.nextElementSibling;a&&a.classList.contains("error-log-detail")&&(a.style.display=a.style.display==="none"?"":"none")}))})}catch(s){if(s&&s.name==="AbortError")return;h.innerHTML=`
Failed: ${escapeHtml(s.message)}
`,S.textContent=""}}async function v(){if(confirm("Clear the entire error log? This cannot be undone."))try{const s=await(await secureFetch("/api/v1/error-logs",{method:"DELETE",headers:{"content-type":"application/json"},body:JSON.stringify({confirm:"CLEAR"})})).json();s.success?(await N(),u(!1),showNotification("\u2705 Error logs cleared","success",3e3)):showNotification("\u274C "+(s.error||"Clear failed"),"error",4e3)}catch(x){showNotification("\u274C "+x.message,"error",4e3)}}let w;function g(){P?.addEventListener("change",()=>u(!1)),M?.addEventListener("change",()=>u(!1)),D?.addEventListener("input",()=>{clearTimeout(w),w=setTimeout(()=>u(!1),250)});let x;[b,T].forEach(s=>{s?.addEventListener("change",()=>{clearTimeout(x),x=setTimeout(()=>u(!1),250)})}),A?.addEventListener("click",()=>u(!1)),H?.addEventListener("click",()=>u(!0)),I?.addEventListener("click",v),wireModal(k,z)}B?.addEventListener("click",async()=>{k?.classList.add("show"),await N(),u(!1)}),g()})(),(function(){injectModal("container-logs-modal",`
+
`);const b=document.getElementById("error-log-modal"),C=document.getElementById("view-error-logs"),j=document.getElementById("error-log-refresh"),k=document.getElementById("error-log-clear"),z=document.getElementById("error-log-close"),P=document.getElementById("error-log-level"),H=document.getElementById("error-log-context"),A=document.getElementById("error-log-search"),x=document.getElementById("error-log-since"),B=document.getElementById("error-log-until"),w=document.getElementById("error-log-container"),M=document.getElementById("error-log-load-more"),L=document.getElementById("error-log-total"),$=50;let u=0,v=null,D=0,I=[];function R(E){if(!E)return null;const s=new Date(E);return isNaN(s.getTime())?null:s.toISOString()}async function N(){try{const E=await fetch("/api/v1/error-logs/contexts");if(!E.ok)return;const s=await E.json();if(!s.success||!Array.isArray(s.contexts))return;I=s.contexts;const p=H.value;H.innerHTML='';for(const l of s.contexts){const g=document.createElement("option");g.value=l.name,g.textContent=`${l.name} (${l.count})`,H.appendChild(g)}p&&s.contexts.some(l=>l.name===p)&&(H.value=p)}catch{}}function O(){const E=new URLSearchParams;E.set("limit",String($)),E.set("offset",String(u)),P.value&&E.set("level",P.value),H.value&&E.set("context",H.value);const s=R(x.value),p=R(B.value);s&&E.set("since",s),p&&E.set("until",p);const l=(A.value||"").trim();return l&&E.set("search",l),E}async function m(E){try{E?(v&&v.abort(),v=new AbortController):(v&&v.abort(),v=new AbortController,u=0,D++,w.innerHTML='
Loading...
');const s=D,p=O(),l=await fetch("/api/v1/error-logs?"+p.toString(),{signal:v.signal});if(!l.ok){w.innerHTML=`
Failed: HTTP ${l.status}
`,M.style.display="none",L.textContent="";return}const g=await l.json();if(!g.success){w.innerHTML=`
Failed: ${escapeHtml(g.error||"unknown")}
`,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=`
\u{1F4CB}${escapeHtml(e)}
`,M.style.display="none",L.textContent=g.total?`${g.total} total`:"";return}let t="";E||(t='',t+='',t+='',t+='',t+='',t+='',t+='',t+="");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+='',t+=``,t+=``,t+=``,t+=``,t+=``,t+="",e.detail&&(t+=``)}if(!E)t+="
WhenLevelContextMessageIP
${escapeHtml(i)}${escapeHtml(a)}${escapeHtml(n)}${escapeHtml(r)}${escapeHtml(d)}
",w.innerHTML=t;else{const e=w.querySelector("table");e&&e.insertAdjacentHTML("beforeend",t)}u+=c.length,M.style.display=g.hasMore?"":"none",L.textContent=`${g.total} total${g.hasMore?" (showing "+u+")":""}`,w.querySelectorAll(".error-log-row").forEach(e=>{e.dataset.wired||(e.dataset.wired="true",e.addEventListener("click",()=>{const a=e.nextElementSibling;a&&a.classList.contains("error-log-detail")&&(a.style.display=a.style.display==="none"?"":"none")}))})}catch(s){if(s&&s.name==="AbortError")return;w.innerHTML=`
Failed: ${escapeHtml(s.message)}
`,L.textContent=""}}async function y(){if(confirm("Clear the entire error log? This cannot be undone."))try{const s=await(await secureFetch("/api/v1/error-logs",{method:"DELETE",headers:{"content-type":"application/json"},body:JSON.stringify({confirm:"CLEAR"})})).json();s.success?(await N(),m(!1),showNotification("\u2705 Error logs cleared","success",3e3)):showNotification("\u274C "+(s.error||"Clear failed"),"error",4e3)}catch(E){showNotification("\u274C "+E.message,"error",4e3)}}let S;function f(){P?.addEventListener("change",()=>m(!1)),H?.addEventListener("change",()=>m(!1)),A?.addEventListener("input",()=>{clearTimeout(S),S=setTimeout(()=>m(!1),250)});let E;[x,B].forEach(s=>{s?.addEventListener("change",()=>{clearTimeout(E),E=setTimeout(()=>m(!1),250)})}),j?.addEventListener("click",()=>m(!1)),M?.addEventListener("click",()=>m(!0)),k?.addEventListener("click",y),wireModal(b,z)}C?.addEventListener("click",async()=>{b?.classList.add("show"),await N(),m(!1)}),f()})(),(function(){injectModal("container-logs-modal",`
@@ -648,14 +648,14 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};
-
`);const k=document.getElementById("container-logs-modal"),B=document.getElementById("cl-container-select"),A=document.getElementById("cl-log-content"),I=document.getElementById("cl-log-search"),z=document.getElementById("cl-log-tail"),P=document.getElementById("cl-refresh"),M=document.getElementById("cl-stream"),D=document.getElementById("cl-download"),b=document.getElementById("cl-clear-search"),T=document.getElementById("cl-close"),h=document.getElementById("cl-close-btn"),H=document.getElementById("cl-stream-status"),S=document.getElementById("cl-stream-indicator"),$=document.getElementById("cl-stream-text"),f=document.getElementById("cl-line-count"),E=document.getElementById("cl-filter-count"),O=document.getElementById("cl-image"),L=document.getElementById("cl-status"),j=document.getElementById("cl-created");let N=null,R=[],u=[],v=null,w=!1,g=null;function x(r){if(!r)return"-";const d=new Date(r);return isNaN(d.getTime())?r:d.toLocaleString()}function s(r){if(!r)return"";const d=document.createElement("div");return d.textContent=r,d.innerHTML}function p(r,d){const y=r.stream==="stderr"?"log-stderr":"log-stdout",C=r.stream==="stderr"?"\u26A0\uFE0F":"\u{1F4E4}";return` -
+
`);const b=document.getElementById("container-logs-modal"),C=document.getElementById("cl-container-select"),j=document.getElementById("cl-log-content"),k=document.getElementById("cl-log-search"),z=document.getElementById("cl-log-tail"),P=document.getElementById("cl-refresh"),H=document.getElementById("cl-stream"),A=document.getElementById("cl-download"),x=document.getElementById("cl-clear-search"),B=document.getElementById("cl-close"),w=document.getElementById("cl-close-btn"),M=document.getElementById("cl-stream-status"),L=document.getElementById("cl-stream-indicator"),$=document.getElementById("cl-stream-text"),u=document.getElementById("cl-line-count"),v=document.getElementById("cl-filter-count"),D=document.getElementById("cl-image"),I=document.getElementById("cl-status"),R=document.getElementById("cl-created");let N=null,O=[],m=[],y=null,S=!1,f=null;function E(r){if(!r)return"-";const d=new Date(r);return isNaN(d.getTime())?r:d.toLocaleString()}function s(r){if(!r)return"";const d=document.createElement("div");return d.textContent=r,d.innerHTML}function p(r,d){const h=r.stream==="stderr"?"log-stderr":"log-stdout",T=r.stream==="stderr"?"\u26A0\uFE0F":"\u{1F4E4}";return` +
${d+1} - ${C} + ${T} ${s(r.text)}
- `}function l(r,d=""){if(!r||r.length===0){A.innerHTML='
No logs available
',f.textContent="0 lines",E.textContent="0 filtered";return}if(R=r,u=d?r.filter(y=>y.text&&y.text.toLowerCase().includes(d.toLowerCase())):r,f.textContent=`${r.length} lines`,E.textContent=d?`${u.length} of ${r.length} shown`:`${r.length} shown`,u.length===0){A.innerHTML=`
No logs match "${s(d)}"
`;return}A.innerHTML=u.map((y,C)=>p(y,C)).join(""),A.scrollTop=A.scrollHeight}async function m(){try{const d=(await getJSON("/api/v1/logs/containers")).containers||[],y=B.value;B.innerHTML='',d.forEach(C=>{const U=document.createElement("option");U.value=C.id,U.textContent=`${C.name} (${C.image.split(":")[0]}) - ${C.status}`,U.dataset.name=C.name,U.dataset.image=C.image,U.dataset.status=C.status,U.dataset.created=C.created,B.appendChild(U)}),y&&B.querySelector(`option[value="${y}"]`)&&(B.value=y,c(y))}catch(r){console.error("Failed to load containers:",r)}}function c(r){const d=B.querySelector(`option[value="${r}"]`);d&&(O.textContent=d.dataset.image||"-",L.textContent=d.dataset.status||"-",L.style.color=d.dataset.status==="running"?"var(--ok-fg, #4ade80)":"var(--bad-fg, #ef4444)",j.textContent=x(d.dataset.created))}async function t(){const r=B.value;if(!r){A.innerHTML='
Select a container to view logs
';return}a(),N=r,c(r);const d=z.value,y=I.value.trim();A.innerHTML='
Loading logs...
';try{const C=`/api/v1/logs/container/${r}${d!=="all"?`?tail=${d}`:""}`,U=await getJSON(C);U.logs&&U.logs.length>0?l(U.logs,y):(A.innerHTML='
No logs found for this container
',f.textContent="0 lines",E.textContent="0 filtered")}catch(C){A.innerHTML=`
Error loading logs: ${s(C.message)}
`}}function e(){const r=B.value;if(!r)return;a(),w=!0,M.textContent="\u23F9 Stop",H.style.display="flex",S.textContent="\u{1F7E2}",$.textContent="Connecting...";const d=`/api/v1/logs/stream/${r}`;v=new EventSource(d),v.onopen=()=>{S.textContent="\u{1F7E2}",$.textContent="Connected - streaming logs"},v.onmessage=y=>{try{const C=JSON.parse(y.data);if(C.error){S.textContent="\u{1F534}",$.textContent=`Error: ${C.error}`;return}R.push(C),u.push(C),f.textContent=`${R.length} lines`,E.textContent=`${u.length} shown`;const U=I.value.trim();if(!U||C.text&&C.text.toLowerCase().includes(U.toLowerCase())){const q=document.createElement("div");q.innerHTML=p(C,u.length-1);const F=q.firstElementChild;F.style.background="#1a3a1a",A.appendChild(F),A.scrollTop=A.scrollHeight}}catch(C){console.error("Error parsing log:",C)}},v.onerror=()=>{S.textContent="\u{1F534}",$.textContent="Disconnected",w=!1,M.textContent="\u25B6 Stream"},k._eventSource=v}function a(){v&&(v.close(),v=null),k._eventSource&&(k._eventSource.close(),k._eventSource=null),w=!1,M.textContent="\u25B6 Stream",H.style.display="none"}function o(){if(!R||R.length===0){showNotification("No logs to download","error");return}const r=B.querySelector(`option[value="${N}"]`)?.dataset.name||N,d=new Date().toISOString().replace(/[:.]/g,"-"),y=`${r}-logs-${d}.txt`,C=R.map(_=>{const J=_.timestamp||"",X=_.stream==="stderr"?"[ERR]":"[OUT]";return`${J?J+" ":""}${X} ${_.text}`}).join(` -`),U=new Blob([C],{type:"text/plain"}),q=URL.createObjectURL(U),F=document.createElement("a");F.href=q,F.download=y,document.body.appendChild(F),F.click(),document.body.removeChild(F),URL.revokeObjectURL(q),showNotification(`Downloaded ${R.length} log lines`,"success")}B?.addEventListener("change",()=>{t()}),z?.addEventListener("change",()=>{t()}),P?.addEventListener("click",()=>{t()}),M?.addEventListener("click",()=>{w?a():e()}),D?.addEventListener("click",()=>{o()}),b?.addEventListener("click",()=>{I.value="",l(R,"")}),I?.addEventListener("input",()=>{clearTimeout(g),g=setTimeout(()=>{l(R,I.value.trim())},300)}),I?.addEventListener("keydown",r=>{r.key==="Escape"&&(I.value="",l(R,""))}),document.getElementById("view-container-logs")?.addEventListener("click",()=>{k.classList.add("show"),m()});function n(){a(),k.classList.remove("show")}T?.addEventListener("click",n),h?.addEventListener("click",n),document.addEventListener("keydown",r=>{r.key==="Escape"&&k.classList.contains("show")&&n()}),k.addEventListener("click",r=>{r.target===k&&n()}),window.openContainerLogsModal=function(r,d){k.classList.add("show"),m().then(()=>{const y=Array.from(B.options).find(C=>C.value===r||C.dataset.name===d);y?(B.value=y.value,c(y.value),t()):r?(N=r,O.textContent=d||r,L.textContent="-",j.textContent="-",t()):A.innerHTML='
Select a container to view logs
'})}})(),(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='
Select a container to view logs
';return}a(),N=r,c(r);const d=z.value,h=k.value.trim();j.innerHTML='
Loading logs...
';try{const T=`/api/v1/logs/container/${r}${d!=="all"?`?tail=${d}`:""}`,U=await getJSON(T);U.logs&&U.logs.length>0?l(U.logs,h):(j.innerHTML='
No logs found for this container
',u.textContent="0 lines",v.textContent="0 filtered")}catch(T){j.innerHTML=`
Error loading logs: ${s(T.message)}
`}}function e(){const r=C.value;if(!r)return;a(),S=!0,H.textContent="\u23F9 Stop",M.style.display="flex",L.textContent="\u{1F7E2}",$.textContent="Connecting...";const d=`/api/v1/logs/stream/${r}`;y=new EventSource(d),y.onopen=()=>{L.textContent="\u{1F7E2}",$.textContent="Connected - streaming logs"},y.onmessage=h=>{try{const T=JSON.parse(h.data);if(T.error){L.textContent="\u{1F534}",$.textContent=`Error: ${T.error}`;return}O.push(T),m.push(T),u.textContent=`${O.length} lines`,v.textContent=`${m.length} shown`;const U=k.value.trim();if(!U||T.text&&T.text.toLowerCase().includes(U.toLowerCase())){const q=document.createElement("div");q.innerHTML=p(T,m.length-1);const F=q.firstElementChild;F.style.background="#1a3a1a",j.appendChild(F),j.scrollTop=j.scrollHeight}}catch(T){console.error("Error parsing log:",T)}},y.onerror=()=>{L.textContent="\u{1F534}",$.textContent="Disconnected",S=!1,H.textContent="\u25B6 Stream"},b._eventSource=y}function a(){y&&(y.close(),y=null),b._eventSource&&(b._eventSource.close(),b._eventSource=null),S=!1,H.textContent="\u25B6 Stream",M.style.display="none"}function o(){if(!O||O.length===0){showNotification("No logs to download","error");return}const r=C.querySelector(`option[value="${N}"]`)?.dataset.name||N,d=new Date().toISOString().replace(/[:.]/g,"-"),h=`${r}-logs-${d}.txt`,T=O.map(_=>{const J=_.timestamp||"",X=_.stream==="stderr"?"[ERR]":"[OUT]";return`${J?J+" ":""}${X} ${_.text}`}).join(` +`),U=new Blob([T],{type:"text/plain"}),q=URL.createObjectURL(U),F=document.createElement("a");F.href=q,F.download=h,document.body.appendChild(F),F.click(),document.body.removeChild(F),URL.revokeObjectURL(q),showNotification(`Downloaded ${O.length} log lines`,"success")}C?.addEventListener("change",()=>{t()}),z?.addEventListener("change",()=>{t()}),P?.addEventListener("click",()=>{t()}),H?.addEventListener("click",()=>{S?a():e()}),A?.addEventListener("click",()=>{o()}),x?.addEventListener("click",()=>{k.value="",l(O,"")}),k?.addEventListener("input",()=>{clearTimeout(f),f=setTimeout(()=>{l(O,k.value.trim())},300)}),k?.addEventListener("keydown",r=>{r.key==="Escape"&&(k.value="",l(O,""))}),document.getElementById("view-container-logs")?.addEventListener("click",()=>{b.classList.add("show"),g()});function n(){a(),b.classList.remove("show")}B?.addEventListener("click",n),w?.addEventListener("click",n),document.addEventListener("keydown",r=>{r.key==="Escape"&&b.classList.contains("show")&&n()}),b.addEventListener("click",r=>{r.target===b&&n()}),window.openContainerLogsModal=function(r,d){b.classList.add("show"),g().then(()=>{const h=Array.from(C.options).find(T=>T.value===r||T.dataset.name===d);h?(C.value=h.value,c(h.value),t()):r?(N=r,D.textContent=d||r,I.textContent="-",R.textContent="-",t()):j.innerHTML='
Select a container to view logs
'})}})(),(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}};
- `);const B=document.getElementById("journald-modal"),A=document.getElementById("jd-unit-select"),I=document.getElementById("jd-search"),z=document.getElementById("jd-tail"),P=document.getElementById("jd-refresh"),M=document.getElementById("jd-stream"),D=document.getElementById("jd-clear-search"),b=document.getElementById("jd-close"),T=document.getElementById("jd-close-btn"),h=document.getElementById("jd-content"),H=document.getElementById("jd-line-count"),S=document.getElementById("jd-filter-count"),$=document.getElementById("jd-overflow"),f=document.getElementById("jd-unit-display"),E=document.getElementById("jd-stream-state");let O=!1,L=[],j=!1,N=null,R=null;function u(c){const t=document.createElement("div");return t.textContent=String(c),t.innerHTML}function v(c){O=c,A.innerHTML="",k.forEach(t=>{const e=document.createElement("option");e.value=t.unit,e.textContent=t.label+" ("+t.unit+")",A.appendChild(e)}),A.disabled=!c,c?(P.disabled=!1,M.disabled=!1):(h.innerHTML='
journald bind-mount not available in this container.
Requires /var/log/journal + /usr/bin/journalctl mounted (start.sh).
',P.disabled=!0,M.disabled=!0)}async function w(){try{const c=await fetch("/api/v1/logs/journal/units");if(!c.ok){v(!1);return}const t=await c.json();v(!!t.available)}catch{v(!1)}}function g(){const c=(I.value||"").trim().toLowerCase(),t=c?L.filter(e=>(e.textContent||"").toLowerCase().includes(c)):L;if(t.length===0)h.innerHTML='
No entries'+(c?` matching "${u(c)}"`:"")+"
";else{const e=t.map(o=>{const i=o.timestamp?u(o.timestamp):"\u2014",n=u(o.textContent);return`
${i}${n}
`}).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='
Loading\u2026
';try{const a=new URL("/api/v1/logs/journal",window.location.origin);a.searchParams.set("unit",c),a.searchParams.set("tail",String(t)),e&&a.searchParams.set("search",e);const o=await fetch(a.toString()),i=await o.json();if(!o.ok||!i.success){h.innerHTML='
Failed: '+u(i&&i.error||"HTTP "+o.status)+"
";return}f.textContent=c,L=(i.entries||[]).map(n=>({timestamp:n.timestamp,unit:n.unit,textContent:n.text||""})),$.style.display="none",g()}catch(a){h.innerHTML='
Error: '+u(a.message)+"
"}}function s(){if(!O)return;p();const c=A.value;if(!c)return;const t=(I.value||"").trim();f.textContent=c,M.textContent="\u23F8 Stop",M.classList.add("streaming"),E.textContent="streaming",E.style.color="var(--ok-fg, #4ade80)",L=[],g(),$.style.display="none";const e=new URL("/api/v1/logs/journal/stream",window.location.origin);e.searchParams.set("unit",c),t&&e.searchParams.set("search",t),N=new EventSource(e.toString()),N.onmessage=a=>{try{const o=JSON.parse(a.data);if(o.error){/stream (exceeded|line cap)/.test(o.error)&&($.style.display="",p()),h.innerHTML+='
\u26A0 '+u(o.error)+"
",h.scrollTop=h.scrollHeight;return}L.push({timestamp:o.timestamp,unit:o.unit||c,textContent:o.text||""}),L.length>5e3&&(L=L.slice(L.length-5e3),$.style.display=""),g()}catch{}},N.onerror=()=>{},j=!0}function p(){if(j=!1,N){try{N.close()}catch{}N=null}M.textContent="\u25B6 Stream",M.classList.remove("streaming"),E.textContent="disconnected",E.style.color="var(--muted)"}function l(){p(),B.classList.remove("show")}P.addEventListener("click",x),M.addEventListener("click",()=>j?p():s()),D.addEventListener("click",()=>{I.value="",g()}),I.addEventListener("input",()=>{clearTimeout(R),R=setTimeout(g,200)}),I.addEventListener("keydown",c=>{c.key==="Escape"&&(I.value="",g())}),b.addEventListener("click",l),T.addEventListener("click",l),B.addEventListener("click",c=>{c.target===B&&l()}),document.addEventListener("keydown",c=>{c.key==="Escape"&&B.classList.contains("show")&&l()}),A.addEventListener("change",()=>{L.length>0&&x()});function m(){B.classList.add("show"),w()}window.openJournaldModal=m,document.getElementById("view-journald-logs")?.addEventListener("click",m)})(),(function(){injectModal("snapshot-modal",`
+ `);const C=document.getElementById("journald-modal"),j=document.getElementById("jd-unit-select"),k=document.getElementById("jd-search"),z=document.getElementById("jd-tail"),P=document.getElementById("jd-refresh"),H=document.getElementById("jd-stream"),A=document.getElementById("jd-clear-search"),x=document.getElementById("jd-close"),B=document.getElementById("jd-close-btn"),w=document.getElementById("jd-content"),M=document.getElementById("jd-line-count"),L=document.getElementById("jd-filter-count"),$=document.getElementById("jd-overflow"),u=document.getElementById("jd-unit-display"),v=document.getElementById("jd-stream-state");let D=!1,I=[],R=!1,N=null,O=null;function m(c){const t=document.createElement("div");return t.textContent=String(c),t.innerHTML}function y(c){D=c,j.innerHTML="",b.forEach(t=>{const e=document.createElement("option");e.value=t.unit,e.textContent=t.label+" ("+t.unit+")",j.appendChild(e)}),j.disabled=!c,c?(P.disabled=!1,H.disabled=!1):(w.innerHTML='
journald bind-mount not available in this container.
Requires /var/log/journal + /usr/bin/journalctl mounted (start.sh).
',P.disabled=!0,H.disabled=!0)}async function S(){try{const c=await fetch("/api/v1/logs/journal/units");if(!c.ok){y(!1);return}const t=await c.json();y(!!t.available)}catch{y(!1)}}function f(){const c=(k.value||"").trim().toLowerCase(),t=c?I.filter(e=>(e.textContent||"").toLowerCase().includes(c)):I;if(t.length===0)w.innerHTML='
No entries'+(c?` matching "${m(c)}"`:"")+"
";else{const e=t.map(o=>{const i=o.timestamp?m(o.timestamp):"\u2014",n=m(o.textContent);return`
${i}${n}
`}).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='
Loading\u2026
';try{const a=new URL("/api/v1/logs/journal",window.location.origin);a.searchParams.set("unit",c),a.searchParams.set("tail",String(t)),e&&a.searchParams.set("search",e);const o=await fetch(a.toString()),i=await o.json();if(!o.ok||!i.success){w.innerHTML='
Failed: '+m(i&&i.error||"HTTP "+o.status)+"
";return}u.textContent=c,I=(i.entries||[]).map(n=>({timestamp:n.timestamp,unit:n.unit,textContent:n.text||""})),$.style.display="none",f()}catch(a){w.innerHTML='
Error: '+m(a.message)+"
"}}function s(){if(!D)return;p();const c=j.value;if(!c)return;const t=(k.value||"").trim();u.textContent=c,H.textContent="\u23F8 Stop",H.classList.add("streaming"),v.textContent="streaming",v.style.color="var(--ok-fg, #4ade80)",I=[],f(),$.style.display="none";const e=new URL("/api/v1/logs/journal/stream",window.location.origin);e.searchParams.set("unit",c),t&&e.searchParams.set("search",t),N=new EventSource(e.toString()),N.onmessage=a=>{try{const o=JSON.parse(a.data);if(o.error){/stream (exceeded|line cap)/.test(o.error)&&($.style.display="",p()),w.innerHTML+='
\u26A0 '+m(o.error)+"
",w.scrollTop=w.scrollHeight;return}I.push({timestamp:o.timestamp,unit:o.unit||c,textContent:o.text||""}),I.length>5e3&&(I=I.slice(I.length-5e3),$.style.display=""),f()}catch{}},N.onerror=()=>{},R=!0}function p(){if(R=!1,N){try{N.close()}catch{}N=null}H.textContent="\u25B6 Stream",H.classList.remove("streaming"),v.textContent="disconnected",v.style.color="var(--muted)"}function l(){p(),C.classList.remove("show")}P.addEventListener("click",E),H.addEventListener("click",()=>R?p():s()),A.addEventListener("click",()=>{k.value="",f()}),k.addEventListener("input",()=>{clearTimeout(O),O=setTimeout(f,200)}),k.addEventListener("keydown",c=>{c.key==="Escape"&&(k.value="",f())}),x.addEventListener("click",l),B.addEventListener("click",l),C.addEventListener("click",c=>{c.target===C&&l()}),document.addEventListener("keydown",c=>{c.key==="Escape"&&C.classList.contains("show")&&l()}),j.addEventListener("change",()=>{I.length>0&&E()});function g(){C.classList.add("show"),S()}window.openJournaldModal=g,document.getElementById("view-journald-logs")?.addEventListener("click",g)})(),(function(){injectModal("snapshot-modal",`

\u{1F4BE} Container Snapshots

-
`);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=`
Detection failed: ${escapeHtml(f.error)}
`,b.style.display="block";return}let x='
';for(const[p,l]of Object.entries(f.services)){const m=E[p]||"\u{1F4E6}",c=O[p]||p,t=l.source?`${escapeHtml(l.source)}`:"",e=l.version?`v${escapeHtml(l.version)}`:"",a=(l.hasApiKey||l.hasToken)&&l.status==="connected"?'Key saved':"";x+=`
- ${m} +
`);const b=document.getElementById("arr-setup-modal"),C=document.getElementById("arr-setup-btn"),j=document.getElementById("arr-setup-cancel"),k=document.getElementById("smart-connect-btn"),z=document.getElementById("smart-phase-detect"),P=document.getElementById("smart-phase-credentials"),H=document.getElementById("smart-phase-progress"),A=document.getElementById("smart-phase-results"),x=document.getElementById("smart-detect-results"),B=document.getElementById("smart-credential-inputs"),w=document.getElementById("smart-progress-steps"),M=document.getElementById("smart-results-content"),L=document.getElementById("smart-plex-libraries"),$=document.getElementById("smart-retry-btn");let u=null;const v={plex:"\u{1F3AC}",radarr:"\u{1F3AC}",sonarr:"\u{1F4FA}",prowlarr:"\u{1F50D}",seerr:"\u{1F4CB}"},D={plex:"Plex",radarr:"Radarr (Movies)",sonarr:"Sonarr (TV)",prowlarr:"Prowlarr (Indexers)",seerr:"Seerr"};function I(f){z.style.display=f==="detect"?"block":"none",P.style.display=f==="credentials"?"block":"none",H.style.display=f==="progress"?"block":"none",A.style.display=f==="results"?"block":"none"}function R(f){const E={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=E[f]||E.not_found;return`${s.icon} ${s.text}`}async function N(){I("detect"),x.style.display="none";try{if(u=await(await fetch("/api/v1/arr/smart-detect")).json(),!u.success){x.innerHTML=`
Detection failed: ${escapeHtml(u.error)}
`,x.style.display="block";return}let E='
';for(const[p,l]of Object.entries(u.services)){const g=v[p]||"\u{1F4E6}",c=D[p]||p,t=l.source?`${escapeHtml(l.source)}`:"",e=l.version?`v${escapeHtml(l.version)}`:"",a=(l.hasApiKey||l.hasToken)&&l.status==="connected"?'Key saved':"";E+=`
+ ${g}
${c}
${t} ${e} ${a}
- ${j(l.status)} -
`}x+="
";const s=f.summary;x+=`
+ ${R(l.status)} +
`}E+="
";const s=u.summary;E+=`
${escapeHtml(String(s.fullyConnected))}/${escapeHtml(String(s.totalDetected+(5-s.totalDetected)))} services detected · ${escapeHtml(String(s.fullyConnected))} connected${s.needsApiKey>0?` · ${escapeHtml(String(s.needsApiKey))} needs API key`:""} -
`,b.innerHTML=x,b.style.display="block",R(f),setTimeout(()=>{L("credentials")},800)}catch(g){b.innerHTML=`
Error: ${escapeHtml(g.message)}
`,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.innerHTML=E,x.style.display="block",O(u),setTimeout(()=>{I("credentials")},800)}catch(f){x.innerHTML=`
Error: ${escapeHtml(f.message)}
`,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}};
-
`}const l=s.plex;if(l){const c=l.status==="connected";x+=`
+
`}const l=s.plex;if(l){const c=l.status==="connected";E+=`
\u{1F3AC} Plex - ${j(l.status)} + ${R(l.status)} ${escapeHtml(l.source||"")}
-
`}const m=s.seerr;if(m){const c=m.status==="connected";let t="";if(m.configuredServices){const e=m.configuredServices;t=`
+
`}const g=s.seerr;if(g){const c=g.status==="connected";let t="";if(g.configuredServices){const e=g.configuredServices;t=`
Configured: ${e.radarr?"✓ Radarr":"✗ Radarr"} · ${e.sonarr?"✓ Sonarr":"✗ Sonarr"} · ${e.plex?"✓ Plex":"✗ Plex"} -
`}x+=`
+
`}E+=`
\u{1F4CB} Seerr - ${j(m.status)} + ${R(g.status)}
${t} -
`}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+=`
${c} - ${escapeHtml(m.step)} - ${escapeHtml(m.details||"")} -
`}h.innerHTML=l,setTimeout(()=>v(p),500)}catch(s){h.innerHTML=`
Connection error: ${escapeHtml(s.message)}
`}}function v(g){L("results");const x=g.summary||{},s=x.failed===0&&x.succeeded>0,p=s?"var(--ok-fg)":"#f39c12",l=s?"✓":"⚠",m=s?"All Connected!":`${escapeHtml(String(x.succeeded))}/${escapeHtml(String(x.totalSteps))} Steps Succeeded`;let c=`
+ ${escapeHtml(g.step)} + ${escapeHtml(g.details||"")} +
`}w.innerHTML=l,setTimeout(()=>y(p),500)}catch(s){w.innerHTML=`
Connection error: ${escapeHtml(s.message)}
`}}function y(f){I("results");const E=f.summary||{},s=E.failed===0&&E.succeeded>0,p=s?"var(--ok-fg)":"#f39c12",l=s?"✓":"⚠",g=s?"All Connected!":`${escapeHtml(String(E.succeeded))}/${escapeHtml(String(E.totalSteps))} Steps Succeeded`;let c=`
${l}
-
${m}
-
${escapeHtml(String(x.succeeded))} succeeded, ${escapeHtml(String(x.failed))} failed
-
`;c+='
';for(const t of g.steps||[]){const e=t.status==="success"?'':'';c+=`
+
${g}
+
${escapeHtml(String(E.succeeded))} succeeded, ${escapeHtml(String(E.failed))} failed
+
`;c+='
';for(const t of f.steps||[]){const e=t.status==="success"?'':'';c+=`
${e} ${escapeHtml(t.step)} ${escapeHtml(t.details||"")} -
`}c+="
",H.innerHTML=c,$.style.display=x.failed>0?"block":"none",g.steps?.some(t=>t.step.includes("Plex")&&t.status==="success")&&w()}async function w(){try{const x=await(await fetch("/api/v1/plex/libraries")).json();if(x.success&&x.libraries?.length>0){let s=`
-

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

-
`;for(const p of x.libraries){const l=p.type==="movie"?"\u{1F3AC}":p.type==="show"?"\u{1F4FA}":"\u{1F3B5}";s+=`
+
`}c+="
",M.innerHTML=c,$.style.display=E.failed>0?"block":"none",f.steps?.some(t=>t.step.includes("Plex")&&t.status==="success")&&S()}async function S(){try{const E=await(await fetch("/api/v1/plex/libraries")).json();if(E.success&&E.libraries?.length>0){let s=`
+

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

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

\u{1F514} Notification Settings

@@ -1109,15 +1109,15 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};
-
`);const B=document.getElementById("notifications-modal"),A=document.getElementById("manage-notifications"),I=document.getElementById("notifications-save"),z=document.getElementById("notifications-cancel");["discord","telegram","ntfy","email"].forEach(S=>{const $=document.getElementById(`${S}-enabled`),f=document.getElementById(`${S}-config`);$?.addEventListener("change",()=>{f.style.display=$.checked?"block":"none"})});const P=document.getElementById("health-check-enabled"),M=document.getElementById("health-check-config");P?.addEventListener("change",()=>{M.style.opacity=P.checked?"1":"0.5"});async function D(){try{const $=await(await fetch("/api/v1/notifications/config")).json();if($.success){const f=$.config;document.getElementById("notifications-enabled").checked=f.enabled,document.getElementById("discord-enabled").checked=f.providers?.discord?.enabled||!1,document.getElementById("telegram-enabled").checked=f.providers?.telegram?.enabled||!1,document.getElementById("ntfy-enabled").checked=f.providers?.ntfy?.enabled||!1,document.getElementById("email-enabled").checked=f.providers?.email?.enabled||!1,document.getElementById("discord-config").style.display=f.providers?.discord?.enabled?"block":"none",document.getElementById("telegram-config").style.display=f.providers?.telegram?.enabled?"block":"none",document.getElementById("ntfy-config").style.display=f.providers?.ntfy?.enabled?"block":"none",document.getElementById("email-config").style.display=f.providers?.email?.enabled?"block":"none",f.providers?.ntfy?.serverUrl&&(document.getElementById("ntfy-server").value=f.providers.ntfy.serverUrl),f.providers?.email?.host&&(document.getElementById("email-host").value=f.providers.email.host),f.providers?.email?.from&&(document.getElementById("email-from").value=f.providers.email.from),f.providers?.email?.to&&(document.getElementById("email-to").value=f.providers.email.to),f.providers?.email?.port&&(document.getElementById("email-port").value=f.providers.email.port),f.providers?.email?.secure!==void 0&&(document.getElementById("email-secure").checked=f.providers.email.secure===!0),f.providers?.email?.username&&(document.getElementById("email-user").value=f.providers.email.username);const E=document.getElementById("email-pass");f.providers?.email?.hasPassword?(E.value="",E.placeholder="saved \u2014 leave blank to keep"):E.placeholder="app password",document.getElementById("health-check-enabled").checked=f.healthCheck?.enabled||!1,f.healthCheck?.intervalMinutes&&(document.getElementById("health-check-interval").value=f.healthCheck.intervalMinutes),f.healthCheck?.lastCheck&&(document.getElementById("health-check-status").textContent=`Last check: ${new Date(f.healthCheck.lastCheck).toLocaleString()}`),document.getElementById("event-container-down").checked=f.events?.["container-down"]!==!1,document.getElementById("event-container-up").checked=f.events?.["container-up"]===!0,document.getElementById("event-deploy-success").checked=f.events?.["deploy-success"]!==!1,document.getElementById("event-deploy-failed").checked=f.events?.["deploy-failed"]!==!1,document.getElementById("event-ssl-cert-expiry").checked=f.events?.["ssl-cert-expiry"]!==!1,document.getElementById("event-dns-propagation").checked=f.events?.["dns-propagation"]!==!1,document.getElementById("event-drift-detected").checked=f.events?.["drift-detected"]!==!1,document.getElementById("event-dependency-restart").checked=f.events?.["dependency-restart"]!==!1,document.getElementById("event-recipe-removed").checked=f.events?.["recipe-removed"]!==!1,document.getElementById("event-workflow").checked=f.events?.workflow!==!1,document.getElementById("event-backup-complete").checked=f.events?.["backup-complete"]!==!1,document.getElementById("event-backup-failed").checked=f.events?.["backup-failed"]!==!1,document.getElementById("event-update-available").checked=f.events?.["update-available"]!==!1,document.getElementById("event-resource-alert").checked=f.events?.alert!==!1}}catch(S){k.logError("[Notifications] Load Config",S,{function:"loadConfig"})}}async function b(){try{const $=await(await fetch("/api/v1/notifications/history?limit=10")).json(),f=document.getElementById("notification-history");$.success&&$.history?.length>0?f.innerHTML=$.history.map(E=>{const O=new Date(E.timestamp).toLocaleString();return` + `);const C=document.getElementById("notifications-modal"),j=document.getElementById("manage-notifications"),k=document.getElementById("notifications-save"),z=document.getElementById("notifications-cancel");["discord","telegram","ntfy","email"].forEach(L=>{const $=document.getElementById(`${L}-enabled`),u=document.getElementById(`${L}-config`);$?.addEventListener("change",()=>{u.style.display=$.checked?"block":"none"})});const P=document.getElementById("health-check-enabled"),H=document.getElementById("health-check-config");P?.addEventListener("change",()=>{H.style.opacity=P.checked?"1":"0.5"});async function A(){try{const $=await(await fetch("/api/v1/notifications/config")).json();if($.success){const u=$.config;document.getElementById("notifications-enabled").checked=u.enabled,document.getElementById("discord-enabled").checked=u.providers?.discord?.enabled||!1,document.getElementById("telegram-enabled").checked=u.providers?.telegram?.enabled||!1,document.getElementById("ntfy-enabled").checked=u.providers?.ntfy?.enabled||!1,document.getElementById("email-enabled").checked=u.providers?.email?.enabled||!1,document.getElementById("discord-config").style.display=u.providers?.discord?.enabled?"block":"none",document.getElementById("telegram-config").style.display=u.providers?.telegram?.enabled?"block":"none",document.getElementById("ntfy-config").style.display=u.providers?.ntfy?.enabled?"block":"none",document.getElementById("email-config").style.display=u.providers?.email?.enabled?"block":"none",u.providers?.ntfy?.serverUrl&&(document.getElementById("ntfy-server").value=u.providers.ntfy.serverUrl),u.providers?.email?.host&&(document.getElementById("email-host").value=u.providers.email.host),u.providers?.email?.from&&(document.getElementById("email-from").value=u.providers.email.from),u.providers?.email?.to&&(document.getElementById("email-to").value=u.providers.email.to),u.providers?.email?.port&&(document.getElementById("email-port").value=u.providers.email.port),u.providers?.email?.secure!==void 0&&(document.getElementById("email-secure").checked=u.providers.email.secure===!0),u.providers?.email?.username&&(document.getElementById("email-user").value=u.providers.email.username);const v=document.getElementById("email-pass");u.providers?.email?.hasPassword?(v.value="",v.placeholder="saved \u2014 leave blank to keep"):v.placeholder="app password",document.getElementById("health-check-enabled").checked=u.healthCheck?.enabled||!1,u.healthCheck?.intervalMinutes&&(document.getElementById("health-check-interval").value=u.healthCheck.intervalMinutes),u.healthCheck?.lastCheck&&(document.getElementById("health-check-status").textContent=`Last check: ${new Date(u.healthCheck.lastCheck).toLocaleString()}`),document.getElementById("event-container-down").checked=u.events?.["container-down"]!==!1,document.getElementById("event-container-up").checked=u.events?.["container-up"]===!0,document.getElementById("event-deploy-success").checked=u.events?.["deploy-success"]!==!1,document.getElementById("event-deploy-failed").checked=u.events?.["deploy-failed"]!==!1,document.getElementById("event-ssl-cert-expiry").checked=u.events?.["ssl-cert-expiry"]!==!1,document.getElementById("event-dns-propagation").checked=u.events?.["dns-propagation"]!==!1,document.getElementById("event-drift-detected").checked=u.events?.["drift-detected"]!==!1,document.getElementById("event-dependency-restart").checked=u.events?.["dependency-restart"]!==!1,document.getElementById("event-recipe-removed").checked=u.events?.["recipe-removed"]!==!1,document.getElementById("event-workflow").checked=u.events?.workflow!==!1,document.getElementById("event-backup-complete").checked=u.events?.["backup-complete"]!==!1,document.getElementById("event-backup-failed").checked=u.events?.["backup-failed"]!==!1,document.getElementById("event-update-available").checked=u.events?.["update-available"]!==!1,document.getElementById("event-resource-alert").checked=u.events?.alert!==!1}}catch(L){b.logError("[Notifications] Load Config",L,{function:"loadConfig"})}}async function x(){try{const $=await(await fetch("/api/v1/notifications/history?limit=10")).json(),u=document.getElementById("notification-history");$.success&&$.history?.length>0?u.innerHTML=$.history.map(v=>{const D=new Date(v.timestamp).toLocaleString();return`
- ${E.type==="success"?"\u2713":E.type==="error"?"\u2717":"\u2139"} + ${v.type==="success"?"\u2713":v.type==="error"?"\u2717":"\u2139"}
-
${escapeHtml(E.title)}
-
${O}
+
${escapeHtml(v.title)}
+
${D}
- `}).join(""):f.innerHTML='
No notifications yet
'}catch(S){k.logError("[Notifications] Load History",S,{function:"loadHistory"})}}async function T(){try{const S={enabled:document.getElementById("notifications-enabled").checked,providers:{discord:{enabled:document.getElementById("discord-enabled").checked,webhookUrl:document.getElementById("discord-webhook").value.trim()},telegram:{enabled:document.getElementById("telegram-enabled").checked,botToken:document.getElementById("telegram-bot-token").value.trim(),chatId:document.getElementById("telegram-chat-id").value.trim()},ntfy:{enabled:document.getElementById("ntfy-enabled").checked,serverUrl:document.getElementById("ntfy-server").value.trim()||"https://ntfy.sh",topic:document.getElementById("ntfy-topic").value.trim()},email:{enabled:document.getElementById("email-enabled").checked,host:document.getElementById("email-host").value.trim(),port:parseInt(document.getElementById("email-port").value)||587,secure:document.getElementById("email-secure").checked,username:document.getElementById("email-user").value.trim(),password:document.getElementById("email-pass").value.trim(),from:document.getElementById("email-from").value.trim(),to:document.getElementById("email-to").value.trim()}},events:{"container-down":document.getElementById("event-container-down").checked,"container-up":document.getElementById("event-container-up").checked,"deploy-success":document.getElementById("event-deploy-success").checked,"deploy-failed":document.getElementById("event-deploy-failed").checked,"ssl-cert-expiry":document.getElementById("event-ssl-cert-expiry").checked,"dns-propagation":document.getElementById("event-dns-propagation").checked,"drift-detected":document.getElementById("event-drift-detected").checked,"dependency-restart":document.getElementById("event-dependency-restart").checked,"recipe-removed":document.getElementById("event-recipe-removed").checked,workflow:document.getElementById("event-workflow").checked,"backup-complete":document.getElementById("event-backup-complete").checked,"backup-failed":document.getElementById("event-backup-failed").checked,"update-available":document.getElementById("event-update-available").checked,alert:document.getElementById("event-resource-alert").checked},healthCheck:{enabled:document.getElementById("health-check-enabled").checked,intervalMinutes:parseInt(document.getElementById("health-check-interval").value)||5}},f=await(await secureFetch("/api/v1/notifications/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(S)})).json();f.success?(showNotification("Notification settings saved","success",3e3),B.classList.remove("show")):showNotification(`Failed to save: ${f.error}`,"error",3e3)}catch(S){showNotification(`Error: ${S.message}`,"error",3e3)}}async function h(S){try{const f=await(await secureFetch("/api/v1/notifications/test",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({provider:S})})).json();f.success?showNotification(`Test ${S} notification sent!`,"success",3e3):showNotification(`Test failed: ${f.error}`,"error",3e3)}catch($){showNotification(`Error: ${$.message}`,"error",3e3)}}document.getElementById("discord-test")?.addEventListener("click",()=>h("discord")),document.getElementById("telegram-test")?.addEventListener("click",()=>h("telegram")),document.getElementById("ntfy-test")?.addEventListener("click",()=>h("ntfy")),document.getElementById("email-test")?.addEventListener("click",()=>h("email")),document.getElementById("health-check-now")?.addEventListener("click",async()=>{try{const $=await(await secureFetch("/api/v1/notifications/health-check",{method:"POST"})).json();$.success&&(document.getElementById("health-check-status").textContent=`Last check: ${new Date($.lastCheck).toLocaleString()} (${$.containersMonitored} containers)`,showNotification("Health check completed","success",2e3))}catch(S){showNotification(`Error: ${S.message}`,"error",3e3)}}),A?.addEventListener("click",()=>{B.classList.add("show"),D(),b()}),I?.addEventListener("click",T),document.getElementById("notifications-send-test")?.addEventListener("click",async()=>{const S=document.getElementById("notifications-send-test"),$=S.textContent;S.textContent="Sending...",S.disabled=!0;try{const E=await(await secureFetch("/api/v1/notifications/send",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({event:"test",data:{message:"This is a test notification from DashCaddy."},type:"info"})})).json();E.success?(showNotification("Test notification sent!","success",3e3),H()):showNotification(`Test failed: ${E.results?.map(O=>`${O.provider}: ${O.error||"ok"}`).join(", ")}`,"error",5e3)}catch(f){showNotification(`Error: ${f.message}`,"error",3e3)}finally{S.textContent=$,S.disabled=!1}});async function H(){try{const $=await(await fetch("/api/v1/notifications/status")).json();if($.success&&$.lastSent){const f=document.getElementById("last-notification-sent");f&&(f.textContent=`Last sent: ${new Date($.lastSent).toLocaleString()}`)}}catch{}}wireModal(B,z)})(),(function(){document.addEventListener("click",k=>{const B=k.target.closest(".panel-tab");if(!B)return;const A=B.dataset.panel;if(!A)return;const I=B.closest(".panel-tabs"),z=I.closest(".weather-modal-content");I.querySelectorAll(".panel-tab").forEach(M=>M.classList.remove("active")),B.classList.add("active"),z.querySelectorAll(".panel-section").forEach(M=>M.classList.remove("active"));const P=z.querySelector("#"+A);P&&P.classList.add("active")})})(),(function(){var k=["dashcaddy_site_config","dashcaddy_onboarding","dashcaddy-encryption-key","dashcaddy-setup","dashcaddy-config","theme","user-themes","custom-theme","custom-apps","custom-services","toolbar-sections","weather-location","weather-zip","weather-geo","weather-unit","clock-style","clock-chimes","clock-chime-volume"];function B(){for(var e={},a=0;a + `}).join(""):u.innerHTML='
No notifications yet
'}catch(L){b.logError("[Notifications] Load History",L,{function:"loadHistory"})}}async function B(){try{const L={enabled:document.getElementById("notifications-enabled").checked,providers:{discord:{enabled:document.getElementById("discord-enabled").checked,webhookUrl:document.getElementById("discord-webhook").value.trim()},telegram:{enabled:document.getElementById("telegram-enabled").checked,botToken:document.getElementById("telegram-bot-token").value.trim(),chatId:document.getElementById("telegram-chat-id").value.trim()},ntfy:{enabled:document.getElementById("ntfy-enabled").checked,serverUrl:document.getElementById("ntfy-server").value.trim()||"https://ntfy.sh",topic:document.getElementById("ntfy-topic").value.trim()},email:{enabled:document.getElementById("email-enabled").checked,host:document.getElementById("email-host").value.trim(),port:parseInt(document.getElementById("email-port").value)||587,secure:document.getElementById("email-secure").checked,username:document.getElementById("email-user").value.trim(),password:document.getElementById("email-pass").value.trim(),from:document.getElementById("email-from").value.trim(),to:document.getElementById("email-to").value.trim()}},events:{"container-down":document.getElementById("event-container-down").checked,"container-up":document.getElementById("event-container-up").checked,"deploy-success":document.getElementById("event-deploy-success").checked,"deploy-failed":document.getElementById("event-deploy-failed").checked,"ssl-cert-expiry":document.getElementById("event-ssl-cert-expiry").checked,"dns-propagation":document.getElementById("event-dns-propagation").checked,"drift-detected":document.getElementById("event-drift-detected").checked,"dependency-restart":document.getElementById("event-dependency-restart").checked,"recipe-removed":document.getElementById("event-recipe-removed").checked,workflow:document.getElementById("event-workflow").checked,"backup-complete":document.getElementById("event-backup-complete").checked,"backup-failed":document.getElementById("event-backup-failed").checked,"update-available":document.getElementById("event-update-available").checked,alert:document.getElementById("event-resource-alert").checked},healthCheck:{enabled:document.getElementById("health-check-enabled").checked,intervalMinutes:parseInt(document.getElementById("health-check-interval").value)||5}},u=await(await secureFetch("/api/v1/notifications/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(L)})).json();u.success?(showNotification("Notification settings saved","success",3e3),C.classList.remove("show")):showNotification(`Failed to save: ${u.error}`,"error",3e3)}catch(L){showNotification(`Error: ${L.message}`,"error",3e3)}}async function w(L){try{const u=await(await secureFetch("/api/v1/notifications/test",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({provider:L})})).json();u.success?showNotification(`Test ${L} notification sent!`,"success",3e3):showNotification(`Test failed: ${u.error}`,"error",3e3)}catch($){showNotification(`Error: ${$.message}`,"error",3e3)}}document.getElementById("discord-test")?.addEventListener("click",()=>w("discord")),document.getElementById("telegram-test")?.addEventListener("click",()=>w("telegram")),document.getElementById("ntfy-test")?.addEventListener("click",()=>w("ntfy")),document.getElementById("email-test")?.addEventListener("click",()=>w("email")),document.getElementById("health-check-now")?.addEventListener("click",async()=>{try{const $=await(await secureFetch("/api/v1/notifications/health-check",{method:"POST"})).json();$.success&&(document.getElementById("health-check-status").textContent=`Last check: ${new Date($.lastCheck).toLocaleString()} (${$.containersMonitored} containers)`,showNotification("Health check completed","success",2e3))}catch(L){showNotification(`Error: ${L.message}`,"error",3e3)}}),j?.addEventListener("click",()=>{C.classList.add("show"),A(),x()}),k?.addEventListener("click",B),document.getElementById("notifications-send-test")?.addEventListener("click",async()=>{const L=document.getElementById("notifications-send-test"),$=L.textContent;L.textContent="Sending...",L.disabled=!0;try{const v=await(await secureFetch("/api/v1/notifications/send",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({event:"test",data:{message:"This is a test notification from DashCaddy."},type:"info"})})).json();v.success?(showNotification("Test notification sent!","success",3e3),M()):showNotification(`Test failed: ${v.results?.map(D=>`${D.provider}: ${D.error||"ok"}`).join(", ")}`,"error",5e3)}catch(u){showNotification(`Error: ${u.message}`,"error",3e3)}finally{L.textContent=$,L.disabled=!1}});async function M(){try{const $=await(await fetch("/api/v1/notifications/status")).json();if($.success&&$.lastSent){const u=document.getElementById("last-notification-sent");u&&(u.textContent=`Last sent: ${new Date($.lastSent).toLocaleString()}`)}}catch{}}wireModal(C,z)})(),(function(){document.addEventListener("click",b=>{const C=b.target.closest(".panel-tab");if(!C)return;const j=C.dataset.panel;if(!j)return;const k=C.closest(".panel-tabs"),z=k.closest(".weather-modal-content");k.querySelectorAll(".panel-tab").forEach(H=>H.classList.remove("active")),C.classList.add("active"),z.querySelectorAll(".panel-section").forEach(H=>H.classList.remove("active"));const P=z.querySelector("#"+j);P&&P.classList.add("active")})})(),(function(){var b=["dashcaddy_site_config","dashcaddy_onboarding","dashcaddy-encryption-key","dashcaddy-setup","dashcaddy-config","theme","user-themes","custom-theme","custom-apps","custom-services","toolbar-sections","weather-location","weather-zip","weather-geo","weather-unit","clock-style","clock-chimes","clock-chime-volume"];function C(){for(var e={},a=0;a

\u{1F4BE} Backup & Restore

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

\u2795 Add New Schedule

',O.innerHTML=i,O.querySelectorAll(".schedule-toggle").forEach(function(C){C.addEventListener("change",function(){v(C.dataset.appid,{enabled:C.checked})})}),O.querySelectorAll(".schedule-select").forEach(function(C){C.addEventListener("change",function(){v(C.dataset.appid,{schedule:C.value})})}),O.querySelectorAll(".retention-input").forEach(function(C){C.addEventListener("change",function(){v(C.dataset.appid,{retention:{keep:parseInt(C.value)||7}})})}),O.querySelectorAll(".schedule-run-now").forEach(function(C){C.addEventListener("click",function(){w(C.dataset.appid)})}),O.querySelectorAll(".schedule-delete").forEach(function(C){C.addEventListener("click",function(){g(C.dataset.appid)})}),document.getElementById("add-schedule-btn")?.addEventListener("click",x)}catch(C){O.innerHTML='
Failed to load: '+escapeHtml(C.message)+"
"}}}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='
\u{1F4BE}
No backup files on disk
Run a backup to create backup files
';return}for(var i={},n=0;n";y+='
';for(var C=Object.keys(i).sort(),U=0;U
'+escapeHtml(d)+' ('+q.length+" backup(s))
";for(var F=0;F
'+r.sizeFormatted+'
'+_+'
'}y+=""}y+="",j.innerHTML=y,j.querySelectorAll(".disk-compare-btn").forEach(function(J){J.addEventListener("click",function(){c(J.dataset.appid,J.dataset.filename)})}),j.querySelectorAll(".disk-restore-btn").forEach(function(J){J.addEventListener("click",function(){t(J.dataset.appid,J.dataset.filename)})})}catch(J){j.innerHTML='
Failed: '+escapeHtml(J.message)+"
"}}}async function p(){if(L){L.innerHTML='
Loading...
';try{var e=await fetch("/api/v1/backups/history?limit=50"),a=await e.json();if(!a.success||!a.history?.length){L.innerHTML='
\u{1F4CB} No backup history yet
';return}for(var o='
',i=0;i',o+='
',o+=' '+escapeHtml(n.name||"backup")+"",o+='
',o+=' '+escapeHtml(n.status)+"",n.status==="success"&&(o+=' '),o+="
",o+="
",o+='
',o+=" "+new Date(n.timestamp).toLocaleString()+" | "+r+" MB | "+(n.duration?(n.duration/1e3).toFixed(1)+"s":"--"),n.encrypted&&(o+=" | \u{1F512}"),o+="
",o+="
"}o+="",L.innerHTML=o,L.querySelectorAll(".backup-restore-btn").forEach(function(d){d.addEventListener("click",function(){window.__restoreServerBackup(d.dataset.backupId)})})}catch(d){L.innerHTML='
Failed: '+escapeHtml(d.message)+"
"}}}window.__restoreServerBackup=async function(e){if(confirm("Restore from this server backup? This will overwrite current configuration."))try{var a=await secureFetch("/api/v1/backups/restore/"+e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({restoreServices:!0,restoreConfig:!0})}),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.
`;return}}catch{}N.innerHTML='
Loading...
';try{var o=await fetch("/api/v1/services"),i=await o.json(),n=i.services||[];if(n.length===0){N.innerHTML='
\u{1F4E6} No apps deployed yet
';return}for(var r='
',N.innerHTML=r,document.getElementById("pit-load-btn")?.addEventListener("click",function(){var y=document.getElementById("pit-app-select")?.value;y&&m(y)})}catch(y){N.innerHTML='
Failed: '+escapeHtml(y.message)+"
"}}}async function m(e){var a=document.getElementById("pit-backups-list");if(a){a.innerHTML='
Loading backups...
';try{var o=await fetch("/api/v1/backups/files/"+encodeURIComponent(e)),i=await o.json();if(!i.success||!i.files||i.files.length===0){a.innerHTML='
\u{1F4BE} No backup files for '+escapeHtml(e)+"
";return}for(var n='
'+i.files.length+' backup(s)
',r=0;r
'+d.sizeFormatted+'
'+y+'
'}n+="",a.innerHTML=n,a.querySelectorAll(".pit-compare-btn").forEach(function(C){C.addEventListener("click",function(){c(C.dataset.appid,C.dataset.filename)})}),a.querySelectorAll(".pit-restore-btn").forEach(function(C){C.addEventListener("click",function(){t(C.dataset.appid,C.dataset.filename)})})}catch(C){a.innerHTML='
Failed: '+escapeHtml(C.message)+"
"}}}async function c(e,a){try{var o=await secureFetch("/api/v1/backups/compare/"+encodeURIComponent(a),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({})}),i=await o.json();if(!i.success){showNotification("Compare failed: "+(i.error||"Unknown"),"error");return}var n=i.diff,r='

\u{1F4CA} Compare: '+escapeHtml(a)+'

Size: '+(n.sizeFormatted||"?")+" | Created: "+new Date(n.timestamp).toLocaleString()+"
";if(n.services){var d=n.services.hasChanges?"\u{1F534}":"\u{1F7E2}";r+='
'+d+' Services (backup vs current)
Backup: '+n.services.backupCount+" services | Current: "+n.services.currentCount+" services
",n.services.hasChanges&&(r+='
Services differ \u2014 restoring will replace current configuration
'),r+="
"}if(n.config){var y=n.config.hasChanges?"\u{1F534}":"\u{1F7E2}";r+='
'+y+" Configuration
",n.config.hasChanges?r+='
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(C){C.target===this&&this.remove()})}catch(C){showNotification("Compare error: "+C.message,"error")}}async function t(e,a){if(confirm("Restore "+a+" for "+e+`? + `);var P=document.getElementById("backup-modal"),H=document.getElementById("backup-restore-btn"),A=document.getElementById("backup-cancel"),x=document.getElementById("backup-export-btn"),B=document.getElementById("backup-select-file"),w=document.getElementById("backup-file-input"),M=document.getElementById("backup-file-name"),L=document.getElementById("backup-preview"),$=document.getElementById("backup-preview-content"),u=document.getElementById("backup-do-restore-btn"),v=document.getElementById("backup-result"),D=document.getElementById("backup-schedules-container"),I=document.getElementById("backup-history-container"),R=document.getElementById("backup-disk-container"),N=document.getElementById("pointintime-container"),O=null;H?.addEventListener("click",function(){P.classList.add("show"),v&&(v.style.display="none"),L&&(L.style.display="none"),M&&(M.style.display="none"),O=null}),wireModal(P,A),x?.addEventListener("click",async function(){x.disabled=!0,x.innerHTML=' Exporting...';try{var e=await fetch("/api/v1/backup/export"),a=await e.json();a.browserState=C();var o=new Blob([JSON.stringify(a,null,2)],{type:"application/json"}),i=URL.createObjectURL(o),n=document.createElement("a");n.href=i,n.download="dashcaddy-backup-"+new Date().toISOString().split("T")[0]+".json",document.body.appendChild(n),n.click(),document.body.removeChild(n),URL.revokeObjectURL(i);var r=Object.keys(a.browserState).length,d=a.themes?Object.keys(a.themes).length:0;v.innerHTML="\u2705 Full backup downloaded \u2014 server config + "+r+" browser settings"+(d?" + "+d+" themes":""),v.style.display="block",v.style.background="color-mix(in srgb, var(--ok-fg) 15%, transparent)",v.style.border="1px solid var(--ok-fg)"}catch(h){v.innerHTML="\u274C Export failed: "+escapeHtml(h.message),v.style.display="block",v.style.background="color-mix(in srgb, var(--bad-fg) 15%, transparent)",v.style.border="1px solid var(--bad-fg)"}x.disabled=!1,x.innerHTML="\u2B07\uFE0F Download Full Backup"}),B?.addEventListener("click",function(){w.click()}),w?.addEventListener("change",async function(e){var a=e.target.files[0];if(a){M.textContent="\u{1F4C4} "+a.name,M.style.display="block",v.style.display="none";try{var o=await a.text(),i=JSON.parse(o);if(k(i)){O=i;var n='
Legacy format (v'+escapeHtml(i.version)+")
";n+='
',i.services?.length&&(n+='\u{1F4CB} '+i.services.length+" services"),i.customApps?.length&&(n+='\u{1F4E6} '+i.customApps.length+" custom apps"),i.theme&&(n+='\u{1F3A8} Theme: '+escapeHtml(i.theme)+""),i.userThemes&&(n+='\u{1F3A8} '+Object.keys(i.userThemes).length+" custom themes"),n+="
",$.innerHTML=n,L.style.display="block";return}var r=await secureFetch("/api/v1/backup/preview",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)}),d=await r.json();if(d.success){O=i;var n='
Exported: '+new Date(i.exportedAt).toLocaleString()+" (v"+escapeHtml(i.version)+")
";n+='
Server Config
',n+='
';for(var h in d.preview.files){var T=d.preview.files[h],U=T.action==="create"?"\u{1F195}":"\u{1F4DD}";n+=''+U+" "+escapeHtml(T.description)+""}n+="
",d.preview.serviceCount&&(n+='
'+d.preview.serviceCount+" services
"),d.preview.themeCount&&(n+='
\u{1F3A8} '+d.preview.themeCount+" custom themes
"),d.preview.browserStateCount&&(n+='
Browser Preferences
',n+='
\u{1F5A5}\uFE0F '+d.preview.browserStateCount+" saved settings (theme, weather, clock, widgets, etc.)
"),$.innerHTML=n,L.style.display="block"}else v.innerHTML="\u26A0\uFE0F Invalid backup file: "+escapeHtml(d.error),v.style.display="block",v.style.background="color-mix(in srgb, #f39c12 15%, transparent)",v.style.border="1px solid #f39c12",L.style.display="none"}catch(q){v.innerHTML="\u274C Could not read file: "+escapeHtml(q.message),v.style.display="block",v.style.background="color-mix(in srgb, var(--bad-fg) 15%, transparent)",v.style.border="1px solid var(--bad-fg)",L.style.display="none"}}}),u?.addEventListener("click",async function(){if(O&&confirm("This will overwrite your current configuration and browser preferences. Continue?")){u.disabled=!0,u.innerHTML=' Restoring...';try{if(k(O)){z(O),v.innerHTML="\u2705 Legacy backup restored \u2014 browser settings and services imported.",v.style.background="color-mix(in srgb, var(--ok-fg) 15%, transparent)",v.style.border="1px solid var(--ok-fg)",v.style.display="block",setTimeout(function(){location.reload()},2e3),u.disabled=!1,u.innerHTML="\u26A1 Restore Everything";return}var e=document.getElementById("backup-reload-caddy")?.checked??!0,a=await secureFetch("/api/v1/backup/restore",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({backup:O,options:{reloadCaddy:e}})}),o=await a.json(),i=0;if(O.browserState&&(i=j(O.browserState)),o.success){var n="\u2705 "+o.message;i>0&&(n+='
'+i+" browser settings restored"),o.results.caddyReloaded&&(n+='
Caddy configuration reloaded'),v.innerHTML=n,v.style.background="color-mix(in srgb, var(--ok-fg) 15%, transparent)",v.style.border="1px solid var(--ok-fg)",setTimeout(function(){location.reload()},2e3)}else v.innerHTML="\u26A0\uFE0F "+escapeHtml(o.message),i>0&&(v.innerHTML+='
'+i+" browser settings were restored"),o.results?.errors?.length>0&&(v.innerHTML+="
"+o.results.errors.map(function(r){return escapeHtml(r.file)+": "+escapeHtml(r.error)}).join(", ")+""),v.style.background="color-mix(in srgb, #f39c12 15%, transparent)",v.style.border="1px solid #f39c12";v.style.display="block"}catch(r){v.innerHTML="\u274C Restore failed: "+escapeHtml(r.message),v.style.display="block",v.style.background="color-mix(in srgb, var(--bad-fg) 15%, transparent)",v.style.border="1px solid var(--bad-fg)"}u.disabled=!1,u.innerHTML="\u26A1 Restore Everything"}});async function m(){if(D){D.innerHTML='
Loading...
';try{var e=await fetch("/api/v1/backups/schedule"),a=await e.json();if(a.premiumRequired){D.innerHTML=`
\u2B50
Premium Feature
Auto-backup scheduling requires a DashCaddy Premium subscription.
`;return}if(!a.success)throw new Error(a.error||"Failed to load schedules");var o=a.schedules||[];if(o.length===0){D.innerHTML='
\u23F0
No backup schedules configured
Select apps below to enable auto-backup
';return}for(var i='
',n=0;n
Schedule:
Keep last:
Next run: '+escapeHtml(d)+"
Last run: "+escapeHtml(h)+'
'}i+="",i+='

\u2795 Add New Schedule

',D.innerHTML=i,D.querySelectorAll(".schedule-toggle").forEach(function(T){T.addEventListener("change",function(){y(T.dataset.appid,{enabled:T.checked})})}),D.querySelectorAll(".schedule-select").forEach(function(T){T.addEventListener("change",function(){y(T.dataset.appid,{schedule:T.value})})}),D.querySelectorAll(".retention-input").forEach(function(T){T.addEventListener("change",function(){y(T.dataset.appid,{retention:{keep:parseInt(T.value)||7}})})}),D.querySelectorAll(".schedule-run-now").forEach(function(T){T.addEventListener("click",function(){S(T.dataset.appid)})}),D.querySelectorAll(".schedule-delete").forEach(function(T){T.addEventListener("click",function(){f(T.dataset.appid)})}),document.getElementById("add-schedule-btn")?.addEventListener("click",E)}catch(T){D.innerHTML='
Failed to load: '+escapeHtml(T.message)+"
"}}}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='
\u{1F4BE}
No backup files on disk
Run a backup to create backup files
';return}for(var i={},n=0;n";h+='
';for(var T=Object.keys(i).sort(),U=0;U
'+escapeHtml(d)+' ('+q.length+" backup(s))
";for(var F=0;F
'+r.sizeFormatted+'
'+_+'
'}h+=""}h+="",R.innerHTML=h,R.querySelectorAll(".disk-compare-btn").forEach(function(J){J.addEventListener("click",function(){c(J.dataset.appid,J.dataset.filename)})}),R.querySelectorAll(".disk-restore-btn").forEach(function(J){J.addEventListener("click",function(){t(J.dataset.appid,J.dataset.filename)})})}catch(J){R.innerHTML='
Failed: '+escapeHtml(J.message)+"
"}}}async function p(){if(I){I.innerHTML='
Loading...
';try{var e=await fetch("/api/v1/backups/history?limit=50"),a=await e.json();if(!a.success||!a.history?.length){I.innerHTML='
\u{1F4CB} No backup history yet
';return}for(var o='
',i=0;i',o+='
',o+=' '+escapeHtml(n.name||"backup")+"",o+='
',o+=' '+escapeHtml(n.status)+"",n.status==="success"&&(o+=' '),o+="
",o+="
",o+='
',o+=" "+new Date(n.timestamp).toLocaleString()+" | "+r+" MB | "+(n.duration?(n.duration/1e3).toFixed(1)+"s":"--"),n.encrypted&&(o+=" | \u{1F512}"),o+="
",o+="
"}o+="",I.innerHTML=o,I.querySelectorAll(".backup-restore-btn").forEach(function(d){d.addEventListener("click",function(){window.__restoreServerBackup(d.dataset.backupId)})})}catch(d){I.innerHTML='
Failed: '+escapeHtml(d.message)+"
"}}}window.__restoreServerBackup=async function(e){if(confirm("Restore from this server backup? This will overwrite current configuration."))try{var a=await secureFetch("/api/v1/backups/restore/"+e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({restoreServices:!0,restoreConfig:!0})}),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.
`;return}}catch{}N.innerHTML='
Loading...
';try{var o=await fetch("/api/v1/services"),i=await o.json(),n=i.services||[];if(n.length===0){N.innerHTML='
\u{1F4E6} No apps deployed yet
';return}for(var r='
',N.innerHTML=r,document.getElementById("pit-load-btn")?.addEventListener("click",function(){var h=document.getElementById("pit-app-select")?.value;h&&g(h)})}catch(h){N.innerHTML='
Failed: '+escapeHtml(h.message)+"
"}}}async function g(e){var a=document.getElementById("pit-backups-list");if(a){a.innerHTML='
Loading backups...
';try{var o=await fetch("/api/v1/backups/files/"+encodeURIComponent(e)),i=await o.json();if(!i.success||!i.files||i.files.length===0){a.innerHTML='
\u{1F4BE} No backup files for '+escapeHtml(e)+"
";return}for(var n='
'+i.files.length+' backup(s)
',r=0;r
'+d.sizeFormatted+'
'+h+'
'}n+="",a.innerHTML=n,a.querySelectorAll(".pit-compare-btn").forEach(function(T){T.addEventListener("click",function(){c(T.dataset.appid,T.dataset.filename)})}),a.querySelectorAll(".pit-restore-btn").forEach(function(T){T.addEventListener("click",function(){t(T.dataset.appid,T.dataset.filename)})})}catch(T){a.innerHTML='
Failed: '+escapeHtml(T.message)+"
"}}}async function c(e,a){try{var o=await secureFetch("/api/v1/backups/compare/"+encodeURIComponent(a),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({})}),i=await o.json();if(!i.success){showNotification("Compare failed: "+(i.error||"Unknown"),"error");return}var n=i.diff,r='

\u{1F4CA} Compare: '+escapeHtml(a)+'

Size: '+(n.sizeFormatted||"?")+" | Created: "+new Date(n.timestamp).toLocaleString()+"
";if(n.services){var d=n.services.hasChanges?"\u{1F534}":"\u{1F7E2}";r+='
'+d+' Services (backup vs current)
Backup: '+n.services.backupCount+" services | Current: "+n.services.currentCount+" services
",n.services.hasChanges&&(r+='
Services differ \u2014 restoring will replace current configuration
'),r+="
"}if(n.config){var h=n.config.hasChanges?"\u{1F534}":"\u{1F7E2}";r+='
'+h+" Configuration
",n.config.hasChanges?r+='
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='
No running containers found
';return}let c='
';for(const[t,e]of Object.entries(l)){const a=e.current||e,o=a.cpu?.percent||0,i=a.memory?.percent||0,n=S(o),r=$(i),d=a.memory?.usage||a.memory?.used||0,y=a.memory?.limit||0,C=a.network?.rxBytes||a.network?.rx||0,U=a.network?.txBytes||a.network?.tx||0,q=e.aggregated;c+=` +
`);const b=document.getElementById("stats-modal"),C=document.getElementById("container-stats-btn"),j=document.getElementById("stats-cancel"),k=document.getElementById("stats-refresh-btn"),z=document.getElementById("stats-auto-refresh"),P=document.getElementById("stats-container"),H=document.getElementById("stats-aggregated-container"),A=document.getElementById("stats-alerts-container"),x=document.getElementById("stats-last-update");let B=null,w=null;function M(l){if(l===0||!l)return"0 B";const g=1024,c=["B","KB","MB","GB"],t=Math.floor(Math.log(l)/Math.log(g));return parseFloat((l/Math.pow(g,t)).toFixed(1))+" "+c[t]}function L(l){return l<30?"#2ecc71":l<70?"#f39c12":"#e74c3c"}function $(l){return l<50?"#2ecc71":l<80?"#f39c12":"#e74c3c"}async function u(){try{let l=null,g=!1;try{const e=await(await fetch("/api/v1/monitoring/stats")).json();e.success&&e.stats&&(l=e.stats,g=!0,w=e.stats)}catch{}if(!g){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};w=l}}if(!l||Object.keys(l).length===0){P.innerHTML='
No running containers found
';return}let c='
';for(const[t,e]of Object.entries(l)){const a=e.current||e,o=a.cpu?.percent||0,i=a.memory?.percent||0,n=L(o),r=$(i),d=a.memory?.usage||a.memory?.used||0,h=a.memory?.limit||0,T=a.network?.rxBytes||a.network?.rx||0,U=a.network?.txBytes||a.network?.tx||0,q=e.aggregated;c+=`
${e.name||t} @@ -1330,18 +1330,18 @@ This will replace current configuration, credentials, and data. Containers will
${i.toFixed(1)}%
-
${H(d)} / ${H(y)}
+
${M(d)} / ${M(h)}
Network
- \u2193 ${H(C)} + \u2193 ${M(T)} / - \u2191 ${H(U)} + \u2191 ${M(U)}
- `}c+="",P.innerHTML=c,b.textContent="Updated: "+new Date().toLocaleTimeString()}catch(l){P.innerHTML=`
\u274C Failed to load stats: ${escapeHtml(l.message)}
`}}async function E(){if(!M)return;const l=h;if(!l||Object.keys(l).length===0){M.innerHTML='
\u{1F4C8}No monitoring data available. Open the Live Stats tab first.
';return}let m='
';for(const[c,t]of Object.entries(l)){const e=t.aggregated;e&&(m+=`
+
`}c+="
",P.innerHTML=c,x.textContent="Updated: "+new Date().toLocaleTimeString()}catch(l){P.innerHTML=`
\u274C Failed to load stats: ${escapeHtml(l.message)}
`}}async function v(){if(!H)return;const l=w;if(!l||Object.keys(l).length===0){H.innerHTML='
\u{1F4C8}No monitoring data available. Open the Live Stats tab first.
';return}let g='
';for(const[c,t]of Object.entries(l)){const e=t.aggregated;e&&(g+=`
${t.name||c}
${e.cpu?.avg?.toFixed(1)||0}%Avg CPU
@@ -1350,13 +1350,13 @@ This will replace current configuration, credentials, and data. Containers will
${e.memory?.max?.toFixed(1)||0}%Max Mem
${e.dataPoints?`
${e.dataPoints} data points over ${e.timeRange||24}h
`:""} -
`)}m+="
",M.innerHTML=m}async function O(){if(!D)return;D.innerHTML='
Loading alerts...
';const l=h;if(!l||Object.keys(l).length===0){D.innerHTML='
\u{1F514}No containers found. Open the Live Stats tab first.
';return}let m=!1;try{m=(await(await fetch("/api/v1/license/feature/resource-alerts")).json()).available}catch{m=!1}let c=[];try{const r=await(await fetch("/api/v1/monitoring/alerts?limit=50")).json();r.success&&(c=r.history||[])}catch{}let t={};try{const r=await(await fetch("/api/v1/monitoring/alerts/config")).json();r.success&&(t=r.configs||{})}catch{}const a=Object.entries(l).map(([n,r])=>{const d=t[n]||{cpuThreshold:80,memoryThreshold:90,diskIOThreshold:50,autoRestart:!1,enabled:!1};return` + `)}g+="",H.innerHTML=g}async function D(){if(!A)return;A.innerHTML='
Loading alerts...
';const l=w;if(!l||Object.keys(l).length===0){A.innerHTML='
\u{1F514}No containers found. Open the Live Stats tab first.
';return}let g=!1;try{g=(await(await fetch("/api/v1/license/feature/resource-alerts")).json()).available}catch{g=!1}let c=[];try{const r=await(await fetch("/api/v1/monitoring/alerts?limit=50")).json();r.success&&(c=r.history||[])}catch{}let t={};try{const r=await(await fetch("/api/v1/monitoring/alerts/config")).json();r.success&&(t=r.configs||{})}catch{}const a=Object.entries(l).map(([n,r])=>{const d=t[n]||{cpuThreshold:80,memoryThreshold:90,diskIOThreshold:50,autoRestart:!1,enabled:!1};return` ${r.name||n} - - - - + + + + @@ -1370,7 +1370,7 @@ This will replace current configuration, credentials, and data. Containers will ${d} ${n.autoRestartTriggered?"\u21BB":""} - `}).join(""),i=m?` + `}).join(""),i=g?`

\u2699\uFE0F Alert Configuration

@@ -1401,7 +1401,7 @@ This will replace current configuration, credentials, and data. Containers will

Upgrade to configure resource alert thresholds per container.

- `;D.innerHTML=` + `;A.innerHTML=` ${i}

\u{1F4CB} Recent Alerts

@@ -1423,7 +1423,7 @@ This will replace current configuration, credentials, and data. Containers will
`:'
No alerts recorded yet.
'}
- `,document.getElementById("save-all-alerts")?.addEventListener("click",async()=>{const n={};document.querySelectorAll("#stats-alerts-container tr[data-container]").forEach(r=>{const d=r.dataset.container;n[d]={cpuThreshold:parseInt(r.querySelector(".alert-cpu")?.value)||80,memoryThreshold:parseInt(r.querySelector(".alert-mem")?.value)||90,diskIOThreshold:parseInt(r.querySelector(".alert-disk")?.value)||50,autoRestart:!!r.querySelector(".alert-autorestart")?.checked,enabled:!0}});try{const d=await(await secureFetch("/api/v1/monitoring/alerts/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({configs:n})})).json(),y=document.getElementById("save-all-alerts");y.textContent=d.success?"\u2705 Saved":"\u274C Failed",setTimeout(()=>{y.textContent="Save All"},2e3)}catch{const d=document.getElementById("save-all-alerts");d.textContent="\u274C Error",setTimeout(()=>{d.textContent="Save All"},2e3)}}),document.getElementById("go-to-notifications")?.addEventListener("click",n=>{n.preventDefault(),k.classList.remove("show"),j(),document.getElementById("manage-notifications")?.click()}),document.querySelectorAll(".alert-test-btn").forEach(n=>{n.addEventListener("click",async()=>{const r=n.textContent;n.textContent="...";try{await secureFetch(`/api/v1/monitoring/alerts/${n.dataset.container}/test`,{method:"POST"}),n.textContent="\u2705",showNotification("Test alert sent for "+n.dataset.name,"success",3e3)}catch{n.textContent="\u274C"}setTimeout(()=>{n.textContent=r},2e3)})}),document.getElementById("upgrade-for-alerts")?.addEventListener("click",()=>{k.classList.remove("show"),j(),typeof openLicenseModal=="function"&&openLicenseModal()})}function L(){T&&clearInterval(T),z?.checked&&(T=setInterval(f,DC.POLL.STATS))}function j(){T&&(clearInterval(T),T=null)}B?.addEventListener("click",()=>{k.classList.add("show"),f(),L()}),A?.addEventListener("click",()=>{k.classList.remove("show"),j()}),k?.addEventListener("click",l=>{l.target===k&&(k.classList.remove("show"),j())}),I?.addEventListener("click",f),z?.addEventListener("change",()=>{z.checked?L():j()}),document.querySelector('[data-panel="stats-aggregated"]')?.addEventListener("click",E),document.querySelector('[data-panel="stats-alerts"]')?.addEventListener("click",O);const N=document.getElementById("stats-history-container"),R=document.getElementById("stats-history-container-area"),u=document.querySelectorAll(".stats-range-btn");let v="1h";function w(l){switch(l){case"1h":return 3600*1e3;case"24h":return 1440*60*1e3;case"7d":return 10080*60*1e3;case"30d":return 720*60*60*1e3;case"1y":return 365*24*60*60*1e3;default:return 3600*1e3}}function g(l){return l==="raw"?"live (10s samples)":l==="hourly"?"hourly average":l==="daily"?"daily average":l}function x(l,m,c,t,e){if(!l||l.length===0)return`
No data for ${escapeHtml(t)}
`;const a=l.map(m).filter(_=>_!=null);if(a.length===0)return`
No data for ${escapeHtml(t)}
`;const o=Math.max(...a,1),i=Math.min(...a,0),n=o-i||1,r=600,d=80,y=4,C=(r-y*2)/Math.max(a.length-1,1),U=a.map((_,J)=>{const X=y+J*C,Q=d-y-(_-i)/n*(d-y*2);return`${X.toFixed(1)},${Q.toFixed(1)}`}).join(" "),q=a[a.length-1],F=a.reduce((_,J)=>_+J,0)/a.length;return` + `,document.getElementById("save-all-alerts")?.addEventListener("click",async()=>{const n={};document.querySelectorAll("#stats-alerts-container tr[data-container]").forEach(r=>{const d=r.dataset.container;n[d]={cpuThreshold:parseInt(r.querySelector(".alert-cpu")?.value)||80,memoryThreshold:parseInt(r.querySelector(".alert-mem")?.value)||90,diskIOThreshold:parseInt(r.querySelector(".alert-disk")?.value)||50,autoRestart:!!r.querySelector(".alert-autorestart")?.checked,enabled:!0}});try{const d=await(await secureFetch("/api/v1/monitoring/alerts/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({configs:n})})).json(),h=document.getElementById("save-all-alerts");h.textContent=d.success?"\u2705 Saved":"\u274C Failed",setTimeout(()=>{h.textContent="Save All"},2e3)}catch{const d=document.getElementById("save-all-alerts");d.textContent="\u274C Error",setTimeout(()=>{d.textContent="Save All"},2e3)}}),document.getElementById("go-to-notifications")?.addEventListener("click",n=>{n.preventDefault(),b.classList.remove("show"),R(),document.getElementById("manage-notifications")?.click()}),document.querySelectorAll(".alert-test-btn").forEach(n=>{n.addEventListener("click",async()=>{const r=n.textContent;n.textContent="...";try{await secureFetch(`/api/v1/monitoring/alerts/${n.dataset.container}/test`,{method:"POST"}),n.textContent="\u2705",showNotification("Test alert sent for "+n.dataset.name,"success",3e3)}catch{n.textContent="\u274C"}setTimeout(()=>{n.textContent=r},2e3)})}),document.getElementById("upgrade-for-alerts")?.addEventListener("click",()=>{b.classList.remove("show"),R(),typeof openLicenseModal=="function"&&openLicenseModal()})}function I(){B&&clearInterval(B),z?.checked&&(B=setInterval(u,DC.POLL.STATS))}function R(){B&&(clearInterval(B),B=null)}C?.addEventListener("click",()=>{b.classList.add("show"),u(),I()}),j?.addEventListener("click",()=>{b.classList.remove("show"),R()}),b?.addEventListener("click",l=>{l.target===b&&(b.classList.remove("show"),R())}),k?.addEventListener("click",u),z?.addEventListener("change",()=>{z.checked?I():R()}),document.querySelector('[data-panel="stats-aggregated"]')?.addEventListener("click",v),document.querySelector('[data-panel="stats-alerts"]')?.addEventListener("click",D);const N=document.getElementById("stats-history-container"),O=document.getElementById("stats-history-container-area"),m=document.querySelectorAll(".stats-range-btn");let y="1h";function S(l){switch(l){case"1h":return 3600*1e3;case"24h":return 1440*60*1e3;case"7d":return 10080*60*1e3;case"30d":return 720*60*60*1e3;case"1y":return 365*24*60*60*1e3;default:return 3600*1e3}}function f(l){return l==="raw"?"live (10s samples)":l==="hourly"?"hourly average":l==="daily"?"daily average":l}function E(l,g,c,t,e){if(!l||l.length===0)return`
No data for ${escapeHtml(t)}
`;const a=l.map(g).filter(_=>_!=null);if(a.length===0)return`
No data for ${escapeHtml(t)}
`;const o=Math.max(...a,1),i=Math.min(...a,0),n=o-i||1,r=600,d=80,h=4,T=(r-h*2)/Math.max(a.length-1,1),U=a.map((_,J)=>{const X=h+J*T,Q=d-h-(_-i)/n*(d-h*2);return`${X.toFixed(1)},${Q.toFixed(1)}`}).join(" "),q=a[a.length-1],F=a.reduce((_,J)=>_+J,0)/a.length;return`
${escapeHtml(t)} @@ -1433,11 +1433,11 @@ This will replace current configuration, credentials, and data. Containers will
- `}function s(){if(!N)return;const l=h||{},m=N.value,c=Object.entries(l);if(c.length===0){N.innerHTML='';return}N.innerHTML=c.map(([t,e])=>``).join(""),m&&l[m]&&(N.value=m)}async function p(){if(!R||!N)return;const l=N.value;if(!l){R.innerHTML='
\u{1F4CA}No container selected.
';return}const m=Date.now(),c=m-w(v);R.innerHTML='
Loading history...
';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='
\u{1F4CA}No container selected.
';return}const g=Date.now(),c=g-S(y);O.innerHTML='
Loading history...
';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)}.
`;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,h=i?U=>U.network?.txMB||0:U=>U.network?.txMB||0;let T=`
- ${a.length} samples \xB7 ${escapeHtml(g(o))} \xB7 ${new Date(c).toLocaleString()} \u2192 ${new Date(m).toLocaleString()} + ${a.length} samples \xB7 ${escapeHtml(f(o))} \xB7 ${new Date(c).toLocaleString()} \u2192 ${new Date(g).toLocaleString()}
- `;C+=x(a,n,"#2ecc71","CPU","%"),C+=x(a,r,"#3498db","Memory","%"),C+=x(a,d,"#9b59b6","Network RX"," MB"),C+=x(a,y,"#e67e22","Network TX"," MB"),R.innerHTML=C}catch(t){R.innerHTML=`
\u26A0\uFE0FFailed to load history: ${escapeHtml(t.message)}
`}}u.forEach(l=>{l.addEventListener("click",()=>{u.forEach(m=>m.classList.remove("active")),l.classList.add("active"),v=l.dataset.range,p()})}),N?.addEventListener("change",p),document.querySelector('[data-panel="stats-history"]')?.addEventListener("click",()=>{s(),p()})})(),(function(){injectModal("health-modal",`
+ `;T+=E(a,n,"#2ecc71","CPU","%"),T+=E(a,r,"#3498db","Memory","%"),T+=E(a,d,"#9b59b6","Network RX"," MB"),T+=E(a,h,"#e67e22","Network TX"," MB"),O.innerHTML=T}catch(t){O.innerHTML=`
\u26A0\uFE0FFailed to load history: ${escapeHtml(t.message)}
`}}m.forEach(l=>{l.addEventListener("click",()=>{m.forEach(g=>g.classList.remove("active")),l.classList.add("active"),y=l.dataset.range,p()})}),N?.addEventListener("change",p),document.querySelector('[data-panel="stats-history"]')?.addEventListener("click",()=>{s(),p()})})(),(function(){injectModal("health-modal",`

\u{1F3E5} Health Check Dashboard

-
`);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+='',d+='',d+='';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+=``,d+=``,d+=``,d+=``,d+=``,d+=``,d+=``,d+="",d+=``}d+="
ServiceStatusUptime 24hUptime 7dAvg ResponseLast Check
${escapeHtml(y.name||y.serviceId)}${C?"Up":"Down"}${typeof q=="number"?q.toFixed(1)+"%":q}${typeof F=="number"?F.toFixed(1)+"%":F}${_}${J}
",z.innerHTML=d,D.textContent="Updated "+new Date().toLocaleTimeString(),z.querySelectorAll("tr[data-health-id]").forEach(y=>{y.addEventListener("click",async()=>{const C=y.dataset.healthId,U=document.getElementById("health-detail-"+C);if(U){if(U.style.display!=="none"){U.style.display="none";return}U.style.display="";try{const F=await(await fetch(`/api/v1/health-checks/${C}/stats?hours=24`)).json();if(F.success&&F.stats){const _=F.stats,J=_.responseTime||{};U.querySelector("td").innerHTML=` +
`);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+='',d+='',d+='';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+=``,d+=``,d+=``,d+=``,d+=``,d+=``,d+=``,d+="",d+=``}d+="
ServiceStatusUptime 24hUptime 7dAvg ResponseLast Check
${escapeHtml(h.name||h.serviceId)}${T?"Up":"Down"}${typeof q=="number"?q.toFixed(1)+"%":q}${typeof F=="number"?F.toFixed(1)+"%":F}${_}${J}
",z.innerHTML=d,A.textContent="Updated "+new Date().toLocaleTimeString(),z.querySelectorAll("tr[data-health-id]").forEach(h=>{h.addEventListener("click",async()=>{const T=h.dataset.healthId,U=document.getElementById("health-detail-"+T);if(U){if(U.style.display!=="none"){U.style.display="none";return}U.style.display="";try{const F=await(await fetch(`/api/v1/health-checks/${T}/stats?hours=24`)).json();if(F.success&&F.stats){const _=F.stats,J=_.responseTime||{};U.querySelector("td").innerHTML=`
Total Checks
${_.totalChecks||0}
Uptime
${(_.uptime||0).toFixed(2)}%
@@ -1571,14 +1571,14 @@ This will replace current configuration, credentials, and data. Containers will
Max Response
${Math.round(J.max||0)}ms
Up Checks
${_.upChecks||0}
Down Checks
${_.downChecks||0}
-
`}else U.querySelector("td").innerHTML='
No detailed stats available for this period.
'}catch(q){U.querySelector("td").innerHTML=`
Failed: ${escapeHtml(q.message)}
`}}})})}catch(i){z.innerHTML=`
Failed to load health status: ${escapeHtml(i.message)}
`}}async function t(){try{const[i,n]=await Promise.all([fetch("/api/v1/health-checks/incidents"),fetch("/api/v1/health-checks/incidents/history?limit=50")]),r=await i.json(),d=await n.json();let y="";const C=r.success&&r.incidents?r.incidents:[];if(C.length>0){y+='

Open Incidents ('+C.length+")

";for(const q of C)y+=`
+
`}else U.querySelector("td").innerHTML='
No detailed stats available for this period.
'}catch(q){U.querySelector("td").innerHTML=`
Failed: ${escapeHtml(q.message)}
`}}})})}catch(i){z.innerHTML=`
Failed to load health status: ${escapeHtml(i.message)}
`}}async function t(){try{const[i,n]=await Promise.all([fetch("/api/v1/health-checks/incidents"),fetch("/api/v1/health-checks/incidents/history?limit=50")]),r=await i.json(),d=await n.json();let h="";const T=r.success&&r.incidents?r.incidents:[];if(T.length>0){h+='

Open Incidents ('+T.length+")

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

Incident History

',y+='',y+='';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+=``,y+=``,y+=``,y+=``,y+=``,y+=``,y+=""}y+="
ServiceTypeSeverityStatusDurationWhen
${escapeHtml(q.serviceId)}${escapeHtml(q.type)}${m(q.severity)}${q.status}${_}${timeAgo(q.createdAt)}
"}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+='';for(const y of r){const C=y.status==="up";d+='',d+=``,d+=``,d+=``,d+='"}d+="
ServiceStatusSLA TargetActions
${escapeHtml(y.name||y.serviceId)}${C?"Up":"Down"}${y.sla?.target?y.sla.target+"%":"-"}',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",`
+
`;h+="
"}else h+='
All services operational \u2014 no open incidents
';const U=d.success&&d.history?d.history:[];if(U.length>0){h+='

Incident History

',h+='',h+='';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+=``,h+=``,h+=``,h+=``,h+=``,h+=``,h+=""}h+="
ServiceTypeSeverityStatusDurationWhen
${escapeHtml(q.serviceId)}${escapeHtml(q.type)}${g(q.severity)}${q.status}${_}${timeAgo(q.createdAt)}
"}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+='';for(const h of r){const T=h.status==="up";d+='',d+=``,d+=``,d+=``,d+='"}d+="
ServiceStatusSLA TargetActions
${escapeHtml(h.name||h.serviceId)}${T?"Up":"Down"}${h.sla?.target?h.sla.target+"%":"-"}',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

- `);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='
\u2705All containers are up to date.
',D.textContent="",document.getElementById("updates-update-all-btn").style.display="none",document.getElementById("updates-count-badge").style.display="none",window._pendingUpdates=[];return}let a='';a+='';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+=``,a+=``,a+=``,a+=``,a+='"}a+="
ContainerImageCurrentLatestActions
${escapeHtml(n.containerName)}${escapeHtml(n.imageName)}${escapeHtml(n.currentDigest)}${escapeHtml(n.latestDigest)}',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='
Loading...
';const t=await(await fetch("/api/v1/updates/history?limit=50")).json(),e=t.success&&t.history?t.history:[];if(e.length===0){P.innerHTML='
\u{1F4CB}No update history yet.
';return}let a='';a+='';for(const o of e){const i=o.status==="success",n=o.duration?o.duration<1e3?o.duration+"ms":Math.round(o.duration/1e3)+"s":"-";a+='',a+=``,a+=``,a+=``,a+=``,a+=``,a+="",!i&&o.error&&(a+=``)}a+="
WhenContainerImageDurationStatus
${timeAgo(o.timestamp)}${escapeHtml(o.containerName)}${escapeHtml(o.imageName)}${n}${i?"\u2713 success":"\u2717 failed"}
${escapeHtml(o.error)}
",P.innerHTML=a}catch(c){P.innerHTML=`
Failed: ${escapeHtml(c.message)}
`}}async function S(){try{M.innerHTML='
Loading...
';const[c,t]=await Promise.all([fetch("/api/v1/stats/containers"),fetch("/api/v1/updates/auto-update")]),e=await c.json(),a=await t.json(),o=e.success&&e.stats?e.stats:[],i=a.success&&a.config?a.config:{};if(o.length===0){M.innerHTML='
\u{1F916}No running containers found.
';return}let n='
Auto-updates run during maintenance window (default 2AM-4AM). Daily = every day, Weekly = Sundays, Monthly = 1st of month.
';n+='',n+='';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+=``,n+=``,n+=``,n+=``,n+=``,n+=``,n+=``,n+=""}n+="
ContainerScheduleWindowRollbackLast RunActions
${escapeHtml(d)} - ';a+='';for(const n of e){const r=(()=>{const d=window.APPS||[];for(const h of d)if(h.containerId===n.containerId||h.name===n.containerName||h.id===n.containerName)return h.id;return n.containerName})();a+=``,a+=``,a+=``,a+=``,a+=``,a+='"}a+="
ContainerImageCurrentLatestActions
${escapeHtml(n.containerName)}${escapeHtml(n.imageName)}${escapeHtml(n.currentDigest)}${escapeHtml(n.latestDigest)}',a+=``,a+=``,a+="
",z.innerHTML=a,A.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 T=await(await secureFetch(`/api/v1/updates/update/${encodeURIComponent(r)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({autoRollback:!0})})).json();if(T.success)n.textContent="Done!",n.style.background="var(--ok-fg)",setTimeout(()=>x(),2e3);else throw new Error(T.error||"Update failed")}catch(h){n.textContent="Failed",n.style.color="var(--bad-fg)",showNotification("Update error: "+h.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 T=await(await secureFetch(`/api/v1/updates/rollback/${encodeURIComponent(r)}`,{method:"POST"})).json();if(T.success)n.textContent="Rolled back!",setTimeout(()=>x(),2e3);else throw new Error(T.error||"Rollback failed")}catch(h){n.textContent="Failed",showNotification("Rollback error: "+h.message,"error"),setTimeout(()=>{n.textContent="Rollback",n.disabled=!1},3e3)}}})})}catch(c){z.innerHTML=`
Failed: ${escapeHtml(c.message)}
`}}async function B(){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,x()},3e3)}document.getElementById("updates-update-all-btn")?.addEventListener("click",B);async function w(){k.textContent="\u{1F50D} Checking...",k.disabled=!0;try{const t=await(await secureFetch("/api/v1/updates/check",{method:"POST"})).json();if(!t.success)throw new Error(t.error);k.textContent="\u2705 Done!",await x()}catch(c){k.textContent="\u274C Failed",showNotification("Check error: "+c.message,"error")}setTimeout(()=>{k.textContent="\u{1F50D} Check for Updates",k.disabled=!1},3e3)}async function M(){try{P.innerHTML='
Loading...
';const t=await(await fetch("/api/v1/updates/history?limit=50")).json(),e=t.success&&t.history?t.history:[];if(e.length===0){P.innerHTML='
\u{1F4CB}No update history yet.
';return}let a='';a+='';for(const o of e){const i=o.status==="success",n=o.duration?o.duration<1e3?o.duration+"ms":Math.round(o.duration/1e3)+"s":"-";a+='',a+=``,a+=``,a+=``,a+=``,a+=``,a+="",!i&&o.error&&(a+=``)}a+="
WhenContainerImageDurationStatus
${timeAgo(o.timestamp)}${escapeHtml(o.containerName)}${escapeHtml(o.imageName)}${n}${i?"\u2713 success":"\u2717 failed"}
${escapeHtml(o.error)}
",P.innerHTML=a}catch(c){P.innerHTML=`
Failed: ${escapeHtml(c.message)}
`}}async function L(){try{H.innerHTML='
Loading...
';const[c,t]=await Promise.all([fetch("/api/v1/stats/containers"),fetch("/api/v1/updates/auto-update")]),e=await c.json(),a=await t.json(),o=e.success&&e.stats?e.stats:[],i=a.success&&a.config?a.config:{};if(o.length===0){H.innerHTML='
\u{1F916}No running containers found.
';return}let n='
Auto-updates run during maintenance window (default 2AM-4AM). Daily = every day, Weekly = Sundays, Monthly = 1st of month.
';n+='',n+='';for(const r of o){const d=r.name||r.Names?.[0]?.replace(/^\//,"")||r.Id?.substring(0,12),h=r.containerId||r.Id,T=i[h]||{},U=T.enabled?T.schedule||"weekly":"",q=T.autoRollback!==!1,F=T.maintenanceWindow||"",_=T.lastAutoUpdate?timeAgo(T.lastAutoUpdate):"Never";n+=``,n+=``,n+=``,n+=``,n+=``,n+=``,n+=``,n+=""}n+="
ContainerScheduleWindowRollbackLast RunActions
${escapeHtml(d)} + ${_}
",M.innerHTML=n,M.querySelectorAll(".save-auto-btn").forEach(r=>{r.addEventListener("click",async()=>{const d=r.dataset.id,y=r.closest("tr"),C=y.querySelector(".auto-schedule").value,U=y.querySelector(".auto-rollback").checked,q=y.querySelector(".auto-window").value.trim();r.textContent="Saving...",r.disabled=!0;try{const _=await(await secureFetch(`/api/v1/updates/auto-update/${encodeURIComponent(d)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({enabled:!!C,schedule:C||"weekly",autoRollback:U,maintenanceWindow:q||void 0})})).json();if(_.success)r.textContent="\u2713 Saved";else throw new Error(_.error)}catch(F){r.textContent="\u2717 Error",showNotification("Save error: "+F.message,"error")}setTimeout(()=>{r.textContent="Save",r.disabled=!1},2e3)})})}catch(c){M.innerHTML=`
Failed: ${escapeHtml(c.message)}
`}}const $=document.getElementById("dashcaddy-current-version"),f=document.getElementById("dashcaddy-update-badge"),E=document.getElementById("dashcaddy-update-details"),O=document.getElementById("dashcaddy-new-version"),L=document.getElementById("dashcaddy-changelog"),j=document.getElementById("dashcaddy-apply-btn"),N=document.getElementById("dashcaddy-check-btn"),R=document.getElementById("dashcaddy-rollback-btn"),u=document.getElementById("dashcaddy-status-bar"),v=document.getElementById("dashcaddy-history-container");let w=null;function g(c,t){u&&(u.style.display="block",u.style.background=t==="error"?"var(--bad-bg)":t==="success"?"var(--ok-bg)":"var(--bg)",u.style.color=t==="error"?"var(--bad-fg)":t==="success"?"var(--ok-fg)":"var(--fg)",u.textContent=c)}async function x(){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(w=e,e.success&&e.available&&e.remote){f.style.display="",E.style.display="",O.textContent="v"+e.remote.version,L.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 f.style.display="none",E.style.display="none",await x(),c||g("You are running the latest version.","success");c||(N.textContent="Check for Updates",N.disabled=!1)}catch(t){c||(g("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;j.textContent="Updating...",j.disabled=!0,g("Downloading and applying update...","info");try{const t=await(await secureFetch("/api/v1/system/update-apply",{method:"POST"})).json();if(t.success)return g("Update initiated: v"+(t.fromVersion||"?")+" \u2192 v"+(t.toVersion||"?")+". The container will restart shortly.","success"),j.textContent="Applied!",document.querySelectorAll(".update-dot").forEach(e=>e.remove()),!0;throw new Error(t.error||"Update failed")}catch(c){throw g("Update failed: "+c.message,"error"),j.textContent="Update Now",j.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){v.innerHTML='
\u{1F4E6}No self-update history.
';return}let a='';a+='';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+='',a+='",a+='",a+='",a+='",a+="",o.error&&(a+='"),o.note&&(a+='")}a+="
WhenVersionFromStatus
'+timeAgo(o.timestamp)+"v'+escapeHtml(o.version)+(o.rollback?" (rollback)":"")+"v'+escapeHtml(o.fromVersion||"?")+"'+i+"
'+escapeHtml(o.error)+"
'+escapeHtml(o.note)+"
",v.innerHTML=a}catch(c){v.innerHTML='
Failed: '+escapeHtml(c.message)+"
"}}async function m(){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: +
${_}
",H.innerHTML=n,H.querySelectorAll(".save-auto-btn").forEach(r=>{r.addEventListener("click",async()=>{const d=r.dataset.id,h=r.closest("tr"),T=h.querySelector(".auto-schedule").value,U=h.querySelector(".auto-rollback").checked,q=h.querySelector(".auto-window").value.trim();r.textContent="Saving...",r.disabled=!0;try{const _=await(await secureFetch(`/api/v1/updates/auto-update/${encodeURIComponent(d)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({enabled:!!T,schedule:T||"weekly",autoRollback:U,maintenanceWindow:q||void 0})})).json();if(_.success)r.textContent="\u2713 Saved";else throw new Error(_.error)}catch(F){r.textContent="\u2717 Error",showNotification("Save error: "+F.message,"error")}setTimeout(()=>{r.textContent="Save",r.disabled=!1},2e3)})})}catch(c){H.innerHTML=`
Failed: ${escapeHtml(c.message)}
`}}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+='';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+='',a+='",a+='",a+='",a+='",a+="",o.error&&(a+='"),o.note&&(a+='")}a+="
WhenVersionFromStatus
'+timeAgo(o.timestamp)+"v'+escapeHtml(o.version)+(o.rollback?" (rollback)":"")+"v'+escapeHtml(o.fromVersion||"?")+"'+i+"
'+escapeHtml(o.error)+"
'+escapeHtml(o.note)+"
",y.innerHTML=a}catch(c){y.innerHTML='
Failed: '+escapeHtml(c.message)+"
"}}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

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

\u{1F4E6} Import Docker Compose

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

Terminal

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

\u{1F4DC} Audit Log

- `);const k=document.getElementById("audit-modal"),B=document.getElementById("audit-log-btn"),A=document.getElementById("audit-cancel"),I=document.getElementById("audit-refresh-btn"),z=document.getElementById("audit-clear-btn"),P=document.getElementById("audit-filter"),M=document.getElementById("audit-outcome-filter"),D=document.getElementById("audit-since"),b=document.getElementById("audit-until"),T=document.getElementById("audit-log-container"),h=document.getElementById("audit-load-more");let H=0,S=null,$=0;const f=50;function E(N){if(!N)return null;const R=new Date(N);return isNaN(R.getTime())?null:R.toISOString()}async function O(N){try{N?(S&&S.abort(),S=new AbortController):(S&&S.abort(),S=new AbortController,H=0,$++,T.innerHTML='
Loading...
');const R=$,u=new URLSearchParams;u.set("limit",String(f)),u.set("offset",String(H));const v=P.value,w=M.value,g=E(D.value),x=E(b.value);v&&u.set("action",v),w&&u.set("outcome",w),g&&u.set("since",g),x&&u.set("until",x);const s=await fetch("/api/v1/audit-logs?"+u.toString(),{signal:S.signal});if(!s.ok){T.innerHTML=`
Failed: HTTP ${s.status}
`,h.style.display="none";return}const p=await s.json();if(!p.success){T.innerHTML=`
Failed: ${escapeHtml(p.error||"unknown")}
`,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+='',m+='',m+='',m+='',m+='',m+='',m+="");for(const c of l){const t=c.outcome==="success",e=L(c);m+='',m+=``,m+=``,m+=``,m+=``,m+=``,m+=``,m+="",c.details&&Object.keys(c.details).length>0&&(m+=``)}if(!N)m+="
WhenActorIPActionResourceResult
${timeAgo(c.timestamp)}${e}${escapeHtml(c.ip||"-")}${escapeHtml(c.action||"-")}${escapeHtml(c.resource||"-")}${t?"\u2713":"\u2717"} ${escapeHtml(c.outcome||"")}
",T.innerHTML=m;else{const c=T.querySelector("table");c&&c.insertAdjacentHTML("beforeend",m)}H+=l.length,h.style.display=p.hasMore?"":"none",T.querySelectorAll(".audit-row").forEach(c=>{c.dataset.wired||(c.dataset.wired="true",c.addEventListener("click",()=>{const t=c.nextElementSibling;t&&t.classList.contains("audit-detail")&&(t.style.display=t.style.display==="none"?"":"none")}))})}catch(R){if(R&&R.name==="AbortError")return;T.innerHTML=`
Failed: ${escapeHtml(R.message)}
`}}function L(N){const R=N.details||{},u=R.userEmail,v=R.userId,w=R.userRole,g=R.viaProvider;if(u){const x=w?` [${escapeHtml(w)}${g?"/"+escapeHtml(g):""}]`:"";return`${escapeHtml(u)}${x}`}return v?`${escapeHtml(v)}`:N.ip?'anon':'system'}B?.addEventListener("click",()=>{k?.classList.add("show"),O(!1)}),wireModal(k,A),I?.addEventListener("click",()=>O(!1)),P?.addEventListener("change",()=>O(!1)),M?.addEventListener("change",()=>O(!1));let j;[D,b].forEach(N=>{N?.addEventListener("change",()=>{clearTimeout(j),j=setTimeout(()=>O(!1),250)})}),h?.addEventListener("click",()=>O(!0)),z?.addEventListener("click",async()=>{if(confirm("Clear the entire audit log? This cannot be undone."))try{const R=await(await secureFetch("/api/v1/audit-logs",{method:"DELETE",headers:{"content-type":"application/json"},body:JSON.stringify({confirm:"CLEAR"})})).json();R.success?O(!1):showNotification("Error: "+(R.error||"Clear failed"),"error")}catch(N){showNotification("Error: "+N.message,"error")}})})(),(function(){injectModal("security-modal",`
+
`);const b=document.getElementById("audit-modal"),C=document.getElementById("audit-log-btn"),j=document.getElementById("audit-cancel"),k=document.getElementById("audit-refresh-btn"),z=document.getElementById("audit-clear-btn"),P=document.getElementById("audit-filter"),H=document.getElementById("audit-outcome-filter"),A=document.getElementById("audit-since"),x=document.getElementById("audit-until"),B=document.getElementById("audit-log-container"),w=document.getElementById("audit-load-more");let M=0,L=null,$=0;const u=50;function v(N){if(!N)return null;const O=new Date(N);return isNaN(O.getTime())?null:O.toISOString()}async function D(N){try{N?(L&&L.abort(),L=new AbortController):(L&&L.abort(),L=new AbortController,M=0,$++,B.innerHTML='
Loading...
');const O=$,m=new URLSearchParams;m.set("limit",String(u)),m.set("offset",String(M));const y=P.value,S=H.value,f=v(A.value),E=v(x.value);y&&m.set("action",y),S&&m.set("outcome",S),f&&m.set("since",f),E&&m.set("until",E);const s=await fetch("/api/v1/audit-logs?"+m.toString(),{signal:L.signal});if(!s.ok){B.innerHTML=`
Failed: HTTP ${s.status}
`,w.style.display="none";return}const p=await s.json();if(!p.success){B.innerHTML=`
Failed: ${escapeHtml(p.error||"unknown")}
`,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+='',g+='',g+='',g+='',g+='',g+='',g+="");for(const c of l){const t=c.outcome==="success",e=I(c);g+='',g+=``,g+=``,g+=``,g+=``,g+=``,g+=``,g+="",c.details&&Object.keys(c.details).length>0&&(g+=``)}if(!N)g+="
WhenActorIPActionResourceResult
${timeAgo(c.timestamp)}${e}${escapeHtml(c.ip||"-")}${escapeHtml(c.action||"-")}${escapeHtml(c.resource||"-")}${t?"\u2713":"\u2717"} ${escapeHtml(c.outcome||"")}
",B.innerHTML=g;else{const c=B.querySelector("table");c&&c.insertAdjacentHTML("beforeend",g)}M+=l.length,w.style.display=p.hasMore?"":"none",B.querySelectorAll(".audit-row").forEach(c=>{c.dataset.wired||(c.dataset.wired="true",c.addEventListener("click",()=>{const t=c.nextElementSibling;t&&t.classList.contains("audit-detail")&&(t.style.display=t.style.display==="none"?"":"none")}))})}catch(O){if(O&&O.name==="AbortError")return;B.innerHTML=`
Failed: ${escapeHtml(O.message)}
`}}function I(N){const O=N.details||{},m=O.userEmail,y=O.userId,S=O.userRole,f=O.viaProvider;if(m){const E=S?` [${escapeHtml(S)}${f?"/"+escapeHtml(f):""}]`:"";return`${escapeHtml(m)}${E}`}return y?`${escapeHtml(y)}`:N.ip?'anon':'system'}C?.addEventListener("click",()=>{b?.classList.add("show"),D(!1)}),wireModal(b,j),k?.addEventListener("click",()=>D(!1)),P?.addEventListener("change",()=>D(!1)),H?.addEventListener("change",()=>D(!1));let R;[A,x].forEach(N=>{N?.addEventListener("change",()=>{clearTimeout(R),R=setTimeout(()=>D(!1),250)})}),w?.addEventListener("click",()=>D(!0)),z?.addEventListener("click",async()=>{if(confirm("Clear the entire audit log? This cannot be undone."))try{const O=await(await secureFetch("/api/v1/audit-logs",{method:"DELETE",headers:{"content-type":"application/json"},body:JSON.stringify({confirm:"CLEAR"})})).json();O.success?D(!1):showNotification("Error: "+(O.error||"Clear failed"),"error")}catch(N){showNotification("Error: "+N.message,"error")}})})(),(function(){injectModal("security-modal",`

\u{1F6E1}\uFE0F Security Center

- `);const k=document.getElementById("security-modal"),B=document.getElementById("security-center-btn"),A=document.getElementById("sec-cancel"),I=k.querySelectorAll(".sec-tab"),z=k.querySelectorAll(".sec-panel");let P=[],M=[],D=null;I.forEach(s=>{s.addEventListener("click",()=>{I.forEach(p=>p.classList.toggle("active",p===s)),z.forEach(p=>p.style.display=p.dataset.panel===s.dataset.tab?"":"none"),s.dataset.tab==="overview"&&H(),s.dataset.tab==="events"&&j(),s.dataset.tab==="hosts"&&v()})}),B&&B.addEventListener("click",()=>{k.classList.add("show"),H(),T()}),A.addEventListener("click",b),k.addEventListener("click",s=>{s.target===k&&b()});function b(){k.classList.remove("show"),h()}function T(){if(h(),!!document.getElementById("sec-live-tail").checked&&!(typeof EventSource>"u"))try{D=new EventSource("/api/v1/security/events/stream"),D.addEventListener("init",s=>{try{P=JSON.parse(s.data).events||[],N()}catch{}}),D.addEventListener("security",s=>{try{const p=JSON.parse(s.data);P.unshift(p),P.length>500&&(P.length=500);const l=k.querySelector(".sec-tab.active")?.dataset?.tab;l==="events"?N():l==="overview"&&H()}catch{}}),D.onerror=()=>{}}catch(s){console.warn("[security] SSE failed:",s.message)}}function h(){if(D){try{D.close()}catch{}D=null}}document.getElementById("sec-live-tail").addEventListener("change",()=>{k.classList.contains("show")&&T()});async function H(){try{const s=new Date(Date.now()-864e5).toISOString(),[p,l,m]=await Promise.all([fetch(`/api/v1/security/events/stats?since=${encodeURIComponent(s)}`),fetch("/api/v1/security/hosts"),fetch(`/api/v1/security/events?limit=1&since=${encodeURIComponent(s)}`)]),c=(await p.json()).data||{},t=(await l.json()).data?.hosts||[],e=(await m.json()).data?.total||0;document.querySelector('#sec-stats [data-key="total"]').textContent=`${e} events (24h)`,document.querySelector('#sec-stats [data-key="warn"]').textContent=`${c.by_severity?.warn||0} warnings`,document.querySelector('#sec-stats [data-key="error"]').textContent=`${c.by_severity?.error||0} errors`,document.querySelector('#sec-stats [data-key="denied"]').textContent=`${c.by_outcome?.denied||0} denied`,document.querySelector('#sec-stats [data-key="hosts"]').textContent=`${t.length} hosts`,S("sec-top-actors",c.top_actors||[]),S("sec-top-targets",c.top_targets||[])}catch(s){console.warn("[security] refreshOverview failed:",s.message)}}function S(s,p){const l=document.getElementById(s);if(!p.length){l.innerHTML='
No data
';return}l.innerHTML=''+p.map(m=>``).join("")+"
${g(String(m.key))}${m.count}
"}const $=document.getElementById("sec-filter-source"),f=document.getElementById("sec-filter-severity"),E=document.getElementById("sec-filter-host"),O=document.getElementById("sec-filter-actor"),L=document.getElementById("sec-refresh-btn");[$,f,E].forEach(s=>s.addEventListener("change",j)),O.addEventListener("input",x(j,250)),L.addEventListener("click",j);async function j(){try{const s=new URLSearchParams;s.set("limit","200"),$.value&&s.set("source_type",$.value),f.value&&s.set("severity",f.value),E.value&&s.set("source_host",E.value),O.value&&s.set("actor_prefix",O.value),P=(await(await fetch(`/api/v1/security/events?${s}`)).json()).data.events||[],N(),(!E.options.length||E.options.length===1)&&await u()}catch(s){document.getElementById("sec-events-container").innerHTML='
Load failed: '+g(s.message)+"
"}}function N(){const s=document.getElementById("sec-events-container");if(!P.length){s.innerHTML='
No events
';return}s.innerHTML=P.slice(0,200).map(R).join("")}function R(s){const p=s.severity||"info",l={critical:"#c0392b",error:"#e74c3c",warn:"#f39c12",notice:"#3498db",info:"#7f8c8d"}[p]||"#7f8c8d",m=s.ts?new Date(s.ts).toLocaleTimeString():"",c=s.source_type||"",t=s.actor||"\u2014",e=s.target||"",a=s.action||"",o=s.outcome||"";return`
- ${g(p)} - ${g(c)} - ${g(t)} - ${g(a)} ${g(e)} - ${g(o)} - ${g(m)} -
`}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.
';return}l.innerHTML=p.map(m=>{const c=m.enabled?m.last_seen_at?Date.now()-Date.parse(m.last_seen_at)>18e5?"\u{1F7E1} stale":"\u{1F7E2} online":"\u26AA registered":"\u{1F534} disabled";return`
+
`);const b=document.getElementById("security-modal"),C=document.getElementById("security-center-btn"),j=document.getElementById("sec-cancel"),k=b.querySelectorAll(".sec-tab"),z=b.querySelectorAll(".sec-panel");let P=[],H=[],A=null;k.forEach(s=>{s.addEventListener("click",()=>{k.forEach(p=>p.classList.toggle("active",p===s)),z.forEach(p=>p.style.display=p.dataset.panel===s.dataset.tab?"":"none"),s.dataset.tab==="overview"&&M(),s.dataset.tab==="events"&&R(),s.dataset.tab==="hosts"&&y()})}),C&&C.addEventListener("click",()=>{b.classList.add("show"),M(),B()}),j.addEventListener("click",x),b.addEventListener("click",s=>{s.target===b&&x()});function x(){b.classList.remove("show"),w()}function B(){if(w(),!!document.getElementById("sec-live-tail").checked&&!(typeof EventSource>"u"))try{A=new EventSource("/api/v1/security/events/stream"),A.addEventListener("init",s=>{try{P=JSON.parse(s.data).events||[],N()}catch{}}),A.addEventListener("security",s=>{try{const p=JSON.parse(s.data);P.unshift(p),P.length>500&&(P.length=500);const l=b.querySelector(".sec-tab.active")?.dataset?.tab;l==="events"?N():l==="overview"&&M()}catch{}}),A.onerror=()=>{}}catch(s){console.warn("[security] SSE failed:",s.message)}}function w(){if(A){try{A.close()}catch{}A=null}}document.getElementById("sec-live-tail").addEventListener("change",()=>{b.classList.contains("show")&&B()});async function M(){try{const s=new Date(Date.now()-864e5).toISOString(),[p,l,g]=await Promise.all([fetch(`/api/v1/security/events/stats?since=${encodeURIComponent(s)}`),fetch("/api/v1/security/hosts"),fetch(`/api/v1/security/events?limit=1&since=${encodeURIComponent(s)}`)]),c=(await p.json()).data||{},t=(await l.json()).data?.hosts||[],e=(await g.json()).data?.total||0;document.querySelector('#sec-stats [data-key="total"]').textContent=`${e} events (24h)`,document.querySelector('#sec-stats [data-key="warn"]').textContent=`${c.by_severity?.warn||0} warnings`,document.querySelector('#sec-stats [data-key="error"]').textContent=`${c.by_severity?.error||0} errors`,document.querySelector('#sec-stats [data-key="denied"]').textContent=`${c.by_outcome?.denied||0} denied`,document.querySelector('#sec-stats [data-key="hosts"]').textContent=`${t.length} hosts`,L("sec-top-actors",c.top_actors||[]),L("sec-top-targets",c.top_targets||[])}catch(s){console.warn("[security] refreshOverview failed:",s.message)}}function L(s,p){const l=document.getElementById(s);if(!p.length){l.innerHTML='
No data
';return}l.innerHTML=''+p.map(g=>``).join("")+"
${f(String(g.key))}${g.count}
"}const $=document.getElementById("sec-filter-source"),u=document.getElementById("sec-filter-severity"),v=document.getElementById("sec-filter-host"),D=document.getElementById("sec-filter-actor"),I=document.getElementById("sec-refresh-btn");[$,u,v].forEach(s=>s.addEventListener("change",R)),D.addEventListener("input",E(R,250)),I.addEventListener("click",R);async function R(){try{const s=new URLSearchParams;s.set("limit","200"),$.value&&s.set("source_type",$.value),u.value&&s.set("severity",u.value),v.value&&s.set("source_host",v.value),D.value&&s.set("actor_prefix",D.value),P=(await(await fetch(`/api/v1/security/events?${s}`)).json()).data.events||[],N(),(!v.options.length||v.options.length===1)&&await m()}catch(s){document.getElementById("sec-events-container").innerHTML='
Load failed: '+f(s.message)+"
"}}function N(){const s=document.getElementById("sec-events-container");if(!P.length){s.innerHTML='
No events
';return}s.innerHTML=P.slice(0,200).map(O).join("")}function O(s){const p=s.severity||"info",l={critical:"#c0392b",error:"#e74c3c",warn:"#f39c12",notice:"#3498db",info:"#7f8c8d"}[p]||"#7f8c8d",g=s.ts?new Date(s.ts).toLocaleTimeString():"",c=s.source_type||"",t=s.actor||"\u2014",e=s.target||"",a=s.action||"",o=s.outcome||"";return`
+ ${f(p)} + ${f(c)} + ${f(t)} + ${f(a)} ${f(e)} + ${f(o)} + ${f(g)} +
`}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.
';return}l.innerHTML=p.map(g=>{const c=g.enabled?g.last_seen_at?Date.now()-Date.parse(g.last_seen_at)>18e5?"\u{1F7E1} stale":"\u{1F7E2} online":"\u26AA registered":"\u{1F534} disabled";return`
- ${g(m.label||m.id)} - ${g(m.type)} + ${f(g.label||g.id)} + ${f(g.type)}
- id: ${g(m.id)} \xB7 - registered ${new Date(m.registered_at).toLocaleDateString()} \xB7 - last seen ${m.last_seen_at?new Date(m.last_seen_at).toLocaleString():"never"} + id: ${f(g.id)} \xB7 + registered ${new Date(g.registered_at).toLocaleDateString()} \xB7 + last seen ${g.last_seen_at?new Date(g.last_seen_at).toLocaleString():"never"}
${c} - ${m.id==="self"?"":``} + ${g.id==="self"?"":``}
-
`}).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

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