+ ${Object.keys(b).length>0?`
Volumes:
- ${Object.entries(k).map(([d,y])=>`
${d}: ${escapeHtml(y)}
`).join("")}
+ ${Object.entries(b).map(([d,f])=>`
${d}: ${escapeHtml(f)}
`).join("")}
`:""}
- Timezone: ${escapeHtml(I)} • IP: ${escapeHtml(i)} ${c?"• Tailscale only":""}
+ Timezone: ${escapeHtml(L)} • IP: ${escapeHtml(l)} ${c?"• Tailscale only":""}
${j.network?`
Docker network: ${escapeHtml(j.network.name)}
`:""}
- `}function v(){const u=document.querySelectorAll("#recipe-component-list input[data-component-id]"),m=new Set;u.forEach(k=>{k.checked&&m.add(k.dataset.componentId)});const L=j.components||[];return L.filter(k=>k.required).forEach(k=>m.add(k.id)),L.filter(k=>m.has(k.id))}async function B(){const u=document.getElementById("recipe-progress-list"),m=document.getElementById("recipe-deploy-result");m.style.display="none",u.innerHTML="";const L=v();for(const c of L){const d=document.createElement("div");d.id=`recipe-progress-${c.id}`,d.style.cssText="display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 6px; background: var(--card-bg); border: 1px solid var(--border); font-size: 0.85rem;",d.innerHTML=`
+ `}function y(){const m=document.querySelectorAll("#recipe-component-list input[data-component-id]"),v=new Set;m.forEach(b=>{b.checked&&v.add(b.dataset.componentId)});const I=j.components||[];return I.filter(b=>b.required).forEach(b=>v.add(b.id)),I.filter(b=>v.has(b.id))}async function B(){const m=document.getElementById("recipe-progress-list"),v=document.getElementById("recipe-deploy-result");v.style.display="none",m.innerHTML="";const I=y();for(const c of I){const d=document.createElement("div");d.id=`recipe-progress-${c.id}`,d.style.cssText="display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 6px; background: var(--card-bg); border: 1px solid var(--border); font-size: 0.85rem;",d.innerHTML=`
\u23F3
${escapeHtml(c.role||c.id)}
Queued
- `,u.appendChild(d)}const k=document.querySelectorAll("#recipe-volume-list input[data-volume-key]"),I={};k.forEach(c=>{I[c.dataset.volumeKey]=c.value});const i={selectedComponents:L.map(c=>c.id),sharedConfig:{ip:document.getElementById("recipe-ip").value||"host.docker.internal",timezone:document.getElementById("recipe-timezone").value||"UTC",tailscaleOnly:document.getElementById("recipe-tailscale").checked,volumes:I},componentOverrides:{}};for(const c of L)H(c.id,"deploying","Deploying...");try{const d=await(await secureFetch("/api/v1/recipes/deploy",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({recipeId:j.id,config:i})})).json();if(d.success){for(const y of d.deployed||[])H(y.id,"success",y.url?`Running \u2192 ${y.url}`:"Running");for(const y of d.errors||[])H(y.componentId,"error",y.error);m.style.display="",m.innerHTML=`
+ `,m.appendChild(d)}const b=document.querySelectorAll("#recipe-volume-list input[data-volume-key]"),L={};b.forEach(c=>{L[c.dataset.volumeKey]=c.value});const l={selectedComponents:I.map(c=>c.id),sharedConfig:{ip:document.getElementById("recipe-ip").value||"host.docker.internal",timezone:document.getElementById("recipe-timezone").value||"UTC",tailscaleOnly:document.getElementById("recipe-tailscale").checked,volumes:L},componentOverrides:{}};for(const c of I)$(c.id,"deploying","Deploying...");try{const d=await(await secureFetch("/api/v1/recipes/deploy",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({recipeId:j.id,config:l})})).json();if(d.success){for(const f of d.deployed||[])$(f.id,"success",f.url?`Running \u2192 ${f.url}`:"Running");for(const f of d.errors||[])$(f.componentId,"error",f.error);v.style.display="",v.innerHTML=`
${escapeHtml(d.message||"Deployed!")}
${d.setupInstructions?`
Setup tips:
-
${d.setupInstructions.map(y=>`- ${escapeHtml(y)}
`).join("")}
+
${d.setupInstructions.map(f=>`- ${escapeHtml(f)}
`).join("")}
`:""}
- `,showNotification(`${j.name} recipe deployed successfully!`,"success",5e3),window.loadServices&&window.loadServices()}else m.style.display="",m.innerHTML=`
+ `,showNotification(`${j.name} recipe deployed successfully!`,"success",5e3),window.loadServices&&window.loadServices()}else v.style.display="",v.innerHTML=`
Deployment failed: ${escapeHtml(d.error||"Unknown error")}
-
`,showNotification(`Recipe deployment failed: ${d.error}`,"error",5e3)}catch(c){m.style.display="",m.innerHTML=`
+
`,showNotification(`Recipe deployment failed: ${d.error}`,"error",5e3)}catch(c){v.style.display="",v.innerHTML=`
Network error: ${escapeHtml(c.message)}
-
`}}function H(u,m,L){const k=document.getElementById(`recipe-progress-${u}`);if(!k)return;const I=k.querySelector(".recipe-progress-icon"),i=k.querySelector(".recipe-progress-status");m==="deploying"?(I.textContent="\u23F3",i.style.color="var(--accent)"):m==="success"?(I.textContent="\u2705",i.style.color="var(--ok-fg)"):m==="error"&&(I.textContent="\u274C",i.style.color="var(--bad-fg)"),i.textContent=L}f.addEventListener("click",()=>{if(g===3){g=4,z(),B();return}g<3&&(g++,z(),g===2&&T(),g===3&&P())}),R.addEventListener("click",()=>{g>1&&g<4&&(g--,z())}),window.groupRecipeCards=function(){const u=document.querySelectorAll(".service-card[data-recipe-id]");if(u.length===0)return;const m={};u.forEach(L=>{const k=L.dataset.recipeId;m[k]||(m[k]=[]),m[k].push(L)});for(const[L,k]of Object.entries(m))k.length<2||k.forEach((I,i)=>{if(I.style.borderLeft="3px solid rgba(142,68,173,0.5)",i===0){let c=I.querySelector(".recipe-group-label");c||(c=document.createElement("div"),c.className="recipe-group-label",c.style.cssText="position: absolute; top: -8px; left: 12px; font-size: 0.6rem; padding: 1px 8px; border-radius: 8px; background: rgba(142,68,173,0.3); color: #d4a5ff; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;",c.textContent=L.replace(/-/g," "),I.style.position="relative",I.appendChild(c))}})},window.manageRecipe=async function(u,m){const L=`/api/v1/recipes/${u}/${m}`,k=m==="remove"?"DELETE":"POST",I=m==="remove"?`/api/v1/recipes/${u}`:L;if(!(m==="remove"&&!confirm(`Remove the entire ${u} recipe? This will delete all containers and configuration.`)))try{const c=await(await secureFetch(I,{method:k})).json();c.success?(showNotification(`Recipe ${m}: ${c.results?.filter(d=>d.status!=="failed").length||0} components processed`,"success",4e3),window.loadServices&&window.loadServices()):showNotification(`Recipe ${m} failed: ${c.error}`,"error",5e3)}catch(i){showNotification(`Network error: ${i.message}`,"error",5e3)}};const O=document.createElement("style");O.textContent=`
+
`}}function $(m,v,I){const b=document.getElementById(`recipe-progress-${m}`);if(!b)return;const L=b.querySelector(".recipe-progress-icon"),l=b.querySelector(".recipe-progress-status");v==="deploying"?(L.textContent="\u23F3",l.style.color="var(--accent)"):v==="success"?(L.textContent="\u2705",l.style.color="var(--ok-fg)"):v==="error"&&(L.textContent="\u274C",l.style.color="var(--bad-fg)"),l.textContent=I}h.addEventListener("click",()=>{if(g===3){g=4,z(),B();return}g<3&&(g++,z(),g===2&&T(),g===3&&D())}),R.addEventListener("click",()=>{g>1&&g<4&&(g--,z())}),window.groupRecipeCards=function(){const m=document.querySelectorAll(".service-card[data-recipe-id]");if(m.length===0)return;const v={};m.forEach(I=>{const b=I.dataset.recipeId;v[b]||(v[b]=[]),v[b].push(I)});for(const[I,b]of Object.entries(v))b.length<2||b.forEach((L,l)=>{if(L.style.borderLeft="3px solid rgba(142,68,173,0.5)",l===0){let c=L.querySelector(".recipe-group-label");c||(c=document.createElement("div"),c.className="recipe-group-label",c.style.cssText="position: absolute; top: -8px; left: 12px; font-size: 0.6rem; padding: 1px 8px; border-radius: 8px; background: rgba(142,68,173,0.3); color: #d4a5ff; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;",c.textContent=I.replace(/-/g," "),L.style.position="relative",L.appendChild(c))}})},window.manageRecipe=async function(m,v){const I=`/api/v1/recipes/${m}/${v}`,b=v==="remove"?"DELETE":"POST",L=v==="remove"?`/api/v1/recipes/${m}`:I;if(!(v==="remove"&&!confirm(`Remove the entire ${m} recipe? This will delete all containers and configuration.`)))try{const c=await(await secureFetch(L,{method:b})).json();c.success?(showNotification(`Recipe ${v}: ${c.results?.filter(d=>d.status!=="failed").length||0} components processed`,"success",4e3),window.loadServices&&window.loadServices()):showNotification(`Recipe ${v} failed: ${c.error}`,"error",5e3)}catch(l){showNotification(`Network error: ${l.message}`,"error",5e3)}};const U=document.createElement("style");U.textContent=`
.recipe-step {
flex: 1;
text-align: center;
@@ -550,7 +550,7 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};
.recipe-step-panel {
min-height: 180px;
}
- `,document.head.appendChild(O),E()})(),(function(){document.getElementById("reload-caddy-top")?.addEventListener("click",async()=>{const x=document.getElementById("reload-caddy-top"),b=x.textContent;try{x.textContent="\u23F3 Reloading...",x.disabled=!0;const j=await secureFetch("/api/v1/caddy/reload",{method:"POST",headers:{"Content-Type":"application/json"}}),g=await j.json();if(j.ok&&g.success)x.textContent="\u2705 Reloaded!",setTimeout(()=>{x.textContent=b,x.disabled=!1},2e3);else throw new Error(g.error||"Reload failed")}catch(j){x.textContent="\u274C Failed",showNotification(`Failed to reload Caddy: ${j.message}`,"error"),setTimeout(()=>{x.textContent=b,x.disabled=!1},2e3)}})})(),(function(){injectModal("error-log-modal",`
+ `,document.head.appendChild(U),E()})(),(function(){document.getElementById("reload-caddy-top")?.addEventListener("click",async()=>{const w=document.getElementById("reload-caddy-top"),x=w.textContent;try{w.textContent="\u23F3 Reloading...",w.disabled=!0;const j=await secureFetch("/api/v1/caddy/reload",{method:"POST",headers:{"Content-Type":"application/json"}}),g=await j.json();if(j.ok&&g.success)w.textContent="\u2705 Reloaded!",setTimeout(()=>{w.textContent=x,w.disabled=!1},2e3);else throw new Error(g.error||"Reload failed")}catch(j){w.textContent="\u274C Failed",showNotification(`Failed to reload Caddy: ${j.message}`,"error"),setTimeout(()=>{w.textContent=x,w.disabled=!1},2e3)}})})(),(function(){injectModal("error-log-modal",`
\u{1F4CB} Error Logs
@@ -597,8 +597,8 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};
-
`);const x=document.getElementById("error-log-modal"),b=document.getElementById("view-error-logs"),j=document.getElementById("error-log-refresh"),g=document.getElementById("error-log-clear"),N=document.getElementById("error-log-close"),A=document.getElementById("error-log-level"),D=document.getElementById("error-log-context"),R=document.getElementById("error-log-search"),f=document.getElementById("error-log-since"),C=document.getElementById("error-log-until"),E=document.getElementById("error-log-container"),M=document.getElementById("error-log-load-more"),w=document.getElementById("error-log-total"),z=50;let S=0,T=null,P=0,v=[];function B(I){if(!I)return null;const i=new Date(I);return isNaN(i.getTime())?null:i.toISOString()}async function H(){try{const I=await fetch("/api/v1/error-logs/contexts");if(!I.ok)return;const i=await I.json();if(!i.success||!Array.isArray(i.contexts))return;v=i.contexts;const c=D.value;D.innerHTML='
';for(const d of i.contexts){const y=document.createElement("option");y.value=d.name,y.textContent=`${d.name} (${d.count})`,D.appendChild(y)}c&&i.contexts.some(d=>d.name===c)&&(D.value=c)}catch{}}function O(){const I=new URLSearchParams;I.set("limit",String(z)),I.set("offset",String(S)),A.value&&I.set("level",A.value),D.value&&I.set("context",D.value);const i=B(f.value),c=B(C.value);i&&I.set("since",i),c&&I.set("until",c);const d=(R.value||"").trim();return d&&I.set("search",d),I}async function u(I){try{I?(T&&T.abort(),T=new AbortController):(T&&T.abort(),T=new AbortController,S=0,P++,E.innerHTML='
Loading...
');const i=P,c=O(),d=await fetch("/api/v1/error-logs?"+c.toString(),{signal:T.signal});if(!d.ok){E.innerHTML=`
Failed: HTTP ${d.status}
`,M.style.display="none",w.textContent="";return}const y=await d.json();if(!y.success){E.innerHTML=`
Failed: ${escapeHtml(y.error||"unknown")}
`,M.style.display="none",w.textContent="";return}if(!I&&i!==P)return;const p=Array.isArray(y.logs)?y.logs:[];if(p.length===0&&!I){const e=y.filters&&(y.filters.level||y.filters.context||y.filters.search||y.filters.since||y.filters.until)?"No error log entries match your filters.":"\u2705 No errors logged! Everything is working smoothly.";E.innerHTML=`
\u{1F4CB}${escapeHtml(e)}
`,M.style.display="none",w.textContent=y.total?`${y.total} total`:"";return}let n="";I||(n='
',n+='',n+='| When | ',n+='Level | ',n+='Context | ',n+='Message | ',n+='IP | ',n+="
");for(const e of p){const r=(e.level||"?").toUpperCase(),s=r==="ERR"?"var(--bad-fg)":r==="WARN"?"var(--warn-fg, #f0c674)":"var(--muted)",a=e.timestamp?new Date(e.timestamp).toLocaleString():"\u2014",t=e.context||"\u2014",o=(e.error||"").split(`
-`)[0],l=e.request&&e.request.ip||"";n+='',n+=`| ${escapeHtml(a)} | `,n+=`${escapeHtml(r)} | `,n+=`${escapeHtml(t)} | `,n+=`${escapeHtml(o)} | `,n+=`${escapeHtml(l)} | `,n+="
",e.detail&&(n+=`${escapeHtml(e.detail)} |
`)}if(!I)n+="
",E.innerHTML=n;else{const e=E.querySelector("table");e&&e.insertAdjacentHTML("beforeend",n)}S+=p.length,M.style.display=y.hasMore?"":"none",w.textContent=`${y.total} total${y.hasMore?" (showing "+S+")":""}`,E.querySelectorAll(".error-log-row").forEach(e=>{e.dataset.wired||(e.dataset.wired="true",e.addEventListener("click",()=>{const r=e.nextElementSibling;r&&r.classList.contains("error-log-detail")&&(r.style.display=r.style.display==="none"?"":"none")}))})}catch(i){if(i&&i.name==="AbortError")return;E.innerHTML=`
Failed: ${escapeHtml(i.message)}
`,w.textContent=""}}async function m(){if(confirm("Clear the entire error log? This cannot be undone."))try{const i=await(await secureFetch("/api/v1/error-logs",{method:"DELETE",headers:{"content-type":"application/json"},body:JSON.stringify({confirm:"CLEAR"})})).json();i.success?(await H(),u(!1),showNotification("\u2705 Error logs cleared","success",3e3)):showNotification("\u274C "+(i.error||"Clear failed"),"error",4e3)}catch(I){showNotification("\u274C "+I.message,"error",4e3)}}let L;function k(){A?.addEventListener("change",()=>u(!1)),D?.addEventListener("change",()=>u(!1)),R?.addEventListener("input",()=>{clearTimeout(L),L=setTimeout(()=>u(!1),250)});let I;[f,C].forEach(i=>{i?.addEventListener("change",()=>{clearTimeout(I),I=setTimeout(()=>u(!1),250)})}),j?.addEventListener("click",()=>u(!1)),M?.addEventListener("click",()=>u(!0)),g?.addEventListener("click",m),wireModal(x,N)}b?.addEventListener("click",async()=>{x?.classList.add("show"),await H(),u(!1)}),k()})(),(function(){injectModal("container-logs-modal",`
+
`);const w=document.getElementById("error-log-modal"),x=document.getElementById("view-error-logs"),j=document.getElementById("error-log-refresh"),g=document.getElementById("error-log-clear"),N=document.getElementById("error-log-close"),P=document.getElementById("error-log-level"),M=document.getElementById("error-log-context"),R=document.getElementById("error-log-search"),h=document.getElementById("error-log-since"),C=document.getElementById("error-log-until"),E=document.getElementById("error-log-container"),H=document.getElementById("error-log-load-more"),k=document.getElementById("error-log-total"),z=50;let S=0,T=null,D=0,y=[];function B(L){if(!L)return null;const l=new Date(L);return isNaN(l.getTime())?null:l.toISOString()}async function $(){try{const L=await fetch("/api/v1/error-logs/contexts");if(!L.ok)return;const l=await L.json();if(!l.success||!Array.isArray(l.contexts))return;y=l.contexts;const c=M.value;M.innerHTML='
';for(const d of l.contexts){const f=document.createElement("option");f.value=d.name,f.textContent=`${d.name} (${d.count})`,M.appendChild(f)}c&&l.contexts.some(d=>d.name===c)&&(M.value=c)}catch{}}function U(){const L=new URLSearchParams;L.set("limit",String(z)),L.set("offset",String(S)),P.value&&L.set("level",P.value),M.value&&L.set("context",M.value);const l=B(h.value),c=B(C.value);l&&L.set("since",l),c&&L.set("until",c);const d=(R.value||"").trim();return d&&L.set("search",d),L}async function m(L){try{L?(T&&T.abort(),T=new AbortController):(T&&T.abort(),T=new AbortController,S=0,D++,E.innerHTML='
Loading...
');const l=D,c=U(),d=await fetch("/api/v1/error-logs?"+c.toString(),{signal:T.signal});if(!d.ok){E.innerHTML=`
Failed: HTTP ${d.status}
`,H.style.display="none",k.textContent="";return}const f=await d.json();if(!f.success){E.innerHTML=`
Failed: ${escapeHtml(f.error||"unknown")}
`,H.style.display="none",k.textContent="";return}if(!L&&l!==D)return;const p=Array.isArray(f.logs)?f.logs:[];if(p.length===0&&!L){const s=f.filters&&(f.filters.level||f.filters.context||f.filters.search||f.filters.since||f.filters.until)?"No error log entries match your filters.":"\u2705 No errors logged! Everything is working smoothly.";E.innerHTML=`
\u{1F4CB}${escapeHtml(s)}
`,H.style.display="none",k.textContent=f.total?`${f.total} total`:"";return}let e="";L||(e='
',e+='',e+='| When | ',e+='Level | ',e+='Context | ',e+='Message | ',e+='IP | ',e+="
");for(const s of p){const r=(s.level||"?").toUpperCase(),o=r==="ERR"?"var(--bad-fg)":r==="WARN"?"var(--warn-fg, #f0c674)":"var(--muted)",a=s.timestamp?new Date(s.timestamp).toLocaleString():"\u2014",t=s.context||"\u2014",n=(s.error||"").split(`
+`)[0],i=s.request&&s.request.ip||"";e+='',e+=`| ${escapeHtml(a)} | `,e+=`${escapeHtml(r)} | `,e+=`${escapeHtml(t)} | `,e+=`${escapeHtml(n)} | `,e+=`${escapeHtml(i)} | `,e+="
",s.detail&&(e+=`${escapeHtml(s.detail)} |
`)}if(!L)e+="
",E.innerHTML=e;else{const s=E.querySelector("table");s&&s.insertAdjacentHTML("beforeend",e)}S+=p.length,H.style.display=f.hasMore?"":"none",k.textContent=`${f.total} total${f.hasMore?" (showing "+S+")":""}`,E.querySelectorAll(".error-log-row").forEach(s=>{s.dataset.wired||(s.dataset.wired="true",s.addEventListener("click",()=>{const r=s.nextElementSibling;r&&r.classList.contains("error-log-detail")&&(r.style.display=r.style.display==="none"?"":"none")}))})}catch(l){if(l&&l.name==="AbortError")return;E.innerHTML=`
Failed: ${escapeHtml(l.message)}
`,k.textContent=""}}async function v(){if(confirm("Clear the entire error log? This cannot be undone."))try{const l=await(await secureFetch("/api/v1/error-logs",{method:"DELETE",headers:{"content-type":"application/json"},body:JSON.stringify({confirm:"CLEAR"})})).json();l.success?(await $(),m(!1),showNotification("\u2705 Error logs cleared","success",3e3)):showNotification("\u274C "+(l.error||"Clear failed"),"error",4e3)}catch(L){showNotification("\u274C "+L.message,"error",4e3)}}let I;function b(){P?.addEventListener("change",()=>m(!1)),M?.addEventListener("change",()=>m(!1)),R?.addEventListener("input",()=>{clearTimeout(I),I=setTimeout(()=>m(!1),250)});let L;[h,C].forEach(l=>{l?.addEventListener("change",()=>{clearTimeout(L),L=setTimeout(()=>m(!1),250)})}),j?.addEventListener("click",()=>m(!1)),H?.addEventListener("click",()=>m(!0)),g?.addEventListener("click",v),wireModal(w,N)}x?.addEventListener("click",async()=>{w?.classList.add("show"),await $(),m(!1)}),b()})(),(function(){injectModal("container-logs-modal",`
-
`);const x=document.getElementById("container-logs-modal"),b=document.getElementById("cl-container-select"),j=document.getElementById("cl-log-content"),g=document.getElementById("cl-log-search"),N=document.getElementById("cl-log-tail"),A=document.getElementById("cl-refresh"),D=document.getElementById("cl-stream"),R=document.getElementById("cl-download"),f=document.getElementById("cl-clear-search"),C=document.getElementById("cl-close"),E=document.getElementById("cl-close-btn"),M=document.getElementById("cl-stream-status"),w=document.getElementById("cl-stream-indicator"),z=document.getElementById("cl-stream-text"),S=document.getElementById("cl-line-count"),T=document.getElementById("cl-filter-count"),P=document.getElementById("cl-image"),v=document.getElementById("cl-status"),B=document.getElementById("cl-created");let H=null,O=[],u=[],m=null,L=!1,k=null;function I(o){if(!o)return"-";const l=new Date(o);return isNaN(l.getTime())?o:l.toLocaleString()}function i(o){if(!o)return"";const l=document.createElement("div");return l.textContent=o,l.innerHTML}function c(o,l){const h=o.stream==="stderr"?"log-stderr":"log-stdout",$=o.stream==="stderr"?"\u26A0\uFE0F":"\u{1F4E4}";return`
-
- ${l+1}
- ${$}
- ${i(o.text)}
+
`);const w=document.getElementById("container-logs-modal"),x=document.getElementById("cl-container-select"),j=document.getElementById("cl-log-content"),g=document.getElementById("cl-log-search"),N=document.getElementById("cl-log-tail"),P=document.getElementById("cl-refresh"),M=document.getElementById("cl-stream"),R=document.getElementById("cl-download"),h=document.getElementById("cl-clear-search"),C=document.getElementById("cl-close"),E=document.getElementById("cl-close-btn"),H=document.getElementById("cl-stream-status"),k=document.getElementById("cl-stream-indicator"),z=document.getElementById("cl-stream-text"),S=document.getElementById("cl-line-count"),T=document.getElementById("cl-filter-count"),D=document.getElementById("cl-image"),y=document.getElementById("cl-status"),B=document.getElementById("cl-created");let $=null,U=[],m=[],v=null,I=!1,b=null;function L(n){if(!n)return"-";const i=new Date(n);return isNaN(i.getTime())?n:i.toLocaleString()}function l(n){if(!n)return"";const i=document.createElement("div");return i.textContent=n,i.innerHTML}function c(n,i){const u=n.stream==="stderr"?"log-stderr":"log-stdout",A=n.stream==="stderr"?"\u26A0\uFE0F":"\u{1F4E4}";return`
+
+ ${i+1}
+ ${A}
+ ${l(n.text)}
- `}function d(o,l=""){if(!o||o.length===0){j.innerHTML='
No logs available
',S.textContent="0 lines",T.textContent="0 filtered";return}if(O=o,u=l?o.filter(h=>h.text&&h.text.toLowerCase().includes(l.toLowerCase())):o,S.textContent=`${o.length} lines`,T.textContent=l?`${u.length} of ${o.length} shown`:`${o.length} shown`,u.length===0){j.innerHTML=`
No logs match "${i(l)}"
`;return}j.innerHTML=u.map((h,$)=>c(h,$)).join(""),j.scrollTop=j.scrollHeight}async function y(){try{const l=(await getJSON("/api/v1/logs/containers")).containers||[],h=b.value;b.innerHTML='
',l.forEach($=>{const U=document.createElement("option");U.value=$.id,U.textContent=`${$.name} (${$.image.split(":")[0]}) - ${$.status}`,U.dataset.name=$.name,U.dataset.image=$.image,U.dataset.status=$.status,U.dataset.created=$.created,b.appendChild(U)}),h&&b.querySelector(`option[value="${h}"]`)&&(b.value=h,p(h))}catch(o){console.error("Failed to load containers:",o)}}function p(o){const l=b.querySelector(`option[value="${o}"]`);l&&(P.textContent=l.dataset.image||"-",v.textContent=l.dataset.status||"-",v.style.color=l.dataset.status==="running"?"var(--ok-fg, #4ade80)":"var(--bad-fg, #ef4444)",B.textContent=I(l.dataset.created))}async function n(){const o=b.value;if(!o){j.innerHTML='
Select a container to view logs
';return}r(),H=o,p(o);const l=N.value,h=g.value.trim();j.innerHTML='
Loading logs...
';try{const $=`/api/v1/logs/container/${o}${l!=="all"?`?tail=${l}`:""}`,U=await getJSON($);U.logs&&U.logs.length>0?d(U.logs,h):(j.innerHTML='
No logs found for this container
',S.textContent="0 lines",T.textContent="0 filtered")}catch($){j.innerHTML=`
Error loading logs: ${i($.message)}
`}}function e(){const o=b.value;if(!o)return;r(),L=!0,D.textContent="\u23F9 Stop",M.style.display="flex",w.textContent="\u{1F7E2}",z.textContent="Connecting...";const l=`/api/v1/logs/stream/${o}`;m=new EventSource(l),m.onopen=()=>{w.textContent="\u{1F7E2}",z.textContent="Connected - streaming logs"},m.onmessage=h=>{try{const $=JSON.parse(h.data);if($.error){w.textContent="\u{1F534}",z.textContent=`Error: ${$.error}`;return}O.push($),u.push($),S.textContent=`${O.length} lines`,T.textContent=`${u.length} shown`;const U=g.value.trim();if(!U||$.text&&$.text.toLowerCase().includes(U.toLowerCase())){const F=document.createElement("div");F.innerHTML=c($,u.length-1);const _=F.firstElementChild;_.style.background="#1a3a1a",j.appendChild(_),j.scrollTop=j.scrollHeight}}catch($){console.error("Error parsing log:",$)}},m.onerror=()=>{w.textContent="\u{1F534}",z.textContent="Disconnected",L=!1,D.textContent="\u25B6 Stream"},x._eventSource=m}function r(){m&&(m.close(),m=null),x._eventSource&&(x._eventSource.close(),x._eventSource=null),L=!1,D.textContent="\u25B6 Stream",M.style.display="none"}function s(){if(!O||O.length===0){showNotification("No logs to download","error");return}const o=b.querySelector(`option[value="${H}"]`)?.dataset.name||H,l=new Date().toISOString().replace(/[:.]/g,"-"),h=`${o}-logs-${l}.txt`,$=O.map(J=>{const q=J.timestamp||"",K=J.stream==="stderr"?"[ERR]":"[OUT]";return`${q?q+" ":""}${K} ${J.text}`}).join(`
-`),U=new Blob([$],{type:"text/plain"}),F=URL.createObjectURL(U),_=document.createElement("a");_.href=F,_.download=h,document.body.appendChild(_),_.click(),document.body.removeChild(_),URL.revokeObjectURL(F),showNotification(`Downloaded ${O.length} log lines`,"success")}b?.addEventListener("change",()=>{n()}),N?.addEventListener("change",()=>{n()}),A?.addEventListener("click",()=>{n()}),D?.addEventListener("click",()=>{L?r():e()}),R?.addEventListener("click",()=>{s()}),f?.addEventListener("click",()=>{g.value="",d(O,"")}),g?.addEventListener("input",()=>{clearTimeout(k),k=setTimeout(()=>{d(O,g.value.trim())},300)}),g?.addEventListener("keydown",o=>{o.key==="Escape"&&(g.value="",d(O,""))}),document.getElementById("view-container-logs")?.addEventListener("click",()=>{x.classList.add("show"),y()});function t(){r(),x.classList.remove("show")}C?.addEventListener("click",t),E?.addEventListener("click",t),document.addEventListener("keydown",o=>{o.key==="Escape"&&x.classList.contains("show")&&t()}),x.addEventListener("click",o=>{o.target===x&&t()}),window.openContainerLogsModal=function(o,l){x.classList.add("show"),y().then(()=>{const h=Array.from(b.options).find($=>$.value===o||$.dataset.name===l);h?(b.value=h.value,p(h.value),n()):o?(H=o,P.textContent=l||o,v.textContent="-",B.textContent="-",n()):j.innerHTML='
Select a container to view logs
'})}})(),(function(){"use strict";const x=[{unit:"caddy",label:"Caddy (reverse proxy)"},{unit:"dashcaddy-api",label:"DashCaddy API (host systemd unit, not this container)"},{unit:"docker",label:"Docker daemon"},{unit:"ssh",label:"SSH server"},{unit:"systemd-journald",label:"systemd-journald"},{unit:"tailscaled",label:"Tailscale"},{unit:"networkd-dispatcher",label:"Networkd dispatcher"}];injectModal("journald-modal",`
+ `}function d(n,i=""){if(!n||n.length===0){j.innerHTML='
No logs available
',S.textContent="0 lines",T.textContent="0 filtered";return}if(U=n,m=i?n.filter(u=>u.text&&u.text.toLowerCase().includes(i.toLowerCase())):n,S.textContent=`${n.length} lines`,T.textContent=i?`${m.length} of ${n.length} shown`:`${n.length} shown`,m.length===0){j.innerHTML=`
No logs match "${l(i)}"
`;return}j.innerHTML=m.map((u,A)=>c(u,A)).join(""),j.scrollTop=j.scrollHeight}async function f(){try{const i=(await getJSON("/api/v1/logs/containers")).containers||[],u=x.value;x.innerHTML='
',i.forEach(A=>{const O=document.createElement("option");O.value=A.id,O.textContent=`${A.name} (${A.image.split(":")[0]}) - ${A.status}`,O.dataset.name=A.name,O.dataset.image=A.image,O.dataset.status=A.status,O.dataset.created=A.created,x.appendChild(O)}),u&&x.querySelector(`option[value="${u}"]`)&&(x.value=u,p(u))}catch(n){console.error("Failed to load containers:",n)}}function p(n){const i=x.querySelector(`option[value="${n}"]`);i&&(D.textContent=i.dataset.image||"-",y.textContent=i.dataset.status||"-",y.style.color=i.dataset.status==="running"?"var(--ok-fg, #4ade80)":"var(--bad-fg, #ef4444)",B.textContent=L(i.dataset.created))}async function e(){const n=x.value;if(!n){j.innerHTML='
Select a container to view logs
';return}r(),$=n,p(n);const i=N.value,u=g.value.trim();j.innerHTML='
Loading logs...
';try{const A=`/api/v1/logs/container/${n}${i!=="all"?`?tail=${i}`:""}`,O=await getJSON(A);O.logs&&O.logs.length>0?d(O.logs,u):(j.innerHTML='
No logs found for this container
',S.textContent="0 lines",T.textContent="0 filtered")}catch(A){j.innerHTML=`
Error loading logs: ${l(A.message)}
`}}function s(){const n=x.value;if(!n)return;r(),I=!0,M.textContent="\u23F9 Stop",H.style.display="flex",k.textContent="\u{1F7E2}",z.textContent="Connecting...";const i=`/api/v1/logs/stream/${n}`;v=new EventSource(i),v.onopen=()=>{k.textContent="\u{1F7E2}",z.textContent="Connected - streaming logs"},v.onmessage=u=>{try{const A=JSON.parse(u.data);if(A.error){k.textContent="\u{1F534}",z.textContent=`Error: ${A.error}`;return}U.push(A),m.push(A),S.textContent=`${U.length} lines`,T.textContent=`${m.length} shown`;const O=g.value.trim();if(!O||A.text&&A.text.toLowerCase().includes(O.toLowerCase())){const _=document.createElement("div");_.innerHTML=c(A,m.length-1);const F=_.firstElementChild;F.style.background="#1a3a1a",j.appendChild(F),j.scrollTop=j.scrollHeight}}catch(A){console.error("Error parsing log:",A)}},v.onerror=()=>{k.textContent="\u{1F534}",z.textContent="Disconnected",I=!1,M.textContent="\u25B6 Stream"},w._eventSource=v}function r(){v&&(v.close(),v=null),w._eventSource&&(w._eventSource.close(),w._eventSource=null),I=!1,M.textContent="\u25B6 Stream",H.style.display="none"}function o(){if(!U||U.length===0){showNotification("No logs to download","error");return}const n=x.querySelector(`option[value="${$}"]`)?.dataset.name||$,i=new Date().toISOString().replace(/[:.]/g,"-"),u=`${n}-logs-${i}.txt`,A=U.map(q=>{const J=q.timestamp||"",G=q.stream==="stderr"?"[ERR]":"[OUT]";return`${J?J+" ":""}${G} ${q.text}`}).join(`
+`),O=new Blob([A],{type:"text/plain"}),_=URL.createObjectURL(O),F=document.createElement("a");F.href=_,F.download=u,document.body.appendChild(F),F.click(),document.body.removeChild(F),URL.revokeObjectURL(_),showNotification(`Downloaded ${U.length} log lines`,"success")}x?.addEventListener("change",()=>{e()}),N?.addEventListener("change",()=>{e()}),P?.addEventListener("click",()=>{e()}),M?.addEventListener("click",()=>{I?r():s()}),R?.addEventListener("click",()=>{o()}),h?.addEventListener("click",()=>{g.value="",d(U,"")}),g?.addEventListener("input",()=>{clearTimeout(b),b=setTimeout(()=>{d(U,g.value.trim())},300)}),g?.addEventListener("keydown",n=>{n.key==="Escape"&&(g.value="",d(U,""))}),document.getElementById("view-container-logs")?.addEventListener("click",()=>{w.classList.add("show"),f()});function t(){r(),w.classList.remove("show")}C?.addEventListener("click",t),E?.addEventListener("click",t),document.addEventListener("keydown",n=>{n.key==="Escape"&&w.classList.contains("show")&&t()}),w.addEventListener("click",n=>{n.target===w&&t()}),window.openContainerLogsModal=function(n,i){w.classList.add("show"),f().then(()=>{const u=Array.from(x.options).find(A=>A.value===n||A.dataset.name===i);u?(x.value=u.value,p(u.value),e()):n?($=n,D.textContent=i||n,y.textContent="-",B.textContent="-",e()):j.innerHTML='
Select a container to view logs
'})}})(),(function(){"use strict";const w=[{unit:"caddy",label:"Caddy (reverse proxy)"},{unit:"dashcaddy-api",label:"DashCaddy API (host systemd unit, not this container)"},{unit:"docker",label:"Docker daemon"},{unit:"ssh",label:"SSH server"},{unit:"systemd-journald",label:"systemd-journald"},{unit:"tailscaled",label:"Tailscale"},{unit:"networkd-dispatcher",label:"Networkd dispatcher"}];injectModal("journald-modal",`
- `);const b=document.getElementById("journald-modal"),j=document.getElementById("jd-unit-select"),g=document.getElementById("jd-search"),N=document.getElementById("jd-tail"),A=document.getElementById("jd-refresh"),D=document.getElementById("jd-stream"),R=document.getElementById("jd-clear-search"),f=document.getElementById("jd-close"),C=document.getElementById("jd-close-btn"),E=document.getElementById("jd-content"),M=document.getElementById("jd-line-count"),w=document.getElementById("jd-filter-count"),z=document.getElementById("jd-overflow"),S=document.getElementById("jd-unit-display"),T=document.getElementById("jd-stream-state");let P=!1,v=[],B=!1,H=null,O=null;function u(p){const n=document.createElement("div");return n.textContent=String(p),n.innerHTML}function m(p){P=p,j.innerHTML="",x.forEach(n=>{const e=document.createElement("option");e.value=n.unit,e.textContent=n.label+" ("+n.unit+")",j.appendChild(e)}),j.disabled=!p,p?(A.disabled=!1,D.disabled=!1):(E.innerHTML='
journald bind-mount not available in this container.
Requires /var/log/journal + /usr/bin/journalctl mounted (start.sh).
',A.disabled=!0,D.disabled=!0)}async function L(){try{const p=await fetch("/api/v1/logs/journal/units");if(!p.ok){m(!1);return}const n=await p.json();m(!!n.available)}catch{m(!1)}}function k(){const p=(g.value||"").trim().toLowerCase(),n=p?v.filter(e=>(e.textContent||"").toLowerCase().includes(p)):v;if(n.length===0)E.innerHTML='
No entries'+(p?` matching "${u(p)}"`:"")+"
";else{const e=n.map(s=>{const a=s.timestamp?u(s.timestamp):"\u2014",t=u(s.textContent);return`
${a}${t}
`}).join("");E.innerHTML=e,E.scrollHeight-E.scrollTop-E.clientHeight<80&&(E.scrollTop=E.scrollHeight)}M.textContent=`${v.length} entries`,w.textContent=p?`${n.length} of ${v.length} shown`:`${v.length} shown`}async function I(){if(!P)return;c();const p=j.value;if(!p)return;const n=Math.max(1,Math.min(5e3,Number(N.value)||200)),e=(g.value||"").trim();E.innerHTML='
Loading\u2026
';try{const r=new URL("/api/v1/logs/journal",window.location.origin);r.searchParams.set("unit",p),r.searchParams.set("tail",String(n)),e&&r.searchParams.set("search",e);const s=await fetch(r.toString()),a=await s.json();if(!s.ok||!a.success){E.innerHTML='
Failed: '+u(a&&a.error||"HTTP "+s.status)+"
";return}S.textContent=p,v=(a.entries||[]).map(t=>({timestamp:t.timestamp,unit:t.unit,textContent:t.text||""})),z.style.display="none",k()}catch(r){E.innerHTML='
Error: '+u(r.message)+"
"}}function i(){if(!P)return;c();const p=j.value;if(!p)return;const n=(g.value||"").trim();S.textContent=p,D.textContent="\u23F8 Stop",D.classList.add("streaming"),T.textContent="streaming",T.style.color="var(--ok-fg, #4ade80)",v=[],k(),z.style.display="none";const e=new URL("/api/v1/logs/journal/stream",window.location.origin);e.searchParams.set("unit",p),n&&e.searchParams.set("search",n),H=new EventSource(e.toString()),H.onmessage=r=>{try{const s=JSON.parse(r.data);if(s.error){/stream (exceeded|line cap)/.test(s.error)&&(z.style.display="",c()),E.innerHTML+='
\u26A0 '+u(s.error)+"
",E.scrollTop=E.scrollHeight;return}v.push({timestamp:s.timestamp,unit:s.unit||p,textContent:s.text||""}),v.length>5e3&&(v=v.slice(v.length-5e3),z.style.display=""),k()}catch{}},H.onerror=()=>{},B=!0}function c(){if(B=!1,H){try{H.close()}catch{}H=null}D.textContent="\u25B6 Stream",D.classList.remove("streaming"),T.textContent="disconnected",T.style.color="var(--muted)"}function d(){c(),b.classList.remove("show")}A.addEventListener("click",I),D.addEventListener("click",()=>B?c():i()),R.addEventListener("click",()=>{g.value="",k()}),g.addEventListener("input",()=>{clearTimeout(O),O=setTimeout(k,200)}),g.addEventListener("keydown",p=>{p.key==="Escape"&&(g.value="",k())}),f.addEventListener("click",d),C.addEventListener("click",d),b.addEventListener("click",p=>{p.target===b&&d()}),document.addEventListener("keydown",p=>{p.key==="Escape"&&b.classList.contains("show")&&d()}),j.addEventListener("change",()=>{v.length>0&&I()});function y(){b.classList.add("show"),L()}window.openJournaldModal=y,document.getElementById("view-journald-logs")?.addEventListener("click",y)})(),(function(){injectModal("snapshot-modal",`
+ `);const x=document.getElementById("journald-modal"),j=document.getElementById("jd-unit-select"),g=document.getElementById("jd-search"),N=document.getElementById("jd-tail"),P=document.getElementById("jd-refresh"),M=document.getElementById("jd-stream"),R=document.getElementById("jd-clear-search"),h=document.getElementById("jd-close"),C=document.getElementById("jd-close-btn"),E=document.getElementById("jd-content"),H=document.getElementById("jd-line-count"),k=document.getElementById("jd-filter-count"),z=document.getElementById("jd-overflow"),S=document.getElementById("jd-unit-display"),T=document.getElementById("jd-stream-state");let D=!1,y=[],B=!1,$=null,U=null;function m(p){const e=document.createElement("div");return e.textContent=String(p),e.innerHTML}function v(p){D=p,j.innerHTML="",w.forEach(e=>{const s=document.createElement("option");s.value=e.unit,s.textContent=e.label+" ("+e.unit+")",j.appendChild(s)}),j.disabled=!p,p?(P.disabled=!1,M.disabled=!1):(E.innerHTML='
journald bind-mount not available in this container.
Requires /var/log/journal + /usr/bin/journalctl mounted (start.sh).
',P.disabled=!0,M.disabled=!0)}async function I(){try{const p=await fetch("/api/v1/logs/journal/units");if(!p.ok){v(!1);return}const e=await p.json();v(!!e.available)}catch{v(!1)}}function b(){const p=(g.value||"").trim().toLowerCase(),e=p?y.filter(s=>(s.textContent||"").toLowerCase().includes(p)):y;if(e.length===0)E.innerHTML='
No entries'+(p?` matching "${m(p)}"`:"")+"
";else{const s=e.map(o=>{const a=o.timestamp?m(o.timestamp):"\u2014",t=m(o.textContent);return`
${a}${t}
`}).join("");E.innerHTML=s,E.scrollHeight-E.scrollTop-E.clientHeight<80&&(E.scrollTop=E.scrollHeight)}H.textContent=`${y.length} entries`,k.textContent=p?`${e.length} of ${y.length} shown`:`${y.length} shown`}async function L(){if(!D)return;c();const p=j.value;if(!p)return;const e=Math.max(1,Math.min(5e3,Number(N.value)||200)),s=(g.value||"").trim();E.innerHTML='
Loading\u2026
';try{const r=new URL("/api/v1/logs/journal",window.location.origin);r.searchParams.set("unit",p),r.searchParams.set("tail",String(e)),s&&r.searchParams.set("search",s);const o=await fetch(r.toString()),a=await o.json();if(!o.ok||!a.success){E.innerHTML='
Failed: '+m(a&&a.error||"HTTP "+o.status)+"
";return}S.textContent=p,y=(a.entries||[]).map(t=>({timestamp:t.timestamp,unit:t.unit,textContent:t.text||""})),z.style.display="none",b()}catch(r){E.innerHTML='
Error: '+m(r.message)+"
"}}function l(){if(!D)return;c();const p=j.value;if(!p)return;const e=(g.value||"").trim();S.textContent=p,M.textContent="\u23F8 Stop",M.classList.add("streaming"),T.textContent="streaming",T.style.color="var(--ok-fg, #4ade80)",y=[],b(),z.style.display="none";const s=new URL("/api/v1/logs/journal/stream",window.location.origin);s.searchParams.set("unit",p),e&&s.searchParams.set("search",e),$=new EventSource(s.toString()),$.onmessage=r=>{try{const o=JSON.parse(r.data);if(o.error){/stream (exceeded|line cap)/.test(o.error)&&(z.style.display="",c()),E.innerHTML+='
\u26A0 '+m(o.error)+"
",E.scrollTop=E.scrollHeight;return}y.push({timestamp:o.timestamp,unit:o.unit||p,textContent:o.text||""}),y.length>5e3&&(y=y.slice(y.length-5e3),z.style.display=""),b()}catch{}},$.onerror=()=>{},B=!0}function c(){if(B=!1,$){try{$.close()}catch{}$=null}M.textContent="\u25B6 Stream",M.classList.remove("streaming"),T.textContent="disconnected",T.style.color="var(--muted)"}function d(){c(),x.classList.remove("show")}P.addEventListener("click",L),M.addEventListener("click",()=>B?c():l()),R.addEventListener("click",()=>{g.value="",b()}),g.addEventListener("input",()=>{clearTimeout(U),U=setTimeout(b,200)}),g.addEventListener("keydown",p=>{p.key==="Escape"&&(g.value="",b())}),h.addEventListener("click",d),C.addEventListener("click",d),x.addEventListener("click",p=>{p.target===x&&d()}),document.addEventListener("keydown",p=>{p.key==="Escape"&&x.classList.contains("show")&&d()}),j.addEventListener("change",()=>{y.length>0&&L()});function f(){x.classList.add("show"),I()}window.openJournaldModal=f,document.getElementById("view-journald-logs")?.addEventListener("click",f)})(),(function(){injectModal("snapshot-modal",`
\u{1F4BE} Container Snapshots
@@ -752,7 +752,7 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};
-
`);const x=document.getElementById("snapshot-modal"),b=document.getElementById("snapshot-btn"),j=document.getElementById("snapshot-close"),g=document.getElementById("snapshot-container-select"),N=document.getElementById("snapshot-details"),A=document.getElementById("snapshot-create-btn"),D=document.getElementById("snapshot-create-status");let R=null;async function f(){try{const S=await(await fetch("/api/v1/containers")).json();if(!S.success||!S.containers)return;g.innerHTML='
';for(const T of S.containers){const P=document.createElement("option");P.value=T.id,P.textContent=`${T.name||T.id} (${T.image||"unknown"})`,P.dataset.name=T.name,P.dataset.image=T.image,P.dataset.status=T.status,P.dataset.created=T.created,g.appendChild(P)}}catch(z){console.error("Failed to load containers:",z)}}function C(z){if(!z||!z.value){N.style.display="none",R=null;return}R=z.value,document.getElementById("snapshot-image").textContent=z.dataset.image||"-",document.getElementById("snapshot-status").textContent=z.dataset.status||"-",document.getElementById("snapshot-created").textContent=z.dataset.created?new Date(z.dataset.created*1e3).toLocaleString():"-",document.getElementById("snapshot-id").textContent=z.value.substring(0,12),N.style.display=""}async function E(){if(!R){D.textContent="Please select a container first",D.style.color="var(--bad-fg)";return}const z=document.getElementById("snapshot-name").value.trim();if(!z){D.textContent="Please enter a snapshot name",D.style.color="var(--bad-fg)";return}const S=document.getElementById("snapshot-leave-running").checked;A.disabled=!0,A.textContent="Creating...",D.textContent="";try{const P=await(await fetch(`/api/v1/containers/${encodeURIComponent(R)}/checkpoint`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:z,leaveRunning:S})})).json();P.success?(D.textContent=`\u2713 Snapshot "${z}" created successfully`,D.style.color="var(--ok-fg)",document.getElementById("snapshot-name").value=""):(D.textContent=`\u2717 Failed: ${P.error||"Unknown error"}`,D.style.color="var(--bad-fg)")}catch(T){D.textContent=`\u2717 Error: ${T.message}`,D.style.color="var(--bad-fg)"}finally{A.disabled=!1,A.textContent="\u{1F4BE} Create Snapshot"}}function M(){x.classList.add("show"),f()}function w(){x.classList.remove("show"),N.style.display="none",R=null,g.selectedIndex=0}b?.addEventListener("click",M),j?.addEventListener("click",w),wireModal(x,j),g?.addEventListener("change",z=>{const S=g.options[g.selectedIndex];C(S)}),A?.addEventListener("click",E),x?.querySelectorAll(".panel-tab").forEach(z=>{z.addEventListener("click",()=>{x.querySelectorAll(".panel-tab").forEach(S=>S.classList.remove("active")),x.querySelectorAll(".panel-section").forEach(S=>S.classList.remove("active")),z.classList.add("active"),x.querySelector(`#${z.dataset.panel}`).classList.add("active")})})})(),(function(){injectModal("arr-setup-modal",`
+
`);const w=document.getElementById("snapshot-modal"),x=document.getElementById("snapshot-btn"),j=document.getElementById("snapshot-close"),g=document.getElementById("snapshot-container-select"),N=document.getElementById("snapshot-details"),P=document.getElementById("snapshot-create-btn"),M=document.getElementById("snapshot-create-status");let R=null;async function h(){try{const S=await(await fetch("/api/v1/containers")).json();if(!S.success||!S.containers)return;g.innerHTML='
';for(const T of S.containers){const D=document.createElement("option");D.value=T.id,D.textContent=`${T.name||T.id} (${T.image||"unknown"})`,D.dataset.name=T.name,D.dataset.image=T.image,D.dataset.status=T.status,D.dataset.created=T.created,g.appendChild(D)}}catch(z){console.error("Failed to load containers:",z)}}function C(z){if(!z||!z.value){N.style.display="none",R=null;return}R=z.value,document.getElementById("snapshot-image").textContent=z.dataset.image||"-",document.getElementById("snapshot-status").textContent=z.dataset.status||"-",document.getElementById("snapshot-created").textContent=z.dataset.created?new Date(z.dataset.created*1e3).toLocaleString():"-",document.getElementById("snapshot-id").textContent=z.value.substring(0,12),N.style.display=""}async function E(){if(!R){M.textContent="Please select a container first",M.style.color="var(--bad-fg)";return}const z=document.getElementById("snapshot-name").value.trim();if(!z){M.textContent="Please enter a snapshot name",M.style.color="var(--bad-fg)";return}const S=document.getElementById("snapshot-leave-running").checked;P.disabled=!0,P.textContent="Creating...",M.textContent="";try{const D=await(await fetch(`/api/v1/containers/${encodeURIComponent(R)}/checkpoint`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:z,leaveRunning:S})})).json();D.success?(M.textContent=`\u2713 Snapshot "${z}" created successfully`,M.style.color="var(--ok-fg)",document.getElementById("snapshot-name").value=""):(M.textContent=`\u2717 Failed: ${D.error||"Unknown error"}`,M.style.color="var(--bad-fg)")}catch(T){M.textContent=`\u2717 Error: ${T.message}`,M.style.color="var(--bad-fg)"}finally{P.disabled=!1,P.textContent="\u{1F4BE} Create Snapshot"}}function H(){w.classList.add("show"),h()}function k(){w.classList.remove("show"),N.style.display="none",R=null,g.selectedIndex=0}x?.addEventListener("click",H),j?.addEventListener("click",k),wireModal(w,j),g?.addEventListener("change",z=>{const S=g.options[g.selectedIndex];C(S)}),P?.addEventListener("click",E),w?.querySelectorAll(".panel-tab").forEach(z=>{z.addEventListener("click",()=>{w.querySelectorAll(".panel-tab").forEach(S=>S.classList.remove("active")),w.querySelectorAll(".panel-section").forEach(S=>S.classList.remove("active")),z.classList.add("active"),w.querySelector(`#${z.dataset.panel}`).classList.add("active")})})})(),(function(){injectModal("arr-setup-modal",`
\u{1F3AC} Smart Arr Connect
@@ -831,73 +831,73 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};
-
`);const x=document.getElementById("arr-setup-modal"),b=document.getElementById("arr-setup-btn"),j=document.getElementById("arr-setup-cancel"),g=document.getElementById("smart-connect-btn"),N=document.getElementById("smart-phase-detect"),A=document.getElementById("smart-phase-credentials"),D=document.getElementById("smart-phase-progress"),R=document.getElementById("smart-phase-results"),f=document.getElementById("smart-detect-results"),C=document.getElementById("smart-credential-inputs"),E=document.getElementById("smart-progress-steps"),M=document.getElementById("smart-results-content"),w=document.getElementById("smart-plex-libraries"),z=document.getElementById("smart-retry-btn");let S=null;const T={plex:"\u{1F3AC}",radarr:"\u{1F3AC}",sonarr:"\u{1F4FA}",prowlarr:"\u{1F50D}",seerr:"\u{1F4CB}"},P={plex:"Plex",radarr:"Radarr (Movies)",sonarr:"Sonarr (TV)",prowlarr:"Prowlarr (Indexers)",seerr:"Seerr"};function v(k){N.style.display=k==="detect"?"block":"none",A.style.display=k==="credentials"?"block":"none",D.style.display=k==="progress"?"block":"none",R.style.display=k==="results"?"block":"none"}function B(k){const I={connected:{bg:"var(--ok-fg)",icon:"✓",text:"Connected"},needs_key:{bg:"#f39c12",icon:"🔑",text:"Needs API Key"},not_found:{bg:"var(--muted)",icon:"—",text:"Not Found"},error:{bg:"var(--bad-fg)",icon:"✗",text:"Error"}},i=I[k]||I.not_found;return`
${i.icon} ${i.text}`}async function H(){v("detect"),f.style.display="none";try{if(S=await(await fetch("/api/v1/arr/smart-detect")).json(),!S.success){f.innerHTML=`
Detection failed: ${escapeHtml(S.error)}
`,f.style.display="block";return}let I='
';for(const[c,d]of Object.entries(S.services)){const y=T[c]||"\u{1F4E6}",p=P[c]||c,n=d.source?`
${escapeHtml(d.source)}`:"",e=d.version?`
v${escapeHtml(d.version)}`:"",r=(d.hasApiKey||d.hasToken)&&d.status==="connected"?'
Key saved':"";I+=`
- ${y}
+
`);const w=document.getElementById("arr-setup-modal"),x=document.getElementById("arr-setup-btn"),j=document.getElementById("arr-setup-cancel"),g=document.getElementById("smart-connect-btn"),N=document.getElementById("smart-phase-detect"),P=document.getElementById("smart-phase-credentials"),M=document.getElementById("smart-phase-progress"),R=document.getElementById("smart-phase-results"),h=document.getElementById("smart-detect-results"),C=document.getElementById("smart-credential-inputs"),E=document.getElementById("smart-progress-steps"),H=document.getElementById("smart-results-content"),k=document.getElementById("smart-plex-libraries"),z=document.getElementById("smart-retry-btn");let S=null;const T={plex:"\u{1F3AC}",radarr:"\u{1F3AC}",sonarr:"\u{1F4FA}",prowlarr:"\u{1F50D}",seerr:"\u{1F4CB}"},D={plex:"Plex",radarr:"Radarr (Movies)",sonarr:"Sonarr (TV)",prowlarr:"Prowlarr (Indexers)",seerr:"Seerr"};function y(b){N.style.display=b==="detect"?"block":"none",P.style.display=b==="credentials"?"block":"none",M.style.display=b==="progress"?"block":"none",R.style.display=b==="results"?"block":"none"}function B(b){const L={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=L[b]||L.not_found;return`
${l.icon} ${l.text}`}async function $(){y("detect"),h.style.display="none";try{if(S=await(await fetch("/api/v1/arr/smart-detect")).json(),!S.success){h.innerHTML=`
Detection failed: ${escapeHtml(S.error)}
`,h.style.display="block";return}let L='
';for(const[c,d]of Object.entries(S.services)){const f=T[c]||"\u{1F4E6}",p=D[c]||c,e=d.source?`
${escapeHtml(d.source)}`:"",s=d.version?`
v${escapeHtml(d.version)}`:"",r=(d.hasApiKey||d.hasToken)&&d.status==="connected"?'
Key saved':"";L+=`
+
${f}
${p}
- ${n} ${e} ${r}
+ ${e} ${s} ${r}
${B(d.status)}
-
`}I+="
";const i=S.summary;I+=`
- ${escapeHtml(String(i.fullyConnected))}/${escapeHtml(String(i.totalDetected+(5-i.totalDetected)))} services detected ·
- ${escapeHtml(String(i.fullyConnected))} connected${i.needsApiKey>0?` · ${escapeHtml(String(i.needsApiKey))} needs API key`:""}
-
`,f.innerHTML=I,f.style.display="block",O(S),setTimeout(()=>{v("credentials")},800)}catch(k){f.innerHTML=`
Error: ${escapeHtml(k.message)}
`,f.style.display="block"}}function O(k){let I="";const i=k.services,c=["radarr","sonarr","prowlarr"];for(const p of c){const n=i[p];if(!n||n.status==="not_found"&&!n.url)continue;const e=T[p],r=P[p],s=n.status==="connected";I+=`
+
`}L+="
";const l=S.summary;L+=`
+ ${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`:""}
+
`,h.innerHTML=L,h.style.display="block",U(S),setTimeout(()=>{y("credentials")},800)}catch(b){h.innerHTML=`
Error: ${escapeHtml(b.message)}
`,h.style.display="block"}}function U(b){let L="";const l=b.services,c=["radarr","sonarr","prowlarr"];for(const p of c){const e=l[p];if(!e||e.status==="not_found"&&!e.url)continue;const s=T[p],r=D[p],o=e.status==="connected";L+=`
- ${e}
+ ${s}
${r}
- ${s?'✓ Connected':""}
+ ${o?'✓ Connected':""}
-
-
`}const d=i.plex;if(d){const p=d.status==="connected";I+=`
+
+
`}const d=l.plex;if(d){const p=d.status==="connected";L+=`
\u{1F3AC}
Plex
${B(d.status)}
${escapeHtml(d.source||"")}
-
`}const y=i.seerr;if(y){const p=y.status==="connected";let n="";if(y.configuredServices){const e=y.configuredServices;n=`
- Configured: ${e.radarr?"✓ Radarr":"✗ Radarr"} ·
- ${e.sonarr?"✓ Sonarr":"✗ Sonarr"} ·
- ${e.plex?"✓ Plex":"✗ Plex"}
-
`}I+=`
+
`}const f=l.seerr;if(f){const p=f.status==="connected";let e="";if(f.configuredServices){const s=f.configuredServices;e=`
+ Configured: ${s.radarr?"✓ Radarr":"✗ Radarr"} ·
+ ${s.sonarr?"✓ Sonarr":"✗ Sonarr"} ·
+ ${s.plex?"✓ Plex":"✗ Plex"}
+
`}L+=`
\u{1F4CB}
Seerr
- ${B(y.status)}
+ ${B(f.status)}
- ${n}
-
`}C.innerHTML=I}window.smartTestConnection=async function(k){const I=document.getElementById(`smart-${k}-url`),i=document.getElementById(`smart-${k}-key`),c=document.getElementById(`smart-${k}-status`),d=I?.value.trim(),y=i?.value.trim();if(!d||!y){c.innerHTML='
Enter URL and API key';return}c.innerHTML='
';try{const n=await(await secureFetch("/api/v1/arr/test-connection",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({service:k,url:d,apiKey:y})})).json();n.success?c.innerHTML=`
✓ ${escapeHtml(n.appName||"Connected")} v${escapeHtml(n.version||"")}`:c.innerHTML=`
✗ ${escapeHtml(n.error)}`}catch(p){c.innerHTML=`
✗ ${escapeHtml(p.message)}`}};async function u(){v("progress"),E.innerHTML='
';const k={};for(const i of["radarr","sonarr","prowlarr"]){const c=document.getElementById(`smart-${i}-url`)?.value.trim(),d=document.getElementById(`smart-${i}-key`)?.value.trim();d&&c?k[i]={apiKey:d,url:c}:d&&(k[i]={apiKey:d})}const I={services:Object.keys(k).length>0?k:void 0,configurePlex:document.getElementById("smart-opt-plex")?.checked,configureProwlarr:document.getElementById("smart-opt-prowlarr")?.checked,configureSeerr:document.getElementById("smart-opt-seerr")?.checked,saveCredentials:document.getElementById("smart-opt-save")?.checked};try{const c=await(await secureFetch("/api/v1/arr/smart-connect",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(I)})).json();let d="";for(const y of c.steps||[]){const p=y.status==="success"?'
✓':'
✗',n=y.status==="success"?"var(--muted)":"var(--bad-fg)";d+=`
+ ${e}
+
`}C.innerHTML=L,C.querySelectorAll("[data-smart-test]").forEach(p=>{p.addEventListener("click",()=>window.smartTestConnection(p.dataset.smartTest))})}window.smartTestConnection=async function(b){const L=document.getElementById(`smart-${b}-url`),l=document.getElementById(`smart-${b}-key`),c=document.getElementById(`smart-${b}-status`),d=L?.value.trim(),f=l?.value.trim();if(!d||!f){c.innerHTML='
Enter URL and API key';return}c.innerHTML='
';try{const e=await(await secureFetch("/api/v1/arr/test-connection",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({service:b,url:d,apiKey:f})})).json();e.success?c.innerHTML=`
✓ ${escapeHtml(e.appName||"Connected")} v${escapeHtml(e.version||"")}`:c.innerHTML=`
✗ ${escapeHtml(e.error)}`}catch(p){c.innerHTML=`
✗ ${escapeHtml(p.message)}`}};async function m(){y("progress"),E.innerHTML='
';const b={};for(const l of["radarr","sonarr","prowlarr"]){const c=document.getElementById(`smart-${l}-url`)?.value.trim(),d=document.getElementById(`smart-${l}-key`)?.value.trim();d&&c?b[l]={apiKey:d,url:c}:d&&(b[l]={apiKey:d})}const L={services:Object.keys(b).length>0?b:void 0,configurePlex:document.getElementById("smart-opt-plex")?.checked,configureProwlarr:document.getElementById("smart-opt-prowlarr")?.checked,configureSeerr:document.getElementById("smart-opt-seerr")?.checked,saveCredentials:document.getElementById("smart-opt-save")?.checked};try{const c=await(await secureFetch("/api/v1/arr/smart-connect",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(L)})).json();let d="";for(const f of c.steps||[]){const p=f.status==="success"?'
✓':'
✗',e=f.status==="success"?"var(--muted)":"var(--bad-fg)";d+=`
${p}
- ${escapeHtml(y.step)}
- ${escapeHtml(y.details||"")}
-
`}E.innerHTML=d,setTimeout(()=>m(c),500)}catch(i){E.innerHTML=`
Connection error: ${escapeHtml(i.message)}
`}}function m(k){v("results");const I=k.summary||{},i=I.failed===0&&I.succeeded>0,c=i?"var(--ok-fg)":"#f39c12",d=i?"✓":"⚠",y=i?"All Connected!":`${escapeHtml(String(I.succeeded))}/${escapeHtml(String(I.totalSteps))} Steps Succeeded`;let p=`
+ ${escapeHtml(f.step)}
+ ${escapeHtml(f.details||"")}
+
`}E.innerHTML=d,setTimeout(()=>v(c),500)}catch(l){E.innerHTML=`
Connection error: ${escapeHtml(l.message)}
`}}function v(b){y("results");const L=b.summary||{},l=L.failed===0&&L.succeeded>0,c=l?"var(--ok-fg)":"#f39c12",d=l?"✓":"⚠",f=l?"All Connected!":`${escapeHtml(String(L.succeeded))}/${escapeHtml(String(L.totalSteps))} Steps Succeeded`;let p=`
${d}
-
${y}
-
${escapeHtml(String(I.succeeded))} succeeded, ${escapeHtml(String(I.failed))} failed
-
`;p+='
';for(const n of k.steps||[]){const e=n.status==="success"?'
✓':'
✗';p+=`
- ${e} ${escapeHtml(n.step)} ${escapeHtml(n.details||"")}
-
`}p+="
",M.innerHTML=p,z.style.display=I.failed>0?"block":"none",k.steps?.some(n=>n.step.includes("Plex")&&n.status==="success")&&L()}async function L(){try{const I=await(await fetch("/api/v1/plex/libraries")).json();if(I.success&&I.libraries?.length>0){let i=`
-
\u{1F3AC} ${escapeHtml(I.serverName)} Libraries
-
`;for(const c of I.libraries){const d=c.type==="movie"?"\u{1F3AC}":c.type==="show"?"\u{1F4FA}":"\u{1F3B5}";i+=`
+
${f}
+
${escapeHtml(String(L.succeeded))} succeeded, ${escapeHtml(String(L.failed))} failed
+
`;p+='
';for(const e of b.steps||[]){const s=e.status==="success"?'
✓':'
✗';p+=`
+ ${s} ${escapeHtml(e.step)} ${escapeHtml(e.details||"")}
+
`}p+="
",H.innerHTML=p,z.style.display=L.failed>0?"block":"none",b.steps?.some(e=>e.step.includes("Plex")&&e.status==="success")&&I()}async function I(){try{const L=await(await fetch("/api/v1/plex/libraries")).json();if(L.success&&L.libraries?.length>0){let l=`
+
\u{1F3AC} ${escapeHtml(L.serverName)} Libraries
+
`;for(const c of L.libraries){const d=c.type==="movie"?"\u{1F3AC}":c.type==="show"?"\u{1F4FA}":"\u{1F3B5}";l+=`
${d} ${escapeHtml(c.title)}
${escapeHtml(String(c.count))} items
-
`}i+="
",w.innerHTML=i,w.style.display="block"}}catch{}}b?.addEventListener("click",()=>{x.classList.add("show"),w.style.display="none",H()}),wireModal(x,j),g?.addEventListener("click",u),z?.addEventListener("click",u)})(),(function(){const x=new ErrorHandler;injectModal("notifications-modal",`
+
`}l+="
",k.innerHTML=l,k.style.display="block"}}catch{}}x?.addEventListener("click",()=>{w.classList.add("show"),k.style.display="none",$()}),wireModal(w,j),g?.addEventListener("click",m),z?.addEventListener("click",m)})(),(function(){const w=new ErrorHandler;injectModal("notifications-modal",`
\u{1F514} Notification Settings
@@ -1109,15 +1109,15 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};
-
`);const b=document.getElementById("notifications-modal"),j=document.getElementById("manage-notifications"),g=document.getElementById("notifications-save"),N=document.getElementById("notifications-cancel");["discord","telegram","ntfy","email"].forEach(w=>{const z=document.getElementById(`${w}-enabled`),S=document.getElementById(`${w}-config`);z?.addEventListener("change",()=>{S.style.display=z.checked?"block":"none"})});const A=document.getElementById("health-check-enabled"),D=document.getElementById("health-check-config");A?.addEventListener("change",()=>{D.style.opacity=A.checked?"1":"0.5"});async function R(){try{const z=await(await fetch("/api/v1/notifications/config")).json();if(z.success){const S=z.config;document.getElementById("notifications-enabled").checked=S.enabled,document.getElementById("discord-enabled").checked=S.providers?.discord?.enabled||!1,document.getElementById("telegram-enabled").checked=S.providers?.telegram?.enabled||!1,document.getElementById("ntfy-enabled").checked=S.providers?.ntfy?.enabled||!1,document.getElementById("email-enabled").checked=S.providers?.email?.enabled||!1,document.getElementById("discord-config").style.display=S.providers?.discord?.enabled?"block":"none",document.getElementById("telegram-config").style.display=S.providers?.telegram?.enabled?"block":"none",document.getElementById("ntfy-config").style.display=S.providers?.ntfy?.enabled?"block":"none",document.getElementById("email-config").style.display=S.providers?.email?.enabled?"block":"none",S.providers?.ntfy?.serverUrl&&(document.getElementById("ntfy-server").value=S.providers.ntfy.serverUrl),S.providers?.email?.host&&(document.getElementById("email-host").value=S.providers.email.host),S.providers?.email?.from&&(document.getElementById("email-from").value=S.providers.email.from),S.providers?.email?.to&&(document.getElementById("email-to").value=S.providers.email.to),S.providers?.email?.port&&(document.getElementById("email-port").value=S.providers.email.port),S.providers?.email?.secure!==void 0&&(document.getElementById("email-secure").checked=S.providers.email.secure===!0),S.providers?.email?.username&&(document.getElementById("email-user").value=S.providers.email.username);const T=document.getElementById("email-pass");S.providers?.email?.hasPassword?(T.value="",T.placeholder="saved \u2014 leave blank to keep"):T.placeholder="app password",document.getElementById("health-check-enabled").checked=S.healthCheck?.enabled||!1,S.healthCheck?.intervalMinutes&&(document.getElementById("health-check-interval").value=S.healthCheck.intervalMinutes),S.healthCheck?.lastCheck&&(document.getElementById("health-check-status").textContent=`Last check: ${new Date(S.healthCheck.lastCheck).toLocaleString()}`),document.getElementById("event-container-down").checked=S.events?.["container-down"]!==!1,document.getElementById("event-container-up").checked=S.events?.["container-up"]===!0,document.getElementById("event-deploy-success").checked=S.events?.["deploy-success"]!==!1,document.getElementById("event-deploy-failed").checked=S.events?.["deploy-failed"]!==!1,document.getElementById("event-ssl-cert-expiry").checked=S.events?.["ssl-cert-expiry"]!==!1,document.getElementById("event-dns-propagation").checked=S.events?.["dns-propagation"]!==!1,document.getElementById("event-drift-detected").checked=S.events?.["drift-detected"]!==!1,document.getElementById("event-dependency-restart").checked=S.events?.["dependency-restart"]!==!1,document.getElementById("event-recipe-removed").checked=S.events?.["recipe-removed"]!==!1,document.getElementById("event-workflow").checked=S.events?.workflow!==!1,document.getElementById("event-backup-complete").checked=S.events?.["backup-complete"]!==!1,document.getElementById("event-backup-failed").checked=S.events?.["backup-failed"]!==!1,document.getElementById("event-update-available").checked=S.events?.["update-available"]!==!1,document.getElementById("event-resource-alert").checked=S.events?.alert!==!1}}catch(w){x.logError("[Notifications] Load Config",w,{function:"loadConfig"})}}async function f(){try{const z=await(await fetch("/api/v1/notifications/history?limit=10")).json(),S=document.getElementById("notification-history");z.success&&z.history?.length>0?S.innerHTML=z.history.map(T=>{const P=new Date(T.timestamp).toLocaleString();return`
+