",g.innerHTML=I,S("setup-step-summary")}async function B(g){try{const I=await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(g)});return I.ok?(await I.json(),!0):(console.error("Failed to save config to server:",I.status),!1)}catch(I){return console.error("Error saving config to server:",I),!1}}async function C(){const g={setupComplete:!0,configurationType:f,timestamp:new Date().toISOString(),timezone:document.getElementById("setup-timezone")?.value||Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC"};f==="homelab"?(g.tld=document.getElementById("setup-tld")?.value?.trim()||".home",g.caName=document.getElementById("setup-ca-name")?.value?.trim()||"",g.dns={provider:"technitium",ip:document.getElementById("setup-dns-ip")?.value?.trim()||"",port:document.getElementById("setup-dns-port")?.value?.trim()||DC.DEFAULTS.DNS_PORT,token:document.getElementById("setup-dns-token")?.value?.trim()||""},g.defaults={dnsType:"private",sslType:"internal",targetIP:"localhost"}):f==="simple"?(g.defaultIP=document.getElementById("setup-simple-ip")?.value?.trim()||"localhost",g.defaults={dnsType:"none",sslType:"none",targetIP:g.defaultIP}):f==="public"&&(g.domain=document.getElementById("setup-public-domain")?.value?.trim()||"",g.email=document.getElementById("setup-public-email")?.value?.trim()||"",g.routingMode=document.querySelector('input[name="routing-mode"]:checked')?.value||"subdirectory",g.defaults={dnsType:g.routingMode==="subdirectory"?"none":"public",sslType:"letsencrypt",targetIP:"localhost"});const I=await B(g);safeSet("dashcaddy-config",JSON.stringify(g)),safeSet("dashcaddy-setup","completed"),document.getElementById("setup-wizard").style.display="none";const c=f==="homelab"?"Professional Home Lab":f==="simple"?"Simple Setup":"Public Server",l=I?"server (shared across all devices)":"locally (this browser only)";showNotification(`Setup Complete! Configured for: ${c}. Settings saved to: ${l}`,"success",5e3),setTimeout(()=>location.reload(),500)}const m=document.getElementById("setup-step-1-next");m&&(m.onclick=function(g){g.preventDefault();const I=document.querySelector('input[name="config-type"]:checked');I&&(f=I.value),S(f==="homelab"?"setup-step-homelab":f==="simple"?"setup-step-simple":f==="public"?"setup-step-public":"setup-step-homelab")});const h=document.getElementById("setup-skip");h&&(h.onclick=async function(g){g.preventDefault(),confirm("Skip setup? You can run it later from Settings.")&&(await B({setupComplete:!0,skipped:!0,timestamp:new Date().toISOString()}),safeSet("dashcaddy-setup","skipped"),document.getElementById("setup-wizard").style.display="none")});const u=document.getElementById("setup-tld");u&&(u.oninput=function(g){const I=g.target.value||".home",c=document.getElementById("tld-preview"),l=document.getElementById("tld-preview-2");c&&(c.textContent=I),l&&(l.textContent=I)});const y=document.getElementById("setup-homelab-back");y&&(y.onclick=function(g){g.preventDefault(),S("setup-step-1")});const x=document.getElementById("setup-homelab-next");x&&(x.onclick=function(g){g.preventDefault();const I=document.getElementById("setup-tld")?.value?.trim()||"",c=document.getElementById("setup-ca-name")?.value?.trim()||"",l=document.getElementById("setup-dns-ip")?.value?.trim()||"";if(!I||!I.startsWith(".")){showNotification("Please enter a valid TLD starting with a dot (e.g., .home)","warning");return}if(!c){showNotification("Please enter a Certificate Authority name","warning");return}if(!l){showNotification("Please enter your DNS server IP address","warning");return}D()});const O=document.getElementById("setup-simple-back");O&&(O.onclick=function(g){g.preventDefault(),S("setup-step-1")});const A=document.getElementById("setup-simple-next");A&&(A.onclick=function(g){g.preventDefault(),D()}),document.querySelectorAll('input[name="routing-mode"]').forEach(function(g){g.onchange=function(){var I=document.getElementById("dns-requirement-note");I&&(I.textContent=this.value==="subdirectory"?"Only one DNS record needed (for the main domain)":"You'll need to configure DNS manually for each subdomain")}});const N=document.getElementById("setup-public-back");N&&(N.onclick=function(g){g.preventDefault(),S("setup-step-1")});const z=document.getElementById("setup-public-next");z&&(z.onclick=function(g){g.preventDefault();const I=document.getElementById("setup-public-domain")?.value?.trim()||"",c=document.getElementById("setup-public-email")?.value?.trim()||"";if(!I){showNotification("Please enter your domain name","warning");return}if(!c||!c.includes("@")){showNotification("Please enter a valid email address","warning");return}D()});const H=document.getElementById("setup-summary-back");H&&(H.onclick=function(g){g.preventDefault(),f==="homelab"?S("setup-step-homelab"):f==="simple"?S("setup-step-simple"):f==="public"&&S("setup-step-public")});const L=document.getElementById("setup-finish");L&&(L.onclick=function(g){g.preventDefault(),C()}),window.getGlobalConfig=async function(){try{const I=await fetch("/api/v1/config");if(I.ok){const c=await I.json();if(c&&c.setupComplete)return c}}catch{console.warn("Could not fetch config from server")}const g=safeGet("dashcaddy-config");return g?JSON.parse(g):{setupComplete:!1,configurationType:"homelab",tld:".home",caName:"",defaults:{dnsType:"private",sslType:"internal",targetIP:"localhost"}}},window.resetSetupWizard=async function(){if(confirm("Reset DashCaddy configuration? This will show the setup wizard again.")){try{await secureFetch("/api/v1/config",{method:"DELETE"})}catch{console.warn("Could not delete server config")}safeRemove("dashcaddy-setup"),safeRemove("dashcaddy-config"),location.reload()}}})(),(function(){injectModal("app-selector-modal",`
+ `,T+="
",S.innerHTML=T,C("setup-step-summary")}async function $(S){try{const T=await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(S)});return T.ok?(await T.json(),!0):(console.error("Failed to save config to server:",T.status),!1)}catch(T){return console.error("Error saving config to server:",T),!1}}async function L(){const S={setupComplete:!0,configurationType:w,timestamp:new Date().toISOString(),timezone:document.getElementById("setup-timezone")?.value||Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC"};w==="homelab"?(S.tld=document.getElementById("setup-tld")?.value?.trim()||".home",S.caName=document.getElementById("setup-ca-name")?.value?.trim()||"",S.dns={provider:"technitium",ip:document.getElementById("setup-dns-ip")?.value?.trim()||"",port:document.getElementById("setup-dns-port")?.value?.trim()||DC.DEFAULTS.DNS_PORT,token:document.getElementById("setup-dns-token")?.value?.trim()||""},S.defaults={dnsType:"private",sslType:"internal",targetIP:"localhost"}):w==="simple"?(S.defaultIP=document.getElementById("setup-simple-ip")?.value?.trim()||"localhost",S.defaults={dnsType:"none",sslType:"none",targetIP:S.defaultIP}):w==="public"&&(S.domain=document.getElementById("setup-public-domain")?.value?.trim()||"",S.email=document.getElementById("setup-public-email")?.value?.trim()||"",S.routingMode=document.querySelector('input[name="routing-mode"]:checked')?.value||"subdirectory",S.defaults={dnsType:S.routingMode==="subdirectory"?"none":"public",sslType:"letsencrypt",targetIP:"localhost"});const T=await $(S);safeSet("dashcaddy-config",JSON.stringify(S)),safeSet("dashcaddy-setup","completed"),document.getElementById("setup-wizard").style.display="none";const b=w==="homelab"?"Professional Home Lab":w==="simple"?"Simple Setup":"Public Server",u=T?"server (shared across all devices)":"locally (this browser only)";showNotification(`Setup Complete! Configured for: ${b}. Settings saved to: ${u}`,"success",5e3),setTimeout(()=>location.reload(),500)}const g=document.getElementById("setup-step-1-next");g&&(g.onclick=function(S){S.preventDefault();const T=document.querySelector('input[name="config-type"]:checked');T&&(w=T.value),C(w==="homelab"?"setup-step-homelab":w==="simple"?"setup-step-simple":w==="public"?"setup-step-public":"setup-step-homelab")});const k=document.getElementById("setup-skip");k&&(k.onclick=async function(S){S.preventDefault(),confirm("Skip setup? You can run it later from Settings.")&&(await $({setupComplete:!0,skipped:!0,timestamp:new Date().toISOString()}),safeSet("dashcaddy-setup","skipped"),document.getElementById("setup-wizard").style.display="none")});const f=document.getElementById("setup-tld");f&&(f.oninput=function(S){const T=S.target.value||".home",b=document.getElementById("tld-preview"),u=document.getElementById("tld-preview-2");b&&(b.textContent=T),u&&(u.textContent=T)});const x=document.getElementById("setup-homelab-back");x&&(x.onclick=function(S){S.preventDefault(),C("setup-step-1")});const E=document.getElementById("setup-homelab-next");E&&(E.onclick=function(S){S.preventDefault();const T=document.getElementById("setup-tld")?.value?.trim()||"",b=document.getElementById("setup-ca-name")?.value?.trim()||"",u=document.getElementById("setup-dns-ip")?.value?.trim()||"";if(!T||!T.startsWith(".")){showNotification("Please enter a valid TLD starting with a dot (e.g., .home)","warning");return}if(!b){showNotification("Please enter a Certificate Authority name","warning");return}if(!u){showNotification("Please enter your DNS server IP address","warning");return}P()});const R=document.getElementById("setup-simple-back");R&&(R.onclick=function(S){S.preventDefault(),C("setup-step-1")});const O=document.getElementById("setup-simple-next");O&&(O.onclick=function(S){S.preventDefault(),P()}),document.querySelectorAll('input[name="routing-mode"]').forEach(function(S){S.onchange=function(){var T=document.getElementById("dns-requirement-note");T&&(T.textContent=this.value==="subdirectory"?"Only one DNS record needed (for the main domain)":"You'll need to configure DNS manually for each subdomain")}});const D=document.getElementById("setup-public-back");D&&(D.onclick=function(S){S.preventDefault(),C("setup-step-1")});const N=document.getElementById("setup-public-next");N&&(N.onclick=function(S){S.preventDefault();const T=document.getElementById("setup-public-domain")?.value?.trim()||"",b=document.getElementById("setup-public-email")?.value?.trim()||"";if(!T){showNotification("Please enter your domain name","warning");return}if(!b||!b.includes("@")){showNotification("Please enter a valid email address","warning");return}P()});const M=document.getElementById("setup-summary-back");M&&(M.onclick=function(S){S.preventDefault(),w==="homelab"?C("setup-step-homelab"):w==="simple"?C("setup-step-simple"):w==="public"&&C("setup-step-public")});const H=document.getElementById("setup-finish");H&&(H.onclick=function(S){S.preventDefault(),L()}),window.getGlobalConfig=async function(){try{const T=await fetch("/api/v1/config");if(T.ok){const b=await T.json();if(b&&b.setupComplete)return b}}catch{console.warn("Could not fetch config from server")}const S=safeGet("dashcaddy-config");return S?JSON.parse(S):{setupComplete:!1,configurationType:"homelab",tld:".home",caName:"",defaults:{dnsType:"private",sslType:"internal",targetIP:"localhost"}}},window.resetSetupWizard=async function(){if(confirm("Reset DashCaddy configuration? This will show the setup wizard again.")){try{await secureFetch("/api/v1/config",{method:"DELETE"})}catch{console.warn("Could not delete server config")}safeRemove("dashcaddy-setup"),safeRemove("dashcaddy-config"),location.reload()}}})(),(function(){injectModal("app-selector-modal",`
Choose an App
@@ -333,52 +333,52 @@ This will reset the logo, favicon, title, and position.`))try{if((await secureFe
-
`);const f="custom-apps";let E=null,M=null;const k=document.getElementById("app-selector-modal"),S=document.getElementById("app-selector-grid");async function D(){try{const t=await(await fetch("/api/v1/apps/templates")).json();if(t.success)return E=t.templates,M=t.categories,!0}catch(e){console.error("Failed to fetch app templates:",e)}return!1}async function B(e){try{return await(await fetch(`/api/v1/apps/ports/${e}/check`)).json()}catch(t){return console.error("Failed to check port:",t),{available:!0}}}async function C(e){try{const s=await(await fetch(`/api/v1/apps/ports/${e}/suggest`)).json();if(s.success)return s.suggestedPort}catch(t){console.error("Failed to get suggested port:",t)}return e}async function m(){if(S.innerHTML='
Loading app templates...
',!E&&!await D()){S.innerHTML='
Failed to load app templates. Please try again.
';return}S.innerHTML="";const e={};for(const[s,r]of Object.entries(E)){const p=r.category||"Other";e[p]||(e[p]=[]),e[p].push({id:s,...r})}const t=M?Object.keys(M):Object.keys(e).sort();for(const s of t){const r=e[s];if(!r||r.length===0)continue;r.sort((o,i)=>(i.popularity||0)-(o.popularity||0));const p=document.createElement("div");p.className="app-category-header";const b=M?.[s]||{};p.innerHTML=`${escapeHtml(b.icon||"")} ${escapeHtml(s)}`,b.color&&(p.style.borderBottomColor=b.color),S.appendChild(p),r.forEach(o=>{const i=document.createElement("div");i.className="app-option";const v=o.isDashboardWidget,d=v&&safeGet("widget-"+o.id+"-enabled")!=="false",w=v?`
${d?"ON":"OFF"}
`:"",T=!v&&o.difficulty?`
${escapeHtml(o.difficulty)}
`:"";i.innerHTML=`
-
${escapeHtml(o.icon||"\u{1F4E6}")}
-
${escapeHtml(o.name)}
-
${escapeHtml(o.description||"")}
- ${w}${T}
- `,v?i.onclick=()=>h(o,i):i.onclick=()=>u(o),S.appendChild(i)})}window.renderRecipeCards&&await window.renderRecipeCards(S)}function h(e,t){const s="widget-"+e.id+"-enabled",p=!(safeGet(s)!=="false");safeSet(s,String(p));const b=e.widgetSelector;if(b){const i=document.querySelector(b);i&&(i.style.display=p?"":"none")}const o=t.querySelector('div[style*="border-radius: 4px"]');o&&(o.textContent=p?"ON":"OFF",o.style.background=p?"#2ecc7130":"#e74c3c30",o.style.color=p?"#2ecc71":"#e74c3c"),showNotification(`${e.name} widget ${p?"enabled":"disabled"}`,"success",2e3)}async function u(e){const t=document.getElementById("app-deploy-modal"),s=document.getElementById("app-deploy-title"),r=document.getElementById("deploy-subdomain"),p=document.getElementById("deploy-url-preview"),b=document.getElementById("deploy-ip"),o=document.getElementById("deploy-port"),i=document.getElementById("deploy-tailscale-only"),v=document.getElementById("tailscale-status");try{const q=await(await secureFetch("/api/v1/apps/check-existing",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({appId:e.id})})).json();if(q.success&&q.exists){const W=q.container;confirm(`Found existing ${e.name} container:
+ `);const w="custom-apps";let B=null,z=null;const I=document.getElementById("app-selector-modal"),C=document.getElementById("app-selector-grid");async function P(){try{const l=await(await fetch("/api/v1/apps/templates")).json();if(l.success)return B=l.templates,z=l.categories,!0}catch(o){console.error("Failed to fetch app templates:",o)}return!1}async function $(o){try{return await(await fetch(`/api/v1/apps/ports/${o}/check`)).json()}catch(l){return console.error("Failed to check port:",l),{available:!0}}}async function L(o){try{const v=await(await fetch(`/api/v1/apps/ports/${o}/suggest`)).json();if(v.success)return v.suggestedPort}catch(l){console.error("Failed to get suggested port:",l)}return o}async function g(){if(C.innerHTML='
Loading app templates...
',!B&&!await P()){C.innerHTML='
Failed to load app templates. Please try again.
';return}C.innerHTML="";const o={};for(const[v,r]of Object.entries(B)){const p=r.category||"Other";o[p]||(o[p]=[]),o[p].push({id:v,...r})}const l=z?Object.keys(z):Object.keys(o).sort();for(const v of l){const r=o[v];if(!r||r.length===0)continue;r.sort((n,e)=>(e.popularity||0)-(n.popularity||0));const p=document.createElement("div");p.className="app-category-header";const m=z?.[v]||{};p.innerHTML=`${escapeHtml(m.icon||"")} ${escapeHtml(v)}`,m.color&&(p.style.borderBottomColor=m.color),C.appendChild(p),r.forEach(n=>{const e=document.createElement("div");e.className="app-option";const a=n.isDashboardWidget,t=a&&safeGet("widget-"+n.id+"-enabled")!=="false",s=a?`
${t?"ON":"OFF"}
`:"",i=!a&&n.difficulty?`
${escapeHtml(n.difficulty)}
`:"";e.innerHTML=`
+
${escapeHtml(n.icon||"\u{1F4E6}")}
+
${escapeHtml(n.name)}
+
${escapeHtml(n.description||"")}
+ ${s}${i}
+ `,a?e.onclick=()=>k(n,e):e.onclick=()=>f(n),C.appendChild(e)})}window.renderRecipeCards&&await window.renderRecipeCards(C)}function k(o,l){const v="widget-"+o.id+"-enabled",p=!(safeGet(v)!=="false");safeSet(v,String(p));const m=o.widgetSelector;if(m){const e=document.querySelector(m);e&&(e.style.display=p?"":"none")}const n=l.querySelector('div[style*="border-radius: 4px"]');n&&(n.textContent=p?"ON":"OFF",n.style.background=p?"#2ecc7130":"#e74c3c30",n.style.color=p?"#2ecc71":"#e74c3c"),showNotification(`${o.name} widget ${p?"enabled":"disabled"}`,"success",2e3)}async function f(o){const l=document.getElementById("app-deploy-modal"),v=document.getElementById("app-deploy-title"),r=document.getElementById("deploy-subdomain"),p=document.getElementById("deploy-url-preview"),m=document.getElementById("deploy-ip"),n=document.getElementById("deploy-port"),e=document.getElementById("deploy-tailscale-only"),a=document.getElementById("tailscale-status");try{const J=await(await secureFetch("/api/v1/apps/check-existing",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({appId:o.id})})).json();if(J.success&&J.exists){const V=J.container;confirm(`Found existing ${o.name} container:
-Container: ${W.name}
-Status: ${W.status}
-Port: ${W.primaryPort||"N/A"}
+Container: ${V.name}
+Status: ${V.status}
+Port: ${V.primaryPort||"N/A"}
Would you like to use this existing container?
Click OK to configure DNS/Caddy for the existing container.
-Click Cancel to deploy a new container.`)&&(e._useExisting=!0,e._existingContainer=W)}}catch{}s.textContent=`Deploy ${e.name}`;const d=e.subdomain||e.id.replace(/-/g,"");r.value=d;const w=document.getElementById("subpath-compat-warning");if(w)if(SITE.routingMode==="subdirectory"){const j=e.subpathSupport||"strip";j==="none"?(w.style.display="block",w.innerHTML='⚠ '+e.name+" does not support subdirectory mode. It may not work correctly at a subpath."):j==="strip"?(w.style.display="block",w.innerHTML='ⓘ '+e.name+" has unverified subdirectory support. It may require additional configuration."):w.style.display="none"}else w.style.display="none";const T=SITE.defaults.dnsType||(SITE.configurationType==="public"?"public":"private"),$=SITE.defaults.sslType||(SITE.configurationType==="public"?"letsencrypt":"internal"),P=document.querySelector(`input[name="dns-type"][value="${T}"]`),R=document.querySelector(`input[name="ssl-type"][value="${$}"]`);P?P.checked=!0:document.querySelector('input[name="dns-type"][value="private"]').checked=!0,R?R.checked=!0:document.querySelector('input[name="ssl-type"][value="internal"]').checked=!0,b.value=SITE.defaults.targetIP||"localhost",i.checked=!1;const U=document.querySelector("#app-deploy-modal .flex-col-gap")?.closest("div"),_=document.querySelector("#app-deploy-modal details"),J=_?.querySelector("div");if(_&&J&&(SITE.configurationType==="public"||SITE.configurationType==="homelab")){const j=document.querySelectorAll('#app-deploy-modal input[name="dns-type"]')[0]?.closest("div.flex-col-gap")?.parentElement,q=document.querySelectorAll('#app-deploy-modal input[name="ssl-type"]')[0]?.closest("div.flex-col-gap")?.parentElement;j&&!j.dataset.moved&&(J.appendChild(j),j.dataset.moved="1"),q&&!q.dataset.moved&&(J.appendChild(q),q.dataset.moved="1")}const F=document.getElementById("media-path-section"),K=document.getElementById("deploy-media-path"),se=document.getElementById("media-path-description");if(e.mediaMount){F.style.display="block",K.value="",K.placeholder="/media/Movies, /media/TVShows or click Browse";const j=document.getElementById("detected-mounts-container"),q=document.getElementById("detected-mounts-list");try{const G=await(await fetch("/api/v1/media/detected-mounts")).json();if(G.success&&G.mounts.length>0){j.style.display="block",q.innerHTML="";const ee=[...new Set(G.mounts.map(Z=>Z.hostPath))];K.value=ee.join(", "),G.mounts.forEach(Z=>{const Y=document.createElement("button");Y.type="button";const le=ee.includes(Z.hostPath);Y.style.cssText=`padding: 8px 14px; font-size: 0.85rem; background: color-mix(in srgb, var(--success) ${le?"40%":"15%"}, var(--card-bg)); border: 1px solid var(--success); border-radius: 6px; cursor: pointer; color: var(--fg);`,Y.innerHTML=`${escapeHtml(Z.folderName)} from ${escapeHtml(Z.sourceImage)}`,Y.title=`${Z.hostPath} (from ${Z.sourceContainer})`,Y.onclick=()=>{const re=K.value.split(",").map(de=>de.trim()).filter(de=>de),ce=re.indexOf(Z.hostPath);ce>=0?(re.splice(ce,1),Y.style.background="color-mix(in srgb, var(--success) 15%, var(--card-bg))"):(re.push(Z.hostPath),Y.style.background="color-mix(in srgb, var(--success) 40%, var(--card-bg))"),K.value=re.join(", ")},q.appendChild(Y)})}else j.style.display="none"}catch{j.style.display="none"}document.getElementById("browse-media-btn").onclick=()=>{openFolderBrowser(K)}}else F.style.display="none",K.value="",document.getElementById("detected-mounts-container").style.display="none";const V=document.getElementById("plex-claim-section");V&&(e.id==="plex"||e.claimToken?(V.style.display="block",document.getElementById("deploy-plex-claim").value=""):V.style.display="none");const Q=document.getElementById("volume-mounts-section"),te=document.getElementById("volume-mounts-list");if(te.innerHTML="",e.docker?.volumes?.length){const j=e.mediaMount?.containerPath,q=e.docker.volumes.filter(W=>!W.includes("{{MEDIA_PATH}}")&&!(j&&W.endsWith(":"+j)));q.length>0?(Q.style.display="block",q.forEach((W,G)=>{const[ee,Z]=W.split(":"),Y=document.createElement("div");Y.style.cssText="display: flex; gap: 6px; align-items: center;",Y.innerHTML=`
- ⚠ '+o.name+" does not support subdirectory mode. It may not work correctly at a subpath."):q==="strip"?(s.style.display="block",s.innerHTML='ⓘ '+o.name+" has unverified subdirectory support. It may require additional configuration."):s.style.display="none"}else s.style.display="none";const i=SITE.defaults.dnsType||(SITE.configurationType==="public"?"public":"private"),h=SITE.defaults.sslType||(SITE.configurationType==="public"?"letsencrypt":"internal"),d=document.querySelector(`input[name="dns-type"][value="${i}"]`),A=document.querySelector(`input[name="ssl-type"][value="${h}"]`);d?d.checked=!0:document.querySelector('input[name="dns-type"][value="private"]').checked=!0,A?A.checked=!0:document.querySelector('input[name="ssl-type"][value="internal"]').checked=!0,m.value=SITE.defaults.targetIP||"localhost",e.checked=!1;const F=document.querySelector("#app-deploy-modal .flex-col-gap")?.closest("div"),j=document.querySelector("#app-deploy-modal details"),_=j?.querySelector("div");if(j&&_&&(SITE.configurationType==="public"||SITE.configurationType==="homelab")){const q=document.querySelectorAll('#app-deploy-modal input[name="dns-type"]')[0]?.closest("div.flex-col-gap")?.parentElement,J=document.querySelectorAll('#app-deploy-modal input[name="ssl-type"]')[0]?.closest("div.flex-col-gap")?.parentElement;q&&!q.dataset.moved&&(_.appendChild(q),q.dataset.moved="1"),J&&!J.dataset.moved&&(_.appendChild(J),J.dataset.moved="1")}const U=document.getElementById("media-path-section"),W=document.getElementById("deploy-media-path"),Z=document.getElementById("media-path-description");if(o.mediaMount){U.style.display="block",W.value="",W.placeholder="/media/Movies, /media/TVShows or click Browse";const q=document.getElementById("detected-mounts-container"),J=document.getElementById("detected-mounts-list");try{const K=await(await fetch("/api/v1/media/detected-mounts")).json();if(K.success&&K.mounts.length>0){q.style.display="block",J.innerHTML="";const te=[...new Set(K.mounts.map(ee=>ee.hostPath))];W.value=te.join(", "),K.mounts.forEach(ee=>{const Y=document.createElement("button");Y.type="button";const le=te.includes(ee.hostPath);Y.style.cssText=`padding: 8px 14px; font-size: 0.85rem; background: color-mix(in srgb, var(--success) ${le?"40%":"15%"}, var(--card-bg)); border: 1px solid var(--success); border-radius: 6px; cursor: pointer; color: var(--fg);`,Y.innerHTML=`${escapeHtml(ee.folderName)} from ${escapeHtml(ee.sourceImage)}`,Y.title=`${ee.hostPath} (from ${ee.sourceContainer})`,Y.onclick=()=>{const re=W.value.split(",").map(de=>de.trim()).filter(de=>de),ce=re.indexOf(ee.hostPath);ce>=0?(re.splice(ce,1),Y.style.background="color-mix(in srgb, var(--success) 15%, var(--card-bg))"):(re.push(ee.hostPath),Y.style.background="color-mix(in srgb, var(--success) 40%, var(--card-bg))"),W.value=re.join(", ")},J.appendChild(Y)})}else q.style.display="none"}catch{q.style.display="none"}document.getElementById("browse-media-btn").onclick=()=>{openFolderBrowser(W)}}else U.style.display="none",W.value="",document.getElementById("detected-mounts-container").style.display="none";const G=document.getElementById("plex-claim-section");G&&(o.id==="plex"||o.claimToken?(G.style.display="block",document.getElementById("deploy-plex-claim").value=""):G.style.display="none");const Q=document.getElementById("volume-mounts-section"),ae=document.getElementById("volume-mounts-list");if(ae.innerHTML="",o.docker?.volumes?.length){const q=o.mediaMount?.containerPath,J=o.docker.volumes.filter(V=>!V.includes("{{MEDIA_PATH}}")&&!(q&&V.endsWith(":"+q)));J.length>0?(Q.style.display="block",J.forEach((V,K)=>{const[te,ee]=V.split(":"),Y=document.createElement("div");Y.style.cssText="display: flex; gap: 6px; align-items: center;",Y.innerHTML=`
+
- \u2192 ${Z}
+ \u2192 ${ee}
- `,te.appendChild(Y),Y.querySelector(".vol-browse-btn").onclick=()=>{const le=Y.querySelector(".vol-host-path");openFolderBrowser(le)}})):Q.style.display="none"}else Q.style.display="none";const ne=e.defaultPort||8080;o.value="",o.placeholder=`Default: ${ne}`;let X=document.getElementById("deploy-port-status");X||(X=document.createElement("div"),X.id="deploy-port-status",X.style.cssText="font-size: 0.8rem; margin-top: 4px;",o.parentNode.appendChild(X));async function oe(){const j=o.value||ne;X.innerHTML='Checking port...';const q=await B(j);if(q.available)X.innerHTML=`Port ${escapeHtml(String(j))} is available`;else{const W=await C(ne);X.innerHTML=`
- Port ${escapeHtml(j)} in use by ${escapeHtml(q.conflict?.usedBy||"unknown")}
- `;const G=document.createElement("button");G.type="button",G.textContent=`Use ${W}`,G.style.cssText="margin-left: 8px; padding: 2px 8px; font-size: 0.75rem; cursor: pointer;",G.onclick=()=>{document.getElementById("deploy-port").value=W,X.innerHTML=`Using suggested port ${escapeHtml(String(W))}`},X.appendChild(G)}}let ie;o.oninput=function(){clearTimeout(ie),ie=setTimeout(oe,500)},oe();try{const q=await(await fetch("/api/v1/tailscale/status")).json();q.success&&q.installed&&q.connected?v.innerHTML=`
+ `,ae.appendChild(Y),Y.querySelector(".vol-browse-btn").onclick=()=>{const le=Y.querySelector(".vol-host-path");openFolderBrowser(le)}})):Q.style.display="none"}else Q.style.display="none";const ne=o.defaultPort||8080;n.value="",n.placeholder=`Default: ${ne}`;let X=document.getElementById("deploy-port-status");X||(X=document.createElement("div"),X.id="deploy-port-status",X.style.cssText="font-size: 0.8rem; margin-top: 4px;",n.parentNode.appendChild(X));async function se(){const q=n.value||ne;X.innerHTML='Checking port...';const J=await $(q);if(J.available)X.innerHTML=`Port ${escapeHtml(String(q))} is available`;else{const V=await L(ne);X.innerHTML=`
+ Port ${escapeHtml(q)} in use by ${escapeHtml(J.conflict?.usedBy||"unknown")}
+ `;const K=document.createElement("button");K.type="button",K.textContent=`Use ${V}`,K.style.cssText="margin-left: 8px; padding: 2px 8px; font-size: 0.75rem; cursor: pointer;",K.onclick=()=>{document.getElementById("deploy-port").value=V,X.innerHTML=`Using suggested port ${escapeHtml(String(V))}`},X.appendChild(K)}}let ie;n.oninput=function(){clearTimeout(ie),ie=setTimeout(se,500)},se();try{const J=await(await fetch("/api/v1/tailscale/status")).json();J.success&&J.installed&&J.connected?a.innerHTML=`
Connected
- ${q.self?.hostname} (${q.self?.ip})
- | ${q.deviceCount} devices
- `:q.installed?v.innerHTML='Not connected':(v.innerHTML='Not available',i.disabled=!0)}catch{v.innerHTML='Could not check status'}function ae(){const j=r.value||"subdomain",q=document.querySelector('input[name="dns-type"]:checked').value,W=document.querySelector('input[name="ssl-type"]:checked').value;let G="";if(SITE.routingMode==="subdirectory"&&SITE.domain)G=`https://${SITE.domain}/${j}`;else if(q==="private")G=`${W==="none"?"http":"https"}://${buildDomain(j)}`;else if(q==="public"){const ee=W==="none"?"http":"https",Z=SITE.domain||j;G=SITE.domain?`${ee}://${j}.${SITE.domain}`:`${ee}://${j}`}else{const ee=o.value||e.defaultPort||DC.DEFAULTS.SERVICE_PORT;G=`http://${b.value}:${ee}`}p.textContent=G}r.oninput=ae,b.oninput=ae,o.oninput=ae,document.querySelectorAll('input[name="dns-type"]').forEach(j=>{j.onchange=ae}),document.querySelectorAll('input[name="ssl-type"]').forEach(j=>{j.onchange=ae}),ae(),k.classList.remove("show"),t.classList.add("show"),t.dataset.appTemplate=JSON.stringify(e)}async function y(e){const t=e.appTemplate,s=safeGetJSON(f,[]),r=t._useExisting&&t._existingContainer,p=s.find(b=>b.id===e.subdomain);if(!(p&&!r&&!confirm(`An app with subdomain "${e.subdomain}" already exists. Redeploy?`))){if(p){const b=s.indexOf(p);s.splice(b,1),safeSet(f,JSON.stringify(s))}if(r)e.port=t._existingContainer.primaryPort;else{const b=e.port||t.defaultPort||8080;showNotification(`Checking port ${b} availability...`,"info",0);const o=await B(b);if(!o.available){const i=await C(t.defaultPort||8080);if(confirm(`Port ${b} is already in use by ${o.conflict?.usedBy||"another container"}.
+ ${J.self?.hostname} (${J.self?.ip})
+ | ${J.deviceCount} devices
+ `:J.installed?a.innerHTML='Not connected':(a.innerHTML='Not available',e.disabled=!0)}catch{a.innerHTML='Could not check status'}function oe(){const q=r.value||"subdomain",J=document.querySelector('input[name="dns-type"]:checked').value,V=document.querySelector('input[name="ssl-type"]:checked').value;let K="";if(SITE.routingMode==="subdirectory"&&SITE.domain)K=`https://${SITE.domain}/${q}`;else if(J==="private")K=`${V==="none"?"http":"https"}://${buildDomain(q)}`;else if(J==="public"){const te=V==="none"?"http":"https",ee=SITE.domain||q;K=SITE.domain?`${te}://${q}.${SITE.domain}`:`${te}://${q}`}else{const te=n.value||o.defaultPort||DC.DEFAULTS.SERVICE_PORT;K=`http://${m.value}:${te}`}p.textContent=K}r.oninput=oe,m.oninput=oe,n.oninput=oe,document.querySelectorAll('input[name="dns-type"]').forEach(q=>{q.onchange=oe}),document.querySelectorAll('input[name="ssl-type"]').forEach(q=>{q.onchange=oe}),oe(),I.classList.remove("show"),l.classList.add("show"),l.dataset.appTemplate=JSON.stringify(o)}async function x(o){const l=o.appTemplate,v=safeGetJSON(w,[]),r=l._useExisting&&l._existingContainer,p=v.find(m=>m.id===o.subdomain);if(!(p&&!r&&!confirm(`An app with subdomain "${o.subdomain}" already exists. Redeploy?`))){if(p){const m=v.indexOf(p);v.splice(m,1),safeSet(w,JSON.stringify(v))}if(r)o.port=l._existingContainer.primaryPort;else{const m=o.port||l.defaultPort||8080;showNotification(`Checking port ${m} availability...`,"info",0);const n=await $(m);if(!n.available){const e=await L(l.defaultPort||8080);if(confirm(`Port ${m} is already in use by ${n.conflict?.usedBy||"another container"}.
-Would you like to use port ${i} instead?`))e.port=i;else{showNotification("Deployment cancelled - port conflict","error",5e3);return}}}showNotification(r?`Configuring ${t.name} with existing container...`:`Deploying ${t.name}...`,"info",0);try{const b={appId:t.id,config:{subdomain:e.subdomain,ip:e.ip,createDns:e.dnsType==="private",port:e.port||t.defaultPort||null,sslType:e.sslType,dnsType:e.dnsType,tailscaleOnly:e.tailscaleOnly||!1,mediaPath:e.mediaPath||null,plexClaimToken:e.plexClaimToken||null,customVolumes:e.customVolumes||null}};r&&(b.config.useExisting=!0,b.config.existingContainerId=t._existingContainer.id,b.config.existingPort=t._existingContainer.primaryPort,!e.port&&t._existingContainer.primaryPort&&(b.config.port=t._existingContainer.primaryPort));const i=await(await secureFetch("/api/v1/apps/deploy",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(b)})).json();if(i.success){const v={id:e.subdomain,name:t.name,logo:`/assets/${t.id}.png`,containerId:i.containerId,url:i.url,ip:e.ip,appTemplate:t.id,tailscaleOnly:e.tailscaleOnly||!1};s.push(v),safeSet(f,JSON.stringify(s)),window.APPS&&!window.APPS.some(w=>w.id===t.id)&&(window.APPS.push(v),typeof window.buildGrid=="function"&&window.buildGrid(),typeof window.refreshAll=="function"&&setTimeout(()=>window.refreshAll(),500));let d=i.usedExisting?`${t.name} configured with existing container!
-URL: ${i.url}`:`${t.name} deployed successfully!
-URL: ${i.url}`;i.warning&&(d+=`
+Would you like to use port ${e} instead?`))o.port=e;else{showNotification("Deployment cancelled - port conflict","error",5e3);return}}}showNotification(r?`Configuring ${l.name} with existing container...`:`Deploying ${l.name}...`,"info",0);try{const m={appId:l.id,config:{subdomain:o.subdomain,ip:o.ip,createDns:o.dnsType==="private",port:o.port||l.defaultPort||null,sslType:o.sslType,dnsType:o.dnsType,tailscaleOnly:o.tailscaleOnly||!1,mediaPath:o.mediaPath||null,plexClaimToken:o.plexClaimToken||null,customVolumes:o.customVolumes||null}};r&&(m.config.useExisting=!0,m.config.existingContainerId=l._existingContainer.id,m.config.existingPort=l._existingContainer.primaryPort,!o.port&&l._existingContainer.primaryPort&&(m.config.port=l._existingContainer.primaryPort));const e=await(await secureFetch("/api/v1/apps/deploy",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(m)})).json();if(e.success){const a={id:o.subdomain,name:l.name,logo:`/assets/${l.id}.png`,containerId:e.containerId,url:e.url,ip:o.ip,appTemplate:l.id,tailscaleOnly:o.tailscaleOnly||!1};v.push(a),safeSet(w,JSON.stringify(v)),window.APPS&&!window.APPS.some(s=>s.id===l.id)&&(window.APPS.push(a),typeof window.buildGrid=="function"&&window.buildGrid(),typeof window.refreshAll=="function"&&setTimeout(()=>window.refreshAll(),500));let t=e.usedExisting?`${l.name} configured with existing container!
+URL: ${e.url}`:`${l.name} deployed successfully!
+URL: ${e.url}`;e.warning&&(t+=`
-\u26A0 Warning: ${i.warning}`),showNotification(d,"success",8e3),delete t._useExisting,delete t._existingContainer,i.url&&i.url.startsWith("https://")&&x(i.url,t.name),i.setupInstructions&&i.setupInstructions.length>0&&setTimeout(()=>{const w=i.setupInstructions.join(`
-`);showNotification(`Setup Instructions for ${t.name}: ${w}`,"info",1e4)},1e3)}else throw new Error(i.error||"Deployment failed")}catch(b){console.error("Deployment error:",b),showNotification(`Failed to deploy ${t.name}: ${b.message}`,"error",8e3)}}}async function x(e,t){showNotification(`\u23F3 Generating SSL certificate for ${t}...`,"warning",6e4);let s=0;const r=12,p=async()=>{s++;try{const b=await fetch(e,{method:"HEAD",mode:"no-cors"});return showNotification(`\u2705 ${t} is ready! SSL certificate generated.`,"success",5e3),!0}catch{return s{window.APPS.some(s=>s.id===t.id)||window.APPS.push(t)})}document.getElementById("add-service-btn")?.addEventListener("click",()=>{m(),k.classList.add("show")}),wireModal(k,document.getElementById("app-selector-cancel"));const A=document.getElementById("app-deploy-modal");document.getElementById("app-deploy-cancel")?.addEventListener("click",()=>{A.classList.remove("show")}),document.getElementById("app-deploy-confirm")?.addEventListener("click",()=>{const e=JSON.parse(A.dataset.appTemplate),t=document.getElementById("deploy-media-path").value.trim(),s=[];document.querySelectorAll("#volume-mounts-list .vol-host-path").forEach(p=>{s.push({hostPath:p.value.trim(),containerPath:p.dataset.containerPath})});const r={appTemplate:e,subdomain:document.getElementById("deploy-subdomain").value.trim(),dnsType:document.querySelector('input[name="dns-type"]:checked').value,sslType:document.querySelector('input[name="ssl-type"]:checked').value,ip:document.getElementById("deploy-ip").value.trim(),port:document.getElementById("deploy-port").value.trim(),tailscaleOnly:document.getElementById("deploy-tailscale-only").checked,mediaPath:t||null,plexClaimToken:document.getElementById("deploy-plex-claim")?.value.trim()||null,customVolumes:s.length>0?s:null,resources:{cpus:parseFloat(document.getElementById("deploy-cpu-limit").value)||0,memory:parseFloat(document.getElementById("deploy-memory-limit").value)||0}};if(!r.subdomain){showNotification("Please enter a subdomain or domain name","warning");return}if(e.mediaMount?.required&&!t){showNotification("Please enter a media library path for this application","warning");return}A.classList.remove("show"),y(r)}),wireModal(A);const N=document.getElementById("folder-browser-modal"),z=document.getElementById("folder-browser-path"),H=document.getElementById("folder-browser-list"),L=document.getElementById("folder-browser-selected"),g=document.getElementById("folder-browser-selected-list");let I="",c=[],l=null;window.openFolderBrowser=function(e){l=e,c=e.value.split(",").map(t=>t.trim()).filter(t=>t),I="",n(),a(""),N.classList.add("show")};async function a(e){z.textContent=e||"Select a drive...",H.innerHTML='
`;return}I=s.path||"",z.textContent=I||"Select a drive...";let r="";s.parent&&s.parent!==s.path&&(r+=`
+\u26A0 Warning: ${e.warning}`),showNotification(t,"success",8e3),delete l._useExisting,delete l._existingContainer,e.url&&e.url.startsWith("https://")&&E(e.url,l.name),e.setupInstructions&&e.setupInstructions.length>0&&setTimeout(()=>{const s=e.setupInstructions.join(`
+`);showNotification(`Setup Instructions for ${l.name}: ${s}`,"info",1e4)},1e3)}else throw new Error(e.error||"Deployment failed")}catch(m){console.error("Deployment error:",m),showNotification(`Failed to deploy ${l.name}: ${m.message}`,"error",8e3)}}}async function E(o,l){showNotification(`\u23F3 Generating SSL certificate for ${l}...`,"warning",6e4);let v=0;const r=12,p=async()=>{v++;try{const m=await fetch(o,{method:"HEAD",mode:"no-cors"});return showNotification(`\u2705 ${l} is ready! SSL certificate generated.`,"success",5e3),!0}catch{return v{window.APPS.some(v=>v.id===l.id)||window.APPS.push(l)})}document.getElementById("add-service-btn")?.addEventListener("click",()=>{g(),I.classList.add("show")}),wireModal(I,document.getElementById("app-selector-cancel"));const O=document.getElementById("app-deploy-modal");document.getElementById("app-deploy-cancel")?.addEventListener("click",()=>{O.classList.remove("show")}),document.getElementById("app-deploy-confirm")?.addEventListener("click",()=>{const o=JSON.parse(O.dataset.appTemplate),l=document.getElementById("deploy-media-path").value.trim(),v=[];document.querySelectorAll("#volume-mounts-list .vol-host-path").forEach(p=>{v.push({hostPath:p.value.trim(),containerPath:p.dataset.containerPath})});const r={appTemplate:o,subdomain:document.getElementById("deploy-subdomain").value.trim(),dnsType:document.querySelector('input[name="dns-type"]:checked').value,sslType:document.querySelector('input[name="ssl-type"]:checked').value,ip:document.getElementById("deploy-ip").value.trim(),port:document.getElementById("deploy-port").value.trim(),tailscaleOnly:document.getElementById("deploy-tailscale-only").checked,mediaPath:l||null,plexClaimToken:document.getElementById("deploy-plex-claim")?.value.trim()||null,customVolumes:v.length>0?v:null,resources:{cpus:parseFloat(document.getElementById("deploy-cpu-limit").value)||0,memory:parseFloat(document.getElementById("deploy-memory-limit").value)||0}};if(!r.subdomain){showNotification("Please enter a subdomain or domain name","warning");return}if(o.mediaMount?.required&&!l){showNotification("Please enter a media library path for this application","warning");return}O.classList.remove("show"),x(r)}),wireModal(O);const D=document.getElementById("folder-browser-modal"),N=document.getElementById("folder-browser-path"),M=document.getElementById("folder-browser-list"),H=document.getElementById("folder-browser-selected"),S=document.getElementById("folder-browser-selected-list");let T="",b=[],u=null;window.openFolderBrowser=function(o){u=o,b=o.value.split(",").map(l=>l.trim()).filter(l=>l),T="",c(),y(""),D.classList.add("show")};async function y(o){N.textContent=o||"Select a drive...",M.innerHTML='
`}}function S(b,u,y){const c=document.getElementById(`recipe-progress-${b}`);if(!c)return;const o=c.querySelector(".recipe-progress-icon"),l=c.querySelector(".recipe-progress-status");u==="deploying"?(o.textContent="\u23F3",l.style.color="var(--accent)"):u==="success"?(o.textContent="\u2705",l.style.color="var(--ok-fg)"):u==="error"&&(o.textContent="\u274C",l.style.color="var(--bad-fg)"),l.textContent=y}g.addEventListener("click",()=>{if(I===3){I=4,R(),H();return}I<3&&(I++,R(),I===2&&D(),I===3&&N())}),L.addEventListener("click",()=>{I>1&&I<4&&(I--,R())}),window.groupRecipeCards=function(){const b=document.querySelectorAll(".service-card[data-recipe-id]");if(b.length===0)return;const u={};b.forEach(y=>{const c=y.dataset.recipeId;u[c]||(u[c]=[]),u[c].push(y)});for(const[y,c]of Object.entries(u))c.length<2||c.forEach((o,l)=>{if(o.style.borderLeft="3px solid rgba(142,68,173,0.5)",l===0){let v=o.querySelector(".recipe-group-label");v||(v=document.createElement("div"),v.className="recipe-group-label",v.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;",v.textContent=y.replace(/-/g," "),o.style.position="relative",o.appendChild(v))}})},window.manageRecipe=async function(b,u){const y=`/api/v1/recipes/${b}/${u}`,c=u==="remove"?"DELETE":"POST",o=u==="remove"?`/api/v1/recipes/${b}`:y;if(!(u==="remove"&&!confirm(`Remove the entire ${b} recipe? This will delete all containers and configuration.`)))try{const v=await(await secureFetch(o,{method:c})).json();v.success?(showNotification(`Recipe ${u}: ${v.results?.filter(r=>r.status!=="failed").length||0} components processed`,"success",4e3),window.loadServices&&window.loadServices()):showNotification(`Recipe ${u} failed: ${v.error}`,"error",5e3)}catch(l){showNotification(`Network error: ${l.message}`,"error",5e3)}};const T=document.createElement("style");T.textContent=`
.recipe-step {
flex: 1;
text-align: center;
@@ -550,16 +550,16 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};
.recipe-step-panel {
min-height: 180px;
}
- `,document.head.appendChild(I),u()})(),(function(){document.getElementById("reload-caddy-top")?.addEventListener("click",async()=>{const f=document.getElementById("reload-caddy-top"),E=f.textContent;try{f.textContent="\u23F3 Reloading...",f.disabled=!0;const M=await secureFetch("/api/v1/caddy/reload",{method:"POST",headers:{"Content-Type":"application/json"}}),k=await M.json();if(M.ok&&k.success)f.textContent="\u2705 Reloaded!",setTimeout(()=>{f.textContent=E,f.disabled=!1},2e3);else throw new Error(k.error||"Reload failed")}catch(M){f.textContent="\u274C Failed",showNotification(`Failed to reload Caddy: ${M.message}`,"error"),setTimeout(()=>{f.textContent=E,f.disabled=!1},2e3)}})})(),(function(){injectModal("error-log-modal",'
\u{1F4CB} Error Logs
Loading error logs...
');const f=document.getElementById("error-log-modal"),E=document.getElementById("error-log-content"),M=document.getElementById("view-error-logs"),k=document.getElementById("error-log-refresh"),S=document.getElementById("error-log-clear"),D=document.getElementById("error-log-close");async function B(){E.innerHTML='
\u2705 No errors logged! Everything is working smoothly.
':E.innerHTML=h.logs.map(u=>`
+ `,document.head.appendChild(T),f()})(),(function(){document.getElementById("reload-caddy-top")?.addEventListener("click",async()=>{const w=document.getElementById("reload-caddy-top"),B=w.textContent;try{w.textContent="\u23F3 Reloading...",w.disabled=!0;const z=await secureFetch("/api/v1/caddy/reload",{method:"POST",headers:{"Content-Type":"application/json"}}),I=await z.json();if(z.ok&&I.success)w.textContent="\u2705 Reloaded!",setTimeout(()=>{w.textContent=B,w.disabled=!1},2e3);else throw new Error(I.error||"Reload failed")}catch(z){w.textContent="\u274C Failed",showNotification(`Failed to reload Caddy: ${z.message}`,"error"),setTimeout(()=>{w.textContent=B,w.disabled=!1},2e3)}})})(),(function(){injectModal("error-log-modal",'
\u{1F4CB} Error Logs
Loading error logs...
');const w=document.getElementById("error-log-modal"),B=document.getElementById("error-log-content"),z=document.getElementById("view-error-logs"),I=document.getElementById("error-log-refresh"),C=document.getElementById("error-log-clear"),P=document.getElementById("error-log-close");async function $(){B.innerHTML='
`,m.style.display="block"}}function I(n){let e="";const t=n.services,s=["radarr","sonarr","prowlarr"];for(const b of s){const o=t[b];if(!o||o.status==="not_found"&&!o.url)continue;const i=N[b],v=z[b],d=o.status==="connected";e+=`
`,g.style.display="block"}}function T(c){let o="";const l=c.services,v=["radarr","sonarr","prowlarr"];for(const m of v){const n=l[m];if(!n||n.status==="not_found"&&!n.url)continue;const e=D[m],a=N[m],t=n.status==="connected";o+=`
`}h.innerHTML=e}window.smartTestConnection=async function(n){const e=document.getElementById(`smart-${n}-url`),t=document.getElementById(`smart-${n}-key`),s=document.getElementById(`smart-${n}-status`),r=e?.value.trim(),p=t?.value.trim();if(!r||!p){s.innerHTML='Enter URL and API key';return}s.innerHTML='';try{const o=await(await secureFetch("/api/v1/arr/test-connection",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({service:n,url:r,apiKey:p})})).json();o.success?s.innerHTML=`✓ ${escapeHtml(o.appName||"Connected")} v${escapeHtml(o.version||"")}`:s.innerHTML=`✗ ${escapeHtml(o.error)}`}catch(b){s.innerHTML=`✗ ${escapeHtml(b.message)}`}};async function c(){H("progress"),u.innerHTML='
Connecting services...
';const n={};for(const t of["radarr","sonarr","prowlarr"]){const s=document.getElementById(`smart-${t}-url`)?.value.trim(),r=document.getElementById(`smart-${t}-key`)?.value.trim();r&&s?n[t]={apiKey:r,url:s}:r&&(n[t]={apiKey:r})}const e={services:Object.keys(n).length>0?n: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 s=await(await secureFetch("/api/v1/arr/smart-connect",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)})).json();let r="";for(const p of s.steps||[]){const b=p.status==="success"?'✓':'✗',o=p.status==="success"?"var(--muted)":"var(--bad-fg)";r+=`
- ${b}
+ ${n}
+
`}k.innerHTML=o}window.smartTestConnection=async function(c){const o=document.getElementById(`smart-${c}-url`),l=document.getElementById(`smart-${c}-key`),v=document.getElementById(`smart-${c}-status`),r=o?.value.trim(),p=l?.value.trim();if(!r||!p){v.innerHTML='Enter URL and API key';return}v.innerHTML='';try{const n=await(await secureFetch("/api/v1/arr/test-connection",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({service:c,url:r,apiKey:p})})).json();n.success?v.innerHTML=`✓ ${escapeHtml(n.appName||"Connected")} v${escapeHtml(n.version||"")}`:v.innerHTML=`✗ ${escapeHtml(n.error)}`}catch(m){v.innerHTML=`✗ ${escapeHtml(m.message)}`}};async function b(){M("progress"),f.innerHTML='
Connecting services...
';const c={};for(const l of["radarr","sonarr","prowlarr"]){const v=document.getElementById(`smart-${l}-url`)?.value.trim(),r=document.getElementById(`smart-${l}-key`)?.value.trim();r&&v?c[l]={apiKey:r,url:v}:r&&(c[l]={apiKey:r})}const o={services:Object.keys(c).length>0?c:void 0,configurePlex:document.getElementById("smart-opt-plex")?.checked,configureProwlarr:document.getElementById("smart-opt-prowlarr")?.checked,configureSeerr:document.getElementById("smart-opt-seerr")?.checked,saveCredentials:document.getElementById("smart-opt-save")?.checked};try{const v=await(await secureFetch("/api/v1/arr/smart-connect",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)})).json();let r="";for(const p of v.steps||[]){const m=p.status==="success"?'✓':'✗',n=p.status==="success"?"var(--muted)":"var(--bad-fg)";r+=`
"}}}window.__restoreServerBackup=async function(a){if(confirm("Restore from this server backup? This will overwrite current configuration."))try{var n=await secureFetch("/api/v1/backups/restore/"+a,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({restoreServices:!0,restoreConfig:!0})}),e=await n.json();e.success?(showNotification("Restore completed successfully!","success"),location.reload()):showNotification("Restore failed: "+(e.error||"Unknown error"),"error")}catch(t){showNotification("Restore error: "+t.message,"error")}},document.querySelector('[data-panel="backup-automated"]')?.addEventListener("click",g),document.querySelector('[data-panel="backup-history-tab"]')?.addEventListener("click",l)})(),(function(){injectModal("stats-modal",`
"}}}window.__restoreServerBackup=async function(e){if(confirm("Restore from this server backup? This will overwrite current configuration."))try{var a=await secureFetch("/api/v1/backups/restore/"+e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({restoreServices:!0,restoreConfig:!0})}),t=await a.json();t.success?(showNotification("Restore completed successfully!","success"),location.reload()):showNotification("Restore failed: "+(t.error||"Unknown error"),"error")}catch(s){showNotification("Restore error: "+s.message,"error")}},document.querySelector('[data-panel="backup-automated"]')?.addEventListener("click",T),document.querySelector('[data-panel="backup-history-tab"]')?.addEventListener("click",n)})(),(function(){injectModal("stats-modal",`
\u{1F4CA} Resource Monitor
@@ -987,6 +987,7 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};
+
@@ -1009,6 +1010,27 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ \u{1F4CA}
+ Choose a container and time range to view history.
+
+
+
+
@@ -1034,80 +1056,94 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};
- `);const f=document.getElementById("stats-modal"),E=document.getElementById("container-stats-btn"),M=document.getElementById("stats-cancel"),k=document.getElementById("stats-refresh-btn"),S=document.getElementById("stats-auto-refresh"),D=document.getElementById("stats-container"),B=document.getElementById("stats-aggregated-container"),C=document.getElementById("stats-alerts-container"),m=document.getElementById("stats-last-update");let h=null,u=null;function y(g){if(g===0||!g)return"0 B";const I=1024,c=["B","KB","MB","GB"],l=Math.floor(Math.log(g)/Math.log(I));return parseFloat((g/Math.pow(I,l)).toFixed(1))+" "+c[l]}function x(g){return g<30?"#2ecc71":g<70?"#f39c12":"#e74c3c"}function O(g){return g<50?"#2ecc71":g<80?"#f39c12":"#e74c3c"}async function A(){try{let g=null,I=!1;try{const a=await(await fetch("/api/v1/monitoring/stats")).json();a.success&&a.stats&&(g=a.stats,I=!0,u=a.stats)}catch{}if(!I){const a=await(await fetch("/api/v1/stats/containers")).json();if(a.success&&a.stats){g={};for(const n of a.stats)g[n.name]={name:n.name,current:{cpu:n.cpu,memory:{percent:n.memory.percent,usage:n.memory.used,limit:n.memory.limit,usageMB:Math.round(n.memory.used/1048576),limitMB:Math.round(n.memory.limit/1048576)},network:{rxBytes:n.network.rx,txBytes:n.network.tx,rxMB:(n.network.rx/1048576).toFixed(1),txMB:(n.network.tx/1048576).toFixed(1)},disk:{readMB:0,writeMB:0}},status:n.status};u=g}}if(!g||Object.keys(g).length===0){D.innerHTML='
';try{const e=await(await fetch(`/api/v1/monitoring/history/${encodeURIComponent(r)}?startTime=${m}&endTime=${p}`)).json();if(!e.success)throw new Error(e.error||"Failed to load history");const a=e.samples||[],t=e.tier||"raw";if(a.length===0){T.innerHTML=`
\u{1F4CA}No data for the last ${u}. Tier: ${c(t)}.
\u{1F3E5}No health checks configured. Go to the Configure tab to add services.
';return}const a=Object.values(l.status);let n='
';n+='
',n+='
Service
Status
',n+='
Uptime 24h
Uptime 7d
',n+='
Avg Response
Last Check
';for(const e of a){const t=e.status==="up",s=t?"var(--dot-ok)":"var(--dot-bad)",r=e.uptime?.["24h"]??"-",p=e.uptime?.["7d"]??"-",b=e.avgResponseTime!=null?Math.round(e.avgResponseTime)+"ms":"-",o=e.timestamp?timeAgo(e.timestamp):"-";n+=`
\u{1F3E5}No health checks configured. Go to the Configure tab to add services.
';return}const y=Object.values(u.status);let c='
';c+='
',c+='
Service
Status
',c+='
Uptime 24h
Uptime 7d
',c+='
Avg Response
Last Check
';for(const o of y){const l=o.status==="up",v=l?"var(--dot-ok)":"var(--dot-bad)",r=o.uptime?.["24h"]??"-",p=o.uptime?.["7d"]??"-",m=o.avgResponseTime!=null?Math.round(o.avgResponseTime)+"ms":"-",n=o.timestamp?timeAgo(o.timestamp):"-";c+=`
';for(const r of s){const p=r.status==="resolved",b=p&&r.duration?r.duration<6e4?Math.round(r.duration/1e3)+"s":Math.round(r.duration/6e4)+"m":"-";e+='
',e+=`
${escapeHtml(r.serviceId)}
`,e+=`
${escapeHtml(r.type)}
`,e+=`
${N(r.severity)}
`,e+=`
${r.status}
`,e+=`
${b}
`,e+=`
${timeAgo(r.createdAt)}
`,e+="
"}e+="
"}D.innerHTML=e||'
\u{1F6A8}No incidents recorded yet.
'}catch(c){D.innerHTML=`
Failed: ${escapeHtml(c.message)}
`}}async function L(){try{const l=await(await fetch("/api/v1/health-checks/status")).json(),a=l.success&&l.status?Object.values(l.status):[];if(a.length===0){B.innerHTML='
\u2699\uFE0FNo health checks configured yet. Click "Add Health Check" below.
';return}let n='
';n+='
Service
Status
SLA Target
Actions
';for(const e of a){const t=e.status==="up";n+='
',n+=`
${escapeHtml(e.name||e.serviceId)}
`,n+=`
${t?"Up":"Down"}
`,n+=`
${e.sla?.target?e.sla.target+"%":"-"}
`,n+='
',n+=``,n+=``,n+="
"}n+="
",B.innerHTML=n}catch(c){B.innerHTML=`
Failed: ${escapeHtml(c.message)}
`}}function g(c,l,a,n,e,t,s){O=c||null,u.textContent=c?"Edit Health Check":"Add Health Check",document.getElementById("health-form-id").value=c||"",document.getElementById("health-form-id").disabled=!!c,document.getElementById("health-form-name").value=l||"",document.getElementById("health-form-url").value=a||"",document.getElementById("health-form-timeout").value=n||1e4,document.getElementById("health-form-codes").value=e||"200",document.getElementById("health-form-sla").value=t||99.9,document.getElementById("health-form-slow").value=s||5e3,h.style.display="",m.style.display="none"}function I(){h.style.display="none",m.style.display="",O=null}m?.addEventListener("click",()=>g("","","",1e4,"200",99.9,5e3)),y?.addEventListener("click",I),x?.addEventListener("click",async()=>{const c=O||document.getElementById("health-form-id").value.trim();if(!c)return showNotification("Service ID is required","warning");const l=document.getElementById("health-form-url").value.trim();if(!l)return showNotification("URL is required","warning");const a=document.getElementById("health-form-codes").value.split(",").map(e=>parseInt(e.trim())).filter(Boolean),n={name:document.getElementById("health-form-name").value.trim()||c,url:l,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{x.textContent="Saving...",x.disabled=!0;const t=await(await secureFetch(`/api/v1/health-checks/${encodeURIComponent(c)}/configure`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})).json();if(!t.success)throw new Error(t.error||"Save failed");I(),L(),z()}catch(e){showNotification("Error: "+e.message,"error")}finally{x.textContent="Save",x.disabled=!1}}),document.addEventListener("health-edit",async c=>{const l=c.detail;g(l,"","",1e4,"200",99.9,5e3)}),document.addEventListener("health-delete",async c=>{const l=c.detail;if(confirm(`Delete health check for "${l}"?`))try{const n=await(await secureFetch(`/api/v1/health-checks/${encodeURIComponent(l)}/configure`,{method:"DELETE"})).json();if(!n.success)throw new Error(n.error);L(),z()}catch(a){showNotification("Error: "+a.message,"error")}}),E?.addEventListener("click",()=>{f?.classList.add("show"),z()}),wireModal(f,M),k?.addEventListener("click",z),document.querySelector('[data-panel="health-incidents"]')?.addEventListener("click",H),document.querySelector('[data-panel="health-config"]')?.addEventListener("click",L)})(),(function(){injectModal("updates-modal",`
';for(const r of v){const p=r.status==="resolved",m=p&&r.duration?r.duration<6e4?Math.round(r.duration/1e3)+"s":Math.round(r.duration/6e4)+"m":"-";o+='
',o+=`
${escapeHtml(r.serviceId)}
`,o+=`
${escapeHtml(r.type)}
`,o+=`
${D(r.severity)}
`,o+=`
${r.status}
`,o+=`
${m}
`,o+=`
${timeAgo(r.createdAt)}
`,o+="
"}o+="
"}P.innerHTML=o||'
\u{1F6A8}No incidents recorded yet.
'}catch(b){P.innerHTML=`
Failed: ${escapeHtml(b.message)}
`}}async function H(){try{const u=await(await fetch("/api/v1/health-checks/status")).json(),y=u.success&&u.status?Object.values(u.status):[];if(y.length===0){$.innerHTML='
\u2699\uFE0FNo health checks configured yet. Click "Add Health Check" below.
';return}let c='
';c+='
Service
Status
SLA Target
Actions
';for(const o of y){const l=o.status==="up";c+='
',c+=`
${escapeHtml(o.name||o.serviceId)}
`,c+=`
${l?"Up":"Down"}
`,c+=`
${o.sla?.target?o.sla.target+"%":"-"}
`,c+='
',c+=``,c+=``,c+="
"}c+="
",$.innerHTML=c}catch(b){$.innerHTML=`
Failed: ${escapeHtml(b.message)}
`}}function S(b,u,y,c,o,l,v){R=b||null,f.textContent=b?"Edit Health Check":"Add Health Check",document.getElementById("health-form-id").value=b||"",document.getElementById("health-form-id").disabled=!!b,document.getElementById("health-form-name").value=u||"",document.getElementById("health-form-url").value=y||"",document.getElementById("health-form-timeout").value=c||1e4,document.getElementById("health-form-codes").value=o||"200",document.getElementById("health-form-sla").value=l||99.9,document.getElementById("health-form-slow").value=v||5e3,k.style.display="",g.style.display="none"}function T(){k.style.display="none",g.style.display="",R=null}g?.addEventListener("click",()=>S("","","",1e4,"200",99.9,5e3)),x?.addEventListener("click",T),E?.addEventListener("click",async()=>{const b=R||document.getElementById("health-form-id").value.trim();if(!b)return showNotification("Service ID is required","warning");const u=document.getElementById("health-form-url").value.trim();if(!u)return showNotification("URL is required","warning");const y=document.getElementById("health-form-codes").value.split(",").map(o=>parseInt(o.trim())).filter(Boolean),c={name:document.getElementById("health-form-name").value.trim()||b,url:u,timeout:parseInt(document.getElementById("health-form-timeout").value)||1e4,expectedStatusCodes:y.length?y:[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 l=await(await secureFetch(`/api/v1/health-checks/${encodeURIComponent(b)}/configure`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(c)})).json();if(!l.success)throw new Error(l.error||"Save failed");T(),H(),N()}catch(o){showNotification("Error: "+o.message,"error")}finally{E.textContent="Save",E.disabled=!1}}),document.addEventListener("health-edit",async b=>{const u=b.detail;S(u,"","",1e4,"200",99.9,5e3)}),document.addEventListener("health-delete",async b=>{const u=b.detail;if(confirm(`Delete health check for "${u}"?`))try{const c=await(await secureFetch(`/api/v1/health-checks/${encodeURIComponent(u)}/configure`,{method:"DELETE"})).json();if(!c.success)throw new Error(c.error);H(),N()}catch(y){showNotification("Error: "+y.message,"error")}}),B?.addEventListener("click",()=>{w?.classList.add("show"),N()}),wireModal(w,z),I?.addEventListener("click",N),document.querySelector('[data-panel="health-incidents"]')?.addEventListener("click",M),document.querySelector('[data-panel="health-config"]')?.addEventListener("click",H)})(),(function(){injectModal("updates-modal",`
\u2B06\uFE0F Update Management
@@ -1282,17 +1318,17 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};
- `);const f=document.getElementById("updates-modal"),E=document.getElementById("updates-btn"),M=document.getElementById("updates-cancel"),k=document.getElementById("updates-check-btn"),S=document.getElementById("updates-available-container"),D=document.getElementById("updates-history-container"),B=document.getElementById("updates-auto-container"),C=document.getElementById("updates-last-check");async function m(){try{const b=await(await fetch("/api/v1/updates/available")).json();if(!b.success)throw new Error(b.error);const o=b.updates||[];if(o.length===0){S.innerHTML='
\u2705All containers are up to date.
',C.textContent="";return}let i='
';i+='
Container
Image
Current
Latest
Actions
';for(const v of o)i+='
',i+=`
${escapeHtml(v.containerName)}
`,i+=`
${escapeHtml(v.imageName)}
`,i+=`
${escapeHtml(v.currentDigest)}
`,i+=`
${escapeHtml(v.latestDigest)}
`,i+='
',i+=``,i+=``,i+="
";i+="
",S.innerHTML=i,C.textContent=o.length+" update(s) available",S.querySelectorAll(".update-now-btn").forEach(v=>{v.addEventListener("click",async()=>{const d=v.dataset.id,w=v.dataset.name;if(confirm(`Update "${w}" to the latest version? The container will restart.`)){v.textContent="Updating...",v.disabled=!0;try{const $=await(await secureFetch(`/api/v1/updates/update/${encodeURIComponent(d)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({autoRollback:!0})})).json();if($.success)v.textContent="Done!",v.style.background="var(--ok-fg)",setTimeout(()=>m(),2e3);else throw new Error($.error||"Update failed")}catch(T){v.textContent="Failed",v.style.color="var(--bad-fg)",showNotification("Update error: "+T.message,"error"),setTimeout(()=>{v.textContent="Update",v.disabled=!1,v.style.color="",v.style.background=""},3e3)}}})}),S.querySelectorAll(".rollback-btn").forEach(v=>{v.addEventListener("click",async()=>{const d=v.dataset.id,w=v.dataset.name;if(confirm(`Rollback "${w}" to its previous version?`)){v.textContent="Rolling back...",v.disabled=!0;try{const $=await(await secureFetch(`/api/v1/updates/rollback/${encodeURIComponent(d)}`,{method:"POST"})).json();if($.success)v.textContent="Rolled back!",setTimeout(()=>m(),2e3);else throw new Error($.error||"Rollback failed")}catch(T){v.textContent="Failed",showNotification("Rollback error: "+T.message,"error"),setTimeout(()=>{v.textContent="Rollback",v.disabled=!1},3e3)}}})})}catch(p){S.innerHTML=`
Failed: ${escapeHtml(p.message)}
`}}async function h(){k.textContent="\u{1F50D} Checking...",k.disabled=!0;try{const b=await(await secureFetch("/api/v1/updates/check",{method:"POST"})).json();if(!b.success)throw new Error(b.error);k.textContent="\u2705 Done!",await m()}catch(p){k.textContent="\u274C Failed",showNotification("Check error: "+p.message,"error")}setTimeout(()=>{k.textContent="\u{1F50D} Check for Updates",k.disabled=!1},3e3)}async function u(){try{D.innerHTML='
Auto-updates run during maintenance window (default 2AM-4AM). Daily = every day, Weekly = Sundays, Monthly = 1st of month.
';w+='
',w+='
Container
Schedule
Window
Rollback
Last Run
Actions
';for(const T of v){const $=T.name||T.Names?.[0]?.replace(/^\//,"")||T.Id?.substring(0,12),P=T.containerId||T.Id,R=d[P]||{},U=R.enabled?R.schedule||"weekly":"",_=R.autoRollback!==!1,J=R.maintenanceWindow||"",F=R.lastAutoUpdate?timeAgo(R.lastAutoUpdate):"Never";w+=`
`}}const x=document.getElementById("dashcaddy-current-version"),O=document.getElementById("dashcaddy-update-badge"),A=document.getElementById("dashcaddy-update-details"),N=document.getElementById("dashcaddy-new-version"),z=document.getElementById("dashcaddy-changelog"),H=document.getElementById("dashcaddy-apply-btn"),L=document.getElementById("dashcaddy-check-btn"),g=document.getElementById("dashcaddy-rollback-btn"),I=document.getElementById("dashcaddy-status-bar"),c=document.getElementById("dashcaddy-history-container");let l=null;function a(p,b){I&&(I.style.display="block",I.style.background=b==="error"?"var(--bad-bg)":b==="success"?"var(--ok-bg)":"var(--bg)",I.style.color=b==="error"?"var(--bad-fg)":b==="success"?"var(--ok-fg)":"var(--fg)",I.textContent=p)}async function n(){try{const b=await(await fetch("/api/v1/system/version")).json();b.success&&(x.textContent="v"+b.version+(b.commit?" ("+b.commit.substring(0,7)+")":""))}catch{x.textContent="Unable to fetch version"}}async function e(p){p||(L.textContent="Checking...",L.disabled=!0);try{const o=await(await fetch("/api/v1/system/update-check")).json();if(l=o,o.success&&o.available&&o.remote){O.style.display="",A.style.display="",N.textContent="v"+o.remote.version,z.textContent=o.remote.changelog||"No changelog available.";const i=document.getElementById("updates-btn");if(i&&!i.querySelector(".update-dot")){const d=document.createElement("span");d.className="update-dot",d.style.cssText="position:absolute;top:2px;right:2px;width:8px;height:8px;border-radius:50%;background:var(--accent);",i.style.position="relative",i.appendChild(d)}const v=document.getElementById("updates-dashcaddy-tab");if(v&&!v.querySelector(".update-dot")){const d=document.createElement("span");d.className="update-dot",d.style.cssText="display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--accent);margin-left:4px;vertical-align:middle;",v.appendChild(d)}}else O.style.display="none",A.style.display="none",await n(),p||a("You are running the latest version.","success");p||(L.textContent="Check for Updates",L.disabled=!1)}catch(b){p||(a("Failed to check: "+b.message,"error"),L.textContent="Check for Updates",L.disabled=!1)}}async function t(){if(confirm("Apply DashCaddy update? The API container will restart.")){H.textContent="Updating...",H.disabled=!0,a("Downloading and applying update...","info");try{const b=await(await secureFetch("/api/v1/system/update-apply",{method:"POST"})).json();if(b.success)a("Update initiated: v"+(b.fromVersion||"?")+" \u2192 v"+(b.toVersion||"?")+". The container will restart shortly.","success"),H.textContent="Applied!",document.querySelectorAll(".update-dot").forEach(o=>o.remove());else throw new Error(b.error||"Update failed")}catch(p){a("Update failed: "+p.message,"error"),H.textContent="Update Now",H.disabled=!1}}}async function s(){try{const b=await(await fetch("/api/v1/system/update-history")).json(),o=b.success&&b.history?b.history:[];if(o.length===0){c.innerHTML='
\u{1F4E6}No self-update history.
';return}let i='
';i+='
When
Version
From
Status
';for(const v of o){const d=v.status==="success"?"\u2713 success":v.status==="pending"?"\u23F3 pending":v.status==="partial"?"\u26A0 partial":"\u2717 "+v.status,w=v.status==="success"?"var(--ok-fg)":v.status==="pending"?"var(--muted)":"var(--bad-fg)";i+='
"}}async function r(){try{const b=await(await fetch("/api/v1/system/rollback-versions")).json(),o=b.success&&b.versions?b.versions:[];if(o.length===0){showNotification("No rollback versions available.","info");return}const i=prompt(`Available rollback versions:
-`+o.join(`
+ `);const w=document.getElementById("updates-modal"),B=document.getElementById("updates-btn"),z=document.getElementById("updates-cancel"),I=document.getElementById("updates-check-btn"),C=document.getElementById("updates-available-container"),P=document.getElementById("updates-history-container"),$=document.getElementById("updates-auto-container"),L=document.getElementById("updates-last-check");async function g(){try{const m=await(await fetch("/api/v1/updates/available")).json();if(!m.success)throw new Error(m.error);const n=m.updates||[];if(n.length===0){C.innerHTML='
\u2705All containers are up to date.
',L.textContent="";return}let e='
';e+='
Container
Image
Current
Latest
Actions
';for(const a of n)e+='
',e+=`
${escapeHtml(a.containerName)}
`,e+=`
${escapeHtml(a.imageName)}
`,e+=`
${escapeHtml(a.currentDigest)}
`,e+=`
${escapeHtml(a.latestDigest)}
`,e+='
',e+=``,e+=``,e+="
";e+="
",C.innerHTML=e,L.textContent=n.length+" update(s) available",C.querySelectorAll(".update-now-btn").forEach(a=>{a.addEventListener("click",async()=>{const t=a.dataset.id,s=a.dataset.name;if(confirm(`Update "${s}" to the latest version? The container will restart.`)){a.textContent="Updating...",a.disabled=!0;try{const h=await(await secureFetch(`/api/v1/updates/update/${encodeURIComponent(t)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({autoRollback:!0})})).json();if(h.success)a.textContent="Done!",a.style.background="var(--ok-fg)",setTimeout(()=>g(),2e3);else throw new Error(h.error||"Update failed")}catch(i){a.textContent="Failed",a.style.color="var(--bad-fg)",showNotification("Update error: "+i.message,"error"),setTimeout(()=>{a.textContent="Update",a.disabled=!1,a.style.color="",a.style.background=""},3e3)}}})}),C.querySelectorAll(".rollback-btn").forEach(a=>{a.addEventListener("click",async()=>{const t=a.dataset.id,s=a.dataset.name;if(confirm(`Rollback "${s}" to its previous version?`)){a.textContent="Rolling back...",a.disabled=!0;try{const h=await(await secureFetch(`/api/v1/updates/rollback/${encodeURIComponent(t)}`,{method:"POST"})).json();if(h.success)a.textContent="Rolled back!",setTimeout(()=>g(),2e3);else throw new Error(h.error||"Rollback failed")}catch(i){a.textContent="Failed",showNotification("Rollback error: "+i.message,"error"),setTimeout(()=>{a.textContent="Rollback",a.disabled=!1},3e3)}}})})}catch(p){C.innerHTML=`
Failed: ${escapeHtml(p.message)}
`}}async function k(){I.textContent="\u{1F50D} Checking...",I.disabled=!0;try{const m=await(await secureFetch("/api/v1/updates/check",{method:"POST"})).json();if(!m.success)throw new Error(m.error);I.textContent="\u2705 Done!",await g()}catch(p){I.textContent="\u274C Failed",showNotification("Check error: "+p.message,"error")}setTimeout(()=>{I.textContent="\u{1F50D} Check for Updates",I.disabled=!1},3e3)}async function f(){try{P.innerHTML='
Auto-updates run during maintenance window (default 2AM-4AM). Daily = every day, Weekly = Sundays, Monthly = 1st of month.
';s+='
',s+='
Container
Schedule
Window
Rollback
Last Run
Actions
';for(const i of a){const h=i.name||i.Names?.[0]?.replace(/^\//,"")||i.Id?.substring(0,12),d=i.containerId||i.Id,A=t[d]||{},F=A.enabled?A.schedule||"weekly":"",j=A.autoRollback!==!1,_=A.maintenanceWindow||"",U=A.lastAutoUpdate?timeAgo(A.lastAutoUpdate):"Never";s+=`
`}}const E=document.getElementById("dashcaddy-current-version"),R=document.getElementById("dashcaddy-update-badge"),O=document.getElementById("dashcaddy-update-details"),D=document.getElementById("dashcaddy-new-version"),N=document.getElementById("dashcaddy-changelog"),M=document.getElementById("dashcaddy-apply-btn"),H=document.getElementById("dashcaddy-check-btn"),S=document.getElementById("dashcaddy-rollback-btn"),T=document.getElementById("dashcaddy-status-bar"),b=document.getElementById("dashcaddy-history-container");let u=null;function y(p,m){T&&(T.style.display="block",T.style.background=m==="error"?"var(--bad-bg)":m==="success"?"var(--ok-bg)":"var(--bg)",T.style.color=m==="error"?"var(--bad-fg)":m==="success"?"var(--ok-fg)":"var(--fg)",T.textContent=p)}async function c(){try{const m=await(await fetch("/api/v1/system/version")).json();m.success&&(E.textContent="v"+m.version+(m.commit?" ("+m.commit.substring(0,7)+")":""))}catch{E.textContent="Unable to fetch version"}}async function o(p){p||(H.textContent="Checking...",H.disabled=!0);try{const n=await(await fetch("/api/v1/system/update-check")).json();if(u=n,n.success&&n.available&&n.remote){R.style.display="",O.style.display="",D.textContent="v"+n.remote.version,N.textContent=n.remote.changelog||"No changelog available.";const e=document.getElementById("updates-btn");if(e&&!e.querySelector(".update-dot")){const t=document.createElement("span");t.className="update-dot",t.style.cssText="position:absolute;top:2px;right:2px;width:8px;height:8px;border-radius:50%;background:var(--accent);",e.style.position="relative",e.appendChild(t)}const a=document.getElementById("updates-dashcaddy-tab");if(a&&!a.querySelector(".update-dot")){const t=document.createElement("span");t.className="update-dot",t.style.cssText="display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--accent);margin-left:4px;vertical-align:middle;",a.appendChild(t)}}else R.style.display="none",O.style.display="none",await c(),p||y("You are running the latest version.","success");p||(H.textContent="Check for Updates",H.disabled=!1)}catch(m){p||(y("Failed to check: "+m.message,"error"),H.textContent="Check for Updates",H.disabled=!1)}}async function l(){if(confirm("Apply DashCaddy update? The API container will restart.")){M.textContent="Updating...",M.disabled=!0,y("Downloading and applying update...","info");try{const m=await(await secureFetch("/api/v1/system/update-apply",{method:"POST"})).json();if(m.success)y("Update initiated: v"+(m.fromVersion||"?")+" \u2192 v"+(m.toVersion||"?")+". The container will restart shortly.","success"),M.textContent="Applied!",document.querySelectorAll(".update-dot").forEach(n=>n.remove());else throw new Error(m.error||"Update failed")}catch(p){y("Update failed: "+p.message,"error"),M.textContent="Update Now",M.disabled=!1}}}async function v(){try{const m=await(await fetch("/api/v1/system/update-history")).json(),n=m.success&&m.history?m.history:[];if(n.length===0){b.innerHTML='
\u{1F4E6}No self-update history.
';return}let e='
';e+='
When
Version
From
Status
';for(const a of n){const t=a.status==="success"?"\u2713 success":a.status==="pending"?"\u23F3 pending":a.status==="partial"?"\u26A0 partial":"\u2717 "+a.status,s=a.status==="success"?"var(--ok-fg)":a.status==="pending"?"var(--muted)":"var(--bad-fg)";e+='
"}}async function r(){try{const m=await(await fetch("/api/v1/system/rollback-versions")).json(),n=m.success&&m.versions?m.versions:[];if(n.length===0){showNotification("No rollback versions available.","info");return}const e=prompt(`Available rollback versions:
+`+n.join(`
`)+`
-Enter version to rollback to:`);if(!i)return;if(!o.includes(i)){showNotification("Invalid version: "+i,"error");return}if(!confirm("Rollback DashCaddy to v"+i+"? The container will restart."))return;a("Rolling back to v"+i+"...","info");const d=await(await secureFetch("/api/v1/system/rollback",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({version:i})})).json();if(d.success)a("Rollback to v"+i+" initiated. Container will restart.","success");else throw new Error(d.error||"Rollback failed")}catch(p){a("Rollback failed: "+p.message,"error")}}L?.addEventListener("click",()=>e(!1)),H?.addEventListener("click",t),g?.addEventListener("click",r),k?.addEventListener("click",h),E?.addEventListener("click",()=>{f?.classList.add("show"),m()}),wireModal(f,M),document.querySelector('[data-panel="updates-history"]')?.addEventListener("click",u),document.querySelector('[data-panel="updates-auto"]')?.addEventListener("click",y),document.querySelector('[data-panel="updates-dashcaddy"]')?.addEventListener("click",()=>{n(),s(),l||e(!0)}),setTimeout(()=>e(!0),5e3)})(),(function(){injectModal("docker-resources-modal",`
+Enter version to rollback to:`);if(!e)return;if(!n.includes(e)){showNotification("Invalid version: "+e,"error");return}if(!confirm("Rollback DashCaddy to v"+e+"? The container will restart."))return;y("Rolling back to v"+e+"...","info");const t=await(await secureFetch("/api/v1/system/rollback",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({version:e})})).json();if(t.success)y("Rollback to v"+e+" initiated. Container will restart.","success");else throw new Error(t.error||"Rollback failed")}catch(p){y("Rollback failed: "+p.message,"error")}}H?.addEventListener("click",()=>o(!1)),M?.addEventListener("click",l),S?.addEventListener("click",r),I?.addEventListener("click",k),B?.addEventListener("click",()=>{w?.classList.add("show"),g()}),wireModal(w,z),document.querySelector('[data-panel="updates-history"]')?.addEventListener("click",f),document.querySelector('[data-panel="updates-auto"]')?.addEventListener("click",x),document.querySelector('[data-panel="updates-dashcaddy"]')?.addEventListener("click",()=>{c(),v(),u||o(!0)}),setTimeout(()=>o(!0),5e3)})(),(function(){injectModal("docker-resources-modal",`
\u{1F433} Docker Resources
Manage volumes, networks, and view disk usage.
@@ -1341,7 +1377,7 @@ Enter version to rollback to:`);if(!i)return;if(!o.includes(i)){showNotification
",C.innerHTML=u,C.querySelectorAll(".dr-vol-del").forEach(y=>{y.addEventListener("click",async()=>{if(confirm(`Delete volume "${y.dataset.name}"? Data will be lost.`)){y.textContent="...",y.disabled=!0;try{await deleteAPI(`/api/v1/docker/volumes/${encodeURIComponent(y.dataset.name)}?force=true`),S()}catch(x){showNotification("Delete failed: "+x.message,"error"),y.textContent="Delete",y.disabled=!1}}})})}catch(m){C.innerHTML=`
Failed: ${escapeHtml(m.message)}
`}}document.getElementById("dr-vol-create")?.addEventListener("click",async()=>{const C=document.getElementById("dr-vol-name"),m=C.value.trim();if(!m){showNotification("Enter a volume name","warning");return}try{await postJSON("/api/v1/docker/volumes",{name:m}),C.value="",showNotification(`Volume "${m}" created`,"success"),S()}catch(h){showNotification("Create failed: "+h.message,"error")}});async function D(){const C=document.getElementById("dr-net-list");try{const h=(await getJSON("/api/v1/docker/networks")).networks||[];if(h.length===0){C.innerHTML='
\u{1F310}No networks found.
';return}let u='
';u+='
Name
Driver
Scope
Containers
Actions
';for(const y of h){const x=["bridge","host","none"].includes(y.name);u+='
",L.innerHTML=f,L.querySelectorAll(".dr-vol-del").forEach(x=>{x.addEventListener("click",async()=>{if(confirm(`Delete volume "${x.dataset.name}"? Data will be lost.`)){x.textContent="...",x.disabled=!0;try{await deleteAPI(`/api/v1/docker/volumes/${encodeURIComponent(x.dataset.name)}?force=true`),C()}catch(E){showNotification("Delete failed: "+E.message,"error"),x.textContent="Delete",x.disabled=!1}}})})}catch(g){L.innerHTML=`
Failed: ${escapeHtml(g.message)}
`}}document.getElementById("dr-vol-create")?.addEventListener("click",async()=>{const L=document.getElementById("dr-vol-name"),g=L.value.trim();if(!g){showNotification("Enter a volume name","warning");return}try{await postJSON("/api/v1/docker/volumes",{name:g}),L.value="",showNotification(`Volume "${g}" created`,"success"),C()}catch(k){showNotification("Create failed: "+k.message,"error")}});async function P(){const L=document.getElementById("dr-net-list");try{const k=(await getJSON("/api/v1/docker/networks")).networks||[];if(k.length===0){L.innerHTML='
\u{1F310}No networks found.
';return}let f='
';f+='
Name
Driver
Scope
Containers
Actions
';for(const x of k){const E=["bridge","host","none"].includes(x.name);f+='
';for(const u of m.results){const y=u.status==="deployed"||u.status==="created"?"\u2705":u.status==="exists"?"\u26A1":u.status==="skipped"?"\u23ED":"\u274C";h+='
';for(const f of g.results){const x=f.status==="deployed"||f.status==="created"?"\u2705":f.status==="exists"?"\u26A1":f.status==="skipped"?"\u23ED":"\u274C";k+='
`}}E?.addEventListener("click",()=>{f?.classList.add("show"),u(!1)}),wireModal(f,M),k?.addEventListener("click",()=>u(!1)),D?.addEventListener("change",()=>u(!1)),C?.addEventListener("click",()=>u(!0)),S?.addEventListener("click",async()=>{if(confirm("Clear the entire audit log? This cannot be undone."))try{const x=await(await secureFetch("/api/v1/audit-logs",{method:"DELETE"})).json();x.success?u(!1):showNotification("Error: "+(x.error||"Clear failed"),"error")}catch(y){showNotification("Error: "+y.message,"error")}})})(),(function(){injectModal("weather-modal",`
Weather Settings
+
`);const w=document.getElementById("audit-modal"),B=document.getElementById("audit-log-btn"),z=document.getElementById("audit-cancel"),I=document.getElementById("audit-refresh-btn"),C=document.getElementById("audit-clear-btn"),P=document.getElementById("audit-filter"),$=document.getElementById("audit-log-container"),L=document.getElementById("audit-load-more");let g=0;const k=50;async function f(x){try{x||(g=0,$.innerHTML='
`}}B?.addEventListener("click",()=>{w?.classList.add("show"),f(!1)}),wireModal(w,z),I?.addEventListener("click",()=>f(!1)),P?.addEventListener("change",()=>f(!1)),L?.addEventListener("click",()=>f(!0)),C?.addEventListener("click",async()=>{if(confirm("Clear the entire audit log? This cannot be undone."))try{const E=await(await secureFetch("/api/v1/audit-logs",{method:"DELETE"})).json();E.success?f(!1):showNotification("Error: "+(E.error||"Clear failed"),"error")}catch(x){showNotification("Error: "+x.message,"error")}})})(),(function(){injectModal("weather-modal",`
Weather Settings
Enter a city name, postal code, or “City, Country”
@@ -1442,23 +1478,23 @@ Enter version to rollback to:`);if(!i)return;if(!o.includes(i)){showNotification
-
`);const f="weather-location",E="weather-zip",M="weather-geo",k="weather-unit";!safeGet(f)&&safeGet(E)&&safeSet(f,safeGet(E));function S(){return safeGet(k)||"imperial"}function D(){return{icon:document.querySelector(".weather-icon"),temp:document.querySelector(".weather-temp"),condition:document.querySelector(".weather-condition"),location:document.querySelector(".weather-location"),wind:document.querySelector(".weather-wind")}}const B={0:"Clear sky",1:"Mainly clear",2:"Partly cloudy",3:"Overcast",45:"Fog",48:"Rime fog",51:"Light drizzle",53:"Drizzle",55:"Dense drizzle",56:"Freezing drizzle",57:"Dense freezing drizzle",61:"Light rain",63:"Moderate rain",65:"Heavy rain",66:"Light freezing rain",67:"Heavy freezing rain",71:"Light snow",73:"Moderate snow",75:"Heavy snow",77:"Snow grains",80:"Light showers",81:"Moderate showers",82:"Violent showers",85:"Light snow showers",86:"Heavy snow showers",95:"Thunderstorm",96:"Thunderstorm with hail",99:"Severe thunderstorm"},C={0:"\u2600\uFE0F",1:"\u{1F324}\uFE0F",2:"\u26C5",3:"\u2601\uFE0F",45:"\u{1F32B}\uFE0F",48:"\u{1F32B}\uFE0F",51:"\u{1F326}\uFE0F",53:"\u{1F326}\uFE0F",55:"\u{1F326}\uFE0F",56:"\u{1F328}\uFE0F",57:"\u{1F328}\uFE0F",61:"\u{1F326}\uFE0F",63:"\u{1F327}\uFE0F",65:"\u{1F327}\uFE0F",66:"\u{1F328}\uFE0F",67:"\u{1F328}\uFE0F",71:"\u{1F328}\uFE0F",73:"\u2744\uFE0F",75:"\u2744\uFE0F",77:"\u2744\uFE0F",80:"\u{1F326}\uFE0F",81:"\u{1F327}\uFE0F",82:"\u{1F327}\uFE0F",85:"\u{1F328}\uFE0F",86:"\u2744\uFE0F",95:"\u26C8\uFE0F",96:"\u26C8\uFE0F",99:"\u26C8\uFE0F"},m=["N","NNE","NE","ENE","E","ESE","SE","SSE","S","SSW","SW","WSW","W","WNW","NW","NNW"];function h(z){return m[Math.round(z/22.5)%16]}async function u(z){const H=safeGet(M);if(H)try{const l=JSON.parse(H);if(l.query===z)return l}catch{}const L=await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(z)}&count=1&language=en&format=json`);if(!L.ok)throw new Error("Geocoding failed");const g=await L.json();if(!g.results||!g.results.length)throw new Error("Location not found");const I=g.results[0],c={query:z,lat:I.latitude,lon:I.longitude,city:I.name,state:I.admin1||"",country:I.country||"",countryCode:I.country_code||""};return safeSet(M,JSON.stringify(c)),c}function y(z){return z.countryCode==="US"&&z.state?`${z.city}, ${z.state}`:z.country?`${z.city}, ${z.country}`:z.city}async function x(z){try{const H=await u(z),L=S(),g=L==="metric"?"celsius":"fahrenheit",I=L==="metric"?"kmh":"mph",c=`https://api.open-meteo.com/v1/forecast?latitude=${H.lat}&longitude=${H.lon}¤t=temperature_2m,weather_code,wind_speed_10m,wind_direction_10m&temperature_unit=${g}&wind_speed_unit=${I}`,l=await fetch(c);if(!l.ok)throw new Error("Weather fetch failed");const n=(await l.json()).current,e=n.weather_code;return{temp:Math.round(n.temperature_2m),condition:B[e]||"Unknown",icon:C[e]||"\u{1F324}\uFE0F",locationStr:y(H),windSpeed:Math.round(n.wind_speed_10m),windDir:h(n.wind_direction_10m),unit:L}}catch(H){return console.warn("Weather fetch failed:",H),null}}async function O(){const z=D();if(!z.icon||!z.temp||!z.condition||!z.location||!z.wind){console.warn("Weather widget elements not found");return}const H=safeGet(f);if(!H){z.location.textContent="Set Location",z.temp.textContent="--\xB0",z.condition.textContent="Click \u2699\uFE0F to configure",z.wind.textContent="--",z.icon.innerHTML='\u{1F324}\uFE0F';return}try{const L=await x(H);if(L){const g=L.unit==="metric"?"\xB0C":"\xB0F",I=L.unit==="metric"?"km/h":"mph";z.location.textContent=L.locationStr,z.temp.textContent=`${L.temp}${g}`,z.condition.textContent=L.condition,z.wind.textContent=`Wind: ${L.windSpeed} ${I} ${L.windDir}`,z.icon.innerHTML=`${escapeHtml(L.icon)}`}}catch(L){console.error("Weather update error:",L),z.location.textContent="Weather Error",z.temp.textContent="Error",z.condition.textContent="Failed to load",z.wind.textContent="--"}}const A=document.getElementById("weather-modal"),N=document.getElementById("weather-location-input");document.getElementById("weather-settings")?.addEventListener("click",()=>{N.value=safeGet(f)||"";const z=S(),H=A.querySelector(`input[name="weather-unit-radio"][value="${z}"]`);H&&(H.checked=!0),A.classList.add("show"),N.focus()}),document.getElementById("weather-cancel")?.addEventListener("click",()=>{A.classList.remove("show")}),document.getElementById("weather-save")?.addEventListener("click",()=>{const z=N.value.trim();if(z){safeGet(f)!==z&&safeSet(M,""),safeSet(f,z);const L=A.querySelector('input[name="weather-unit-radio"]:checked'),g=L?L.value:"imperial",I=S();safeSet(k,g),I!==g&&safeSet(M,""),A.classList.remove("show"),O()}else showNotification("Please enter a location (e.g., Hamburg, London, 90210)","warning")}),wireModal(A),document.addEventListener("keydown",z=>{z.key==="Escape"&&A.classList.contains("show")&&A.classList.remove("show")}),O(),setInterval(O,DC.POLL.WEATHER)})(),(function(){const f=document.getElementById("clock-widget"),E=document.getElementById("clock-render");if(!f||!E)return;const M=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],k=["January","February","March","April","May","June","July","August","September","October","November","December"],S=["XII","I","II","III","IV","V","VI","VII","VIII","IX","X","XI"];let D=safeGet("clock-style")||"default",B=-1,C=!1,m="",h="",u=null,y=null;function x(o){if(C||safeGet("clock-chimes")!=="true")return;C=!0;const i=parseInt(safeGet("clock-chime-volume")||"50",10)/100;let v=0;function d(){if(v>=o){C=!1;return}const w=new Audio("/assets/sounds/church-bell.mp3");w.volume=i,w.play().catch(()=>{}),v++,v{C=!1},2500)}d()}function O(o){return M[o.getDay()]+", "+k[o.getMonth()]+" "+o.getDate()+", "+o.getFullYear()}function A(){h="",u=null}function N(){return h!=="digital"&&(E.innerHTML='
`).join("")}async function H(){try{const n=await(await fetch("/api/v1/license/status")).json();n.success&&(z(n.license),I(n.license))}catch(a){console.warn("Failed to load license status:",a.message)}}async function L(){const a=E.value.trim();if(!a){A("Please enter a license code.");return}O(),M.disabled=!0,M.textContent="Activating...";try{const e=await(await secureFetch("/api/v1/license/activate",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:a})})).json();e.success?(N(e.message),E.value="",z(e.license),showNotification("License activated! Premium features unlocked.","success",5e3),I(e.license)):A(e.error||"Activation failed")}catch(n){A("Network error: "+n.message)}finally{M.disabled=!1,M.textContent="Activate"}}async function g(){if(confirm("Deactivate your license? You can reuse the code on another machine.")){k.disabled=!0,k.textContent="Deactivating...";try{const n=await(await secureFetch("/api/v1/license/deactivate",{method:"POST"})).json();n.success?(N(n.message),await H(),showNotification("License deactivated.","info",3e3),I({active:!1})):A(n.error||"Deactivation failed")}catch(a){A("Network error: "+a.message)}finally{k.disabled=!1,k.textContent="Deactivate"}}}function I(a){const n=document.getElementById("license-status-topbar"),e=document.getElementById("license-topbar-icon"),t=document.getElementById("license-topbar-text"),s=document.getElementById("license-topbar-time");if(n)if(n.className="license-status-topbar "+(a.active?"premium":"free"),a.active)if(e.textContent="\u2605",t.textContent="PREMIUM",a.lifetime)s.textContent="\xB7 LIFETIME";else{const r=a.daysRemaining;s.textContent=r!=null?"\xB7 "+r+"d remaining":""}else e.textContent="\u2606",t.textContent=a.expired?"EXPIRED":"FREE TIER",s.textContent=""}function c(){O(),H(),f.classList.add("show")}E.addEventListener("input",function(){let a=this.value.toUpperCase().replace(/[^A-Z0-9-]/g,"");if(a.length>this._prevLength&&(a=a.replace(/-/g,""),a.length>2&&!a.startsWith("DC")&&(a="DC"+a),a.startsWith("DC")&&a.length>2)){const n=["DC"],e=a.substring(2);for(let t=0;t{a.key==="Enter"&&L()}),wireModal(f,document.getElementById("license-cancel"));const l=document.getElementById("license-status-topbar");l&&l.addEventListener("click",()=>window.openLicenseModal&&window.openLicenseModal()),window.openLicenseModal=c,window.checkPremiumFeature=async function(a){try{return(await(await fetch(`/api/v1/license/feature/${a}`)).json()).available}catch{return!1}},H().then(a=>{x&&I(x)})})();
+
`).join("")}async function M(){try{const c=await(await fetch("/api/v1/license/status")).json();c.success&&(N(c.license),T(c.license))}catch(y){console.warn("Failed to load license status:",y.message)}}async function H(){const y=B.value.trim();if(!y){O("Please enter a license code.");return}R(),z.disabled=!0,z.textContent="Activating...";try{const o=await(await secureFetch("/api/v1/license/activate",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:y})})).json();o.success?(D(o.message),B.value="",N(o.license),showNotification("License activated! Premium features unlocked.","success",5e3),T(o.license)):O(o.error||"Activation failed")}catch(c){O("Network error: "+c.message)}finally{z.disabled=!1,z.textContent="Activate"}}async function S(){if(confirm("Deactivate your license? You can reuse the code on another machine.")){I.disabled=!0,I.textContent="Deactivating...";try{const c=await(await secureFetch("/api/v1/license/deactivate",{method:"POST"})).json();c.success?(D(c.message),await M(),showNotification("License deactivated.","info",3e3),T({active:!1})):O(c.error||"Deactivation failed")}catch(y){O("Network error: "+y.message)}finally{I.disabled=!1,I.textContent="Deactivate"}}}function T(y){const c=document.getElementById("license-status-topbar"),o=document.getElementById("license-topbar-icon"),l=document.getElementById("license-topbar-text"),v=document.getElementById("license-topbar-time");if(c)if(c.className="license-status-topbar "+(y.active?"premium":"free"),y.active)if(o.textContent="\u2605",l.textContent="PREMIUM",y.lifetime)v.textContent="\xB7 LIFETIME";else{const r=y.daysRemaining;v.textContent=r!=null?"\xB7 "+r+"d remaining":""}else o.textContent="\u2606",l.textContent=y.expired?"EXPIRED":"FREE TIER",v.textContent=""}function b(){R(),M(),w.classList.add("show")}B.addEventListener("input",function(){let y=this.value.toUpperCase().replace(/[^A-Z0-9-]/g,"");if(y.length>this._prevLength&&(y=y.replace(/-/g,""),y.length>2&&!y.startsWith("DC")&&(y="DC"+y),y.startsWith("DC")&&y.length>2)){const c=["DC"],o=y.substring(2);for(let l=0;l{y.key==="Enter"&&H()}),wireModal(w,document.getElementById("license-cancel"));const u=document.getElementById("license-status-topbar");u&&u.addEventListener("click",()=>window.openLicenseModal&&window.openLicenseModal()),window.openLicenseModal=b,window.checkPremiumFeature=async function(y){try{return(await(await fetch(`/api/v1/license/feature/${y}`)).json()).available}catch{return!1}},M().then(y=>{E&&T(E)})})();
diff --git a/status/js/backup-restore.js b/status/js/backup-restore.js
index f462aec..0fd1866 100644
--- a/status/js/backup-restore.js
+++ b/status/js/backup-restore.js
@@ -384,6 +384,9 @@
});
// === Automated Backups Tab ===
+ // Holds the destination currently being edited in the form
+ var currentDestination = { type: 'local' };
+
async function loadBackupSchedule() {
if (!scheduleContainer) return;
try {
@@ -394,6 +397,10 @@
var autoKey = Object.keys(cfg)[0];
var auto = autoKey ? cfg[autoKey] : null;
+ // Pull existing destination (first one) — fall back to local
+ var existingDest = (auto?.destinations && auto.destinations[0]) || { type: 'local' };
+ currentDestination = JSON.parse(JSON.stringify(existingDest));
+
var html = '