From 1bee77d5fbba4f98e9e5bf198a8e89d202cc26d4 Mon Sep 17 00:00:00 2001 From: Hermes Date: Wed, 12 Aug 2026 18:53:59 -0700 Subject: [PATCH] Add i18n language selector to dashboard frontend - New i18n.js module: window.DCI18n.t(key), language dropdown selector - Supports 5 languages (en, es, fr, de, ar) with RTL for Arabic - Translations loaded from /api/v1/i18n/translations/:lang - Language preference persisted in localStorage - Added to features.js bundle --- status/build.js | 1 + status/dist/features.js | 374 ++++++++++++++++++++-------------------- status/js/i18n.js | 207 ++++++++++++++++++++++ status/sw.js | 2 +- 4 files changed, 396 insertions(+), 188 deletions(-) create mode 100644 status/js/i18n.js diff --git a/status/build.js b/status/build.js index 352a9da..bc79bed 100644 --- a/status/build.js +++ b/status/build.js @@ -77,6 +77,7 @@ const bundles = { // window.wireModal + window.injectModal + window.escapeHtml helpers // defined in globals.js (already in core.js). JS('share-modal.js'), + JS('i18n.js'), ], 'onboarding.js': [ JS('driver.min.js'), diff --git a/status/dist/features.js b/status/dist/features.js index 545556b..bc1f2d1 100644 --- a/status/dist/features.js +++ b/status/dist/features.js @@ -90,44 +90,44 @@ - `);const m=document.getElementById("logo-modal"),E=document.getElementById("logo-preview-dark"),O=document.getElementById("logo-preview-light"),k=document.getElementById("logo-status"),N=document.getElementById("logo-same-both"),R=document.getElementById("logo-dual-uploads"),z=document.getElementById("logo-single-upload"),A=document.getElementById("logo-upload-dark"),p=document.getElementById("logo-upload-light"),B=document.getElementById("logo-upload-single"),h=document.querySelector("#brand .brand-logo-dark"),H=document.querySelector("#brand .brand-logo-light"),w=document.querySelector(".top-row"),$=document.getElementById("dashboard-title"),S=DC.NAME;let L=null,U=null,I=null,D="left",y=S;N?.addEventListener("change",()=>{N.checked?(R.style.display="none",z.style.display="",L=null,U=null):(R.style.display="flex",z.style.display="none",I=null)});function P(n,e){if(!n||!n.type.startsWith("image/")){showNotification("Please select an image file","warning");return}const t=new FileReader;t.onload=o=>e(o.target.result),t.readAsDataURL(n)}A?.addEventListener("change",n=>{P(n.target.files[0],e=>{L=e,E.src=e,k.textContent="New dark logo ready to save"})}),p?.addEventListener("change",n=>{P(n.target.files[0],e=>{U=e,O.src=e,k.textContent="New light logo ready to save"})}),B?.addEventListener("change",n=>{P(n.target.files[0],e=>{I=e,E.src=e,O.src=e,k.textContent="New logo ready to save (both themes)"})});function T(n){w.setAttribute("data-logo-pos",n),document.querySelectorAll(".logo-pos-btn").forEach(e=>{e.style.background=e.dataset.pos===n?"var(--accent)":"var(--card-bg)",e.style.color=e.dataset.pos===n?"white":"var(--fg)"})}function g(n){y=n||S,document.title=y;const e=document.querySelector(".dashboard-title");e&&(e.textContent=y)}async function C(){try{const n=await fetch("/api/v1/logo");if(n.ok){const e=await n.json();e.customLogoDark&&(h.src=e.customLogoDark,E.src=e.customLogoDark),e.customLogoLight&&(H.src=e.customLogoLight,O.src=e.customLogoLight),!e.customLogoDark&&!e.customLogoLight&&e.customLogo&&(h.src=e.customLogo,H.src=e.customLogo,E.src=e.customLogo,O.src=e.customLogo),e.isDefault||(k.textContent="Using custom logo"),e.position&&(D=e.position,T(e.position)),e.dashboardTitle&&g(e.dashboardTitle)}}catch(n){console.warn("Could not load custom logo:",n.message)}}document.querySelectorAll(".logo-pos-btn").forEach(n=>{n.addEventListener("click",()=>{D=n.dataset.pos,T(D)})}),document.getElementById("brand")?.addEventListener("click",()=>{L=null,U=null,I=null,A&&(A.value=""),p&&(p.value=""),B&&(B.value=""),N&&(N.checked=!1),R.style.display="flex",z.style.display="none",E.src=h.src,O.src=H.src;const n=h.src.includes("custom-logo")||H.src.includes("custom-logo");k.textContent=n?"Using custom logo":"Using default logos",T(D),$.value=y,m.classList.add("show")}),document.getElementById("logo-save")?.addEventListener("click",async()=>{try{const n=$.value.trim()||S,e={position:D,dashboardTitle:n};N?.checked&&I?(e.dataDark=I,e.dataLight=I):(L&&(e.dataDark=L),U&&(e.dataLight=U));const t=await secureFetch("/api/v1/logo",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});if(t.ok){const o=await t.json(),r="?t="+Date.now();o.pathDark&&(h.src=o.pathDark+r,E.src=o.pathDark+r),o.pathLight&&(H.src=o.pathLight+r,O.src=o.pathLight+r),T(D),g(n),m.classList.remove("show")}else{const o=await t.json();showNotification("Failed to save: "+o.error,"error")}}catch(n){showNotification("Error saving: "+n.message,"error")}}),document.getElementById("logo-reset")?.addEventListener("click",async()=>{if(confirm(`Reset all branding to DashCaddy defaults? + `);const y=document.getElementById("logo-modal"),C=document.getElementById("logo-preview-dark"),O=document.getElementById("logo-preview-light"),E=document.getElementById("logo-status"),M=document.getElementById("logo-same-both"),R=document.getElementById("logo-dual-uploads"),P=document.getElementById("logo-single-upload"),A=document.getElementById("logo-upload-dark"),p=document.getElementById("logo-upload-light"),T=document.getElementById("logo-upload-single"),x=document.querySelector("#brand .brand-logo-dark"),$=document.querySelector("#brand .brand-logo-light"),k=document.querySelector(".top-row"),H=document.getElementById("dashboard-title"),L=DC.NAME;let u=null,j=null,v=null,N="left",g=L;M?.addEventListener("change",()=>{M.checked?(R.style.display="none",P.style.display="",u=null,j=null):(R.style.display="flex",P.style.display="none",v=null)});function D(n,e){if(!n||!n.type.startsWith("image/")){showNotification("Please select an image file","warning");return}const t=new FileReader;t.onload=o=>e(o.target.result),t.readAsDataURL(n)}A?.addEventListener("change",n=>{D(n.target.files[0],e=>{u=e,C.src=e,E.textContent="New dark logo ready to save"})}),p?.addEventListener("change",n=>{D(n.target.files[0],e=>{j=e,O.src=e,E.textContent="New light logo ready to save"})}),T?.addEventListener("change",n=>{D(n.target.files[0],e=>{v=e,C.src=e,O.src=e,E.textContent="New logo ready to save (both themes)"})});function B(n){k.setAttribute("data-logo-pos",n),document.querySelectorAll(".logo-pos-btn").forEach(e=>{e.style.background=e.dataset.pos===n?"var(--accent)":"var(--card-bg)",e.style.color=e.dataset.pos===n?"white":"var(--fg)"})}function f(n){g=n||L,document.title=g;const e=document.querySelector(".dashboard-title");e&&(e.textContent=g)}async function I(){try{const n=await fetch("/api/v1/logo");if(n.ok){const e=await n.json();e.customLogoDark&&(x.src=e.customLogoDark,C.src=e.customLogoDark),e.customLogoLight&&($.src=e.customLogoLight,O.src=e.customLogoLight),!e.customLogoDark&&!e.customLogoLight&&e.customLogo&&(x.src=e.customLogo,$.src=e.customLogo,C.src=e.customLogo,O.src=e.customLogo),e.isDefault||(E.textContent="Using custom logo"),e.position&&(N=e.position,B(e.position)),e.dashboardTitle&&f(e.dashboardTitle)}}catch(n){console.warn("Could not load custom logo:",n.message)}}document.querySelectorAll(".logo-pos-btn").forEach(n=>{n.addEventListener("click",()=>{N=n.dataset.pos,B(N)})}),document.getElementById("brand")?.addEventListener("click",()=>{u=null,j=null,v=null,A&&(A.value=""),p&&(p.value=""),T&&(T.value=""),M&&(M.checked=!1),R.style.display="flex",P.style.display="none",C.src=x.src,O.src=$.src;const n=x.src.includes("custom-logo")||$.src.includes("custom-logo");E.textContent=n?"Using custom logo":"Using default logos",B(N),H.value=g,y.classList.add("show")}),document.getElementById("logo-save")?.addEventListener("click",async()=>{try{const n=H.value.trim()||L,e={position:N,dashboardTitle:n};M?.checked&&v?(e.dataDark=v,e.dataLight=v):(u&&(e.dataDark=u),j&&(e.dataLight=j));const t=await secureFetch("/api/v1/logo",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});if(t.ok){const o=await t.json(),r="?t="+Date.now();o.pathDark&&(x.src=o.pathDark+r,C.src=o.pathDark+r),o.pathLight&&($.src=o.pathLight+r,O.src=o.pathLight+r),B(N),f(n),y.classList.remove("show")}else{const o=await t.json();showNotification("Failed to save: "+o.error,"error")}}catch(n){showNotification("Error saving: "+n.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",E.src="/assets/dashcaddy-logo-dark.png",O.src="/assets/dashcaddy-logo-light.png",k.textContent="Using default logos",L=null,U=null,I=null,$.value=S,g(S),D="left",T("left")),(await secureFetch("/api/v1/favicon",{method:"DELETE"})).ok){const t=document.querySelector('link[rel="icon"]'),o=document.getElementById("favicon-preview"),r=document.getElementById("favicon-status");t&&(t.href="/assets/dashcaddy-favicon.ico?t="+Date.now()),o&&(o.src="/assets/dashcaddy-favicon.ico?t="+Date.now()),r&&(r.textContent="Using DashCaddy favicon"),l=null}}catch(n){showNotification("Error resetting branding: "+n.message,"error")}}),wireModal(m,document.getElementById("logo-cancel"));const f=document.getElementById("favicon-preview"),M=document.getElementById("favicon-status"),i=document.getElementById("favicon-upload"),c=document.querySelector('link[rel="icon"]')||document.createElement("link");let l=null;document.querySelector('link[rel="icon"]')||(c.rel="icon",c.href="/assets/dashcaddy-favicon.ico",document.head.appendChild(c));async function b(){try{const n=await fetch("/api/v1/favicon");if(n.ok){const e=await n.json();e.customFavicon&&(c.href=e.customFavicon+"?t="+Date.now(),f.src=e.customFavicon+"?t="+Date.now(),M.textContent="Using custom favicon")}}catch(n){console.warn("Could not load custom favicon:",n.message)}}i?.addEventListener("change",n=>{const e=n.target.files[0];if(!e)return;if(!e.type.match(/^image\/(png|svg\+xml)$/)){showNotification("Please select a PNG or SVG file","warning"),i.value="";return}const t=new FileReader;t.onload=o=>{l=o.target.result,f.src=l,M.textContent="New favicon ready to save"},t.readAsDataURL(e)}),document.getElementById("logo-save")?.addEventListener("click",async()=>{if(l)try{const n=await secureFetch("/api/v1/favicon",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({data:l})});if(n.ok){const e=await n.json();c.href=e.path+"?t="+Date.now(),f.src=e.path+"?t="+Date.now(),M.textContent="Using custom favicon",l=null}else{const e=await n.json();showNotification("Failed to save favicon: "+e.error,"error")}}catch(n){showNotification("Error saving favicon: "+n.message,"error")}}),b(),C();const u=document.getElementById("settings-timezone");u&&(new MutationObserver(()=>{m.classList.contains("show")&&u.options.length===0&&(async()=>{let e;try{const t=await fetch("/api/v1/config");t.ok&&(e=(await t.json()).timezone)}catch{}window.populateTimezoneSelect(u,e)})()}).observe(m,{attributes:!0,attributeFilter:["class"]}),document.getElementById("logo-save")?.addEventListener("click",async()=>{const e=u.value;if(e)try{const t=await fetch("/api/v1/config");if(!t.ok)return;const o=await t.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(t){console.warn("Failed to save timezone:",t.message)}}))})(),window.populateTimezoneSelect=function(m,E){const O=Intl.supportedValuesOf("timeZone"),k=E||Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC";m.innerHTML="";for(const N of O){const R=document.createElement("option");R.value=N,R.textContent=N.replace(/_/g," "),N===k&&(R.selected=!0),m.appendChild(R)}},(function(){let m="homelab",E=null;async function O(){try{const P=await fetch("/api/v1/config");if(P.ok&&(E=await P.json(),E&&E.setupComplete))return document.getElementById("setup-wizard").style.display="none",!0}catch(P){console.warn("Could not fetch server config, checking localStorage fallback:",P.message)}return safeGet("dashcaddy-setup")?(document.getElementById("setup-wizard").style.display="none",!0):(document.getElementById("setup-wizard").style.display="flex",!1)}O();const k=document.getElementById("setup-timezone");k&&window.populateTimezoneSelect(k);function N(y){document.querySelectorAll(".setup-step").forEach(T=>{T.style.display="none"});const P=document.getElementById(y);P&&(P.style.display="block")}function R(){const y=document.getElementById("setup-summary-content");if(!y)return;let P='
';if(m==="homelab"){const g=document.getElementById("setup-tld")?.value?.trim()||".home",C=document.getElementById("setup-ca-name")?.value?.trim()||"",f=document.getElementById("setup-dns-ip")?.value?.trim()||"",M=document.getElementById("setup-dns-port")?.value?.trim()||DC.DEFAULTS.DNS_PORT;P+=` +This will reset the logo, favicon, title, and position.`))try{if((await secureFetch("/api/v1/logo",{method:"DELETE"})).ok&&(x.src="/assets/dashcaddy-logo-dark.png",$.src="/assets/dashcaddy-logo-light.png",C.src="/assets/dashcaddy-logo-dark.png",O.src="/assets/dashcaddy-logo-light.png",E.textContent="Using default logos",u=null,j=null,v=null,H.value=L,f(L),N="left",B("left")),(await secureFetch("/api/v1/favicon",{method:"DELETE"})).ok){const t=document.querySelector('link[rel="icon"]'),o=document.getElementById("favicon-preview"),r=document.getElementById("favicon-status");t&&(t.href="/assets/dashcaddy-favicon.ico?t="+Date.now()),o&&(o.src="/assets/dashcaddy-favicon.ico?t="+Date.now()),r&&(r.textContent="Using DashCaddy favicon"),l=null}}catch(n){showNotification("Error resetting branding: "+n.message,"error")}}),wireModal(y,document.getElementById("logo-cancel"));const b=document.getElementById("favicon-preview"),z=document.getElementById("favicon-status"),i=document.getElementById("favicon-upload"),c=document.querySelector('link[rel="icon"]')||document.createElement("link");let l=null;document.querySelector('link[rel="icon"]')||(c.rel="icon",c.href="/assets/dashcaddy-favicon.ico",document.head.appendChild(c));async function w(){try{const n=await fetch("/api/v1/favicon");if(n.ok){const e=await n.json();e.customFavicon&&(c.href=e.customFavicon+"?t="+Date.now(),b.src=e.customFavicon+"?t="+Date.now(),z.textContent="Using custom favicon")}}catch(n){console.warn("Could not load custom favicon:",n.message)}}i?.addEventListener("change",n=>{const e=n.target.files[0];if(!e)return;if(!e.type.match(/^image\/(png|svg\+xml)$/)){showNotification("Please select a PNG or SVG file","warning"),i.value="";return}const t=new FileReader;t.onload=o=>{l=o.target.result,b.src=l,z.textContent="New favicon ready to save"},t.readAsDataURL(e)}),document.getElementById("logo-save")?.addEventListener("click",async()=>{if(l)try{const n=await secureFetch("/api/v1/favicon",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({data:l})});if(n.ok){const e=await n.json();c.href=e.path+"?t="+Date.now(),b.src=e.path+"?t="+Date.now(),z.textContent="Using custom favicon",l=null}else{const e=await n.json();showNotification("Failed to save favicon: "+e.error,"error")}}catch(n){showNotification("Error saving favicon: "+n.message,"error")}}),w(),I();const m=document.getElementById("settings-timezone");m&&(new MutationObserver(()=>{y.classList.contains("show")&&m.options.length===0&&(async()=>{let e;try{const t=await fetch("/api/v1/config");t.ok&&(e=(await t.json()).timezone)}catch{}window.populateTimezoneSelect(m,e)})()}).observe(y,{attributes:!0,attributeFilter:["class"]}),document.getElementById("logo-save")?.addEventListener("click",async()=>{const e=m.value;if(e)try{const t=await fetch("/api/v1/config");if(!t.ok)return;const o=await t.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(t){console.warn("Failed to save timezone:",t.message)}}))})(),window.populateTimezoneSelect=function(y,C){const O=Intl.supportedValuesOf("timeZone"),E=C||Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC";y.innerHTML="";for(const M of O){const R=document.createElement("option");R.value=M,R.textContent=M.replace(/_/g," "),M===E&&(R.selected=!0),y.appendChild(R)}},(function(){let y="homelab",C=null;async function O(){try{const D=await fetch("/api/v1/config");if(D.ok&&(C=await D.json(),C&&C.setupComplete))return document.getElementById("setup-wizard").style.display="none",!0}catch(D){console.warn("Could not fetch server config, checking localStorage fallback:",D.message)}return safeGet("dashcaddy-setup")?(document.getElementById("setup-wizard").style.display="none",!0):(document.getElementById("setup-wizard").style.display="flex",!1)}O();const E=document.getElementById("setup-timezone");E&&window.populateTimezoneSelect(E);function M(g){document.querySelectorAll(".setup-step").forEach(B=>{B.style.display="none"});const D=document.getElementById(g);D&&(D.style.display="block")}function R(){const g=document.getElementById("setup-summary-content");if(!g)return;let D='
';if(y==="homelab"){const f=document.getElementById("setup-tld")?.value?.trim()||".home",I=document.getElementById("setup-ca-name")?.value?.trim()||"",b=document.getElementById("setup-dns-ip")?.value?.trim()||"",z=document.getElementById("setup-dns-port")?.value?.trim()||DC.DEFAULTS.DNS_PORT;D+=`

Home Lab Configuration

-
TLD: ${g}
-
Certificate Authority: ${C}
-
DNS Server: ${f}:${M}
-
Example URLs: https://uptime${g}, https://nextcloud${g}
+
TLD: ${f}
+
Certificate Authority: ${I}
+
DNS Server: ${b}:${z}
+
Example URLs: https://uptime${f}, https://nextcloud${f}
- `}else if(m==="simple"){const g=document.getElementById("setup-simple-ip")?.value?.trim()||"localhost";P+=` + `}else if(y==="simple"){const f=document.getElementById("setup-simple-ip")?.value?.trim()||"localhost";D+=`

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(m==="public"){const g=document.getElementById("setup-public-domain")?.value?.trim()||"",C=document.getElementById("setup-public-email")?.value?.trim()||"",f=document.querySelector('input[name="routing-mode"]:checked')?.value||"subdirectory",M=f==="subdirectory"?`https://${g}/sonarr, https://${g}/grafana`:`https://sonarr.${g}, https://grafana.${g}`;P+=` + `}else if(y==="public"){const f=document.getElementById("setup-public-domain")?.value?.trim()||"",I=document.getElementById("setup-public-email")?.value?.trim()||"",b=document.querySelector('input[name="routing-mode"]:checked')?.value||"subdirectory",z=b==="subdirectory"?`https://${f}/sonarr, https://${f}/grafana`:`https://sonarr.${f}, https://grafana.${f}`;D+=`

Public Server

-
Domain: ${g}
+
Domain: ${f}
SSL: Let's Encrypt
-
Email: ${C}
-
Routing: ${f==="subdirectory"?"Subdirectory (domain.com/app)":"Subdomain (app.domain.com)"}
-
Example URLs: ${M}
+
Email: ${I}
+
Routing: ${b==="subdirectory"?"Subdirectory (domain.com/app)":"Subdomain (app.domain.com)"}
+
Example URLs: ${z}
- `}const T=document.getElementById("setup-timezone")?.value||Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC";P+=` + `}const B=document.getElementById("setup-timezone")?.value||Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC";D+=`
-
Timezone: ${T.replace(/_/g," ")}
+
Timezone: ${B.replace(/_/g," ")}
- `,P+=` + `,D+=`
\u{1F4BE} @@ -140,7 +140,7 @@ This will reset the logo, favicon, title, and position.`))try{if((await secureFe
- `,P+="
",y.innerHTML=P,N("setup-step-summary")}async function z(y){try{const P=await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(y)});return P.ok?(await P.json(),!0):(errorHandler.logError("[SetupWizard] Save Config",new Error(`Server returned ${P.status}`),{function:"saveConfigToServer"}),!1)}catch(P){return errorHandler.logError("[SetupWizard] Save Config",P,{function:"saveConfigToServer"}),!1}}async function A(){const y={setupComplete:!0,configurationType:m,timestamp:new Date().toISOString(),timezone:document.getElementById("setup-timezone")?.value||Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC"};if(m==="homelab"){y.tld=document.getElementById("setup-tld")?.value?.trim()||".home",y.caName=document.getElementById("setup-ca-name")?.value?.trim()||"";const C=document.getElementById("setup-dns-provider")?.value||"technitium";y.dns={provider:C,ip:document.getElementById("setup-dns-ip")?.value?.trim()||"",port:document.getElementById("setup-dns-port")?.value?.trim()||DC.DEFAULTS.DNS_PORT,token:document.getElementById("setup-dns-token")?.value?.trim()||""},y.defaults={dnsType:"private",sslType:"internal",targetIP:"localhost"}}else m==="simple"?(y.defaultIP=document.getElementById("setup-simple-ip")?.value?.trim()||"localhost",y.defaults={dnsType:"none",sslType:"none",targetIP:y.defaultIP}):m==="public"&&(y.domain=document.getElementById("setup-public-domain")?.value?.trim()||"",y.email=document.getElementById("setup-public-email")?.value?.trim()||"",y.routingMode=document.querySelector('input[name="routing-mode"]:checked')?.value||"subdirectory",y.defaults={dnsType:y.routingMode==="subdirectory"?"none":"public",sslType:"letsencrypt",targetIP:"localhost"});const P=await z(y);safeSet("dashcaddy-config",JSON.stringify(y)),safeSet("dashcaddy-setup","completed"),document.getElementById("setup-wizard").style.display="none";const T=m==="homelab"?"Professional Home Lab":m==="simple"?"Simple Setup":"Public Server",g=P?"server (shared across all devices)":"locally (this browser only)";showNotification(`Setup Complete! Configured for: ${T}. Settings saved to: ${g}`,"success",5e3),setTimeout(()=>location.reload(),500)}const p=document.getElementById("setup-step-1-next");p&&(p.onclick=function(y){y.preventDefault();const P=document.querySelector('input[name="config-type"]:checked');P&&(m=P.value),N(m==="homelab"?"setup-step-homelab":m==="simple"?"setup-step-simple":m==="public"?"setup-step-public":"setup-step-homelab")});const B=document.getElementById("setup-skip");B&&(B.onclick=async function(y){y.preventDefault(),confirm("Skip setup? You can run it later from Settings.")&&(await z({setupComplete:!0,skipped:!0,timestamp:new Date().toISOString()}),safeSet("dashcaddy-setup","skipped"),document.getElementById("setup-wizard").style.display="none")});const h=document.getElementById("setup-tld");h&&(h.oninput=function(y){const P=y.target.value||".home",T=document.getElementById("tld-preview"),g=document.getElementById("tld-preview-2");T&&(T.textContent=P),g&&(g.textContent=P)});const H=document.getElementById("setup-homelab-back");H&&(H.onclick=function(y){y.preventDefault(),N("setup-step-1")});const w=document.getElementById("setup-homelab-next");w&&(w.onclick=function(y){y.preventDefault();const P=document.getElementById("setup-tld")?.value?.trim()||"",T=document.getElementById("setup-ca-name")?.value?.trim()||"",g=document.getElementById("setup-dns-ip")?.value?.trim()||"";if(!P||!P.startsWith(".")){showNotification("Please enter a valid TLD starting with a dot (e.g., .home)","warning");return}if(!T){showNotification("Please enter a Certificate Authority name","warning");return}if(!g){showNotification("Please enter your DNS server IP address","warning");return}R()});const $=document.getElementById("setup-simple-back");$&&($.onclick=function(y){y.preventDefault(),N("setup-step-1")});const S=document.getElementById("setup-simple-next");S&&(S.onclick=function(y){y.preventDefault(),R()}),document.querySelectorAll('input[name="routing-mode"]').forEach(function(y){y.onchange=function(){var P=document.getElementById("dns-requirement-note");P&&(P.textContent=this.value==="subdirectory"?"Only one DNS record needed (for the main domain)":"You'll need to configure DNS manually for each subdomain")}});const L=document.getElementById("setup-public-back");L&&(L.onclick=function(y){y.preventDefault(),N("setup-step-1")});const U=document.getElementById("setup-public-next");U&&(U.onclick=function(y){y.preventDefault();const P=document.getElementById("setup-public-domain")?.value?.trim()||"",T=document.getElementById("setup-public-email")?.value?.trim()||"";if(!P){showNotification("Please enter your domain name","warning");return}if(!T||!T.includes("@")){showNotification("Please enter a valid email address","warning");return}R()});const I=document.getElementById("setup-summary-back");I&&(I.onclick=function(y){y.preventDefault(),m==="homelab"?N("setup-step-homelab"):m==="simple"?N("setup-step-simple"):m==="public"&&N("setup-step-public")});const D=document.getElementById("setup-finish");D&&(D.onclick=function(y){y.preventDefault(),A()}),window.getGlobalConfig=async function(){try{const P=await fetch("/api/v1/config");if(P.ok){const T=await P.json();if(T&&T.setupComplete)return T}}catch{console.warn("Could not fetch config from server")}const y=safeGet("dashcaddy-config");return y?JSON.parse(y):{setupComplete:!1,configurationType:"homelab",tld:".home",caName:"",defaults:{dnsType:"private",sslType:"internal",targetIP:"localhost"}}},window.resetSetupWizard=async function(){if(confirm("Reset DashCaddy configuration? This will show the setup wizard again.")){try{await secureFetch("/api/v1/config",{method:"DELETE"})}catch{console.warn("Could not delete server config")}safeRemove("dashcaddy-setup"),safeRemove("dashcaddy-config"),location.reload()}}})(),(function(){const m=new ErrorHandler;injectModal("app-selector-modal",`
+ `,D+="
",g.innerHTML=D,M("setup-step-summary")}async function P(g){try{const D=await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(g)});return D.ok?(await D.json(),!0):(errorHandler.logError("[SetupWizard] Save Config",new Error(`Server returned ${D.status}`),{function:"saveConfigToServer"}),!1)}catch(D){return errorHandler.logError("[SetupWizard] Save Config",D,{function:"saveConfigToServer"}),!1}}async function A(){const g={setupComplete:!0,configurationType:y,timestamp:new Date().toISOString(),timezone:document.getElementById("setup-timezone")?.value||Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC"};if(y==="homelab"){g.tld=document.getElementById("setup-tld")?.value?.trim()||".home",g.caName=document.getElementById("setup-ca-name")?.value?.trim()||"";const I=document.getElementById("setup-dns-provider")?.value||"technitium";g.dns={provider:I,ip:document.getElementById("setup-dns-ip")?.value?.trim()||"",port:document.getElementById("setup-dns-port")?.value?.trim()||DC.DEFAULTS.DNS_PORT,token:document.getElementById("setup-dns-token")?.value?.trim()||""},g.defaults={dnsType:"private",sslType:"internal",targetIP:"localhost"}}else y==="simple"?(g.defaultIP=document.getElementById("setup-simple-ip")?.value?.trim()||"localhost",g.defaults={dnsType:"none",sslType:"none",targetIP:g.defaultIP}):y==="public"&&(g.domain=document.getElementById("setup-public-domain")?.value?.trim()||"",g.email=document.getElementById("setup-public-email")?.value?.trim()||"",g.routingMode=document.querySelector('input[name="routing-mode"]:checked')?.value||"subdirectory",g.defaults={dnsType:g.routingMode==="subdirectory"?"none":"public",sslType:"letsencrypt",targetIP:"localhost"});const D=await P(g);safeSet("dashcaddy-config",JSON.stringify(g)),safeSet("dashcaddy-setup","completed"),document.getElementById("setup-wizard").style.display="none";const B=y==="homelab"?"Professional Home Lab":y==="simple"?"Simple Setup":"Public Server",f=D?"server (shared across all devices)":"locally (this browser only)";showNotification(`Setup Complete! Configured for: ${B}. Settings saved to: ${f}`,"success",5e3),setTimeout(()=>location.reload(),500)}const p=document.getElementById("setup-step-1-next");p&&(p.onclick=function(g){g.preventDefault();const D=document.querySelector('input[name="config-type"]:checked');D&&(y=D.value),M(y==="homelab"?"setup-step-homelab":y==="simple"?"setup-step-simple":y==="public"?"setup-step-public":"setup-step-homelab")});const T=document.getElementById("setup-skip");T&&(T.onclick=async function(g){g.preventDefault(),confirm("Skip setup? You can run it later from Settings.")&&(await P({setupComplete:!0,skipped:!0,timestamp:new Date().toISOString()}),safeSet("dashcaddy-setup","skipped"),document.getElementById("setup-wizard").style.display="none")});const x=document.getElementById("setup-tld");x&&(x.oninput=function(g){const D=g.target.value||".home",B=document.getElementById("tld-preview"),f=document.getElementById("tld-preview-2");B&&(B.textContent=D),f&&(f.textContent=D)});const $=document.getElementById("setup-homelab-back");$&&($.onclick=function(g){g.preventDefault(),M("setup-step-1")});const k=document.getElementById("setup-homelab-next");k&&(k.onclick=function(g){g.preventDefault();const D=document.getElementById("setup-tld")?.value?.trim()||"",B=document.getElementById("setup-ca-name")?.value?.trim()||"",f=document.getElementById("setup-dns-ip")?.value?.trim()||"";if(!D||!D.startsWith(".")){showNotification("Please enter a valid TLD starting with a dot (e.g., .home)","warning");return}if(!B){showNotification("Please enter a Certificate Authority name","warning");return}if(!f){showNotification("Please enter your DNS server IP address","warning");return}R()});const H=document.getElementById("setup-simple-back");H&&(H.onclick=function(g){g.preventDefault(),M("setup-step-1")});const L=document.getElementById("setup-simple-next");L&&(L.onclick=function(g){g.preventDefault(),R()}),document.querySelectorAll('input[name="routing-mode"]').forEach(function(g){g.onchange=function(){var D=document.getElementById("dns-requirement-note");D&&(D.textContent=this.value==="subdirectory"?"Only one DNS record needed (for the main domain)":"You'll need to configure DNS manually for each subdomain")}});const u=document.getElementById("setup-public-back");u&&(u.onclick=function(g){g.preventDefault(),M("setup-step-1")});const j=document.getElementById("setup-public-next");j&&(j.onclick=function(g){g.preventDefault();const D=document.getElementById("setup-public-domain")?.value?.trim()||"",B=document.getElementById("setup-public-email")?.value?.trim()||"";if(!D){showNotification("Please enter your domain name","warning");return}if(!B||!B.includes("@")){showNotification("Please enter a valid email address","warning");return}R()});const v=document.getElementById("setup-summary-back");v&&(v.onclick=function(g){g.preventDefault(),y==="homelab"?M("setup-step-homelab"):y==="simple"?M("setup-step-simple"):y==="public"&&M("setup-step-public")});const N=document.getElementById("setup-finish");N&&(N.onclick=function(g){g.preventDefault(),A()}),window.getGlobalConfig=async function(){try{const D=await fetch("/api/v1/config");if(D.ok){const B=await D.json();if(B&&B.setupComplete)return B}}catch{console.warn("Could not fetch config from server")}const g=safeGet("dashcaddy-config");return g?JSON.parse(g):{setupComplete:!1,configurationType:"homelab",tld:".home",caName:"",defaults:{dnsType:"private",sslType:"internal",targetIP:"localhost"}}},window.resetSetupWizard=async function(){if(confirm("Reset DashCaddy configuration? This will show the setup wizard again.")){try{await secureFetch("/api/v1/config",{method:"DELETE"})}catch{console.warn("Could not delete server config")}safeRemove("dashcaddy-setup"),safeRemove("dashcaddy-config"),location.reload()}}})(),(function(){const y=new ErrorHandler;injectModal("app-selector-modal",`

Choose an App

@@ -346,12 +346,12 @@ This will reset the logo, favicon, title, and position.`))try{if((await secureFe
- `);const E="custom-apps";let O=null,k=null;const N=document.getElementById("app-selector-modal"),R=document.getElementById("app-selector-grid");async function z(){try{const c=await(await fetch("/api/v1/apps/templates")).json();if(c.success)return O=c.templates,k=c.categories,!0}catch(i){m.logError("[AppSelector] Fetch Templates",i,{function:"fetchApiTemplates"})}return!1}async function A(i){try{return await(await fetch(`/api/v1/apps/ports/${i}/check`)).json()}catch(c){return m.logError("[AppSelector] Check Port",c,{function:"checkPortAvailability"}),{available:!0}}}async function p(i){try{const l=await(await fetch(`/api/v1/apps/ports/${i}/suggest`)).json();if(l.success)return l.suggestedPort}catch(c){m.logError("[AppSelector] Get Suggested Port",c,{function:"getSuggestedPort"})}return i}async function B(){if(R.innerHTML='
Loading app templates...
',!O&&!await z()){R.innerHTML='
Failed to load app templates. Please try again.
';return}R.innerHTML="";const i={};for(const[l,b]of Object.entries(O)){const u=b.category||"Other";i[u]||(i[u]=[]),i[u].push({id:l,...b})}const c=k?Object.keys(k):Object.keys(i).sort();for(const l of c){const b=i[l];if(!b||b.length===0)continue;b.sort((e,t)=>(t.popularity||0)-(e.popularity||0));const u=document.createElement("div");u.className="app-category-header";const n=k?.[l]||{};u.innerHTML=`${escapeHtml(n.icon||"")} ${escapeHtml(l)}`,n.color&&(u.style.borderBottomColor=n.color),R.appendChild(u),b.forEach(e=>{const t=document.createElement("div");t.className="app-option";const o=e.isDashboardWidget,r=o&&safeGet("widget-"+e.id+"-enabled")!=="false",a=o?`
${r?"ON":"OFF"}
`:"",s=!o&&e.difficulty?`
${escapeHtml(e.difficulty)}
`:"";t.innerHTML=` + `);const C="custom-apps";let O=null,E=null;const M=document.getElementById("app-selector-modal"),R=document.getElementById("app-selector-grid");async function P(){try{const c=await(await fetch("/api/v1/apps/templates")).json();if(c.success)return O=c.templates,E=c.categories,!0}catch(i){y.logError("[AppSelector] Fetch Templates",i,{function:"fetchApiTemplates"})}return!1}async function A(i){try{return await(await fetch(`/api/v1/apps/ports/${i}/check`)).json()}catch(c){return y.logError("[AppSelector] Check Port",c,{function:"checkPortAvailability"}),{available:!0}}}async function p(i){try{const l=await(await fetch(`/api/v1/apps/ports/${i}/suggest`)).json();if(l.success)return l.suggestedPort}catch(c){y.logError("[AppSelector] Get Suggested Port",c,{function:"getSuggestedPort"})}return i}async function T(){if(R.innerHTML='
Loading app templates...
',!O&&!await P()){R.innerHTML='
Failed to load app templates. Please try again.
';return}R.innerHTML="";const i={};for(const[l,w]of Object.entries(O)){const m=w.category||"Other";i[m]||(i[m]=[]),i[m].push({id:l,...w})}const c=E?Object.keys(E):Object.keys(i).sort();for(const l of c){const w=i[l];if(!w||w.length===0)continue;w.sort((e,t)=>(t.popularity||0)-(e.popularity||0));const m=document.createElement("div");m.className="app-category-header";const n=E?.[l]||{};m.innerHTML=`${escapeHtml(n.icon||"")} ${escapeHtml(l)}`,n.color&&(m.style.borderBottomColor=n.color),R.appendChild(m),w.forEach(e=>{const t=document.createElement("div");t.className="app-option";const o=e.isDashboardWidget,r=o&&safeGet("widget-"+e.id+"-enabled")!=="false",a=o?`
${r?"ON":"OFF"}
`:"",s=!o&&e.difficulty?`
${escapeHtml(e.difficulty)}
`:"";t.innerHTML=`
${escapeHtml(e.icon||"\u{1F4E6}")}
${escapeHtml(e.name)}
${escapeHtml(e.description||"")}
${a}${s} - `,o?t.onclick=()=>h(e,t):t.onclick=()=>H(e),R.appendChild(t)})}window.renderRecipeCards&&await window.renderRecipeCards(R)}function h(i,c){const l="widget-"+i.id+"-enabled",u=!(safeGet(l)!=="false");safeSet(l,String(u));const n=i.widgetSelector;if(n){const t=document.querySelector(n);t&&(t.style.display=u?"":"none")}const e=c.querySelector('div[style*="border-radius: 4px"]');e&&(e.textContent=u?"ON":"OFF",e.style.background=u?"#2ecc7130":"#e74c3c30",e.style.color=u?"#2ecc71":"#e74c3c"),showNotification(`${i.name} widget ${u?"enabled":"disabled"}`,"success",2e3)}async function H(i){const c=document.getElementById("app-deploy-modal"),l=document.getElementById("app-deploy-title"),b=document.getElementById("deploy-subdomain"),u=document.getElementById("deploy-url-preview"),n=document.getElementById("deploy-ip"),e=document.getElementById("deploy-port"),t=document.getElementById("deploy-tailscale-only"),o=document.getElementById("tailscale-status");try{const W=await(await secureFetch("/api/v1/apps/check-existing",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({appId:i.id})})).json();if(W.success&&W.exists){const V=W.container;confirm(`Found existing ${i.name} container: + `,o?t.onclick=()=>x(e,t):t.onclick=()=>$(e),R.appendChild(t)})}window.renderRecipeCards&&await window.renderRecipeCards(R)}function x(i,c){const l="widget-"+i.id+"-enabled",m=!(safeGet(l)!=="false");safeSet(l,String(m));const n=i.widgetSelector;if(n){const t=document.querySelector(n);t&&(t.style.display=m?"":"none")}const e=c.querySelector('div[style*="border-radius: 4px"]');e&&(e.textContent=m?"ON":"OFF",e.style.background=m?"#2ecc7130":"#e74c3c30",e.style.color=m?"#2ecc71":"#e74c3c"),showNotification(`${i.name} widget ${m?"enabled":"disabled"}`,"success",2e3)}async function $(i){const c=document.getElementById("app-deploy-modal"),l=document.getElementById("app-deploy-title"),w=document.getElementById("deploy-subdomain"),m=document.getElementById("deploy-url-preview"),n=document.getElementById("deploy-ip"),e=document.getElementById("deploy-port"),t=document.getElementById("deploy-tailscale-only"),o=document.getElementById("tailscale-status");try{const G=await(await secureFetch("/api/v1/apps/check-existing",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({appId:i.id})})).json();if(G.success&&G.exists){const V=G.container;confirm(`Found existing ${i.name} container: Container: ${V.name} Status: ${V.status} @@ -360,38 +360,38 @@ Port: ${V.primaryPort||"N/A"} Would you like to use this existing container? Click OK to configure DNS/Caddy for the existing container. -Click Cancel to deploy a new container.`)&&(i._useExisting=!0,i._existingContainer=V)}}catch{}l.textContent=`Deploy ${i.name}`;const r=i.subdomain||i.id.replace(/-/g,"");b.value=r;const a=document.getElementById("subpath-compat-warning");if(a)if(SITE.routingMode==="subdirectory"){const q=i.subpathSupport||"strip";q==="none"?(a.style.display="block",a.innerHTML=''+i.name+" does not support subdirectory mode. It may not work correctly at a subpath."):q==="strip"?(a.style.display="block",a.innerHTML='ⓘ '+i.name+" has unverified subdirectory support. It may require additional configuration."):a.style.display="none"}else a.style.display="none";const s=SITE.defaults.dnsType||(SITE.configurationType==="public"?"public":"private"),d=SITE.defaults.sslType||(SITE.configurationType==="public"?"letsencrypt":"internal"),x=document.querySelector(`input[name="dns-type"][value="${s}"]`),v=document.querySelector(`input[name="ssl-type"][value="${d}"]`);x?x.checked=!0:document.querySelector('input[name="dns-type"][value="private"]').checked=!0,v?v.checked=!0:document.querySelector('input[name="ssl-type"][value="internal"]').checked=!0,n.value=SITE.defaults.targetIP||"localhost",t.checked=!1;const j=document.querySelector("#app-deploy-modal .flex-col-gap")?.closest("div"),_=document.querySelector("#app-deploy-modal details"),F=_?.querySelector("div");if(_&&F&&(SITE.configurationType==="public"||SITE.configurationType==="homelab")){const q=document.querySelectorAll('#app-deploy-modal input[name="dns-type"]')[0]?.closest("div.flex-col-gap")?.parentElement,W=document.querySelectorAll('#app-deploy-modal input[name="ssl-type"]')[0]?.closest("div.flex-col-gap")?.parentElement;q&&!q.dataset.moved&&(F.appendChild(q),q.dataset.moved="1"),W&&!W.dataset.moved&&(F.appendChild(W),W.dataset.moved="1")}const G=document.getElementById("media-path-section"),J=document.getElementById("deploy-media-path"),X=document.getElementById("media-path-description");if(i.mediaMount){G.style.display="block",J.value="",J.placeholder="/media/Movies, /media/TVShows or click Browse";const q=document.getElementById("detected-mounts-container"),W=document.getElementById("detected-mounts-list");try{const K=await(await fetch("/api/v1/media/detected-mounts")).json();if(K.success&&K.mounts.length>0){q.style.display="block",W.innerHTML="";const te=[...new Set(K.mounts.map(ee=>ee.hostPath))];J.value=te.join(", "),K.mounts.forEach(ee=>{const Z=document.createElement("button");Z.type="button";const de=te.includes(ee.hostPath);Z.style.cssText=`padding: 8px 14px; font-size: 0.85rem; background: color-mix(in srgb, var(--success) ${de?"40%":"15%"}, var(--card-bg)); border: 1px solid var(--success); border-radius: 6px; cursor: pointer; color: var(--fg);`,Z.innerHTML=`${escapeHtml(ee.folderName)}
from ${escapeHtml(ee.sourceImage)}`,Z.title=`${ee.hostPath} (from ${ee.sourceContainer})`,Z.onclick=()=>{const le=J.value.split(",").map(ce=>ce.trim()).filter(ce=>ce),pe=le.indexOf(ee.hostPath);pe>=0?(le.splice(pe,1),Z.style.background="color-mix(in srgb, var(--success) 15%, var(--card-bg))"):(le.push(ee.hostPath),Z.style.background="color-mix(in srgb, var(--success) 40%, var(--card-bg))"),J.value=le.join(", ")},W.appendChild(Z)})}else q.style.display="none"}catch{q.style.display="none"}document.getElementById("browse-media-btn").onclick=()=>{openFolderBrowser(J)}}else G.style.display="none",J.value="",document.getElementById("detected-mounts-container").style.display="none";const Q=document.getElementById("plex-claim-section");Q&&(i.id==="plex"||i.claimToken?(Q.style.display="block",document.getElementById("deploy-plex-claim").value=""):Q.style.display="none");const ne=document.getElementById("volume-mounts-section"),oe=document.getElementById("volume-mounts-list");if(oe.innerHTML="",i.docker?.volumes?.length){const q=i.mediaMount?.containerPath,W=i.docker.volumes.filter(V=>!V.includes("{{MEDIA_PATH}}")&&!(q&&V.endsWith(":"+q)));W.length>0?(ne.style.display="block",W.forEach((V,K)=>{const[te,ee]=V.split(":"),Z=document.createElement("div");Z.style.cssText="display: flex; gap: 6px; align-items: center;",Z.innerHTML=` +Click Cancel to deploy a new container.`)&&(i._useExisting=!0,i._existingContainer=V)}}catch{}l.textContent=`Deploy ${i.name}`;const r=i.subdomain||i.id.replace(/-/g,"");w.value=r;const a=document.getElementById("subpath-compat-warning");if(a)if(SITE.routingMode==="subdirectory"){const q=i.subpathSupport||"strip";q==="none"?(a.style.display="block",a.innerHTML=''+i.name+" does not support subdirectory mode. It may not work correctly at a subpath."):q==="strip"?(a.style.display="block",a.innerHTML='ⓘ '+i.name+" has unverified subdirectory support. It may require additional configuration."):a.style.display="none"}else a.style.display="none";const s=SITE.defaults.dnsType||(SITE.configurationType==="public"?"public":"private"),d=SITE.defaults.sslType||(SITE.configurationType==="public"?"letsencrypt":"internal"),S=document.querySelector(`input[name="dns-type"][value="${s}"]`),h=document.querySelector(`input[name="ssl-type"][value="${d}"]`);S?S.checked=!0:document.querySelector('input[name="dns-type"][value="private"]').checked=!0,h?h.checked=!0:document.querySelector('input[name="ssl-type"][value="internal"]').checked=!0,n.value=SITE.defaults.targetIP||"localhost",t.checked=!1;const U=document.querySelector("#app-deploy-modal .flex-col-gap")?.closest("div"),_=document.querySelector("#app-deploy-modal details"),F=_?.querySelector("div");if(_&&F&&(SITE.configurationType==="public"||SITE.configurationType==="homelab")){const q=document.querySelectorAll('#app-deploy-modal input[name="dns-type"]')[0]?.closest("div.flex-col-gap")?.parentElement,G=document.querySelectorAll('#app-deploy-modal input[name="ssl-type"]')[0]?.closest("div.flex-col-gap")?.parentElement;q&&!q.dataset.moved&&(F.appendChild(q),q.dataset.moved="1"),G&&!G.dataset.moved&&(F.appendChild(G),G.dataset.moved="1")}const W=document.getElementById("media-path-section"),J=document.getElementById("deploy-media-path"),X=document.getElementById("media-path-description");if(i.mediaMount){W.style.display="block",J.value="",J.placeholder="/media/Movies, /media/TVShows or click Browse";const q=document.getElementById("detected-mounts-container"),G=document.getElementById("detected-mounts-list");try{const K=await(await fetch("/api/v1/media/detected-mounts")).json();if(K.success&&K.mounts.length>0){q.style.display="block",G.innerHTML="";const te=[...new Set(K.mounts.map(ee=>ee.hostPath))];J.value=te.join(", "),K.mounts.forEach(ee=>{const Z=document.createElement("button");Z.type="button";const de=te.includes(ee.hostPath);Z.style.cssText=`padding: 8px 14px; font-size: 0.85rem; background: color-mix(in srgb, var(--success) ${de?"40%":"15%"}, var(--card-bg)); border: 1px solid var(--success); border-radius: 6px; cursor: pointer; color: var(--fg);`,Z.innerHTML=`${escapeHtml(ee.folderName)}
from ${escapeHtml(ee.sourceImage)}`,Z.title=`${ee.hostPath} (from ${ee.sourceContainer})`,Z.onclick=()=>{const le=J.value.split(",").map(ce=>ce.trim()).filter(ce=>ce),pe=le.indexOf(ee.hostPath);pe>=0?(le.splice(pe,1),Z.style.background="color-mix(in srgb, var(--success) 15%, var(--card-bg))"):(le.push(ee.hostPath),Z.style.background="color-mix(in srgb, var(--success) 40%, var(--card-bg))"),J.value=le.join(", ")},G.appendChild(Z)})}else q.style.display="none"}catch{q.style.display="none"}document.getElementById("browse-media-btn").onclick=()=>{openFolderBrowser(J)}}else W.style.display="none",J.value="",document.getElementById("detected-mounts-container").style.display="none";const Q=document.getElementById("plex-claim-section");Q&&(i.id==="plex"||i.claimToken?(Q.style.display="block",document.getElementById("deploy-plex-claim").value=""):Q.style.display="none");const ne=document.getElementById("volume-mounts-section"),oe=document.getElementById("volume-mounts-list");if(oe.innerHTML="",i.docker?.volumes?.length){const q=i.mediaMount?.containerPath,G=i.docker.volumes.filter(V=>!V.includes("{{MEDIA_PATH}}")&&!(q&&V.endsWith(":"+q)));G.length>0?(ne.style.display="block",G.forEach((V,K)=>{const[te,ee]=V.split(":"),Z=document.createElement("div");Z.style.cssText="display: flex; gap: 6px; align-items: center;",Z.innerHTML=` \u2192 ${ee} - `,oe.appendChild(Z),Z.querySelector(".vol-browse-btn").onclick=()=>{const de=Z.querySelector(".vol-host-path");openFolderBrowser(de)}})):ne.style.display="none"}else ne.style.display="none";const se=i.defaultPort||8080;e.value="",e.placeholder=`Default: ${se}`;let Y=document.getElementById("deploy-port-status");Y||(Y=document.createElement("div"),Y.id="deploy-port-status",Y.style.cssText="font-size: 0.8rem; margin-top: 4px;",e.parentNode.appendChild(Y));async function ie(){const q=e.value||se;Y.innerHTML='Checking port...';const W=await A(q);if(W.available)Y.innerHTML=`Port ${escapeHtml(String(q))} is available`;else{const V=await p(se);Y.innerHTML=` - Port ${escapeHtml(q)} in use by ${escapeHtml(W.conflict?.usedBy||"unknown")} - `;const K=document.createElement("button");K.type="button",K.textContent=`Use ${V}`,K.style.cssText="margin-left: 8px; padding: 2px 8px; font-size: 0.75rem; cursor: pointer;",K.onclick=()=>{document.getElementById("deploy-port").value=V,Y.innerHTML=`Using suggested port ${escapeHtml(String(V))}`},Y.appendChild(K)}}let re;e.oninput=function(){clearTimeout(re),re=setTimeout(ie,500)},ie();try{const W=await(await fetch("/api/v1/tailscale/status")).json();W.success&&W.installed&&W.connected?o.innerHTML=` + `,oe.appendChild(Z),Z.querySelector(".vol-browse-btn").onclick=()=>{const de=Z.querySelector(".vol-host-path");openFolderBrowser(de)}})):ne.style.display="none"}else ne.style.display="none";const se=i.defaultPort||8080;e.value="",e.placeholder=`Default: ${se}`;let Y=document.getElementById("deploy-port-status");Y||(Y=document.createElement("div"),Y.id="deploy-port-status",Y.style.cssText="font-size: 0.8rem; margin-top: 4px;",e.parentNode.appendChild(Y));async function ie(){const q=e.value||se;Y.innerHTML='Checking port...';const G=await A(q);if(G.available)Y.innerHTML=`Port ${escapeHtml(String(q))} is available`;else{const V=await p(se);Y.innerHTML=` + Port ${escapeHtml(q)} in use by ${escapeHtml(G.conflict?.usedBy||"unknown")} + `;const K=document.createElement("button");K.type="button",K.textContent=`Use ${V}`,K.style.cssText="margin-left: 8px; padding: 2px 8px; font-size: 0.75rem; cursor: pointer;",K.onclick=()=>{document.getElementById("deploy-port").value=V,Y.innerHTML=`Using suggested port ${escapeHtml(String(V))}`},Y.appendChild(K)}}let re;e.oninput=function(){clearTimeout(re),re=setTimeout(ie,500)},ie();try{const G=await(await fetch("/api/v1/tailscale/status")).json();G.success&&G.installed&&G.connected?o.innerHTML=` Connected - ${W.self?.hostname} (${W.self?.ip}) - | ${W.deviceCount} devices - `:W.installed?o.innerHTML='Not connected':(o.innerHTML='Not available',t.disabled=!0)}catch{o.innerHTML='Could not check status'}function ae(){const q=b.value||"subdomain",W=document.querySelector('input[name="dns-type"]:checked').value,V=document.querySelector('input[name="ssl-type"]:checked').value;let K="";if(SITE.routingMode==="subdirectory"&&SITE.domain)K=`https://${SITE.domain}/${q}`;else if(W==="private")K=`${V==="none"?"http":"https"}://${buildDomain(q)}`;else if(W==="public"){const te=V==="none"?"http":"https",ee=SITE.domain||q;K=SITE.domain?`${te}://${q}.${SITE.domain}`:`${te}://${q}`}else{const te=e.value||i.defaultPort||DC.DEFAULTS.SERVICE_PORT;K=`http://${n.value}:${te}`}u.textContent=K}b.oninput=ae,n.oninput=ae,e.oninput=ae,document.querySelectorAll('input[name="dns-type"]').forEach(q=>{q.onchange=ae}),document.querySelectorAll('input[name="ssl-type"]').forEach(q=>{q.onchange=ae}),ae(),N.classList.remove("show"),c.classList.add("show"),c.dataset.appTemplate=JSON.stringify(i)}async function w(i){const c=i.appTemplate,l=safeGetJSON(E,[]),b=c._useExisting&&c._existingContainer,u=l.find(n=>n.id===i.subdomain);if(!(u&&!b&&!confirm(`An app with subdomain "${i.subdomain}" already exists. Redeploy?`))){if(u){const n=l.indexOf(u);l.splice(n,1),safeSet(E,JSON.stringify(l))}if(b)i.port=c._existingContainer.primaryPort;else{const n=i.port||c.defaultPort||8080;showNotification(`Checking port ${n} availability...`,"info",0);const e=await A(n);if(!e.available){const t=await p(c.defaultPort||8080);if(confirm(`Port ${n} is already in use by ${e.conflict?.usedBy||"another container"}. + ${G.self?.hostname} (${G.self?.ip}) + | ${G.deviceCount} devices + `:G.installed?o.innerHTML='Not connected':(o.innerHTML='Not available',t.disabled=!0)}catch{o.innerHTML='Could not check status'}function ae(){const q=w.value||"subdomain",G=document.querySelector('input[name="dns-type"]:checked').value,V=document.querySelector('input[name="ssl-type"]:checked').value;let K="";if(SITE.routingMode==="subdirectory"&&SITE.domain)K=`https://${SITE.domain}/${q}`;else if(G==="private")K=`${V==="none"?"http":"https"}://${buildDomain(q)}`;else if(G==="public"){const te=V==="none"?"http":"https",ee=SITE.domain||q;K=SITE.domain?`${te}://${q}.${SITE.domain}`:`${te}://${q}`}else{const te=e.value||i.defaultPort||DC.DEFAULTS.SERVICE_PORT;K=`http://${n.value}:${te}`}m.textContent=K}w.oninput=ae,n.oninput=ae,e.oninput=ae,document.querySelectorAll('input[name="dns-type"]').forEach(q=>{q.onchange=ae}),document.querySelectorAll('input[name="ssl-type"]').forEach(q=>{q.onchange=ae}),ae(),M.classList.remove("show"),c.classList.add("show"),c.dataset.appTemplate=JSON.stringify(i)}async function k(i){const c=i.appTemplate,l=safeGetJSON(C,[]),w=c._useExisting&&c._existingContainer,m=l.find(n=>n.id===i.subdomain);if(!(m&&!w&&!confirm(`An app with subdomain "${i.subdomain}" already exists. Redeploy?`))){if(m){const n=l.indexOf(m);l.splice(n,1),safeSet(C,JSON.stringify(l))}if(w)i.port=c._existingContainer.primaryPort;else{const n=i.port||c.defaultPort||8080;showNotification(`Checking port ${n} availability...`,"info",0);const e=await A(n);if(!e.available){const t=await p(c.defaultPort||8080);if(confirm(`Port ${n} is already in use by ${e.conflict?.usedBy||"another container"}. -Would you like to use port ${t} instead?`))i.port=t;else{showNotification("Deployment cancelled - port conflict","error",5e3);return}}}showNotification(b?`Configuring ${c.name} with existing container...`:`Deploying ${c.name}...`,"info",0);try{const n={appId:c.id,config:{subdomain:i.subdomain,ip:i.ip,createDns:i.dnsType==="private",port:i.port||c.defaultPort||null,sslType:i.sslType,dnsType:i.dnsType,tailscaleOnly:i.tailscaleOnly||!1,mediaPath:i.mediaPath||null,plexClaimToken:i.plexClaimToken||null,customVolumes:i.customVolumes||null}};b&&(n.config.useExisting=!0,n.config.existingContainerId=c._existingContainer.id,n.config.existingPort=c._existingContainer.primaryPort,!i.port&&c._existingContainer.primaryPort&&(n.config.port=c._existingContainer.primaryPort));const t=await(await secureFetch("/api/v1/apps/deploy",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})).json();if(t.success){const o={id:i.subdomain,name:c.name,logo:`/assets/${c.id}.png`,containerId:t.containerId,url:t.url,ip:i.ip,appTemplate:c.id,tailscaleOnly:i.tailscaleOnly||!1};l.push(o),safeSet(E,JSON.stringify(l)),window.APPS&&!window.APPS.some(a=>a.id===c.id)&&(window.APPS.push(o),typeof window.buildGrid=="function"&&window.buildGrid(),typeof window.refreshAll=="function"&&setTimeout(()=>window.refreshAll(),500));let r=t.usedExisting?`${c.name} configured with existing container! +Would you like to use port ${t} instead?`))i.port=t;else{showNotification("Deployment cancelled - port conflict","error",5e3);return}}}showNotification(w?`Configuring ${c.name} with existing container...`:`Deploying ${c.name}...`,"info",0);try{const n={appId:c.id,config:{subdomain:i.subdomain,ip:i.ip,createDns:i.dnsType==="private",port:i.port||c.defaultPort||null,sslType:i.sslType,dnsType:i.dnsType,tailscaleOnly:i.tailscaleOnly||!1,mediaPath:i.mediaPath||null,plexClaimToken:i.plexClaimToken||null,customVolumes:i.customVolumes||null}};w&&(n.config.useExisting=!0,n.config.existingContainerId=c._existingContainer.id,n.config.existingPort=c._existingContainer.primaryPort,!i.port&&c._existingContainer.primaryPort&&(n.config.port=c._existingContainer.primaryPort));const t=await(await secureFetch("/api/v1/apps/deploy",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})).json();if(t.success){const o={id:i.subdomain,name:c.name,logo:`/assets/${c.id}.png`,containerId:t.containerId,url:t.url,ip:i.ip,appTemplate:c.id,tailscaleOnly:i.tailscaleOnly||!1};l.push(o),safeSet(C,JSON.stringify(l)),window.APPS&&!window.APPS.some(a=>a.id===c.id)&&(window.APPS.push(o),typeof window.buildGrid=="function"&&window.buildGrid(),typeof window.refreshAll=="function"&&setTimeout(()=>window.refreshAll(),500));let r=t.usedExisting?`${c.name} configured with existing container! URL: ${t.url}`:`${c.name} deployed successfully! URL: ${t.url}`;t.warning&&(r+=` -\u26A0 Warning: ${t.warning}`),showNotification(r,"success",8e3),delete c._useExisting,delete c._existingContainer,t.url&&t.url.startsWith("https://")&&$(t.url,c.name),t.setupInstructions&&t.setupInstructions.length>0&&setTimeout(()=>{const a=t.setupInstructions.join(` -`);showNotification(`Setup Instructions for ${c.name}: ${a}`,"info",1e4)},1e3)}else throw new Error(t.error||"Deployment failed")}catch(n){m.logError("[AppSelector] Deployment",n,{function:"deploy"}),showNotification(`Failed to deploy ${c.name}: ${n.message}`,"error",8e3)}}}async function $(i,c){showNotification(`\u23F3 Generating SSL certificate for ${c}...`,"warning",6e4);let l=0;const b=12,u=async()=>{l++;try{const n=await fetch(i,{method:"HEAD",mode:"no-cors"});return showNotification(`\u2705 ${c} is ready! SSL certificate generated.`,"success",5e3),!0}catch{return l{window.APPS.some(l=>l.id===c.id)||window.APPS.push(c)})}document.getElementById("add-service-btn")?.addEventListener("click",()=>{B(),N.classList.add("show")}),wireModal(N,document.getElementById("app-selector-cancel"));const L=document.getElementById("app-deploy-modal");document.getElementById("app-deploy-cancel")?.addEventListener("click",()=>{L.classList.remove("show")}),document.getElementById("app-deploy-confirm")?.addEventListener("click",()=>{const i=JSON.parse(L.dataset.appTemplate),c=document.getElementById("deploy-media-path").value.trim(),l=[];document.querySelectorAll("#volume-mounts-list .vol-host-path").forEach(u=>{l.push({hostPath:u.value.trim(),containerPath:u.dataset.containerPath})});const b={appTemplate:i,subdomain:document.getElementById("deploy-subdomain").value.trim(),dnsType:document.querySelector('input[name="dns-type"]:checked').value,sslType:document.querySelector('input[name="ssl-type"]:checked').value,ip:document.getElementById("deploy-ip").value.trim(),port:document.getElementById("deploy-port").value.trim(),tailscaleOnly:document.getElementById("deploy-tailscale-only").checked,mediaPath:c||null,plexClaimToken:document.getElementById("deploy-plex-claim")?.value.trim()||null,customVolumes:l.length>0?l:null,resources:{cpus:parseFloat(document.getElementById("deploy-cpu-limit").value)||0,memory:parseFloat(document.getElementById("deploy-memory-limit").value)||0}};if(!b.subdomain){showNotification("Please enter a subdomain or domain name","warning");return}if(i.mediaMount?.required&&!c){showNotification("Please enter a media library path for this application","warning");return}L.classList.remove("show"),w(b)}),wireModal(L);const U=document.getElementById("folder-browser-modal"),I=document.getElementById("folder-browser-path"),D=document.getElementById("folder-browser-list"),y=document.getElementById("folder-browser-selected"),P=document.getElementById("folder-browser-selected-list");let T="",g=[],C=null;window.openFolderBrowser=function(i){C=i,g=i.value.split(",").map(c=>c.trim()).filter(c=>c),T="",M(),f(""),U.classList.add("show")};async function f(i){I.textContent=i||"Select a drive...",D.innerHTML='
Loading...
';try{const l=await(await fetch(`/api/v1/browse/directories?path=${encodeURIComponent(i)}`)).json();if(!l.success){D.innerHTML=`
Error: ${escapeHtml(l.error)}
`;return}T=l.path||"",I.textContent=T||"Select a drive...";let b="";l.parent&&l.parent!==l.path&&(b+=`
+\u26A0 Warning: ${t.warning}`),showNotification(r,"success",8e3),delete c._useExisting,delete c._existingContainer,t.url&&t.url.startsWith("https://")&&H(t.url,c.name),t.setupInstructions&&t.setupInstructions.length>0&&setTimeout(()=>{const a=t.setupInstructions.join(` +`);showNotification(`Setup Instructions for ${c.name}: ${a}`,"info",1e4)},1e3)}else throw new Error(t.error||"Deployment failed")}catch(n){y.logError("[AppSelector] Deployment",n,{function:"deploy"}),showNotification(`Failed to deploy ${c.name}: ${n.message}`,"error",8e3)}}}async function H(i,c){showNotification(`\u23F3 Generating SSL certificate for ${c}...`,"warning",6e4);let l=0;const w=12,m=async()=>{l++;try{const n=await fetch(i,{method:"HEAD",mode:"no-cors"});return showNotification(`\u2705 ${c} is ready! SSL certificate generated.`,"success",5e3),!0}catch{return l{window.APPS.some(l=>l.id===c.id)||window.APPS.push(c)})}document.getElementById("add-service-btn")?.addEventListener("click",()=>{T(),M.classList.add("show")}),wireModal(M,document.getElementById("app-selector-cancel"));const u=document.getElementById("app-deploy-modal");document.getElementById("app-deploy-cancel")?.addEventListener("click",()=>{u.classList.remove("show")}),document.getElementById("app-deploy-confirm")?.addEventListener("click",()=>{const i=JSON.parse(u.dataset.appTemplate),c=document.getElementById("deploy-media-path").value.trim(),l=[];document.querySelectorAll("#volume-mounts-list .vol-host-path").forEach(m=>{l.push({hostPath:m.value.trim(),containerPath:m.dataset.containerPath})});const w={appTemplate:i,subdomain:document.getElementById("deploy-subdomain").value.trim(),dnsType:document.querySelector('input[name="dns-type"]:checked').value,sslType:document.querySelector('input[name="ssl-type"]:checked').value,ip:document.getElementById("deploy-ip").value.trim(),port:document.getElementById("deploy-port").value.trim(),tailscaleOnly:document.getElementById("deploy-tailscale-only").checked,mediaPath:c||null,plexClaimToken:document.getElementById("deploy-plex-claim")?.value.trim()||null,customVolumes:l.length>0?l:null,resources:{cpus:parseFloat(document.getElementById("deploy-cpu-limit").value)||0,memory:parseFloat(document.getElementById("deploy-memory-limit").value)||0}};if(!w.subdomain){showNotification("Please enter a subdomain or domain name","warning");return}if(i.mediaMount?.required&&!c){showNotification("Please enter a media library path for this application","warning");return}u.classList.remove("show"),k(w)}),wireModal(u);const j=document.getElementById("folder-browser-modal"),v=document.getElementById("folder-browser-path"),N=document.getElementById("folder-browser-list"),g=document.getElementById("folder-browser-selected"),D=document.getElementById("folder-browser-selected-list");let B="",f=[],I=null;window.openFolderBrowser=function(i){I=i,f=i.value.split(",").map(c=>c.trim()).filter(c=>c),B="",z(),b(""),j.classList.add("show")};async function b(i){v.textContent=i||"Select a drive...",N.innerHTML='
Loading...
';try{const l=await(await fetch(`/api/v1/browse/directories?path=${encodeURIComponent(i)}`)).json();if(!l.success){N.innerHTML=`
Error: ${escapeHtml(l.error)}
`;return}B=l.path||"",v.textContent=B||"Select a drive...";let w="";l.parent&&l.parent!==l.path&&(w+=`
\u2B06\uFE0F .. Parent Directory -
`),l.items.length===0&&!l.parent?b+='
No browseable drives configured. Check your docker-compose.yml volume mounts.
':l.items.length===0?b+='
No subfolders found
':l.items.forEach(u=>{const n=u.type==="drive"?"\u{1F4BE}":"\u{1F4C1}",e=g.includes(u.path),t=e?"background: color-mix(in srgb, var(--success) 20%, transparent);":"";b+=`
+
`),l.items.length===0&&!l.parent?w+='
No browseable drives configured. Check your docker-compose.yml volume mounts.
':l.items.length===0?w+='
No subfolders found
':l.items.forEach(m=>{const n=m.type==="drive"?"\u{1F4BE}":"\u{1F4C1}",e=f.includes(m.path),t=e?"background: color-mix(in srgb, var(--success) 20%, transparent);":"";w+=`
${n} - ${escapeHtml(u.name)} + ${escapeHtml(m.name)} ${e?'\u2713':""} -
`}),D.innerHTML=b,D.querySelectorAll(".folder-item").forEach(u=>{u.addEventListener("click",()=>{f(u.dataset.path)}),u.addEventListener("mouseenter",()=>{u.style.background="var(--card-bg)"}),u.addEventListener("mouseleave",()=>{const n=g.includes(u.dataset.path);u.style.background=n?"color-mix(in srgb, var(--success) 20%, transparent)":""})})}catch(c){D.innerHTML=`
Failed to load: ${escapeHtml(c.message)}
`}}function M(){if(g.length===0){y.style.display="none";return}y.style.display="block",P.innerHTML=g.map(i=>` +
`}),N.innerHTML=w,N.querySelectorAll(".folder-item").forEach(m=>{m.addEventListener("click",()=>{b(m.dataset.path)}),m.addEventListener("mouseenter",()=>{m.style.background="var(--card-bg)"}),m.addEventListener("mouseleave",()=>{const n=f.includes(m.dataset.path);m.style.background=n?"color-mix(in srgb, var(--success) 20%, transparent)":""})})}catch(c){N.innerHTML=`
Failed to load: ${escapeHtml(c.message)}
`}}function z(){if(f.length===0){g.style.display="none";return}g.style.display="block",D.innerHTML=f.map(i=>` ${escapeHtml(i)} - `).join("")}window.removeSelectedFolder=function(i){g=g.filter(c=>c!==i),M(),f(T)},document.getElementById("folder-browser-select-current").addEventListener("click",()=>{T&&!g.includes(T)&&(g.push(T),M(),f(T))}),wireModal(U,document.getElementById("folder-browser-cancel")),document.getElementById("folder-browser-done").addEventListener("click",()=>{C&&(C.value=g.join(", ")),U.classList.remove("show")}),S()})(),(function(){injectModal("recipe-deploy-modal",`
+ `).join("")}window.removeSelectedFolder=function(i){f=f.filter(c=>c!==i),z(),b(B)},document.getElementById("folder-browser-select-current").addEventListener("click",()=>{B&&!f.includes(B)&&(f.push(B),z(),b(B))}),wireModal(j,document.getElementById("folder-browser-cancel")),document.getElementById("folder-browser-done").addEventListener("click",()=>{I&&(I.value=f.join(", ")),j.classList.remove("show")}),L()})(),(function(){injectModal("recipe-deploy-modal",`

Deploy Recipe

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

\u{1F4CB} Error Logs

Loading error logs...
');const m=document.getElementById("error-log-modal"),E=document.getElementById("error-log-content"),O=document.getElementById("view-error-logs"),k=document.getElementById("error-log-refresh"),N=document.getElementById("error-log-clear"),R=document.getElementById("error-log-close");async function z(){E.innerHTML='
Loading error logs...
';try{const B=await(await fetch("/api/v1/error-logs")).json();B.success&&B.logs?B.logs.length===0?E.innerHTML='
\u2705 No errors logged! Everything is working smoothly.
':E.innerHTML=B.logs.map(h=>` + `,document.head.appendChild(D),x()})(),(function(){document.getElementById("reload-caddy-top")?.addEventListener("click",async()=>{const y=document.getElementById("reload-caddy-top"),C=y.textContent;try{y.textContent="\u23F3 Reloading...",y.disabled=!0;const O=await secureFetch("/api/v1/caddy/reload",{method:"POST",headers:{"Content-Type":"application/json"}}),E=await O.json();if(O.ok&&E.success)y.textContent="\u2705 Reloaded!",setTimeout(()=>{y.textContent=C,y.disabled=!1},2e3);else throw new Error(E.error||"Reload failed")}catch(O){y.textContent="\u274C Failed",showNotification(`Failed to reload Caddy: ${O.message}`,"error"),setTimeout(()=>{y.textContent=C,y.disabled=!1},2e3)}})})(),(function(){injectModal("error-log-modal",'

\u{1F4CB} Error Logs

Loading error logs...
');const y=document.getElementById("error-log-modal"),C=document.getElementById("error-log-content"),O=document.getElementById("view-error-logs"),E=document.getElementById("error-log-refresh"),M=document.getElementById("error-log-clear"),R=document.getElementById("error-log-close");async function P(){C.innerHTML='
Loading error logs...
';try{const T=await(await fetch("/api/v1/error-logs")).json();T.success&&T.logs?T.logs.length===0?C.innerHTML='
\u2705 No errors logged! Everything is working smoothly.
':C.innerHTML=T.logs.map(x=>`
- ${new Date(h.timestamp).toLocaleString()} + ${new Date(x.timestamp).toLocaleString()} ERROR
- ${escapeHtml(h.context)}: ${escapeHtml(h.error)} - ${h.details?`
${escapeHtml(h.details)}`:""} + ${escapeHtml(x.context)}: ${escapeHtml(x.error)} + ${x.details?`
${escapeHtml(x.details)}`:""}
- `).join(""):E.innerHTML='
\u274C Failed to load error logs
'}catch(p){E.innerHTML=`
\u274C Error loading logs: ${escapeHtml(p.message)}
`}}async function A(){if(confirm("Clear all error logs?"))try{(await(await secureFetch("/api/v1/error-logs",{method:"DELETE"})).json()).success?(showNotification("\u2705 Error logs cleared","success",3e3),z()):showNotification("\u274C Failed to clear logs","error",3e3)}catch(p){showNotification(`\u274C Error: ${p.message}`,"error",3e3)}}O?.addEventListener("click",()=>{m.classList.add("show"),z()}),k?.addEventListener("click",z),N?.addEventListener("click",A),wireModal(m,R)})(),(function(){injectModal("container-logs-modal",`
+ `).join(""):C.innerHTML='
\u274C Failed to load error logs
'}catch(p){C.innerHTML=`
\u274C Error loading logs: ${escapeHtml(p.message)}
`}}async function A(){if(confirm("Clear all error logs?"))try{(await(await secureFetch("/api/v1/error-logs",{method:"DELETE"})).json()).success?(showNotification("\u2705 Error logs cleared","success",3e3),P()):showNotification("\u274C Failed to clear logs","error",3e3)}catch(p){showNotification(`\u274C Error: ${p.message}`,"error",3e3)}}O?.addEventListener("click",()=>{y.classList.add("show"),P()}),E?.addEventListener("click",P),M?.addEventListener("click",A),wireModal(y,R)})(),(function(){injectModal("container-logs-modal",`
@@ -622,14 +622,14 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};
-
`);const m=document.getElementById("container-logs-modal"),E=document.getElementById("cl-container-select"),O=document.getElementById("cl-log-content"),k=document.getElementById("cl-log-search"),N=document.getElementById("cl-log-tail"),R=document.getElementById("cl-refresh"),z=document.getElementById("cl-stream"),A=document.getElementById("cl-download"),p=document.getElementById("cl-clear-search"),B=document.getElementById("cl-close"),h=document.getElementById("cl-close-btn"),H=document.getElementById("cl-stream-status"),w=document.getElementById("cl-stream-indicator"),$=document.getElementById("cl-stream-text"),S=document.getElementById("cl-line-count"),L=document.getElementById("cl-filter-count"),U=document.getElementById("cl-image"),I=document.getElementById("cl-status"),D=document.getElementById("cl-created");let y=null,P=[],T=[],g=null,C=!1,f=null;function M(s){if(!s)return"-";const d=new Date(s);return isNaN(d.getTime())?s:d.toLocaleString()}function i(s){if(!s)return"";const d=document.createElement("div");return d.textContent=s,d.innerHTML}function c(s,d){const x=s.stream==="stderr"?"log-stderr":"log-stdout",v=s.stream==="stderr"?"\u26A0\uFE0F":"\u{1F4E4}";return` -
+
`);const y=document.getElementById("container-logs-modal"),C=document.getElementById("cl-container-select"),O=document.getElementById("cl-log-content"),E=document.getElementById("cl-log-search"),M=document.getElementById("cl-log-tail"),R=document.getElementById("cl-refresh"),P=document.getElementById("cl-stream"),A=document.getElementById("cl-download"),p=document.getElementById("cl-clear-search"),T=document.getElementById("cl-close"),x=document.getElementById("cl-close-btn"),$=document.getElementById("cl-stream-status"),k=document.getElementById("cl-stream-indicator"),H=document.getElementById("cl-stream-text"),L=document.getElementById("cl-line-count"),u=document.getElementById("cl-filter-count"),j=document.getElementById("cl-image"),v=document.getElementById("cl-status"),N=document.getElementById("cl-created");let g=null,D=[],B=[],f=null,I=!1,b=null;function z(s){if(!s)return"-";const d=new Date(s);return isNaN(d.getTime())?s:d.toLocaleString()}function i(s){if(!s)return"";const d=document.createElement("div");return d.textContent=s,d.innerHTML}function c(s,d){const S=s.stream==="stderr"?"log-stderr":"log-stdout",h=s.stream==="stderr"?"\u26A0\uFE0F":"\u{1F4E4}";return` +
${d+1} - ${v} + ${h} ${i(s.text)}
- `}function l(s,d=""){if(!s||s.length===0){O.innerHTML='
No logs available
',S.textContent="0 lines",L.textContent="0 filtered";return}if(P=s,T=d?s.filter(x=>x.text&&x.text.toLowerCase().includes(d.toLowerCase())):s,S.textContent=`${s.length} lines`,L.textContent=d?`${T.length} of ${s.length} shown`:`${s.length} shown`,T.length===0){O.innerHTML=`
No logs match "${i(d)}"
`;return}O.innerHTML=T.map((x,v)=>c(x,v)).join(""),O.scrollTop=O.scrollHeight}async function b(){try{const d=(await getJSON("/api/v1/logs/containers")).containers||[],x=E.value;E.innerHTML='',d.forEach(v=>{const j=document.createElement("option");j.value=v.id,j.textContent=`${v.name} (${v.image.split(":")[0]}) - ${v.status}`,j.dataset.name=v.name,j.dataset.image=v.image,j.dataset.status=v.status,j.dataset.created=v.created,E.appendChild(j)}),x&&E.querySelector(`option[value="${x}"]`)&&(E.value=x,u(x))}catch(s){console.error("Failed to load containers:",s)}}function u(s){const d=E.querySelector(`option[value="${s}"]`);d&&(U.textContent=d.dataset.image||"-",I.textContent=d.dataset.status||"-",I.style.color=d.dataset.status==="running"?"var(--ok-fg, #4ade80)":"var(--bad-fg, #ef4444)",D.textContent=M(d.dataset.created))}async function n(){const s=E.value;if(!s){O.innerHTML='
Select a container to view logs
';return}t(),y=s,u(s);const d=N.value,x=k.value.trim();O.innerHTML='
Loading logs...
';try{const v=`/api/v1/logs/container/${s}${d!=="all"?`?tail=${d}`:""}`,j=await getJSON(v);j.logs&&j.logs.length>0?l(j.logs,x):(O.innerHTML='
No logs found for this container
',S.textContent="0 lines",L.textContent="0 filtered")}catch(v){O.innerHTML=`
Error loading logs: ${i(v.message)}
`}}function e(){const s=E.value;if(!s)return;t(),C=!0,z.textContent="\u23F9 Stop",H.style.display="flex",w.textContent="\u{1F7E2}",$.textContent="Connecting...";const d=`/api/v1/logs/stream/${s}`;g=new EventSource(d),g.onopen=()=>{w.textContent="\u{1F7E2}",$.textContent="Connected - streaming logs"},g.onmessage=x=>{try{const v=JSON.parse(x.data);if(v.error){w.textContent="\u{1F534}",$.textContent=`Error: ${v.error}`;return}P.push(v),T.push(v),S.textContent=`${P.length} lines`,L.textContent=`${T.length} shown`;const j=k.value.trim();if(!j||v.text&&v.text.toLowerCase().includes(j.toLowerCase())){const _=document.createElement("div");_.innerHTML=c(v,T.length-1);const F=_.firstElementChild;F.style.background="#1a3a1a",O.appendChild(F),O.scrollTop=O.scrollHeight}}catch(v){console.error("Error parsing log:",v)}},g.onerror=()=>{w.textContent="\u{1F534}",$.textContent="Disconnected",C=!1,z.textContent="\u25B6 Stream"},m._eventSource=g}function t(){g&&(g.close(),g=null),m._eventSource&&(m._eventSource.close(),m._eventSource=null),C=!1,z.textContent="\u25B6 Stream",H.style.display="none"}function o(){if(!P||P.length===0){showNotification("No logs to download","error");return}const s=E.querySelector(`option[value="${y}"]`)?.dataset.name||y,d=new Date().toISOString().replace(/[:.]/g,"-"),x=`${s}-logs-${d}.txt`,v=P.map(G=>{const J=G.timestamp||"",X=G.stream==="stderr"?"[ERR]":"[OUT]";return`${J?J+" ":""}${X} ${G.text}`}).join(` -`),j=new Blob([v],{type:"text/plain"}),_=URL.createObjectURL(j),F=document.createElement("a");F.href=_,F.download=x,document.body.appendChild(F),F.click(),document.body.removeChild(F),URL.revokeObjectURL(_),showNotification(`Downloaded ${P.length} log lines`,"success")}E?.addEventListener("change",()=>{n()}),N?.addEventListener("change",()=>{n()}),R?.addEventListener("click",()=>{n()}),z?.addEventListener("click",()=>{C?t():e()}),A?.addEventListener("click",()=>{o()}),p?.addEventListener("click",()=>{k.value="",l(P,"")}),k?.addEventListener("input",()=>{clearTimeout(f),f=setTimeout(()=>{l(P,k.value.trim())},300)}),k?.addEventListener("keydown",s=>{s.key==="Escape"&&(k.value="",l(P,""))}),document.getElementById("view-container-logs")?.addEventListener("click",()=>{m.classList.add("show"),b()});function a(){t(),m.classList.remove("show")}B?.addEventListener("click",a),h?.addEventListener("click",a),document.addEventListener("keydown",s=>{s.key==="Escape"&&m.classList.contains("show")&&a()}),m.addEventListener("click",s=>{s.target===m&&a()}),window.openContainerLogsModal=function(s,d){m.classList.add("show"),b().then(()=>{const x=Array.from(E.options).find(v=>v.value===s||v.dataset.name===d);x?(E.value=x.value,u(x.value),n()):s?(y=s,U.textContent=d||s,I.textContent="-",D.textContent="-",n()):O.innerHTML='
Select a container to view logs
'})}})(),(function(){injectModal("snapshot-modal",`
+ `}function l(s,d=""){if(!s||s.length===0){O.innerHTML='
No logs available
',L.textContent="0 lines",u.textContent="0 filtered";return}if(D=s,B=d?s.filter(S=>S.text&&S.text.toLowerCase().includes(d.toLowerCase())):s,L.textContent=`${s.length} lines`,u.textContent=d?`${B.length} of ${s.length} shown`:`${s.length} shown`,B.length===0){O.innerHTML=`
No logs match "${i(d)}"
`;return}O.innerHTML=B.map((S,h)=>c(S,h)).join(""),O.scrollTop=O.scrollHeight}async function w(){try{const d=(await getJSON("/api/v1/logs/containers")).containers||[],S=C.value;C.innerHTML='',d.forEach(h=>{const U=document.createElement("option");U.value=h.id,U.textContent=`${h.name} (${h.image.split(":")[0]}) - ${h.status}`,U.dataset.name=h.name,U.dataset.image=h.image,U.dataset.status=h.status,U.dataset.created=h.created,C.appendChild(U)}),S&&C.querySelector(`option[value="${S}"]`)&&(C.value=S,m(S))}catch(s){console.error("Failed to load containers:",s)}}function m(s){const d=C.querySelector(`option[value="${s}"]`);d&&(j.textContent=d.dataset.image||"-",v.textContent=d.dataset.status||"-",v.style.color=d.dataset.status==="running"?"var(--ok-fg, #4ade80)":"var(--bad-fg, #ef4444)",N.textContent=z(d.dataset.created))}async function n(){const s=C.value;if(!s){O.innerHTML='
Select a container to view logs
';return}t(),g=s,m(s);const d=M.value,S=E.value.trim();O.innerHTML='
Loading logs...
';try{const h=`/api/v1/logs/container/${s}${d!=="all"?`?tail=${d}`:""}`,U=await getJSON(h);U.logs&&U.logs.length>0?l(U.logs,S):(O.innerHTML='
No logs found for this container
',L.textContent="0 lines",u.textContent="0 filtered")}catch(h){O.innerHTML=`
Error loading logs: ${i(h.message)}
`}}function e(){const s=C.value;if(!s)return;t(),I=!0,P.textContent="\u23F9 Stop",$.style.display="flex",k.textContent="\u{1F7E2}",H.textContent="Connecting...";const d=`/api/v1/logs/stream/${s}`;f=new EventSource(d),f.onopen=()=>{k.textContent="\u{1F7E2}",H.textContent="Connected - streaming logs"},f.onmessage=S=>{try{const h=JSON.parse(S.data);if(h.error){k.textContent="\u{1F534}",H.textContent=`Error: ${h.error}`;return}D.push(h),B.push(h),L.textContent=`${D.length} lines`,u.textContent=`${B.length} shown`;const U=E.value.trim();if(!U||h.text&&h.text.toLowerCase().includes(U.toLowerCase())){const _=document.createElement("div");_.innerHTML=c(h,B.length-1);const F=_.firstElementChild;F.style.background="#1a3a1a",O.appendChild(F),O.scrollTop=O.scrollHeight}}catch(h){console.error("Error parsing log:",h)}},f.onerror=()=>{k.textContent="\u{1F534}",H.textContent="Disconnected",I=!1,P.textContent="\u25B6 Stream"},y._eventSource=f}function t(){f&&(f.close(),f=null),y._eventSource&&(y._eventSource.close(),y._eventSource=null),I=!1,P.textContent="\u25B6 Stream",$.style.display="none"}function o(){if(!D||D.length===0){showNotification("No logs to download","error");return}const s=C.querySelector(`option[value="${g}"]`)?.dataset.name||g,d=new Date().toISOString().replace(/[:.]/g,"-"),S=`${s}-logs-${d}.txt`,h=D.map(W=>{const J=W.timestamp||"",X=W.stream==="stderr"?"[ERR]":"[OUT]";return`${J?J+" ":""}${X} ${W.text}`}).join(` +`),U=new Blob([h],{type:"text/plain"}),_=URL.createObjectURL(U),F=document.createElement("a");F.href=_,F.download=S,document.body.appendChild(F),F.click(),document.body.removeChild(F),URL.revokeObjectURL(_),showNotification(`Downloaded ${D.length} log lines`,"success")}C?.addEventListener("change",()=>{n()}),M?.addEventListener("change",()=>{n()}),R?.addEventListener("click",()=>{n()}),P?.addEventListener("click",()=>{I?t():e()}),A?.addEventListener("click",()=>{o()}),p?.addEventListener("click",()=>{E.value="",l(D,"")}),E?.addEventListener("input",()=>{clearTimeout(b),b=setTimeout(()=>{l(D,E.value.trim())},300)}),E?.addEventListener("keydown",s=>{s.key==="Escape"&&(E.value="",l(D,""))}),document.getElementById("view-container-logs")?.addEventListener("click",()=>{y.classList.add("show"),w()});function a(){t(),y.classList.remove("show")}T?.addEventListener("click",a),x?.addEventListener("click",a),document.addEventListener("keydown",s=>{s.key==="Escape"&&y.classList.contains("show")&&a()}),y.addEventListener("click",s=>{s.target===y&&a()}),window.openContainerLogsModal=function(s,d){y.classList.add("show"),w().then(()=>{const S=Array.from(C.options).find(h=>h.value===s||h.dataset.name===d);S?(C.value=S.value,m(S.value),n()):s?(g=s,j.textContent=d||s,v.textContent="-",N.textContent="-",n()):O.innerHTML='
Select a container to view logs
'})}})(),(function(){injectModal("snapshot-modal",`

\u{1F4BE} Container Snapshots

-
`);const m=document.getElementById("snapshot-modal"),E=document.getElementById("snapshot-btn"),O=document.getElementById("snapshot-close"),k=document.getElementById("snapshot-container-select"),N=document.getElementById("snapshot-details"),R=document.getElementById("snapshot-create-btn"),z=document.getElementById("snapshot-create-status");let A=null;async function p(){try{const S=await(await fetch("/api/v1/containers")).json();if(!S.success||!S.containers)return;k.innerHTML='';for(const L of S.containers){const U=document.createElement("option");U.value=L.id,U.textContent=`${L.name||L.id} (${L.image||"unknown"})`,U.dataset.name=L.name,U.dataset.image=L.image,U.dataset.status=L.status,U.dataset.created=L.created,k.appendChild(U)}}catch($){console.error("Failed to load containers:",$)}}function B($){if(!$||!$.value){N.style.display="none",A=null;return}A=$.value,document.getElementById("snapshot-image").textContent=$.dataset.image||"-",document.getElementById("snapshot-status").textContent=$.dataset.status||"-",document.getElementById("snapshot-created").textContent=$.dataset.created?new Date($.dataset.created*1e3).toLocaleString():"-",document.getElementById("snapshot-id").textContent=$.value.substring(0,12),N.style.display=""}async function h(){if(!A){z.textContent="Please select a container first",z.style.color="var(--bad-fg)";return}const $=document.getElementById("snapshot-name").value.trim();if(!$){z.textContent="Please enter a snapshot name",z.style.color="var(--bad-fg)";return}const S=document.getElementById("snapshot-leave-running").checked;R.disabled=!0,R.textContent="Creating...",z.textContent="";try{const U=await(await fetch(`/api/v1/containers/${encodeURIComponent(A)}/checkpoint`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:$,leaveRunning:S})})).json();U.success?(z.textContent=`\u2713 Snapshot "${$}" created successfully`,z.style.color="var(--ok-fg)",document.getElementById("snapshot-name").value=""):(z.textContent=`\u2717 Failed: ${U.error||"Unknown error"}`,z.style.color="var(--bad-fg)")}catch(L){z.textContent=`\u2717 Error: ${L.message}`,z.style.color="var(--bad-fg)"}finally{R.disabled=!1,R.textContent="\u{1F4BE} Create Snapshot"}}function H(){m.classList.add("show"),p()}function w(){m.classList.remove("show"),N.style.display="none",A=null,k.selectedIndex=0}E?.addEventListener("click",H),O?.addEventListener("click",w),wireModal(m,O),k?.addEventListener("change",$=>{const S=k.options[k.selectedIndex];B(S)}),R?.addEventListener("click",h),m?.querySelectorAll(".panel-tab").forEach($=>{$.addEventListener("click",()=>{m.querySelectorAll(".panel-tab").forEach(S=>S.classList.remove("active")),m.querySelectorAll(".panel-section").forEach(S=>S.classList.remove("active")),$.classList.add("active"),m.querySelector(`#${$.dataset.panel}`).classList.add("active")})})})(),(function(){injectModal("arr-setup-modal",`
+
`);const y=document.getElementById("snapshot-modal"),C=document.getElementById("snapshot-btn"),O=document.getElementById("snapshot-close"),E=document.getElementById("snapshot-container-select"),M=document.getElementById("snapshot-details"),R=document.getElementById("snapshot-create-btn"),P=document.getElementById("snapshot-create-status");let A=null;async function p(){try{const L=await(await fetch("/api/v1/containers")).json();if(!L.success||!L.containers)return;E.innerHTML='';for(const u of L.containers){const j=document.createElement("option");j.value=u.id,j.textContent=`${u.name||u.id} (${u.image||"unknown"})`,j.dataset.name=u.name,j.dataset.image=u.image,j.dataset.status=u.status,j.dataset.created=u.created,E.appendChild(j)}}catch(H){console.error("Failed to load containers:",H)}}function T(H){if(!H||!H.value){M.style.display="none",A=null;return}A=H.value,document.getElementById("snapshot-image").textContent=H.dataset.image||"-",document.getElementById("snapshot-status").textContent=H.dataset.status||"-",document.getElementById("snapshot-created").textContent=H.dataset.created?new Date(H.dataset.created*1e3).toLocaleString():"-",document.getElementById("snapshot-id").textContent=H.value.substring(0,12),M.style.display=""}async function x(){if(!A){P.textContent="Please select a container first",P.style.color="var(--bad-fg)";return}const H=document.getElementById("snapshot-name").value.trim();if(!H){P.textContent="Please enter a snapshot name",P.style.color="var(--bad-fg)";return}const L=document.getElementById("snapshot-leave-running").checked;R.disabled=!0,R.textContent="Creating...",P.textContent="";try{const j=await(await fetch(`/api/v1/containers/${encodeURIComponent(A)}/checkpoint`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:H,leaveRunning:L})})).json();j.success?(P.textContent=`\u2713 Snapshot "${H}" created successfully`,P.style.color="var(--ok-fg)",document.getElementById("snapshot-name").value=""):(P.textContent=`\u2717 Failed: ${j.error||"Unknown error"}`,P.style.color="var(--bad-fg)")}catch(u){P.textContent=`\u2717 Error: ${u.message}`,P.style.color="var(--bad-fg)"}finally{R.disabled=!1,R.textContent="\u{1F4BE} Create Snapshot"}}function $(){y.classList.add("show"),p()}function k(){y.classList.remove("show"),M.style.display="none",A=null,E.selectedIndex=0}C?.addEventListener("click",$),O?.addEventListener("click",k),wireModal(y,O),E?.addEventListener("change",H=>{const L=E.options[E.selectedIndex];T(L)}),R?.addEventListener("click",x),y?.querySelectorAll(".panel-tab").forEach(H=>{H.addEventListener("click",()=>{y.querySelectorAll(".panel-tab").forEach(L=>L.classList.remove("active")),y.querySelectorAll(".panel-section").forEach(L=>L.classList.remove("active")),H.classList.add("active"),y.querySelector(`#${H.dataset.panel}`).classList.add("active")})})})(),(function(){injectModal("arr-setup-modal",`

\u{1F3AC} Smart Arr Connect

@@ -762,73 +762,73 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};

-
`);const m=document.getElementById("arr-setup-modal"),E=document.getElementById("arr-setup-btn"),O=document.getElementById("arr-setup-cancel"),k=document.getElementById("smart-connect-btn"),N=document.getElementById("smart-phase-detect"),R=document.getElementById("smart-phase-credentials"),z=document.getElementById("smart-phase-progress"),A=document.getElementById("smart-phase-results"),p=document.getElementById("smart-detect-results"),B=document.getElementById("smart-credential-inputs"),h=document.getElementById("smart-progress-steps"),H=document.getElementById("smart-results-content"),w=document.getElementById("smart-plex-libraries"),$=document.getElementById("smart-retry-btn");let S=null;const L={plex:"\u{1F3AC}",radarr:"\u{1F3AC}",sonarr:"\u{1F4FA}",prowlarr:"\u{1F50D}",seerr:"\u{1F4CB}"},U={plex:"Plex",radarr:"Radarr (Movies)",sonarr:"Sonarr (TV)",prowlarr:"Prowlarr (Indexers)",seerr:"Seerr"};function I(f){N.style.display=f==="detect"?"block":"none",R.style.display=f==="credentials"?"block":"none",z.style.display=f==="progress"?"block":"none",A.style.display=f==="results"?"block":"none"}function D(f){const M={connected:{bg:"var(--ok-fg)",icon:"✓",text:"Connected"},needs_key:{bg:"#f39c12",icon:"🔑",text:"Needs API Key"},not_found:{bg:"var(--muted)",icon:"—",text:"Not Found"},error:{bg:"var(--bad-fg)",icon:"✗",text:"Error"}},i=M[f]||M.not_found;return`${i.icon} ${i.text}`}async function y(){I("detect"),p.style.display="none";try{if(S=await(await fetch("/api/v1/arr/smart-detect")).json(),!S.success){p.innerHTML=`
Detection failed: ${escapeHtml(S.error)}
`,p.style.display="block";return}let M='
';for(const[c,l]of Object.entries(S.services)){const b=L[c]||"\u{1F4E6}",u=U[c]||c,n=l.source?`${escapeHtml(l.source)}`:"",e=l.version?`v${escapeHtml(l.version)}`:"",t=(l.hasApiKey||l.hasToken)&&l.status==="connected"?'Key saved':"";M+=`
- ${b} +
`);const y=document.getElementById("arr-setup-modal"),C=document.getElementById("arr-setup-btn"),O=document.getElementById("arr-setup-cancel"),E=document.getElementById("smart-connect-btn"),M=document.getElementById("smart-phase-detect"),R=document.getElementById("smart-phase-credentials"),P=document.getElementById("smart-phase-progress"),A=document.getElementById("smart-phase-results"),p=document.getElementById("smart-detect-results"),T=document.getElementById("smart-credential-inputs"),x=document.getElementById("smart-progress-steps"),$=document.getElementById("smart-results-content"),k=document.getElementById("smart-plex-libraries"),H=document.getElementById("smart-retry-btn");let L=null;const u={plex:"\u{1F3AC}",radarr:"\u{1F3AC}",sonarr:"\u{1F4FA}",prowlarr:"\u{1F50D}",seerr:"\u{1F4CB}"},j={plex:"Plex",radarr:"Radarr (Movies)",sonarr:"Sonarr (TV)",prowlarr:"Prowlarr (Indexers)",seerr:"Seerr"};function v(b){M.style.display=b==="detect"?"block":"none",R.style.display=b==="credentials"?"block":"none",P.style.display=b==="progress"?"block":"none",A.style.display=b==="results"?"block":"none"}function N(b){const z={connected:{bg:"var(--ok-fg)",icon:"✓",text:"Connected"},needs_key:{bg:"#f39c12",icon:"🔑",text:"Needs API Key"},not_found:{bg:"var(--muted)",icon:"—",text:"Not Found"},error:{bg:"var(--bad-fg)",icon:"✗",text:"Error"}},i=z[b]||z.not_found;return`${i.icon} ${i.text}`}async function g(){v("detect"),p.style.display="none";try{if(L=await(await fetch("/api/v1/arr/smart-detect")).json(),!L.success){p.innerHTML=`
Detection failed: ${escapeHtml(L.error)}
`,p.style.display="block";return}let z='
';for(const[c,l]of Object.entries(L.services)){const w=u[c]||"\u{1F4E6}",m=j[c]||c,n=l.source?`${escapeHtml(l.source)}`:"",e=l.version?`v${escapeHtml(l.version)}`:"",t=(l.hasApiKey||l.hasToken)&&l.status==="connected"?'Key saved':"";z+=`
+ ${w}
-
${u}
+
${m}
${n} ${e} ${t}
- ${D(l.status)} -
`}M+="
";const i=S.summary;M+=`
+ ${N(l.status)} +
`}z+="
";const i=L.summary;z+=`
${escapeHtml(String(i.fullyConnected))}/${escapeHtml(String(i.totalDetected+(5-i.totalDetected)))} services detected · ${escapeHtml(String(i.fullyConnected))} connected${i.needsApiKey>0?` · ${escapeHtml(String(i.needsApiKey))} needs API key`:""} -
`,p.innerHTML=M,p.style.display="block",P(S),setTimeout(()=>{I("credentials")},800)}catch(f){p.innerHTML=`
Error: ${escapeHtml(f.message)}
`,p.style.display="block"}}function P(f){let M="";const i=f.services,c=["radarr","sonarr","prowlarr"];for(const u of c){const n=i[u];if(!n||n.status==="not_found"&&!n.url)continue;const e=L[u],t=U[u],o=n.status==="connected";M+=`
+
`,p.innerHTML=z,p.style.display="block",D(L),setTimeout(()=>{v("credentials")},800)}catch(b){p.innerHTML=`
Error: ${escapeHtml(b.message)}
`,p.style.display="block"}}function D(b){let z="";const i=b.services,c=["radarr","sonarr","prowlarr"];for(const m of c){const n=i[m];if(!n||n.status==="not_found"&&!n.url)continue;const e=u[m],t=j[m],o=n.status==="connected";z+=`
${e} ${t} - + ${o?'✓ Connected':""}
-
-
- -
`}const l=i.plex;if(l){const u=l.status==="connected";M+=`
+ +
`}const l=i.plex;if(l){const m=l.status==="connected";z+=`
\u{1F3AC} Plex - ${D(l.status)} + ${N(l.status)} ${escapeHtml(l.source||"")}
-
`}const b=i.seerr;if(b){const u=b.status==="connected";let n="";if(b.configuredServices){const e=b.configuredServices;n=`
+
`}const w=i.seerr;if(w){const m=w.status==="connected";let n="";if(w.configuredServices){const e=w.configuredServices;n=`
Configured: ${e.radarr?"✓ Radarr":"✗ Radarr"} · ${e.sonarr?"✓ Sonarr":"✗ Sonarr"} · ${e.plex?"✓ Plex":"✗ Plex"} -
`}M+=`
+
`}z+=`
\u{1F4CB} Seerr - ${D(b.status)} + ${N(w.status)}
${n} -
`}B.innerHTML=M}window.smartTestConnection=async function(f){const M=document.getElementById(`smart-${f}-url`),i=document.getElementById(`smart-${f}-key`),c=document.getElementById(`smart-${f}-status`),l=M?.value.trim(),b=i?.value.trim();if(!l||!b){c.innerHTML='Enter URL and API key';return}c.innerHTML='';try{const n=await(await secureFetch("/api/v1/arr/test-connection",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({service:f,url:l,apiKey:b})})).json();n.success?c.innerHTML=`✓ ${escapeHtml(n.appName||"Connected")} v${escapeHtml(n.version||"")}`:c.innerHTML=`✗ ${escapeHtml(n.error)}`}catch(u){c.innerHTML=`✗ ${escapeHtml(u.message)}`}};async function T(){I("progress"),h.innerHTML='
Connecting services...
';const f={};for(const i of["radarr","sonarr","prowlarr"]){const c=document.getElementById(`smart-${i}-url`)?.value.trim(),l=document.getElementById(`smart-${i}-key`)?.value.trim();l&&c?f[i]={apiKey:l,url:c}:l&&(f[i]={apiKey:l})}const M={services:Object.keys(f).length>0?f:void 0,configurePlex:document.getElementById("smart-opt-plex")?.checked,configureProwlarr:document.getElementById("smart-opt-prowlarr")?.checked,configureSeerr:document.getElementById("smart-opt-seerr")?.checked,saveCredentials:document.getElementById("smart-opt-save")?.checked};try{const c=await(await secureFetch("/api/v1/arr/smart-connect",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(M)})).json();let l="";for(const b of c.steps||[]){const u=b.status==="success"?'':'',n=b.status==="success"?"var(--muted)":"var(--bad-fg)";l+=`
- ${u} - ${escapeHtml(b.step)} - ${escapeHtml(b.details||"")} -
`}h.innerHTML=l,setTimeout(()=>g(c),500)}catch(i){h.innerHTML=`
Connection error: ${escapeHtml(i.message)}
`}}function g(f){I("results");const M=f.summary||{},i=M.failed===0&&M.succeeded>0,c=i?"var(--ok-fg)":"#f39c12",l=i?"✓":"⚠",b=i?"All Connected!":`${escapeHtml(String(M.succeeded))}/${escapeHtml(String(M.totalSteps))} Steps Succeeded`;let u=`
+
`}T.innerHTML=z}window.smartTestConnection=async function(b){const z=document.getElementById(`smart-${b}-url`),i=document.getElementById(`smart-${b}-key`),c=document.getElementById(`smart-${b}-status`),l=z?.value.trim(),w=i?.value.trim();if(!l||!w){c.innerHTML='Enter URL and API key';return}c.innerHTML='';try{const n=await(await secureFetch("/api/v1/arr/test-connection",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({service:b,url:l,apiKey:w})})).json();n.success?c.innerHTML=`✓ ${escapeHtml(n.appName||"Connected")} v${escapeHtml(n.version||"")}`:c.innerHTML=`✗ ${escapeHtml(n.error)}`}catch(m){c.innerHTML=`✗ ${escapeHtml(m.message)}`}};async function B(){v("progress"),x.innerHTML='
Connecting services...
';const b={};for(const i of["radarr","sonarr","prowlarr"]){const c=document.getElementById(`smart-${i}-url`)?.value.trim(),l=document.getElementById(`smart-${i}-key`)?.value.trim();l&&c?b[i]={apiKey:l,url:c}:l&&(b[i]={apiKey:l})}const z={services:Object.keys(b).length>0?b:void 0,configurePlex:document.getElementById("smart-opt-plex")?.checked,configureProwlarr:document.getElementById("smart-opt-prowlarr")?.checked,configureSeerr:document.getElementById("smart-opt-seerr")?.checked,saveCredentials:document.getElementById("smart-opt-save")?.checked};try{const c=await(await secureFetch("/api/v1/arr/smart-connect",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(z)})).json();let l="";for(const w of c.steps||[]){const m=w.status==="success"?'':'',n=w.status==="success"?"var(--muted)":"var(--bad-fg)";l+=`
+ ${m} + ${escapeHtml(w.step)} + ${escapeHtml(w.details||"")} +
`}x.innerHTML=l,setTimeout(()=>f(c),500)}catch(i){x.innerHTML=`
Connection error: ${escapeHtml(i.message)}
`}}function f(b){v("results");const z=b.summary||{},i=z.failed===0&&z.succeeded>0,c=i?"var(--ok-fg)":"#f39c12",l=i?"✓":"⚠",w=i?"All Connected!":`${escapeHtml(String(z.succeeded))}/${escapeHtml(String(z.totalSteps))} Steps Succeeded`;let m=`
${l}
-
${b}
-
${escapeHtml(String(M.succeeded))} succeeded, ${escapeHtml(String(M.failed))} failed
-
`;u+='
';for(const n of f.steps||[]){const e=n.status==="success"?'':'';u+=`
+
${w}
+
${escapeHtml(String(z.succeeded))} succeeded, ${escapeHtml(String(z.failed))} failed
+
`;m+='
';for(const n of b.steps||[]){const e=n.status==="success"?'':'';m+=`
${e} ${escapeHtml(n.step)} ${escapeHtml(n.details||"")} -
`}u+="
",H.innerHTML=u,$.style.display=M.failed>0?"block":"none",f.steps?.some(n=>n.step.includes("Plex")&&n.status==="success")&&C()}async function C(){try{const M=await(await fetch("/api/v1/plex/libraries")).json();if(M.success&&M.libraries?.length>0){let i=`
-

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

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

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

+
`;for(const c of z.libraries){const l=c.type==="movie"?"\u{1F3AC}":c.type==="show"?"\u{1F4FA}":"\u{1F3B5}";i+=`
${l} ${escapeHtml(c.title)} ${escapeHtml(String(c.count))} items -
`}i+="
",w.innerHTML=i,w.style.display="block"}}catch{}}E?.addEventListener("click",()=>{m.classList.add("show"),w.style.display="none",y()}),wireModal(m,O),k?.addEventListener("click",T),$?.addEventListener("click",T)})(),(function(){const m=new ErrorHandler;injectModal("notifications-modal",`
+
`}i+="
",k.innerHTML=i,k.style.display="block"}}catch{}}C?.addEventListener("click",()=>{y.classList.add("show"),k.style.display="none",g()}),wireModal(y,O),E?.addEventListener("click",B),H?.addEventListener("click",B)})(),(function(){const y=new ErrorHandler;injectModal("notifications-modal",`

\u{1F514} Notification Settings

@@ -1013,15 +1013,15 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};
-
`);const E=document.getElementById("notifications-modal"),O=document.getElementById("manage-notifications"),k=document.getElementById("notifications-save"),N=document.getElementById("notifications-cancel");["discord","telegram","ntfy","email"].forEach(w=>{const $=document.getElementById(`${w}-enabled`),S=document.getElementById(`${w}-config`);$?.addEventListener("change",()=>{S.style.display=$.checked?"block":"none"})});const R=document.getElementById("health-check-enabled"),z=document.getElementById("health-check-config");R?.addEventListener("change",()=>{z.style.opacity=R.checked?"1":"0.5"});async function A(){try{const $=await(await fetch("/api/v1/notifications/config")).json();if($.success){const S=$.config;document.getElementById("notifications-enabled").checked=S.enabled,document.getElementById("discord-enabled").checked=S.providers?.discord?.enabled||!1,document.getElementById("telegram-enabled").checked=S.providers?.telegram?.enabled||!1,document.getElementById("ntfy-enabled").checked=S.providers?.ntfy?.enabled||!1,document.getElementById("email-enabled").checked=S.providers?.email?.enabled||!1,document.getElementById("discord-config").style.display=S.providers?.discord?.enabled?"block":"none",document.getElementById("telegram-config").style.display=S.providers?.telegram?.enabled?"block":"none",document.getElementById("ntfy-config").style.display=S.providers?.ntfy?.enabled?"block":"none",document.getElementById("email-config").style.display=S.providers?.email?.enabled?"block":"none",S.providers?.ntfy?.serverUrl&&(document.getElementById("ntfy-server").value=S.providers.ntfy.serverUrl),S.providers?.email?.host&&(document.getElementById("email-host").value=S.providers.email.host),S.providers?.email?.from&&(document.getElementById("email-from").value=S.providers.email.from),document.getElementById("health-check-enabled").checked=S.healthCheck?.enabled||!1,S.healthCheck?.intervalMinutes&&(document.getElementById("health-check-interval").value=S.healthCheck.intervalMinutes),S.healthCheck?.lastCheck&&(document.getElementById("health-check-status").textContent=`Last check: ${new Date(S.healthCheck.lastCheck).toLocaleString()}`),document.getElementById("event-container-down").checked=S.events?.containerDown!==!1,document.getElementById("event-container-up").checked=S.events?.containerUp!==!1,document.getElementById("event-deploy-success").checked=S.events?.deploymentSuccess!==!1,document.getElementById("event-deploy-failed").checked=S.events?.deploymentFailed!==!1,document.getElementById("event-resource-alert").checked=S.events?.resourceAlert!==!1}}catch(w){m.logError("[Notifications] Load Config",w,{function:"loadConfig"})}}async function p(){try{const $=await(await fetch("/api/v1/notifications/history?limit=10")).json(),S=document.getElementById("notification-history");$.success&&$.history?.length>0?S.innerHTML=$.history.map(L=>{const U=new Date(L.timestamp).toLocaleString();return` +
`);const C=document.getElementById("notifications-modal"),O=document.getElementById("manage-notifications"),E=document.getElementById("notifications-save"),M=document.getElementById("notifications-cancel");["discord","telegram","ntfy","email"].forEach(k=>{const H=document.getElementById(`${k}-enabled`),L=document.getElementById(`${k}-config`);H?.addEventListener("change",()=>{L.style.display=H.checked?"block":"none"})});const R=document.getElementById("health-check-enabled"),P=document.getElementById("health-check-config");R?.addEventListener("change",()=>{P.style.opacity=R.checked?"1":"0.5"});async function A(){try{const H=await(await fetch("/api/v1/notifications/config")).json();if(H.success){const L=H.config;document.getElementById("notifications-enabled").checked=L.enabled,document.getElementById("discord-enabled").checked=L.providers?.discord?.enabled||!1,document.getElementById("telegram-enabled").checked=L.providers?.telegram?.enabled||!1,document.getElementById("ntfy-enabled").checked=L.providers?.ntfy?.enabled||!1,document.getElementById("email-enabled").checked=L.providers?.email?.enabled||!1,document.getElementById("discord-config").style.display=L.providers?.discord?.enabled?"block":"none",document.getElementById("telegram-config").style.display=L.providers?.telegram?.enabled?"block":"none",document.getElementById("ntfy-config").style.display=L.providers?.ntfy?.enabled?"block":"none",document.getElementById("email-config").style.display=L.providers?.email?.enabled?"block":"none",L.providers?.ntfy?.serverUrl&&(document.getElementById("ntfy-server").value=L.providers.ntfy.serverUrl),L.providers?.email?.host&&(document.getElementById("email-host").value=L.providers.email.host),L.providers?.email?.from&&(document.getElementById("email-from").value=L.providers.email.from),document.getElementById("health-check-enabled").checked=L.healthCheck?.enabled||!1,L.healthCheck?.intervalMinutes&&(document.getElementById("health-check-interval").value=L.healthCheck.intervalMinutes),L.healthCheck?.lastCheck&&(document.getElementById("health-check-status").textContent=`Last check: ${new Date(L.healthCheck.lastCheck).toLocaleString()}`),document.getElementById("event-container-down").checked=L.events?.containerDown!==!1,document.getElementById("event-container-up").checked=L.events?.containerUp!==!1,document.getElementById("event-deploy-success").checked=L.events?.deploymentSuccess!==!1,document.getElementById("event-deploy-failed").checked=L.events?.deploymentFailed!==!1,document.getElementById("event-resource-alert").checked=L.events?.resourceAlert!==!1}}catch(k){y.logError("[Notifications] Load Config",k,{function:"loadConfig"})}}async function p(){try{const H=await(await fetch("/api/v1/notifications/history?limit=10")).json(),L=document.getElementById("notification-history");H.success&&H.history?.length>0?L.innerHTML=H.history.map(u=>{const j=new Date(u.timestamp).toLocaleString();return`
- ${L.type==="success"?"\u2713":L.type==="error"?"\u2717":"\u2139"} + ${u.type==="success"?"\u2713":u.type==="error"?"\u2717":"\u2139"}
-
${escapeHtml(L.title)}
-
${U}
+
${escapeHtml(u.title)}
+
${j}
- `}).join(""):S.innerHTML='
No notifications yet
'}catch(w){m.logError("[Notifications] Load History",w,{function:"loadHistory"})}}async function B(){try{const w={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,user:document.getElementById("email-user").value.trim(),pass:document.getElementById("email-pass").value.trim(),from:document.getElementById("email-from").value.trim(),to:document.getElementById("email-to").value.trim()}},events:{containerDown:document.getElementById("event-container-down").checked,containerUp:document.getElementById("event-container-up").checked,deploymentSuccess:document.getElementById("event-deploy-success").checked,deploymentFailed:document.getElementById("event-deploy-failed").checked,resourceAlert:document.getElementById("event-resource-alert").checked},healthCheck:{enabled:document.getElementById("health-check-enabled").checked,intervalMinutes:parseInt(document.getElementById("health-check-interval").value)||5}},S=await(await secureFetch("/api/v1/notifications/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(w)})).json();S.success?(showNotification("Notification settings saved","success",3e3),E.classList.remove("show")):showNotification(`Failed to save: ${S.error}`,"error",3e3)}catch(w){showNotification(`Error: ${w.message}`,"error",3e3)}}async function h(w){try{const S=await(await secureFetch("/api/v1/notifications/test",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({provider:w})})).json();S.success?showNotification(`Test ${w} notification sent!`,"success",3e3):showNotification(`Test failed: ${S.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(w){showNotification(`Error: ${w.message}`,"error",3e3)}}),O?.addEventListener("click",()=>{E.classList.add("show"),A(),p()}),k?.addEventListener("click",B),document.getElementById("notifications-send-test")?.addEventListener("click",async()=>{const w=document.getElementById("notifications-send-test"),$=w.textContent;w.textContent="Sending...",w.disabled=!0;try{const L=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();L.success?(showNotification("Test notification sent!","success",3e3),H()):showNotification(`Test failed: ${L.results?.map(U=>`${U.provider}: ${U.error||"ok"}`).join(", ")}`,"error",5e3)}catch(S){showNotification(`Error: ${S.message}`,"error",3e3)}finally{w.textContent=$,w.disabled=!1}});async function H(){try{const $=await(await fetch("/api/v1/notifications/status")).json();if($.success&&$.lastSent){const S=document.getElementById("last-notification-sent");S&&(S.textContent=`Last sent: ${new Date($.lastSent).toLocaleString()}`)}}catch{}}wireModal(E,N)})(),(function(){document.addEventListener("click",m=>{const E=m.target.closest(".panel-tab");if(!E)return;const O=E.dataset.panel;if(!O)return;const k=E.closest(".panel-tabs"),N=k.closest(".weather-modal-content");k.querySelectorAll(".panel-tab").forEach(z=>z.classList.remove("active")),E.classList.add("active"),N.querySelectorAll(".panel-section").forEach(z=>z.classList.remove("active"));const R=N.querySelector("#"+O);R&&R.classList.add("active")})})(),(function(){var m=["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 E(){for(var e={},t=0;t + `}).join(""):L.innerHTML='
No notifications yet
'}catch(k){y.logError("[Notifications] Load History",k,{function:"loadHistory"})}}async function T(){try{const k={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,user:document.getElementById("email-user").value.trim(),pass:document.getElementById("email-pass").value.trim(),from:document.getElementById("email-from").value.trim(),to:document.getElementById("email-to").value.trim()}},events:{containerDown:document.getElementById("event-container-down").checked,containerUp:document.getElementById("event-container-up").checked,deploymentSuccess:document.getElementById("event-deploy-success").checked,deploymentFailed:document.getElementById("event-deploy-failed").checked,resourceAlert:document.getElementById("event-resource-alert").checked},healthCheck:{enabled:document.getElementById("health-check-enabled").checked,intervalMinutes:parseInt(document.getElementById("health-check-interval").value)||5}},L=await(await secureFetch("/api/v1/notifications/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(k)})).json();L.success?(showNotification("Notification settings saved","success",3e3),C.classList.remove("show")):showNotification(`Failed to save: ${L.error}`,"error",3e3)}catch(k){showNotification(`Error: ${k.message}`,"error",3e3)}}async function x(k){try{const L=await(await secureFetch("/api/v1/notifications/test",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({provider:k})})).json();L.success?showNotification(`Test ${k} notification sent!`,"success",3e3):showNotification(`Test failed: ${L.error}`,"error",3e3)}catch(H){showNotification(`Error: ${H.message}`,"error",3e3)}}document.getElementById("discord-test")?.addEventListener("click",()=>x("discord")),document.getElementById("telegram-test")?.addEventListener("click",()=>x("telegram")),document.getElementById("ntfy-test")?.addEventListener("click",()=>x("ntfy")),document.getElementById("email-test")?.addEventListener("click",()=>x("email")),document.getElementById("health-check-now")?.addEventListener("click",async()=>{try{const H=await(await secureFetch("/api/v1/notifications/health-check",{method:"POST"})).json();H.success&&(document.getElementById("health-check-status").textContent=`Last check: ${new Date(H.lastCheck).toLocaleString()} (${H.containersMonitored} containers)`,showNotification("Health check completed","success",2e3))}catch(k){showNotification(`Error: ${k.message}`,"error",3e3)}}),O?.addEventListener("click",()=>{C.classList.add("show"),A(),p()}),E?.addEventListener("click",T),document.getElementById("notifications-send-test")?.addEventListener("click",async()=>{const k=document.getElementById("notifications-send-test"),H=k.textContent;k.textContent="Sending...",k.disabled=!0;try{const u=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();u.success?(showNotification("Test notification sent!","success",3e3),$()):showNotification(`Test failed: ${u.results?.map(j=>`${j.provider}: ${j.error||"ok"}`).join(", ")}`,"error",5e3)}catch(L){showNotification(`Error: ${L.message}`,"error",3e3)}finally{k.textContent=H,k.disabled=!1}});async function $(){try{const H=await(await fetch("/api/v1/notifications/status")).json();if(H.success&&H.lastSent){const L=document.getElementById("last-notification-sent");L&&(L.textContent=`Last sent: ${new Date(H.lastSent).toLocaleString()}`)}}catch{}}wireModal(C,M)})(),(function(){document.addEventListener("click",y=>{const C=y.target.closest(".panel-tab");if(!C)return;const O=C.dataset.panel;if(!O)return;const E=C.closest(".panel-tabs"),M=E.closest(".weather-modal-content");E.querySelectorAll(".panel-tab").forEach(P=>P.classList.remove("active")),C.classList.add("active"),M.querySelectorAll(".panel-section").forEach(P=>P.classList.remove("active"));const R=M.querySelector("#"+O);R&&R.classList.add("active")})})(),(function(){var y=["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={},t=0;t

\u{1F4BE} Backup & Restore

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

\u2795 Add New Schedule

',U.innerHTML=r,U.querySelectorAll(".schedule-toggle").forEach(function(v){v.addEventListener("change",function(){g(v.dataset.appid,{enabled:v.checked})})}),U.querySelectorAll(".schedule-select").forEach(function(v){v.addEventListener("change",function(){g(v.dataset.appid,{schedule:v.value})})}),U.querySelectorAll(".retention-input").forEach(function(v){v.addEventListener("change",function(){g(v.dataset.appid,{retention:{keep:parseInt(v.value)||7}})})}),U.querySelectorAll(".schedule-run-now").forEach(function(v){v.addEventListener("click",function(){C(v.dataset.appid)})}),U.querySelectorAll(".schedule-delete").forEach(function(v){v.addEventListener("click",function(){f(v.dataset.appid)})}),document.getElementById("add-schedule-btn")?.addEventListener("click",M)}catch(v){U.innerHTML='
Failed to load: '+escapeHtml(v.message)+"
"}}}async function g(e,t){try{var o=await secureFetch("/api/v1/backups/schedule",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({appId:e,...t})}),r=await o.json();r.success?showNotification("Schedule updated for "+e,"success"):(showNotification("Update failed: "+(r.error||"Unknown"),"error"),T())}catch(a){showNotification("Error: "+a.message,"error")}}async function C(e){try{var t=await secureFetch("/api/v1/backups/backup/"+encodeURIComponent(e),{method:"POST",headers:{"Content-Type":"application/json"}}),o=await t.json();o.success?showNotification("Backup started for "+e+"!","success"):showNotification("Backup failed: "+(o.error||"Unknown"),"error")}catch(r){showNotification("Error: "+r.message,"error")}}async function f(e){if(confirm("Remove backup schedule for "+e+"?"))try{var t=await secureFetch("/api/v1/backups/schedule/"+encodeURIComponent(e),{method:"DELETE"}),o=await t.json();o.success?(showNotification("Schedule removed for "+e,"success"),T()):showNotification("Delete failed: "+(o.error||"Unknown"),"error")}catch(r){showNotification("Error: "+r.message,"error")}}async function M(){var e=document.getElementById("new-schedule-appid")?.value?.trim(),t=document.getElementById("new-schedule-interval")?.value||"daily",o=parseInt(document.getElementById("new-schedule-retention")?.value)||7;if(!e){showNotification("Please enter an App ID","warning");return}try{var r=await secureFetch("/api/v1/backups/schedule",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({appId:e,schedule:t,retention:{keep:o},enabled:!0})}),a=await r.json();if(a.success){showNotification("Schedule created for "+e,"success"),T();var s=document.getElementById("new-schedule-appid");s&&(s.value="")}else showNotification("Failed: "+(a.error||"Unknown"),"error")}catch(d){showNotification("Error: "+d.message,"error")}}async function i(){if(D){D.innerHTML='
Loading...
';try{var e=await fetch("/api/v1/backups/files"),t=await e.json();if(!t.success)throw new Error(t.error||"Failed to load");var o=t.files||[];if(o.length===0){D.innerHTML='
\u{1F4BE}
No backup files on disk
Run a backup to create backup files
';return}for(var r={},a=0;a";x+='
';for(var v=Object.keys(r).sort(),j=0;j
'+escapeHtml(d)+' ('+_.length+" backup(s))
";for(var F=0;F<_.length;F++){var s=_[F],G=new Date(s.timestamp).toLocaleString();x+='
'+escapeHtml(s.name)+'
'+s.sizeFormatted+'
'+G+'
'}x+="
"}x+="",D.innerHTML=x,D.querySelectorAll(".disk-compare-btn").forEach(function(J){J.addEventListener("click",function(){u(J.dataset.appid,J.dataset.filename)})}),D.querySelectorAll(".disk-restore-btn").forEach(function(J){J.addEventListener("click",function(){n(J.dataset.appid,J.dataset.filename)})})}catch(J){D.innerHTML='
Failed: '+escapeHtml(J.message)+"
"}}}async function c(){if(I){I.innerHTML='
Loading...
';try{var e=await fetch("/api/v1/backups/history?limit=50"),t=await e.json();if(!t.success||!t.history?.length){I.innerHTML='
\u{1F4CB} No backup history yet
';return}for(var o='
',r=0;r',o+='
',o+=' '+escapeHtml(a.name||"backup")+"",o+='
',o+=' '+escapeHtml(a.status)+"",a.status==="success"&&(o+=' '),o+="
",o+="
",o+='
',o+=" "+new Date(a.timestamp).toLocaleString()+" | "+s+" MB | "+(a.duration?(a.duration/1e3).toFixed(1)+"s":"--"),a.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 t=await secureFetch("/api/v1/backups/restore/"+e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({restoreServices:!0,restoreConfig:!0})}),o=await t.json();o.success?(showNotification("Restore completed successfully!","success"),location.reload()):showNotification("Restore failed: "+(o.error||"Unknown error"),"error")}catch(r){showNotification("Restore error: "+r.message,"error")}},document.querySelector('[data-panel="backup-schedules-tab"]')?.addEventListener("click",T),document.querySelector('[data-panel="backup-disk-tab"]')?.addEventListener("click",i),document.querySelector('[data-panel="backup-pointintime-tab"]')?.addEventListener("click",l),document.querySelector('[data-panel="backup-history-tab"]')?.addEventListener("click",c);async function l(){if(y){try{var e=await fetch("/api/v1/license/status"),t=await e.json();if(t.tier!=="premium"){y.innerHTML=`
\u2B50
Premium Feature
Point-in-time restore requires DashCaddy Premium with auto-backup enabled.
`;return}}catch{}y.innerHTML='
Loading...
';try{var o=await fetch("/api/v1/services"),r=await o.json(),a=r.services||[];if(a.length===0){y.innerHTML='
\u{1F4E6} No apps deployed yet
';return}for(var s='
',y.innerHTML=s,document.getElementById("pit-load-btn")?.addEventListener("click",function(){var x=document.getElementById("pit-app-select")?.value;x&&b(x)})}catch(x){y.innerHTML='
Failed: '+escapeHtml(x.message)+"
"}}}async function b(e){var t=document.getElementById("pit-backups-list");if(t){t.innerHTML='
Loading backups...
';try{var o=await fetch("/api/v1/backups/files/"+encodeURIComponent(e)),r=await o.json();if(!r.success||!r.files||r.files.length===0){t.innerHTML='
\u{1F4BE} No backup files for '+escapeHtml(e)+"
";return}for(var a='
'+r.files.length+' backup(s)
',s=0;s
'+d.sizeFormatted+'
'+x+'
'}a+="",t.innerHTML=a,t.querySelectorAll(".pit-compare-btn").forEach(function(v){v.addEventListener("click",function(){u(v.dataset.appid,v.dataset.filename)})}),t.querySelectorAll(".pit-restore-btn").forEach(function(v){v.addEventListener("click",function(){n(v.dataset.appid,v.dataset.filename)})})}catch(v){t.innerHTML='
Failed: '+escapeHtml(v.message)+"
"}}}async function u(e,t){try{var o=await secureFetch("/api/v1/backups/compare/"+encodeURIComponent(t),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({})}),r=await o.json();if(!r.success){showNotification("Compare failed: "+(r.error||"Unknown"),"error");return}var a=r.diff,s='

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

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

\u2795 Add New Schedule

',j.innerHTML=r,j.querySelectorAll(".schedule-toggle").forEach(function(h){h.addEventListener("change",function(){f(h.dataset.appid,{enabled:h.checked})})}),j.querySelectorAll(".schedule-select").forEach(function(h){h.addEventListener("change",function(){f(h.dataset.appid,{schedule:h.value})})}),j.querySelectorAll(".retention-input").forEach(function(h){h.addEventListener("change",function(){f(h.dataset.appid,{retention:{keep:parseInt(h.value)||7}})})}),j.querySelectorAll(".schedule-run-now").forEach(function(h){h.addEventListener("click",function(){I(h.dataset.appid)})}),j.querySelectorAll(".schedule-delete").forEach(function(h){h.addEventListener("click",function(){b(h.dataset.appid)})}),document.getElementById("add-schedule-btn")?.addEventListener("click",z)}catch(h){j.innerHTML='
Failed to load: '+escapeHtml(h.message)+"
"}}}async function f(e,t){try{var o=await secureFetch("/api/v1/backups/schedule",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({appId:e,...t})}),r=await o.json();r.success?showNotification("Schedule updated for "+e,"success"):(showNotification("Update failed: "+(r.error||"Unknown"),"error"),B())}catch(a){showNotification("Error: "+a.message,"error")}}async function I(e){try{var t=await secureFetch("/api/v1/backups/backup/"+encodeURIComponent(e),{method:"POST",headers:{"Content-Type":"application/json"}}),o=await t.json();o.success?showNotification("Backup started for "+e+"!","success"):showNotification("Backup failed: "+(o.error||"Unknown"),"error")}catch(r){showNotification("Error: "+r.message,"error")}}async function b(e){if(confirm("Remove backup schedule for "+e+"?"))try{var t=await secureFetch("/api/v1/backups/schedule/"+encodeURIComponent(e),{method:"DELETE"}),o=await t.json();o.success?(showNotification("Schedule removed for "+e,"success"),B()):showNotification("Delete failed: "+(o.error||"Unknown"),"error")}catch(r){showNotification("Error: "+r.message,"error")}}async function z(){var e=document.getElementById("new-schedule-appid")?.value?.trim(),t=document.getElementById("new-schedule-interval")?.value||"daily",o=parseInt(document.getElementById("new-schedule-retention")?.value)||7;if(!e){showNotification("Please enter an App ID","warning");return}try{var r=await secureFetch("/api/v1/backups/schedule",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({appId:e,schedule:t,retention:{keep:o},enabled:!0})}),a=await r.json();if(a.success){showNotification("Schedule created for "+e,"success"),B();var s=document.getElementById("new-schedule-appid");s&&(s.value="")}else showNotification("Failed: "+(a.error||"Unknown"),"error")}catch(d){showNotification("Error: "+d.message,"error")}}async function i(){if(N){N.innerHTML='
Loading...
';try{var e=await fetch("/api/v1/backups/files"),t=await e.json();if(!t.success)throw new Error(t.error||"Failed to load");var o=t.files||[];if(o.length===0){N.innerHTML='
\u{1F4BE}
No backup files on disk
Run a backup to create backup files
';return}for(var r={},a=0;a";S+='
';for(var h=Object.keys(r).sort(),U=0;U
'+escapeHtml(d)+' ('+_.length+" backup(s))
";for(var F=0;F<_.length;F++){var s=_[F],W=new Date(s.timestamp).toLocaleString();S+='
'+escapeHtml(s.name)+'
'+s.sizeFormatted+'
'+W+'
'}S+="
"}S+="",N.innerHTML=S,N.querySelectorAll(".disk-compare-btn").forEach(function(J){J.addEventListener("click",function(){m(J.dataset.appid,J.dataset.filename)})}),N.querySelectorAll(".disk-restore-btn").forEach(function(J){J.addEventListener("click",function(){n(J.dataset.appid,J.dataset.filename)})})}catch(J){N.innerHTML='
Failed: '+escapeHtml(J.message)+"
"}}}async function c(){if(v){v.innerHTML='
Loading...
';try{var e=await fetch("/api/v1/backups/history?limit=50"),t=await e.json();if(!t.success||!t.history?.length){v.innerHTML='
\u{1F4CB} No backup history yet
';return}for(var o='
',r=0;r',o+='
',o+=' '+escapeHtml(a.name||"backup")+"",o+='
',o+=' '+escapeHtml(a.status)+"",a.status==="success"&&(o+=' '),o+="
",o+="
",o+='
',o+=" "+new Date(a.timestamp).toLocaleString()+" | "+s+" MB | "+(a.duration?(a.duration/1e3).toFixed(1)+"s":"--"),a.encrypted&&(o+=" | \u{1F512}"),o+="
",o+="
"}o+="",v.innerHTML=o,v.querySelectorAll(".backup-restore-btn").forEach(function(d){d.addEventListener("click",function(){window.__restoreServerBackup(d.dataset.backupId)})})}catch(d){v.innerHTML='
Failed: '+escapeHtml(d.message)+"
"}}}window.__restoreServerBackup=async function(e){if(confirm("Restore from this server backup? This will overwrite current configuration."))try{var t=await secureFetch("/api/v1/backups/restore/"+e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({restoreServices:!0,restoreConfig:!0})}),o=await t.json();o.success?(showNotification("Restore completed successfully!","success"),location.reload()):showNotification("Restore failed: "+(o.error||"Unknown error"),"error")}catch(r){showNotification("Restore error: "+r.message,"error")}},document.querySelector('[data-panel="backup-schedules-tab"]')?.addEventListener("click",B),document.querySelector('[data-panel="backup-disk-tab"]')?.addEventListener("click",i),document.querySelector('[data-panel="backup-pointintime-tab"]')?.addEventListener("click",l),document.querySelector('[data-panel="backup-history-tab"]')?.addEventListener("click",c);async function l(){if(g){try{var e=await fetch("/api/v1/license/status"),t=await e.json();if(t.tier!=="premium"){g.innerHTML=`
\u2B50
Premium Feature
Point-in-time restore requires DashCaddy Premium with auto-backup enabled.
`;return}}catch{}g.innerHTML='
Loading...
';try{var o=await fetch("/api/v1/services"),r=await o.json(),a=r.services||[];if(a.length===0){g.innerHTML='
\u{1F4E6} No apps deployed yet
';return}for(var s='
',g.innerHTML=s,document.getElementById("pit-load-btn")?.addEventListener("click",function(){var S=document.getElementById("pit-app-select")?.value;S&&w(S)})}catch(S){g.innerHTML='
Failed: '+escapeHtml(S.message)+"
"}}}async function w(e){var t=document.getElementById("pit-backups-list");if(t){t.innerHTML='
Loading backups...
';try{var o=await fetch("/api/v1/backups/files/"+encodeURIComponent(e)),r=await o.json();if(!r.success||!r.files||r.files.length===0){t.innerHTML='
\u{1F4BE} No backup files for '+escapeHtml(e)+"
";return}for(var a='
'+r.files.length+' backup(s)
',s=0;s
'+d.sizeFormatted+'
'+S+'
'}a+="",t.innerHTML=a,t.querySelectorAll(".pit-compare-btn").forEach(function(h){h.addEventListener("click",function(){m(h.dataset.appid,h.dataset.filename)})}),t.querySelectorAll(".pit-restore-btn").forEach(function(h){h.addEventListener("click",function(){n(h.dataset.appid,h.dataset.filename)})})}catch(h){t.innerHTML='
Failed: '+escapeHtml(h.message)+"
"}}}async function m(e,t){try{var o=await secureFetch("/api/v1/backups/compare/"+encodeURIComponent(t),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({})}),r=await o.json();if(!r.success){showNotification("Compare failed: "+(r.error||"Unknown"),"error");return}var a=r.diff,s='

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

Size: '+(a.sizeFormatted||"?")+" | Created: "+new Date(a.timestamp).toLocaleString()+"
";if(a.services){var d=a.services.hasChanges?"\u{1F534}":"\u{1F7E2}";s+='
'+d+' Services (backup vs current)
Backup: '+a.services.backupCount+" services | Current: "+a.services.currentCount+" services
",a.services.hasChanges&&(s+='
Services differ \u2014 restoring will replace current configuration
'),s+="
"}if(a.config){var S=a.config.hasChanges?"\u{1F534}":"\u{1F7E2}";s+='
'+S+" Configuration
",a.config.hasChanges?s+='
Configuration differs \u2014 restoring will replace current settings
':s+='
No changes
',s+="
"}s+='
',document.body.insertAdjacentHTML("beforeend",s),document.getElementById("compare-close-btn")?.addEventListener("click",function(){document.getElementById("compare-overlay")?.remove()}),document.getElementById("compare-overlay")?.addEventListener("click",function(h){h.target===this&&this.remove()})}catch(h){showNotification("Compare error: "+h.message,"error")}}async function n(e,t){if(confirm("Restore "+t+" for "+e+`? This will replace current configuration, credentials, and data. Containers will be restarted.`))try{var o=await secureFetch("/api/v1/apps/"+encodeURIComponent(e)+"/revert/"+encodeURIComponent(t),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({restartContainers:!0})}),r=await o.json();r.success?(showNotification(e+" restored to "+t,"success"),setTimeout(function(){location.reload()},1500)):showNotification("Restore failed: "+(r.error||"Unknown"),"error")}catch(a){showNotification("Restore error: "+a.message,"error")}}})(),(function(){injectModal("stats-modal",`
@@ -1209,7 +1209,7 @@ This will replace current configuration, credentials, and data. Containers will
- `);const m=document.getElementById("stats-modal"),E=document.getElementById("container-stats-btn"),O=document.getElementById("stats-cancel"),k=document.getElementById("stats-refresh-btn"),N=document.getElementById("stats-auto-refresh"),R=document.getElementById("stats-container"),z=document.getElementById("stats-aggregated-container"),A=document.getElementById("stats-alerts-container"),p=document.getElementById("stats-last-update");let B=null,h=null;function H(l){if(l===0||!l)return"0 B";const b=1024,u=["B","KB","MB","GB"],n=Math.floor(Math.log(l)/Math.log(b));return parseFloat((l/Math.pow(b,n)).toFixed(1))+" "+u[n]}function w(l){return l<30?"#2ecc71":l<70?"#f39c12":"#e74c3c"}function $(l){return l<50?"#2ecc71":l<80?"#f39c12":"#e74c3c"}async function S(){try{let l=null,b=!1;try{const e=await(await fetch("/api/v1/monitoring/stats")).json();e.success&&e.stats&&(l=e.stats,b=!0,h=e.stats)}catch{}if(!b){const e=await(await fetch("/api/v1/stats/containers")).json();if(e.success&&e.stats){l={};for(const t of e.stats)l[t.name]={name:t.name,current:{cpu:t.cpu,memory:{percent:t.memory.percent,usage:t.memory.used,limit:t.memory.limit,usageMB:Math.round(t.memory.used/1048576),limitMB:Math.round(t.memory.limit/1048576)},network:{rxBytes:t.network.rx,txBytes:t.network.tx,rxMB:(t.network.rx/1048576).toFixed(1),txMB:(t.network.tx/1048576).toFixed(1)},disk:{readMB:0,writeMB:0}},status:t.status};h=l}}if(!l||Object.keys(l).length===0){R.innerHTML='
No running containers found
';return}let u='
';for(const[n,e]of Object.entries(l)){const t=e.current||e,o=t.cpu?.percent||0,r=t.memory?.percent||0,a=w(o),s=$(r),d=t.memory?.usage||t.memory?.used||0,x=t.memory?.limit||0,v=t.network?.rxBytes||t.network?.rx||0,j=t.network?.txBytes||t.network?.tx||0,_=e.aggregated;u+=` +
`);const y=document.getElementById("stats-modal"),C=document.getElementById("container-stats-btn"),O=document.getElementById("stats-cancel"),E=document.getElementById("stats-refresh-btn"),M=document.getElementById("stats-auto-refresh"),R=document.getElementById("stats-container"),P=document.getElementById("stats-aggregated-container"),A=document.getElementById("stats-alerts-container"),p=document.getElementById("stats-last-update");let T=null,x=null;function $(l){if(l===0||!l)return"0 B";const w=1024,m=["B","KB","MB","GB"],n=Math.floor(Math.log(l)/Math.log(w));return parseFloat((l/Math.pow(w,n)).toFixed(1))+" "+m[n]}function k(l){return l<30?"#2ecc71":l<70?"#f39c12":"#e74c3c"}function H(l){return l<50?"#2ecc71":l<80?"#f39c12":"#e74c3c"}async function L(){try{let l=null,w=!1;try{const e=await(await fetch("/api/v1/monitoring/stats")).json();e.success&&e.stats&&(l=e.stats,w=!0,x=e.stats)}catch{}if(!w){const e=await(await fetch("/api/v1/stats/containers")).json();if(e.success&&e.stats){l={};for(const t of e.stats)l[t.name]={name:t.name,current:{cpu:t.cpu,memory:{percent:t.memory.percent,usage:t.memory.used,limit:t.memory.limit,usageMB:Math.round(t.memory.used/1048576),limitMB:Math.round(t.memory.limit/1048576)},network:{rxBytes:t.network.rx,txBytes:t.network.tx,rxMB:(t.network.rx/1048576).toFixed(1),txMB:(t.network.tx/1048576).toFixed(1)},disk:{readMB:0,writeMB:0}},status:t.status};x=l}}if(!l||Object.keys(l).length===0){R.innerHTML='
No running containers found
';return}let m='
';for(const[n,e]of Object.entries(l)){const t=e.current||e,o=t.cpu?.percent||0,r=t.memory?.percent||0,a=k(o),s=H(r),d=t.memory?.usage||t.memory?.used||0,S=t.memory?.limit||0,h=t.network?.rxBytes||t.network?.rx||0,U=t.network?.txBytes||t.network?.tx||0,_=e.aggregated;m+=`
${e.name||n} @@ -1234,19 +1234,19 @@ This will replace current configuration, credentials, and data. Containers will
${r.toFixed(1)}%
-
${H(d)} / ${H(x)}
+
${$(d)} / ${$(S)}
Network
- \u2193 ${H(v)} + \u2193 ${$(h)} / - \u2191 ${H(j)} + \u2191 ${$(U)}
- `}u+="",R.innerHTML=u,p.textContent="Updated: "+new Date().toLocaleTimeString()}catch(l){R.innerHTML=`
\u274C Failed to load stats: ${escapeHtml(l.message)}
`}}async function L(){if(!z)return;const l=h;if(!l||Object.keys(l).length===0){z.innerHTML='
\u{1F4C8}No monitoring data available. Open the Live Stats tab first.
';return}let b='
';for(const[u,n]of Object.entries(l)){const e=n.aggregated;e&&(b+=`
-
${n.name||u}
+
`}m+="
",R.innerHTML=m,p.textContent="Updated: "+new Date().toLocaleTimeString()}catch(l){R.innerHTML=`
\u274C Failed to load stats: ${escapeHtml(l.message)}
`}}async function u(){if(!P)return;const l=x;if(!l||Object.keys(l).length===0){P.innerHTML='
\u{1F4C8}No monitoring data available. Open the Live Stats tab first.
';return}let w='
';for(const[m,n]of Object.entries(l)){const e=n.aggregated;e&&(w+=`
+
${n.name||m}
${e.cpu?.avg?.toFixed(1)||0}%Avg CPU
${e.cpu?.max?.toFixed(1)||0}%Max CPU
@@ -1254,18 +1254,18 @@ 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
`:""} -
`)}b+="
",z.innerHTML=b}async function U(){if(!A)return;A.innerHTML='
Loading alerts...
';const l=h;if(!l||Object.keys(l).length===0){A.innerHTML='
\u{1F514}No containers found. Open the Live Stats tab first.
';return}let b=!1;try{b=(await(await fetch("/api/v1/license/feature/resource-alerts")).json()).available}catch{b=!1}let u=[];try{const s=await(await fetch("/api/v1/monitoring/alerts?limit=50")).json();s.success&&(u=s.history||[])}catch{}let n={};try{const s=await(await fetch("/api/v1/monitoring/alerts/config")).json();s.success&&(n=s.configs||{})}catch{}const t=Object.entries(l).map(([a,s])=>{const d=n[a]||{cpuThreshold:80,memoryThreshold:90,diskIOThreshold:50,autoRestart:!1,enabled:!1};return` + `)}w+="",P.innerHTML=w}async function j(){if(!A)return;A.innerHTML='
Loading alerts...
';const l=x;if(!l||Object.keys(l).length===0){A.innerHTML='
\u{1F514}No containers found. Open the Live Stats tab first.
';return}let w=!1;try{w=(await(await fetch("/api/v1/license/feature/resource-alerts")).json()).available}catch{w=!1}let m=[];try{const s=await(await fetch("/api/v1/monitoring/alerts?limit=50")).json();s.success&&(m=s.history||[])}catch{}let n={};try{const s=await(await fetch("/api/v1/monitoring/alerts/config")).json();s.success&&(n=s.configs||{})}catch{}const t=Object.entries(l).map(([a,s])=>{const d=n[a]||{cpuThreshold:80,memoryThreshold:90,diskIOThreshold:50,autoRestart:!1,enabled:!1};return` ${s.name||a} - - - - + + + + - `}).join(""),o=u.map(a=>{const s=new Date(a.timestamp).toLocaleString(),d=a.notified?"\u2713":"\u2014";return` + `}).join(""),o=m.map(a=>{const s=new Date(a.timestamp).toLocaleString(),d=a.notified?"\u2713":"\u2014";return` ${s} ${a.containerName||a.containerId} @@ -1274,7 +1274,7 @@ This will replace current configuration, credentials, and data. Containers will ${d} ${a.autoRestartTriggered?"\u21BB":""} - `}).join(""),r=b?` + `}).join(""),r=w?`

\u2699\uFE0F Alert Configuration

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

\u{1F3E5} Health Check Dashboard

-
`);const m=document.getElementById("health-modal"),E=document.getElementById("health-check-btn"),O=document.getElementById("health-cancel"),k=document.getElementById("health-refresh-btn"),N=document.getElementById("health-status-container"),R=document.getElementById("health-incidents-container"),z=document.getElementById("health-config-container"),A=document.getElementById("health-last-update"),p=document.getElementById("health-add-btn"),B=document.getElementById("health-config-form"),h=document.getElementById("health-form-title"),H=document.getElementById("health-form-cancel"),w=document.getElementById("health-form-save"),$="dashcaddy-health-settings",S={retentionDays:30,pollingInterval:30,diskUsageThreshold:80},L=document.getElementById("health-global-save"),U=document.getElementById("health-global-reset"),I=document.getElementById("health-global-status"),D=document.getElementById("health-setting-retention"),y=document.getElementById("health-setting-interval"),P=document.getElementById("health-setting-disk-threshold");function T(){try{const t=safeGet($),o=t?JSON.parse(t):{};return Object.assign({},S,o)}catch{return Object.assign({},S)}}function g(){const t=T();D&&(D.value=t.retentionDays),y&&(y.value=t.pollingInterval),P&&(P.value=t.diskUsageThreshold)}function C(){const t={retentionDays:Math.max(1,Math.min(3650,parseInt(D?.value)||S.retentionDays)),pollingInterval:Math.max(5,Math.min(3600,parseInt(y?.value)||S.pollingInterval)),diskUsageThreshold:Math.max(50,Math.min(99,parseInt(P?.value)||S.diskUsageThreshold))};try{safeSet($,JSON.stringify(t)),g(),I&&(I.textContent="Saved \u2713",I.style.color="var(--ok-fg)",setTimeout(()=>{I&&(I.textContent="")},2500)),typeof showNotification=="function"&&showNotification("Global health settings saved","success")}catch(o){I&&(I.textContent="Save failed",I.style.color="var(--bad-fg)"),typeof showNotification=="function"&&showNotification("Failed to save settings: "+o.message,"error")}}function f(){try{safeSet($,JSON.stringify(S))}catch{}g(),I&&(I.textContent="Reset to defaults \u2713",I.style.color="var(--ok-fg)",setTimeout(()=>{I&&(I.textContent="")},2500))}g(),L?.addEventListener("click",C),U?.addEventListener("click",f);let M=null;function i(t){return t>=99.9?"var(--ok-fg)":t>=95?"#f39c12":"var(--bad-fg)"}function c(t){const o={critical:"var(--bad-fg)",high:"#ff6b6b",medium:"#f39c12",low:"var(--muted)"};return`${t}`}async function l(){try{const o=await(await fetch("/api/v1/health-checks/status")).json();if(!o.success||!o.status||Object.keys(o.status).length===0){N.innerHTML='
\u{1F3E5}No health checks configured. Go to the Configure tab to add services.
';return}const r=Object.values(o.status);let a='';a+='',a+='',a+='',a+='';for(const s of r){const d=s.status==="up",x=d?"var(--dot-ok)":"var(--dot-bad)",v=s.uptime?.["24h"]??"-",j=s.uptime?.["7d"]??"-",_=s.avgResponseTime!=null?Math.round(s.avgResponseTime)+"ms":"-",F=s.timestamp?timeAgo(s.timestamp):"-";a+=``,a+=``,a+=``,a+=``,a+=``,a+=``,a+=``,a+="",a+=``}a+="
ServiceStatusUptime 24hUptime 7dAvg ResponseLast Check
${escapeHtml(s.name||s.serviceId)}${d?"Up":"Down"}${typeof v=="number"?v.toFixed(1)+"%":v}${typeof j=="number"?j.toFixed(1)+"%":j}${_}${F}
",N.innerHTML=a,A.textContent="Updated "+new Date().toLocaleTimeString(),N.querySelectorAll("tr[data-health-id]").forEach(s=>{s.addEventListener("click",async()=>{const d=s.dataset.healthId,x=document.getElementById("health-detail-"+d);if(x){if(x.style.display!=="none"){x.style.display="none";return}x.style.display="";try{const j=await(await fetch(`/api/v1/health-checks/${d}/stats?hours=24`)).json();if(j.success&&j.stats){const _=j.stats,F=_.responseTime||{};x.querySelector("td").innerHTML=` + `);const y=document.getElementById("health-modal"),C=document.getElementById("health-check-btn"),O=document.getElementById("health-cancel"),E=document.getElementById("health-refresh-btn"),M=document.getElementById("health-status-container"),R=document.getElementById("health-incidents-container"),P=document.getElementById("health-config-container"),A=document.getElementById("health-last-update"),p=document.getElementById("health-add-btn"),T=document.getElementById("health-config-form"),x=document.getElementById("health-form-title"),$=document.getElementById("health-form-cancel"),k=document.getElementById("health-form-save"),H="dashcaddy-health-settings",L={retentionDays:30,pollingInterval:30,diskUsageThreshold:80},u=document.getElementById("health-global-save"),j=document.getElementById("health-global-reset"),v=document.getElementById("health-global-status"),N=document.getElementById("health-setting-retention"),g=document.getElementById("health-setting-interval"),D=document.getElementById("health-setting-disk-threshold");function B(){try{const t=safeGet(H),o=t?JSON.parse(t):{};return Object.assign({},L,o)}catch{return Object.assign({},L)}}function f(){const t=B();N&&(N.value=t.retentionDays),g&&(g.value=t.pollingInterval),D&&(D.value=t.diskUsageThreshold)}function I(){const t={retentionDays:Math.max(1,Math.min(3650,parseInt(N?.value)||L.retentionDays)),pollingInterval:Math.max(5,Math.min(3600,parseInt(g?.value)||L.pollingInterval)),diskUsageThreshold:Math.max(50,Math.min(99,parseInt(D?.value)||L.diskUsageThreshold))};try{safeSet(H,JSON.stringify(t)),f(),v&&(v.textContent="Saved \u2713",v.style.color="var(--ok-fg)",setTimeout(()=>{v&&(v.textContent="")},2500)),typeof showNotification=="function"&&showNotification("Global health settings saved","success")}catch(o){v&&(v.textContent="Save failed",v.style.color="var(--bad-fg)"),typeof showNotification=="function"&&showNotification("Failed to save settings: "+o.message,"error")}}function b(){try{safeSet(H,JSON.stringify(L))}catch{}f(),v&&(v.textContent="Reset to defaults \u2713",v.style.color="var(--ok-fg)",setTimeout(()=>{v&&(v.textContent="")},2500))}f(),u?.addEventListener("click",I),j?.addEventListener("click",b);let z=null;function i(t){return t>=99.9?"var(--ok-fg)":t>=95?"#f39c12":"var(--bad-fg)"}function c(t){const o={critical:"var(--bad-fg)",high:"#ff6b6b",medium:"#f39c12",low:"var(--muted)"};return`${t}`}async function l(){try{const o=await(await fetch("/api/v1/health-checks/status")).json();if(!o.success||!o.status||Object.keys(o.status).length===0){M.innerHTML='
\u{1F3E5}No health checks configured. Go to the Configure tab to add services.
';return}const r=Object.values(o.status);let a='';a+='',a+='',a+='',a+='';for(const s of r){const d=s.status==="up",S=d?"var(--dot-ok)":"var(--dot-bad)",h=s.uptime?.["24h"]??"-",U=s.uptime?.["7d"]??"-",_=s.avgResponseTime!=null?Math.round(s.avgResponseTime)+"ms":"-",F=s.timestamp?timeAgo(s.timestamp):"-";a+=``,a+=``,a+=``,a+=``,a+=``,a+=``,a+=``,a+="",a+=``}a+="
ServiceStatusUptime 24hUptime 7dAvg ResponseLast Check
${escapeHtml(s.name||s.serviceId)}${d?"Up":"Down"}${typeof h=="number"?h.toFixed(1)+"%":h}${typeof U=="number"?U.toFixed(1)+"%":U}${_}${F}
",M.innerHTML=a,A.textContent="Updated "+new Date().toLocaleTimeString(),M.querySelectorAll("tr[data-health-id]").forEach(s=>{s.addEventListener("click",async()=>{const d=s.dataset.healthId,S=document.getElementById("health-detail-"+d);if(S){if(S.style.display!=="none"){S.style.display="none";return}S.style.display="";try{const U=await(await fetch(`/api/v1/health-checks/${d}/stats?hours=24`)).json();if(U.success&&U.stats){const _=U.stats,F=_.responseTime||{};S.querySelector("td").innerHTML=`
Total Checks
${_.totalChecks||0}
Uptime
${(_.uptime||0).toFixed(2)}%
@@ -1465,14 +1465,14 @@ This will replace current configuration, credentials, and data. Containers will
Max Response
${Math.round(F.max||0)}ms
Up Checks
${_.upChecks||0}
Down Checks
${_.downChecks||0}
-
`}else x.querySelector("td").innerHTML='
No detailed stats available for this period.
'}catch(v){x.querySelector("td").innerHTML=`
Failed: ${escapeHtml(v.message)}
`}}})})}catch(t){N.innerHTML=`
Failed to load health status: ${escapeHtml(t.message)}
`}}async function b(){try{const[t,o]=await Promise.all([fetch("/api/v1/health-checks/incidents"),fetch("/api/v1/health-checks/incidents/history?limit=50")]),r=await t.json(),a=await o.json();let s="";const d=r.success&&r.incidents?r.incidents:[];if(d.length>0){s+='

Open Incidents ('+d.length+")

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

Open Incidents ('+d.length+")

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

Incident History

',s+='',s+='';for(const v of x){const j=v.status==="resolved",_=j&&v.duration?v.duration<6e4?Math.round(v.duration/1e3)+"s":Math.round(v.duration/6e4)+"m":"-";s+='',s+=``,s+=``,s+=``,s+=``,s+=``,s+=``,s+=""}s+="
ServiceTypeSeverityStatusDurationWhen
${escapeHtml(v.serviceId)}${escapeHtml(v.type)}${c(v.severity)}${v.status}${_}${timeAgo(v.createdAt)}
"}R.innerHTML=s||'
\u{1F6A8}No incidents recorded yet.
'}catch(t){R.innerHTML=`
Failed: ${escapeHtml(t.message)}
`}}async function u(){try{const o=await(await fetch("/api/v1/health-checks/status")).json(),r=o.success&&o.status?Object.values(o.status):[];if(r.length===0){z.innerHTML='
\u2699\uFE0FNo health checks configured yet. Click "Add Health Check" below.
';return}let a='';a+='';for(const s of r){const d=s.status==="up";a+='',a+=``,a+=``,a+=``,a+='"}a+="
ServiceStatusSLA TargetActions
${escapeHtml(s.name||s.serviceId)}${d?"Up":"Down"}${s.sla?.target?s.sla.target+"%":"-"}',a+=``,a+=``,a+="
",z.innerHTML=a}catch(t){z.innerHTML=`
Failed: ${escapeHtml(t.message)}
`}}function n(t,o,r,a,s,d,x){M=t||null,h.textContent=t?"Edit Health Check":"Add Health Check",document.getElementById("health-form-id").value=t||"",document.getElementById("health-form-id").disabled=!!t,document.getElementById("health-form-name").value=o||"",document.getElementById("health-form-url").value=r||"",document.getElementById("health-form-timeout").value=a||1e4,document.getElementById("health-form-codes").value=s||"200",document.getElementById("health-form-sla").value=d||99.9,document.getElementById("health-form-slow").value=x||5e3,B.style.display="",p.style.display="none"}function e(){B.style.display="none",p.style.display="",M=null}p?.addEventListener("click",()=>n("","","",1e4,"200",99.9,5e3)),H?.addEventListener("click",e),w?.addEventListener("click",async()=>{const t=M||document.getElementById("health-form-id").value.trim();if(!t)return showNotification("Service ID is required","warning");const o=document.getElementById("health-form-url").value.trim();if(!o)return showNotification("URL is required","warning");const r=document.getElementById("health-form-codes").value.split(",").map(s=>parseInt(s.trim())).filter(Boolean),a={name:document.getElementById("health-form-name").value.trim()||t,url:o,timeout:parseInt(document.getElementById("health-form-timeout").value)||1e4,expectedStatusCodes:r.length?r:[200],sla:{target:parseFloat(document.getElementById("health-form-sla").value)||99.9},slowResponseThreshold:parseInt(document.getElementById("health-form-slow").value)||5e3};try{w.textContent="Saving...",w.disabled=!0;const d=await(await secureFetch(`/api/v1/health-checks/${encodeURIComponent(t)}/configure`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(a)})).json();if(!d.success)throw new Error(d.error||"Save failed");e(),u(),l()}catch(s){showNotification("Error: "+s.message,"error")}finally{w.textContent="Save",w.disabled=!1}}),document.addEventListener("health-edit",async t=>{const o=t.detail;n(o,"","",1e4,"200",99.9,5e3)}),document.addEventListener("health-delete",async t=>{const o=t.detail;if(confirm(`Delete health check for "${o}"?`))try{const a=await(await secureFetch(`/api/v1/health-checks/${encodeURIComponent(o)}/configure`,{method:"DELETE"})).json();if(!a.success)throw new Error(a.error);u(),l()}catch(r){showNotification("Error: "+r.message,"error")}}),E?.addEventListener("click",()=>{m?.classList.add("show"),l()}),wireModal(m,O),k?.addEventListener("click",l),document.querySelector('[data-panel="health-incidents"]')?.addEventListener("click",b),document.querySelector('[data-panel="health-config"]')?.addEventListener("click",u)})(),(function(){injectModal("updates-modal",`
+
${escapeHtml(h.message)}
+
Started ${timeAgo(h.createdAt)} \xB7 ${h.occurrences||1} occurrence(s)
+
`;s+="
"}else s+='
All services operational \u2014 no open incidents
';const S=a.success&&a.history?a.history:[];if(S.length>0){s+='

Incident History

',s+='',s+='';for(const h of S){const U=h.status==="resolved",_=U&&h.duration?h.duration<6e4?Math.round(h.duration/1e3)+"s":Math.round(h.duration/6e4)+"m":"-";s+='',s+=``,s+=``,s+=``,s+=``,s+=``,s+=``,s+=""}s+="
ServiceTypeSeverityStatusDurationWhen
${escapeHtml(h.serviceId)}${escapeHtml(h.type)}${c(h.severity)}${h.status}${_}${timeAgo(h.createdAt)}
"}R.innerHTML=s||'
\u{1F6A8}No incidents recorded yet.
'}catch(t){R.innerHTML=`
Failed: ${escapeHtml(t.message)}
`}}async function m(){try{const o=await(await fetch("/api/v1/health-checks/status")).json(),r=o.success&&o.status?Object.values(o.status):[];if(r.length===0){P.innerHTML='
\u2699\uFE0FNo health checks configured yet. Click "Add Health Check" below.
';return}let a='';a+='';for(const s of r){const d=s.status==="up";a+='',a+=``,a+=``,a+=``,a+='"}a+="
ServiceStatusSLA TargetActions
${escapeHtml(s.name||s.serviceId)}${d?"Up":"Down"}${s.sla?.target?s.sla.target+"%":"-"}',a+=``,a+=``,a+="
",P.innerHTML=a}catch(t){P.innerHTML=`
Failed: ${escapeHtml(t.message)}
`}}function n(t,o,r,a,s,d,S){z=t||null,x.textContent=t?"Edit Health Check":"Add Health Check",document.getElementById("health-form-id").value=t||"",document.getElementById("health-form-id").disabled=!!t,document.getElementById("health-form-name").value=o||"",document.getElementById("health-form-url").value=r||"",document.getElementById("health-form-timeout").value=a||1e4,document.getElementById("health-form-codes").value=s||"200",document.getElementById("health-form-sla").value=d||99.9,document.getElementById("health-form-slow").value=S||5e3,T.style.display="",p.style.display="none"}function e(){T.style.display="none",p.style.display="",z=null}p?.addEventListener("click",()=>n("","","",1e4,"200",99.9,5e3)),$?.addEventListener("click",e),k?.addEventListener("click",async()=>{const t=z||document.getElementById("health-form-id").value.trim();if(!t)return showNotification("Service ID is required","warning");const o=document.getElementById("health-form-url").value.trim();if(!o)return showNotification("URL is required","warning");const r=document.getElementById("health-form-codes").value.split(",").map(s=>parseInt(s.trim())).filter(Boolean),a={name:document.getElementById("health-form-name").value.trim()||t,url:o,timeout:parseInt(document.getElementById("health-form-timeout").value)||1e4,expectedStatusCodes:r.length?r:[200],sla:{target:parseFloat(document.getElementById("health-form-sla").value)||99.9},slowResponseThreshold:parseInt(document.getElementById("health-form-slow").value)||5e3};try{k.textContent="Saving...",k.disabled=!0;const d=await(await secureFetch(`/api/v1/health-checks/${encodeURIComponent(t)}/configure`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(a)})).json();if(!d.success)throw new Error(d.error||"Save failed");e(),m(),l()}catch(s){showNotification("Error: "+s.message,"error")}finally{k.textContent="Save",k.disabled=!1}}),document.addEventListener("health-edit",async t=>{const o=t.detail;n(o,"","",1e4,"200",99.9,5e3)}),document.addEventListener("health-delete",async t=>{const o=t.detail;if(confirm(`Delete health check for "${o}"?`))try{const a=await(await secureFetch(`/api/v1/health-checks/${encodeURIComponent(o)}/configure`,{method:"DELETE"})).json();if(!a.success)throw new Error(a.error);m(),l()}catch(r){showNotification("Error: "+r.message,"error")}}),C?.addEventListener("click",()=>{y?.classList.add("show"),l()}),wireModal(y,O),E?.addEventListener("click",l),document.querySelector('[data-panel="health-incidents"]')?.addEventListener("click",w),document.querySelector('[data-panel="health-config"]')?.addEventListener("click",m)})(),(function(){injectModal("updates-modal",`

\u2B06\uFE0F Update Management

- `);const m=document.getElementById("updates-modal"),E=document.getElementById("updates-btn"),O=document.getElementById("updates-cancel"),k=document.getElementById("updates-check-btn"),N=document.getElementById("updates-available-container"),R=document.getElementById("updates-history-container"),z=document.getElementById("updates-auto-container"),A=document.getElementById("updates-last-check");async function p(){try{const n=await(await fetch("/api/v1/updates/available")).json();if(!n.success)throw new Error(n.error);const e=n.updates||[];if(e.length===0){N.innerHTML='
\u2705All containers are up to date.
',A.textContent="",document.getElementById("updates-update-all-btn").style.display="none",document.getElementById("updates-count-badge").style.display="none",window._pendingUpdates=[];return}let t='';t+='';for(const a of e){const s=(()=>{const d=window.APPS||[];for(const x of d)if(x.containerId===a.containerId||x.name===a.containerName||x.id===a.containerName)return x.id;return a.containerName})();t+=``,t+=``,t+=``,t+=``,t+=``,t+='"}t+="
ContainerImageCurrentLatestActions
${escapeHtml(a.containerName)}${escapeHtml(a.imageName)}${escapeHtml(a.currentDigest)}${escapeHtml(a.latestDigest)}',t+=``,t+=``,t+="
",N.innerHTML=t,A.textContent=e.length+" update(s) available";const o=document.getElementById("updates-count-badge"),r=document.getElementById("updates-update-all-btn");o&&(o.textContent=e.length+" pending",o.style.display=""),r&&e.length>0&&(r.style.display=""),window._pendingUpdates=e,N.querySelectorAll(".update-now-btn").forEach(a=>{a.addEventListener("click",async()=>{const s=a.dataset.id,d=a.dataset.name;if(confirm(`Update "${d}" to the latest version? The container will restart.`)){a.textContent="Updating...",a.disabled=!0;try{const v=await(await secureFetch(`/api/v1/updates/update/${encodeURIComponent(s)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({autoRollback:!0})})).json();if(v.success)a.textContent="Done!",a.style.background="var(--ok-fg)",setTimeout(()=>p(),2e3);else throw new Error(v.error||"Update failed")}catch(x){a.textContent="Failed",a.style.color="var(--bad-fg)",showNotification("Update error: "+x.message,"error"),setTimeout(()=>{a.textContent="Update",a.disabled=!1,a.style.color="",a.style.background=""},3e3)}}})}),N.querySelectorAll(".rollback-btn").forEach(a=>{a.addEventListener("click",async()=>{const s=a.dataset.id,d=a.dataset.name;if(confirm(`Rollback "${d}" to its previous version?`)){a.textContent="Rolling back...",a.disabled=!0;try{const v=await(await secureFetch(`/api/v1/updates/rollback/${encodeURIComponent(s)}`,{method:"POST"})).json();if(v.success)a.textContent="Rolled back!",setTimeout(()=>p(),2e3);else throw new Error(v.error||"Rollback failed")}catch(x){a.textContent="Failed",showNotification("Rollback error: "+x.message,"error"),setTimeout(()=>{a.textContent="Rollback",a.disabled=!1},3e3)}}})})}catch(u){N.innerHTML=`
Failed: ${escapeHtml(u.message)}
`}}async function B(){const u=window._pendingUpdates||[];if(!u.length)return;const n=document.getElementById("updates-update-all-btn");if(!confirm(`Update all ${u.length} containers? Each will restart.`))return;n.textContent="\u23F3 Updating...",n.disabled=!0;let e=0,t=0;for(const o of u)try{(await(await secureFetch(`/api/v1/updates/update/${encodeURIComponent(o.containerId)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({autoRollback:!0})})).json()).success?e++:t++}catch{t++}n.textContent="\u2705 Done",showNotification(`Update all: ${e} succeeded, ${t} failed.`,e>0&&t===0?"success":"error"),setTimeout(()=>{n.textContent="\u2B06\uFE0F Update All",n.disabled=!1,p()},3e3)}document.getElementById("updates-update-all-btn")?.addEventListener("click",B);async function h(){k.textContent="\u{1F50D} Checking...",k.disabled=!0;try{const n=await(await secureFetch("/api/v1/updates/check",{method:"POST"})).json();if(!n.success)throw new Error(n.error);k.textContent="\u2705 Done!",await p()}catch(u){k.textContent="\u274C Failed",showNotification("Check error: "+u.message,"error")}setTimeout(()=>{k.textContent="\u{1F50D} Check for Updates",k.disabled=!1},3e3)}async function H(){try{R.innerHTML='
Loading...
';const n=await(await fetch("/api/v1/updates/history?limit=50")).json(),e=n.success&&n.history?n.history:[];if(e.length===0){R.innerHTML='
\u{1F4CB}No update history yet.
';return}let t='';t+='';for(const o of e){const r=o.status==="success",a=o.duration?o.duration<1e3?o.duration+"ms":Math.round(o.duration/1e3)+"s":"-";t+='',t+=``,t+=``,t+=``,t+=``,t+=``,t+="",!r&&o.error&&(t+=``)}t+="
WhenContainerImageDurationStatus
${timeAgo(o.timestamp)}${escapeHtml(o.containerName)}${escapeHtml(o.imageName)}${a}${r?"\u2713 success":"\u2717 failed"}
${escapeHtml(o.error)}
",R.innerHTML=t}catch(u){R.innerHTML=`
Failed: ${escapeHtml(u.message)}
`}}async function w(){try{z.innerHTML='
Loading...
';const[u,n]=await Promise.all([fetch("/api/v1/stats/containers"),fetch("/api/v1/updates/auto-update")]),e=await u.json(),t=await n.json(),o=e.success&&e.stats?e.stats:[],r=t.success&&t.config?t.config:{};if(o.length===0){z.innerHTML='
\u{1F916}No running containers found.
';return}let a='
Auto-updates run during maintenance window (default 2AM-4AM). Daily = every day, Weekly = Sundays, Monthly = 1st of month.
';a+='',a+='';for(const s of o){const d=s.name||s.Names?.[0]?.replace(/^\//,"")||s.Id?.substring(0,12),x=s.containerId||s.Id,v=r[x]||{},j=v.enabled?v.schedule||"weekly":"",_=v.autoRollback!==!1,F=v.maintenanceWindow||"",G=v.lastAutoUpdate?timeAgo(v.lastAutoUpdate):"Never";a+=``,a+=``,a+=``,a+=``,a+=``,a+=``,a+=``,a+=""}a+="
ContainerScheduleWindowRollbackLast RunActions
${escapeHtml(d)} - ${G}
",z.innerHTML=a,z.querySelectorAll(".save-auto-btn").forEach(s=>{s.addEventListener("click",async()=>{const d=s.dataset.id,x=s.closest("tr"),v=x.querySelector(".auto-schedule").value,j=x.querySelector(".auto-rollback").checked,_=x.querySelector(".auto-window").value.trim();s.textContent="Saving...",s.disabled=!0;try{const G=await(await secureFetch(`/api/v1/updates/auto-update/${encodeURIComponent(d)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({enabled:!!v,schedule:v||"weekly",autoRollback:j,maintenanceWindow:_||void 0})})).json();if(G.success)s.textContent="\u2713 Saved";else throw new Error(G.error)}catch(F){s.textContent="\u2717 Error",showNotification("Save error: "+F.message,"error")}setTimeout(()=>{s.textContent="Save",s.disabled=!1},2e3)})})}catch(u){z.innerHTML=`
Failed: ${escapeHtml(u.message)}
`}}const $=document.getElementById("dashcaddy-current-version"),S=document.getElementById("dashcaddy-update-badge"),L=document.getElementById("dashcaddy-update-details"),U=document.getElementById("dashcaddy-new-version"),I=document.getElementById("dashcaddy-changelog"),D=document.getElementById("dashcaddy-apply-btn"),y=document.getElementById("dashcaddy-check-btn"),P=document.getElementById("dashcaddy-rollback-btn"),T=document.getElementById("dashcaddy-status-bar"),g=document.getElementById("dashcaddy-history-container");let C=null;function f(u,n){T&&(T.style.display="block",T.style.background=n==="error"?"var(--bad-bg)":n==="success"?"var(--ok-bg)":"var(--bg)",T.style.color=n==="error"?"var(--bad-fg)":n==="success"?"var(--ok-fg)":"var(--fg)",T.textContent=u)}async function M(){try{const n=await(await fetch("/api/v1/system/version")).json();if(n.success){const e=n.commit&&n.commit!=="unknown"?n.commit:null;$.textContent="v"+n.version+(e?" ("+e.substring(0,7)+")":"")}}catch{$.textContent="Unable to fetch version"}}async function i(u){u||(y.textContent="Checking...",y.disabled=!0);try{const e=await(await fetch("/api/v1/system/update-check")).json();if(C=e,e.success&&e.available&&e.remote){S.style.display="",L.style.display="",U.textContent="v"+e.remote.version,I.textContent=e.remote.changelog||"No changelog available.";const t=document.getElementById("updates-btn");if(t&&!t.querySelector(".update-dot")){const r=document.createElement("span");r.className="update-dot",r.style.cssText="position:absolute;top:2px;right:2px;width:8px;height:8px;border-radius:50%;background:var(--accent);",t.style.position="relative",t.appendChild(r)}const o=document.getElementById("updates-dashcaddy-tab");if(o&&!o.querySelector(".update-dot")){const r=document.createElement("span");r.className="update-dot",r.style.cssText="display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--accent);margin-left:4px;vertical-align:middle;",o.appendChild(r)}}else S.style.display="none",L.style.display="none",await M(),u||f("You are running the latest version.","success");u||(y.textContent="Check for Updates",y.disabled=!1)}catch(n){u||(f("Failed to check: "+n.message,"error"),y.textContent="Check for Updates",y.disabled=!1)}}async function c(){if(!confirm("Apply DashCaddy update? The API container will restart."))return!1;D.textContent="Updating...",D.disabled=!0,f("Downloading and applying update...","info");try{const n=await(await secureFetch("/api/v1/system/update-apply",{method:"POST"})).json();if(n.success)return f("Update initiated: v"+(n.fromVersion||"?")+" \u2192 v"+(n.toVersion||"?")+". The container will restart shortly.","success"),D.textContent="Applied!",document.querySelectorAll(".update-dot").forEach(e=>e.remove()),!0;throw new Error(n.error||"Update failed")}catch(u){throw f("Update failed: "+u.message,"error"),D.textContent="Update Now",D.disabled=!1,u}}async function l(){try{const n=await(await fetch("/api/v1/system/update-history")).json(),e=n.success&&n.history?n.history:[];if(e.length===0){g.innerHTML='
\u{1F4E6}No self-update history.
';return}let t='';t+='';for(const o of e){const r=o.status==="success"?"\u2713 success":o.status==="pending"?"\u23F3 pending":o.status==="partial"?"\u26A0 partial":"\u2717 "+o.status,a=o.status==="success"?"var(--ok-fg)":o.status==="pending"?"var(--muted)":"var(--bad-fg)";t+='',t+='",t+='",t+='",t+='",t+="",o.error&&(t+='"),o.note&&(t+='")}t+="
WhenVersionFromStatus
'+timeAgo(o.timestamp)+"v'+escapeHtml(o.version)+(o.rollback?" (rollback)":"")+"v'+escapeHtml(o.fromVersion||"?")+"'+r+"
'+escapeHtml(o.error)+"
'+escapeHtml(o.note)+"
",g.innerHTML=t}catch(u){g.innerHTML='
Failed: '+escapeHtml(u.message)+"
"}}async function b(){try{const n=await(await fetch("/api/v1/system/rollback-versions")).json(),e=n.success&&n.versions?n.versions:[];if(e.length===0){showNotification("No rollback versions available.","info");return}const t=prompt(`Available rollback versions: + `);const y=document.getElementById("updates-modal"),C=document.getElementById("updates-btn"),O=document.getElementById("updates-cancel"),E=document.getElementById("updates-check-btn"),M=document.getElementById("updates-available-container"),R=document.getElementById("updates-history-container"),P=document.getElementById("updates-auto-container"),A=document.getElementById("updates-last-check");async function p(){try{const n=await(await fetch("/api/v1/updates/available")).json();if(!n.success)throw new Error(n.error);const e=n.updates||[];if(e.length===0){M.innerHTML='
\u2705All containers are up to date.
',A.textContent="",document.getElementById("updates-update-all-btn").style.display="none",document.getElementById("updates-count-badge").style.display="none",window._pendingUpdates=[];return}let t='';t+='';for(const a of e){const s=(()=>{const d=window.APPS||[];for(const S of d)if(S.containerId===a.containerId||S.name===a.containerName||S.id===a.containerName)return S.id;return a.containerName})();t+=``,t+=``,t+=``,t+=``,t+=``,t+='"}t+="
ContainerImageCurrentLatestActions
${escapeHtml(a.containerName)}${escapeHtml(a.imageName)}${escapeHtml(a.currentDigest)}${escapeHtml(a.latestDigest)}',t+=``,t+=``,t+="
",M.innerHTML=t,A.textContent=e.length+" update(s) available";const o=document.getElementById("updates-count-badge"),r=document.getElementById("updates-update-all-btn");o&&(o.textContent=e.length+" pending",o.style.display=""),r&&e.length>0&&(r.style.display=""),window._pendingUpdates=e,M.querySelectorAll(".update-now-btn").forEach(a=>{a.addEventListener("click",async()=>{const s=a.dataset.id,d=a.dataset.name;if(confirm(`Update "${d}" to the latest version? The container will restart.`)){a.textContent="Updating...",a.disabled=!0;try{const h=await(await secureFetch(`/api/v1/updates/update/${encodeURIComponent(s)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({autoRollback:!0})})).json();if(h.success)a.textContent="Done!",a.style.background="var(--ok-fg)",setTimeout(()=>p(),2e3);else throw new Error(h.error||"Update failed")}catch(S){a.textContent="Failed",a.style.color="var(--bad-fg)",showNotification("Update error: "+S.message,"error"),setTimeout(()=>{a.textContent="Update",a.disabled=!1,a.style.color="",a.style.background=""},3e3)}}})}),M.querySelectorAll(".rollback-btn").forEach(a=>{a.addEventListener("click",async()=>{const s=a.dataset.id,d=a.dataset.name;if(confirm(`Rollback "${d}" to its previous version?`)){a.textContent="Rolling back...",a.disabled=!0;try{const h=await(await secureFetch(`/api/v1/updates/rollback/${encodeURIComponent(s)}`,{method:"POST"})).json();if(h.success)a.textContent="Rolled back!",setTimeout(()=>p(),2e3);else throw new Error(h.error||"Rollback failed")}catch(S){a.textContent="Failed",showNotification("Rollback error: "+S.message,"error"),setTimeout(()=>{a.textContent="Rollback",a.disabled=!1},3e3)}}})})}catch(m){M.innerHTML=`
Failed: ${escapeHtml(m.message)}
`}}async function T(){const m=window._pendingUpdates||[];if(!m.length)return;const n=document.getElementById("updates-update-all-btn");if(!confirm(`Update all ${m.length} containers? Each will restart.`))return;n.textContent="\u23F3 Updating...",n.disabled=!0;let e=0,t=0;for(const o of m)try{(await(await secureFetch(`/api/v1/updates/update/${encodeURIComponent(o.containerId)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({autoRollback:!0})})).json()).success?e++:t++}catch{t++}n.textContent="\u2705 Done",showNotification(`Update all: ${e} succeeded, ${t} failed.`,e>0&&t===0?"success":"error"),setTimeout(()=>{n.textContent="\u2B06\uFE0F Update All",n.disabled=!1,p()},3e3)}document.getElementById("updates-update-all-btn")?.addEventListener("click",T);async function x(){E.textContent="\u{1F50D} Checking...",E.disabled=!0;try{const n=await(await secureFetch("/api/v1/updates/check",{method:"POST"})).json();if(!n.success)throw new Error(n.error);E.textContent="\u2705 Done!",await p()}catch(m){E.textContent="\u274C Failed",showNotification("Check error: "+m.message,"error")}setTimeout(()=>{E.textContent="\u{1F50D} Check for Updates",E.disabled=!1},3e3)}async function $(){try{R.innerHTML='
Loading...
';const n=await(await fetch("/api/v1/updates/history?limit=50")).json(),e=n.success&&n.history?n.history:[];if(e.length===0){R.innerHTML='
\u{1F4CB}No update history yet.
';return}let t='';t+='';for(const o of e){const r=o.status==="success",a=o.duration?o.duration<1e3?o.duration+"ms":Math.round(o.duration/1e3)+"s":"-";t+='',t+=``,t+=``,t+=``,t+=``,t+=``,t+="",!r&&o.error&&(t+=``)}t+="
WhenContainerImageDurationStatus
${timeAgo(o.timestamp)}${escapeHtml(o.containerName)}${escapeHtml(o.imageName)}${a}${r?"\u2713 success":"\u2717 failed"}
${escapeHtml(o.error)}
",R.innerHTML=t}catch(m){R.innerHTML=`
Failed: ${escapeHtml(m.message)}
`}}async function k(){try{P.innerHTML='
Loading...
';const[m,n]=await Promise.all([fetch("/api/v1/stats/containers"),fetch("/api/v1/updates/auto-update")]),e=await m.json(),t=await n.json(),o=e.success&&e.stats?e.stats:[],r=t.success&&t.config?t.config:{};if(o.length===0){P.innerHTML='
\u{1F916}No running containers found.
';return}let a='
Auto-updates run during maintenance window (default 2AM-4AM). Daily = every day, Weekly = Sundays, Monthly = 1st of month.
';a+='',a+='';for(const s of o){const d=s.name||s.Names?.[0]?.replace(/^\//,"")||s.Id?.substring(0,12),S=s.containerId||s.Id,h=r[S]||{},U=h.enabled?h.schedule||"weekly":"",_=h.autoRollback!==!1,F=h.maintenanceWindow||"",W=h.lastAutoUpdate?timeAgo(h.lastAutoUpdate):"Never";a+=``,a+=``,a+=``,a+=``,a+=``,a+=``,a+=``,a+=""}a+="
ContainerScheduleWindowRollbackLast RunActions
${escapeHtml(d)} + ${W}
",P.innerHTML=a,P.querySelectorAll(".save-auto-btn").forEach(s=>{s.addEventListener("click",async()=>{const d=s.dataset.id,S=s.closest("tr"),h=S.querySelector(".auto-schedule").value,U=S.querySelector(".auto-rollback").checked,_=S.querySelector(".auto-window").value.trim();s.textContent="Saving...",s.disabled=!0;try{const W=await(await secureFetch(`/api/v1/updates/auto-update/${encodeURIComponent(d)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({enabled:!!h,schedule:h||"weekly",autoRollback:U,maintenanceWindow:_||void 0})})).json();if(W.success)s.textContent="\u2713 Saved";else throw new Error(W.error)}catch(F){s.textContent="\u2717 Error",showNotification("Save error: "+F.message,"error")}setTimeout(()=>{s.textContent="Save",s.disabled=!1},2e3)})})}catch(m){P.innerHTML=`
Failed: ${escapeHtml(m.message)}
`}}const H=document.getElementById("dashcaddy-current-version"),L=document.getElementById("dashcaddy-update-badge"),u=document.getElementById("dashcaddy-update-details"),j=document.getElementById("dashcaddy-new-version"),v=document.getElementById("dashcaddy-changelog"),N=document.getElementById("dashcaddy-apply-btn"),g=document.getElementById("dashcaddy-check-btn"),D=document.getElementById("dashcaddy-rollback-btn"),B=document.getElementById("dashcaddy-status-bar"),f=document.getElementById("dashcaddy-history-container");let I=null;function b(m,n){B&&(B.style.display="block",B.style.background=n==="error"?"var(--bad-bg)":n==="success"?"var(--ok-bg)":"var(--bg)",B.style.color=n==="error"?"var(--bad-fg)":n==="success"?"var(--ok-fg)":"var(--fg)",B.textContent=m)}async function z(){try{const n=await(await fetch("/api/v1/system/version")).json();if(n.success){const e=n.commit&&n.commit!=="unknown"?n.commit:null;H.textContent="v"+n.version+(e?" ("+e.substring(0,7)+")":"")}}catch{H.textContent="Unable to fetch version"}}async function i(m){m||(g.textContent="Checking...",g.disabled=!0);try{const e=await(await fetch("/api/v1/system/update-check")).json();if(I=e,e.success&&e.available&&e.remote){L.style.display="",u.style.display="",j.textContent="v"+e.remote.version,v.textContent=e.remote.changelog||"No changelog available.";const t=document.getElementById("updates-btn");if(t&&!t.querySelector(".update-dot")){const r=document.createElement("span");r.className="update-dot",r.style.cssText="position:absolute;top:2px;right:2px;width:8px;height:8px;border-radius:50%;background:var(--accent);",t.style.position="relative",t.appendChild(r)}const o=document.getElementById("updates-dashcaddy-tab");if(o&&!o.querySelector(".update-dot")){const r=document.createElement("span");r.className="update-dot",r.style.cssText="display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--accent);margin-left:4px;vertical-align:middle;",o.appendChild(r)}}else L.style.display="none",u.style.display="none",await z(),m||b("You are running the latest version.","success");m||(g.textContent="Check for Updates",g.disabled=!1)}catch(n){m||(b("Failed to check: "+n.message,"error"),g.textContent="Check for Updates",g.disabled=!1)}}async function c(){if(!confirm("Apply DashCaddy update? The API container will restart."))return!1;N.textContent="Updating...",N.disabled=!0,b("Downloading and applying update...","info");try{const n=await(await secureFetch("/api/v1/system/update-apply",{method:"POST"})).json();if(n.success)return b("Update initiated: v"+(n.fromVersion||"?")+" \u2192 v"+(n.toVersion||"?")+". The container will restart shortly.","success"),N.textContent="Applied!",document.querySelectorAll(".update-dot").forEach(e=>e.remove()),!0;throw new Error(n.error||"Update failed")}catch(m){throw b("Update failed: "+m.message,"error"),N.textContent="Update Now",N.disabled=!1,m}}async function l(){try{const n=await(await fetch("/api/v1/system/update-history")).json(),e=n.success&&n.history?n.history:[];if(e.length===0){f.innerHTML='
\u{1F4E6}No self-update history.
';return}let t='';t+='';for(const o of e){const r=o.status==="success"?"\u2713 success":o.status==="pending"?"\u23F3 pending":o.status==="partial"?"\u26A0 partial":"\u2717 "+o.status,a=o.status==="success"?"var(--ok-fg)":o.status==="pending"?"var(--muted)":"var(--bad-fg)";t+='',t+='",t+='",t+='",t+='",t+="",o.error&&(t+='"),o.note&&(t+='")}t+="
WhenVersionFromStatus
'+timeAgo(o.timestamp)+"v'+escapeHtml(o.version)+(o.rollback?" (rollback)":"")+"v'+escapeHtml(o.fromVersion||"?")+"'+r+"
'+escapeHtml(o.error)+"
'+escapeHtml(o.note)+"
",f.innerHTML=t}catch(m){f.innerHTML='
Failed: '+escapeHtml(m.message)+"
"}}async function w(){try{const n=await(await fetch("/api/v1/system/rollback-versions")).json(),e=n.success&&n.versions?n.versions:[];if(e.length===0){showNotification("No rollback versions available.","info");return}const t=prompt(`Available rollback versions: `+e.join(` `)+` -Enter version to rollback to:`);if(!t)return;if(!e.includes(t)){showNotification("Invalid version: "+t,"error");return}if(!confirm("Rollback DashCaddy to v"+t+"? The container will restart."))return;f("Rolling back to v"+t+"...","info");const r=await(await secureFetch("/api/v1/system/rollback",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({version:t})})).json();if(r.success)f("Rollback to v"+t+" initiated. Container will restart.","success");else throw new Error(r.error||"Rollback failed")}catch(u){f("Rollback failed: "+u.message,"error")}}y?.addEventListener("click",()=>i(!1)),D?.addEventListener("click",()=>c().catch(()=>{})),P?.addEventListener("click",b),k?.addEventListener("click",h),E?.addEventListener("click",()=>{m?.classList.add("show"),p()}),wireModal(m,O),window.openUpdateModal=function(u){m?.classList.add("show"),p().then(()=>{if(!u)return;const n=N.querySelector(`[data-app-id="${u}"]`);n&&(n.scrollIntoView({behavior:"smooth",block:"center"}),n.style.background="rgba(249,115,22,0.15)",setTimeout(()=>{n.style.background=""},3e3))})},document.querySelector('[data-panel="updates-history"]')?.addEventListener("click",H),document.querySelector('[data-panel="updates-auto"]')?.addEventListener("click",w),document.querySelector('[data-panel="updates-dashcaddy"]')?.addEventListener("click",()=>{M(),l(),C||i(!0)}),window.dcApplyUpdate=c,window.dcCheckForUpdate=i,setTimeout(()=>i(!0),5e3)})(),(function(){injectModal("docker-resources-modal",`
+Enter version to rollback to:`);if(!t)return;if(!e.includes(t)){showNotification("Invalid version: "+t,"error");return}if(!confirm("Rollback DashCaddy to v"+t+"? The container will restart."))return;b("Rolling back to v"+t+"...","info");const r=await(await secureFetch("/api/v1/system/rollback",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({version:t})})).json();if(r.success)b("Rollback to v"+t+" initiated. Container will restart.","success");else throw new Error(r.error||"Rollback failed")}catch(m){b("Rollback failed: "+m.message,"error")}}g?.addEventListener("click",()=>i(!1)),N?.addEventListener("click",()=>c().catch(()=>{})),D?.addEventListener("click",w),E?.addEventListener("click",x),C?.addEventListener("click",()=>{y?.classList.add("show"),p()}),wireModal(y,O),window.openUpdateModal=function(m){y?.classList.add("show"),p().then(()=>{if(!m)return;const n=M.querySelector(`[data-app-id="${m}"]`);n&&(n.scrollIntoView({behavior:"smooth",block:"center"}),n.style.background="rgba(249,115,22,0.15)",setTimeout(()=>{n.style.background=""},3e3))})},document.querySelector('[data-panel="updates-history"]')?.addEventListener("click",$),document.querySelector('[data-panel="updates-auto"]')?.addEventListener("click",k),document.querySelector('[data-panel="updates-dashcaddy"]')?.addEventListener("click",()=>{z(),l(),I||i(!0)}),window.dcApplyUpdate=c,window.dcCheckForUpdate=i,setTimeout(()=>i(!0),5e3)})(),(function(){injectModal("docker-resources-modal",`

\u{1F433} Docker Resources

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

\u{1F4E6} Import Docker Compose

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

Terminal

@@ -1655,11 +1655,11 @@ Enter version to rollback to:`);if(!t)return;if(!e.includes(t)){showNotification
- `);const m=document.getElementById("exec-modal"),E=document.getElementById("exec-terminal"),O=document.getElementById("exec-close");let k=null,N=null,R=null;function z(){if(N){try{N.close()}catch{}N=null}if(k){try{k.dispose()}catch{}k=null}R=null,E.innerHTML=""}function A(p,B){if(z(),document.getElementById("exec-title").textContent=`Terminal \u2014 ${B||p}`,m?.classList.add("show"),typeof Terminal>"u"){E.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"&&(R=new FitAddon.FitAddon,k.loadAddon(R)),k.open(E),R&&setTimeout(()=>R.fit(),50);const h=location.protocol==="https:"?"wss:":"ws:";N=new WebSocket(`${h}//${location.host}/ws/exec/${encodeURIComponent(p)}`),N.binaryType="arraybuffer",N.onopen=()=>{if(k.writeln("\x1B[32mConnecting...\x1B[0m"),R){const w=R.proposeDimensions();w&&N.send(JSON.stringify({type:"resize",cols:w.cols,rows:w.rows}))}},N.onmessage=w=>{if(typeof w.data=="string"){try{const $=JSON.parse(w.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(w.data)}else k.write(new Uint8Array(w.data))},N.onclose=()=>{k&&k.writeln(`\r -\x1B[33mDisconnected.\x1B[0m`)},N.onerror=()=>{k&&k.writeln(`\r -\x1B[31mConnection error.\x1B[0m`)},k.onData(w=>{N&&N.readyState===WebSocket.OPEN&&N.send(w)}),k.onResize(({cols:w,rows:$})=>{N&&N.readyState===WebSocket.OPEN&&N.send(JSON.stringify({type:"resize",cols:w,rows:$}))});const H=()=>{R&&R.fit()};window.addEventListener("resize",H),m._resizeHandler=H}O?.addEventListener("click",()=>{z(),m._resizeHandler&&window.removeEventListener("resize",m._resizeHandler),m?.classList.remove("show")}),m?.addEventListener("click",p=>{p.target===m&&(z(),m._resizeHandler&&window.removeEventListener("resize",m._resizeHandler),m?.classList.remove("show"))}),window.openExecModal=A})(),(function(){injectModal("audit-modal",`
+
`);const y=document.getElementById("exec-modal"),C=document.getElementById("exec-terminal"),O=document.getElementById("exec-close");let E=null,M=null,R=null;function P(){if(M){try{M.close()}catch{}M=null}if(E){try{E.dispose()}catch{}E=null}R=null,C.innerHTML=""}function A(p,T){if(P(),document.getElementById("exec-title").textContent=`Terminal \u2014 ${T||p}`,y?.classList.add("show"),typeof Terminal>"u"){C.innerHTML='
xterm.js not loaded
';return}E=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"&&(R=new FitAddon.FitAddon,E.loadAddon(R)),E.open(C),R&&setTimeout(()=>R.fit(),50);const x=location.protocol==="https:"?"wss:":"ws:";M=new WebSocket(`${x}//${location.host}/ws/exec/${encodeURIComponent(p)}`),M.binaryType="arraybuffer",M.onopen=()=>{if(E.writeln("\x1B[32mConnecting...\x1B[0m"),R){const k=R.proposeDimensions();k&&M.send(JSON.stringify({type:"resize",cols:k.cols,rows:k.rows}))}},M.onmessage=k=>{if(typeof k.data=="string"){try{const H=JSON.parse(k.data);if(H.type==="connected"){E.writeln(`\x1B[32mConnected (${H.shell})\x1B[0m\r +`);return}if(H.type==="error"){E.writeln(`\x1B[31mError: ${H.message}\x1B[0m`);return}if(H.type==="exit"){E.writeln(`\r +\x1B[33mSession ended.\x1B[0m`);return}}catch{}E.write(k.data)}else E.write(new Uint8Array(k.data))},M.onclose=()=>{E&&E.writeln(`\r +\x1B[33mDisconnected.\x1B[0m`)},M.onerror=()=>{E&&E.writeln(`\r +\x1B[31mConnection error.\x1B[0m`)},E.onData(k=>{M&&M.readyState===WebSocket.OPEN&&M.send(k)}),E.onResize(({cols:k,rows:H})=>{M&&M.readyState===WebSocket.OPEN&&M.send(JSON.stringify({type:"resize",cols:k,rows:H}))});const $=()=>{R&&R.fit()};window.addEventListener("resize",$),y._resizeHandler=$}O?.addEventListener("click",()=>{P(),y._resizeHandler&&window.removeEventListener("resize",y._resizeHandler),y?.classList.remove("show")}),y?.addEventListener("click",p=>{p.target===y&&(P(),y._resizeHandler&&window.removeEventListener("resize",y._resizeHandler),y?.classList.remove("show"))}),window.openExecModal=A})(),(function(){injectModal("audit-modal",`

\u{1F4DC} Audit Log

- `);const m=document.getElementById("audit-modal"),E=document.getElementById("audit-log-btn"),O=document.getElementById("audit-cancel"),k=document.getElementById("audit-refresh-btn"),N=document.getElementById("audit-clear-btn"),R=document.getElementById("audit-filter"),z=document.getElementById("audit-log-container"),A=document.getElementById("audit-load-more");let p=0;const B=50;async function h(H){try{H||(p=0,z.innerHTML='
Loading...
');const w=R.value;let $=`/api/v1/audit-logs?limit=${B}&offset=${p}`;w&&($+=`&action=${encodeURIComponent(w)}`);const L=await(await fetch($)).json(),U=L.success&&L.entries?L.entries:[];if(U.length===0&&!H){z.innerHTML='
\u{1F4DC}No audit log entries yet. Actions will be logged automatically.
',A.style.display="none";return}let I="";H||(I='',I+='');for(const D of U){const y=D.outcome==="success";I+='',I+=``,I+=``,I+=``,I+=``,I+=``,I+="",D.details&&Object.keys(D.details).length>0&&(I+=``)}if(!H)I+="
WhenIPActionResourceResult
${timeAgo(D.timestamp)}${escapeHtml(D.ip||"-")}${escapeHtml(D.action||"-")}${escapeHtml(D.resource||"-")}${y?"\u2713":"\u2717"}
",z.innerHTML=I;else{const D=z.querySelector("table");D&&D.insertAdjacentHTML("beforeend",I)}p+=U.length,A.style.display=U.length>=B?"":"none",z.querySelectorAll(".audit-row").forEach(D=>{D.dataset.wired||(D.dataset.wired="true",D.addEventListener("click",()=>{const y=D.nextElementSibling;y&&y.classList.contains("audit-detail")&&(y.style.display=y.style.display==="none"?"":"none")}))})}catch(w){z.innerHTML=`
Failed: ${escapeHtml(w.message)}
`}}E?.addEventListener("click",()=>{m?.classList.add("show"),h(!1)}),wireModal(m,O),k?.addEventListener("click",()=>h(!1)),R?.addEventListener("change",()=>h(!1)),A?.addEventListener("click",()=>h(!0)),N?.addEventListener("click",async()=>{if(confirm("Clear the entire audit log? This cannot be undone."))try{const w=await(await secureFetch("/api/v1/audit-logs",{method:"DELETE"})).json();w.success?h(!1):showNotification("Error: "+(w.error||"Clear failed"),"error")}catch(H){showNotification("Error: "+H.message,"error")}})})(),(function(){injectModal("security-modal",`
+
`);const y=document.getElementById("audit-modal"),C=document.getElementById("audit-log-btn"),O=document.getElementById("audit-cancel"),E=document.getElementById("audit-refresh-btn"),M=document.getElementById("audit-clear-btn"),R=document.getElementById("audit-filter"),P=document.getElementById("audit-log-container"),A=document.getElementById("audit-load-more");let p=0;const T=50;async function x($){try{$||(p=0,P.innerHTML='
Loading...
');const k=R.value;let H=`/api/v1/audit-logs?limit=${T}&offset=${p}`;k&&(H+=`&action=${encodeURIComponent(k)}`);const u=await(await fetch(H)).json(),j=u.success&&u.entries?u.entries:[];if(j.length===0&&!$){P.innerHTML='
\u{1F4DC}No audit log entries yet. Actions will be logged automatically.
',A.style.display="none";return}let v="";$||(v='',v+='');for(const N of j){const g=N.outcome==="success";v+='',v+=``,v+=``,v+=``,v+=``,v+=``,v+="",N.details&&Object.keys(N.details).length>0&&(v+=``)}if(!$)v+="
WhenIPActionResourceResult
${timeAgo(N.timestamp)}${escapeHtml(N.ip||"-")}${escapeHtml(N.action||"-")}${escapeHtml(N.resource||"-")}${g?"\u2713":"\u2717"}
",P.innerHTML=v;else{const N=P.querySelector("table");N&&N.insertAdjacentHTML("beforeend",v)}p+=j.length,A.style.display=j.length>=T?"":"none",P.querySelectorAll(".audit-row").forEach(N=>{N.dataset.wired||(N.dataset.wired="true",N.addEventListener("click",()=>{const g=N.nextElementSibling;g&&g.classList.contains("audit-detail")&&(g.style.display=g.style.display==="none"?"":"none")}))})}catch(k){P.innerHTML=`
Failed: ${escapeHtml(k.message)}
`}}C?.addEventListener("click",()=>{y?.classList.add("show"),x(!1)}),wireModal(y,O),E?.addEventListener("click",()=>x(!1)),R?.addEventListener("change",()=>x(!1)),A?.addEventListener("click",()=>x(!0)),M?.addEventListener("click",async()=>{if(confirm("Clear the entire audit log? This cannot be undone."))try{const k=await(await secureFetch("/api/v1/audit-logs",{method:"DELETE"})).json();k.success?x(!1):showNotification("Error: "+(k.error||"Clear failed"),"error")}catch($){showNotification("Error: "+$.message,"error")}})})(),(function(){injectModal("security-modal",`

\u{1F6E1}\uFE0F Security Center

- `);const m=document.getElementById("security-modal"),E=document.getElementById("security-center-btn"),O=document.getElementById("sec-cancel"),k=m.querySelectorAll(".sec-tab"),N=m.querySelectorAll(".sec-panel");let R=[],z=[],A=null;k.forEach(i=>{i.addEventListener("click",()=>{k.forEach(c=>c.classList.toggle("active",c===i)),N.forEach(c=>c.style.display=c.dataset.panel===i.dataset.tab?"":"none"),i.dataset.tab==="overview"&&H(),i.dataset.tab==="events"&&D(),i.dataset.tab==="hosts"&&g()})}),E&&E.addEventListener("click",()=>{m.classList.add("show"),H(),B()}),O.addEventListener("click",p),m.addEventListener("click",i=>{i.target===m&&p()});function p(){m.classList.remove("show"),h()}function B(){if(h(),!!document.getElementById("sec-live-tail").checked&&!(typeof EventSource>"u"))try{A=new EventSource("/api/v1/security/events/stream"),A.addEventListener("init",i=>{try{R=JSON.parse(i.data).events||[],y()}catch{}}),A.addEventListener("security",i=>{try{const c=JSON.parse(i.data);R.unshift(c),R.length>500&&(R.length=500);const l=m.querySelector(".sec-tab.active")?.dataset?.tab;l==="events"?y():l==="overview"&&H()}catch{}}),A.onerror=()=>{}}catch(i){console.warn("[security] SSE failed:",i.message)}}function h(){if(A){try{A.close()}catch{}A=null}}document.getElementById("sec-live-tail").addEventListener("change",()=>{m.classList.contains("show")&&B()});async function H(){try{const i=new Date(Date.now()-864e5).toISOString(),[c,l,b]=await Promise.all([fetch(`/api/v1/security/events/stats?since=${encodeURIComponent(i)}`),fetch("/api/v1/security/hosts"),fetch(`/api/v1/security/events?limit=1&since=${encodeURIComponent(i)}`)]),u=(await c.json()).data||{},n=(await l.json()).data?.hosts||[],e=(await b.json()).data?.total||0;document.querySelector('#sec-stats [data-key="total"]').textContent=`${e} events (24h)`,document.querySelector('#sec-stats [data-key="warn"]').textContent=`${u.by_severity?.warn||0} warnings`,document.querySelector('#sec-stats [data-key="error"]').textContent=`${u.by_severity?.error||0} errors`,document.querySelector('#sec-stats [data-key="denied"]').textContent=`${u.by_outcome?.denied||0} denied`,document.querySelector('#sec-stats [data-key="hosts"]').textContent=`${n.length} hosts`,w("sec-top-actors",u.top_actors||[]),w("sec-top-targets",u.top_targets||[])}catch(i){console.warn("[security] refreshOverview failed:",i.message)}}function w(i,c){const l=document.getElementById(i);if(!c.length){l.innerHTML='
No data
';return}l.innerHTML=''+c.map(b=>``).join("")+"
${f(String(b.key))}${b.count}
"}const $=document.getElementById("sec-filter-source"),S=document.getElementById("sec-filter-severity"),L=document.getElementById("sec-filter-host"),U=document.getElementById("sec-filter-actor"),I=document.getElementById("sec-refresh-btn");[$,S,L].forEach(i=>i.addEventListener("change",D)),U.addEventListener("input",M(D,250)),I.addEventListener("click",D);async function D(){try{const i=new URLSearchParams;i.set("limit","200"),$.value&&i.set("source_type",$.value),S.value&&i.set("severity",S.value),L.value&&i.set("source_host",L.value),U.value&&i.set("actor_prefix",U.value),R=(await(await fetch(`/api/v1/security/events?${i}`)).json()).data.events||[],y(),(!L.options.length||L.options.length===1)&&await T()}catch(i){document.getElementById("sec-events-container").innerHTML='
Load failed: '+f(i.message)+"
"}}function y(){const i=document.getElementById("sec-events-container");if(!R.length){i.innerHTML='
No events
';return}i.innerHTML=R.slice(0,200).map(P).join("")}function P(i){const c=i.severity||"info",l={critical:"#c0392b",error:"#e74c3c",warn:"#f39c12",notice:"#3498db",info:"#7f8c8d"}[c]||"#7f8c8d",b=i.ts?new Date(i.ts).toLocaleTimeString():"",u=i.source_type||"",n=i.actor||"\u2014",e=i.target||"",t=i.action||"",o=i.outcome||"";return`
- ${f(c)} - ${f(u)} - ${f(n)} - ${f(t)} ${f(e)} - ${f(o)} - ${f(b)} -
`}async function T(){try{const c=(await(await fetch("/api/v1/security/hosts")).json()).data?.hosts||[],l=L.value;L.innerHTML=''+c.map(b=>``).join(""),l&&(L.value=l)}catch{}}document.getElementById("sec-host-register-btn").addEventListener("click",C),document.getElementById("sec-hosts-refresh").addEventListener("click",g);async function g(){try{const c=(await(await fetch("/api/v1/security/hosts")).json()).data?.hosts||[];z=c;const l=document.getElementById("sec-hosts-container");if(!c.length){l.innerHTML='
No hosts registered. Click \u2795 Register Host to add one.
';return}l.innerHTML=c.map(b=>{const u=b.enabled?b.last_seen_at?Date.now()-Date.parse(b.last_seen_at)>18e5?"\u{1F7E1} stale":"\u{1F7E2} online":"\u26AA registered":"\u{1F534} disabled";return`
+
`);const y=document.getElementById("security-modal"),C=document.getElementById("security-center-btn"),O=document.getElementById("sec-cancel"),E=y.querySelectorAll(".sec-tab"),M=y.querySelectorAll(".sec-panel");let R=[],P=[],A=null;E.forEach(i=>{i.addEventListener("click",()=>{E.forEach(c=>c.classList.toggle("active",c===i)),M.forEach(c=>c.style.display=c.dataset.panel===i.dataset.tab?"":"none"),i.dataset.tab==="overview"&&$(),i.dataset.tab==="events"&&N(),i.dataset.tab==="hosts"&&f()})}),C&&C.addEventListener("click",()=>{y.classList.add("show"),$(),T()}),O.addEventListener("click",p),y.addEventListener("click",i=>{i.target===y&&p()});function p(){y.classList.remove("show"),x()}function T(){if(x(),!!document.getElementById("sec-live-tail").checked&&!(typeof EventSource>"u"))try{A=new EventSource("/api/v1/security/events/stream"),A.addEventListener("init",i=>{try{R=JSON.parse(i.data).events||[],g()}catch{}}),A.addEventListener("security",i=>{try{const c=JSON.parse(i.data);R.unshift(c),R.length>500&&(R.length=500);const l=y.querySelector(".sec-tab.active")?.dataset?.tab;l==="events"?g():l==="overview"&&$()}catch{}}),A.onerror=()=>{}}catch(i){console.warn("[security] SSE failed:",i.message)}}function x(){if(A){try{A.close()}catch{}A=null}}document.getElementById("sec-live-tail").addEventListener("change",()=>{y.classList.contains("show")&&T()});async function $(){try{const i=new Date(Date.now()-864e5).toISOString(),[c,l,w]=await Promise.all([fetch(`/api/v1/security/events/stats?since=${encodeURIComponent(i)}`),fetch("/api/v1/security/hosts"),fetch(`/api/v1/security/events?limit=1&since=${encodeURIComponent(i)}`)]),m=(await c.json()).data||{},n=(await l.json()).data?.hosts||[],e=(await w.json()).data?.total||0;document.querySelector('#sec-stats [data-key="total"]').textContent=`${e} events (24h)`,document.querySelector('#sec-stats [data-key="warn"]').textContent=`${m.by_severity?.warn||0} warnings`,document.querySelector('#sec-stats [data-key="error"]').textContent=`${m.by_severity?.error||0} errors`,document.querySelector('#sec-stats [data-key="denied"]').textContent=`${m.by_outcome?.denied||0} denied`,document.querySelector('#sec-stats [data-key="hosts"]').textContent=`${n.length} hosts`,k("sec-top-actors",m.top_actors||[]),k("sec-top-targets",m.top_targets||[])}catch(i){console.warn("[security] refreshOverview failed:",i.message)}}function k(i,c){const l=document.getElementById(i);if(!c.length){l.innerHTML='
No data
';return}l.innerHTML=''+c.map(w=>``).join("")+"
${b(String(w.key))}${w.count}
"}const H=document.getElementById("sec-filter-source"),L=document.getElementById("sec-filter-severity"),u=document.getElementById("sec-filter-host"),j=document.getElementById("sec-filter-actor"),v=document.getElementById("sec-refresh-btn");[H,L,u].forEach(i=>i.addEventListener("change",N)),j.addEventListener("input",z(N,250)),v.addEventListener("click",N);async function N(){try{const i=new URLSearchParams;i.set("limit","200"),H.value&&i.set("source_type",H.value),L.value&&i.set("severity",L.value),u.value&&i.set("source_host",u.value),j.value&&i.set("actor_prefix",j.value),R=(await(await fetch(`/api/v1/security/events?${i}`)).json()).data.events||[],g(),(!u.options.length||u.options.length===1)&&await B()}catch(i){document.getElementById("sec-events-container").innerHTML='
Load failed: '+b(i.message)+"
"}}function g(){const i=document.getElementById("sec-events-container");if(!R.length){i.innerHTML='
No events
';return}i.innerHTML=R.slice(0,200).map(D).join("")}function D(i){const c=i.severity||"info",l={critical:"#c0392b",error:"#e74c3c",warn:"#f39c12",notice:"#3498db",info:"#7f8c8d"}[c]||"#7f8c8d",w=i.ts?new Date(i.ts).toLocaleTimeString():"",m=i.source_type||"",n=i.actor||"\u2014",e=i.target||"",t=i.action||"",o=i.outcome||"";return`
+ ${b(c)} + ${b(m)} + ${b(n)} + ${b(t)} ${b(e)} + ${b(o)} + ${b(w)} +
`}async function B(){try{const c=(await(await fetch("/api/v1/security/hosts")).json()).data?.hosts||[],l=u.value;u.innerHTML=''+c.map(w=>``).join(""),l&&(u.value=l)}catch{}}document.getElementById("sec-host-register-btn").addEventListener("click",I),document.getElementById("sec-hosts-refresh").addEventListener("click",f);async function f(){try{const c=(await(await fetch("/api/v1/security/hosts")).json()).data?.hosts||[];P=c;const l=document.getElementById("sec-hosts-container");if(!c.length){l.innerHTML='
No hosts registered. Click \u2795 Register Host to add one.
';return}l.innerHTML=c.map(w=>{const m=w.enabled?w.last_seen_at?Date.now()-Date.parse(w.last_seen_at)>18e5?"\u{1F7E1} stale":"\u{1F7E2} online":"\u26AA registered":"\u{1F534} disabled";return`
- ${f(b.label||b.id)} - ${f(b.type)} + ${b(w.label||w.id)} + ${b(w.type)}
- id: ${f(b.id)} \xB7 - registered ${new Date(b.registered_at).toLocaleDateString()} \xB7 - last seen ${b.last_seen_at?new Date(b.last_seen_at).toLocaleString():"never"} + id: ${b(w.id)} \xB7 + registered ${new Date(w.registered_at).toLocaleDateString()} \xB7 + last seen ${w.last_seen_at?new Date(w.last_seen_at).toLocaleString():"never"}
- ${u} - ${b.id==="self"?"":``} + ${m} + ${w.id==="self"?"":``}
-
`}).join(""),l.querySelectorAll(".sec-host-del").forEach(b=>{b.addEventListener("click",async()=>{confirm(`Remove host ${b.dataset.id}? Events already received will remain in the store.`)&&(await fetch(`/api/v1/security/hosts/${encodeURIComponent(b.dataset.id)}`,{method:"DELETE"}),g())})})}catch(i){document.getElementById("sec-hosts-container").innerHTML='
Load failed: '+f(i.message)+"
"}}async function C(){const i=prompt("Host id (lowercase, no spaces):");if(!i)return;const c=prompt("Display label:",i)||i,l=prompt('Type ("dashcaddy", "service", or "agent"):',"agent")||"agent";try{const b=await fetch("/api/v1/security/hosts",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:i,label:c,type:l})}),u=await b.json();if(!b.ok){alert("Failed: "+(u?.error?.message||b.statusText));return}alert(`\u2705 Host registered! + `}).join(""),l.querySelectorAll(".sec-host-del").forEach(w=>{w.addEventListener("click",async()=>{confirm(`Remove host ${w.dataset.id}? Events already received will remain in the store.`)&&(await fetch(`/api/v1/security/hosts/${encodeURIComponent(w.dataset.id)}`,{method:"DELETE"}),f())})})}catch(i){document.getElementById("sec-hosts-container").innerHTML='
Load failed: '+b(i.message)+"
"}}async function I(){const i=prompt("Host id (lowercase, no spaces):");if(!i)return;const c=prompt("Display label:",i)||i,l=prompt('Type ("dashcaddy", "service", or "agent"):',"agent")||"agent";try{const w=await fetch("/api/v1/security/hosts",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:i,label:c,type:l})}),m=await w.json();if(!w.ok){alert("Failed: "+(m?.error?.message||w.statusText));return}alert(`\u2705 Host registered! -id: ${u.data.host.id} -label: ${u.data.host.label} -type: ${u.data.host.type} +id: ${m.data.host.id} +label: ${m.data.host.label} +type: ${m.data.host.type} \u{1F511} API KEY (save this NOW \u2014 won't be shown again): -${u.data.api_key} +${m.data.api_key} Send this key as: Authorization: Bearer -To endpoint: POST /api/v1/security/events/ingest or /events/batch`),g()}catch(b){alert("Failed: "+b.message)}}function f(i){return String(i).replace(/[&<>"']/g,c=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[c])}function M(i,c){let l;return function(){clearTimeout(l),l=setTimeout(()=>i.apply(this,arguments),c)}}})(),(function(){const m=new ErrorHandler;injectModal("weather-modal",`

Weather Settings

+To endpoint: POST /api/v1/security/events/ingest or /events/batch`),f()}catch(w){alert("Failed: "+w.message)}}function b(i){return String(i).replace(/[&<>"']/g,c=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[c])}function z(i,c){let l;return function(){clearTimeout(l),l=setTimeout(()=>i.apply(this,arguments),c)}}})(),(function(){const y=new ErrorHandler;injectModal("weather-modal",`

Weather Settings

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