",x.innerHTML=D,N("setup-step-summary")}async function z(x){try{const D=await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(x)});return D.ok?(await D.json(),!0):(errorHandler.logError("[SetupWizard] Save Config",new Error(`Server returned ${D.status}`),{function:"saveConfigToServer"}),!1)}catch(D){return errorHandler.logError("[SetupWizard] Save Config",D,{function:"saveConfigToServer"}),!1}}async function P(){const x={setupComplete:!0,configurationType:h,timestamp:new Date().toISOString(),timezone:document.getElementById("setup-timezone")?.value||Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC"};if(h==="homelab"){x.tld=document.getElementById("setup-tld")?.value?.trim()||".home",x.caName=document.getElementById("setup-ca-name")?.value?.trim()||"";const b=document.getElementById("setup-dns-provider")?.value||"technitium";x.dns={provider:b,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()||""},x.defaults={dnsType:"private",sslType:"internal",targetIP:"localhost"}}else h==="simple"?(x.defaultIP=document.getElementById("setup-simple-ip")?.value?.trim()||"localhost",x.defaults={dnsType:"none",sslType:"none",targetIP:x.defaultIP}):h==="public"&&(x.domain=document.getElementById("setup-public-domain")?.value?.trim()||"",x.email=document.getElementById("setup-public-email")?.value?.trim()||"",x.routingMode=document.querySelector('input[name="routing-mode"]:checked')?.value||"subdirectory",x.defaults={dnsType:x.routingMode==="subdirectory"?"none":"public",sslType:"letsencrypt",targetIP:"localhost"});const D=await z(x);safeSet("dashcaddy-config",JSON.stringify(x)),safeSet("dashcaddy-setup","completed"),document.getElementById("setup-wizard").style.display="none";const y=h==="homelab"?"Professional Home Lab":h==="simple"?"Simple Setup":"Public Server",m=D?"server (shared across all devices)":"locally (this browser only)";showNotification(`Setup Complete! Configured for: ${y}. Settings saved to: ${m}`,"success",5e3),setTimeout(()=>location.reload(),500)}const g=document.getElementById("setup-step-1-next");g&&(g.onclick=function(x){x.preventDefault();const D=document.querySelector('input[name="config-type"]:checked');D&&(h=D.value),N(h==="homelab"?"setup-step-homelab":h==="simple"?"setup-step-simple":h==="public"?"setup-step-public":"setup-step-homelab")});const T=document.getElementById("setup-skip");T&&(T.onclick=async function(x){x.preventDefault(),confirm("Skip setup? You can run it later from Settings.")&&(await z({setupComplete:!0,skipped:!0,timestamp:new Date().toISOString()}),safeSet("dashcaddy-setup","skipped"),document.getElementById("setup-wizard").style.display="none")});const w=document.getElementById("setup-tld");w&&(w.oninput=function(x){const D=x.target.value||".home",y=document.getElementById("tld-preview"),m=document.getElementById("tld-preview-2");y&&(y.textContent=D),m&&(m.textContent=D)});const H=document.getElementById("setup-homelab-back");H&&(H.onclick=function(x){x.preventDefault(),N("setup-step-1")});const E=document.getElementById("setup-homelab-next");E&&(E.onclick=function(x){x.preventDefault();const D=document.getElementById("setup-tld")?.value?.trim()||"",y=document.getElementById("setup-ca-name")?.value?.trim()||"",m=document.getElementById("setup-dns-ip")?.value?.trim()||"";if(!D||!D.startsWith(".")){showNotification("Please enter a valid TLD starting with a dot (e.g., .home)","warning");return}if(!y){showNotification("Please enter a Certificate Authority name","warning");return}if(!m){showNotification("Please enter your DNS server IP address","warning");return}R()});const $=document.getElementById("setup-simple-back");$&&($.onclick=function(x){x.preventDefault(),N("setup-step-1")});const I=document.getElementById("setup-simple-next");I&&(I.onclick=function(x){x.preventDefault(),R()}),document.querySelectorAll('input[name="routing-mode"]').forEach(function(x){x.onchange=function(){var D=document.getElementById("dns-requirement-note");D&&(D.textContent=this.value==="subdirectory"?"Only one DNS record needed (for the main domain)":"You'll need to configure DNS manually for each subdomain")}});const L=document.getElementById("setup-public-back");L&&(L.onclick=function(x){x.preventDefault(),N("setup-step-1")});const j=document.getElementById("setup-public-next");j&&(j.onclick=function(x){x.preventDefault();const D=document.getElementById("setup-public-domain")?.value?.trim()||"",y=document.getElementById("setup-public-email")?.value?.trim()||"";if(!D){showNotification("Please enter your domain name","warning");return}if(!y||!y.includes("@")){showNotification("Please enter a valid email address","warning");return}R()});const M=document.getElementById("setup-summary-back");M&&(M.onclick=function(x){x.preventDefault(),h==="homelab"?N("setup-step-homelab"):h==="simple"?N("setup-step-simple"):h==="public"&&N("setup-step-public")});const O=document.getElementById("setup-finish");O&&(O.onclick=function(x){x.preventDefault(),P()}),window.getGlobalConfig=async function(){try{const D=await fetch("/api/v1/config");if(D.ok){const y=await D.json();if(y&&y.setupComplete)return y}}catch{console.warn("Could not fetch config from server")}const x=safeGet("dashcaddy-config");return x?JSON.parse(x):{setupComplete:!1,configurationType:"homelab",tld:".home",caName:"",defaults:{dnsType:"private",sslType:"internal",targetIP:"localhost"}}},window.resetSetupWizard=async function(){if(confirm("Reset DashCaddy configuration? This will show the setup wizard again.")){try{await secureFetch("/api/v1/config",{method:"DELETE"})}catch{console.warn("Could not delete server config")}safeRemove("dashcaddy-setup"),safeRemove("dashcaddy-config"),location.reload()}}})(),(function(){const h=new ErrorHandler;injectModal("app-selector-modal",`
Choose an App
`),injectModal("app-deploy-modal",`
Deploy Application
Your app will be available at: uptime.home
\u2713 Detected from existing media servers:
Select folders from existing servers, browse, or type paths manually. Separate multiple with commas.
Token expires in 4 minutes! Get it right before clicking Deploy. Leave empty to configure Plex manually later.
Checking Tailscale status...
\u2699\uFE0F Advanced Options
Use 'localhost' for same-host containers, or specific IP for remote services
Customize where container data is stored on the host. Media volumes are configured above.
Optional CPU and memory constraints. Leave at 0 for unlimited.
`);const S="custom-apps";let A=null,k=null;const N=document.getElementById("app-selector-modal"),R=document.getElementById("app-selector-grid");async function z(){try{const l=await(await fetch("/api/v1/apps/templates")).json();if(l.success)return A=l.templates,k=l.categories,!0}catch(s){h.logError("[AppSelector] Fetch Templates",s,{function:"fetchApiTemplates"})}return!1}async function P(s){try{return await(await fetch(`/api/v1/apps/ports/${s}/check`)).json()}catch(l){return h.logError("[AppSelector] Check Port",l,{function:"checkPortAvailability"}),{available:!0}}}async function g(s){try{const r=await(await fetch(`/api/v1/apps/ports/${s}/suggest`)).json();if(r.success)return r.suggestedPort}catch(l){h.logError("[AppSelector] Get Suggested Port",l,{function:"getSuggestedPort"})}return s}async function T(){if(R.innerHTML='
Loading app templates...
',!A&&!await z()){R.innerHTML='
Failed to load app templates. Please try again.
';return}R.innerHTML="";const s={};for(const[r,f]of Object.entries(A)){const u=f.category||"Other";s[u]||(s[u]=[]),s[u].push({id:r,...f})}const l=k?Object.keys(k):Object.keys(s).sort();for(const r of l){const f=s[r];if(!f||f.length===0)continue;f.sort((e,o)=>(o.popularity||0)-(e.popularity||0));const u=document.createElement("div");u.className="app-category-header";const t=k?.[r]||{};u.innerHTML=`${escapeHtml(t.icon||"")} ${escapeHtml(r)}`,t.color&&(u.style.borderBottomColor=t.color),R.appendChild(u),f.forEach(e=>{const o=document.createElement("div");o.className="app-option";const a=e.isDashboardWidget,d=a&&safeGet("widget-"+e.id+"-enabled")!=="false",n=a?`
${d?"ON":"OFF"}
`:"",i=!a&&e.difficulty?`
${escapeHtml(e.difficulty)}
`:"";o.innerHTML=`
${escapeHtml(e.icon||"\u{1F4E6}")}
${escapeHtml(e.name)}
${escapeHtml(e.description||"")}
${n}${i}
`,a?o.onclick=()=>w(e,o):o.onclick=()=>H(e),R.appendChild(o)})}window.renderRecipeCards&&await window.renderRecipeCards(R)}function w(s,l){const r="widget-"+s.id+"-enabled",u=!(safeGet(r)!=="false");safeSet(r,String(u));const t=s.widgetSelector;if(t){const o=document.querySelector(t);o&&(o.style.display=u?"":"none")}const e=l.querySelector('div[style*="border-radius: 4px"]');e&&(e.textContent=u?"ON":"OFF",e.style.background=u?"#2ecc7130":"#e74c3c30",e.style.color=u?"#2ecc71":"#e74c3c"),showNotification(`${s.name} widget ${u?"enabled":"disabled"}`,"success",2e3)}async function H(s){const l=document.getElementById("app-deploy-modal"),r=document.getElementById("app-deploy-title"),f=document.getElementById("deploy-subdomain"),u=document.getElementById("deploy-url-preview"),t=document.getElementById("deploy-ip"),e=document.getElementById("deploy-port"),o=document.getElementById("deploy-tailscale-only"),a=document.getElementById("tailscale-status");try{const W=await(await secureFetch("/api/v1/apps/check-existing",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({appId:s.id})})).json();if(W.success&&W.exists){const V=W.container;confirm(`Found existing ${s.name} container:
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.`)&&(s._useExisting=!0,s._existingContainer=V)}}catch{}r.textContent=`Deploy ${s.name}`;const d=s.subdomain||s.id.replace(/-/g,"");f.value=d;const n=document.getElementById("subpath-compat-warning");if(n)if(SITE.routingMode==="subdirectory"){const _=s.subpathSupport||"strip";_==="none"?(n.style.display="block",n.innerHTML='⚠ '+s.name+" does not support subdirectory mode. It may not work correctly at a subpath."):_==="strip"?(n.style.display="block",n.innerHTML='ⓘ '+s.name+" has unverified subdirectory support. It may require additional configuration."):n.style.display="none"}else n.style.display="none";const i=SITE.defaults.dnsType||(SITE.configurationType==="public"?"public":"private"),c=SITE.defaults.sslType||(SITE.configurationType==="public"?"letsencrypt":"internal"),C=document.querySelector(`input[name="dns-type"][value="${i}"]`),B=document.querySelector(`input[name="ssl-type"][value="${c}"]`);C?C.checked=!0:document.querySelector('input[name="dns-type"][value="private"]').checked=!0,B?B.checked=!0:document.querySelector('input[name="ssl-type"][value="internal"]').checked=!0,t.value=SITE.defaults.targetIP||"localhost",o.checked=!1;const F=document.querySelector("#app-deploy-modal .flex-col-gap")?.closest("div"),q=document.querySelector("#app-deploy-modal details"),U=q?.querySelector("div");if(q&&U&&(SITE.configurationType==="public"||SITE.configurationType==="homelab")){const _=document.querySelectorAll('#app-deploy-modal input[name="dns-type"]')[0]?.closest("div.flex-col-gap")?.parentElement,W=document.querySelectorAll('#app-deploy-modal input[name="ssl-type"]')[0]?.closest("div.flex-col-gap")?.parentElement;_&&!_.dataset.moved&&(U.appendChild(_),_.dataset.moved="1"),W&&!W.dataset.moved&&(U.appendChild(W),W.dataset.moved="1")}const G=document.getElementById("media-path-section"),J=document.getElementById("deploy-media-path"),X=document.getElementById("media-path-description");if(s.mediaMount){G.style.display="block",J.value="",J.placeholder="/media/Movies, /media/TVShows or click Browse";const _=document.getElementById("detected-mounts-container"),W=document.getElementById("detected-mounts-list");try{const K=await(await fetch("/api/v1/media/detected-mounts")).json();if(K.success&&K.mounts.length>0){_.style.display="block",W.innerHTML="";const te=[...new Set(K.mounts.map(ee=>ee.hostPath))];J.value=te.join(", "),K.mounts.forEach(ee=>{const Z=document.createElement("button");Z.type="button";const le=te.includes(ee.hostPath);Z.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);`,Z.innerHTML=`${escapeHtml(ee.folderName)} from ${escapeHtml(ee.sourceImage)}`,Z.title=`${ee.hostPath} (from ${ee.sourceContainer})`,Z.onclick=()=>{const de=J.value.split(",").map(ce=>ce.trim()).filter(ce=>ce),pe=de.indexOf(ee.hostPath);pe>=0?(de.splice(pe,1),Z.style.background="color-mix(in srgb, var(--success) 15%, var(--card-bg))"):(de.push(ee.hostPath),Z.style.background="color-mix(in srgb, var(--success) 40%, var(--card-bg))"),J.value=de.join(", ")},W.appendChild(Z)})}else _.style.display="none"}catch{_.style.display="none"}document.getElementById("browse-media-btn").onclick=()=>{openFolderBrowser(J)}}else G.style.display="none",J.value="",document.getElementById("detected-mounts-container").style.display="none";const Q=document.getElementById("plex-claim-section");Q&&(s.id==="plex"||s.claimToken?(Q.style.display="block",document.getElementById("deploy-plex-claim").value=""):Q.style.display="none");const ne=document.getElementById("volume-mounts-section"),oe=document.getElementById("volume-mounts-list");if(oe.innerHTML="",s.docker?.volumes?.length){const _=s.mediaMount?.containerPath,W=s.docker.volumes.filter(V=>!V.includes("{{MEDIA_PATH}}")&&!(_&&V.endsWith(":"+_)));W.length>0?(ne.style.display="block",W.forEach((V,K)=>{const[te,ee]=V.split(":"),Z=document.createElement("div");Z.style.cssText="display: flex; gap: 6px; align-items: center;",Z.innerHTML=`
\u2192 ${ee}
`,oe.appendChild(Z),Z.querySelector(".vol-browse-btn").onclick=()=>{const le=Z.querySelector(".vol-host-path");openFolderBrowser(le)}})):ne.style.display="none"}else ne.style.display="none";const se=s.defaultPort||8080;e.value="",e.placeholder=`Default: ${se}`;let Y=document.getElementById("deploy-port-status");Y||(Y=document.createElement("div"),Y.id="deploy-port-status",Y.style.cssText="font-size: 0.8rem; margin-top: 4px;",e.parentNode.appendChild(Y));async function ie(){const _=e.value||se;Y.innerHTML='Checking port...';const W=await P(_);if(W.available)Y.innerHTML=`Port ${escapeHtml(String(_))} is available`;else{const V=await g(se);Y.innerHTML=`
Port ${escapeHtml(_)} in use by ${escapeHtml(W.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,Y.innerHTML=`Using suggested port ${escapeHtml(String(V))}`},Y.appendChild(K)}}let re;e.oninput=function(){clearTimeout(re),re=setTimeout(ie,500)},ie();try{const W=await(await fetch("/api/v1/tailscale/status")).json();W.success&&W.installed&&W.connected?a.innerHTML=`
Connected${W.self?.hostname} (${W.self?.ip})| ${W.deviceCount} devices
`:W.installed?a.innerHTML='Not connected':(a.innerHTML='Not available',o.disabled=!0)}catch{a.innerHTML='Could not check status'}function ae(){const _=f.value||"subdomain",W=document.querySelector('input[name="dns-type"]:checked').value,V=document.querySelector('input[name="ssl-type"]:checked').value;let K="";if(SITE.routingMode==="subdirectory"&&SITE.domain)K=`https://${SITE.domain}/${_}`;else if(W==="private")K=`${V==="none"?"http":"https"}://${buildDomain(_)}`;else if(W==="public"){const te=V==="none"?"http":"https",ee=SITE.domain||_;K=SITE.domain?`${te}://${_}.${SITE.domain}`:`${te}://${_}`}else{const te=e.value||s.defaultPort||DC.DEFAULTS.SERVICE_PORT;K=`http://${t.value}:${te}`}u.textContent=K}f.oninput=ae,t.oninput=ae,e.oninput=ae,document.querySelectorAll('input[name="dns-type"]').forEach(_=>{_.onchange=ae}),document.querySelectorAll('input[name="ssl-type"]').forEach(_=>{_.onchange=ae}),ae(),N.classList.remove("show"),l.classList.add("show"),l.dataset.appTemplate=JSON.stringify(s)}async function E(s){const l=s.appTemplate,r=safeGetJSON(S,[]),f=l._useExisting&&l._existingContainer,u=r.find(t=>t.id===s.subdomain);if(!(u&&!f&&!confirm(`An app with subdomain "${s.subdomain}" already exists. Redeploy?`))){if(u){const t=r.indexOf(u);r.splice(t,1),safeSet(S,JSON.stringify(r))}if(f)s.port=l._existingContainer.primaryPort;else{const t=s.port||l.defaultPort||8080;showNotification(`Checking port ${t} availability...`,"info",0);const e=await P(t);if(!e.available){const o=await g(l.defaultPort||8080);if(confirm(`Port ${t} is already in use by ${e.conflict?.usedBy||"another container"}.
Would you like to use port ${o} instead?`))s.port=o;else{showNotification("Deployment cancelled - port conflict","error",5e3);return}}}showNotification(f?`Configuring ${l.name} with existing container...`:`Deploying ${l.name}...`,"info",0);try{const t={appId:l.id,config:{subdomain:s.subdomain,ip:s.ip,createDns:s.dnsType==="private",port:s.port||l.defaultPort||null,sslType:s.sslType,dnsType:s.dnsType,tailscaleOnly:s.tailscaleOnly||!1,mediaPath:s.mediaPath||null,plexClaimToken:s.plexClaimToken||null,customVolumes:s.customVolumes||null}};f&&(t.config.useExisting=!0,t.config.existingContainerId=l._existingContainer.id,t.config.existingPort=l._existingContainer.primaryPort,!s.port&&l._existingContainer.primaryPort&&(t.config.port=l._existingContainer.primaryPort));const o=await(await secureFetch("/api/v1/apps/deploy",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json();if(o.success){const a={id:s.subdomain,name:l.name,logo:`/assets/${l.id}.png`,containerId:o.containerId,url:o.url,ip:s.ip,appTemplate:l.id,tailscaleOnly:s.tailscaleOnly||!1};r.push(a),safeSet(S,JSON.stringify(r)),window.APPS&&!window.APPS.some(n=>n.id===l.id)&&(window.APPS.push(a),typeof window.buildGrid=="function"&&window.buildGrid(),typeof window.refreshAll=="function"&&setTimeout(()=>window.refreshAll(),500));let d=o.usedExisting?`${l.name} configured with existing container!
URL: ${o.url}`:`${l.name} deployed successfully!
URL: ${o.url}`;o.warning&&(d+=`
\u26A0 Warning: ${o.warning}`),showNotification(d,"success",8e3),delete l._useExisting,delete l._existingContainer,o.url&&o.url.startsWith("https://")&&$(o.url,l.name),o.setupInstructions&&o.setupInstructions.length>0&&setTimeout(()=>{const n=o.setupInstructions.join(`
`);showNotification(`Setup Instructions for ${l.name}: ${n}`,"info",1e4)},1e3)}else throw new Error(o.error||"Deployment failed")}catch(t){h.logError("[AppSelector] Deployment",t,{function:"deploy"}),showNotification(`Failed to deploy ${l.name}: ${t.message}`,"error",8e3)}}}async function $(s,l){showNotification(`\u23F3 Generating SSL certificate for ${l}...`,"warning",6e4);let r=0;const f=12,u=async()=>{r++;try{const t=await fetch(s,{method:"HEAD",mode:"no-cors"});return showNotification(`\u2705 ${l} is ready! SSL certificate generated.`,"success",5e3),!0}catch{return r{window.APPS.some(r=>r.id===l.id)||window.APPS.push(l)})}document.getElementById("add-service-btn")?.addEventListener("click",()=>{T(),N.classList.add("show")}),wireModal(N,document.getElementById("app-selector-cancel"));const L=document.getElementById("app-deploy-modal");document.getElementById("app-deploy-cancel")?.addEventListener("click",()=>{L.classList.remove("show")}),document.getElementById("app-deploy-confirm")?.addEventListener("click",()=>{const s=JSON.parse(L.dataset.appTemplate),l=document.getElementById("deploy-media-path").value.trim(),r=[];document.querySelectorAll("#volume-mounts-list .vol-host-path").forEach(u=>{r.push({hostPath:u.value.trim(),containerPath:u.dataset.containerPath})});const f={appTemplate:s,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:r.length>0?r:null,resources:{cpus:parseFloat(document.getElementById("deploy-cpu-limit").value)||0,memory:parseFloat(document.getElementById("deploy-memory-limit").value)||0}};if(!f.subdomain){showNotification("Please enter a subdomain or domain name","warning");return}if(s.mediaMount?.required&&!l){showNotification("Please enter a media library path for this application","warning");return}L.classList.remove("show"),E(f)}),wireModal(L);const j=document.getElementById("folder-browser-modal"),M=document.getElementById("folder-browser-path"),O=document.getElementById("folder-browser-list"),x=document.getElementById("folder-browser-selected"),D=document.getElementById("folder-browser-selected-list");let y="",m=[],b=null;window.openFolderBrowser=function(s){b=s,m=s.value.split(",").map(l=>l.trim()).filter(l=>l),y="",v(),p(""),j.classList.add("show")};async function p(s){M.textContent=s||"Select a drive...",O.innerHTML='
');const h=document.getElementById("error-log-modal"),S=document.getElementById("error-log-content"),A=document.getElementById("view-error-logs"),k=document.getElementById("error-log-refresh"),N=document.getElementById("error-log-clear"),R=document.getElementById("error-log-close");async function z(){S.innerHTML='
Create and manage Docker container checkpoints for instant state recovery.
Image:
Status:
Created:
Container ID:
\u{1F4BE}Select a container to view its snapshots
`);const h=document.getElementById("snapshot-modal"),S=document.getElementById("snapshot-btn"),A=document.getElementById("snapshot-close"),k=document.getElementById("snapshot-container-select"),N=document.getElementById("snapshot-details"),R=document.getElementById("snapshot-create-btn"),z=document.getElementById("snapshot-create-status");let P=null;async function g(){try{const I=await(await fetch("/api/v1/containers")).json();if(!I.success||!I.containers)return;k.innerHTML='';for(const L of I.containers){const j=document.createElement("option");j.value=L.id,j.textContent=`${L.name||L.id} (${L.image||"unknown"})`,j.dataset.name=L.name,j.dataset.image=L.image,j.dataset.status=L.status,j.dataset.created=L.created,k.appendChild(j)}}catch($){console.error("Failed to load containers:",$)}}function T($){if(!$||!$.value){N.style.display="none",P=null;return}P=$.value,document.getElementById("snapshot-image").textContent=$.dataset.image||"-",document.getElementById("snapshot-status").textContent=$.dataset.status||"-",document.getElementById("snapshot-created").textContent=$.dataset.created?new Date($.dataset.created*1e3).toLocaleString():"-",document.getElementById("snapshot-id").textContent=$.value.substring(0,12),N.style.display=""}async function w(){if(!P){z.textContent="Please select a container first",z.style.color="var(--bad-fg)";return}const $=document.getElementById("snapshot-name").value.trim();if(!$){z.textContent="Please enter a snapshot name",z.style.color="var(--bad-fg)";return}const I=document.getElementById("snapshot-leave-running").checked;R.disabled=!0,R.textContent="Creating...",z.textContent="";try{const j=await(await fetch(`/api/v1/containers/${encodeURIComponent(P)}/checkpoint`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:$,leaveRunning:I})})).json();j.success?(z.textContent=`\u2713 Snapshot "${$}" created successfully`,z.style.color="var(--ok-fg)",document.getElementById("snapshot-name").value=""):(z.textContent=`\u2717 Failed: ${j.error||"Unknown error"}`,z.style.color="var(--bad-fg)")}catch(L){z.textContent=`\u2717 Error: ${L.message}`,z.style.color="var(--bad-fg)"}finally{R.disabled=!1,R.textContent="\u{1F4BE} Create Snapshot"}}function H(){h.classList.add("show"),g()}function E(){h.classList.remove("show"),N.style.display="none",P=null,k.selectedIndex=0}S?.addEventListener("click",H),A?.addEventListener("click",E),wireModal(h,A),k?.addEventListener("change",$=>{const I=k.options[k.selectedIndex];T(I)}),R?.addEventListener("click",w),h?.querySelectorAll(".panel-tab").forEach($=>{$.addEventListener("click",()=>{h.querySelectorAll(".panel-tab").forEach(I=>I.classList.remove("active")),h.querySelectorAll(".panel-section").forEach(I=>I.classList.remove("active")),$.classList.add("active"),h.querySelector(`#${$.dataset.panel}`).classList.add("active")})})})(),(function(){injectModal("arr-setup-modal",`
\u{1F3AC} Smart Arr Connect
Auto-discover and connect your entire media stack.
`,g.style.display="block"}}function D(p){let v="";const s=p.services,l=["radarr","sonarr","prowlarr"];for(const u of l){const t=s[u];if(!t||t.status==="not_found"&&!t.url)continue;const e=L[u],o=j[u],a=t.status==="connected";v+=`
`}T.innerHTML=v}window.smartTestConnection=async function(p){const v=document.getElementById(`smart-${p}-url`),s=document.getElementById(`smart-${p}-key`),l=document.getElementById(`smart-${p}-status`),r=v?.value.trim(),f=s?.value.trim();if(!r||!f){l.innerHTML='Enter URL and API key';return}l.innerHTML='';try{const t=await(await secureFetch("/api/v1/arr/test-connection",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({service:p,url:r,apiKey:f})})).json();t.success?l.innerHTML=`✓ ${escapeHtml(t.appName||"Connected")} v${escapeHtml(t.version||"")}`:l.innerHTML=`✗ ${escapeHtml(t.error)}`}catch(u){l.innerHTML=`✗ ${escapeHtml(u.message)}`}};async function y(){M("progress"),w.innerHTML='
Connecting services...
';const p={};for(const s of["radarr","sonarr","prowlarr"]){const l=document.getElementById(`smart-${s}-url`)?.value.trim(),r=document.getElementById(`smart-${s}-key`)?.value.trim();r&&l?p[s]={apiKey:r,url:l}:r&&(p[s]={apiKey:r})}const v={services:Object.keys(p).length>0?p: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 l=await(await secureFetch("/api/v1/arr/smart-connect",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(v)})).json();let r="";for(const f of l.steps||[]){const u=f.status==="success"?'✓':'✗',t=f.status==="success"?"var(--muted)":"var(--bad-fg)";r+=`
"}}}async function m(e,o){try{var a=await secureFetch("/api/v1/backups/schedule",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({appId:e,...o})}),d=await a.json();d.success?showNotification("Schedule updated for "+e,"success"):(showNotification("Update failed: "+(d.error||"Unknown"),"error"),y())}catch(n){showNotification("Error: "+n.message,"error")}}async function b(e){try{var o=await secureFetch("/api/v1/backups/backup/"+encodeURIComponent(e),{method:"POST",headers:{"Content-Type":"application/json"}}),a=await o.json();a.success?showNotification("Backup started for "+e+"!","success"):showNotification("Backup failed: "+(a.error||"Unknown"),"error")}catch(d){showNotification("Error: "+d.message,"error")}}async function p(e){if(confirm("Remove backup schedule for "+e+"?"))try{var o=await secureFetch("/api/v1/backups/schedule/"+encodeURIComponent(e),{method:"DELETE"}),a=await o.json();a.success?(showNotification("Schedule removed for "+e,"success"),y()):showNotification("Delete failed: "+(a.error||"Unknown"),"error")}catch(d){showNotification("Error: "+d.message,"error")}}async function v(){var e=document.getElementById("new-schedule-appid")?.value?.trim(),o=document.getElementById("new-schedule-interval")?.value||"daily",a=parseInt(document.getElementById("new-schedule-retention")?.value)||7;if(!e){showNotification("Please enter an App ID","warning");return}try{var d=await secureFetch("/api/v1/backups/schedule",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({appId:e,schedule:o,retention:{keep:a},enabled:!0})}),n=await d.json();if(n.success){showNotification("Schedule created for "+e,"success"),y();var i=document.getElementById("new-schedule-appid");i&&(i.value="")}else showNotification("Failed: "+(n.error||"Unknown"),"error")}catch(c){showNotification("Error: "+c.message,"error")}}async function s(){if(O){O.innerHTML='
Loading...
';try{var e=await fetch("/api/v1/backups/files"),o=await e.json();if(!o.success)throw new Error(o.error||"Failed to load");var a=o.files||[];if(a.length===0){O.innerHTML='
"}}}window.__restoreServerBackup=async function(e){if(confirm("Restore from this server backup? This will overwrite current configuration."))try{var o=await secureFetch("/api/v1/backups/restore/"+e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({restoreServices:!0,restoreConfig:!0})}),a=await o.json();a.success?(showNotification("Restore completed successfully!","success"),location.reload()):showNotification("Restore failed: "+(a.error||"Unknown error"),"error")}catch(d){showNotification("Restore error: "+d.message,"error")}},document.querySelector('[data-panel="backup-schedules-tab"]')?.addEventListener("click",y),document.querySelector('[data-panel="backup-disk-tab"]')?.addEventListener("click",s),document.querySelector('[data-panel="backup-pointintime-tab"]')?.addEventListener("click",r),document.querySelector('[data-panel="backup-history-tab"]')?.addEventListener("click",l);async function r(){if(x){try{var e=await fetch("/api/v1/license/status"),o=await e.json();if(o.tier!=="premium"){x.innerHTML=`
\u2B50
Premium Feature
Point-in-time restore requires DashCaddy Premium with auto-backup enabled.
Configuration differs \u2014 restoring will replace current settings
':i+='
No changes
',i+="
"}i+='
',document.body.insertAdjacentHTML("beforeend",i),document.getElementById("compare-close-btn")?.addEventListener("click",function(){document.getElementById("compare-overlay")?.remove()}),document.getElementById("compare-overlay")?.addEventListener("click",function(B){B.target===this&&this.remove()})}catch(B){showNotification("Compare error: "+B.message,"error")}}async function t(e,o){if(confirm("Restore "+o+" for "+e+`?
This will replace current configuration, credentials, and data. Containers will be restarted.`))try{var a=await secureFetch("/api/v1/apps/"+encodeURIComponent(e)+"/revert/"+encodeURIComponent(o),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({restartContainers:!0})}),d=await a.json();d.success?(showNotification(e+" restored to "+o,"success"),setTimeout(function(){location.reload()},1500)):showNotification("Restore failed: "+(d.error||"Unknown"),"error")}catch(n){showNotification("Restore error: "+n.message,"error")}}})(),(function(){injectModal("stats-modal",`
\u{1F4CA} Resource Monitor
Real-time and historical CPU, memory, network, and disk usage for containers.
Loading container stats...
\u{1F4C8}
Loading 24-hour aggregated metrics...
\u{1F4CA}
Choose a container and time range to view history.
\u{1F514}
Loading alert configurations...
`);const h=document.getElementById("stats-modal"),S=document.getElementById("container-stats-btn"),A=document.getElementById("stats-cancel"),k=document.getElementById("stats-refresh-btn"),N=document.getElementById("stats-auto-refresh"),R=document.getElementById("stats-container"),z=document.getElementById("stats-aggregated-container"),P=document.getElementById("stats-alerts-container"),g=document.getElementById("stats-last-update");let T=null,w=null;function H(r){if(r===0||!r)return"0 B";const f=1024,u=["B","KB","MB","GB"],t=Math.floor(Math.log(r)/Math.log(f));return parseFloat((r/Math.pow(f,t)).toFixed(1))+" "+u[t]}function E(r){return r<30?"#2ecc71":r<70?"#f39c12":"#e74c3c"}function $(r){return r<50?"#2ecc71":r<80?"#f39c12":"#e74c3c"}async function I(){try{let r=null,f=!1;try{const e=await(await fetch("/api/v1/monitoring/stats")).json();e.success&&e.stats&&(r=e.stats,f=!0,w=e.stats)}catch{}if(!f){const e=await(await fetch("/api/v1/stats/containers")).json();if(e.success&&e.stats){r={};for(const o of e.stats)r[o.name]={name:o.name,current:{cpu:o.cpu,memory:{percent:o.memory.percent,usage:o.memory.used,limit:o.memory.limit,usageMB:Math.round(o.memory.used/1048576),limitMB:Math.round(o.memory.limit/1048576)},network:{rxBytes:o.network.rx,txBytes:o.network.tx,rxMB:(o.network.rx/1048576).toFixed(1),txMB:(o.network.tx/1048576).toFixed(1)},disk:{readMB:0,writeMB:0}},status:o.status};w=r}}if(!r||Object.keys(r).length===0){R.innerHTML='
';try{const e=await(await fetch(`/api/v1/monitoring/history/${encodeURIComponent(r)}?startTime=${u}&endTime=${f}`)).json();if(!e.success)throw new Error(e.error||"Failed to load history");const o=e.samples||[],a=e.tier||"raw";if(o.length===0){D.innerHTML=`
\u{1F4CA}No data for the last ${m}. Tier: ${p(a)}.
\u{1F3E5}No health checks configured. Go to the Configure tab to add services.
';return}const b=Object.values(m.status);let p='
';p+='
',p+='
Service
Status
',p+='
Uptime 24h
Uptime 7d
',p+='
Avg Response
Last Check
';for(const v of b){const s=v.status==="up",l=s?"var(--dot-ok)":"var(--dot-bad)",r=v.uptime?.["24h"]??"-",f=v.uptime?.["7d"]??"-",u=v.avgResponseTime!=null?Math.round(v.avgResponseTime)+"ms":"-",t=v.timestamp?timeAgo(v.timestamp):"-";p+=`
';for(const r of l){const f=r.status==="resolved",u=f&&r.duration?r.duration<6e4?Math.round(r.duration/1e3)+"s":Math.round(r.duration/6e4)+"m":"-";v+='
',v+=`
${escapeHtml(r.serviceId)}
`,v+=`
${escapeHtml(r.type)}
`,v+=`
${L(r.severity)}
`,v+=`
${r.status}
`,v+=`
${u}
`,v+=`
${timeAgo(r.createdAt)}
`,v+="
"}v+="
"}R.innerHTML=v||'
\u{1F6A8}No incidents recorded yet.
'}catch(y){R.innerHTML=`
Failed: ${escapeHtml(y.message)}
`}}async function O(){try{const m=await(await fetch("/api/v1/health-checks/status")).json(),b=m.success&&m.status?Object.values(m.status):[];if(b.length===0){z.innerHTML='
\u2699\uFE0FNo health checks configured yet. Click "Add Health Check" below.
';return}let p='
';p+='
Service
Status
SLA Target
Actions
';for(const v of b){const s=v.status==="up";p+='
',p+=`
${escapeHtml(v.name||v.serviceId)}
`,p+=`
${s?"Up":"Down"}
`,p+=`
${v.sla?.target?v.sla.target+"%":"-"}
`,p+='
',p+=``,p+=``,p+="
"}p+="
",z.innerHTML=p}catch(y){z.innerHTML=`
Failed: ${escapeHtml(y.message)}
`}}function x(y,m,b,p,v,s,l){$=y||null,w.textContent=y?"Edit Health Check":"Add Health Check",document.getElementById("health-form-id").value=y||"",document.getElementById("health-form-id").disabled=!!y,document.getElementById("health-form-name").value=m||"",document.getElementById("health-form-url").value=b||"",document.getElementById("health-form-timeout").value=p||1e4,document.getElementById("health-form-codes").value=v||"200",document.getElementById("health-form-sla").value=s||99.9,document.getElementById("health-form-slow").value=l||5e3,T.style.display="",g.style.display="none"}function D(){T.style.display="none",g.style.display="",$=null}g?.addEventListener("click",()=>x("","","",1e4,"200",99.9,5e3)),H?.addEventListener("click",D),E?.addEventListener("click",async()=>{const y=$||document.getElementById("health-form-id").value.trim();if(!y)return showNotification("Service ID is required","warning");const m=document.getElementById("health-form-url").value.trim();if(!m)return showNotification("URL is required","warning");const b=document.getElementById("health-form-codes").value.split(",").map(v=>parseInt(v.trim())).filter(Boolean),p={name:document.getElementById("health-form-name").value.trim()||y,url:m,timeout:parseInt(document.getElementById("health-form-timeout").value)||1e4,expectedStatusCodes:b.length?b:[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 s=await(await secureFetch(`/api/v1/health-checks/${encodeURIComponent(y)}/configure`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(p)})).json();if(!s.success)throw new Error(s.error||"Save failed");D(),O(),j()}catch(v){showNotification("Error: "+v.message,"error")}finally{E.textContent="Save",E.disabled=!1}}),document.addEventListener("health-edit",async y=>{const m=y.detail;x(m,"","",1e4,"200",99.9,5e3)}),document.addEventListener("health-delete",async y=>{const m=y.detail;if(confirm(`Delete health check for "${m}"?`))try{const p=await(await secureFetch(`/api/v1/health-checks/${encodeURIComponent(m)}/configure`,{method:"DELETE"})).json();if(!p.success)throw new Error(p.error);O(),j()}catch(b){showNotification("Error: "+b.message,"error")}}),S?.addEventListener("click",()=>{h?.classList.add("show"),j()}),wireModal(h,A),k?.addEventListener("click",j),document.querySelector('[data-panel="health-incidents"]')?.addEventListener("click",M),document.querySelector('[data-panel="health-config"]')?.addEventListener("click",O)})(),(function(){injectModal("updates-modal",`
\u2B06\uFE0F Update Management
Check for container image updates, apply them, and manage rollbacks.
\u{1F4E6} Click "Check for Updates" to scan containers.
Loading update history...
\u{1F916} Loading auto-update configuration...
DashCaddy
Loading...
Update available
New version:
\u{1F4E6}No self-update history.
`);const h=document.getElementById("updates-modal"),S=document.getElementById("updates-btn"),A=document.getElementById("updates-cancel"),k=document.getElementById("updates-check-btn"),N=document.getElementById("updates-available-container"),R=document.getElementById("updates-history-container"),z=document.getElementById("updates-auto-container"),P=document.getElementById("updates-last-check");async function g(){try{const t=await(await fetch("/api/v1/updates/available")).json();if(!t.success)throw new Error(t.error);const e=t.updates||[];if(e.length===0){N.innerHTML='
';for(const n of e){const i=(()=>{const c=window.APPS||[];for(const C of c)if(C.containerId===n.containerId||C.name===n.containerName||C.id===n.containerName)return C.id;return n.containerName})();o+=`
`,o+=`
${escapeHtml(n.containerName)}
`,o+=`
${escapeHtml(n.imageName)}
`,o+=`
${escapeHtml(n.currentDigest)}
`,o+=`
${escapeHtml(n.latestDigest)}
`,o+='
',o+=``,o+=``,o+="
"}o+="
",N.innerHTML=o,P.textContent=e.length+" update(s) available";const a=document.getElementById("updates-count-badge"),d=document.getElementById("updates-update-all-btn");a&&(a.textContent=e.length+" pending",a.style.display=""),d&&e.length>0&&(d.style.display=""),window._pendingUpdates=e,N.querySelectorAll(".update-now-btn").forEach(n=>{n.addEventListener("click",async()=>{const i=n.dataset.id,c=n.dataset.name;if(confirm(`Update "${c}" to the latest version? The container will restart.`)){n.textContent="Updating...",n.disabled=!0;try{const B=await(await secureFetch(`/api/v1/updates/update/${encodeURIComponent(i)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({autoRollback:!0})})).json();if(B.success)n.textContent="Done!",n.style.background="var(--ok-fg)",setTimeout(()=>g(),2e3);else throw new Error(B.error||"Update failed")}catch(C){n.textContent="Failed",n.style.color="var(--bad-fg)",showNotification("Update error: "+C.message,"error"),setTimeout(()=>{n.textContent="Update",n.disabled=!1,n.style.color="",n.style.background=""},3e3)}}})}),N.querySelectorAll(".rollback-btn").forEach(n=>{n.addEventListener("click",async()=>{const i=n.dataset.id,c=n.dataset.name;if(confirm(`Rollback "${c}" to its previous version?`)){n.textContent="Rolling back...",n.disabled=!0;try{const B=await(await secureFetch(`/api/v1/updates/rollback/${encodeURIComponent(i)}`,{method:"POST"})).json();if(B.success)n.textContent="Rolled back!",setTimeout(()=>g(),2e3);else throw new Error(B.error||"Rollback failed")}catch(C){n.textContent="Failed",showNotification("Rollback error: "+C.message,"error"),setTimeout(()=>{n.textContent="Rollback",n.disabled=!1},3e3)}}})})}catch(u){N.innerHTML=`
Failed: ${escapeHtml(u.message)}
`}}async function T(){const u=window._pendingUpdates||[];if(!u.length)return;const t=document.getElementById("updates-update-all-btn");if(!confirm(`Update all ${u.length} containers? Each will restart.`))return;t.textContent="\u23F3 Updating...",t.disabled=!0;let e=0,o=0;for(const a of u)try{(await(await secureFetch(`/api/v1/updates/update/${encodeURIComponent(a.containerId)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({autoRollback:!0})})).json()).success?e++:o++}catch{o++}t.textContent="\u2705 Done",showNotification(`Update all: ${e} succeeded, ${o} failed.`,e>0&&o===0?"success":"error"),setTimeout(()=>{t.textContent="\u2B06\uFE0F Update All",t.disabled=!1,g()},3e3)}document.getElementById("updates-update-all-btn")?.addEventListener("click",T);async function w(){k.textContent="\u{1F50D} Checking...",k.disabled=!0;try{const t=await(await secureFetch("/api/v1/updates/check",{method:"POST"})).json();if(!t.success)throw new Error(t.error);k.textContent="\u2705 Done!",await g()}catch(u){k.textContent="\u274C Failed",showNotification("Check error: "+u.message,"error")}setTimeout(()=>{k.textContent="\u{1F50D} Check for Updates",k.disabled=!1},3e3)}async function H(){try{R.innerHTML='
Auto-updates run during maintenance window (default 2AM-4AM). Daily = every day, Weekly = Sundays, Monthly = 1st of month.
';n+='
',n+='
Container
Schedule
Window
Rollback
Last Run
Actions
';for(const i of a){const c=i.name||i.Names?.[0]?.replace(/^\//,"")||i.Id?.substring(0,12),C=i.containerId||i.Id,B=d[C]||{},F=B.enabled?B.schedule||"weekly":"",q=B.autoRollback!==!1,U=B.maintenanceWindow||"",G=B.lastAutoUpdate?timeAgo(B.lastAutoUpdate):"Never";n+=`
`}}const $=document.getElementById("dashcaddy-current-version"),I=document.getElementById("dashcaddy-update-badge"),L=document.getElementById("dashcaddy-update-details"),j=document.getElementById("dashcaddy-new-version"),M=document.getElementById("dashcaddy-changelog"),O=document.getElementById("dashcaddy-apply-btn"),x=document.getElementById("dashcaddy-check-btn"),D=document.getElementById("dashcaddy-rollback-btn"),y=document.getElementById("dashcaddy-status-bar"),m=document.getElementById("dashcaddy-history-container");let b=null;function p(u,t){y&&(y.style.display="block",y.style.background=t==="error"?"var(--bad-bg)":t==="success"?"var(--ok-bg)":"var(--bg)",y.style.color=t==="error"?"var(--bad-fg)":t==="success"?"var(--ok-fg)":"var(--fg)",y.textContent=u)}async function v(){try{const t=await(await fetch("/api/v1/system/version")).json();if(t.success){const e=t.commit&&t.commit!=="unknown"?t.commit:null;$.textContent="v"+t.version+(e?" ("+e.substring(0,7)+")":"")}}catch{$.textContent="Unable to fetch version"}}async function s(u){u||(x.textContent="Checking...",x.disabled=!0);try{const e=await(await fetch("/api/v1/system/update-check")).json();if(b=e,e.success&&e.available&&e.remote){I.style.display="",L.style.display="",j.textContent="v"+e.remote.version,M.textContent=e.remote.changelog||"No changelog available.";const o=document.getElementById("updates-btn");if(o&&!o.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);",o.style.position="relative",o.appendChild(d)}const a=document.getElementById("updates-dashcaddy-tab");if(a&&!a.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;",a.appendChild(d)}}else I.style.display="none",L.style.display="none",await v(),u||p("You are running the latest version.","success");u||(x.textContent="Check for Updates",x.disabled=!1)}catch(t){u||(p("Failed to check: "+t.message,"error"),x.textContent="Check for Updates",x.disabled=!1)}}async function l(){if(!confirm("Apply DashCaddy update? The API container will restart."))return!1;O.textContent="Updating...",O.disabled=!0,p("Downloading and applying update...","info");try{const t=await(await secureFetch("/api/v1/system/update-apply",{method:"POST"})).json();if(t.success)return p("Update initiated: v"+(t.fromVersion||"?")+" \u2192 v"+(t.toVersion||"?")+". The container will restart shortly.","success"),O.textContent="Applied!",document.querySelectorAll(".update-dot").forEach(e=>e.remove()),!0;throw new Error(t.error||"Update failed")}catch(u){throw p("Update failed: "+u.message,"error"),O.textContent="Update Now",O.disabled=!1,u}}async function r(){try{const t=await(await fetch("/api/v1/system/update-history")).json(),e=t.success&&t.history?t.history:[];if(e.length===0){m.innerHTML='
\u{1F4E6}No self-update history.
';return}let o='
';o+='
When
Version
From
Status
';for(const a of e){const d=a.status==="success"?"\u2713 success":a.status==="pending"?"\u23F3 pending":a.status==="partial"?"\u26A0 partial":"\u2717 "+a.status,n=a.status==="success"?"var(--ok-fg)":a.status==="pending"?"var(--muted)":"var(--bad-fg)";o+='
"}}async function f(){try{const t=await(await fetch("/api/v1/system/rollback-versions")).json(),e=t.success&&t.versions?t.versions:[];if(e.length===0){showNotification("No rollback versions available.","info");return}const o=prompt(`Available rollback versions:
`+e.join(`
`)+`
Enter version to rollback to:`);if(!o)return;if(!e.includes(o)){showNotification("Invalid version: "+o,"error");return}if(!confirm("Rollback DashCaddy to v"+o+"? The container will restart."))return;p("Rolling back to v"+o+"...","info");const d=await(await secureFetch("/api/v1/system/rollback",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({version:o})})).json();if(d.success)p("Rollback to v"+o+" initiated. Container will restart.","success");else throw new Error(d.error||"Rollback failed")}catch(u){p("Rollback failed: "+u.message,"error")}}x?.addEventListener("click",()=>s(!1)),O?.addEventListener("click",()=>l().catch(()=>{})),D?.addEventListener("click",f),k?.addEventListener("click",w),S?.addEventListener("click",()=>{h?.classList.add("show"),g()}),wireModal(h,A),window.openUpdateModal=function(u){h?.classList.add("show"),g().then(()=>{if(!u)return;const t=N.querySelector(`[data-app-id="${u}"]`);t&&(t.scrollIntoView({behavior:"smooth",block:"center"}),t.style.background="rgba(249,115,22,0.15)",setTimeout(()=>{t.style.background=""},3e3))})},document.querySelector('[data-panel="updates-history"]')?.addEventListener("click",H),document.querySelector('[data-panel="updates-auto"]')?.addEventListener("click",E),document.querySelector('[data-panel="updates-dashcaddy"]')?.addEventListener("click",()=>{v(),r(),b||s(!0)}),window.dcApplyUpdate=l,window.dcCheckForUpdate=s,setTimeout(()=>s(!0),5e3)})(),(function(){injectModal("docker-resources-modal",`
",P.innerHTML=w,P.querySelectorAll(".dr-vol-del").forEach(H=>{H.addEventListener("click",async()=>{if(confirm(`Delete volume "${H.dataset.name}"? Data will be lost.`)){H.textContent="...",H.disabled=!0;try{await deleteAPI(`/api/v1/docker/volumes/${encodeURIComponent(H.dataset.name)}?force=true`),N()}catch(E){showNotification("Delete failed: "+E.message,"error"),H.textContent="Delete",H.disabled=!1}}})})}catch(g){P.innerHTML=`
Failed: ${escapeHtml(g.message)}
`}}document.getElementById("dr-vol-create")?.addEventListener("click",async()=>{const P=document.getElementById("dr-vol-name"),g=P.value.trim();if(!g){showNotification("Enter a volume name","warning");return}try{await postJSON("/api/v1/docker/volumes",{name:g}),P.value="",showNotification(`Volume "${g}" created`,"success"),N()}catch(T){showNotification("Create failed: "+T.message,"error")}});async function R(){const P=document.getElementById("dr-net-list");try{const T=(await getJSON("/api/v1/docker/networks")).networks||[];if(T.length===0){P.innerHTML='
\u{1F310}No networks found.
';return}let w='
';w+='
Name
Driver
Scope
Containers
Actions
';for(const H of T){const E=["bridge","host","none"].includes(H.name);w+='
';for(const w of g.results){const H=w.status==="deployed"||w.status==="created"?"\u2705":w.status==="exists"?"\u26A1":w.status==="skipped"?"\u23ED":"\u274C";T+='
`}}S?.addEventListener("click",()=>{h?.classList.add("show"),w(!1)}),wireModal(h,A),k?.addEventListener("click",()=>w(!1)),R?.addEventListener("change",()=>w(!1)),P?.addEventListener("click",()=>w(!0)),N?.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?w(!1):showNotification("Error: "+(E.error||"Clear failed"),"error")}catch(H){showNotification("Error: "+H.message,"error")}})})(),(function(){injectModal("security-modal",`
\u{1F6E1}\uFE0F Security Center
Live events from API, Caddy, fail2ban & shared_bans. Live-tail via SSE.
`}async function y(){try{const l=(await(await fetch("/api/v1/security/hosts")).json()).data?.hosts||[],r=L.value;L.innerHTML=''+l.map(f=>``).join(""),r&&(L.value=r)}catch{}}document.getElementById("sec-host-register-btn").addEventListener("click",b),document.getElementById("sec-hosts-refresh").addEventListener("click",m);async function m(){try{const l=(await(await fetch("/api/v1/security/hosts")).json()).data?.hosts||[];z=l;const r=document.getElementById("sec-hosts-container");if(!l.length){r.innerHTML='
No hosts registered. Click \u2795 Register Host to add one.
id: ${p(f.id)} \xB7
registered ${new Date(f.registered_at).toLocaleDateString()} \xB7
last seen ${f.last_seen_at?new Date(f.last_seen_at).toLocaleString():"never"}
${u}
${f.id==="self"?"":``}
`}).join(""),r.querySelectorAll(".sec-host-del").forEach(f=>{f.addEventListener("click",async()=>{confirm(`Remove host ${f.dataset.id}? Events already received will remain in the store.`)&&(await fetch(`/api/v1/security/hosts/${encodeURIComponent(f.dataset.id)}`,{method:"DELETE"}),m())})})}catch(s){document.getElementById("sec-hosts-container").innerHTML='
Load failed: '+p(s.message)+"
"}}async function b(){const s=prompt("Host id (lowercase, no spaces):");if(!s)return;const l=prompt("Display label:",s)||s,r=prompt('Type ("dashcaddy", "service", or "agent"):',"agent")||"agent";try{const f=await fetch("/api/v1/security/hosts",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:s,label:l,type:r})}),u=await f.json();if(!f.ok){alert("Failed: "+(u?.error?.message||f.statusText));return}alert(`\u2705 Host registered!
id: ${u.data.host.id}
label: ${u.data.host.label}
type: ${u.data.host.type}
\u{1F511} API KEY (save this NOW \u2014 won't be shown again):
${u.data.api_key}
Send this key as: Authorization: Bearer
To endpoint: POST /api/v1/security/events/ingest or /events/batch`),m()}catch(f){alert("Failed: "+f.message)}}function p(s){return String(s).replace(/[&<>"']/g,l=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[l])}function v(s,l){let r;return function(){clearTimeout(r),r=setTimeout(()=>s.apply(this,arguments),l)}}})(),(function(){const h=new ErrorHandler;injectModal("weather-modal",`
Weather Settings
Enter a city name, postal code, or “City, Country”
`);const S="weather-location",A="weather-zip",k="weather-geo",N="weather-unit";!safeGet(S)&&safeGet(A)&&safeSet(S,safeGet(A));function R(){return safeGet(N)||"imperial"}function z(){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 P={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"},g={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"},T=["N","NNE","NE","ENE","E","ESE","SE","SSE","S","SSW","SW","WSW","W","WNW","NW","NNW"];function w(M){return T[Math.round(M/22.5)%16]}async function H(M){const O=safeGet(k);if(O)try{const b=JSON.parse(O);if(b.query===M)return b}catch{}const x=await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(M)}&count=1&language=en&format=json`);if(!x.ok)throw new Error("Geocoding failed");const D=await x.json();if(!D.results||!D.results.length)throw new Error("Location not found");const y=D.results[0],m={query:M,lat:y.latitude,lon:y.longitude,city:y.name,state:y.admin1||"",country:y.country||"",countryCode:y.country_code||""};return safeSet(k,JSON.stringify(m)),m}function E(M){return M.countryCode==="US"&&M.state?`${M.city}, ${M.state}`:M.country?`${M.city}, ${M.country}`:M.city}async function $(M){try{const O=await H(M),x=R(),D=x==="metric"?"celsius":"fahrenheit",y=x==="metric"?"kmh":"mph",m=`https://api.open-meteo.com/v1/forecast?latitude=${O.lat}&longitude=${O.lon}¤t=temperature_2m,weather_code,wind_speed_10m,wind_direction_10m&temperature_unit=${D}&wind_speed_unit=${y}`,b=await fetch(m);if(!b.ok)throw new Error("Weather fetch failed");const v=(await b.json()).current,s=v.weather_code;return{temp:Math.round(v.temperature_2m),condition:P[s]||"Unknown",icon:g[s]||"\u{1F324}\uFE0F",locationStr:E(O),windSpeed:Math.round(v.wind_speed_10m),windDir:w(v.wind_direction_10m),unit:x}}catch(O){return console.warn("Weather fetch failed:",O),null}}async function I(){const M=z();if(!M.icon||!M.temp||!M.condition||!M.location||!M.wind){console.warn("Weather widget elements not found");return}const O=safeGet(S);if(!O){M.location.textContent="Set Location",M.temp.textContent="--\xB0",M.condition.textContent="Click \u2699\uFE0F to configure",M.wind.textContent="--",M.icon.innerHTML='\u{1F324}\uFE0F';return}try{const x=await $(O);if(x){const D=x.unit==="metric"?"\xB0C":"\xB0F",y=x.unit==="metric"?"km/h":"mph";M.location.textContent=x.locationStr,M.temp.textContent=`${x.temp}${D}`,M.condition.textContent=x.condition,M.wind.textContent=`Wind: ${x.windSpeed} ${y} ${x.windDir}`,M.icon.innerHTML=`${escapeHtml(x.icon)}`}}catch(x){h.logError("[Weather] Update Error",x,{function:"updateWeather"}),M.location.textContent="Weather Error",M.temp.textContent="Error",M.condition.textContent="Failed to load",M.wind.textContent="--"}}const L=document.getElementById("weather-modal"),j=document.getElementById("weather-location-input");document.getElementById("weather-settings")?.addEventListener("click",()=>{j.value=safeGet(S)||"";const M=R(),O=L.querySelector(`input[name="weather-unit-radio"][value="${M}"]`);O&&(O.checked=!0),L.classList.add("show"),j.focus()}),document.getElementById("weather-cancel")?.addEventListener("click",()=>{L.classList.remove("show")}),document.getElementById("weather-save")?.addEventListener("click",()=>{const M=j.value.trim();if(M){safeGet(S)!==M&&safeSet(k,""),safeSet(S,M);const x=L.querySelector('input[name="weather-unit-radio"]:checked'),D=x?x.value:"imperial",y=R();safeSet(N,D),y!==D&&safeSet(k,""),L.classList.remove("show"),I()}else showNotification("Please enter a location (e.g., Hamburg, London, 90210)","warning")}),wireModal(L),document.addEventListener("keydown",M=>{M.key==="Escape"&&L.classList.contains("show")&&L.classList.remove("show")}),I(),setInterval(I,DC.POLL.WEATHER)})(),(function(){const h=document.getElementById("clock-widget"),S=document.getElementById("clock-render");if(!h||!S)return;const A=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],k=["January","February","March","April","May","June","July","August","September","October","November","December"],N=["XII","I","II","III","IV","V","VI","VII","VIII","IX","X","XI"];let R=safeGet("clock-style")||"default",z=-1,P=!1,g="",T="",w=null,H=null;function E(t){if(P||safeGet("clock-chimes")!=="true")return;P=!0;const e=parseInt(safeGet("clock-chime-volume")||"50",10)/100;let o=0;function a(){if(o>=t){P=!1;return}const d=new Audio("/assets/sounds/church-bell.mp3");d.volume=e,d.play().catch(()=>{}),o++,o{P=!1},2500)}a()}function $(t){return A[t.getDay()]+", "+k[t.getMonth()]+" "+t.getDate()+", "+t.getFullYear()}function I(){T="",w=null}function L(){return T!=="digital"&&(S.innerHTML='
`).join("")}async function M(){try{const p=await(await fetch("/api/v1/license/status")).json();p.success&&(j(p.license),D(p.license))}catch(b){console.warn("Failed to load license status:",b.message)}}async function O(){const b=S.value.trim();if(!b){I("Please enter a license code.");return}$(),A.disabled=!0,A.textContent="Activating...";try{const v=await(await secureFetch("/api/v1/license/activate",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:b})})).json();v.success?(L(v.message),S.value="",j(v.license),showNotification("License activated! Premium features unlocked.","success",5e3),D(v.license)):I(v.error||"Activation failed")}catch(p){I("Network error: "+p.message)}finally{A.disabled=!1,A.textContent="Activate"}}async function x(){if(confirm("Deactivate your license? You can reuse the code on another machine.")){k.disabled=!0,k.textContent="Deactivating...";try{const p=await(await secureFetch("/api/v1/license/deactivate",{method:"POST"})).json();p.success?(L(p.message),await M(),showNotification("License deactivated.","info",3e3),D({active:!1})):I(p.error||"Deactivation failed")}catch(b){I("Network error: "+b.message)}finally{k.disabled=!1,k.textContent="Deactivate"}}}function D(b){const p=document.getElementById("license-status-topbar"),v=document.getElementById("license-topbar-icon"),s=document.getElementById("license-topbar-text"),l=document.getElementById("license-topbar-time");if(p)if(p.className="license-status-topbar "+(b.active?"premium":"free"),b.active)if(v.textContent="\u2605",s.textContent="PREMIUM",b.lifetime)l.textContent="\xB7 LIFETIME";else{const r=b.daysRemaining;l.textContent=r!=null?"\xB7 "+r+"d remaining":""}else v.textContent="\u2606",s.textContent=b.expired?"EXPIRED":"FREE TIER",l.textContent=""}function y(){$(),M(),h.classList.add("show")}S.addEventListener("input",function(){let b=this.value.toUpperCase().replace(/[^A-Z0-9-]/g,"");if(b.length>this._prevLength&&(b=b.replace(/-/g,""),b.length>2&&!b.startsWith("DC")&&(b="DC"+b),b.startsWith("DC")&&b.length>2)){const p=["DC"],v=b.substring(2);for(let s=0;s{b.key==="Enter"&&O()}),wireModal(h,document.getElementById("license-cancel"));const m=document.getElementById("license-status-topbar");m&&m.addEventListener("click",()=>window.openLicenseModal&&window.openLicenseModal()),window.openLicenseModal=y,window.checkPremiumFeature=async function(b){try{return(await(await fetch(`/api/v1/license/feature/${b}`)).json()).available}catch{return!1}},M().then(b=>{E&&D(E)})})();