diff --git a/status/dist/features.js b/status/dist/features.js index ca074e5..1d72f1c 100644 --- a/status/dist/features.js +++ b/status/dist/features.js @@ -90,19 +90,19 @@ - `);const k=document.getElementById("logo-modal"),E=document.getElementById("logo-preview-dark"),z=document.getElementById("logo-preview-light"),I=document.getElementById("logo-status"),L=document.getElementById("logo-same-both"),A=document.getElementById("logo-dual-uploads"),T=document.getElementById("logo-single-upload"),$=document.getElementById("logo-upload-dark"),y=document.getElementById("logo-upload-light"),S=document.getElementById("logo-upload-single"),w=document.querySelector("#brand .brand-logo-dark"),x=document.querySelector("#brand .brand-logo-light"),C=document.querySelector(".top-row"),M=document.getElementById("dashboard-title"),R=DC.NAME;let D=null,j=null,B=null,P="left",b=R;L?.addEventListener("change",()=>{L.checked?(A.style.display="none",T.style.display="",D=null,j=null):(A.style.display="flex",T.style.display="none",B=null)});function H(a,e){if(!a||!a.type.startsWith("image/")){showNotification("Please select an image file","warning");return}const n=new FileReader;n.onload=t=>e(t.target.result),n.readAsDataURL(a)}$?.addEventListener("change",a=>{H(a.target.files[0],e=>{D=e,E.src=e,I.textContent="New dark logo ready to save"})}),y?.addEventListener("change",a=>{H(a.target.files[0],e=>{j=e,z.src=e,I.textContent="New light logo ready to save"})}),S?.addEventListener("change",a=>{H(a.target.files[0],e=>{B=e,E.src=e,z.src=e,I.textContent="New logo ready to save (both themes)"})});function f(a){C.setAttribute("data-logo-pos",a),document.querySelectorAll(".logo-pos-btn").forEach(e=>{e.style.background=e.dataset.pos===a?"var(--accent)":"var(--card-bg)",e.style.color=e.dataset.pos===a?"white":"var(--fg)"})}function p(a){b=a||R,document.title=b;const e=document.querySelector(".dashboard-title");e&&(e.textContent=b)}async function g(){try{const a=await fetch("/api/v1/logo");if(a.ok){const e=await a.json();e.customLogoDark&&(w.src=e.customLogoDark,E.src=e.customLogoDark),e.customLogoLight&&(x.src=e.customLogoLight,z.src=e.customLogoLight),!e.customLogoDark&&!e.customLogoLight&&e.customLogo&&(w.src=e.customLogo,x.src=e.customLogo,E.src=e.customLogo,z.src=e.customLogo),e.isDefault||(I.textContent="Using custom logo"),e.position&&(P=e.position,f(e.position)),e.dashboardTitle&&p(e.dashboardTitle)}}catch(a){console.warn("Could not load custom logo:",a.message)}}document.querySelectorAll(".logo-pos-btn").forEach(a=>{a.addEventListener("click",()=>{P=a.dataset.pos,f(P)})}),document.getElementById("brand")?.addEventListener("click",()=>{D=null,j=null,B=null,$&&($.value=""),y&&(y.value=""),S&&(S.value=""),L&&(L.checked=!1),A.style.display="flex",T.style.display="none",E.src=w.src,z.src=x.src;const a=w.src.includes("custom-logo")||x.src.includes("custom-logo");I.textContent=a?"Using custom logo":"Using default logos",f(P),M.value=b,k.classList.add("show")}),document.getElementById("logo-save")?.addEventListener("click",async()=>{try{const a=M.value.trim()||R,e={position:P,dashboardTitle:a};L?.checked&&B?(e.dataDark=B,e.dataLight=B):(D&&(e.dataDark=D),j&&(e.dataLight=j));const n=await secureFetch("/api/v1/logo",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});if(n.ok){const t=await n.json(),i="?t="+Date.now();t.pathDark&&(w.src=t.pathDark+i,E.src=t.pathDark+i),t.pathLight&&(x.src=t.pathLight+i,z.src=t.pathLight+i),f(P),p(a),k.classList.remove("show")}else{const t=await n.json();showNotification("Failed to save: "+t.error,"error")}}catch(a){showNotification("Error saving: "+a.message,"error")}}),document.getElementById("logo-reset")?.addEventListener("click",async()=>{if(confirm(`Reset all branding to DashCaddy defaults? + `);const w=document.getElementById("logo-modal"),E=document.getElementById("logo-preview-dark"),N=document.getElementById("logo-preview-light"),I=document.getElementById("logo-status"),L=document.getElementById("logo-same-both"),A=document.getElementById("logo-dual-uploads"),T=document.getElementById("logo-single-upload"),$=document.getElementById("logo-upload-dark"),y=document.getElementById("logo-upload-light"),S=document.getElementById("logo-upload-single"),k=document.querySelector("#brand .brand-logo-dark"),x=document.querySelector("#brand .brand-logo-light"),B=document.querySelector(".top-row"),M=document.getElementById("dashboard-title"),R=DC.NAME;let P=null,j=null,C=null,D="left",b=R;L?.addEventListener("change",()=>{L.checked?(A.style.display="none",T.style.display="",P=null,j=null):(A.style.display="flex",T.style.display="none",C=null)});function H(a,e){if(!a||!a.type.startsWith("image/")){showNotification("Please select an image file","warning");return}const n=new FileReader;n.onload=t=>e(t.target.result),n.readAsDataURL(a)}$?.addEventListener("change",a=>{H(a.target.files[0],e=>{P=e,E.src=e,I.textContent="New dark logo ready to save"})}),y?.addEventListener("change",a=>{H(a.target.files[0],e=>{j=e,N.src=e,I.textContent="New light logo ready to save"})}),S?.addEventListener("change",a=>{H(a.target.files[0],e=>{C=e,E.src=e,N.src=e,I.textContent="New logo ready to save (both themes)"})});function f(a){B.setAttribute("data-logo-pos",a),document.querySelectorAll(".logo-pos-btn").forEach(e=>{e.style.background=e.dataset.pos===a?"var(--accent)":"var(--card-bg)",e.style.color=e.dataset.pos===a?"white":"var(--fg)"})}function p(a){b=a||R,document.title=b;const e=document.querySelector(".dashboard-title");e&&(e.textContent=b)}async function g(){try{const a=await fetch("/api/v1/logo");if(a.ok){const e=await a.json();e.customLogoDark&&(k.src=e.customLogoDark,E.src=e.customLogoDark),e.customLogoLight&&(x.src=e.customLogoLight,N.src=e.customLogoLight),!e.customLogoDark&&!e.customLogoLight&&e.customLogo&&(k.src=e.customLogo,x.src=e.customLogo,E.src=e.customLogo,N.src=e.customLogo),e.isDefault||(I.textContent="Using custom logo"),e.position&&(D=e.position,f(e.position)),e.dashboardTitle&&p(e.dashboardTitle)}}catch(a){console.warn("Could not load custom logo:",a.message)}}document.querySelectorAll(".logo-pos-btn").forEach(a=>{a.addEventListener("click",()=>{D=a.dataset.pos,f(D)})}),document.getElementById("brand")?.addEventListener("click",()=>{P=null,j=null,C=null,$&&($.value=""),y&&(y.value=""),S&&(S.value=""),L&&(L.checked=!1),A.style.display="flex",T.style.display="none",E.src=k.src,N.src=x.src;const a=k.src.includes("custom-logo")||x.src.includes("custom-logo");I.textContent=a?"Using custom logo":"Using default logos",f(D),M.value=b,w.classList.add("show")}),document.getElementById("logo-save")?.addEventListener("click",async()=>{try{const a=M.value.trim()||R,e={position:D,dashboardTitle:a};L?.checked&&C?(e.dataDark=C,e.dataLight=C):(P&&(e.dataDark=P),j&&(e.dataLight=j));const n=await secureFetch("/api/v1/logo",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});if(n.ok){const t=await n.json(),i="?t="+Date.now();t.pathDark&&(k.src=t.pathDark+i,E.src=t.pathDark+i),t.pathLight&&(x.src=t.pathLight+i,N.src=t.pathLight+i),f(D),p(a),w.classList.remove("show")}else{const t=await n.json();showNotification("Failed to save: "+t.error,"error")}}catch(a){showNotification("Error saving: "+a.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&&(w.src="/assets/dashcaddy-logo-dark.png",x.src="/assets/dashcaddy-logo-light.png",E.src="/assets/dashcaddy-logo-dark.png",z.src="/assets/dashcaddy-logo-light.png",I.textContent="Using default logos",D=null,j=null,B=null,M.value=R,p(R),P="left",f("left")),(await secureFetch("/api/v1/favicon",{method:"DELETE"})).ok){const n=document.querySelector('link[rel="icon"]'),t=document.getElementById("favicon-preview"),i=document.getElementById("favicon-status");n&&(n.href="/assets/dashcaddy-favicon.ico?t="+Date.now()),t&&(t.src="/assets/dashcaddy-favicon.ico?t="+Date.now()),i&&(i.textContent="Using DashCaddy favicon"),s=null}}catch(a){showNotification("Error resetting branding: "+a.message,"error")}}),wireModal(k,document.getElementById("logo-cancel"));const v=document.getElementById("favicon-preview"),m=document.getElementById("favicon-status"),r=document.getElementById("favicon-upload"),c=document.querySelector('link[rel="icon"]')||document.createElement("link");let s=null;document.querySelector('link[rel="icon"]')||(c.rel="icon",c.href="/assets/dashcaddy-favicon.ico",document.head.appendChild(c));async function h(){try{const a=await fetch("/api/v1/favicon");if(a.ok){const e=await a.json();e.customFavicon&&(c.href=e.customFavicon+"?t="+Date.now(),v.src=e.customFavicon+"?t="+Date.now(),m.textContent="Using custom favicon")}}catch(a){console.warn("Could not load custom favicon:",a.message)}}r?.addEventListener("change",a=>{const e=a.target.files[0];if(!e)return;if(!e.type.match(/^image\/(png|svg\+xml)$/)){showNotification("Please select a PNG or SVG file","warning"),r.value="";return}const n=new FileReader;n.onload=t=>{s=t.target.result,v.src=s,m.textContent="New favicon ready to save"},n.readAsDataURL(e)}),document.getElementById("logo-save")?.addEventListener("click",async()=>{if(s)try{const a=await secureFetch("/api/v1/favicon",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({data:s})});if(a.ok){const e=await a.json();c.href=e.path+"?t="+Date.now(),v.src=e.path+"?t="+Date.now(),m.textContent="Using custom favicon",s=null}else{const e=await a.json();showNotification("Failed to save favicon: "+e.error,"error")}}catch(a){showNotification("Error saving favicon: "+a.message,"error")}}),h(),g();const u=document.getElementById("settings-timezone");u&&(new MutationObserver(()=>{k.classList.contains("show")&&u.options.length===0&&(async()=>{let e;try{const n=await fetch("/api/v1/config");n.ok&&(e=(await n.json()).timezone)}catch{}window.populateTimezoneSelect(u,e)})()}).observe(k,{attributes:!0,attributeFilter:["class"]}),document.getElementById("logo-save")?.addEventListener("click",async()=>{const e=u.value;if(e)try{const n=await fetch("/api/v1/config");if(!n.ok)return;const t=await n.json();t.timezone=e,t.updatedAt=new Date().toISOString(),await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})}catch(n){console.warn("Failed to save timezone:",n.message)}}))})();const errorHandler=new ErrorHandler;window.populateTimezoneSelect=function(k,E){const z=Intl.supportedValuesOf("timeZone"),I=E||Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC";k.innerHTML="";for(const L of z){const A=document.createElement("option");A.value=L,A.textContent=L.replace(/_/g," "),L===I&&(A.selected=!0),k.appendChild(A)}},(function(){let k="homelab",E=null;async function z(){try{const H=await fetch("/api/v1/config");if(H.ok&&(E=await H.json(),E&&E.setupComplete))return document.getElementById("setup-wizard").style.display="none",!0}catch(H){console.warn("Could not fetch server config, checking localStorage fallback:",H.message)}return safeGet("dashcaddy-setup")?(document.getElementById("setup-wizard").style.display="none",!0):(document.getElementById("setup-wizard").style.display="flex",!1)}z();const I=document.getElementById("setup-timezone");I&&window.populateTimezoneSelect(I);function L(b){document.querySelectorAll(".setup-step").forEach(f=>{f.style.display="none"});const H=document.getElementById(b);H&&(H.style.display="block")}function A(){const b=document.getElementById("setup-summary-content");if(!b)return;let H='
';if(k==="homelab"){const p=document.getElementById("setup-tld")?.value?.trim()||".home",g=document.getElementById("setup-ca-name")?.value?.trim()||"",v=document.getElementById("setup-dns-ip")?.value?.trim()||"",m=document.getElementById("setup-dns-port")?.value?.trim()||DC.DEFAULTS.DNS_PORT;H+=` +This will reset the logo, favicon, title, and position.`))try{if((await secureFetch("/api/v1/logo",{method:"DELETE"})).ok&&(k.src="/assets/dashcaddy-logo-dark.png",x.src="/assets/dashcaddy-logo-light.png",E.src="/assets/dashcaddy-logo-dark.png",N.src="/assets/dashcaddy-logo-light.png",I.textContent="Using default logos",P=null,j=null,C=null,M.value=R,p(R),D="left",f("left")),(await secureFetch("/api/v1/favicon",{method:"DELETE"})).ok){const n=document.querySelector('link[rel="icon"]'),t=document.getElementById("favicon-preview"),i=document.getElementById("favicon-status");n&&(n.href="/assets/dashcaddy-favicon.ico?t="+Date.now()),t&&(t.src="/assets/dashcaddy-favicon.ico?t="+Date.now()),i&&(i.textContent="Using DashCaddy favicon"),s=null}}catch(a){showNotification("Error resetting branding: "+a.message,"error")}}),wireModal(w,document.getElementById("logo-cancel"));const v=document.getElementById("favicon-preview"),u=document.getElementById("favicon-status"),r=document.getElementById("favicon-upload"),c=document.querySelector('link[rel="icon"]')||document.createElement("link");let s=null;document.querySelector('link[rel="icon"]')||(c.rel="icon",c.href="/assets/dashcaddy-favicon.ico",document.head.appendChild(c));async function h(){try{const a=await fetch("/api/v1/favicon");if(a.ok){const e=await a.json();e.customFavicon&&(c.href=e.customFavicon+"?t="+Date.now(),v.src=e.customFavicon+"?t="+Date.now(),u.textContent="Using custom favicon")}}catch(a){console.warn("Could not load custom favicon:",a.message)}}r?.addEventListener("change",a=>{const e=a.target.files[0];if(!e)return;if(!e.type.match(/^image\/(png|svg\+xml)$/)){showNotification("Please select a PNG or SVG file","warning"),r.value="";return}const n=new FileReader;n.onload=t=>{s=t.target.result,v.src=s,u.textContent="New favicon ready to save"},n.readAsDataURL(e)}),document.getElementById("logo-save")?.addEventListener("click",async()=>{if(s)try{const a=await secureFetch("/api/v1/favicon",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({data:s})});if(a.ok){const e=await a.json();c.href=e.path+"?t="+Date.now(),v.src=e.path+"?t="+Date.now(),u.textContent="Using custom favicon",s=null}else{const e=await a.json();showNotification("Failed to save favicon: "+e.error,"error")}}catch(a){showNotification("Error saving favicon: "+a.message,"error")}}),h(),g();const m=document.getElementById("settings-timezone");m&&(new MutationObserver(()=>{w.classList.contains("show")&&m.options.length===0&&(async()=>{let e;try{const n=await fetch("/api/v1/config");n.ok&&(e=(await n.json()).timezone)}catch{}window.populateTimezoneSelect(m,e)})()}).observe(w,{attributes:!0,attributeFilter:["class"]}),document.getElementById("logo-save")?.addEventListener("click",async()=>{const e=m.value;if(e)try{const n=await fetch("/api/v1/config");if(!n.ok)return;const t=await n.json();t.timezone=e,t.updatedAt=new Date().toISOString(),await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})}catch(n){console.warn("Failed to save timezone:",n.message)}}))})();const errorHandler=new ErrorHandler;window.populateTimezoneSelect=function(w,E){const N=Intl.supportedValuesOf("timeZone"),I=E||Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC";w.innerHTML="";for(const L of N){const A=document.createElement("option");A.value=L,A.textContent=L.replace(/_/g," "),L===I&&(A.selected=!0),w.appendChild(A)}},(function(){let w="homelab",E=null;async function N(){try{const H=await fetch("/api/v1/config");if(H.ok&&(E=await H.json(),E&&E.setupComplete))return document.getElementById("setup-wizard").style.display="none",!0}catch(H){console.warn("Could not fetch server config, checking localStorage fallback:",H.message)}return safeGet("dashcaddy-setup")?(document.getElementById("setup-wizard").style.display="none",!0):(document.getElementById("setup-wizard").style.display="flex",!1)}N();const I=document.getElementById("setup-timezone");I&&window.populateTimezoneSelect(I);function L(b){document.querySelectorAll(".setup-step").forEach(f=>{f.style.display="none"});const H=document.getElementById(b);H&&(H.style.display="block")}function A(){const b=document.getElementById("setup-summary-content");if(!b)return;let H='
';if(w==="homelab"){const p=document.getElementById("setup-tld")?.value?.trim()||".home",g=document.getElementById("setup-ca-name")?.value?.trim()||"",v=document.getElementById("setup-dns-ip")?.value?.trim()||"",u=document.getElementById("setup-dns-port")?.value?.trim()||DC.DEFAULTS.DNS_PORT;H+=`

Home Lab Configuration

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

Simple Setup

@@ -112,7 +112,7 @@ This will reset the logo, favicon, title, and position.`))try{if((await secureFe
Example URLs: http://${p}:8080, http://${p}:3000
- `}else if(k==="public"){const p=document.getElementById("setup-public-domain")?.value?.trim()||"",g=document.getElementById("setup-public-email")?.value?.trim()||"",v=document.querySelector('input[name="routing-mode"]:checked')?.value||"subdirectory",m=v==="subdirectory"?`https://${p}/sonarr, https://${p}/grafana`:`https://sonarr.${p}, https://grafana.${p}`;H+=` + `}else if(w==="public"){const p=document.getElementById("setup-public-domain")?.value?.trim()||"",g=document.getElementById("setup-public-email")?.value?.trim()||"",v=document.querySelector('input[name="routing-mode"]:checked')?.value||"subdirectory",u=v==="subdirectory"?`https://${p}/sonarr, https://${p}/grafana`:`https://sonarr.${p}, https://grafana.${p}`;H+=`

Public Server

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

Choose an App

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

Deploy Recipe

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

\u{1F4CB} Error Logs

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

\u{1F4CB} Error Logs

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

\u{1F3AC} Smart Arr Connect

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

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

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

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

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

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

\u{1F514} Notification Settings

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

\u{1F4BE} Backup & Restore

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

\u23F0 Backup Schedule

',l+='
',l+='
',l+='
",l+='
',l+='
",l+="
",l+='
',l+='
",l+='
',l+=' ',l+=' ',l+="
",l+="
",l+='
',l+='

\u2601\uFE0F Backup Destination

',l+='
',l+='
",l+='
',l+='',l+="
",l+='',j.innerHTML=l,document.getElementById("backup-save-schedule")?.addEventListener("click",h),document.getElementById("backup-run-now")?.addEventListener("click",u);var N=document.getElementById("backup-dest-type");N?.addEventListener("change",function(){b={type:N.value},f(N.value)}),f(b.type)}catch(O){j.innerHTML='
Failed to load schedule: '+escapeHtml(O.message)+"
"}}async function f(e){var n=document.getElementById("backup-dest-form");if(n){if(e==="local"){n.innerHTML='
Backups are stored on the host filesystem. No additional configuration required.
';return}var t="";if(e==="dropbox"?(t+='',t+='',t+='',t+='',t+='
Generate a token at Dropbox App Console with files.content.write + files.content.read scopes.
'):e==="webdav"?(t+='',t+='',t+='
',t+='
',t+='
',t+='
',t+='
',t+="
",t+='',t+=''):e==="sftp"&&(t+='
',t+='
',t+='
',t+='
',t+='
',t+="
",t+='',t+='',t+='',t+='",t+='
',t+='
',t+='',t+='',t+=''),t+='
',t+=' ',t+=' ',t+=' ',t+="
",n.innerHTML=t,e==="sftp"){var i=document.getElementById("dest-sftp-authtype"),o=document.getElementById("dest-sftp-password-row"),d=document.getElementById("dest-sftp-key-row");i?.addEventListener("change",function(){i.value==="key"?(o.style.display="none",d.style.display=""):(o.style.display="",d.style.display="none")})}document.getElementById("dest-save-creds")?.addEventListener("click",function(){m(e)}),document.getElementById("dest-test-conn")?.addEventListener("click",function(){s(e)}),document.getElementById("dest-clear-creds")?.addEventListener("click",function(){r(e)}),await g(e)}}function p(e,n){var t=document.getElementById("backup-dest-result");t&&(t.innerHTML=e,t.style.display="block",t.style.background=n?"color-mix(in srgb, var(--ok-fg) 15%, transparent)":"color-mix(in srgb, var(--bad-fg) 15%, transparent)",t.style.border=n?"1px solid var(--ok-fg)":"1px solid var(--bad-fg)")}async function g(e){try{var n=await fetch("/api/v1/backups/credentials/"+e),t=await n.json();if(!t.success||!t.credentials)return;var i=t.credentials;if(e==="dropbox"){var o=document.getElementById("dest-dropbox-token");o&&i.token&&(o.value=i.token)}else if(e==="webdav"){var d=document.getElementById("dest-webdav-url");d&&i.url&&(d.value=i.url);var l=document.getElementById("dest-webdav-username");l&&i.username&&(l.value=i.username);var N=document.getElementById("dest-webdav-password");N&&i.password&&(N.value=i.password)}else if(e==="sftp"){var O=document.getElementById("dest-sftp-host");O&&i.host&&(O.value=i.host);var F=document.getElementById("dest-sftp-port");F&&i.port&&(F.value=i.port);var q=document.getElementById("dest-sftp-username");q&&i.username&&(q.value=i.username);var U=document.getElementById("dest-sftp-password");U&&i.password&&(U.value=i.password);var G=document.getElementById("dest-sftp-privatekey");if(G&&i.privateKey&&(G.value=i.privateKey),i.privateKey){var W=document.getElementById("dest-sftp-authtype");W&&(W.value="key",W.dispatchEvent(new Event("change")))}}}catch{}}function v(e){if(e==="dropbox")return{token:document.getElementById("dest-dropbox-token")?.value};if(e==="webdav")return{url:document.getElementById("dest-webdav-url")?.value,username:document.getElementById("dest-webdav-username")?.value,password:document.getElementById("dest-webdav-password")?.value};if(e==="sftp"){var n=document.getElementById("dest-sftp-authtype")?.value,t={host:document.getElementById("dest-sftp-host")?.value,port:parseInt(document.getElementById("dest-sftp-port")?.value)||22,username:document.getElementById("dest-sftp-username")?.value};return n==="key"?t.privateKey=document.getElementById("dest-sftp-privatekey")?.value:t.password=document.getElementById("dest-sftp-password")?.value,t}return{}}async function m(e){try{var n=v(e),t=await secureFetch("/api/v1/backups/credentials/"+e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)}),i=await t.json();p(i.success?"\u2705 Credentials saved":"\u26A0\uFE0F "+escapeHtml(i.error||"Failed"),i.success)}catch(o){p("\u274C "+escapeHtml(o.message),!1)}}async function r(e){if(confirm("Delete saved "+e+" credentials?"))try{var n=await secureFetch("/api/v1/backups/credentials/"+e,{method:"DELETE"}),t=await n.json();t.success?(p("\u2705 Credentials cleared",!0),f(e)):p("\u26A0\uFE0F "+escapeHtml(t.error||"Failed"),!1)}catch(i){p("\u274C "+escapeHtml(i.message),!1)}}function c(e){var n={type:e};return e==="local"||(e==="dropbox"?n.path=document.getElementById("dest-dropbox-path")?.value||"/dashcaddy-backups":e==="webdav"?n.path=document.getElementById("dest-webdav-path")?.value||"/dashcaddy-backups":e==="sftp"&&(n.path=document.getElementById("dest-sftp-path")?.value||"/dashcaddy-backups")),n}async function s(e){p(' Testing connection...',!0);try{var n=c(e),t=await secureFetch("/api/v1/backups/test-destination",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)}),i=await t.json();if(i.success){var o=i.elapsedMs?" ("+i.elapsedMs+"ms)":"";p("\u2705 Connection OK"+o+" \u2014 write/read/delete probe succeeded",!0)}else p("\u274C "+escapeHtml(i.error||"Connection failed"),!1)}catch(d){p("\u274C "+escapeHtml(d.message),!1)}}async function h(){var e=document.getElementById("backup-schedule-select")?.value,n=parseInt(document.getElementById("backup-retention-select")?.value)||5,t=document.getElementById("backup-encrypt-toggle")?.checked??!0,i=document.getElementById("backup-dest-type")?.value||"local",o=document.getElementById("backup-schedule-result");try{var d=await secureFetch("/api/v1/backups/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({backups:{auto:{enabled:e!=="disabled",schedule:e==="disabled"?"daily":e,include:["all"],encrypt:t,verify:!0,retention:{keep:n},destinations:[c(i)]}}})}),l=await d.json();o&&(o.innerHTML=l.success?"\u2705 Schedule saved":"\u26A0\uFE0F "+escapeHtml(l.error),o.style.display="block",o.style.background=l.success?"color-mix(in srgb, var(--ok-fg) 15%, transparent)":"color-mix(in srgb, var(--bad-fg) 15%, transparent)",o.style.border=l.success?"1px solid var(--ok-fg)":"1px solid var(--bad-fg)",setTimeout(function(){o&&(o.style.display="none")},3e3))}catch(N){o&&(o.innerHTML="\u274C "+escapeHtml(N.message),o.style.display="block",o.style.background="color-mix(in srgb, var(--bad-fg) 15%, transparent)",o.style.border="1px solid var(--bad-fg)")}}async function u(){var e=document.getElementById("backup-run-now"),n=document.getElementById("backup-schedule-result"),t=document.getElementById("backup-dest-type")?.value||"local";e&&(e.disabled=!0,e.innerHTML=' Running...');try{var i=await secureFetch("/api/v1/backups/execute",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({include:["all"],destinations:[c(t)]})}),o=await i.json();if(n){if(o.success){var d=o.backup?.size?(o.backup.size/1024/1024).toFixed(2):"?";n.innerHTML="\u2705 Backup complete ("+d+" MB)",n.style.background="color-mix(in srgb, var(--ok-fg) 15%, transparent)",n.style.border="1px solid var(--ok-fg)"}else n.innerHTML="\u26A0\uFE0F "+escapeHtml(o.error),n.style.background="color-mix(in srgb, var(--bad-fg) 15%, transparent)",n.style.border="1px solid var(--bad-fg)";n.style.display="block"}a()}catch(l){n&&(n.innerHTML="\u274C "+escapeHtml(l.message),n.style.display="block",n.style.background="color-mix(in srgb, var(--bad-fg) 15%, transparent)",n.style.border="1px solid var(--bad-fg)")}e&&(e.disabled=!1,e.innerHTML="\u25B6\uFE0F Run Backup Now")}async function a(){if(B){B.innerHTML='
Loading...
';try{var e=await fetch("/api/v1/backups/history?limit=50"),n=await e.json();if(!n.success||!n.history?.length){B.innerHTML='
\u{1F4CB} No backup history yet
';return}for(var t='
',i=0;i',t+='
',t+=' '+escapeHtml(o.name||"backup")+"",t+='
',t+=' '+escapeHtml(o.status)+"",o.status==="success"&&(t+=' '),t+="
",t+="
",t+='
',t+=" "+new Date(o.timestamp).toLocaleString()+" | "+d+" MB | "+(o.duration?(o.duration/1e3).toFixed(1)+"s":"--"),o.encrypted&&(t+=" | \u{1F512}"),t+="
",t+="
"}t+="",B.innerHTML=t,B.querySelectorAll(".backup-restore-btn").forEach(function(l){l.addEventListener("click",function(){window.__restoreServerBackup(l.dataset.backupId)})})}catch(l){B.innerHTML='
Failed: '+escapeHtml(l.message)+"
"}}}window.__restoreServerBackup=async function(e){if(confirm("Restore from this server backup? This will overwrite current configuration."))try{var n=await secureFetch("/api/v1/backups/restore/"+e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({restoreServices:!0,restoreConfig:!0})}),t=await n.json();t.success?(showNotification("Restore completed successfully!","success"),location.reload()):showNotification("Restore failed: "+(t.error||"Unknown error"),"error")}catch(i){showNotification("Restore error: "+i.message,"error")}},document.querySelector('[data-panel="backup-automated"]')?.addEventListener("click",H),document.querySelector('[data-panel="backup-history-tab"]')?.addEventListener("click",a)})(),(function(){injectModal("stats-modal",`
+
`);var A=document.getElementById("backup-modal"),T=document.getElementById("backup-restore-btn"),$=document.getElementById("backup-cancel"),y=document.getElementById("backup-export-btn"),S=document.getElementById("backup-select-file"),k=document.getElementById("backup-file-input"),x=document.getElementById("backup-file-name"),B=document.getElementById("backup-preview"),M=document.getElementById("backup-preview-content"),R=document.getElementById("backup-do-restore-btn"),P=document.getElementById("backup-result"),j=document.getElementById("backup-schedule-container"),C=document.getElementById("backup-history-container"),D=null;T?.addEventListener("click",function(){A.classList.add("show"),P&&(P.style.display="none"),B&&(B.style.display="none"),x&&(x.style.display="none"),D=null}),wireModal(A,$),y?.addEventListener("click",async function(){y.disabled=!0,y.innerHTML=' Exporting...';try{var e=await fetch("/api/v1/backup/export"),n=await e.json();n.browserState=E();var t=new Blob([JSON.stringify(n,null,2)],{type:"application/json"}),i=URL.createObjectURL(t),o=document.createElement("a");o.href=i,o.download="dashcaddy-backup-"+new Date().toISOString().split("T")[0]+".json",document.body.appendChild(o),o.click(),document.body.removeChild(o),URL.revokeObjectURL(i);var d=Object.keys(n.browserState).length,l=n.themes?Object.keys(n.themes).length:0;P.innerHTML="\u2705 Full backup downloaded \u2014 server config + "+d+" browser settings"+(l?" + "+l+" themes":""),P.style.display="block",P.style.background="color-mix(in srgb, var(--ok-fg) 15%, transparent)",P.style.border="1px solid var(--ok-fg)"}catch(z){P.innerHTML="\u274C Export failed: "+escapeHtml(z.message),P.style.display="block",P.style.background="color-mix(in srgb, var(--bad-fg) 15%, transparent)",P.style.border="1px solid var(--bad-fg)"}y.disabled=!1,y.innerHTML="\u2B07\uFE0F Download Full Backup"}),S?.addEventListener("click",function(){k.click()}),k?.addEventListener("change",async function(e){var n=e.target.files[0];if(n){x.textContent="\u{1F4C4} "+n.name,x.style.display="block",P.style.display="none";try{var t=await n.text(),i=JSON.parse(t);if(I(i)){D=i;var o='
Legacy format (v'+escapeHtml(i.version)+")
";o+='
',i.services?.length&&(o+='\u{1F4CB} '+i.services.length+" services"),i.customApps?.length&&(o+='\u{1F4E6} '+i.customApps.length+" custom apps"),i.theme&&(o+='\u{1F3A8} Theme: '+escapeHtml(i.theme)+""),i.userThemes&&(o+='\u{1F3A8} '+Object.keys(i.userThemes).length+" custom themes"),o+="
",M.innerHTML=o,B.style.display="block";return}var d=await secureFetch("/api/v1/backup/preview",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)}),l=await d.json();if(l.success){D=i;var o='
Exported: '+new Date(i.exportedAt).toLocaleString()+" (v"+escapeHtml(i.version)+")
";o+='
Server Config
',o+='
';for(var z in l.preview.files){var O=l.preview.files[z],F=O.action==="create"?"\u{1F195}":"\u{1F4DD}";o+=''+F+" "+escapeHtml(O.description)+""}o+="
",l.preview.serviceCount&&(o+='
'+l.preview.serviceCount+" services
"),l.preview.themeCount&&(o+='
\u{1F3A8} '+l.preview.themeCount+" custom themes
"),l.preview.browserStateCount&&(o+='
Browser Preferences
',o+='
\u{1F5A5}\uFE0F '+l.preview.browserStateCount+" saved settings (theme, weather, clock, widgets, etc.)
"),M.innerHTML=o,B.style.display="block"}else P.innerHTML="\u26A0\uFE0F Invalid backup file: "+escapeHtml(l.error),P.style.display="block",P.style.background="color-mix(in srgb, #f39c12 15%, transparent)",P.style.border="1px solid #f39c12",B.style.display="none"}catch(q){P.innerHTML="\u274C Could not read file: "+escapeHtml(q.message),P.style.display="block",P.style.background="color-mix(in srgb, var(--bad-fg) 15%, transparent)",P.style.border="1px solid var(--bad-fg)",B.style.display="none"}}}),R?.addEventListener("click",async function(){if(D&&confirm("This will overwrite your current configuration and browser preferences. Continue?")){R.disabled=!0,R.innerHTML=' Restoring...';try{if(I(D)){L(D),P.innerHTML="\u2705 Legacy backup restored \u2014 browser settings and services imported.",P.style.background="color-mix(in srgb, var(--ok-fg) 15%, transparent)",P.style.border="1px solid var(--ok-fg)",P.style.display="block",setTimeout(function(){location.reload()},2e3),R.disabled=!1,R.innerHTML="\u26A1 Restore Everything";return}var e=document.getElementById("backup-reload-caddy")?.checked??!0,n=await secureFetch("/api/v1/backup/restore",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({backup:D,options:{reloadCaddy:e}})}),t=await n.json(),i=0;if(D.browserState&&(i=N(D.browserState)),t.success){var o="\u2705 "+t.message;i>0&&(o+='
'+i+" browser settings restored"),t.results.caddyReloaded&&(o+='
Caddy configuration reloaded'),P.innerHTML=o,P.style.background="color-mix(in srgb, var(--ok-fg) 15%, transparent)",P.style.border="1px solid var(--ok-fg)",setTimeout(function(){location.reload()},2e3)}else P.innerHTML="\u26A0\uFE0F "+escapeHtml(t.message),i>0&&(P.innerHTML+='
'+i+" browser settings were restored"),t.results?.errors?.length>0&&(P.innerHTML+="
"+t.results.errors.map(function(d){return escapeHtml(d.file)+": "+escapeHtml(d.error)}).join(", ")+""),P.style.background="color-mix(in srgb, #f39c12 15%, transparent)",P.style.border="1px solid #f39c12";P.style.display="block"}catch(d){P.innerHTML="\u274C Restore failed: "+escapeHtml(d.message),P.style.display="block",P.style.background="color-mix(in srgb, var(--bad-fg) 15%, transparent)",P.style.border="1px solid var(--bad-fg)"}R.disabled=!1,R.innerHTML="\u26A1 Restore Everything"}});var b={type:"local"};async function H(){if(j)try{var e=await fetch("/api/v1/backups/config"),n=await e.json();if(!n.success)throw new Error(n.error||"Failed to load config");var t=n.config?.backups||{},i=Object.keys(t)[0],o=i?t[i]:null,d=o?.destinations&&o.destinations[0]||{type:"local"};b=JSON.parse(JSON.stringify(d));var l='
';l+='

\u23F0 Backup Schedule

',l+='
',l+='
',l+='
",l+='
',l+='
",l+="
",l+='
',l+='
",l+='
',l+=' ',l+=' ',l+="
",l+="
",l+='
',l+='

\u2601\uFE0F Backup Destination

',l+='
',l+='
",l+='
',l+='',l+="
",l+='',j.innerHTML=l,document.getElementById("backup-save-schedule")?.addEventListener("click",h),document.getElementById("backup-run-now")?.addEventListener("click",m);var z=document.getElementById("backup-dest-type");z?.addEventListener("change",function(){b={type:z.value},f(z.value)}),f(b.type)}catch(O){j.innerHTML='
Failed to load schedule: '+escapeHtml(O.message)+"
"}}async function f(e){var n=document.getElementById("backup-dest-form");if(n){if(e==="local"){n.innerHTML='
Backups are stored on the host filesystem. No additional configuration required.
';return}var t="";if(e==="dropbox"?(t+='',t+='',t+='',t+='',t+='
Generate a token at Dropbox App Console with files.content.write + files.content.read scopes.
'):e==="webdav"?(t+='',t+='',t+='
',t+='
',t+='
',t+='
',t+='
',t+="
",t+='',t+=''):e==="sftp"&&(t+='
',t+='
',t+='
',t+='
',t+='
',t+="
",t+='',t+='',t+='',t+='",t+='
',t+='
',t+='',t+='',t+=''),t+='
',t+=' ',t+=' ',t+=' ',t+="
",n.innerHTML=t,e==="sftp"){var i=document.getElementById("dest-sftp-authtype"),o=document.getElementById("dest-sftp-password-row"),d=document.getElementById("dest-sftp-key-row");i?.addEventListener("change",function(){i.value==="key"?(o.style.display="none",d.style.display=""):(o.style.display="",d.style.display="none")})}document.getElementById("dest-save-creds")?.addEventListener("click",function(){u(e)}),document.getElementById("dest-test-conn")?.addEventListener("click",function(){s(e)}),document.getElementById("dest-clear-creds")?.addEventListener("click",function(){r(e)}),await g(e)}}function p(e,n){var t=document.getElementById("backup-dest-result");t&&(t.innerHTML=e,t.style.display="block",t.style.background=n?"color-mix(in srgb, var(--ok-fg) 15%, transparent)":"color-mix(in srgb, var(--bad-fg) 15%, transparent)",t.style.border=n?"1px solid var(--ok-fg)":"1px solid var(--bad-fg)")}async function g(e){try{var n=await fetch("/api/v1/backups/credentials/"+e),t=await n.json();if(!t.success||!t.credentials)return;var i=t.credentials;if(e==="dropbox"){var o=document.getElementById("dest-dropbox-token");o&&i.token&&(o.value=i.token)}else if(e==="webdav"){var d=document.getElementById("dest-webdav-url");d&&i.url&&(d.value=i.url);var l=document.getElementById("dest-webdav-username");l&&i.username&&(l.value=i.username);var z=document.getElementById("dest-webdav-password");z&&i.password&&(z.value=i.password)}else if(e==="sftp"){var O=document.getElementById("dest-sftp-host");O&&i.host&&(O.value=i.host);var F=document.getElementById("dest-sftp-port");F&&i.port&&(F.value=i.port);var q=document.getElementById("dest-sftp-username");q&&i.username&&(q.value=i.username);var U=document.getElementById("dest-sftp-password");U&&i.password&&(U.value=i.password);var G=document.getElementById("dest-sftp-privatekey");if(G&&i.privateKey&&(G.value=i.privateKey),i.privateKey){var W=document.getElementById("dest-sftp-authtype");W&&(W.value="key",W.dispatchEvent(new Event("change")))}}}catch{}}function v(e){if(e==="dropbox")return{token:document.getElementById("dest-dropbox-token")?.value};if(e==="webdav")return{url:document.getElementById("dest-webdav-url")?.value,username:document.getElementById("dest-webdav-username")?.value,password:document.getElementById("dest-webdav-password")?.value};if(e==="sftp"){var n=document.getElementById("dest-sftp-authtype")?.value,t={host:document.getElementById("dest-sftp-host")?.value,port:parseInt(document.getElementById("dest-sftp-port")?.value)||22,username:document.getElementById("dest-sftp-username")?.value};return n==="key"?t.privateKey=document.getElementById("dest-sftp-privatekey")?.value:t.password=document.getElementById("dest-sftp-password")?.value,t}return{}}async function u(e){try{var n=v(e),t=await secureFetch("/api/v1/backups/credentials/"+e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)}),i=await t.json();p(i.success?"\u2705 Credentials saved":"\u26A0\uFE0F "+escapeHtml(i.error||"Failed"),i.success)}catch(o){p("\u274C "+escapeHtml(o.message),!1)}}async function r(e){if(confirm("Delete saved "+e+" credentials?"))try{var n=await secureFetch("/api/v1/backups/credentials/"+e,{method:"DELETE"}),t=await n.json();t.success?(p("\u2705 Credentials cleared",!0),f(e)):p("\u26A0\uFE0F "+escapeHtml(t.error||"Failed"),!1)}catch(i){p("\u274C "+escapeHtml(i.message),!1)}}function c(e){var n={type:e};return e==="local"||(e==="dropbox"?n.path=document.getElementById("dest-dropbox-path")?.value||"/dashcaddy-backups":e==="webdav"?n.path=document.getElementById("dest-webdav-path")?.value||"/dashcaddy-backups":e==="sftp"&&(n.path=document.getElementById("dest-sftp-path")?.value||"/dashcaddy-backups")),n}async function s(e){p(' Testing connection...',!0);try{var n=c(e),t=await secureFetch("/api/v1/backups/test-destination",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)}),i=await t.json();if(i.success){var o=i.elapsedMs?" ("+i.elapsedMs+"ms)":"";p("\u2705 Connection OK"+o+" \u2014 write/read/delete probe succeeded",!0)}else p("\u274C "+escapeHtml(i.error||"Connection failed"),!1)}catch(d){p("\u274C "+escapeHtml(d.message),!1)}}async function h(){var e=document.getElementById("backup-schedule-select")?.value,n=parseInt(document.getElementById("backup-retention-select")?.value)||5,t=document.getElementById("backup-encrypt-toggle")?.checked??!0,i=document.getElementById("backup-dest-type")?.value||"local",o=document.getElementById("backup-schedule-result");try{var d=await secureFetch("/api/v1/backups/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({backups:{auto:{enabled:e!=="disabled",schedule:e==="disabled"?"daily":e,include:["all"],encrypt:t,verify:!0,retention:{keep:n},destinations:[c(i)]}}})}),l=await d.json();o&&(o.innerHTML=l.success?"\u2705 Schedule saved":"\u26A0\uFE0F "+escapeHtml(l.error),o.style.display="block",o.style.background=l.success?"color-mix(in srgb, var(--ok-fg) 15%, transparent)":"color-mix(in srgb, var(--bad-fg) 15%, transparent)",o.style.border=l.success?"1px solid var(--ok-fg)":"1px solid var(--bad-fg)",setTimeout(function(){o&&(o.style.display="none")},3e3))}catch(z){o&&(o.innerHTML="\u274C "+escapeHtml(z.message),o.style.display="block",o.style.background="color-mix(in srgb, var(--bad-fg) 15%, transparent)",o.style.border="1px solid var(--bad-fg)")}}async function m(){var e=document.getElementById("backup-run-now"),n=document.getElementById("backup-schedule-result"),t=document.getElementById("backup-dest-type")?.value||"local";e&&(e.disabled=!0,e.innerHTML=' Running...');try{var i=await secureFetch("/api/v1/backups/execute",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({include:["all"],destinations:[c(t)]})}),o=await i.json();if(n){if(o.success){var d=o.backup?.size?(o.backup.size/1024/1024).toFixed(2):"?";n.innerHTML="\u2705 Backup complete ("+d+" MB)",n.style.background="color-mix(in srgb, var(--ok-fg) 15%, transparent)",n.style.border="1px solid var(--ok-fg)"}else n.innerHTML="\u26A0\uFE0F "+escapeHtml(o.error),n.style.background="color-mix(in srgb, var(--bad-fg) 15%, transparent)",n.style.border="1px solid var(--bad-fg)";n.style.display="block"}a()}catch(l){n&&(n.innerHTML="\u274C "+escapeHtml(l.message),n.style.display="block",n.style.background="color-mix(in srgb, var(--bad-fg) 15%, transparent)",n.style.border="1px solid var(--bad-fg)")}e&&(e.disabled=!1,e.innerHTML="\u25B6\uFE0F Run Backup Now")}async function a(){if(C){C.innerHTML='
Loading...
';try{var e=await fetch("/api/v1/backups/history?limit=50"),n=await e.json();if(!n.success||!n.history?.length){C.innerHTML='
\u{1F4CB} No backup history yet
';return}for(var t='
',i=0;i',t+='
',t+=' '+escapeHtml(o.name||"backup")+"",t+='
',t+=' '+escapeHtml(o.status)+"",o.status==="success"&&(t+=' '),t+="
",t+="
",t+='
',t+=" "+new Date(o.timestamp).toLocaleString()+" | "+d+" MB | "+(o.duration?(o.duration/1e3).toFixed(1)+"s":"--"),o.encrypted&&(t+=" | \u{1F512}"),t+="
",t+="
"}t+="",C.innerHTML=t,C.querySelectorAll(".backup-restore-btn").forEach(function(l){l.addEventListener("click",function(){window.__restoreServerBackup(l.dataset.backupId)})})}catch(l){C.innerHTML='
Failed: '+escapeHtml(l.message)+"
"}}}window.__restoreServerBackup=async function(e){if(confirm("Restore from this server backup? This will overwrite current configuration."))try{var n=await secureFetch("/api/v1/backups/restore/"+e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({restoreServices:!0,restoreConfig:!0})}),t=await n.json();t.success?(showNotification("Restore completed successfully!","success"),location.reload()):showNotification("Restore failed: "+(t.error||"Unknown error"),"error")}catch(i){showNotification("Restore error: "+i.message,"error")}},document.querySelector('[data-panel="backup-automated"]')?.addEventListener("click",H),document.querySelector('[data-panel="backup-history-tab"]')?.addEventListener("click",a)})(),(function(){injectModal("stats-modal",`

\u{1F4CA} Resource Monitor

- `);const k=document.getElementById("stats-modal"),E=document.getElementById("container-stats-btn"),z=document.getElementById("stats-cancel"),I=document.getElementById("stats-refresh-btn"),L=document.getElementById("stats-auto-refresh"),A=document.getElementById("stats-container"),T=document.getElementById("stats-aggregated-container"),$=document.getElementById("stats-alerts-container"),y=document.getElementById("stats-last-update");let S=null,w=null;function x(s){if(s===0||!s)return"0 B";const h=1024,u=["B","KB","MB","GB"],a=Math.floor(Math.log(s)/Math.log(h));return parseFloat((s/Math.pow(h,a)).toFixed(1))+" "+u[a]}function C(s){return s<30?"#2ecc71":s<70?"#f39c12":"#e74c3c"}function M(s){return s<50?"#2ecc71":s<80?"#f39c12":"#e74c3c"}async function R(){try{let s=null,h=!1;try{const e=await(await fetch("/api/v1/monitoring/stats")).json();e.success&&e.stats&&(s=e.stats,h=!0,w=e.stats)}catch{}if(!h){const e=await(await fetch("/api/v1/stats/containers")).json();if(e.success&&e.stats){s={};for(const n of e.stats)s[n.name]={name:n.name,current:{cpu:n.cpu,memory:{percent:n.memory.percent,usage:n.memory.used,limit:n.memory.limit,usageMB:Math.round(n.memory.used/1048576),limitMB:Math.round(n.memory.limit/1048576)},network:{rxBytes:n.network.rx,txBytes:n.network.tx,rxMB:(n.network.rx/1048576).toFixed(1),txMB:(n.network.tx/1048576).toFixed(1)},disk:{readMB:0,writeMB:0}},status:n.status};w=s}}if(!s||Object.keys(s).length===0){A.innerHTML='
No running containers found
';return}let u='
';for(const[a,e]of Object.entries(s)){const n=e.current||e,t=n.cpu?.percent||0,i=n.memory?.percent||0,o=C(t),d=M(i),l=n.memory?.usage||n.memory?.used||0,N=n.memory?.limit||0,O=n.network?.rxBytes||n.network?.rx||0,F=n.network?.txBytes||n.network?.tx||0,q=e.aggregated;u+=` +
`);const w=document.getElementById("stats-modal"),E=document.getElementById("container-stats-btn"),N=document.getElementById("stats-cancel"),I=document.getElementById("stats-refresh-btn"),L=document.getElementById("stats-auto-refresh"),A=document.getElementById("stats-container"),T=document.getElementById("stats-aggregated-container"),$=document.getElementById("stats-alerts-container"),y=document.getElementById("stats-last-update");let S=null,k=null;function x(s){if(s===0||!s)return"0 B";const h=1024,m=["B","KB","MB","GB"],a=Math.floor(Math.log(s)/Math.log(h));return parseFloat((s/Math.pow(h,a)).toFixed(1))+" "+m[a]}function B(s){return s<30?"#2ecc71":s<70?"#f39c12":"#e74c3c"}function M(s){return s<50?"#2ecc71":s<80?"#f39c12":"#e74c3c"}async function R(){try{let s=null,h=!1;try{const e=await(await fetch("/api/v1/monitoring/stats")).json();e.success&&e.stats&&(s=e.stats,h=!0,k=e.stats)}catch{}if(!h){const e=await(await fetch("/api/v1/stats/containers")).json();if(e.success&&e.stats){s={};for(const n of e.stats)s[n.name]={name:n.name,current:{cpu:n.cpu,memory:{percent:n.memory.percent,usage:n.memory.used,limit:n.memory.limit,usageMB:Math.round(n.memory.used/1048576),limitMB:Math.round(n.memory.limit/1048576)},network:{rxBytes:n.network.rx,txBytes:n.network.tx,rxMB:(n.network.rx/1048576).toFixed(1),txMB:(n.network.tx/1048576).toFixed(1)},disk:{readMB:0,writeMB:0}},status:n.status};k=s}}if(!s||Object.keys(s).length===0){A.innerHTML='
No running containers found
';return}let m='
';for(const[a,e]of Object.entries(s)){const n=e.current||e,t=n.cpu?.percent||0,i=n.memory?.percent||0,o=B(t),d=M(i),l=n.memory?.usage||n.memory?.used||0,z=n.memory?.limit||0,O=n.network?.rxBytes||n.network?.rx||0,F=n.network?.txBytes||n.network?.tx||0,q=e.aggregated;m+=`
${e.name||a} @@ -1138,7 +1138,7 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};
${i.toFixed(1)}%
-
${x(l)} / ${x(N)}
+
${x(l)} / ${x(z)}
Network
@@ -1149,8 +1149,8 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};
- `}u+="",A.innerHTML=u,y.textContent="Updated: "+new Date().toLocaleTimeString()}catch(s){A.innerHTML=`
\u274C Failed to load stats: ${escapeHtml(s.message)}
`}}async function D(){if(!T)return;const s=w;if(!s||Object.keys(s).length===0){T.innerHTML='
\u{1F4C8}No monitoring data available. Open the Live Stats tab first.
';return}let h='
';for(const[u,a]of Object.entries(s)){const e=a.aggregated;e&&(h+=`
-
${a.name||u}
+
`}m+="
",A.innerHTML=m,y.textContent="Updated: "+new Date().toLocaleTimeString()}catch(s){A.innerHTML=`
\u274C Failed to load stats: ${escapeHtml(s.message)}
`}}async function P(){if(!T)return;const s=k;if(!s||Object.keys(s).length===0){T.innerHTML='
\u{1F4C8}No monitoring data available. Open the Live Stats tab first.
';return}let h='
';for(const[m,a]of Object.entries(s)){const e=a.aggregated;e&&(h+=`
+
${a.name||m}
${e.cpu?.avg?.toFixed(1)||0}%Avg CPU
${e.cpu?.max?.toFixed(1)||0}%Max CPU
@@ -1158,49 +1158,49 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};
${e.memory?.max?.toFixed(1)||0}%Max Mem
${e.dataPoints?`
${e.dataPoints} data points over ${e.timeRange||24}h
`:""} -
`)}h+="
",T.innerHTML=h}async function j(){if(!$)return;$.innerHTML='
Loading alerts...
';const s=w;if(!s||Object.keys(s).length===0){$.innerHTML='
\u{1F514}No containers found. Open the Live Stats tab first.
';return}let h='
';for(const[u,a]of Object.entries(s)){const e=a.alertConfig||{};h+=`
+
`)}h+="
",T.innerHTML=h}async function j(){if(!$)return;$.innerHTML='
Loading alerts...
';const s=k;if(!s||Object.keys(s).length===0){$.innerHTML='
\u{1F514}No containers found. Open the Live Stats tab first.
';return}let h='
';for(const[m,a]of Object.entries(s)){const e=a.alertConfig||{};h+=`
- ${a.name||u} + ${a.name||m}
- +
- +
- +
- +
-
`}h+="
",$.innerHTML=h,$.querySelectorAll(".alert-save-btn").forEach(u=>{u.addEventListener("click",async()=>{const a=u.dataset.container,e=$.querySelector(`.alert-enabled[data-container="${a}"]`)?.checked||!1,n=parseInt($.querySelector(`.alert-cpu[data-container="${a}"]`)?.value)||80,t=parseInt($.querySelector(`.alert-mem[data-container="${a}"]`)?.value)||85,i=parseInt($.querySelector(`.alert-cooldown[data-container="${a}"]`)?.value)||15,o=$.querySelector(`.alert-autorestart[data-container="${a}"]`)?.checked||!1;try{const l=await(await secureFetch(`/api/v1/monitoring/alerts/${a}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({enabled:e,cpuThreshold:n,memoryThreshold:t,cooldownMinutes:i,autoRestart:o})})).json();u.textContent=l.success?"\u2705 Saved":"\u26A0\uFE0F Failed",setTimeout(()=>{u.textContent="Save"},2e3)}catch{u.textContent="\u274C Error",setTimeout(()=>{u.textContent="Save"},2e3)}})})}function B(){S&&clearInterval(S),L?.checked&&(S=setInterval(R,DC.POLL.STATS))}function P(){S&&(clearInterval(S),S=null)}E?.addEventListener("click",()=>{k.classList.add("show"),R(),B()}),z?.addEventListener("click",()=>{k.classList.remove("show"),P()}),k?.addEventListener("click",s=>{s.target===k&&(k.classList.remove("show"),P())}),I?.addEventListener("click",R),L?.addEventListener("change",()=>{L.checked?B():P()}),document.querySelector('[data-panel="stats-aggregated"]')?.addEventListener("click",D),document.querySelector('[data-panel="stats-alerts"]')?.addEventListener("click",j);const b=document.getElementById("stats-history-container"),H=document.getElementById("stats-history-container-area"),f=document.querySelectorAll(".stats-range-btn");let p="1h";function g(s){switch(s){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 v(s){return s==="raw"?"live (10s samples)":s==="hourly"?"hourly average":s==="daily"?"daily average":s}function m(s,h,u,a,e){if(!s||s.length===0)return`
No data for ${escapeHtml(a)}
`;const n=s.map(h).filter(G=>G!=null);if(n.length===0)return`
No data for ${escapeHtml(a)}
`;const t=Math.max(...n,1),i=Math.min(...n,0),o=t-i||1,d=600,l=80,N=4,O=(d-N*2)/Math.max(n.length-1,1),F=n.map((G,W)=>{const X=N+W*O,Q=l-N-(G-i)/o*(l-N*2);return`${X.toFixed(1)},${Q.toFixed(1)}`}).join(" "),q=n[n.length-1],U=n.reduce((G,W)=>G+W,0)/n.length;return` + `}h+="",$.innerHTML=h,$.querySelectorAll(".alert-save-btn").forEach(m=>{m.addEventListener("click",async()=>{const a=m.dataset.container,e=$.querySelector(`.alert-enabled[data-container="${a}"]`)?.checked||!1,n=parseInt($.querySelector(`.alert-cpu[data-container="${a}"]`)?.value)||80,t=parseInt($.querySelector(`.alert-mem[data-container="${a}"]`)?.value)||85,i=parseInt($.querySelector(`.alert-cooldown[data-container="${a}"]`)?.value)||15,o=$.querySelector(`.alert-autorestart[data-container="${a}"]`)?.checked||!1;try{const l=await(await secureFetch(`/api/v1/monitoring/alerts/${a}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({enabled:e,cpuThreshold:n,memoryThreshold:t,cooldownMinutes:i,autoRestart:o})})).json();m.textContent=l.success?"\u2705 Saved":"\u26A0\uFE0F Failed",setTimeout(()=>{m.textContent="Save"},2e3)}catch{m.textContent="\u274C Error",setTimeout(()=>{m.textContent="Save"},2e3)}})})}function C(){S&&clearInterval(S),L?.checked&&(S=setInterval(R,DC.POLL.STATS))}function D(){S&&(clearInterval(S),S=null)}E?.addEventListener("click",()=>{w.classList.add("show"),R(),C()}),N?.addEventListener("click",()=>{w.classList.remove("show"),D()}),w?.addEventListener("click",s=>{s.target===w&&(w.classList.remove("show"),D())}),I?.addEventListener("click",R),L?.addEventListener("change",()=>{L.checked?C():D()}),document.querySelector('[data-panel="stats-aggregated"]')?.addEventListener("click",P),document.querySelector('[data-panel="stats-alerts"]')?.addEventListener("click",j);const b=document.getElementById("stats-history-container"),H=document.getElementById("stats-history-container-area"),f=document.querySelectorAll(".stats-range-btn");let p="1h";function g(s){switch(s){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 v(s){return s==="raw"?"live (10s samples)":s==="hourly"?"hourly average":s==="daily"?"daily average":s}function u(s,h,m,a,e){if(!s||s.length===0)return`
No data for ${escapeHtml(a)}
`;const n=s.map(h).filter(G=>G!=null);if(n.length===0)return`
No data for ${escapeHtml(a)}
`;const t=Math.max(...n,1),i=Math.min(...n,0),o=t-i||1,d=600,l=80,z=4,O=(d-z*2)/Math.max(n.length-1,1),F=n.map((G,W)=>{const X=z+W*O,Q=l-z-(G-i)/o*(l-z*2);return`${X.toFixed(1)},${Q.toFixed(1)}`}).join(" "),q=n[n.length-1],U=n.reduce((G,W)=>G+W,0)/n.length;return`
${escapeHtml(a)} last ${q.toFixed(1)}${e} \xB7 avg ${U.toFixed(1)}${e} \xB7 max ${t.toFixed(1)}${e}
- +
- `}function r(){if(!b)return;const s=w||{},h=b.value,u=Object.entries(s);if(u.length===0){b.innerHTML='';return}b.innerHTML=u.map(([a,e])=>``).join(""),h&&s[h]&&(b.value=h)}async function c(){if(!H||!b)return;const s=b.value;if(!s){H.innerHTML='
\u{1F4CA}No container selected.
';return}const h=Date.now(),u=h-g(p);H.innerHTML='
Loading history...
';try{const e=await(await fetch(`/api/v1/monitoring/history/${encodeURIComponent(s)}?startTime=${u}&endTime=${h}`)).json();if(!e.success)throw new Error(e.error||"Failed to load history");const n=e.samples||[],t=e.tier||"raw";if(n.length===0){H.innerHTML=`
\u{1F4CA}No data for the last ${p}. Tier: ${v(t)}.
`;return}const i=t==="raw",o=i?F=>F.cpu?.percent:F=>F.cpu?.avg,d=i?F=>F.memory?.percent:F=>F.memory?.avgPercent,l=i?F=>F.network?.rxMB||0:F=>F.network?.rxMB||0,N=i?F=>F.network?.txMB||0:F=>F.network?.txMB||0;let O=` + `}function r(){if(!b)return;const s=k||{},h=b.value,m=Object.entries(s);if(m.length===0){b.innerHTML='';return}b.innerHTML=m.map(([a,e])=>``).join(""),h&&s[h]&&(b.value=h)}async function c(){if(!H||!b)return;const s=b.value;if(!s){H.innerHTML='
\u{1F4CA}No container selected.
';return}const h=Date.now(),m=h-g(p);H.innerHTML='
Loading history...
';try{const e=await(await fetch(`/api/v1/monitoring/history/${encodeURIComponent(s)}?startTime=${m}&endTime=${h}`)).json();if(!e.success)throw new Error(e.error||"Failed to load history");const n=e.samples||[],t=e.tier||"raw";if(n.length===0){H.innerHTML=`
\u{1F4CA}No data for the last ${p}. Tier: ${v(t)}.
`;return}const i=t==="raw",o=i?F=>F.cpu?.percent:F=>F.cpu?.avg,d=i?F=>F.memory?.percent:F=>F.memory?.avgPercent,l=i?F=>F.network?.rxMB||0:F=>F.network?.rxMB||0,z=i?F=>F.network?.txMB||0:F=>F.network?.txMB||0;let O=`
- ${n.length} samples \xB7 ${escapeHtml(v(t))} \xB7 ${new Date(u).toLocaleString()} \u2192 ${new Date(h).toLocaleString()} + ${n.length} samples \xB7 ${escapeHtml(v(t))} \xB7 ${new Date(m).toLocaleString()} \u2192 ${new Date(h).toLocaleString()}
- `;O+=m(n,o,"#2ecc71","CPU","%"),O+=m(n,d,"#3498db","Memory","%"),O+=m(n,l,"#9b59b6","Network RX"," MB"),O+=m(n,N,"#e67e22","Network TX"," MB"),H.innerHTML=O}catch(a){H.innerHTML=`
\u26A0\uFE0FFailed to load history: ${escapeHtml(a.message)}
`}}f.forEach(s=>{s.addEventListener("click",()=>{f.forEach(h=>h.classList.remove("active")),s.classList.add("active"),p=s.dataset.range,c()})}),b?.addEventListener("change",c),document.querySelector('[data-panel="stats-history"]')?.addEventListener("click",()=>{r(),c()})})(),(function(){injectModal("health-modal",`
+ `;O+=u(n,o,"#2ecc71","CPU","%"),O+=u(n,d,"#3498db","Memory","%"),O+=u(n,l,"#9b59b6","Network RX"," MB"),O+=u(n,z,"#e67e22","Network TX"," MB"),H.innerHTML=O}catch(a){H.innerHTML=`
\u26A0\uFE0FFailed to load history: ${escapeHtml(a.message)}
`}}f.forEach(s=>{s.addEventListener("click",()=>{f.forEach(h=>h.classList.remove("active")),s.classList.add("active"),p=s.dataset.range,c()})}),b?.addEventListener("change",c),document.querySelector('[data-panel="stats-history"]')?.addEventListener("click",()=>{r(),c()})})(),(function(){injectModal("health-modal",`

\u{1F3E5} Health Check Dashboard

-
`);const k=document.getElementById("health-modal"),E=document.getElementById("health-check-btn"),z=document.getElementById("health-cancel"),I=document.getElementById("health-refresh-btn"),L=document.getElementById("health-status-container"),A=document.getElementById("health-incidents-container"),T=document.getElementById("health-config-container"),$=document.getElementById("health-last-update"),y=document.getElementById("health-add-btn"),S=document.getElementById("health-config-form"),w=document.getElementById("health-form-title"),x=document.getElementById("health-form-cancel"),C=document.getElementById("health-form-save");let M=null;function R(f){return f>=99.9?"var(--ok-fg)":f>=95?"#f39c12":"var(--bad-fg)"}function D(f){const p={critical:"var(--bad-fg)",high:"#ff6b6b",medium:"#f39c12",low:"var(--muted)"};return`${f}`}async function j(){try{const p=await(await fetch("/api/v1/health-checks/status")).json();if(!p.success||!p.status||Object.keys(p.status).length===0){L.innerHTML='
\u{1F3E5}No health checks configured. Go to the Configure tab to add services.
';return}const g=Object.values(p.status);let v='';v+='',v+='',v+='',v+='';for(const m of g){const r=m.status==="up",c=r?"var(--dot-ok)":"var(--dot-bad)",s=m.uptime?.["24h"]??"-",h=m.uptime?.["7d"]??"-",u=m.avgResponseTime!=null?Math.round(m.avgResponseTime)+"ms":"-",a=m.timestamp?timeAgo(m.timestamp):"-";v+=``,v+=``,v+=``,v+=``,v+=``,v+=``,v+=``,v+="",v+=``}v+="
ServiceStatusUptime 24hUptime 7dAvg ResponseLast Check
${escapeHtml(m.name||m.serviceId)}${r?"Up":"Down"}${typeof s=="number"?s.toFixed(1)+"%":s}${typeof h=="number"?h.toFixed(1)+"%":h}${u}${a}
",L.innerHTML=v,$.textContent="Updated "+new Date().toLocaleTimeString(),L.querySelectorAll("tr[data-health-id]").forEach(m=>{m.addEventListener("click",async()=>{const r=m.dataset.healthId,c=document.getElementById("health-detail-"+r);if(c){if(c.style.display!=="none"){c.style.display="none";return}c.style.display="";try{const h=await(await fetch(`/api/v1/health-checks/${r}/stats?hours=24`)).json();if(h.success&&h.stats){const u=h.stats,a=u.responseTime||{};c.querySelector("td").innerHTML=` + `);const w=document.getElementById("health-modal"),E=document.getElementById("health-check-btn"),N=document.getElementById("health-cancel"),I=document.getElementById("health-refresh-btn"),L=document.getElementById("health-status-container"),A=document.getElementById("health-incidents-container"),T=document.getElementById("health-config-container"),$=document.getElementById("health-last-update"),y=document.getElementById("health-add-btn"),S=document.getElementById("health-config-form"),k=document.getElementById("health-form-title"),x=document.getElementById("health-form-cancel"),B=document.getElementById("health-form-save");let M=null;function R(f){return f>=99.9?"var(--ok-fg)":f>=95?"#f39c12":"var(--bad-fg)"}function P(f){const p={critical:"var(--bad-fg)",high:"#ff6b6b",medium:"#f39c12",low:"var(--muted)"};return`${f}`}async function j(){try{const p=await(await fetch("/api/v1/health-checks/status")).json();if(!p.success||!p.status||Object.keys(p.status).length===0){L.innerHTML='
\u{1F3E5}No health checks configured. Go to the Configure tab to add services.
';return}const g=Object.values(p.status);let v='';v+='',v+='',v+='',v+='';for(const u of g){const r=u.status==="up",c=r?"var(--dot-ok)":"var(--dot-bad)",s=u.uptime?.["24h"]??"-",h=u.uptime?.["7d"]??"-",m=u.avgResponseTime!=null?Math.round(u.avgResponseTime)+"ms":"-",a=u.timestamp?timeAgo(u.timestamp):"-";v+=``,v+=``,v+=``,v+=``,v+=``,v+=``,v+=``,v+="",v+=``}v+="
ServiceStatusUptime 24hUptime 7dAvg ResponseLast Check
${escapeHtml(u.name||u.serviceId)}${r?"Up":"Down"}${typeof s=="number"?s.toFixed(1)+"%":s}${typeof h=="number"?h.toFixed(1)+"%":h}${m}${a}
",L.innerHTML=v,$.textContent="Updated "+new Date().toLocaleTimeString(),L.querySelectorAll("tr[data-health-id]").forEach(u=>{u.addEventListener("click",async()=>{const r=u.dataset.healthId,c=document.getElementById("health-detail-"+r);if(c){if(c.style.display!=="none"){c.style.display="none";return}c.style.display="";try{const h=await(await fetch(`/api/v1/health-checks/${r}/stats?hours=24`)).json();if(h.success&&h.stats){const m=h.stats,a=m.responseTime||{};c.querySelector("td").innerHTML=`
-
Total Checks
${u.totalChecks||0}
-
Uptime
${(u.uptime||0).toFixed(2)}%
+
Total Checks
${m.totalChecks||0}
+
Uptime
${(m.uptime||0).toFixed(2)}%
Avg Response
${Math.round(a.avg||0)}ms
P95 / P99
${Math.round(a.p95||0)}ms / ${Math.round(a.p99||0)}ms
Min Response
${Math.round(a.min||0)}ms
Max Response
${Math.round(a.max||0)}ms
-
Up Checks
${u.upChecks||0}
-
Down Checks
${u.downChecks||0}
-
`}else c.querySelector("td").innerHTML='
No detailed stats available for this period.
'}catch(s){c.querySelector("td").innerHTML=`
Failed: ${escapeHtml(s.message)}
`}}})})}catch(f){L.innerHTML=`
Failed to load health status: ${escapeHtml(f.message)}
`}}async function B(){try{const[f,p]=await Promise.all([fetch("/api/v1/health-checks/incidents"),fetch("/api/v1/health-checks/incidents/history?limit=50")]),g=await f.json(),v=await p.json();let m="";const r=g.success&&g.incidents?g.incidents:[];if(r.length>0){m+='

Open Incidents ('+r.length+")

";for(const s of r)m+=`
+
Up Checks
${m.upChecks||0}
+
Down Checks
${m.downChecks||0}
+
`}else c.querySelector("td").innerHTML='
No detailed stats available for this period.
'}catch(s){c.querySelector("td").innerHTML=`
Failed: ${escapeHtml(s.message)}
`}}})})}catch(f){L.innerHTML=`
Failed to load health status: ${escapeHtml(f.message)}
`}}async function C(){try{const[f,p]=await Promise.all([fetch("/api/v1/health-checks/incidents"),fetch("/api/v1/health-checks/incidents/history?limit=50")]),g=await f.json(),v=await p.json();let u="";const r=g.success&&g.incidents?g.incidents:[];if(r.length>0){u+='

Open Incidents ('+r.length+")

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

Incident History

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

Incident History

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

\u2B06\uFE0F Update Management

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

\u{1F433} Docker Resources

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

\u{1F4E6} Import Docker Compose

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

Terminal

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

\u{1F4DC} Audit Log

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

Weather Settings

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

Weather Settings

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