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

Home Lab Configuration

@@ -333,12 +333,12 @@ This will reset the logo, favicon, title, and position.`))try{if((await secureFe
-
`);const w="custom-apps";let B=null,z=null;const I=document.getElementById("app-selector-modal"),C=document.getElementById("app-selector-grid");async function P(){try{const l=await(await fetch("/api/v1/apps/templates")).json();if(l.success)return B=l.templates,z=l.categories,!0}catch(o){console.error("Failed to fetch app templates:",o)}return!1}async function $(o){try{return await(await fetch(`/api/v1/apps/ports/${o}/check`)).json()}catch(l){return console.error("Failed to check port:",l),{available:!0}}}async function L(o){try{const v=await(await fetch(`/api/v1/apps/ports/${o}/suggest`)).json();if(v.success)return v.suggestedPort}catch(l){console.error("Failed to get suggested port:",l)}return o}async function g(){if(C.innerHTML='
Loading app templates...
',!B&&!await P()){C.innerHTML='
Failed to load app templates. Please try again.
';return}C.innerHTML="";const o={};for(const[v,r]of Object.entries(B)){const p=r.category||"Other";o[p]||(o[p]=[]),o[p].push({id:v,...r})}const l=z?Object.keys(z):Object.keys(o).sort();for(const v of l){const r=o[v];if(!r||r.length===0)continue;r.sort((n,e)=>(e.popularity||0)-(n.popularity||0));const p=document.createElement("div");p.className="app-category-header";const m=z?.[v]||{};p.innerHTML=`${escapeHtml(m.icon||"")} ${escapeHtml(v)}`,m.color&&(p.style.borderBottomColor=m.color),C.appendChild(p),r.forEach(n=>{const e=document.createElement("div");e.className="app-option";const a=n.isDashboardWidget,t=a&&safeGet("widget-"+n.id+"-enabled")!=="false",s=a?`
${t?"ON":"OFF"}
`:"",i=!a&&n.difficulty?`
${escapeHtml(n.difficulty)}
`:"";e.innerHTML=` -
${escapeHtml(n.icon||"\u{1F4E6}")}
-
${escapeHtml(n.name)}
-
${escapeHtml(n.description||"")}
+
`);const w="custom-apps";let B=null,z=null;const I=document.getElementById("app-selector-modal"),C=document.getElementById("app-selector-grid");async function P(){try{const l=await(await fetch("/api/v1/apps/templates")).json();if(l.success)return B=l.templates,z=l.categories,!0}catch(o){console.error("Failed to fetch app templates:",o)}return!1}async function $(o){try{return await(await fetch(`/api/v1/apps/ports/${o}/check`)).json()}catch(l){return console.error("Failed to check port:",l),{available:!0}}}async function L(o){try{const v=await(await fetch(`/api/v1/apps/ports/${o}/suggest`)).json();if(v.success)return v.suggestedPort}catch(l){console.error("Failed to get suggested port:",l)}return o}async function g(){if(C.innerHTML='
Loading app templates...
',!B&&!await P()){C.innerHTML='
Failed to load app templates. Please try again.
';return}C.innerHTML="";const o={};for(const[v,r]of Object.entries(B)){const p=r.category||"Other";o[p]||(o[p]=[]),o[p].push({id:v,...r})}const l=z?Object.keys(z):Object.keys(o).sort();for(const v of l){const r=o[v];if(!r||r.length===0)continue;r.sort((a,e)=>(e.popularity||0)-(a.popularity||0));const p=document.createElement("div");p.className="app-category-header";const m=z?.[v]||{};p.innerHTML=`${escapeHtml(m.icon||"")} ${escapeHtml(v)}`,m.color&&(p.style.borderBottomColor=m.color),C.appendChild(p),r.forEach(a=>{const e=document.createElement("div");e.className="app-option";const n=a.isDashboardWidget,t=n&&safeGet("widget-"+a.id+"-enabled")!=="false",s=n?`
${t?"ON":"OFF"}
`:"",i=!n&&a.difficulty?`
${escapeHtml(a.difficulty)}
`:"";e.innerHTML=` +
${escapeHtml(a.icon||"\u{1F4E6}")}
+
${escapeHtml(a.name)}
+
${escapeHtml(a.description||"")}
${s}${i} - `,a?e.onclick=()=>k(n,e):e.onclick=()=>f(n),C.appendChild(e)})}window.renderRecipeCards&&await window.renderRecipeCards(C)}function k(o,l){const v="widget-"+o.id+"-enabled",p=!(safeGet(v)!=="false");safeSet(v,String(p));const m=o.widgetSelector;if(m){const e=document.querySelector(m);e&&(e.style.display=p?"":"none")}const n=l.querySelector('div[style*="border-radius: 4px"]');n&&(n.textContent=p?"ON":"OFF",n.style.background=p?"#2ecc7130":"#e74c3c30",n.style.color=p?"#2ecc71":"#e74c3c"),showNotification(`${o.name} widget ${p?"enabled":"disabled"}`,"success",2e3)}async function f(o){const l=document.getElementById("app-deploy-modal"),v=document.getElementById("app-deploy-title"),r=document.getElementById("deploy-subdomain"),p=document.getElementById("deploy-url-preview"),m=document.getElementById("deploy-ip"),n=document.getElementById("deploy-port"),e=document.getElementById("deploy-tailscale-only"),a=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:o.id})})).json();if(J.success&&J.exists){const V=J.container;confirm(`Found existing ${o.name} container: + `,n?e.onclick=()=>k(a,e):e.onclick=()=>f(a),C.appendChild(e)})}window.renderRecipeCards&&await window.renderRecipeCards(C)}function k(o,l){const v="widget-"+o.id+"-enabled",p=!(safeGet(v)!=="false");safeSet(v,String(p));const m=o.widgetSelector;if(m){const e=document.querySelector(m);e&&(e.style.display=p?"":"none")}const a=l.querySelector('div[style*="border-radius: 4px"]');a&&(a.textContent=p?"ON":"OFF",a.style.background=p?"#2ecc7130":"#e74c3c30",a.style.color=p?"#2ecc71":"#e74c3c"),showNotification(`${o.name} widget ${p?"enabled":"disabled"}`,"success",2e3)}async function f(o){const l=document.getElementById("app-deploy-modal"),v=document.getElementById("app-deploy-title"),r=document.getElementById("deploy-subdomain"),p=document.getElementById("deploy-url-preview"),m=document.getElementById("deploy-ip"),a=document.getElementById("deploy-port"),e=document.getElementById("deploy-tailscale-only"),n=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:o.id})})).json();if(J.success&&J.exists){const V=J.container;confirm(`Found existing ${o.name} container: Container: ${V.name} Status: ${V.status} @@ -352,15 +352,15 @@ Click Cancel to deploy a new container.`)&&(o._useExisting=!0,o._existingContain style="flex: 1; padding: 8px; background: var(--card-bg); color: var(--fg); border: 1px solid var(--border); border-radius: 6px; font-size: 0.85rem;" /> \u2192 ${ee} - `,ae.appendChild(Y),Y.querySelector(".vol-browse-btn").onclick=()=>{const le=Y.querySelector(".vol-host-path");openFolderBrowser(le)}})):Q.style.display="none"}else Q.style.display="none";const ne=o.defaultPort||8080;n.value="",n.placeholder=`Default: ${ne}`;let X=document.getElementById("deploy-port-status");X||(X=document.createElement("div"),X.id="deploy-port-status",X.style.cssText="font-size: 0.8rem; margin-top: 4px;",n.parentNode.appendChild(X));async function se(){const q=n.value||ne;X.innerHTML='Checking port...';const J=await $(q);if(J.available)X.innerHTML=`Port ${escapeHtml(String(q))} is available`;else{const V=await L(ne);X.innerHTML=` + `,ae.appendChild(Y),Y.querySelector(".vol-browse-btn").onclick=()=>{const le=Y.querySelector(".vol-host-path");openFolderBrowser(le)}})):Q.style.display="none"}else Q.style.display="none";const ne=o.defaultPort||8080;a.value="",a.placeholder=`Default: ${ne}`;let X=document.getElementById("deploy-port-status");X||(X=document.createElement("div"),X.id="deploy-port-status",X.style.cssText="font-size: 0.8rem; margin-top: 4px;",a.parentNode.appendChild(X));async function se(){const q=a.value||ne;X.innerHTML='Checking port...';const J=await $(q);if(J.available)X.innerHTML=`Port ${escapeHtml(String(q))} is available`;else{const V=await L(ne);X.innerHTML=` Port ${escapeHtml(q)} in use by ${escapeHtml(J.conflict?.usedBy||"unknown")} - `;const K=document.createElement("button");K.type="button",K.textContent=`Use ${V}`,K.style.cssText="margin-left: 8px; padding: 2px 8px; font-size: 0.75rem; cursor: pointer;",K.onclick=()=>{document.getElementById("deploy-port").value=V,X.innerHTML=`Using suggested port ${escapeHtml(String(V))}`},X.appendChild(K)}}let ie;n.oninput=function(){clearTimeout(ie),ie=setTimeout(se,500)},se();try{const J=await(await fetch("/api/v1/tailscale/status")).json();J.success&&J.installed&&J.connected?a.innerHTML=` + `;const K=document.createElement("button");K.type="button",K.textContent=`Use ${V}`,K.style.cssText="margin-left: 8px; padding: 2px 8px; font-size: 0.75rem; cursor: pointer;",K.onclick=()=>{document.getElementById("deploy-port").value=V,X.innerHTML=`Using suggested port ${escapeHtml(String(V))}`},X.appendChild(K)}}let ie;a.oninput=function(){clearTimeout(ie),ie=setTimeout(se,500)},se();try{const J=await(await fetch("/api/v1/tailscale/status")).json();J.success&&J.installed&&J.connected?n.innerHTML=` Connected ${J.self?.hostname} (${J.self?.ip}) | ${J.deviceCount} devices - `:J.installed?a.innerHTML='Not connected':(a.innerHTML='Not available',e.disabled=!0)}catch{a.innerHTML='Could not check status'}function oe(){const q=r.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}/${q}`;else if(J==="private")K=`${V==="none"?"http":"https"}://${buildDomain(q)}`;else if(J==="public"){const te=V==="none"?"http":"https",ee=SITE.domain||q;K=SITE.domain?`${te}://${q}.${SITE.domain}`:`${te}://${q}`}else{const te=n.value||o.defaultPort||DC.DEFAULTS.SERVICE_PORT;K=`http://${m.value}:${te}`}p.textContent=K}r.oninput=oe,m.oninput=oe,n.oninput=oe,document.querySelectorAll('input[name="dns-type"]').forEach(q=>{q.onchange=oe}),document.querySelectorAll('input[name="ssl-type"]').forEach(q=>{q.onchange=oe}),oe(),I.classList.remove("show"),l.classList.add("show"),l.dataset.appTemplate=JSON.stringify(o)}async function x(o){const l=o.appTemplate,v=safeGetJSON(w,[]),r=l._useExisting&&l._existingContainer,p=v.find(m=>m.id===o.subdomain);if(!(p&&!r&&!confirm(`An app with subdomain "${o.subdomain}" already exists. Redeploy?`))){if(p){const m=v.indexOf(p);v.splice(m,1),safeSet(w,JSON.stringify(v))}if(r)o.port=l._existingContainer.primaryPort;else{const m=o.port||l.defaultPort||8080;showNotification(`Checking port ${m} availability...`,"info",0);const n=await $(m);if(!n.available){const e=await L(l.defaultPort||8080);if(confirm(`Port ${m} is already in use by ${n.conflict?.usedBy||"another container"}. + `:J.installed?n.innerHTML='Not connected':(n.innerHTML='Not available',e.disabled=!0)}catch{n.innerHTML='Could not check status'}function oe(){const q=r.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}/${q}`;else if(J==="private")K=`${V==="none"?"http":"https"}://${buildDomain(q)}`;else if(J==="public"){const te=V==="none"?"http":"https",ee=SITE.domain||q;K=SITE.domain?`${te}://${q}.${SITE.domain}`:`${te}://${q}`}else{const te=a.value||o.defaultPort||DC.DEFAULTS.SERVICE_PORT;K=`http://${m.value}:${te}`}p.textContent=K}r.oninput=oe,m.oninput=oe,a.oninput=oe,document.querySelectorAll('input[name="dns-type"]').forEach(q=>{q.onchange=oe}),document.querySelectorAll('input[name="ssl-type"]').forEach(q=>{q.onchange=oe}),oe(),I.classList.remove("show"),l.classList.add("show"),l.dataset.appTemplate=JSON.stringify(o)}async function x(o){const l=o.appTemplate,v=safeGetJSON(w,[]),r=l._useExisting&&l._existingContainer,p=v.find(m=>m.id===o.subdomain);if(!(p&&!r&&!confirm(`An app with subdomain "${o.subdomain}" already exists. Redeploy?`))){if(p){const m=v.indexOf(p);v.splice(m,1),safeSet(w,JSON.stringify(v))}if(r)o.port=l._existingContainer.primaryPort;else{const m=o.port||l.defaultPort||8080;showNotification(`Checking port ${m} availability...`,"info",0);const a=await $(m);if(!a.available){const e=await L(l.defaultPort||8080);if(confirm(`Port ${m} is already in use by ${a.conflict?.usedBy||"another container"}. -Would you like to use port ${e} instead?`))o.port=e;else{showNotification("Deployment cancelled - port conflict","error",5e3);return}}}showNotification(r?`Configuring ${l.name} with existing container...`:`Deploying ${l.name}...`,"info",0);try{const m={appId:l.id,config:{subdomain:o.subdomain,ip:o.ip,createDns:o.dnsType==="private",port:o.port||l.defaultPort||null,sslType:o.sslType,dnsType:o.dnsType,tailscaleOnly:o.tailscaleOnly||!1,mediaPath:o.mediaPath||null,plexClaimToken:o.plexClaimToken||null,customVolumes:o.customVolumes||null}};r&&(m.config.useExisting=!0,m.config.existingContainerId=l._existingContainer.id,m.config.existingPort=l._existingContainer.primaryPort,!o.port&&l._existingContainer.primaryPort&&(m.config.port=l._existingContainer.primaryPort));const e=await(await secureFetch("/api/v1/apps/deploy",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(m)})).json();if(e.success){const a={id:o.subdomain,name:l.name,logo:`/assets/${l.id}.png`,containerId:e.containerId,url:e.url,ip:o.ip,appTemplate:l.id,tailscaleOnly:o.tailscaleOnly||!1};v.push(a),safeSet(w,JSON.stringify(v)),window.APPS&&!window.APPS.some(s=>s.id===l.id)&&(window.APPS.push(a),typeof window.buildGrid=="function"&&window.buildGrid(),typeof window.refreshAll=="function"&&setTimeout(()=>window.refreshAll(),500));let t=e.usedExisting?`${l.name} configured with existing container! +Would you like to use port ${e} instead?`))o.port=e;else{showNotification("Deployment cancelled - port conflict","error",5e3);return}}}showNotification(r?`Configuring ${l.name} with existing container...`:`Deploying ${l.name}...`,"info",0);try{const m={appId:l.id,config:{subdomain:o.subdomain,ip:o.ip,createDns:o.dnsType==="private",port:o.port||l.defaultPort||null,sslType:o.sslType,dnsType:o.dnsType,tailscaleOnly:o.tailscaleOnly||!1,mediaPath:o.mediaPath||null,plexClaimToken:o.plexClaimToken||null,customVolumes:o.customVolumes||null}};r&&(m.config.useExisting=!0,m.config.existingContainerId=l._existingContainer.id,m.config.existingPort=l._existingContainer.primaryPort,!o.port&&l._existingContainer.primaryPort&&(m.config.port=l._existingContainer.primaryPort));const e=await(await secureFetch("/api/v1/apps/deploy",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(m)})).json();if(e.success){const n={id:o.subdomain,name:l.name,logo:`/assets/${l.id}.png`,containerId:e.containerId,url:e.url,ip:o.ip,appTemplate:l.id,tailscaleOnly:o.tailscaleOnly||!1};v.push(n),safeSet(w,JSON.stringify(v)),window.APPS&&!window.APPS.some(s=>s.id===l.id)&&(window.APPS.push(n),typeof window.buildGrid=="function"&&window.buildGrid(),typeof window.refreshAll=="function"&&setTimeout(()=>window.refreshAll(),500));let t=e.usedExisting?`${l.name} configured with existing container! URL: ${e.url}`:`${l.name} deployed successfully! URL: ${e.url}`;e.warning&&(t+=` @@ -369,10 +369,10 @@ URL: ${e.url}`;e.warning&&(t+=` Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};setTimeout(p,3e3)}function R(){safeGetJSON(w,[]).forEach(l=>{window.APPS.some(v=>v.id===l.id)||window.APPS.push(l)})}document.getElementById("add-service-btn")?.addEventListener("click",()=>{g(),I.classList.add("show")}),wireModal(I,document.getElementById("app-selector-cancel"));const O=document.getElementById("app-deploy-modal");document.getElementById("app-deploy-cancel")?.addEventListener("click",()=>{O.classList.remove("show")}),document.getElementById("app-deploy-confirm")?.addEventListener("click",()=>{const o=JSON.parse(O.dataset.appTemplate),l=document.getElementById("deploy-media-path").value.trim(),v=[];document.querySelectorAll("#volume-mounts-list .vol-host-path").forEach(p=>{v.push({hostPath:p.value.trim(),containerPath:p.dataset.containerPath})});const r={appTemplate:o,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:l||null,plexClaimToken:document.getElementById("deploy-plex-claim")?.value.trim()||null,customVolumes:v.length>0?v:null,resources:{cpus:parseFloat(document.getElementById("deploy-cpu-limit").value)||0,memory:parseFloat(document.getElementById("deploy-memory-limit").value)||0}};if(!r.subdomain){showNotification("Please enter a subdomain or domain name","warning");return}if(o.mediaMount?.required&&!l){showNotification("Please enter a media library path for this application","warning");return}O.classList.remove("show"),x(r)}),wireModal(O);const D=document.getElementById("folder-browser-modal"),N=document.getElementById("folder-browser-path"),M=document.getElementById("folder-browser-list"),H=document.getElementById("folder-browser-selected"),S=document.getElementById("folder-browser-selected-list");let T="",b=[],u=null;window.openFolderBrowser=function(o){u=o,b=o.value.split(",").map(l=>l.trim()).filter(l=>l),T="",c(),y(""),D.classList.add("show")};async function y(o){N.textContent=o||"Select a drive...",M.innerHTML='
Loading...
';try{const v=await(await fetch(`/api/v1/browse/directories?path=${encodeURIComponent(o)}`)).json();if(!v.success){M.innerHTML=`
Error: ${escapeHtml(v.error)}
`;return}T=v.path||"",N.textContent=T||"Select a drive...";let r="";v.parent&&v.parent!==v.path&&(r+=`
\u2B06\uFE0F .. Parent Directory -
`),v.items.length===0&&!v.parent?r+='
No browseable drives configured. Check your docker-compose.yml volume mounts.
':v.items.length===0?r+='
No subfolders found
':v.items.forEach(p=>{const m=p.type==="drive"?"\u{1F4BE}":"\u{1F4C1}",n=b.includes(p.path),e=n?"background: color-mix(in srgb, var(--success) 20%, transparent);":"";r+=`
+
`),v.items.length===0&&!v.parent?r+='
No browseable drives configured. Check your docker-compose.yml volume mounts.
':v.items.length===0?r+='
No subfolders found
':v.items.forEach(p=>{const m=p.type==="drive"?"\u{1F4BE}":"\u{1F4C1}",a=b.includes(p.path),e=a?"background: color-mix(in srgb, var(--success) 20%, transparent);":"";r+=`
${m} ${escapeHtml(p.name)} - ${n?'\u2713':""} + ${a?'\u2713':""}
`}),M.innerHTML=r,M.querySelectorAll(".folder-item").forEach(p=>{p.addEventListener("click",()=>{y(p.dataset.path)}),p.addEventListener("mouseenter",()=>{p.style.background="var(--card-bg)"}),p.addEventListener("mouseleave",()=>{const m=b.includes(p.dataset.path);p.style.background=m?"color-mix(in srgb, var(--success) 20%, transparent)":""})})}catch(l){M.innerHTML=`
Failed to load: ${escapeHtml(l.message)}
`}}function c(){if(b.length===0){H.style.display="none";return}H.style.display="block",S.innerHTML=b.map(o=>` ${escapeHtml(o)} @@ -638,22 +638,22 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}}; - `);const w=document.getElementById("arr-setup-modal"),B=document.getElementById("arr-setup-btn"),z=document.getElementById("arr-setup-cancel"),I=document.getElementById("smart-connect-btn"),C=document.getElementById("smart-phase-detect"),P=document.getElementById("smart-phase-credentials"),$=document.getElementById("smart-phase-progress"),L=document.getElementById("smart-phase-results"),g=document.getElementById("smart-detect-results"),k=document.getElementById("smart-credential-inputs"),f=document.getElementById("smart-progress-steps"),x=document.getElementById("smart-results-content"),E=document.getElementById("smart-plex-libraries"),R=document.getElementById("smart-retry-btn");let O=null;const D={plex:"\u{1F3AC}",radarr:"\u{1F3AC}",sonarr:"\u{1F4FA}",prowlarr:"\u{1F50D}",seerr:"\u{1F4CB}"},N={plex:"Plex",radarr:"Radarr (Movies)",sonarr:"Sonarr (TV)",prowlarr:"Prowlarr (Indexers)",seerr:"Seerr"};function M(c){C.style.display=c==="detect"?"block":"none",P.style.display=c==="credentials"?"block":"none",$.style.display=c==="progress"?"block":"none",L.style.display=c==="results"?"block":"none"}function H(c){const o={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"}},l=o[c]||o.not_found;return`${l.icon} ${l.text}`}async function S(){M("detect"),g.style.display="none";try{if(O=await(await fetch("/api/v1/arr/smart-detect")).json(),!O.success){g.innerHTML=`
Detection failed: ${escapeHtml(O.error)}
`,g.style.display="block";return}let o='
';for(const[v,r]of Object.entries(O.services)){const p=D[v]||"\u{1F4E6}",m=N[v]||v,n=r.source?`${escapeHtml(r.source)}`:"",e=r.version?`v${escapeHtml(r.version)}`:"",a=(r.hasApiKey||r.hasToken)&&r.status==="connected"?'Key saved':"";o+=`
+
`);const w=document.getElementById("arr-setup-modal"),B=document.getElementById("arr-setup-btn"),z=document.getElementById("arr-setup-cancel"),I=document.getElementById("smart-connect-btn"),C=document.getElementById("smart-phase-detect"),P=document.getElementById("smart-phase-credentials"),$=document.getElementById("smart-phase-progress"),L=document.getElementById("smart-phase-results"),g=document.getElementById("smart-detect-results"),k=document.getElementById("smart-credential-inputs"),f=document.getElementById("smart-progress-steps"),x=document.getElementById("smart-results-content"),E=document.getElementById("smart-plex-libraries"),R=document.getElementById("smart-retry-btn");let O=null;const D={plex:"\u{1F3AC}",radarr:"\u{1F3AC}",sonarr:"\u{1F4FA}",prowlarr:"\u{1F50D}",seerr:"\u{1F4CB}"},N={plex:"Plex",radarr:"Radarr (Movies)",sonarr:"Sonarr (TV)",prowlarr:"Prowlarr (Indexers)",seerr:"Seerr"};function M(c){C.style.display=c==="detect"?"block":"none",P.style.display=c==="credentials"?"block":"none",$.style.display=c==="progress"?"block":"none",L.style.display=c==="results"?"block":"none"}function H(c){const o={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"}},l=o[c]||o.not_found;return`${l.icon} ${l.text}`}async function S(){M("detect"),g.style.display="none";try{if(O=await(await fetch("/api/v1/arr/smart-detect")).json(),!O.success){g.innerHTML=`
Detection failed: ${escapeHtml(O.error)}
`,g.style.display="block";return}let o='
';for(const[v,r]of Object.entries(O.services)){const p=D[v]||"\u{1F4E6}",m=N[v]||v,a=r.source?`${escapeHtml(r.source)}`:"",e=r.version?`v${escapeHtml(r.version)}`:"",n=(r.hasApiKey||r.hasToken)&&r.status==="connected"?'Key saved':"";o+=`
${p}
${m}
- ${n} ${e} ${a} + ${a} ${e} ${n}
${H(r.status)}
`}o+="
";const l=O.summary;o+=`
${escapeHtml(String(l.fullyConnected))}/${escapeHtml(String(l.totalDetected+(5-l.totalDetected)))} services detected · ${escapeHtml(String(l.fullyConnected))} connected${l.needsApiKey>0?` · ${escapeHtml(String(l.needsApiKey))} needs API key`:""} -
`,g.innerHTML=o,g.style.display="block",T(O),setTimeout(()=>{M("credentials")},800)}catch(c){g.innerHTML=`
Error: ${escapeHtml(c.message)}
`,g.style.display="block"}}function T(c){let o="";const l=c.services,v=["radarr","sonarr","prowlarr"];for(const m of v){const n=l[m];if(!n||n.status==="not_found"&&!n.url)continue;const e=D[m],a=N[m],t=n.status==="connected";o+=`
+
`,g.innerHTML=o,g.style.display="block",T(O),setTimeout(()=>{M("credentials")},800)}catch(c){g.innerHTML=`
Error: ${escapeHtml(c.message)}
`,g.style.display="block"}}function T(c){let o="";const l=c.services,v=["radarr","sonarr","prowlarr"];for(const m of v){const a=l[m];if(!a||a.status==="not_found"&&!a.url)continue;const e=D[m],n=N[m],t=a.status==="connected";o+=`
${e} - ${a} + ${n} ${t?'✓ Connected':""} @@ -661,7 +661,7 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};
-
@@ -678,7 +678,7 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}}; ${H(r.status)} ${escapeHtml(r.source||"")}
-
`}const p=l.seerr;if(p){const m=p.status==="connected";let n="";if(p.configuredServices){const e=p.configuredServices;n=`
+
`}const p=l.seerr;if(p){const m=p.status==="connected";let a="";if(p.configuredServices){const e=p.configuredServices;a=`
Configured: ${e.radarr?"✓ Radarr":"✗ Radarr"} · ${e.sonarr?"✓ Sonarr":"✗ Sonarr"} · ${e.plex?"✓ Plex":"✗ Plex"} @@ -688,18 +688,18 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}}; Seerr ${H(p.status)}
- ${n} -
`}k.innerHTML=o}window.smartTestConnection=async function(c){const o=document.getElementById(`smart-${c}-url`),l=document.getElementById(`smart-${c}-key`),v=document.getElementById(`smart-${c}-status`),r=o?.value.trim(),p=l?.value.trim();if(!r||!p){v.innerHTML='Enter URL and API key';return}v.innerHTML='';try{const n=await(await secureFetch("/api/v1/arr/test-connection",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({service:c,url:r,apiKey:p})})).json();n.success?v.innerHTML=`✓ ${escapeHtml(n.appName||"Connected")} v${escapeHtml(n.version||"")}`:v.innerHTML=`✗ ${escapeHtml(n.error)}`}catch(m){v.innerHTML=`✗ ${escapeHtml(m.message)}`}};async function b(){M("progress"),f.innerHTML='
Connecting services...
';const c={};for(const l of["radarr","sonarr","prowlarr"]){const v=document.getElementById(`smart-${l}-url`)?.value.trim(),r=document.getElementById(`smart-${l}-key`)?.value.trim();r&&v?c[l]={apiKey:r,url:v}:r&&(c[l]={apiKey:r})}const o={services:Object.keys(c).length>0?c: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 v=await(await secureFetch("/api/v1/arr/smart-connect",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)})).json();let r="";for(const p of v.steps||[]){const m=p.status==="success"?'':'',n=p.status==="success"?"var(--muted)":"var(--bad-fg)";r+=`
+ ${a} +
`}k.innerHTML=o}window.smartTestConnection=async function(c){const o=document.getElementById(`smart-${c}-url`),l=document.getElementById(`smart-${c}-key`),v=document.getElementById(`smart-${c}-status`),r=o?.value.trim(),p=l?.value.trim();if(!r||!p){v.innerHTML='Enter URL and API key';return}v.innerHTML='';try{const a=await(await secureFetch("/api/v1/arr/test-connection",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({service:c,url:r,apiKey:p})})).json();a.success?v.innerHTML=`✓ ${escapeHtml(a.appName||"Connected")} v${escapeHtml(a.version||"")}`:v.innerHTML=`✗ ${escapeHtml(a.error)}`}catch(m){v.innerHTML=`✗ ${escapeHtml(m.message)}`}};async function b(){M("progress"),f.innerHTML='
Connecting services...
';const c={};for(const l of["radarr","sonarr","prowlarr"]){const v=document.getElementById(`smart-${l}-url`)?.value.trim(),r=document.getElementById(`smart-${l}-key`)?.value.trim();r&&v?c[l]={apiKey:r,url:v}:r&&(c[l]={apiKey:r})}const o={services:Object.keys(c).length>0?c: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 v=await(await secureFetch("/api/v1/arr/smart-connect",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)})).json();let r="";for(const p of v.steps||[]){const m=p.status==="success"?'':'',a=p.status==="success"?"var(--muted)":"var(--bad-fg)";r+=`
${m} ${escapeHtml(p.step)} - ${escapeHtml(p.details||"")} + ${escapeHtml(p.details||"")}
`}f.innerHTML=r,setTimeout(()=>u(v),500)}catch(l){f.innerHTML=`
Connection error: ${escapeHtml(l.message)}
`}}function u(c){M("results");const o=c.summary||{},l=o.failed===0&&o.succeeded>0,v=l?"var(--ok-fg)":"#f39c12",r=l?"✓":"⚠",p=l?"All Connected!":`${escapeHtml(String(o.succeeded))}/${escapeHtml(String(o.totalSteps))} Steps Succeeded`;let m=`
${r}
${p}
${escapeHtml(String(o.succeeded))} succeeded, ${escapeHtml(String(o.failed))} failed
-
`;m+='
';for(const n of c.steps||[]){const e=n.status==="success"?'':'';m+=`
- ${e} ${escapeHtml(n.step)} ${escapeHtml(n.details||"")} -
`}m+="
",x.innerHTML=m,R.style.display=o.failed>0?"block":"none",c.steps?.some(n=>n.step.includes("Plex")&&n.status==="success")&&y()}async function y(){try{const o=await(await fetch("/api/v1/plex/libraries")).json();if(o.success&&o.libraries?.length>0){let l=`
+
`;m+='
';for(const a of c.steps||[]){const e=a.status==="success"?'':'';m+=`
+ ${e} ${escapeHtml(a.step)} ${escapeHtml(a.details||"")} +
`}m+="
",x.innerHTML=m,R.style.display=o.failed>0?"block":"none",c.steps?.some(a=>a.step.includes("Plex")&&a.status==="success")&&y()}async function y(){try{const o=await(await fetch("/api/v1/plex/libraries")).json();if(o.success&&o.libraries?.length>0){let l=`

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

`;for(const v of o.libraries){const r=v.type==="movie"?"\u{1F3AC}":v.type==="show"?"\u{1F4FA}":"\u{1F3B5}";l+=`
${r} ${escapeHtml(v.title)} @@ -892,7 +892,7 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};
${O}
- `}).join(""):E.innerHTML='
No notifications yet
'}catch(f){console.error("Failed to load notification history:",f)}}async function g(){try{const f={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}},E=await(await secureFetch("/api/v1/notifications/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(f)})).json();E.success?(showNotification("Notification settings saved","success",3e3),w.classList.remove("show")):showNotification(`Failed to save: ${E.error}`,"error",3e3)}catch(f){showNotification(`Error: ${f.message}`,"error",3e3)}}async function k(f){try{const E=await(await secureFetch("/api/v1/notifications/test",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({provider:f})})).json();E.success?showNotification(`Test ${f} notification sent!`,"success",3e3):showNotification(`Test failed: ${E.error}`,"error",3e3)}catch(x){showNotification(`Error: ${x.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 x=await(await secureFetch("/api/v1/notifications/health-check",{method:"POST"})).json();x.success&&(document.getElementById("health-check-status").textContent=`Last check: ${new Date(x.lastCheck).toLocaleString()} (${x.containersMonitored} containers)`,showNotification("Health check completed","success",2e3))}catch(f){showNotification(`Error: ${f.message}`,"error",3e3)}}),B?.addEventListener("click",()=>{w.classList.add("show"),$(),L()}),z?.addEventListener("click",g),wireModal(w,I)})(),(function(){document.addEventListener("click",w=>{const B=w.target.closest(".panel-tab");if(!B)return;const z=B.dataset.panel;if(!z)return;const I=B.closest(".panel-tabs"),C=I.closest(".weather-modal-content");I.querySelectorAll(".panel-tab").forEach($=>$.classList.remove("active")),B.classList.add("active"),C.querySelectorAll(".panel-section").forEach($=>$.classList.remove("active"));const P=C.querySelector("#"+z);P&&P.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 B(){for(var e={},a=0;a + `}).join(""):E.innerHTML='
No notifications yet
'}catch(f){console.error("Failed to load notification history:",f)}}async function g(){try{const f={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}},E=await(await secureFetch("/api/v1/notifications/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(f)})).json();E.success?(showNotification("Notification settings saved","success",3e3),w.classList.remove("show")):showNotification(`Failed to save: ${E.error}`,"error",3e3)}catch(f){showNotification(`Error: ${f.message}`,"error",3e3)}}async function k(f){try{const E=await(await secureFetch("/api/v1/notifications/test",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({provider:f})})).json();E.success?showNotification(`Test ${f} notification sent!`,"success",3e3):showNotification(`Test failed: ${E.error}`,"error",3e3)}catch(x){showNotification(`Error: ${x.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 x=await(await secureFetch("/api/v1/notifications/health-check",{method:"POST"})).json();x.success&&(document.getElementById("health-check-status").textContent=`Last check: ${new Date(x.lastCheck).toLocaleString()} (${x.containersMonitored} containers)`,showNotification("Health check completed","success",2e3))}catch(f){showNotification(`Error: ${f.message}`,"error",3e3)}}),B?.addEventListener("click",()=>{w.classList.add("show"),$(),L()}),z?.addEventListener("click",g),wireModal(w,I)})(),(function(){document.addEventListener("click",w=>{const B=w.target.closest(".panel-tab");if(!B)return;const z=B.dataset.panel;if(!z)return;const I=B.closest(".panel-tabs"),C=I.closest(".weather-modal-content");I.querySelectorAll(".panel-tab").forEach($=>$.classList.remove("active")),B.classList.add("active"),C.querySelectorAll(".panel-section").forEach($=>$.classList.remove("active"));const P=C.querySelector("#"+z);P&&P.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 B(){for(var e={},n=0;n

\u{1F4BE} Backup & Restore

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

\u23F0 Backup Schedule

',d+='
',d+='
',d+='
",d+='
',d+='
",d+="
",d+='
',d+='
",d+='
',d+=' ',d+=' ',d+="
",d+="
",d+='
',d+='

\u2601\uFE0F Backup Destination

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

\u23F0 Backup Schedule

',d+='
',d+='
',d+='
",d+='
',d+='
",d+="
",d+='
',d+='
",d+='
',d+=' ',d+=' ',d+="
",d+="
",d+='
',d+='

\u2601\uFE0F Backup Destination

',d+='
',d+='
",d+='
',d+='',d+="
",d+='',N.innerHTML=d,document.getElementById("backup-save-schedule")?.addEventListener("click",p),document.getElementById("backup-run-now")?.addEventListener("click",m);var A=document.getElementById("backup-dest-type");A?.addEventListener("change",function(){S={type:A.value},b(A.value)}),b(S.type)}catch(F){N.innerHTML='
Failed to load schedule: '+escapeHtml(F.message)+"
"}}async function b(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 s=document.getElementById("dest-sftp-authtype"),i=document.getElementById("dest-sftp-password-row"),h=document.getElementById("dest-sftp-key-row");s?.addEventListener("change",function(){s.value==="key"?(i.style.display="none",h.style.display=""):(i.style.display="",h.style.display="none")})}document.getElementById("dest-save-creds")?.addEventListener("click",function(){o(e)}),document.getElementById("dest-test-conn")?.addEventListener("click",function(){r(e)}),document.getElementById("dest-clear-creds")?.addEventListener("click",function(){l(e)}),await y(e)}}function u(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 y(e){try{var n=await fetch("/api/v1/backups/credentials/"+e),t=await n.json();if(!t.success||!t.credentials)return;var s=t.credentials;if(e==="dropbox"){var i=document.getElementById("dest-dropbox-token");i&&s.token&&(i.value=s.token)}else if(e==="webdav"){var h=document.getElementById("dest-webdav-url");h&&s.url&&(h.value=s.url);var d=document.getElementById("dest-webdav-username");d&&s.username&&(d.value=s.username);var A=document.getElementById("dest-webdav-password");A&&s.password&&(A.value=s.password)}else if(e==="sftp"){var F=document.getElementById("dest-sftp-host");F&&s.host&&(F.value=s.host);var j=document.getElementById("dest-sftp-port");j&&s.port&&(j.value=s.port);var _=document.getElementById("dest-sftp-username");_&&s.username&&(_.value=s.username);var U=document.getElementById("dest-sftp-password");U&&s.password&&(U.value=s.password);var W=document.getElementById("dest-sftp-privatekey");if(W&&s.privateKey&&(W.value=s.privateKey),s.privateKey){var Z=document.getElementById("dest-sftp-authtype");Z&&(Z.value="key",Z.dispatchEvent(new Event("change")))}}}catch{}}function c(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 o(e){try{var n=c(e),t=await secureFetch("/api/v1/backups/credentials/"+e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)}),s=await t.json();u(s.success?"\u2705 Credentials saved":"\u26A0\uFE0F "+escapeHtml(s.error||"Failed"),s.success)}catch(i){u("\u274C "+escapeHtml(i.message),!1)}}async function l(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?(u("\u2705 Credentials cleared",!0),b(e)):u("\u26A0\uFE0F "+escapeHtml(t.error||"Failed"),!1)}catch(s){u("\u274C "+escapeHtml(s.message),!1)}}function v(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 r(e){u(' Testing connection...',!0);try{var n=v(e),t=await secureFetch("/api/v1/backups/test-destination",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)}),s=await t.json();if(s.success){var i=s.elapsedMs?" ("+s.elapsedMs+"ms)":"";u("\u2705 Connection OK"+i+" \u2014 write/read/delete probe succeeded",!0)}else u("\u274C "+escapeHtml(s.error||"Connection failed"),!1)}catch(h){u("\u274C "+escapeHtml(h.message),!1)}}async function p(){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,s=document.getElementById("backup-dest-type")?.value||"local",i=document.getElementById("backup-schedule-result");try{var h=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:[v(s)]}}})}),d=await h.json();i&&(i.innerHTML=d.success?"\u2705 Schedule saved":"\u26A0\uFE0F "+escapeHtml(d.error),i.style.display="block",i.style.background=d.success?"color-mix(in srgb, var(--ok-fg) 15%, transparent)":"color-mix(in srgb, var(--bad-fg) 15%, transparent)",i.style.border=d.success?"1px solid var(--ok-fg)":"1px solid var(--bad-fg)",setTimeout(function(){i&&(i.style.display="none")},3e3))}catch(A){i&&(i.innerHTML="\u274C "+escapeHtml(A.message),i.style.display="block",i.style.background="color-mix(in srgb, var(--bad-fg) 15%, transparent)",i.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 s=await secureFetch("/api/v1/backups/execute",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({include:["all"],destinations:[v(t)]})}),i=await s.json();if(n){if(i.success){var h=i.backup?.size?(i.backup.size/1024/1024).toFixed(2):"?";n.innerHTML="\u2705 Backup complete ("+h+" 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(i.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(d){n&&(n.innerHTML="\u274C "+escapeHtml(d.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(M){M.innerHTML='
Loading...
';try{var e=await fetch("/api/v1/backups/history?limit=50"),n=await e.json();if(!n.success||!n.history?.length){M.innerHTML='
\u{1F4CB} No backup history yet
';return}for(var t='
',s=0;s',t+='
',t+=' '+escapeHtml(i.name||"backup")+"",t+='
',t+=' '+escapeHtml(i.status)+"",i.status==="success"&&(t+=' '),t+="
",t+="
",t+='
',t+=" "+new Date(i.timestamp).toLocaleString()+" | "+h+" MB | "+(i.duration?(i.duration/1e3).toFixed(1)+"s":"--"),i.encrypted&&(t+=" | \u{1F512}"),t+="
",t+="
"}t+="",M.innerHTML=t,M.querySelectorAll(".backup-restore-btn").forEach(function(d){d.addEventListener("click",function(){window.__restoreServerBackup(d.dataset.backupId)})})}catch(d){M.innerHTML='
Failed: '+escapeHtml(d.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(s){showNotification("Restore error: "+s.message,"error")}},document.querySelector('[data-panel="backup-automated"]')?.addEventListener("click",T),document.querySelector('[data-panel="backup-history-tab"]')?.addEventListener("click",a)})(),(function(){injectModal("stats-modal",`

\u{1F4CA} Resource Monitor

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

\u{1F3E5} Health Check Dashboard

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

Open Incidents ('+l.length+")

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

\u{1F433} Docker Resources

@@ -1478,7 +1478,7 @@ Enter version to rollback to:`);if(!e)return;if(!n.includes(e)){showNotification
-
`);const w="weather-location",B="weather-zip",z="weather-geo",I="weather-unit";!safeGet(w)&&safeGet(B)&&safeSet(w,safeGet(B));function C(){return safeGet(I)||"imperial"}function P(){return{icon:document.querySelector(".weather-icon"),temp:document.querySelector(".weather-temp"),condition:document.querySelector(".weather-condition"),location:document.querySelector(".weather-location"),wind:document.querySelector(".weather-wind")}}const $={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"},L={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"},g=["N","NNE","NE","ENE","E","ESE","SE","SSE","S","SSW","SW","WSW","W","WNW","NW","NNW"];function k(N){return g[Math.round(N/22.5)%16]}async function f(N){const M=safeGet(z);if(M)try{const u=JSON.parse(M);if(u.query===N)return u}catch{}const H=await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(N)}&count=1&language=en&format=json`);if(!H.ok)throw new Error("Geocoding failed");const S=await H.json();if(!S.results||!S.results.length)throw new Error("Location not found");const T=S.results[0],b={query:N,lat:T.latitude,lon:T.longitude,city:T.name,state:T.admin1||"",country:T.country||"",countryCode:T.country_code||""};return safeSet(z,JSON.stringify(b)),b}function x(N){return N.countryCode==="US"&&N.state?`${N.city}, ${N.state}`:N.country?`${N.city}, ${N.country}`:N.city}async function E(N){try{const M=await f(N),H=C(),S=H==="metric"?"celsius":"fahrenheit",T=H==="metric"?"kmh":"mph",b=`https://api.open-meteo.com/v1/forecast?latitude=${M.lat}&longitude=${M.lon}¤t=temperature_2m,weather_code,wind_speed_10m,wind_direction_10m&temperature_unit=${S}&wind_speed_unit=${T}`,u=await fetch(b);if(!u.ok)throw new Error("Weather fetch failed");const c=(await u.json()).current,o=c.weather_code;return{temp:Math.round(c.temperature_2m),condition:$[o]||"Unknown",icon:L[o]||"\u{1F324}\uFE0F",locationStr:x(M),windSpeed:Math.round(c.wind_speed_10m),windDir:k(c.wind_direction_10m),unit:H}}catch(M){return console.warn("Weather fetch failed:",M),null}}async function R(){const N=P();if(!N.icon||!N.temp||!N.condition||!N.location||!N.wind){console.warn("Weather widget elements not found");return}const M=safeGet(w);if(!M){N.location.textContent="Set Location",N.temp.textContent="--\xB0",N.condition.textContent="Click \u2699\uFE0F to configure",N.wind.textContent="--",N.icon.innerHTML='\u{1F324}\uFE0F';return}try{const H=await E(M);if(H){const S=H.unit==="metric"?"\xB0C":"\xB0F",T=H.unit==="metric"?"km/h":"mph";N.location.textContent=H.locationStr,N.temp.textContent=`${H.temp}${S}`,N.condition.textContent=H.condition,N.wind.textContent=`Wind: ${H.windSpeed} ${T} ${H.windDir}`,N.icon.innerHTML=`${escapeHtml(H.icon)}`}}catch(H){console.error("Weather update error:",H),N.location.textContent="Weather Error",N.temp.textContent="Error",N.condition.textContent="Failed to load",N.wind.textContent="--"}}const O=document.getElementById("weather-modal"),D=document.getElementById("weather-location-input");document.getElementById("weather-settings")?.addEventListener("click",()=>{D.value=safeGet(w)||"";const N=C(),M=O.querySelector(`input[name="weather-unit-radio"][value="${N}"]`);M&&(M.checked=!0),O.classList.add("show"),D.focus()}),document.getElementById("weather-cancel")?.addEventListener("click",()=>{O.classList.remove("show")}),document.getElementById("weather-save")?.addEventListener("click",()=>{const N=D.value.trim();if(N){safeGet(w)!==N&&safeSet(z,""),safeSet(w,N);const H=O.querySelector('input[name="weather-unit-radio"]:checked'),S=H?H.value:"imperial",T=C();safeSet(I,S),T!==S&&safeSet(z,""),O.classList.remove("show"),R()}else showNotification("Please enter a location (e.g., Hamburg, London, 90210)","warning")}),wireModal(O),document.addEventListener("keydown",N=>{N.key==="Escape"&&O.classList.contains("show")&&O.classList.remove("show")}),R(),setInterval(R,DC.POLL.WEATHER)})(),(function(){const w=document.getElementById("clock-widget"),B=document.getElementById("clock-render");if(!w||!B)return;const z=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],I=["January","February","March","April","May","June","July","August","September","October","November","December"],C=["XII","I","II","III","IV","V","VI","VII","VIII","IX","X","XI"];let P=safeGet("clock-style")||"default",$=-1,L=!1,g="",k="",f=null,x=null;function E(n){if(L||safeGet("clock-chimes")!=="true")return;L=!0;const e=parseInt(safeGet("clock-chime-volume")||"50",10)/100;let a=0;function t(){if(a>=n){L=!1;return}const s=new Audio("/assets/sounds/church-bell.mp3");s.volume=e,s.play().catch(()=>{}),a++,a{L=!1},2500)}t()}function R(n){return z[n.getDay()]+", "+I[n.getMonth()]+" "+n.getDate()+", "+n.getFullYear()}function O(){k="",f=null}function D(){return k!=="digital"&&(B.innerHTML='
',f={main:B.querySelector(".clock-main"),seconds:B.querySelector(".clock-seconds"),ampm:B.querySelector(".clock-ampm"),date:B.querySelector(".clock-date")},k="digital"),f}function N(n){const e=n.getHours(),a=n.getMinutes(),t=n.getSeconds(),s=e>=12?"PM":"AM",i=e%12||12,h=D();h.main.textContent=`${i}:${String(a).padStart(2,"0")}`,h.seconds.textContent=`:${String(t).padStart(2,"0")}`,h.ampm.textContent=s,h.date.textContent=R(n)}function M(n,e){const a=n.getHours(),t=n.getMinutes(),s=n.getSeconds(),i=a>=12?"PM":"AM",h=a%12||12,d=D();d.main.textContent=`${String(h).padStart(2,"0")}:${String(t).padStart(2,"0")}`,d.seconds.textContent=`:${String(s).padStart(2,"0")}`,d.ampm.textContent=i,d.date.textContent=R(n)}function H(n){const e=n.getHours(),a=n.getMinutes(),t=n.getSeconds(),s=e>=12?"PM":"AM",i=e%12||12,h=String(i).padStart(2," ")+String(a).padStart(2,"0")+String(t).padStart(2,"0");let d='
';if(d+=S(h[0],0),d+=S(h[1],1),d+=':',d+=S(h[2],2),d+=S(h[3],3),d+=':',d+=S(h[4],4),d+=S(h[5],5),d+=`${s}`,d+="
",d+=`
${R(n)}
`,B.innerHTML=d,k="flip",g){for(let A=0;A<6;A++)if(h[A]!==g[A]){const F=B.querySelector(`.flip-card[data-idx="${A}"]`);F&&F.classList.add("flipping")}}g=h}function S(n,e){const a=n===" "?"":n;return`
${a}
${a}
`}function T(n){const e=n.getHours(),a=n.getMinutes(),t=n.getSeconds(),s=e%12||12,i=e>=12?"PM":"AM",h=[Math.floor(s/10),s%10,Math.floor(a/10),a%10,Math.floor(t/10),t%10];let d='
';d+='
HHMMSS
';for(let A=3;A>=0;A--){d+='
';for(let F=0;F<6;F++){const j=h[F]>>A&1;d+=`
`}d+="
"}d+='
';for(let A=0;A<6;A++)d+=`${h[A]}`;d+="
",d+=`
${i}
`,d+="
",d+=`
${R(n)}
`,B.innerHTML=d,k="binary"}function b(n,e){const a=n.getHours(),t=n.getMinutes(),s=n.getSeconds(),i=120,h=i/2,d=i/2,A=s/60*360-90,F=(t+s/60)/60*360-90,j=(a%12+t/60)/12*360-90;let _="";for(let G=1;G<=12;G++){const Q=G/12*2*Math.PI-Math.PI/2,ae=47,ne=h+ae*Math.cos(Q),X=d+ae*Math.sin(Q),se=e?C[G%12]:G;_+=`${se}`}let U="";for(let G=0;G<60;G++){const Q=G/60*2*Math.PI-Math.PI/2,ae=56,ne=G%5===0?52:54,X=h+ne*Math.cos(Q),se=d+ne*Math.sin(Q),ie=h+ae*Math.cos(Q),oe=d+ae*Math.sin(Q),q=G%5===0?1.5:.5;U+=``}const W=` +
`);const w="weather-location",B="weather-zip",z="weather-geo",I="weather-unit";!safeGet(w)&&safeGet(B)&&safeSet(w,safeGet(B));function C(){return safeGet(I)||"imperial"}function P(){return{icon:document.querySelector(".weather-icon"),temp:document.querySelector(".weather-temp"),condition:document.querySelector(".weather-condition"),location:document.querySelector(".weather-location"),wind:document.querySelector(".weather-wind")}}const $={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"},L={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"},g=["N","NNE","NE","ENE","E","ESE","SE","SSE","S","SSW","SW","WSW","W","WNW","NW","NNW"];function k(N){return g[Math.round(N/22.5)%16]}async function f(N){const M=safeGet(z);if(M)try{const u=JSON.parse(M);if(u.query===N)return u}catch{}const H=await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(N)}&count=1&language=en&format=json`);if(!H.ok)throw new Error("Geocoding failed");const S=await H.json();if(!S.results||!S.results.length)throw new Error("Location not found");const T=S.results[0],b={query:N,lat:T.latitude,lon:T.longitude,city:T.name,state:T.admin1||"",country:T.country||"",countryCode:T.country_code||""};return safeSet(z,JSON.stringify(b)),b}function x(N){return N.countryCode==="US"&&N.state?`${N.city}, ${N.state}`:N.country?`${N.city}, ${N.country}`:N.city}async function E(N){try{const M=await f(N),H=C(),S=H==="metric"?"celsius":"fahrenheit",T=H==="metric"?"kmh":"mph",b=`https://api.open-meteo.com/v1/forecast?latitude=${M.lat}&longitude=${M.lon}¤t=temperature_2m,weather_code,wind_speed_10m,wind_direction_10m&temperature_unit=${S}&wind_speed_unit=${T}`,u=await fetch(b);if(!u.ok)throw new Error("Weather fetch failed");const c=(await u.json()).current,o=c.weather_code;return{temp:Math.round(c.temperature_2m),condition:$[o]||"Unknown",icon:L[o]||"\u{1F324}\uFE0F",locationStr:x(M),windSpeed:Math.round(c.wind_speed_10m),windDir:k(c.wind_direction_10m),unit:H}}catch(M){return console.warn("Weather fetch failed:",M),null}}async function R(){const N=P();if(!N.icon||!N.temp||!N.condition||!N.location||!N.wind){console.warn("Weather widget elements not found");return}const M=safeGet(w);if(!M){N.location.textContent="Set Location",N.temp.textContent="--\xB0",N.condition.textContent="Click \u2699\uFE0F to configure",N.wind.textContent="--",N.icon.innerHTML='\u{1F324}\uFE0F';return}try{const H=await E(M);if(H){const S=H.unit==="metric"?"\xB0C":"\xB0F",T=H.unit==="metric"?"km/h":"mph";N.location.textContent=H.locationStr,N.temp.textContent=`${H.temp}${S}`,N.condition.textContent=H.condition,N.wind.textContent=`Wind: ${H.windSpeed} ${T} ${H.windDir}`,N.icon.innerHTML=`${escapeHtml(H.icon)}`}}catch(H){console.error("Weather update error:",H),N.location.textContent="Weather Error",N.temp.textContent="Error",N.condition.textContent="Failed to load",N.wind.textContent="--"}}const O=document.getElementById("weather-modal"),D=document.getElementById("weather-location-input");document.getElementById("weather-settings")?.addEventListener("click",()=>{D.value=safeGet(w)||"";const N=C(),M=O.querySelector(`input[name="weather-unit-radio"][value="${N}"]`);M&&(M.checked=!0),O.classList.add("show"),D.focus()}),document.getElementById("weather-cancel")?.addEventListener("click",()=>{O.classList.remove("show")}),document.getElementById("weather-save")?.addEventListener("click",()=>{const N=D.value.trim();if(N){safeGet(w)!==N&&safeSet(z,""),safeSet(w,N);const H=O.querySelector('input[name="weather-unit-radio"]:checked'),S=H?H.value:"imperial",T=C();safeSet(I,S),T!==S&&safeSet(z,""),O.classList.remove("show"),R()}else showNotification("Please enter a location (e.g., Hamburg, London, 90210)","warning")}),wireModal(O),document.addEventListener("keydown",N=>{N.key==="Escape"&&O.classList.contains("show")&&O.classList.remove("show")}),R(),setInterval(R,DC.POLL.WEATHER)})(),(function(){const w=document.getElementById("clock-widget"),B=document.getElementById("clock-render");if(!w||!B)return;const z=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],I=["January","February","March","April","May","June","July","August","September","October","November","December"],C=["XII","I","II","III","IV","V","VI","VII","VIII","IX","X","XI"];let P=safeGet("clock-style")||"default",$=-1,L=!1,g="",k="",f=null,x=null;function E(a){if(L||safeGet("clock-chimes")!=="true")return;L=!0;const e=parseInt(safeGet("clock-chime-volume")||"50",10)/100;let n=0;function t(){if(n>=a){L=!1;return}const s=new Audio("/assets/sounds/church-bell.mp3");s.volume=e,s.play().catch(()=>{}),n++,n{L=!1},2500)}t()}function R(a){return z[a.getDay()]+", "+I[a.getMonth()]+" "+a.getDate()+", "+a.getFullYear()}function O(){k="",f=null}function D(){return k!=="digital"&&(B.innerHTML='
',f={main:B.querySelector(".clock-main"),seconds:B.querySelector(".clock-seconds"),ampm:B.querySelector(".clock-ampm"),date:B.querySelector(".clock-date")},k="digital"),f}function N(a){const e=a.getHours(),n=a.getMinutes(),t=a.getSeconds(),s=e>=12?"PM":"AM",i=e%12||12,h=D();h.main.textContent=`${i}:${String(n).padStart(2,"0")}`,h.seconds.textContent=`:${String(t).padStart(2,"0")}`,h.ampm.textContent=s,h.date.textContent=R(a)}function M(a,e){const n=a.getHours(),t=a.getMinutes(),s=a.getSeconds(),i=n>=12?"PM":"AM",h=n%12||12,d=D();d.main.textContent=`${String(h).padStart(2,"0")}:${String(t).padStart(2,"0")}`,d.seconds.textContent=`:${String(s).padStart(2,"0")}`,d.ampm.textContent=i,d.date.textContent=R(a)}function H(a){const e=a.getHours(),n=a.getMinutes(),t=a.getSeconds(),s=e>=12?"PM":"AM",i=e%12||12,h=String(i).padStart(2," ")+String(n).padStart(2,"0")+String(t).padStart(2,"0");let d='
';if(d+=S(h[0],0),d+=S(h[1],1),d+=':',d+=S(h[2],2),d+=S(h[3],3),d+=':',d+=S(h[4],4),d+=S(h[5],5),d+=`${s}`,d+="
",d+=`
${R(a)}
`,B.innerHTML=d,k="flip",g){for(let A=0;A<6;A++)if(h[A]!==g[A]){const F=B.querySelector(`.flip-card[data-idx="${A}"]`);F&&F.classList.add("flipping")}}g=h}function S(a,e){const n=a===" "?"":a;return`
${n}
${n}
`}function T(a){const e=a.getHours(),n=a.getMinutes(),t=a.getSeconds(),s=e%12||12,i=e>=12?"PM":"AM",h=[Math.floor(s/10),s%10,Math.floor(n/10),n%10,Math.floor(t/10),t%10];let d='
';d+='
HHMMSS
';for(let A=3;A>=0;A--){d+='
';for(let F=0;F<6;F++){const j=h[F]>>A&1;d+=`
`}d+="
"}d+='
';for(let A=0;A<6;A++)d+=`${h[A]}`;d+="
",d+=`
${i}
`,d+="
",d+=`
${R(a)}
`,B.innerHTML=d,k="binary"}function b(a,e){const n=a.getHours(),t=a.getMinutes(),s=a.getSeconds(),i=120,h=i/2,d=i/2,A=s/60*360-90,F=(t+s/60)/60*360-90,j=(n%12+t/60)/12*360-90;let _="";for(let G=1;G<=12;G++){const Q=G/12*2*Math.PI-Math.PI/2,ae=47,ne=h+ae*Math.cos(Q),X=d+ae*Math.sin(Q),se=e?C[G%12]:G;_+=`${se}`}let U="";for(let G=0;G<60;G++){const Q=G/60*2*Math.PI-Math.PI/2,ae=56,ne=G%5===0?52:54,X=h+ne*Math.cos(Q),se=d+ne*Math.sin(Q),ie=h+ae*Math.cos(Q),oe=d+ae*Math.sin(Q),q=G%5===0?1.5:.5;U+=``}const W=` ${U} ${_} @@ -1486,9 +1486,9 @@ Enter version to rollback to:`);if(!e)return;if(!n.includes(e)){showNotification - `,Z=n.getHours()>=12?"PM":"AM";B.innerHTML=`
${W}
${n.getHours()%12||12}:${String(t).padStart(2,"0")} ${Z}${R(n)}
`,k="analog"}function u(){const n=new Date,e=n.getHours()%12||12,a=n.getMinutes(),t=n.getSeconds(),s="clock-widget"+(P!=="default"?" "+P:"");switch(w.className!==s&&(w.className=s),P){case"lcd":M(n);break;case"lcd-blue":M(n);break;case"lcd-amber":M(n);break;case"lcd-retro":M(n);break;case"lcd-taxi":M(n);break;case"flip":H(n);break;case"binary":T(n);break;case"analog":b(n,!1);break;case"roman":b(n,!0);break;default:N(n)}a===0&&t===0&&e!==$&&($=e,E(e)),a!==0&&($=-1)}function y(){clearTimeout(x);const n=document.hidden?6e4:1e3,e=n-Date.now()%n+25;x=setTimeout(()=>{u(),y()},e)}document.addEventListener("visibilitychange",()=>{g="",O(),u(),y()}),u(),y();const c=[{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 o='
';c.forEach(n=>{o+=`
`);const l=document.getElementById("clock-settings-modal"),v=document.getElementById("clock-chimes-toggle"),r=document.getElementById("clock-chime-volume"),p=document.getElementById("clock-volume-section");function m(){const n=safeGet("clock-style")||"default",e=l.querySelector(`input[value="${n}"]`);e&&(e.checked=!0),v.checked=safeGet("clock-chimes")==="true",r.value=safeGet("clock-chime-volume")||"50",p.style.opacity=v.checked?"1":"0.4"}v?.addEventListener("change",()=>{p.style.opacity=v.checked?"1":"0.4"}),document.getElementById("clock-settings")?.addEventListener("click",()=>{m(),l.classList.add("show")}),document.getElementById("clock-chime-test")?.addEventListener("click",()=>{const n=parseInt(r.value,10)/100,e=new Audio("/assets/sounds/church-bell.mp3");e.volume=n,e.play().catch(()=>{})}),document.getElementById("clock-settings-save")?.addEventListener("click",()=>{const n=l.querySelector('input[name="clock-style-radio"]:checked'),e=n?n.value:"default";safeSet("clock-style",e),safeSet("clock-chimes",String(v.checked)),safeSet("clock-chime-volume",r.value),P=e,g="",O(),u(),y(),l.classList.remove("show"),showNotification("Clock settings saved","success",2e3)}),document.getElementById("clock-settings-cancel")?.addEventListener("click",()=>{l.classList.remove("show")}),wireModal(l),l?.querySelectorAll('input[name="clock-style-radio"]').forEach(n=>{n.addEventListener("change",()=>{P=n.value,g="",O(),u()})})})(),(function(){async function w(){try{const $=await(await fetch("/api/v1/health-checks/status")).json();if(!$.success||!$.status)return;for(const[L,g]of Object.entries($.status)){const k=document.getElementById("uptime-"+L),f=document.getElementById("uptime-bar-"+L);if(!k)continue;const x=g.uptime?.["24h"];if(x!=null){const E=x.toFixed(1);k.textContent=`${E}% uptime`,k.className="uptime-chip",x>=99.9?k.classList.add("excellent"):x>=99?k.classList.add("good"):x>=95?k.classList.add("degraded"):k.classList.add("poor"),f&&(f.style.width=E+"%")}}}catch{console.warn("[Card Badges] Health check API unavailable")}}let B;try{B=new Set(JSON.parse(safeSessionGet("dismissed-updates")||"[]"))}catch{B=new Set}async function z(){try{const $=await(await fetch("/api/v1/updates/available")).json();if(!$.success||(document.querySelectorAll(".update-available-badge").forEach(L=>L.classList.remove("visible")),!$.updates?.length))return;for(const L of $.updates){const g=window.APPS||[];for(const k of g)if(k.containerId===L.containerId||k.id===L.containerName||k.name===L.containerName){if(B.has(k.id))break;const f=document.getElementById("update-badge-"+k.id);f&&(f.classList.add("visible"),f.title=`Image digest changed. Click to dismiss if already up to date. -${L.imageName||""}`,f.style.cursor="pointer",f.onclick=x=>{x.stopPropagation(),f.classList.remove("visible"),B.add(k.id),safeSessionSet("dismissed-updates",JSON.stringify([...B]))});break}}}catch{console.warn("[Card Badges] Updates API unavailable")}}function I(){setTimeout(()=>{w(),z()},5e3),setInterval(()=>{w(),z()},6e4)}const C=window.refreshAll;C&&(window.refreshAll=async function(){try{await C(),setTimeout(w,1e3)}catch(P){console.warn("[Card Badges] Error in refreshAll hook:",P.message)}}),I()})(),(function(){var w=null,B=null,z={},I={dark:"Dark",light:"Light",blue:"Blue",black:"Black",nord:"Nord",dracula:"Dracula","solarized-dark":"Solarized Dark","solarized-light":"Solarized Light",taxi:"Taxi",ocean:"Ocean"},C=[["bg","Background","base"],["card-base","Card","base"],["fg","Text","base"],["muted","Muted Text","base"],["border","Border","base"],["accent","Accent","accent"],["accent-strong","Accent Strong","accent"],["ok-bg","OK Background","status"],["ok-fg","OK Text","status"],["bad-bg","Error Bg","status"],["bad-fg","Error Text","status"],["dot-ok","Dot OK","status"],["dot-bad","Dot Error","status"],["uptime","Uptime Bar","status"],["hover","Hover","advanced"],["card-hover","Card Hover","advanced"],["base","Tags/Badges","advanced"],["fg-muted","Dim Text","advanced"],["success","Success","advanced"],["error","Error","advanced"],["warning","Warning","advanced"]],P=document.getElementById("theme");if(!P)return;var $=document.getElementById("theme-label");function L(t){if(I[t])return I[t];var s=safeGetJSON(window.USER_THEMES_KEY,{});return s[t]&&s[t].name||t}function g(){$&&($.textContent=L(window.getActiveTheme()))}P.addEventListener("click",function(){var t=window.THEMES.slice(),s=window.getActiveTheme(),i=t.indexOf(s),h=t[(i+1)%t.length];window.applyTheme(h),g()}),g();function k(){var t={base:"Base Colors",accent:"Accent",status:"Status",advanced:"Advanced (auto-derived)"},s={};C.forEach(function(h){s[h[2]]||(s[h[2]]=[]),s[h[2]].push(h)});var i="";return Object.keys(t).forEach(function(h){h==="advanced"?(i+='
Show advanced colors ▼
',i+='