';for(const[t,e]of Object.entries(d)){const s=e.current||e,n=s.cpu?.percent||0,i=s.memory?.percent||0,a=E(n),o=H(i),l=s.memory?.usage||s.memory?.used||0,k=s.memory?.limit||0,I=s.network?.rxBytes||s.network?.rx||0,j=s.network?.txBytes||s.network?.tx||0,_=e.aggregated;y+=`
+
`);const b=document.getElementById("stats-modal"),L=document.getElementById("container-stats-btn"),R=document.getElementById("stats-cancel"),I=document.getElementById("stats-refresh-btn"),$=document.getElementById("stats-auto-refresh"),P=document.getElementById("stats-container"),N=document.getElementById("stats-aggregated-container"),D=document.getElementById("stats-alerts-container"),v=document.getElementById("stats-last-update");let T=null,w=null;function B(d){if(d===0||!d)return"0 B";const k=1024,f=["B","KB","MB","GB"],t=Math.floor(Math.log(d)/Math.log(k));return parseFloat((d/Math.pow(k,t)).toFixed(1))+" "+f[t]}function S(d){return d<30?"#2ecc71":d<70?"#f39c12":"#e74c3c"}function H(d){return d<50?"#2ecc71":d<80?"#f39c12":"#e74c3c"}async function C(){try{let d=null,k=!1;try{const e=await(await fetch("/api/v1/monitoring/stats")).json();e.success&&e.stats&&(d=e.stats,k=!0,w=e.stats)}catch{}if(!k){const e=await(await fetch("/api/v1/stats/containers")).json();if(e.success&&e.stats){d={};for(const a of e.stats)d[a.name]={name:a.name,current:{cpu:a.cpu,memory:{percent:a.memory.percent,usage:a.memory.used,limit:a.memory.limit,usageMB:Math.round(a.memory.used/1048576),limitMB:Math.round(a.memory.limit/1048576)},network:{rxBytes:a.network.rx,txBytes:a.network.tx,rxMB:(a.network.rx/1048576).toFixed(1),txMB:(a.network.tx/1048576).toFixed(1)},disk:{readMB:0,writeMB:0}},status:a.status};w=d}}if(!d||Object.keys(d).length===0){P.innerHTML='
\u2699\uFE0F Alert Configuration
@@ -1279,7 +1279,7 @@ This will replace current configuration, credentials, and data. Containers will
|
- ${s}
+ ${a}
@@ -1296,7 +1296,7 @@ This will replace current configuration, credentials, and data. Containers will
${i}
\u{1F4CB} Recent Alerts
- ${n?`
+ ${o?`
@@ -1309,26 +1309,26 @@ This will replace current configuration, credentials, and data. Containers will
|
- ${n}
+ ${o}
`:'
No alerts recorded yet.
'}
- `,document.getElementById("save-all-alerts")?.addEventListener("click",async()=>{const a={};document.querySelectorAll("#stats-alerts-container tr[data-container]").forEach(o=>{const l=o.dataset.container;a[l]={cpuThreshold:parseInt(o.querySelector(".alert-cpu")?.value)||80,memoryThreshold:parseInt(o.querySelector(".alert-mem")?.value)||90,diskIOThreshold:parseInt(o.querySelector(".alert-disk")?.value)||50,autoRestart:!!o.querySelector(".alert-autorestart")?.checked,enabled:!0}});try{const l=await(await secureFetch("/api/v1/monitoring/alerts/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({configs:a})})).json(),k=document.getElementById("save-all-alerts");k.textContent=l.success?"\u2705 Saved":"\u274C Failed",setTimeout(()=>{k.textContent="Save All"},2e3)}catch{const l=document.getElementById("save-all-alerts");l.textContent="\u274C Error",setTimeout(()=>{l.textContent="Save All"},2e3)}}),document.getElementById("go-to-notifications")?.addEventListener("click",a=>{a.preventDefault(),h.classList.remove("show"),z(),document.getElementById("manage-notifications")?.click()}),document.querySelectorAll(".alert-test-btn").forEach(a=>{a.addEventListener("click",async()=>{const o=a.textContent;a.textContent="...";try{await secureFetch(`/api/v1/monitoring/alerts/${a.dataset.container}/test`,{method:"POST"}),a.textContent="\u2705",showNotification("Test alert sent for "+a.dataset.name,"success",3e3)}catch{a.textContent="\u274C"}setTimeout(()=>{a.textContent=o},2e3)})}),document.getElementById("upgrade-for-alerts")?.addEventListener("click",()=>{h.classList.remove("show"),z(),typeof openLicenseModal=="function"&&openLicenseModal()})}function f(){T&&clearInterval(T),$?.checked&&(T=setInterval(C,DC.POLL.STATS))}function z(){T&&(clearInterval(T),T=null)}L?.addEventListener("click",()=>{h.classList.add("show"),C(),f()}),R?.addEventListener("click",()=>{h.classList.remove("show"),z()}),h?.addEventListener("click",d=>{d.target===h&&(h.classList.remove("show"),z())}),S?.addEventListener("click",C),$?.addEventListener("change",()=>{$.checked?f():z()}),document.querySelector('[data-panel="stats-aggregated"]')?.addEventListener("click",v),document.querySelector('[data-panel="stats-alerts"]')?.addEventListener("click",O);const A=document.getElementById("stats-history-container"),U=document.getElementById("stats-history-container-area"),p=document.querySelectorAll(".stats-range-btn");let u="1h";function b(d){switch(d){case"1h":return 3600*1e3;case"24h":return 1440*60*1e3;case"7d":return 10080*60*1e3;case"30d":return 720*60*60*1e3;case"1y":return 365*24*60*60*1e3;default:return 3600*1e3}}function g(d){return d==="raw"?"live (10s samples)":d==="hourly"?"hourly average":d==="daily"?"daily average":d}function M(d,x,y,t,e){if(!d||d.length===0)return`
No data for ${escapeHtml(t)}
`;const s=d.map(x).filter(q=>q!=null);if(s.length===0)return`
No data for ${escapeHtml(t)}
`;const n=Math.max(...s,1),i=Math.min(...s,0),a=n-i||1,o=600,l=80,k=4,I=(o-k*2)/Math.max(s.length-1,1),j=s.map((q,G)=>{const X=k+G*I,Q=l-k-(q-i)/a*(l-k*2);return`${X.toFixed(1)},${Q.toFixed(1)}`}).join(" "),_=s[s.length-1],F=s.reduce((q,G)=>q+G,0)/s.length;return`
+ `,document.getElementById("save-all-alerts")?.addEventListener("click",async()=>{const n={};document.querySelectorAll("#stats-alerts-container tr[data-container]").forEach(s=>{const l=s.dataset.container;n[l]={cpuThreshold:parseInt(s.querySelector(".alert-cpu")?.value)||80,memoryThreshold:parseInt(s.querySelector(".alert-mem")?.value)||90,diskIOThreshold:parseInt(s.querySelector(".alert-disk")?.value)||50,autoRestart:!!s.querySelector(".alert-autorestart")?.checked,enabled:!0}});try{const l=await(await secureFetch("/api/v1/monitoring/alerts/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({configs:n})})).json(),p=document.getElementById("save-all-alerts");p.textContent=l.success?"\u2705 Saved":"\u274C Failed",setTimeout(()=>{p.textContent="Save All"},2e3)}catch{const l=document.getElementById("save-all-alerts");l.textContent="\u274C Error",setTimeout(()=>{l.textContent="Save All"},2e3)}}),document.getElementById("go-to-notifications")?.addEventListener("click",n=>{n.preventDefault(),b.classList.remove("show"),z(),document.getElementById("manage-notifications")?.click()}),document.querySelectorAll(".alert-test-btn").forEach(n=>{n.addEventListener("click",async()=>{const s=n.textContent;n.textContent="...";try{await secureFetch(`/api/v1/monitoring/alerts/${n.dataset.container}/test`,{method:"POST"}),n.textContent="\u2705",showNotification("Test alert sent for "+n.dataset.name,"success",3e3)}catch{n.textContent="\u274C"}setTimeout(()=>{n.textContent=s},2e3)})}),document.getElementById("upgrade-for-alerts")?.addEventListener("click",()=>{b.classList.remove("show"),z(),typeof openLicenseModal=="function"&&openLicenseModal()})}function h(){T&&clearInterval(T),$?.checked&&(T=setInterval(C,DC.POLL.STATS))}function z(){T&&(clearInterval(T),T=null)}L?.addEventListener("click",()=>{b.classList.add("show"),C(),h()}),R?.addEventListener("click",()=>{b.classList.remove("show"),z()}),b?.addEventListener("click",d=>{d.target===b&&(b.classList.remove("show"),z())}),I?.addEventListener("click",C),$?.addEventListener("change",()=>{$.checked?h():z()}),document.querySelector('[data-panel="stats-aggregated"]')?.addEventListener("click",y),document.querySelector('[data-panel="stats-alerts"]')?.addEventListener("click",O);const A=document.getElementById("stats-history-container"),j=document.getElementById("stats-history-container-area"),u=document.querySelectorAll(".stats-range-btn");let m="1h";function x(d){switch(d){case"1h":return 3600*1e3;case"24h":return 1440*60*1e3;case"7d":return 10080*60*1e3;case"30d":return 720*60*60*1e3;case"1y":return 365*24*60*60*1e3;default:return 3600*1e3}}function g(d){return d==="raw"?"live (10s samples)":d==="hourly"?"hourly average":d==="daily"?"daily average":d}function M(d,k,f,t,e){if(!d||d.length===0)return`
No data for ${escapeHtml(t)}
`;const a=d.map(k).filter(q=>q!=null);if(a.length===0)return`
No data for ${escapeHtml(t)}
`;const o=Math.max(...a,1),i=Math.min(...a,0),n=o-i||1,s=600,l=80,p=4,E=(s-p*2)/Math.max(a.length-1,1),U=a.map((q,J)=>{const X=p+J*E,Q=l-p-(q-i)/n*(l-p*2);return`${X.toFixed(1)},${Q.toFixed(1)}`}).join(" "),_=a[a.length-1],F=a.reduce((q,J)=>q+J,0)/a.length;return`
${escapeHtml(t)}
- last ${_.toFixed(1)}${e} \xB7 avg ${F.toFixed(1)}${e} \xB7 max ${n.toFixed(1)}${e}
+ last ${_.toFixed(1)}${e} \xB7 avg ${F.toFixed(1)}${e} \xB7 max ${o.toFixed(1)}${e}
-
- `}function r(){if(!A)return;const d=w||{},x=A.value,y=Object.entries(d);if(y.length===0){A.innerHTML='
';return}A.innerHTML=y.map(([t,e])=>`
`).join(""),x&&d[x]&&(A.value=x)}async function c(){if(!U||!A)return;const d=A.value;if(!d){U.innerHTML='
\u{1F4CA}No container selected.
';return}const x=Date.now(),y=x-b(u);U.innerHTML='
Loading history...
';try{const e=await(await fetch(`/api/v1/monitoring/history/${encodeURIComponent(d)}?startTime=${y}&endTime=${x}`)).json();if(!e.success)throw new Error(e.error||"Failed to load history");const s=e.samples||[],n=e.tier||"raw";if(s.length===0){U.innerHTML=`
\u{1F4CA}No data for the last ${u}. Tier: ${g(n)}.
`;return}const i=n==="raw",a=i?j=>j.cpu?.percent:j=>j.cpu?.avg,o=i?j=>j.memory?.percent:j=>j.memory?.avgPercent,l=i?j=>j.network?.rxMB||0:j=>j.network?.rxMB||0,k=i?j=>j.network?.txMB||0:j=>j.network?.txMB||0;let I=`
+ `}function r(){if(!A)return;const d=w||{},k=A.value,f=Object.entries(d);if(f.length===0){A.innerHTML='
';return}A.innerHTML=f.map(([t,e])=>`
`).join(""),k&&d[k]&&(A.value=k)}async function c(){if(!j||!A)return;const d=A.value;if(!d){j.innerHTML='
\u{1F4CA}No container selected.
';return}const k=Date.now(),f=k-x(m);j.innerHTML='
Loading history...
';try{const e=await(await fetch(`/api/v1/monitoring/history/${encodeURIComponent(d)}?startTime=${f}&endTime=${k}`)).json();if(!e.success)throw new Error(e.error||"Failed to load history");const a=e.samples||[],o=e.tier||"raw";if(a.length===0){j.innerHTML=`
\u{1F4CA}No data for the last ${m}. Tier: ${g(o)}.
`;return}const i=o==="raw",n=i?U=>U.cpu?.percent:U=>U.cpu?.avg,s=i?U=>U.memory?.percent:U=>U.memory?.avgPercent,l=i?U=>U.network?.rxMB||0:U=>U.network?.rxMB||0,p=i?U=>U.network?.txMB||0:U=>U.network?.txMB||0;let E=`
- ${s.length} samples \xB7 ${escapeHtml(g(n))} \xB7 ${new Date(y).toLocaleString()} \u2192 ${new Date(x).toLocaleString()}
+ ${a.length} samples \xB7 ${escapeHtml(g(o))} \xB7 ${new Date(f).toLocaleString()} \u2192 ${new Date(k).toLocaleString()}
- `;I+=M(s,a,"#2ecc71","CPU","%"),I+=M(s,o,"#3498db","Memory","%"),I+=M(s,l,"#9b59b6","Network RX"," MB"),I+=M(s,k,"#e67e22","Network TX"," MB"),U.innerHTML=I}catch(t){U.innerHTML=`
\u26A0\uFE0FFailed to load history: ${escapeHtml(t.message)}
`}}p.forEach(d=>{d.addEventListener("click",()=>{p.forEach(x=>x.classList.remove("active")),d.classList.add("active"),u=d.dataset.range,c()})}),A?.addEventListener("change",c),document.querySelector('[data-panel="stats-history"]')?.addEventListener("click",()=>{r(),c()})})(),(function(){injectModal("health-modal",`
+ `;E+=M(a,n,"#2ecc71","CPU","%"),E+=M(a,s,"#3498db","Memory","%"),E+=M(a,l,"#9b59b6","Network RX"," MB"),E+=M(a,p,"#e67e22","Network TX"," MB"),j.innerHTML=E}catch(t){j.innerHTML=`
\u26A0\uFE0FFailed to load history: ${escapeHtml(t.message)}
`}}u.forEach(d=>{d.addEventListener("click",()=>{u.forEach(k=>k.classList.remove("active")),d.classList.add("active"),m=d.dataset.range,c()})}),A?.addEventListener("change",c),document.querySelector('[data-panel="stats-history"]')?.addEventListener("click",()=>{r(),c()})})(),(function(){injectModal("health-modal",`
\u{1F3E5} Health Check Dashboard
@@ -1361,26 +1361,31 @@ This will replace current configuration, credentials, and data. Containers will
\u2699\uFE0F Loading configuration...
-
+
\u{1F30D} Global Settings
Applies to all health checks. Settings are stored locally in this browser.
-
-
-
Health history older than this is pruned.
+
+
+
How often each service's health endpoint is checked.
-
-
-
How often each service is checked.
+
+
+
How often container statistics (CPU/memory) are sampled.
+
+
+
+
Health history older than this is pruned.
+
@@ -1447,24 +1452,24 @@ This will replace current configuration, credentials, and data. Containers will
-
`);const h=document.getElementById("health-modal"),L=document.getElementById("health-check-btn"),R=document.getElementById("health-cancel"),S=document.getElementById("health-refresh-btn"),$=document.getElementById("health-status-container"),P=document.getElementById("health-incidents-container"),N=document.getElementById("health-config-container"),D=document.getElementById("health-last-update"),m=document.getElementById("health-add-btn"),T=document.getElementById("health-config-form"),w=document.getElementById("health-form-title"),B=document.getElementById("health-form-cancel"),E=document.getElementById("health-form-save"),H="dashcaddy-health-settings",C={retentionDays:30,pollingInterval:30,maxEntriesPerService:500,diskUsageThreshold:80},v=document.getElementById("health-global-save"),O=document.getElementById("health-global-reset"),f=document.getElementById("health-global-status"),z=document.getElementById("health-setting-retention"),A=document.getElementById("health-setting-interval"),U=document.getElementById("health-setting-max-entries"),p=document.getElementById("health-setting-disk-threshold");function u(){try{const n=safeGet(H),i=n?JSON.parse(n):{};return Object.assign({},C,i)}catch{return Object.assign({},C)}}function b(){const n=u();z&&(z.value=n.retentionDays),A&&(A.value=n.pollingInterval),U&&(U.value=n.maxEntriesPerService),p&&(p.value=n.diskUsageThreshold)}function g(){const n={retentionDays:Math.max(1,Math.min(3650,parseInt(z?.value)||C.retentionDays)),pollingInterval:Math.max(5,Math.min(3600,parseInt(A?.value)||C.pollingInterval)),maxEntriesPerService:Math.max(10,Math.min(1e5,parseInt(U?.value)||C.maxEntriesPerService)),diskUsageThreshold:Math.max(50,Math.min(99,parseInt(p?.value)||C.diskUsageThreshold))};try{safeSet(H,JSON.stringify(n)),b(),f&&(f.textContent="Saved \u2713",f.style.color="var(--ok-fg)",setTimeout(()=>{f&&(f.textContent="")},2500)),typeof showNotification=="function"&&showNotification("Global health settings saved","success")}catch(i){f&&(f.textContent="Save failed",f.style.color="var(--bad-fg)"),typeof showNotification=="function"&&showNotification("Failed to save settings: "+i.message,"error")}}function M(){try{safeSet(H,JSON.stringify(C))}catch{}b(),f&&(f.textContent="Reset to defaults \u2713",f.style.color="var(--ok-fg)",setTimeout(()=>{f&&(f.textContent="")},2500))}b(),v?.addEventListener("click",g),O?.addEventListener("click",M);let r=null;function c(n){return n>=99.9?"var(--ok-fg)":n>=95?"#f39c12":"var(--bad-fg)"}function d(n){const i={critical:"var(--bad-fg)",high:"#ff6b6b",medium:"#f39c12",low:"var(--muted)"};return`
${n}`}async function x(){try{const i=await(await fetch("/api/v1/health-checks/status")).json();if(!i.success||!i.status||Object.keys(i.status).length===0){$.innerHTML='
\u{1F3E5}No health checks configured. Go to the Configure tab to add services.
';return}const a=Object.values(i.status);let o='
';o+='',o+='| Service | Status | ',o+='Uptime 24h | Uptime 7d | ',o+='Avg Response | Last Check |
';for(const l of a){const k=l.status==="up",I=k?"var(--dot-ok)":"var(--dot-bad)",j=l.uptime?.["24h"]??"-",_=l.uptime?.["7d"]??"-",F=l.avgResponseTime!=null?Math.round(l.avgResponseTime)+"ms":"-",q=l.timestamp?timeAgo(l.timestamp):"-";o+=``,o+=`| ${escapeHtml(l.name||l.serviceId)} | `,o+=`${k?"Up":"Down"} | `,o+=`${typeof j=="number"?j.toFixed(1)+"%":j} | `,o+=`${typeof _=="number"?_.toFixed(1)+"%":_} | `,o+=`${F} | `,o+=`${q} | `,o+="
",o+=` Loading details... |
`}o+="
",$.innerHTML=o,D.textContent="Updated "+new Date().toLocaleTimeString(),$.querySelectorAll("tr[data-health-id]").forEach(l=>{l.addEventListener("click",async()=>{const k=l.dataset.healthId,I=document.getElementById("health-detail-"+k);if(I){if(I.style.display!=="none"){I.style.display="none";return}I.style.display="";try{const _=await(await fetch(`/api/v1/health-checks/${k}/stats?hours=24`)).json();if(_.success&&_.stats){const F=_.stats,q=F.responseTime||{};I.querySelector("td").innerHTML=`
+
`);const b=document.getElementById("health-modal"),L=document.getElementById("health-check-btn"),R=document.getElementById("health-cancel"),I=document.getElementById("health-refresh-btn"),$=document.getElementById("health-status-container"),P=document.getElementById("health-incidents-container"),N=document.getElementById("health-config-container"),D=document.getElementById("health-last-update"),v=document.getElementById("health-add-btn"),T=document.getElementById("health-config-form"),w=document.getElementById("health-form-title"),B=document.getElementById("health-form-cancel"),S=document.getElementById("health-form-save"),H="dashcaddy-health-settings",C={retentionDays:30,pollingInterval:60,statsPollingInterval:30,maxEntriesPerService:500,diskUsageThreshold:80},y=document.getElementById("health-global-save"),O=document.getElementById("health-global-reset"),h=document.getElementById("health-global-status"),z=document.getElementById("health-setting-retention"),A=document.getElementById("health-setting-interval"),j=document.getElementById("health-setting-stats-interval"),u=document.getElementById("health-setting-max-entries"),m=document.getElementById("health-setting-disk-threshold");function x(){try{const i=safeGet(H),n=i?JSON.parse(i):{};return Object.assign({},C,n)}catch{return Object.assign({},C)}}function g(){const i=x();z&&(z.value=i.retentionDays),A&&(A.value=i.pollingInterval),j&&(j.value=i.statsPollingInterval),u&&(u.value=i.maxEntriesPerService),m&&(m.value=i.diskUsageThreshold)}function M(){const i={retentionDays:Math.max(1,Math.min(3650,parseInt(z?.value)||C.retentionDays)),pollingInterval:Math.max(5,Math.min(3600,parseInt(A?.value)||C.pollingInterval)),statsPollingInterval:Math.max(5,Math.min(3600,parseInt(j?.value)||C.statsPollingInterval)),maxEntriesPerService:Math.max(10,Math.min(1e5,parseInt(u?.value)||C.maxEntriesPerService)),diskUsageThreshold:Math.max(50,Math.min(99,parseInt(m?.value)||C.diskUsageThreshold))};try{safeSet(H,JSON.stringify(i)),g(),h&&(h.textContent="Saved \u2713",h.style.color="var(--ok-fg)",setTimeout(()=>{h&&(h.textContent="")},2500)),typeof showNotification=="function"&&showNotification("Global health settings saved","success")}catch(n){h&&(h.textContent="Save failed",h.style.color="var(--bad-fg)"),typeof showNotification=="function"&&showNotification("Failed to save settings: "+n.message,"error")}}function r(){try{safeSet(H,JSON.stringify(C))}catch{}g(),h&&(h.textContent="Reset to defaults \u2713",h.style.color="var(--ok-fg)",setTimeout(()=>{h&&(h.textContent="")},2500))}g(),y?.addEventListener("click",M),O?.addEventListener("click",r);let c=null;function d(i){return i>=99.9?"var(--ok-fg)":i>=95?"#f39c12":"var(--bad-fg)"}function k(i){const n={critical:"var(--bad-fg)",high:"#ff6b6b",medium:"#f39c12",low:"var(--muted)"};return`
${i}`}async function f(){try{const n=await(await fetch("/api/v1/health-checks/status")).json();if(!n.success||!n.status||Object.keys(n.status).length===0){$.innerHTML='
\u{1F3E5}No health checks configured. Go to the Configure tab to add services.
';return}const s=Object.values(n.status);let l='
';l+='',l+='| Service | Status | ',l+='Uptime 24h | Uptime 7d | ',l+='Avg Response | Last Check |
';for(const p of s){const E=p.status==="up",U=E?"var(--dot-ok)":"var(--dot-bad)",_=p.uptime?.["24h"]??"-",F=p.uptime?.["7d"]??"-",q=p.avgResponseTime!=null?Math.round(p.avgResponseTime)+"ms":"-",J=p.timestamp?timeAgo(p.timestamp):"-";l+=``,l+=`| ${escapeHtml(p.name||p.serviceId)} | `,l+=`${E?"Up":"Down"} | `,l+=`${typeof _=="number"?_.toFixed(1)+"%":_} | `,l+=`${typeof F=="number"?F.toFixed(1)+"%":F} | `,l+=`${q} | `,l+=`${J} | `,l+="
",l+=` Loading details... |
`}l+="
",$.innerHTML=l,D.textContent="Updated "+new Date().toLocaleTimeString(),$.querySelectorAll("tr[data-health-id]").forEach(p=>{p.addEventListener("click",async()=>{const E=p.dataset.healthId,U=document.getElementById("health-detail-"+E);if(U){if(U.style.display!=="none"){U.style.display="none";return}U.style.display="";try{const F=await(await fetch(`/api/v1/health-checks/${E}/stats?hours=24`)).json();if(F.success&&F.stats){const q=F.stats,J=q.responseTime||{};U.querySelector("td").innerHTML=`
-
Total Checks
${F.totalChecks||0}
-
Uptime
${(F.uptime||0).toFixed(2)}%
-
Avg Response
${Math.round(q.avg||0)}ms
-
P95 / P99
${Math.round(q.p95||0)}ms / ${Math.round(q.p99||0)}ms
-
Min Response
${Math.round(q.min||0)}ms
-
Max Response
${Math.round(q.max||0)}ms
-
Up Checks
${F.upChecks||0}
-
Down Checks
${F.downChecks||0}
-
`}else I.querySelector("td").innerHTML='
No detailed stats available for this period.
'}catch(j){I.querySelector("td").innerHTML=`
Failed: ${escapeHtml(j.message)}
`}}})})}catch(n){$.innerHTML=`
Failed to load health status: ${escapeHtml(n.message)}
`}}async function y(){try{const[n,i]=await Promise.all([fetch("/api/v1/health-checks/incidents"),fetch("/api/v1/health-checks/incidents/history?limit=50")]),a=await n.json(),o=await i.json();let l="";const k=a.success&&a.incidents?a.incidents:[];if(k.length>0){l+='
Open Incidents ('+k.length+")
";for(const j of k)l+=`
+
Total Checks
${q.totalChecks||0}
+
Uptime
${(q.uptime||0).toFixed(2)}%
+
Avg Response
${Math.round(J.avg||0)}ms
+
P95 / P99
${Math.round(J.p95||0)}ms / ${Math.round(J.p99||0)}ms
+
Min Response
${Math.round(J.min||0)}ms
+
Max Response
${Math.round(J.max||0)}ms
+
Up Checks
${q.upChecks||0}
+
Down Checks
${q.downChecks||0}
+
`}else U.querySelector("td").innerHTML='
No detailed stats available for this period.
'}catch(_){U.querySelector("td").innerHTML=`
Failed: ${escapeHtml(_.message)}
`}}})})}catch(i){$.innerHTML=`
Failed to load health status: ${escapeHtml(i.message)}
`}}async function t(){try{const[i,n]=await Promise.all([fetch("/api/v1/health-checks/incidents"),fetch("/api/v1/health-checks/incidents/history?limit=50")]),s=await i.json(),l=await n.json();let p="";const E=s.success&&s.incidents?s.incidents:[];if(E.length>0){p+='
Open Incidents ('+E.length+")
";for(const _ of E)p+=`
- ${escapeHtml(j.serviceId)}
- ${d(j.severity)}
+ ${escapeHtml(_.serviceId)}
+ ${k(_.severity)}
-
${escapeHtml(j.message)}
-
Started ${timeAgo(j.createdAt)} \xB7 ${j.occurrences||1} occurrence(s)
-
`;l+="
"}else l+='
All services operational \u2014 no open incidents
';const I=o.success&&o.history?o.history:[];if(I.length>0){l+='
Incident History
',l+='
',l+='| Service | Type | Severity | Status | Duration | When |
';for(const j of I){const _=j.status==="resolved",F=_&&j.duration?j.duration<6e4?Math.round(j.duration/1e3)+"s":Math.round(j.duration/6e4)+"m":"-";l+='',l+=`| ${escapeHtml(j.serviceId)} | `,l+=`${escapeHtml(j.type)} | `,l+=`${d(j.severity)} | `,l+=`${j.status} | `,l+=`${F} | `,l+=`${timeAgo(j.createdAt)} | `,l+="
"}l+="
"}P.innerHTML=l||'
\u{1F6A8}No incidents recorded yet.
'}catch(n){P.innerHTML=`
Failed: ${escapeHtml(n.message)}
`}}async function t(){try{const i=await(await fetch("/api/v1/health-checks/status")).json(),a=i.success&&i.status?Object.values(i.status):[];if(a.length===0){N.innerHTML='
\u2699\uFE0FNo health checks configured yet. Click "Add Health Check" below.
';return}let o='
';o+='| Service | Status | SLA Target | Actions |
';for(const l of a){const k=l.status==="up";o+='',o+=`| ${escapeHtml(l.name||l.serviceId)} | `,o+=`${k?"Up":"Down"} | `,o+=`${l.sla?.target?l.sla.target+"%":"-"} | `,o+='',o+=``,o+=``,o+=" |
"}o+="
",N.innerHTML=o}catch(n){N.innerHTML=`
Failed: ${escapeHtml(n.message)}
`}}function e(n,i,a,o,l,k,I){r=n||null,w.textContent=n?"Edit Health Check":"Add Health Check",document.getElementById("health-form-id").value=n||"",document.getElementById("health-form-id").disabled=!!n,document.getElementById("health-form-name").value=i||"",document.getElementById("health-form-url").value=a||"",document.getElementById("health-form-timeout").value=o||1e4,document.getElementById("health-form-codes").value=l||"200",document.getElementById("health-form-sla").value=k||99.9,document.getElementById("health-form-slow").value=I||5e3,T.style.display="",m.style.display="none"}function s(){T.style.display="none",m.style.display="",r=null}m?.addEventListener("click",()=>e("","","",1e4,"200",99.9,5e3)),B?.addEventListener("click",s),E?.addEventListener("click",async()=>{const n=r||document.getElementById("health-form-id").value.trim();if(!n)return showNotification("Service ID is required","warning");const i=document.getElementById("health-form-url").value.trim();if(!i)return showNotification("URL is required","warning");const a=document.getElementById("health-form-codes").value.split(",").map(l=>parseInt(l.trim())).filter(Boolean),o={name:document.getElementById("health-form-name").value.trim()||n,url:i,timeout:parseInt(document.getElementById("health-form-timeout").value)||1e4,expectedStatusCodes:a.length?a:[200],sla:{target:parseFloat(document.getElementById("health-form-sla").value)||99.9},slowResponseThreshold:parseInt(document.getElementById("health-form-slow").value)||5e3};try{E.textContent="Saving...",E.disabled=!0;const k=await(await secureFetch(`/api/v1/health-checks/${encodeURIComponent(n)}/configure`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)})).json();if(!k.success)throw new Error(k.error||"Save failed");s(),t(),x()}catch(l){showNotification("Error: "+l.message,"error")}finally{E.textContent="Save",E.disabled=!1}}),document.addEventListener("health-edit",async n=>{const i=n.detail;e(i,"","",1e4,"200",99.9,5e3)}),document.addEventListener("health-delete",async n=>{const i=n.detail;if(confirm(`Delete health check for "${i}"?`))try{const o=await(await secureFetch(`/api/v1/health-checks/${encodeURIComponent(i)}/configure`,{method:"DELETE"})).json();if(!o.success)throw new Error(o.error);t(),x()}catch(a){showNotification("Error: "+a.message,"error")}}),L?.addEventListener("click",()=>{h?.classList.add("show"),x()}),wireModal(h,R),S?.addEventListener("click",x),document.querySelector('[data-panel="health-incidents"]')?.addEventListener("click",y),document.querySelector('[data-panel="health-config"]')?.addEventListener("click",t)})(),(function(){injectModal("updates-modal",`
+
${escapeHtml(_.message)}
+
Started ${timeAgo(_.createdAt)} \xB7 ${_.occurrences||1} occurrence(s)
+
`;p+="
"}else p+='
All services operational \u2014 no open incidents
';const U=l.success&&l.history?l.history:[];if(U.length>0){p+='
Incident History
',p+='
',p+='| Service | Type | Severity | Status | Duration | When |
';for(const _ of U){const F=_.status==="resolved",q=F&&_.duration?_.duration<6e4?Math.round(_.duration/1e3)+"s":Math.round(_.duration/6e4)+"m":"-";p+='',p+=`| ${escapeHtml(_.serviceId)} | `,p+=`${escapeHtml(_.type)} | `,p+=`${k(_.severity)} | `,p+=`${_.status} | `,p+=`${q} | `,p+=`${timeAgo(_.createdAt)} | `,p+="
"}p+="
"}P.innerHTML=p||'
\u{1F6A8}No incidents recorded yet.
'}catch(i){P.innerHTML=`
Failed: ${escapeHtml(i.message)}
`}}async function e(){try{const n=await(await fetch("/api/v1/health-checks/status")).json(),s=n.success&&n.status?Object.values(n.status):[];if(s.length===0){N.innerHTML='
\u2699\uFE0FNo health checks configured yet. Click "Add Health Check" below.
';return}let l='
';l+='| Service | Status | SLA Target | Actions |
';for(const p of s){const E=p.status==="up";l+='',l+=`| ${escapeHtml(p.name||p.serviceId)} | `,l+=`${E?"Up":"Down"} | `,l+=`${p.sla?.target?p.sla.target+"%":"-"} | `,l+='',l+=``,l+=``,l+=" |
"}l+="
",N.innerHTML=l}catch(i){N.innerHTML=`
Failed: ${escapeHtml(i.message)}
`}}function a(i,n,s,l,p,E,U){c=i||null,w.textContent=i?"Edit Health Check":"Add Health Check",document.getElementById("health-form-id").value=i||"",document.getElementById("health-form-id").disabled=!!i,document.getElementById("health-form-name").value=n||"",document.getElementById("health-form-url").value=s||"",document.getElementById("health-form-timeout").value=l||1e4,document.getElementById("health-form-codes").value=p||"200",document.getElementById("health-form-sla").value=E||99.9,document.getElementById("health-form-slow").value=U||5e3,T.style.display="",v.style.display="none"}function o(){T.style.display="none",v.style.display="",c=null}v?.addEventListener("click",()=>a("","","",1e4,"200",99.9,5e3)),B?.addEventListener("click",o),S?.addEventListener("click",async()=>{const i=c||document.getElementById("health-form-id").value.trim();if(!i)return showNotification("Service ID is required","warning");const n=document.getElementById("health-form-url").value.trim();if(!n)return showNotification("URL is required","warning");const s=document.getElementById("health-form-codes").value.split(",").map(p=>parseInt(p.trim())).filter(Boolean),l={name:document.getElementById("health-form-name").value.trim()||i,url:n,timeout:parseInt(document.getElementById("health-form-timeout").value)||1e4,expectedStatusCodes:s.length?s:[200],sla:{target:parseFloat(document.getElementById("health-form-sla").value)||99.9},slowResponseThreshold:parseInt(document.getElementById("health-form-slow").value)||5e3};try{S.textContent="Saving...",S.disabled=!0;const E=await(await secureFetch(`/api/v1/health-checks/${encodeURIComponent(i)}/configure`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(l)})).json();if(!E.success)throw new Error(E.error||"Save failed");o(),e(),f()}catch(p){showNotification("Error: "+p.message,"error")}finally{S.textContent="Save",S.disabled=!1}}),document.addEventListener("health-edit",async i=>{const n=i.detail;a(n,"","",1e4,"200",99.9,5e3)}),document.addEventListener("health-delete",async i=>{const n=i.detail;if(confirm(`Delete health check for "${n}"?`))try{const l=await(await secureFetch(`/api/v1/health-checks/${encodeURIComponent(n)}/configure`,{method:"DELETE"})).json();if(!l.success)throw new Error(l.error);e(),f()}catch(s){showNotification("Error: "+s.message,"error")}}),L?.addEventListener("click",()=>{b?.classList.add("show"),f()}),wireModal(b,R),I?.addEventListener("click",f),document.querySelector('[data-panel="health-incidents"]')?.addEventListener("click",t),document.querySelector('[data-panel="health-config"]')?.addEventListener("click",e)})(),(function(){injectModal("updates-modal",`
\u2B06\uFE0F Update Management
@@ -1538,17 +1543,17 @@ This will replace current configuration, credentials, and data. Containers will
-
`);const h=document.getElementById("updates-modal"),L=document.getElementById("updates-btn"),R=document.getElementById("updates-cancel"),S=document.getElementById("updates-check-btn"),$=document.getElementById("updates-available-container"),P=document.getElementById("updates-history-container"),N=document.getElementById("updates-auto-container"),D=document.getElementById("updates-last-check");async function m(){try{const t=await(await fetch("/api/v1/updates/available")).json();if(!t.success)throw new Error(t.error);const e=t.updates||[];if(e.length===0){$.innerHTML='
\u2705All containers are up to date.
',D.textContent="",document.getElementById("updates-update-all-btn").style.display="none",document.getElementById("updates-count-badge").style.display="none",window._pendingUpdates=[];return}let s='
';s+='| Container | Image | Current | Latest | Actions |
';for(const a of e){const o=(()=>{const l=window.APPS||[];for(const k of l)if(k.containerId===a.containerId||k.name===a.containerName||k.id===a.containerName)return k.id;return a.containerName})();s+=``,s+=`| ${escapeHtml(a.containerName)} | `,s+=`${escapeHtml(a.imageName)} | `,s+=`${escapeHtml(a.currentDigest)} | `,s+=`${escapeHtml(a.latestDigest)} | `,s+='',s+=``,s+=``,s+=" |
"}s+="
",$.innerHTML=s,D.textContent=e.length+" update(s) available";const n=document.getElementById("updates-count-badge"),i=document.getElementById("updates-update-all-btn");n&&(n.textContent=e.length+" pending",n.style.display=""),i&&e.length>0&&(i.style.display=""),window._pendingUpdates=e,$.querySelectorAll(".update-now-btn").forEach(a=>{a.addEventListener("click",async()=>{const o=a.dataset.id,l=a.dataset.name;if(confirm(`Update "${l}" to the latest version? The container will restart.`)){a.textContent="Updating...",a.disabled=!0;try{const I=await(await secureFetch(`/api/v1/updates/update/${encodeURIComponent(o)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({autoRollback:!0})})).json();if(I.success)a.textContent="Done!",a.style.background="var(--ok-fg)",setTimeout(()=>m(),2e3);else throw new Error(I.error||"Update failed")}catch(k){a.textContent="Failed",a.style.color="var(--bad-fg)",showNotification("Update error: "+k.message,"error"),setTimeout(()=>{a.textContent="Update",a.disabled=!1,a.style.color="",a.style.background=""},3e3)}}})}),$.querySelectorAll(".rollback-btn").forEach(a=>{a.addEventListener("click",async()=>{const o=a.dataset.id,l=a.dataset.name;if(confirm(`Rollback "${l}" to its previous version?`)){a.textContent="Rolling back...",a.disabled=!0;try{const I=await(await secureFetch(`/api/v1/updates/rollback/${encodeURIComponent(o)}`,{method:"POST"})).json();if(I.success)a.textContent="Rolled back!",setTimeout(()=>m(),2e3);else throw new Error(I.error||"Rollback failed")}catch(k){a.textContent="Failed",showNotification("Rollback error: "+k.message,"error"),setTimeout(()=>{a.textContent="Rollback",a.disabled=!1},3e3)}}})})}catch(y){$.innerHTML=`
Failed: ${escapeHtml(y.message)}
`}}async function T(){const y=window._pendingUpdates||[];if(!y.length)return;const t=document.getElementById("updates-update-all-btn");if(!confirm(`Update all ${y.length} containers? Each will restart.`))return;t.textContent="\u23F3 Updating...",t.disabled=!0;let e=0,s=0;for(const n of y)try{(await(await secureFetch(`/api/v1/updates/update/${encodeURIComponent(n.containerId)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({autoRollback:!0})})).json()).success?e++:s++}catch{s++}t.textContent="\u2705 Done",showNotification(`Update all: ${e} succeeded, ${s} failed.`,e>0&&s===0?"success":"error"),setTimeout(()=>{t.textContent="\u2B06\uFE0F Update All",t.disabled=!1,m()},3e3)}document.getElementById("updates-update-all-btn")?.addEventListener("click",T);async function w(){S.textContent="\u{1F50D} Checking...",S.disabled=!0;try{const t=await(await secureFetch("/api/v1/updates/check",{method:"POST"})).json();if(!t.success)throw new Error(t.error);S.textContent="\u2705 Done!",await m()}catch(y){S.textContent="\u274C Failed",showNotification("Check error: "+y.message,"error")}setTimeout(()=>{S.textContent="\u{1F50D} Check for Updates",S.disabled=!1},3e3)}async function B(){try{P.innerHTML='
Loading...
';const t=await(await fetch("/api/v1/updates/history?limit=50")).json(),e=t.success&&t.history?t.history:[];if(e.length===0){P.innerHTML='
\u{1F4CB}No update history yet.
';return}let s='
';s+='| When | Container | Image | Duration | Status |
';for(const n of e){const i=n.status==="success",a=n.duration?n.duration<1e3?n.duration+"ms":Math.round(n.duration/1e3)+"s":"-";s+='',s+=`| ${timeAgo(n.timestamp)} | `,s+=`${escapeHtml(n.containerName)} | `,s+=`${escapeHtml(n.imageName)} | `,s+=`${a} | `,s+=`${i?"\u2713 success":"\u2717 failed"} | `,s+="
",!i&&n.error&&(s+=`| ${escapeHtml(n.error)} |
`)}s+="
",P.innerHTML=s}catch(y){P.innerHTML=`
Failed: ${escapeHtml(y.message)}
`}}async function E(){try{N.innerHTML='
Loading...
';const[y,t]=await Promise.all([fetch("/api/v1/stats/containers"),fetch("/api/v1/updates/auto-update")]),e=await y.json(),s=await t.json(),n=e.success&&e.stats?e.stats:[],i=s.success&&s.config?s.config:{};if(n.length===0){N.innerHTML='
\u{1F916}No running containers found.
';return}let a='
Auto-updates run during maintenance window (default 2AM-4AM). Daily = every day, Weekly = Sundays, Monthly = 1st of month.
';a+='
',a+='| Container | Schedule | Window | Rollback | Last Run | Actions |
';for(const o of n){const l=o.name||o.Names?.[0]?.replace(/^\//,"")||o.Id?.substring(0,12),k=o.containerId||o.Id,I=i[k]||{},j=I.enabled?I.schedule||"weekly":"",_=I.autoRollback!==!1,F=I.maintenanceWindow||"",q=I.lastAutoUpdate?timeAgo(I.lastAutoUpdate):"Never";a+=``,a+=`| ${escapeHtml(l)} | `,a+=`
- | `,a+=` | `,a+=` | `,a+=`${q} | `,a+=` | `,a+="
"}a+="
",N.innerHTML=a,N.querySelectorAll(".save-auto-btn").forEach(o=>{o.addEventListener("click",async()=>{const l=o.dataset.id,k=o.closest("tr"),I=k.querySelector(".auto-schedule").value,j=k.querySelector(".auto-rollback").checked,_=k.querySelector(".auto-window").value.trim();o.textContent="Saving...",o.disabled=!0;try{const q=await(await secureFetch(`/api/v1/updates/auto-update/${encodeURIComponent(l)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({enabled:!!I,schedule:I||"weekly",autoRollback:j,maintenanceWindow:_||void 0})})).json();if(q.success)o.textContent="\u2713 Saved";else throw new Error(q.error)}catch(F){o.textContent="\u2717 Error",showNotification("Save error: "+F.message,"error")}setTimeout(()=>{o.textContent="Save",o.disabled=!1},2e3)})})}catch(y){N.innerHTML=`
Failed: ${escapeHtml(y.message)}
`}}const H=document.getElementById("dashcaddy-current-version"),C=document.getElementById("dashcaddy-update-badge"),v=document.getElementById("dashcaddy-update-details"),O=document.getElementById("dashcaddy-new-version"),f=document.getElementById("dashcaddy-changelog"),z=document.getElementById("dashcaddy-apply-btn"),A=document.getElementById("dashcaddy-check-btn"),U=document.getElementById("dashcaddy-rollback-btn"),p=document.getElementById("dashcaddy-status-bar"),u=document.getElementById("dashcaddy-history-container");let b=null;function g(y,t){p&&(p.style.display="block",p.style.background=t==="error"?"var(--bad-bg)":t==="success"?"var(--ok-bg)":"var(--bg)",p.style.color=t==="error"?"var(--bad-fg)":t==="success"?"var(--ok-fg)":"var(--fg)",p.textContent=y)}async function M(){try{const t=await(await fetch("/api/v1/system/version")).json();if(t.success){const e=t.commit&&t.commit!=="unknown"?t.commit:null;H.textContent="v"+t.version+(e?" ("+e.substring(0,7)+")":"")}}catch{H.textContent="Unable to fetch version"}}async function r(y){y||(A.textContent="Checking...",A.disabled=!0);try{const e=await(await fetch("/api/v1/system/update-check")).json();if(b=e,e.success&&e.available&&e.remote){C.style.display="",v.style.display="",O.textContent="v"+e.remote.version,f.textContent=e.remote.changelog||"No changelog available.";const s=document.getElementById("updates-btn");if(s&&!s.querySelector(".update-dot")){const i=document.createElement("span");i.className="update-dot",i.style.cssText="position:absolute;top:2px;right:2px;width:8px;height:8px;border-radius:50%;background:var(--accent);",s.style.position="relative",s.appendChild(i)}const n=document.getElementById("updates-dashcaddy-tab");if(n&&!n.querySelector(".update-dot")){const i=document.createElement("span");i.className="update-dot",i.style.cssText="display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--accent);margin-left:4px;vertical-align:middle;",n.appendChild(i)}}else C.style.display="none",v.style.display="none",await M(),y||g("You are running the latest version.","success");y||(A.textContent="Check for Updates",A.disabled=!1)}catch(t){y||(g("Failed to check: "+t.message,"error"),A.textContent="Check for Updates",A.disabled=!1)}}async function c(){if(!confirm("Apply DashCaddy update? The API container will restart."))return!1;z.textContent="Updating...",z.disabled=!0,g("Downloading and applying update...","info");try{const t=await(await secureFetch("/api/v1/system/update-apply",{method:"POST"})).json();if(t.success)return g("Update initiated: v"+(t.fromVersion||"?")+" \u2192 v"+(t.toVersion||"?")+". The container will restart shortly.","success"),z.textContent="Applied!",document.querySelectorAll(".update-dot").forEach(e=>e.remove()),!0;throw new Error(t.error||"Update failed")}catch(y){throw g("Update failed: "+y.message,"error"),z.textContent="Update Now",z.disabled=!1,y}}async function d(){try{const t=await(await fetch("/api/v1/system/update-history")).json(),e=t.success&&t.history?t.history:[];if(e.length===0){u.innerHTML='
\u{1F4E6}No self-update history.
';return}let s='
';s+='| When | Version | From | Status |
';for(const n of e){const i=n.status==="success"?"\u2713 success":n.status==="pending"?"\u23F3 pending":n.status==="partial"?"\u26A0 partial":"\u2717 "+n.status,a=n.status==="success"?"var(--ok-fg)":n.status==="pending"?"var(--muted)":"var(--bad-fg)";s+='',s+='| '+timeAgo(n.timestamp)+" | ",s+='v'+escapeHtml(n.version)+(n.rollback?" (rollback)":"")+" | ",s+='v'+escapeHtml(n.fromVersion||"?")+" | ",s+=''+i+" | ",s+="
",n.error&&(s+='| '+escapeHtml(n.error)+" |
"),n.note&&(s+='| '+escapeHtml(n.note)+" |
")}s+="
",u.innerHTML=s}catch(y){u.innerHTML='
Failed: '+escapeHtml(y.message)+"
"}}async function x(){try{const t=await(await fetch("/api/v1/system/rollback-versions")).json(),e=t.success&&t.versions?t.versions:[];if(e.length===0){showNotification("No rollback versions available.","info");return}const s=prompt(`Available rollback versions:
+
`);const b=document.getElementById("updates-modal"),L=document.getElementById("updates-btn"),R=document.getElementById("updates-cancel"),I=document.getElementById("updates-check-btn"),$=document.getElementById("updates-available-container"),P=document.getElementById("updates-history-container"),N=document.getElementById("updates-auto-container"),D=document.getElementById("updates-last-check");async function v(){try{const t=await(await fetch("/api/v1/updates/available")).json();if(!t.success)throw new Error(t.error);const e=t.updates||[];if(e.length===0){$.innerHTML='
\u2705All containers are up to date.
',D.textContent="",document.getElementById("updates-update-all-btn").style.display="none",document.getElementById("updates-count-badge").style.display="none",window._pendingUpdates=[];return}let a='
';a+='| Container | Image | Current | Latest | Actions |
';for(const n of e){const s=(()=>{const l=window.APPS||[];for(const p of l)if(p.containerId===n.containerId||p.name===n.containerName||p.id===n.containerName)return p.id;return n.containerName})();a+=``,a+=`| ${escapeHtml(n.containerName)} | `,a+=`${escapeHtml(n.imageName)} | `,a+=`${escapeHtml(n.currentDigest)} | `,a+=`${escapeHtml(n.latestDigest)} | `,a+='',a+=``,a+=``,a+=" |
"}a+="
",$.innerHTML=a,D.textContent=e.length+" update(s) available";const o=document.getElementById("updates-count-badge"),i=document.getElementById("updates-update-all-btn");o&&(o.textContent=e.length+" pending",o.style.display=""),i&&e.length>0&&(i.style.display=""),window._pendingUpdates=e,$.querySelectorAll(".update-now-btn").forEach(n=>{n.addEventListener("click",async()=>{const s=n.dataset.id,l=n.dataset.name;if(confirm(`Update "${l}" to the latest version? The container will restart.`)){n.textContent="Updating...",n.disabled=!0;try{const E=await(await secureFetch(`/api/v1/updates/update/${encodeURIComponent(s)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({autoRollback:!0})})).json();if(E.success)n.textContent="Done!",n.style.background="var(--ok-fg)",setTimeout(()=>v(),2e3);else throw new Error(E.error||"Update failed")}catch(p){n.textContent="Failed",n.style.color="var(--bad-fg)",showNotification("Update error: "+p.message,"error"),setTimeout(()=>{n.textContent="Update",n.disabled=!1,n.style.color="",n.style.background=""},3e3)}}})}),$.querySelectorAll(".rollback-btn").forEach(n=>{n.addEventListener("click",async()=>{const s=n.dataset.id,l=n.dataset.name;if(confirm(`Rollback "${l}" to its previous version?`)){n.textContent="Rolling back...",n.disabled=!0;try{const E=await(await secureFetch(`/api/v1/updates/rollback/${encodeURIComponent(s)}`,{method:"POST"})).json();if(E.success)n.textContent="Rolled back!",setTimeout(()=>v(),2e3);else throw new Error(E.error||"Rollback failed")}catch(p){n.textContent="Failed",showNotification("Rollback error: "+p.message,"error"),setTimeout(()=>{n.textContent="Rollback",n.disabled=!1},3e3)}}})})}catch(f){$.innerHTML=`
Failed: ${escapeHtml(f.message)}
`}}async function T(){const f=window._pendingUpdates||[];if(!f.length)return;const t=document.getElementById("updates-update-all-btn");if(!confirm(`Update all ${f.length} containers? Each will restart.`))return;t.textContent="\u23F3 Updating...",t.disabled=!0;let e=0,a=0;for(const o of f)try{(await(await secureFetch(`/api/v1/updates/update/${encodeURIComponent(o.containerId)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({autoRollback:!0})})).json()).success?e++:a++}catch{a++}t.textContent="\u2705 Done",showNotification(`Update all: ${e} succeeded, ${a} failed.`,e>0&&a===0?"success":"error"),setTimeout(()=>{t.textContent="\u2B06\uFE0F Update All",t.disabled=!1,v()},3e3)}document.getElementById("updates-update-all-btn")?.addEventListener("click",T);async function w(){I.textContent="\u{1F50D} Checking...",I.disabled=!0;try{const t=await(await secureFetch("/api/v1/updates/check",{method:"POST"})).json();if(!t.success)throw new Error(t.error);I.textContent="\u2705 Done!",await v()}catch(f){I.textContent="\u274C Failed",showNotification("Check error: "+f.message,"error")}setTimeout(()=>{I.textContent="\u{1F50D} Check for Updates",I.disabled=!1},3e3)}async function B(){try{P.innerHTML='
Loading...
';const t=await(await fetch("/api/v1/updates/history?limit=50")).json(),e=t.success&&t.history?t.history:[];if(e.length===0){P.innerHTML='
\u{1F4CB}No update history yet.
';return}let a='
';a+='| When | Container | Image | Duration | Status |
';for(const o of e){const i=o.status==="success",n=o.duration?o.duration<1e3?o.duration+"ms":Math.round(o.duration/1e3)+"s":"-";a+='',a+=`| ${timeAgo(o.timestamp)} | `,a+=`${escapeHtml(o.containerName)} | `,a+=`${escapeHtml(o.imageName)} | `,a+=`${n} | `,a+=`${i?"\u2713 success":"\u2717 failed"} | `,a+="
",!i&&o.error&&(a+=`| ${escapeHtml(o.error)} |
`)}a+="
",P.innerHTML=a}catch(f){P.innerHTML=`
Failed: ${escapeHtml(f.message)}
`}}async function S(){try{N.innerHTML='
Loading...
';const[f,t]=await Promise.all([fetch("/api/v1/stats/containers"),fetch("/api/v1/updates/auto-update")]),e=await f.json(),a=await t.json(),o=e.success&&e.stats?e.stats:[],i=a.success&&a.config?a.config:{};if(o.length===0){N.innerHTML='
\u{1F916}No running containers found.
';return}let n='
Auto-updates run during maintenance window (default 2AM-4AM). Daily = every day, Weekly = Sundays, Monthly = 1st of month.
';n+='
',n+='| Container | Schedule | Window | Rollback | Last Run | Actions |
';for(const s of o){const l=s.name||s.Names?.[0]?.replace(/^\//,"")||s.Id?.substring(0,12),p=s.containerId||s.Id,E=i[p]||{},U=E.enabled?E.schedule||"weekly":"",_=E.autoRollback!==!1,F=E.maintenanceWindow||"",q=E.lastAutoUpdate?timeAgo(E.lastAutoUpdate):"Never";n+=``,n+=`| ${escapeHtml(l)} | `,n+=`
+ | `,n+=` | `,n+=` | `,n+=`${q} | `,n+=` | `,n+="
"}n+="
",N.innerHTML=n,N.querySelectorAll(".save-auto-btn").forEach(s=>{s.addEventListener("click",async()=>{const l=s.dataset.id,p=s.closest("tr"),E=p.querySelector(".auto-schedule").value,U=p.querySelector(".auto-rollback").checked,_=p.querySelector(".auto-window").value.trim();s.textContent="Saving...",s.disabled=!0;try{const q=await(await secureFetch(`/api/v1/updates/auto-update/${encodeURIComponent(l)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({enabled:!!E,schedule:E||"weekly",autoRollback:U,maintenanceWindow:_||void 0})})).json();if(q.success)s.textContent="\u2713 Saved";else throw new Error(q.error)}catch(F){s.textContent="\u2717 Error",showNotification("Save error: "+F.message,"error")}setTimeout(()=>{s.textContent="Save",s.disabled=!1},2e3)})})}catch(f){N.innerHTML=`
Failed: ${escapeHtml(f.message)}
`}}const H=document.getElementById("dashcaddy-current-version"),C=document.getElementById("dashcaddy-update-badge"),y=document.getElementById("dashcaddy-update-details"),O=document.getElementById("dashcaddy-new-version"),h=document.getElementById("dashcaddy-changelog"),z=document.getElementById("dashcaddy-apply-btn"),A=document.getElementById("dashcaddy-check-btn"),j=document.getElementById("dashcaddy-rollback-btn"),u=document.getElementById("dashcaddy-status-bar"),m=document.getElementById("dashcaddy-history-container");let x=null;function g(f,t){u&&(u.style.display="block",u.style.background=t==="error"?"var(--bad-bg)":t==="success"?"var(--ok-bg)":"var(--bg)",u.style.color=t==="error"?"var(--bad-fg)":t==="success"?"var(--ok-fg)":"var(--fg)",u.textContent=f)}async function M(){try{const t=await(await fetch("/api/v1/system/version")).json();if(t.success){const e=t.commit&&t.commit!=="unknown"?t.commit:null;H.textContent="v"+t.version+(e?" ("+e.substring(0,7)+")":"")}}catch{H.textContent="Unable to fetch version"}}async function r(f){f||(A.textContent="Checking...",A.disabled=!0);try{const e=await(await fetch("/api/v1/system/update-check")).json();if(x=e,e.success&&e.available&&e.remote){C.style.display="",y.style.display="",O.textContent="v"+e.remote.version,h.textContent=e.remote.changelog||"No changelog available.";const a=document.getElementById("updates-btn");if(a&&!a.querySelector(".update-dot")){const i=document.createElement("span");i.className="update-dot",i.style.cssText="position:absolute;top:2px;right:2px;width:8px;height:8px;border-radius:50%;background:var(--accent);",a.style.position="relative",a.appendChild(i)}const o=document.getElementById("updates-dashcaddy-tab");if(o&&!o.querySelector(".update-dot")){const i=document.createElement("span");i.className="update-dot",i.style.cssText="display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--accent);margin-left:4px;vertical-align:middle;",o.appendChild(i)}}else C.style.display="none",y.style.display="none",await M(),f||g("You are running the latest version.","success");f||(A.textContent="Check for Updates",A.disabled=!1)}catch(t){f||(g("Failed to check: "+t.message,"error"),A.textContent="Check for Updates",A.disabled=!1)}}async function c(){if(!confirm("Apply DashCaddy update? The API container will restart."))return!1;z.textContent="Updating...",z.disabled=!0,g("Downloading and applying update...","info");try{const t=await(await secureFetch("/api/v1/system/update-apply",{method:"POST"})).json();if(t.success)return g("Update initiated: v"+(t.fromVersion||"?")+" \u2192 v"+(t.toVersion||"?")+". The container will restart shortly.","success"),z.textContent="Applied!",document.querySelectorAll(".update-dot").forEach(e=>e.remove()),!0;throw new Error(t.error||"Update failed")}catch(f){throw g("Update failed: "+f.message,"error"),z.textContent="Update Now",z.disabled=!1,f}}async function d(){try{const t=await(await fetch("/api/v1/system/update-history")).json(),e=t.success&&t.history?t.history:[];if(e.length===0){m.innerHTML='
\u{1F4E6}No self-update history.
';return}let a='
';a+='| When | Version | From | Status |
';for(const o of e){const i=o.status==="success"?"\u2713 success":o.status==="pending"?"\u23F3 pending":o.status==="partial"?"\u26A0 partial":"\u2717 "+o.status,n=o.status==="success"?"var(--ok-fg)":o.status==="pending"?"var(--muted)":"var(--bad-fg)";a+='',a+='| '+timeAgo(o.timestamp)+" | ",a+='v'+escapeHtml(o.version)+(o.rollback?" (rollback)":"")+" | ",a+='v'+escapeHtml(o.fromVersion||"?")+" | ",a+=''+i+" | ",a+="
",o.error&&(a+='| '+escapeHtml(o.error)+" |
"),o.note&&(a+='| '+escapeHtml(o.note)+" |
")}a+="
",m.innerHTML=a}catch(f){m.innerHTML='
Failed: '+escapeHtml(f.message)+"
"}}async function k(){try{const t=await(await fetch("/api/v1/system/rollback-versions")).json(),e=t.success&&t.versions?t.versions:[];if(e.length===0){showNotification("No rollback versions available.","info");return}const a=prompt(`Available rollback versions:
`+e.join(`
`)+`
-Enter version to rollback to:`);if(!s)return;if(!e.includes(s)){showNotification("Invalid version: "+s,"error");return}if(!confirm("Rollback DashCaddy to v"+s+"? The container will restart."))return;g("Rolling back to v"+s+"...","info");const i=await(await secureFetch("/api/v1/system/rollback",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({version:s})})).json();if(i.success)g("Rollback to v"+s+" initiated. Container will restart.","success");else throw new Error(i.error||"Rollback failed")}catch(y){g("Rollback failed: "+y.message,"error")}}A?.addEventListener("click",()=>r(!1)),z?.addEventListener("click",()=>c().catch(()=>{})),U?.addEventListener("click",x),S?.addEventListener("click",w),L?.addEventListener("click",()=>{h?.classList.add("show"),m()}),wireModal(h,R),window.openUpdateModal=function(y){h?.classList.add("show"),m().then(()=>{if(!y)return;const t=$.querySelector(`[data-app-id="${y}"]`);t&&(t.scrollIntoView({behavior:"smooth",block:"center"}),t.style.background="rgba(249,115,22,0.15)",setTimeout(()=>{t.style.background=""},3e3))})},document.querySelector('[data-panel="updates-history"]')?.addEventListener("click",B),document.querySelector('[data-panel="updates-auto"]')?.addEventListener("click",E),document.querySelector('[data-panel="updates-dashcaddy"]')?.addEventListener("click",()=>{M(),d(),b||r(!0)}),window.dcApplyUpdate=c,window.dcCheckForUpdate=r,setTimeout(()=>r(!0),5e3)})(),(function(){injectModal("docker-resources-modal",`
+Enter version to rollback to:`);if(!a)return;if(!e.includes(a)){showNotification("Invalid version: "+a,"error");return}if(!confirm("Rollback DashCaddy to v"+a+"? The container will restart."))return;g("Rolling back to v"+a+"...","info");const i=await(await secureFetch("/api/v1/system/rollback",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({version:a})})).json();if(i.success)g("Rollback to v"+a+" initiated. Container will restart.","success");else throw new Error(i.error||"Rollback failed")}catch(f){g("Rollback failed: "+f.message,"error")}}A?.addEventListener("click",()=>r(!1)),z?.addEventListener("click",()=>c().catch(()=>{})),j?.addEventListener("click",k),I?.addEventListener("click",w),L?.addEventListener("click",()=>{b?.classList.add("show"),v()}),wireModal(b,R),window.openUpdateModal=function(f){b?.classList.add("show"),v().then(()=>{if(!f)return;const t=$.querySelector(`[data-app-id="${f}"]`);t&&(t.scrollIntoView({behavior:"smooth",block:"center"}),t.style.background="rgba(249,115,22,0.15)",setTimeout(()=>{t.style.background=""},3e3))})},document.querySelector('[data-panel="updates-history"]')?.addEventListener("click",B),document.querySelector('[data-panel="updates-auto"]')?.addEventListener("click",S),document.querySelector('[data-panel="updates-dashcaddy"]')?.addEventListener("click",()=>{M(),d(),x||r(!0)}),window.dcApplyUpdate=c,window.dcCheckForUpdate=r,setTimeout(()=>r(!0),5e3)})(),(function(){injectModal("docker-resources-modal",`
\u{1F433} Docker Resources
Manage volumes, networks, and view disk usage.
@@ -1597,7 +1602,7 @@ Enter version to rollback to:`);if(!s)return;if(!e.includes(s)){showNotification
-
`);const h=document.getElementById("docker-resources-modal"),L=document.getElementById("docker-resources-btn"),R=document.getElementById("dr-close");function S(D){if(!D||D===0)return"0 B";const m=["B","KB","MB","GB","TB"],T=Math.floor(Math.log(Math.abs(D))/Math.log(1024));return(D/Math.pow(1024,T)).toFixed(1)+" "+m[T]}async function $(){const D=document.getElementById("dr-vol-list");try{const T=(await getJSON("/api/v1/docker/volumes")).volumes||[];if(T.length===0){D.innerHTML='
\u{1F4E6}No volumes found.
';return}let w='
';w+='| Name | Driver | Scope | Actions |
';for(const B of T){const E=B.name==="buildkit"||B.name.length===64;w+='',w+=`| ${escapeHtml(B.name.length>40?B.name.substring(0,37)+"...":B.name)} | `,w+=`${escapeHtml(B.driver)} | `,w+=`${escapeHtml(B.scope)} | `,w+='',E||(w+=``),w+=" |
"}w+="
",D.innerHTML=w,D.querySelectorAll(".dr-vol-del").forEach(B=>{B.addEventListener("click",async()=>{if(confirm(`Delete volume "${B.dataset.name}"? Data will be lost.`)){B.textContent="...",B.disabled=!0;try{await deleteAPI(`/api/v1/docker/volumes/${encodeURIComponent(B.dataset.name)}?force=true`),$()}catch(E){showNotification("Delete failed: "+E.message,"error"),B.textContent="Delete",B.disabled=!1}}})})}catch(m){D.innerHTML=`
Failed: ${escapeHtml(m.message)}
`}}document.getElementById("dr-vol-create")?.addEventListener("click",async()=>{const D=document.getElementById("dr-vol-name"),m=D.value.trim();if(!m){showNotification("Enter a volume name","warning");return}try{await postJSON("/api/v1/docker/volumes",{name:m}),D.value="",showNotification(`Volume "${m}" created`,"success"),$()}catch(T){showNotification("Create failed: "+T.message,"error")}});async function P(){const D=document.getElementById("dr-net-list");try{const T=(await getJSON("/api/v1/docker/networks")).networks||[];if(T.length===0){D.innerHTML='
\u{1F310}No networks found.
';return}let w='
';w+='| Name | Driver | Scope | Containers | Actions |
';for(const B of T){const E=["bridge","host","none"].includes(B.name);w+='',w+=`| ${escapeHtml(B.name)} | `,w+=`${escapeHtml(B.driver)} | `,w+=`${escapeHtml(B.scope)} | `,w+=`${B.containers} | `,w+='',E||(w+=``),w+=" |
"}w+="
",D.innerHTML=w,D.querySelectorAll(".dr-net-del").forEach(B=>{B.addEventListener("click",async()=>{if(confirm(`Delete network "${B.dataset.name}"?`)){B.textContent="...",B.disabled=!0;try{await deleteAPI(`/api/v1/docker/networks/${encodeURIComponent(B.dataset.id)}`),P()}catch(E){showNotification("Delete failed: "+E.message,"error"),B.textContent="Delete",B.disabled=!1}}})})}catch(m){D.innerHTML=`
Failed: ${escapeHtml(m.message)}
`}}document.getElementById("dr-net-create")?.addEventListener("click",async()=>{const D=document.getElementById("dr-net-name"),m=document.getElementById("dr-net-driver"),T=D.value.trim();if(!T){showNotification("Enter a network name","warning");return}try{await postJSON("/api/v1/docker/networks",{name:T,driver:m.value}),D.value="",showNotification(`Network "${T}" created`,"success"),P()}catch(w){showNotification("Create failed: "+w.message,"error")}});async function N(){const D=document.getElementById("dr-disk-content");try{const m=await getJSON("/api/v1/docker/disk-usage"),T=[{label:"Images",icon:"\u{1F4C0}",count:m.images.count,size:m.images.size,reclaimable:m.images.reclaimable},{label:"Containers",icon:"\u{1F4E6}",count:m.containers.count,size:m.containers.size,extra:`${m.containers.running} running`},{label:"Volumes",icon:"\u{1F4BE}",count:m.volumes.count,size:m.volumes.size,reclaimable:m.volumes.reclaimable},{label:"Build Cache",icon:"\u{1F527}",count:m.buildCache.count,size:m.buildCache.size,reclaimable:m.buildCache.reclaimable}];let w=`
Total: ${S(m.totalSize)}
`;w+='
';for(const B of T)w+='
',w+=`
${B.icon} ${B.label} (${B.count})
`,w+=`
${S(B.size)}
`,B.reclaimable>0&&(w+=`
Reclaimable: ${S(B.reclaimable)}
`),B.extra&&(w+=`
${B.extra}
`),w+="
";w+="
",D.innerHTML=w}catch(m){D.innerHTML=`
Failed: ${escapeHtml(m.message)}
`}}L?.addEventListener("click",()=>{h?.classList.add("show"),$()}),wireModal(h,R),document.querySelector('[data-panel="dr-networks"]')?.addEventListener("click",P),document.querySelector('[data-panel="dr-disk"]')?.addEventListener("click",N)})(),(function(){injectModal("compose-import-modal",`
+
`);const b=document.getElementById("docker-resources-modal"),L=document.getElementById("docker-resources-btn"),R=document.getElementById("dr-close");function I(D){if(!D||D===0)return"0 B";const v=["B","KB","MB","GB","TB"],T=Math.floor(Math.log(Math.abs(D))/Math.log(1024));return(D/Math.pow(1024,T)).toFixed(1)+" "+v[T]}async function $(){const D=document.getElementById("dr-vol-list");try{const T=(await getJSON("/api/v1/docker/volumes")).volumes||[];if(T.length===0){D.innerHTML='
\u{1F4E6}No volumes found.
';return}let w='
';w+='| Name | Driver | Scope | Actions |
';for(const B of T){const S=B.name==="buildkit"||B.name.length===64;w+='',w+=`| ${escapeHtml(B.name.length>40?B.name.substring(0,37)+"...":B.name)} | `,w+=`${escapeHtml(B.driver)} | `,w+=`${escapeHtml(B.scope)} | `,w+='',S||(w+=``),w+=" |
"}w+="
",D.innerHTML=w,D.querySelectorAll(".dr-vol-del").forEach(B=>{B.addEventListener("click",async()=>{if(confirm(`Delete volume "${B.dataset.name}"? Data will be lost.`)){B.textContent="...",B.disabled=!0;try{await deleteAPI(`/api/v1/docker/volumes/${encodeURIComponent(B.dataset.name)}?force=true`),$()}catch(S){showNotification("Delete failed: "+S.message,"error"),B.textContent="Delete",B.disabled=!1}}})})}catch(v){D.innerHTML=`
Failed: ${escapeHtml(v.message)}
`}}document.getElementById("dr-vol-create")?.addEventListener("click",async()=>{const D=document.getElementById("dr-vol-name"),v=D.value.trim();if(!v){showNotification("Enter a volume name","warning");return}try{await postJSON("/api/v1/docker/volumes",{name:v}),D.value="",showNotification(`Volume "${v}" created`,"success"),$()}catch(T){showNotification("Create failed: "+T.message,"error")}});async function P(){const D=document.getElementById("dr-net-list");try{const T=(await getJSON("/api/v1/docker/networks")).networks||[];if(T.length===0){D.innerHTML='
\u{1F310}No networks found.
';return}let w='
';w+='| Name | Driver | Scope | Containers | Actions |
';for(const B of T){const S=["bridge","host","none"].includes(B.name);w+='',w+=`| ${escapeHtml(B.name)} | `,w+=`${escapeHtml(B.driver)} | `,w+=`${escapeHtml(B.scope)} | `,w+=`${B.containers} | `,w+='',S||(w+=``),w+=" |
"}w+="
",D.innerHTML=w,D.querySelectorAll(".dr-net-del").forEach(B=>{B.addEventListener("click",async()=>{if(confirm(`Delete network "${B.dataset.name}"?`)){B.textContent="...",B.disabled=!0;try{await deleteAPI(`/api/v1/docker/networks/${encodeURIComponent(B.dataset.id)}`),P()}catch(S){showNotification("Delete failed: "+S.message,"error"),B.textContent="Delete",B.disabled=!1}}})})}catch(v){D.innerHTML=`
Failed: ${escapeHtml(v.message)}
`}}document.getElementById("dr-net-create")?.addEventListener("click",async()=>{const D=document.getElementById("dr-net-name"),v=document.getElementById("dr-net-driver"),T=D.value.trim();if(!T){showNotification("Enter a network name","warning");return}try{await postJSON("/api/v1/docker/networks",{name:T,driver:v.value}),D.value="",showNotification(`Network "${T}" created`,"success"),P()}catch(w){showNotification("Create failed: "+w.message,"error")}});async function N(){const D=document.getElementById("dr-disk-content");try{const v=await getJSON("/api/v1/docker/disk-usage"),T=[{label:"Images",icon:"\u{1F4C0}",count:v.images.count,size:v.images.size,reclaimable:v.images.reclaimable},{label:"Containers",icon:"\u{1F4E6}",count:v.containers.count,size:v.containers.size,extra:`${v.containers.running} running`},{label:"Volumes",icon:"\u{1F4BE}",count:v.volumes.count,size:v.volumes.size,reclaimable:v.volumes.reclaimable},{label:"Build Cache",icon:"\u{1F527}",count:v.buildCache.count,size:v.buildCache.size,reclaimable:v.buildCache.reclaimable}];let w=`
Total: ${I(v.totalSize)}
`;w+='
';for(const B of T)w+='
',w+=`
${B.icon} ${B.label} (${B.count})
`,w+=`
${I(B.size)}
`,B.reclaimable>0&&(w+=`
Reclaimable: ${I(B.reclaimable)}
`),B.extra&&(w+=`
${B.extra}
`),w+="
";w+="
",D.innerHTML=w}catch(v){D.innerHTML=`
Failed: ${escapeHtml(v.message)}
`}}L?.addEventListener("click",()=>{b?.classList.add("show"),$()}),wireModal(b,R),document.querySelector('[data-panel="dr-networks"]')?.addEventListener("click",P),document.querySelector('[data-panel="dr-disk"]')?.addEventListener("click",N)})(),(function(){injectModal("compose-import-modal",`
\u{1F4E6} Import Docker Compose
Paste a docker-compose.yml to import and deploy services.
@@ -1638,7 +1643,7 @@ Enter version to rollback to:`);if(!s)return;if(!e.includes(s)){showNotification
-
`);const h=document.getElementById("compose-import-modal"),L=document.getElementById("compose-import-btn"),R=document.getElementById("compose-cancel");wireModal(h,R);let S=null;function $(N){document.getElementById("compose-step-paste").style.display=N==="paste"?"":"none",document.getElementById("compose-step-preview").style.display=N==="preview"?"":"none",document.getElementById("compose-step-progress").style.display=N==="progress"?"":"none"}L?.addEventListener("click",()=>{$("paste"),S=null,document.getElementById("compose-yaml").value="",document.getElementById("compose-stack-name").value="",h?.classList.add("show")}),document.getElementById("compose-file-upload")?.addEventListener("change",N=>{const D=N.target.files[0];if(!D)return;const m=new FileReader;m.onload=()=>{document.getElementById("compose-yaml").value=m.result},m.readAsText(D)}),document.getElementById("compose-parse-btn")?.addEventListener("click",async()=>{const N=document.getElementById("compose-yaml").value.trim(),D=document.getElementById("compose-stack-name").value.trim()||"stack";if(!N){showNotification("Paste a docker-compose.yml","warning");return}const m=document.getElementById("compose-parse-btn"),T=m.textContent;m.textContent="Parsing...",m.disabled=!0;try{const w=await postJSON("/api/v1/apps/import-compose",{yaml:N,stackName:D});S=w,S.stackName=D,P(w),$("preview")}catch(w){showNotification("Parse failed: "+w.message,"error")}finally{m.textContent=T,m.disabled=!1}});function P(N){const D=document.getElementById("compose-preview-content");let m="";N.networks&&N.networks.length>0&&(m+=`No hosts registered. Click \u2795 Register Host to add one.
';return}d.innerHTML=c.map(x=>{const y=x.enabled?x.last_seen_at?Date.now()-Date.parse(x.last_seen_at)>18e5?"\u{1F7E1} stale":"\u{1F7E2} online":"\u26AA registered":"\u{1F534} disabled";return`No hosts registered. Click \u2795 Register Host to add one.
';return}d.innerHTML=c.map(k=>{const f=k.enabled?k.last_seen_at?Date.now()-Date.parse(k.last_seen_at)>18e5?"\u{1F7E1} stale":"\u{1F7E2} online":"\u26AA registered":"\u{1F534} disabled";return`