`);const f=document.getElementById("service-creds-modal");let u=null;const y=["sonarr","radarr","prowlarr","overseerr"],l=["sonarr","radarr"];function r(n){return n.externalUrl||n.url||""}function h(n){const d=document.getElementById("svc-creds-error");d.textContent=n,d.style.display=""}function a(){const n=document.getElementById("svc-creds-error");n.textContent="",n.style.display="none"}window.openServiceCredsModal=async function(n){u=n,a();const d=document.getElementById("svc-creds-title"),g=document.getElementById("svc-creds-desc"),e=document.getElementById("svc-creds-seedhost"),s=document.getElementById("svc-creds-apikey"),p=document.getElementById("svc-creds-basic"),t=document.getElementById("svc-creds-quality");d.textContent=n.name+" Credentials";const c=!!n.isExternal,v=y.includes(n.id)||y.includes(n.appTemplate),i=l.includes(n.id)||l.includes(n.appTemplate);e.style.display=c?"":"none",s.style.display=v?"":"none",t.style.display=i?"":"none",p.style.display=c?"none":"";const E=document.getElementById("svc-quality-select");E.innerHTML='',document.getElementById("svc-quality-status").textContent="",c?(g.textContent="Seedhost credentials auto-login past the HTTP prompt. API key bypasses the app login.",document.getElementById("svc-seedhost-pass").placeholder=`Password for ${n.name}`):v?g.textContent="API key bypasses the app login screen automatically.":g.textContent="Credentials are injected automatically when accessing this service.",await b(n),f.classList.add("show")};async function b(n){const d=document.getElementById("svc-creds-dot"),g=document.getElementById("svc-creds-status"),e=document.getElementById("svc-creds-clear");let s=!1;if(n.isExternal){try{const c=await(await fetch(`/api/v1/seedhost-creds?serviceId=${n.id}`)).json();c.success?(document.getElementById("svc-seedhost-user").value=c.username||"",c.hasCredentials&&(s=!0)):document.getElementById("svc-seedhost-user").value=""}catch{}document.getElementById("svc-seedhost-pass").value=""}try{const c=await(await fetch(`/api/v1/services/${n.id}/credentials`)).json();c.success&&(c.hasApiKey?(document.getElementById("svc-apikey-input").value="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",s=!0):document.getElementById("svc-apikey-input").value="",c.hasBasicAuth&&!n.isExternal?(document.getElementById("svc-basic-user").value=c.username||"",s=!0):document.getElementById("svc-basic-user").value="")}catch{}document.getElementById("svc-basic-pass")&&(document.getElementById("svc-basic-pass").value="");const p=n.id||n.appTemplate;if(l.includes(p)&&await m(n),s){d.style.background="var(--ok-fg, #74dfc4)",g.style.color="var(--ok-fg, #74dfc4)",g.textContent="Credentials stored",e.style.display="";const t=document.getElementById(`creds-btn-${n.id}`);t&&t.classList.add("has-creds")}else d.style.background="var(--muted)",g.style.color="var(--muted)",g.textContent="No credentials stored",e.style.display="none"}async function m(n){const d=document.getElementById("svc-quality-select"),g=document.getElementById("svc-quality-status"),e=n.id||n.appTemplate,s=r(n);if(!s){d.innerHTML='';return}d.innerHTML='',g.textContent="";try{const p=new URLSearchParams({service:e,url:s}),c=await(await fetch(`/api/v1/arr/quality-profiles?${p}`)).json();if(!c.success||!c.profiles?.length){d.innerHTML='';return}d.innerHTML="";for(const v of c.profiles){const i=document.createElement("option");i.value=v.id,i.textContent=v.name,d.appendChild(i)}if(c.storedProfileId&&(d.value=String(c.storedProfileId)),!d.value){const v=c.profiles.find(i=>/720/i.test(i.name));v&&(d.value=String(v.id))}!d.value&&c.profiles.length&&(d.value=String(c.profiles[0].id)),g.innerHTML=`${c.profiles.length} profiles loaded`}catch(p){d.innerHTML='',g.innerHTML=`Error: ${p.message}`}}document.getElementById("svc-quality-fetch")?.addEventListener("click",async()=>{if(!u)return;const n=u.id||u.appTemplate,d=r(u),e=document.getElementById("svc-apikey-input")?.value.trim(),s=document.getElementById("svc-quality-select"),p=document.getElementById("svc-quality-status");if(!d){p.innerHTML='No service URL available';return}if(!e||e==="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022"){p.innerHTML='Enter an API key first';return}s.innerHTML='',p.textContent="";try{const t=new URLSearchParams({service:n,url:d,apiKey:e}),v=await(await fetch(`/api/v1/arr/quality-profiles?${t}`)).json();if(!v.success){s.innerHTML='',p.innerHTML=`${v.error||"Failed to fetch profiles"}`;return}if(!v.profiles?.length){s.innerHTML='';return}s.innerHTML="";for(const E of v.profiles){const T=document.createElement("option");T.value=E.id,T.textContent=E.name,s.appendChild(T)}const i=v.profiles.find(E=>/720/i.test(E.name));i?s.value=String(i.id):v.profiles.length&&(s.value=String(v.profiles[0].id)),p.innerHTML=`${v.profiles.length} profiles loaded`}catch(t){s.innerHTML='',p.innerHTML=`${t.message}`}}),document.getElementById("svc-creds-save")?.addEventListener("click",async()=>{if(!u)return;const n=document.getElementById("svc-creds-save");n.textContent="Saving...",n.disabled=!0,a();try{const d=y.includes(u.id)||y.includes(u.appTemplate),g=u.id||u.appTemplate;if(u.isExternal){const p=document.getElementById("svc-seedhost-user").value.trim(),t=document.getElementById("svc-seedhost-pass").value;p&&await secureFetch("/api/v1/seedhost-creds",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:p,password:t||void 0,serviceId:u.id})})}const s=document.getElementById("svc-apikey-input")?.value.trim();if(s&&s!=="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022")if(d){const p=r(u),t=document.getElementById("svc-quality-select"),c=t?.value?parseInt(t.value):void 0,v=t?.selectedOptions?.[0]?.textContent||void 0,E=await(await secureFetch("/api/v1/arr/credentials",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({service:g,apiKey:s,url:p||void 0,qualityProfileId:c||void 0,qualityProfileName:v||void 0})})).json();if(!E.success){h(E.error||"Failed to save API key"),n.textContent="Save",n.disabled=!1;return}E.connectionTest&&!E.connectionTest.success&&h(`API key saved but connection test failed: ${E.connectionTest.error}`)}else await secureFetch(`/api/v1/services/${u.id}/credentials`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({apiKey:s})});else if(d&&l.includes(g)){const p=document.getElementById("svc-quality-select"),t=p?.value?parseInt(p.value):void 0,c=p?.selectedOptions?.[0]?.textContent||void 0;t&&await secureFetch("/api/v1/arr/quality-profiles",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({service:g,qualityProfileId:t,qualityProfileName:c})})}if(!u.isExternal){const p=document.getElementById("svc-basic-user").value.trim(),t=document.getElementById("svc-basic-pass").value;p&&t&&await secureFetch(`/api/v1/services/${u.id}/credentials`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:p,password:t})})}await b(u)}catch(d){o.logError("[ServiceCredentials] Save",d,{function:"saveCredentials"}),h("Failed to save: "+(d.message||"Unknown error"))}n.textContent="Save",n.disabled=!1}),document.getElementById("svc-creds-clear")?.addEventListener("click",async()=>{if(u&&confirm(`Remove stored credentials for ${u.name}?`)){a();try{const n=u.id||u.appTemplate,d=y.includes(n);u.isExternal&&await secureFetch(`/api/v1/seedhost-creds?serviceId=${u.id}`,{method:"DELETE"}),await secureFetch(`/api/v1/services/${u.id}/credentials`,{method:"DELETE"}),d&&await secureFetch(`/api/v1/arr/credentials/${n}`,{method:"DELETE"});const g=document.getElementById(`creds-btn-${u.id}`);g&&g.classList.remove("has-creds"),await b(u)}catch(n){o.logError("[ServiceCredentials] Clear",n,{function:"clearCredentials"}),h("Failed to clear: "+(n.message||"Unknown error"))}}}),document.getElementById("svc-creds-close")?.addEventListener("click",()=>{f.classList.remove("show"),u=null}),f?.addEventListener("click",n=>{n.target===f&&(f.classList.remove("show"),u=null)}),window.refreshCredsButtons=async function(){try{for(const n of window.APPS||[]){if(!n.isExternal&&!n.appTemplate&&!n.url)continue;let d=!1;if(n.isExternal)try{const s=await(await fetch(`/api/v1/seedhost-creds?serviceId=${n.id}`)).json();s.success&&s.hasCredentials&&(d=!0)}catch{}try{const s=await(await fetch(`/api/v1/services/${n.id}/credentials`)).json();s.success&&(s.hasApiKey||s.hasBasicAuth)&&(d=!0)}catch{}const g=document.getElementById(`creds-btn-${n.id}`);g&&g.classList.toggle("has-creds",d)}}catch{}}})(),(function(){const o=new ErrorHandler;injectModal("totp-settings-modal",`
+
`);const y=document.getElementById("service-creds-modal");let p=null;const v=["sonarr","radarr","prowlarr","overseerr"],i=["sonarr","radarr"];function s(o){return o.externalUrl||o.url||""}function m(o){const l=document.getElementById("svc-creds-error");l.textContent=o,l.style.display=""}function t(){const o=document.getElementById("svc-creds-error");o.textContent="",o.style.display="none"}window.openServiceCredsModal=async function(o){p=o,t();const l=document.getElementById("svc-creds-title"),g=document.getElementById("svc-creds-desc"),e=document.getElementById("svc-creds-seedhost"),r=document.getElementById("svc-creds-apikey"),f=document.getElementById("svc-creds-basic"),n=document.getElementById("svc-creds-quality");l.textContent=o.name+" Credentials";const u=!!o.isExternal,b=v.includes(o.id)||v.includes(o.appTemplate),d=i.includes(o.id)||i.includes(o.appTemplate);e.style.display=u?"":"none",r.style.display=b?"":"none",n.style.display=d?"":"none",f.style.display=u?"none":"";const E=document.getElementById("svc-quality-select");E.innerHTML='',document.getElementById("svc-quality-status").textContent="",u?(g.textContent="Seedhost credentials auto-login past the HTTP prompt. API key bypasses the app login.",document.getElementById("svc-seedhost-pass").placeholder=`Password for ${o.name}`):b?g.textContent="API key bypasses the app login screen automatically.":g.textContent="Credentials are injected automatically when accessing this service.",await h(o),y.classList.add("show")};async function h(o){const l=document.getElementById("svc-creds-dot"),g=document.getElementById("svc-creds-status"),e=document.getElementById("svc-creds-clear");let r=!1;if(o.isExternal){try{const u=await(await fetch(`/api/v1/seedhost-creds?serviceId=${o.id}`)).json();u.success?(document.getElementById("svc-seedhost-user").value=u.username||"",u.hasCredentials&&(r=!0)):document.getElementById("svc-seedhost-user").value=""}catch{}document.getElementById("svc-seedhost-pass").value=""}try{const u=await(await fetch(`/api/v1/services/${o.id}/credentials`)).json();u.success&&(u.hasApiKey?(document.getElementById("svc-apikey-input").value="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",r=!0):document.getElementById("svc-apikey-input").value="",u.hasBasicAuth&&!o.isExternal?(document.getElementById("svc-basic-user").value=u.username||"",r=!0):document.getElementById("svc-basic-user").value="")}catch{}document.getElementById("svc-basic-pass")&&(document.getElementById("svc-basic-pass").value="");const f=o.id||o.appTemplate;if(i.includes(f)&&await c(o),r){l.style.background="var(--ok-fg, #74dfc4)",g.style.color="var(--ok-fg, #74dfc4)",g.textContent="Credentials stored",e.style.display="";const n=document.getElementById(`creds-btn-${o.id}`);n&&n.classList.add("has-creds")}else l.style.background="var(--muted)",g.style.color="var(--muted)",g.textContent="No credentials stored",e.style.display="none"}async function c(o){const l=document.getElementById("svc-quality-select"),g=document.getElementById("svc-quality-status"),e=o.id||o.appTemplate,r=s(o);if(!r){l.innerHTML='';return}l.innerHTML='',g.textContent="";try{const f=new URLSearchParams({service:e,url:r}),u=await(await fetch(`/api/v1/arr/quality-profiles?${f}`)).json();if(!u.success||!u.profiles?.length){l.innerHTML='';return}l.innerHTML="";for(const b of u.profiles){const d=document.createElement("option");d.value=b.id,d.textContent=b.name,l.appendChild(d)}if(u.storedProfileId&&(l.value=String(u.storedProfileId)),!l.value){const b=u.profiles.find(d=>/720/i.test(d.name));b&&(l.value=String(b.id))}!l.value&&u.profiles.length&&(l.value=String(u.profiles[0].id)),g.innerHTML=`${u.profiles.length} profiles loaded`}catch(f){l.innerHTML='',g.innerHTML=`Error: ${f.message}`}}document.getElementById("svc-quality-fetch")?.addEventListener("click",async()=>{if(!p)return;const o=p.id||p.appTemplate,l=s(p),e=document.getElementById("svc-apikey-input")?.value.trim(),r=document.getElementById("svc-quality-select"),f=document.getElementById("svc-quality-status");if(!l){f.innerHTML='No service URL available';return}if(!e||e==="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022"){f.innerHTML='Enter an API key first';return}r.innerHTML='',f.textContent="";try{const n=new URLSearchParams({service:o,url:l,apiKey:e}),b=await(await fetch(`/api/v1/arr/quality-profiles?${n}`)).json();if(!b.success){r.innerHTML='',f.innerHTML=`${b.error||"Failed to fetch profiles"}`;return}if(!b.profiles?.length){r.innerHTML='';return}r.innerHTML="";for(const E of b.profiles){const T=document.createElement("option");T.value=E.id,T.textContent=E.name,r.appendChild(T)}const d=b.profiles.find(E=>/720/i.test(E.name));d?r.value=String(d.id):b.profiles.length&&(r.value=String(b.profiles[0].id)),f.innerHTML=`${b.profiles.length} profiles loaded`}catch(n){r.innerHTML='',f.innerHTML=`${n.message}`}}),document.getElementById("svc-creds-save")?.addEventListener("click",async()=>{if(!p)return;const o=document.getElementById("svc-creds-save");o.textContent="Saving...",o.disabled=!0,t();try{const l=v.includes(p.id)||v.includes(p.appTemplate),g=p.id||p.appTemplate;if(p.isExternal){const f=document.getElementById("svc-seedhost-user").value.trim(),n=document.getElementById("svc-seedhost-pass").value;f&&await secureFetch("/api/v1/seedhost-creds",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:f,password:n||void 0,serviceId:p.id})})}const r=document.getElementById("svc-apikey-input")?.value.trim();if(r&&r!=="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022")if(l){const f=s(p),n=document.getElementById("svc-quality-select"),u=n?.value?parseInt(n.value):void 0,b=n?.selectedOptions?.[0]?.textContent||void 0,E=await(await secureFetch("/api/v1/arr/credentials",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({service:g,apiKey:r,url:f||void 0,qualityProfileId:u||void 0,qualityProfileName:b||void 0})})).json();if(!E.success){m(E.error||"Failed to save API key"),o.textContent="Save",o.disabled=!1;return}E.connectionTest&&!E.connectionTest.success&&m(`API key saved but connection test failed: ${E.connectionTest.error}`)}else await secureFetch(`/api/v1/services/${p.id}/credentials`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({apiKey:r})});else if(l&&i.includes(g)){const f=document.getElementById("svc-quality-select"),n=f?.value?parseInt(f.value):void 0,u=f?.selectedOptions?.[0]?.textContent||void 0;n&&await secureFetch("/api/v1/arr/quality-profiles",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({service:g,qualityProfileId:n,qualityProfileName:u})})}if(!p.isExternal){const f=document.getElementById("svc-basic-user").value.trim(),n=document.getElementById("svc-basic-pass").value;f&&n&&await secureFetch(`/api/v1/services/${p.id}/credentials`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:f,password:n})})}await h(p)}catch(l){a.logError("[ServiceCredentials] Save",l,{function:"saveCredentials"}),m("Failed to save: "+(l.message||"Unknown error"))}o.textContent="Save",o.disabled=!1}),document.getElementById("svc-creds-clear")?.addEventListener("click",async()=>{if(p&&confirm(`Remove stored credentials for ${p.name}?`)){t();try{const o=p.id||p.appTemplate,l=v.includes(o);p.isExternal&&await secureFetch(`/api/v1/seedhost-creds?serviceId=${p.id}`,{method:"DELETE"}),await secureFetch(`/api/v1/services/${p.id}/credentials`,{method:"DELETE"}),l&&await secureFetch(`/api/v1/arr/credentials/${o}`,{method:"DELETE"});const g=document.getElementById(`creds-btn-${p.id}`);g&&g.classList.remove("has-creds"),await h(p)}catch(o){a.logError("[ServiceCredentials] Clear",o,{function:"clearCredentials"}),m("Failed to clear: "+(o.message||"Unknown error"))}}}),document.getElementById("svc-creds-close")?.addEventListener("click",()=>{y.classList.remove("show"),p=null}),y?.addEventListener("click",o=>{o.target===y&&(y.classList.remove("show"),p=null)}),window.refreshCredsButtons=async function(){try{for(const o of window.APPS||[]){if(!o.isExternal&&!o.appTemplate&&!o.url)continue;let l=!1;if(o.isExternal)try{const r=await(await fetch(`/api/v1/seedhost-creds?serviceId=${o.id}`)).json();r.success&&r.hasCredentials&&(l=!0)}catch{}try{const r=await(await fetch(`/api/v1/services/${o.id}/credentials`)).json();r.success&&(r.hasApiKey||r.hasBasicAuth)&&(l=!0)}catch{}const g=document.getElementById(`creds-btn-${o.id}`);g&&g.classList.toggle("has-creds",l)}}catch{}}})(),(function(){const a=new ErrorHandler;injectModal("totp-settings-modal",`
Authentication Settings
@@ -182,11 +182,25 @@
Copy this key into your authenticator app:
-
+
+
+
+
+
+ Save a backup file \u2014 if you ever lose your authenticator,
+ this is the only way to recover without SSH access to the server.
+
+
+
+
+
Show QR code (for mobile authenticator apps)
@@ -240,7 +254,7 @@
-
`);async function f(){try{const r=await(await fetch("/api/v1/totp/config")).json();if(!r.success)return;const{enabled:h,sessionDuration:a,isSetUp:b}=r.config,m=document.getElementById("totp-status-dot"),n=document.getElementById("totp-status-text"),d=document.getElementById("totp-status-banner"),g=document.getElementById("totp-setup-section"),e=document.getElementById("totp-qr-section"),s=document.getElementById("totp-duration-section"),p=document.getElementById("totp-disable-section");h&&b?(m.style.background="var(--ok-fg, #7ef2ff)",d.style.borderColor="var(--ok-fg, #7ef2ff)",d.style.background="color-mix(in srgb, var(--ok-fg) 8%, transparent)",n.textContent="TOTP is active",n.style.color="var(--ok-fg, #7ef2ff)",g.style.display="none",e.style.display="none",s.style.display="block",p.style.display="block",document.getElementById("totp-duration-select").value=a):(m.style.background="var(--muted)",d.style.borderColor="var(--border)",d.style.background="transparent",n.textContent="TOTP is not configured",n.style.color="var(--muted)",g.style.display="block",e.style.display="none",s.style.display="none",p.style.display="none"),y(h&&b,a)}catch(l){console.warn("Failed to load TOTP settings:",l)}}const u={"15m":"15 min","30m":"30 min","1h":"1 hour","2h":"2 hours","4h":"4 hours","8h":"8 hours","12h":"12 hours","24h":"24 hours",never:"Disabled"};function y(l,r){const h=document.getElementById("auth-card"),a=document.getElementById("auth-pill"),b=document.getElementById("auth-dot"),m=document.getElementById("auth-status-text");h&&(l?(h.setAttribute("data-status","on"),a.className="badge on",a.textContent="YES",b.className="dot ok at-bl",m.textContent="Session: "+(u[r]||r)):(h.setAttribute("data-status","off"),a.className="badge off",a.textContent="NO",b.className="dot bad at-bl",m.textContent="Not configured"))}document.getElementById("totp-setup-btn")?.addEventListener("click",async()=>{try{const r=await(await secureFetch("/api/v1/totp/setup",{method:"POST"})).json();r.success&&(document.getElementById("totp-qr-image").src=r.qrCode,document.getElementById("totp-manual-key").textContent=r.manualKey,document.getElementById("totp-setup-section").style.display="none",document.getElementById("totp-qr-section").style.display="block",document.getElementById("totp-setup-code").value="",document.getElementById("totp-setup-error").textContent="",document.getElementById("totp-setup-code").focus())}catch(l){o.logError("[TOTP] Setup Failed",l,{function:"setupTOTP"})}}),document.getElementById("totp-import-btn")?.addEventListener("click",async()=>{const l=document.getElementById("totp-import-key").value.trim(),r=document.getElementById("totp-import-error");if(r.textContent="",!l){r.textContent="Paste a Base32 secret key first";return}try{const a=await(await secureFetch("/api/v1/totp/setup",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({secret:l})})).json();a.success?(r.textContent="",document.getElementById("totp-qr-image").src=a.qrCode,document.getElementById("totp-manual-key").textContent=a.manualKey,document.getElementById("totp-setup-section").style.display="none",document.getElementById("totp-qr-section").style.display="block",document.getElementById("totp-setup-code").value="",document.getElementById("totp-setup-error").textContent="",document.getElementById("totp-setup-code").focus()):r.textContent=a.error||a.message||"Import failed"}catch{r.textContent="Connection error \u2014 try refreshing the page"}}),document.getElementById("totp-copy-key")?.addEventListener("click",()=>{const l=document.getElementById("totp-manual-key").textContent;navigator.clipboard.writeText(l).then(()=>{const r=document.getElementById("totp-copy-key");r.textContent="\u2705",setTimeout(()=>{r.textContent="\u{1F4CB}"},2e3)})}),document.getElementById("totp-confirm-setup")?.addEventListener("click",async()=>{const l=document.getElementById("totp-setup-code").value,r=document.getElementById("totp-setup-error");if(!/^\d{6}$/.test(l)){r.textContent="Enter a 6-digit code";return}try{const a=await(await secureFetch("/api/v1/totp/verify-setup",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:l})})).json();a.success?(r.textContent="",f()):r.textContent=a.error||"Invalid code"}catch{r.textContent="Connection error"}}),document.getElementById("totp-setup-code")?.addEventListener("keydown",l=>{l.key==="Enter"&&document.getElementById("totp-confirm-setup")?.click()}),document.getElementById("totp-duration-select")?.addEventListener("change",async l=>{try{await secureFetch("/api/v1/totp/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sessionDuration:l.target.value})}),f()}catch(r){o.logError("[TOTP] Update Session Duration",r,{function:"updateSessionDuration"})}}),document.getElementById("totp-disable-btn")?.addEventListener("click",async()=>{if(confirm("Disable TOTP authentication? All services will be accessible without a code."))try{(await(await secureFetch("/api/v1/totp/disable",{method:"POST",headers:{"Content-Type":"application/json"},body:"{}"})).json()).success&&f()}catch(l){o.logError("[TOTP] Disable Failed",l,{function:"disableTOTP"})}}),document.getElementById("auth-settings-btn")?.addEventListener("click",()=>{f(),openModal("totp-settings-modal")}),document.getElementById("totp-modal-close")?.addEventListener("click",()=>{closeModal("totp-settings-modal")}),document.getElementById("totp-settings-modal")?.addEventListener("click",l=>{l.target.id==="totp-settings-modal"&&closeModal("totp-settings-modal")}),window._updateAuthCard=y,(async()=>{try{const r=await(await fetch("/api/v1/totp/config")).json();if(r.success){const h=r.config.enabled&&r.config.isSetUp;y(h,r.config.sessionDuration)}}catch(l){o.logError("[TOTP] AuthCard Update",l,{function:"authCardUpdate"})}})()})(),(function(){injectModal("token-management-modal",`
+
`);async function y(){try{const s=await(await fetch("/api/v1/totp/config")).json();if(!s.success)return;const{enabled:m,sessionDuration:t,isSetUp:h}=s.config,c=document.getElementById("totp-status-dot"),o=document.getElementById("totp-status-text"),l=document.getElementById("totp-status-banner"),g=document.getElementById("totp-setup-section"),e=document.getElementById("totp-qr-section"),r=document.getElementById("totp-duration-section"),f=document.getElementById("totp-disable-section");if(m&&h){c.style.background="var(--ok-fg, #7ef2ff)",l.style.borderColor="var(--ok-fg, #7ef2ff)",l.style.background="color-mix(in srgb, var(--ok-fg) 8%, transparent)",o.textContent="TOTP is active",o.style.color="var(--ok-fg, #7ef2ff)",g.style.display="block";const n=document.getElementById("totp-setup-btn");n&&(n.textContent="Generate New Secret"),e.style.display="none",r.style.display="block",f.style.display="block",document.getElementById("totp-duration-select").value=t}else c.style.background="var(--muted)",l.style.borderColor="var(--border)",l.style.background="transparent",o.textContent="TOTP is not configured",o.style.color="var(--muted)",g.style.display="block",e.style.display="none",r.style.display="none",f.style.display="none";v(m&&h,t)}catch(i){console.warn("Failed to load TOTP settings:",i)}}const p={"15m":"15 min","30m":"30 min","1h":"1 hour","2h":"2 hours","4h":"4 hours","8h":"8 hours","12h":"12 hours","24h":"24 hours",never:"Disabled"};function v(i,s){const m=document.getElementById("auth-card"),t=document.getElementById("auth-pill"),h=document.getElementById("auth-dot"),c=document.getElementById("auth-status-text");m&&(i?(m.setAttribute("data-status","on"),t.className="badge on",t.textContent="YES",h.className="dot ok at-bl",c.textContent="Session: "+(p[s]||s)):(m.setAttribute("data-status","off"),t.className="badge off",t.textContent="NO",h.className="dot bad at-bl",c.textContent="Not configured"))}document.getElementById("totp-setup-btn")?.addEventListener("click",async()=>{try{const s=await(await secureFetch("/api/v1/totp/setup",{method:"POST"})).json();s.success&&(document.getElementById("totp-qr-image").src=s.qrCode,document.getElementById("totp-manual-key").textContent=s.manualKey,document.getElementById("totp-setup-section").style.display="none",document.getElementById("totp-qr-section").style.display="block",document.getElementById("totp-setup-code").value="",document.getElementById("totp-setup-error").textContent="",document.getElementById("totp-setup-code").focus())}catch(i){a.logError("[TOTP] Setup Failed",i,{function:"setupTOTP"})}}),document.getElementById("totp-import-btn")?.addEventListener("click",async()=>{const i=document.getElementById("totp-import-key").value.trim(),s=document.getElementById("totp-import-error");if(s.textContent="",!i){s.textContent="Paste a Base32 secret key first";return}try{const t=await(await secureFetch("/api/v1/totp/setup",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({secret:i})})).json();t.success?(s.textContent="",document.getElementById("totp-qr-image").src=t.qrCode,document.getElementById("totp-manual-key").textContent=t.manualKey,document.getElementById("totp-setup-section").style.display="none",document.getElementById("totp-qr-section").style.display="block",document.getElementById("totp-setup-code").value="",document.getElementById("totp-setup-error").textContent="",document.getElementById("totp-setup-code").focus()):s.textContent=t.error||t.message||"Import failed"}catch{s.textContent="Connection error \u2014 try refreshing the page"}}),document.getElementById("totp-copy-key")?.addEventListener("click",()=>{const i=document.getElementById("totp-manual-key").textContent;navigator.clipboard.writeText(i).then(()=>{const s=document.getElementById("totp-copy-key");s.textContent="\u2705",setTimeout(()=>{s.textContent="\u{1F4CB}"},2e3)})}),document.getElementById("totp-download-backup")?.addEventListener("click",()=>{const i=document.getElementById("totp-manual-key").textContent.trim();if(!i)return;const s={service:"DashCaddy",type:"totp-secret",secret:i,issuer:"DashCaddy",algorithm:"SHA1",digits:6,period:30,issued:new Date().toISOString(),recovery_url:`${window.location.origin}/ (login screen \u2192 "Lost access?")`,note:'Keep this file somewhere safe. Anyone with this secret can generate your login codes. Use it ONLY to recover TOTP access via the "Lost access?" link on the DashCaddy login screen.'},m=new Blob([JSON.stringify(s,null,2)],{type:"application/json"}),t=URL.createObjectURL(m),h=document.createElement("a");h.href=t,h.download=`dashcaddy-totp-backup-${new Date().toISOString().slice(0,10)}.json`,document.body.appendChild(h),h.click(),document.body.removeChild(h),URL.revokeObjectURL(t);const c=document.getElementById("totp-download-backup");c.textContent="\u2705 Saved",setTimeout(()=>{c.textContent="\u2B07 Download"},2e3)}),document.getElementById("totp-confirm-setup")?.addEventListener("click",async()=>{const i=document.getElementById("totp-setup-code").value,s=document.getElementById("totp-setup-error");if(!/^\d{6}$/.test(i)){s.textContent="Enter a 6-digit code";return}try{const t=await(await secureFetch("/api/v1/totp/verify-setup",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:i})})).json();t.success?(s.textContent="",y()):s.textContent=t.error||"Invalid code"}catch{s.textContent="Connection error"}}),document.getElementById("totp-setup-code")?.addEventListener("keydown",i=>{i.key==="Enter"&&document.getElementById("totp-confirm-setup")?.click()}),document.getElementById("totp-duration-select")?.addEventListener("change",async i=>{try{await secureFetch("/api/v1/totp/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sessionDuration:i.target.value})}),y()}catch(s){a.logError("[TOTP] Update Session Duration",s,{function:"updateSessionDuration"})}}),document.getElementById("totp-disable-btn")?.addEventListener("click",async()=>{if(confirm("Disable TOTP authentication? All services will be accessible without a code."))try{(await(await secureFetch("/api/v1/totp/disable",{method:"POST",headers:{"Content-Type":"application/json"},body:"{}"})).json()).success&&y()}catch(i){a.logError("[TOTP] Disable Failed",i,{function:"disableTOTP"})}}),document.getElementById("auth-settings-btn")?.addEventListener("click",()=>{y(),openModal("totp-settings-modal")}),document.getElementById("totp-modal-close")?.addEventListener("click",()=>{closeModal("totp-settings-modal")}),document.getElementById("totp-settings-modal")?.addEventListener("click",i=>{i.target.id==="totp-settings-modal"&&closeModal("totp-settings-modal")}),window._updateAuthCard=v,(async()=>{try{const s=await(await fetch("/api/v1/totp/config")).json();if(s.success){const m=s.config.enabled&&s.config.isSetUp;v(m,s.config.sessionDuration)}}catch(i){a.logError("[TOTP] AuthCard Update",i,{function:"authCardUpdate"})}})()})(),(function(){injectModal("token-management-modal",`
- `);let u=null;function y(a){u=a;const b=SITE.dnsServers[a]||{},m=document.getElementById("dns-settings-modal");document.getElementById("dns-settings-title").textContent=`${(b.name||a).toUpperCase()} Settings`,document.getElementById("dns-edit-ip").value=b.ip||"",document.getElementById("dns-edit-port").value=b.port||DC.DEFAULTS.DNS_PORT,document.getElementById("dns-edit-name").value=b.name||"",m.classList.add("show")}async function l(){if(!u)return;const a=document.getElementById("dns-edit-ip").value.trim(),b=document.getElementById("dns-edit-port").value.trim()||DC.DEFAULTS.DNS_PORT,m=document.getElementById("dns-edit-name").value.trim();if(!a){showNotification("Server IP is required","warning");return}const n={dnsServers:{}};n.dnsServers[u]={ip:a,port:String(b)},m&&(n.dnsServers[u].name=m);try{const g=await(await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})).json();g.success?(SITE.dnsServers[u]=n.dnsServers[u],showNotification(`${u.toUpperCase()} settings saved`,"success"),h(),window.refreshAll()):showNotification(g.error||"Failed to save settings","error")}catch(d){showNotification("Failed to save: "+d.message,"error")}}async function r(){if(u&&confirm(`Remove ${u.toUpperCase()} from dashboard? This won't affect the actual DNS server.`))try{const b=await(await secureFetch("/api/v1/config")).json();b.dnsServers&&delete b.dnsServers[u];const n=await(await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({dnsServers:b.dnsServers||{}})})).json();if(n.success){delete SITE.dnsServers[u];const d=document.querySelector(`.top [data-app="${u}"]`);d&&d.remove(),showNotification(`${u.toUpperCase()} removed from dashboard`,"success"),h()}else showNotification(n.error||"Failed to remove","error")}catch(a){showNotification("Failed to remove: "+a.message,"error")}}function h(){closeModal("dns-settings-modal"),u=null}document.getElementById("dns-settings-cancel")?.addEventListener("click",h),document.getElementById("dns-settings-save")?.addEventListener("click",l),document.getElementById("dns-settings-delete")?.addEventListener("click",r),document.getElementById("dns-settings-modal")?.addEventListener("click",a=>{a.target.id==="dns-settings-modal"&&h()}),document.querySelector(".top")?.addEventListener("click",a=>{const b=a.target.closest('[id$="-settings"]');if(!b)return;const m=b.id.replace("-settings","");SITE.dnsServers[m]&&(a.stopPropagation(),y(m))}),document.getElementById("refresh")?.addEventListener("click",window.refreshAll)})(),(function(){injectModal("logs-modal",`
+ `);let p=null;function v(t){p=t;const h=SITE.dnsServers[t]||{},c=document.getElementById("dns-settings-modal");document.getElementById("dns-settings-title").textContent=`${(h.name||t).toUpperCase()} Settings`,document.getElementById("dns-edit-ip").value=h.ip||"",document.getElementById("dns-edit-port").value=h.port||DC.DEFAULTS.DNS_PORT,document.getElementById("dns-edit-name").value=h.name||"",c.classList.add("show")}async function i(){if(!p)return;const t=document.getElementById("dns-edit-ip").value.trim(),h=document.getElementById("dns-edit-port").value.trim()||DC.DEFAULTS.DNS_PORT,c=document.getElementById("dns-edit-name").value.trim();if(!t){showNotification("Server IP is required","warning");return}const o={dnsServers:{}};o.dnsServers[p]={ip:t,port:String(h)},c&&(o.dnsServers[p].name=c);try{const g=await(await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)})).json();g.success?(SITE.dnsServers[p]=o.dnsServers[p],showNotification(`${p.toUpperCase()} settings saved`,"success"),m(),window.refreshAll()):showNotification(g.error||"Failed to save settings","error")}catch(l){showNotification("Failed to save: "+l.message,"error")}}async function s(){if(p&&confirm(`Remove ${p.toUpperCase()} from dashboard? This won't affect the actual DNS server.`))try{const h=await(await secureFetch("/api/v1/config")).json();h.dnsServers&&delete h.dnsServers[p];const o=await(await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({dnsServers:h.dnsServers||{}})})).json();if(o.success){delete SITE.dnsServers[p];const l=document.querySelector(`.top [data-app="${p}"]`);l&&l.remove(),showNotification(`${p.toUpperCase()} removed from dashboard`,"success"),m()}else showNotification(o.error||"Failed to remove","error")}catch(t){showNotification("Failed to remove: "+t.message,"error")}}function m(){closeModal("dns-settings-modal"),p=null}document.getElementById("dns-settings-cancel")?.addEventListener("click",m),document.getElementById("dns-settings-save")?.addEventListener("click",i),document.getElementById("dns-settings-delete")?.addEventListener("click",s),document.getElementById("dns-settings-modal")?.addEventListener("click",t=>{t.target.id==="dns-settings-modal"&&m()}),document.querySelector(".top")?.addEventListener("click",t=>{const h=t.target.closest('[id$="-settings"]');if(!h)return;const c=h.id.replace("-settings","");SITE.dnsServers[c]&&(t.stopPropagation(),v(c))}),document.getElementById("refresh")?.addEventListener("click",window.refreshAll)})(),(function(){injectModal("logs-modal",`
- `)})(),(function(){async function o(r){try{const h=await fetch(`/api/v1/caddy/cas?caddyfilePath=${encodeURIComponent(r)}`);if(!h.ok)throw new Error(`Failed to load CAs: ${h.status}`);const a=await h.json();if(a.status==="success"){const b=document.getElementById("existing-ca-select");return b.innerHTML="",a.data.cas.length===0?b.innerHTML='':(b.innerHTML='',a.data.cas.forEach(m=>{const n=document.createElement("option");typeof m=="object"?(n.value=m.id,n.textContent=m.displayName||m.name):(n.value=m,n.textContent=m),b.appendChild(n)})),a.data.cas}else throw new Error(a.message)}catch(h){console.error("Error loading CAs:",h);const a=document.getElementById("existing-ca-select");return a.innerHTML='',[]}}function f(r){const{subdomain:h,port:a,ip:b,sslType:m,caName:n,existingCa:d,enableAuth:g,enableCors:e,customHeaders:s,upstreamPath:p,healthCheck:t,timeout:c,tailscaleOnly:v}=r;let i=`${buildDomain(h)} {
-`;switch(v&&(i+=` @blocked not remote_ip 100.64.0.0/10
-`,i+=` respond @blocked "Access denied. Tailscale connection required." 403
-`),m){case"letsencrypt":break;case"caddy-managed":i+=` tls internal
-`;break;case"existing-ca":d&&(i+=` tls {
- ca ${d}
+ `)})(),(function(){async function a(s){try{const m=await fetch(`/api/v1/caddy/cas?caddyfilePath=${encodeURIComponent(s)}`);if(!m.ok)throw new Error(`Failed to load CAs: ${m.status}`);const t=await m.json();if(t.success){const h=document.getElementById("existing-ca-select");return h.innerHTML="",t.cas.length===0?h.innerHTML='':(h.innerHTML='',t.cas.forEach(c=>{const o=document.createElement("option");typeof c=="object"?(o.value=c.id,o.textContent=c.displayName||c.name):(o.value=c,o.textContent=c),h.appendChild(o)})),t.data.cas}else throw new Error(t.message)}catch(m){console.error("Error loading CAs:",m);const t=document.getElementById("existing-ca-select");return t.innerHTML='',[]}}function y(s){const{subdomain:m,port:t,ip:h,sslType:c,caName:o,existingCa:l,enableAuth:g,enableCors:e,customHeaders:r,upstreamPath:f,healthCheck:n,timeout:u,tailscaleOnly:b}=s;let d=`${buildDomain(m)} {
+`;switch(b&&(d+=` @blocked not remote_ip 100.64.0.0/10
+`,d+=` respond @blocked "Access denied. Tailscale connection required." 403
+`),c){case"letsencrypt":break;case"caddy-managed":d+=` tls internal
+`;break;case"existing-ca":l&&(d+=` tls {
+ ca ${l}
}
-`);break;case"custom-ca":n&&(i+=` tls {
- ca ${n}
+`);break;case"custom-ca":o&&(d+=` tls {
+ ca ${o}
}
-`);break}if(g&&(i+=` basicauth {
+`);break}if(g&&(d+=` basicauth {
admin $2a$14$hashed_password_here
}
-`),e&&(i+=` header {
-`,i+=` Access-Control-Allow-Origin "*"
-`,i+=` Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
-`,i+=` Access-Control-Allow-Headers "Content-Type, Authorization"
-`,i+=` }
-`),s)try{const E=JSON.parse(s);i+=` header {
-`,Object.entries(E).forEach(([T,L])=>{i+=` ${T} "${L}"
-`}),i+=` }
-`}catch{console.warn("Invalid JSON in custom headers")}return t&&(i+=` health_uri ${t}
-`),i+=` reverse_proxy ${b}:${a} {
-`,p&&p!=="/"&&(i+=` rewrite ${p}
-`),c&&c!==30&&(i+=` transport http {
-`,i+=` dial_timeout ${c}s
-`,i+=` response_header_timeout ${c}s
-`,i+=` }
-`),i+=` }
-`,i+=`}
-`,i}async function u(r,h,a=DC.DEFAULTS.TTL){const b=window.getToken(getPrimaryDnsId(),"admin");if(!b)throw new Error("DNS admin token not configured. Please set it in the Tokens menu.");const m=buildDomain(r),n=await secureFetch("/api/v1/dns/record",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:m,ip:h,ttl:a,token:b,server:SITE.dnsIp})});if(!n.ok){const g=await n.text();throw new Error(`DNS API Error: ${n.status} - ${g}`)}const d=await n.json();if(!d.success)throw new Error(`DNS Error: ${d.error||"Unknown error"}`);return d}async function y(r){const h={id:r.subdomain,name:r.name,logo:r.logo||`/assets/${r.subdomain}.png`};r.category&&(h.category=r.category),r.containerId&&(h.containerId=r.containerId);try{const a=await secureFetch("/api/v1/services",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(h)});if(!a.ok){const b=await a.json();throw new Error(b.error||"Failed to save service")}return await window.loadServices(),window.buildGrid(),h}catch(a){throw console.error("Failed to add service to config:",a),a}}async function l(r){const h=document.getElementById("service-subdomain-input").value.trim(),a=document.getElementById("service-ip-input").value.trim()||"localhost",b=document.getElementById("service-port-input").value.trim()||"80",m=await secureFetch("/api/v1/site",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:buildDomain(h),upstream:`${a}:${b}`,config:r})}),n=await m.json();if(!m.ok||!n.success)throw new Error(n.error||`Caddy API Error: ${m.status}`);return n}window.loadExistingCAs=o,window.generateCaddyConfig=f,window.createDnsRecord=u,window.addServiceToConfig=y,window.addToCaddyfile=l})(),(function(){let o=null;function f(a){o=a;const b=document.getElementById("service-edit-modal");document.getElementById("service-edit-title").textContent=`Edit ${a.name}`,document.getElementById("edit-service-name").value=a.name,document.getElementById("edit-service-url-display").textContent=a.url||buildServiceUrl(a.id),document.getElementById("edit-service-logo-preview").src=a.logo||`/assets/${a.id}.png`,document.getElementById("edit-subdomain").value=a.id,document.getElementById("edit-port").value=a.port||"",document.getElementById("edit-ip").value=a.ip||"localhost",document.getElementById("edit-tailscale-only").checked=a.tailscaleOnly||!1,document.getElementById("edit-logo-url").value=a.logo||"";const m=document.getElementById("edit-service-category");m&&(m.dataset.current=a.category||"",typeof window.populateCategorySelects=="function"&&window.populateCategorySelects()),b.classList.add("show")}function u(){closeModal("service-edit-modal"),o=null}async function y(){if(!o)return;const a=document.getElementById("edit-subdomain").value.trim().toLowerCase(),b=document.getElementById("edit-service-name").value.trim(),m=document.getElementById("edit-port").value.trim(),n=document.getElementById("edit-ip").value.trim()||"localhost",d=document.getElementById("edit-tailscale-only").checked,g=document.getElementById("edit-logo-url").value.trim(),e=document.getElementById("edit-service-category")?.value||"";if(!a){showNotification("Subdomain is required","warning");return}const s=o.id,p=[];if(a!==s&&p.push("subdomain"),b&&b!==o.name&&p.push("name"),m&&m!==String(o.port)&&p.push("port"),n!==o.ip&&p.push("ip"),d!==(o.tailscaleOnly||!1)&&p.push("tailscale"),g&&g!==o.logo&&p.push("logo"),e!==(o.category||"")&&p.push("category"),p.length===0){u();return}const t=document.getElementById("service-edit-save");t.textContent="Saving...",t.disabled=!0;try{const v=await(await secureFetch("/api/v1/services/update",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({oldSubdomain:s,newSubdomain:a,name:b||o.name,port:m||o.port,ip:n,tailscaleOnly:d,logo:g||void 0,category:e})})).json();if(!v.success)throw new Error(v.error||"Failed to update service");const i=window.APPS.findIndex(E=>E.id===s);i!==-1&&(window.APPS[i]={...window.APPS[i],id:a,name:b||window.APPS[i].name,port:m||window.APPS[i].port,ip:n,tailscaleOnly:d,logo:g||window.APPS[i].logo,category:e||void 0}),u(),window.buildGrid(),window.refreshAll()}catch(c){console.error("Error saving service changes:",c),showNotification(`Error saving changes: ${c.message}`,"error")}finally{t.textContent="Save Changes",t.disabled=!1}}document.getElementById("edit-logo-file")?.addEventListener("change",async a=>{const b=a.target.files[0];if(!b)return;if(!b.type.startsWith("image/")){showNotification("Please select an image file","warning");return}const m=new FileReader;m.onload=async n=>{const d=n.target.result;if(document.getElementById("edit-service-logo-preview").src=d,document.getElementById("edit-logo-url").value=d,o)try{const e=await(await secureFetch("/api/v1/assets/upload",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({filename:`${o.id}.png`,data:d})})).json();e.success&&e.path&&(document.getElementById("edit-logo-url").value=e.path)}catch{}},m.readAsDataURL(b)}),document.getElementById("service-edit-cancel")?.addEventListener("click",u),document.getElementById("service-edit-save")?.addEventListener("click",y),document.getElementById("service-edit-modal")?.addEventListener("click",a=>{a.target.id==="service-edit-modal"&&u()});function l(a,b,m){return new Promise(n=>{const d=document.getElementById("delete-service-modal"),g=document.getElementById("delete-modal-title"),e=document.getElementById("delete-modal-message"),s=document.getElementById("delete-modal-container-info"),p=document.getElementById("delete-modal-container-name"),t=document.getElementById("delete-modal-help"),c=document.getElementById("delete-modal-cancel"),v=document.getElementById("delete-modal-remove"),i=document.getElementById("delete-modal-delete");g.textContent=`Delete "${a}"`,b?(e.innerHTML="This service has an associated Docker container. Choose how to proceed:",s.style.display="block",p.textContent=`Container ID: ${m?.slice(0,12)||"Unknown"}`,t.style.display="block",i.style.display="block"):(e.textContent="Remove this service from the dashboard?",s.style.display="none",t.style.display="none",i.style.display="none");const E=()=>{d.classList.remove("show"),c.removeEventListener("click",T),v.removeEventListener("click",L),i.removeEventListener("click",P),d.removeEventListener("click",S)},T=()=>{E(),n(null)},L=()=>{E(),n(!1)},P=()=>{E(),n(!0)},S=B=>{B.target===d&&(E(),n(null))};c.addEventListener("click",T),v.addEventListener("click",L),i.addEventListener("click",P),d.addEventListener("click",S),d.classList.add("show")})}async function r(a,b,m){const n=document.getElementById(`update-btn-${m}`),d=n?.textContent;if(confirm(`Update ${b} to the latest version?
+`),e&&(d+=` header {
+`,d+=` Access-Control-Allow-Origin "*"
+`,d+=` Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
+`,d+=` Access-Control-Allow-Headers "Content-Type, Authorization"
+`,d+=` }
+`),r)try{const E=JSON.parse(r);d+=` header {
+`,Object.entries(E).forEach(([T,L])=>{d+=` ${T} "${L}"
+`}),d+=` }
+`}catch{console.warn("Invalid JSON in custom headers")}return n&&(d+=` health_uri ${n}
+`),d+=` reverse_proxy ${h}:${t} {
+`,f&&f!=="/"&&(d+=` rewrite ${f}
+`),u&&u!==30&&(d+=` transport http {
+`,d+=` dial_timeout ${u}s
+`,d+=` response_header_timeout ${u}s
+`,d+=` }
+`),d+=` }
+`,d+=`}
+`,d}async function p(s,m,t=DC.DEFAULTS.TTL){const h=window.getToken(getPrimaryDnsId(),"admin");if(!h)throw new Error("DNS admin token not configured. Please set it in the Tokens menu.");const c=buildDomain(s),o=await secureFetch("/api/v1/dns/record",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:c,ip:m,ttl:t,token:h,server:SITE.dnsIp})});if(!o.ok){const g=await o.text();throw new Error(`DNS API Error: ${o.status} - ${g}`)}const l=await o.json();if(!l.success)throw new Error(`DNS Error: ${l.error||"Unknown error"}`);return l}async function v(s){const m={id:s.subdomain,name:s.name,logo:s.logo||`/assets/${s.subdomain}.png`};s.category&&(m.category=s.category),s.containerId&&(m.containerId=s.containerId);try{const t=await secureFetch("/api/v1/services",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(m)});if(!t.ok){const h=await t.json();throw new Error(h.error||"Failed to save service")}return await window.loadServices(),window.buildGrid(),m}catch(t){throw console.error("Failed to add service to config:",t),t}}async function i(s){const m=document.getElementById("service-subdomain-input").value.trim(),t=document.getElementById("service-ip-input").value.trim()||"localhost",h=document.getElementById("service-port-input").value.trim()||"80",c=await secureFetch("/api/v1/site",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:buildDomain(m),upstream:`${t}:${h}`,config:s})}),o=await c.json();if(!c.ok||!o.success)throw new Error(o.error||`Caddy API Error: ${c.status}`);return o}window.loadExistingCAs=a,window.generateCaddyConfig=y,window.createDnsRecord=p,window.addServiceToConfig=v,window.addToCaddyfile=i})(),(function(){let a=null;function y(t){a=t;const h=document.getElementById("service-edit-modal");document.getElementById("service-edit-title").textContent=`Edit ${t.name}`,document.getElementById("edit-service-name").value=t.name,document.getElementById("edit-service-url-display").textContent=t.url||buildServiceUrl(t.id),document.getElementById("edit-service-logo-preview").src=t.logo||`/assets/${t.id}.png`,document.getElementById("edit-subdomain").value=t.id,document.getElementById("edit-port").value=t.port||"",document.getElementById("edit-ip").value=t.ip||"localhost",document.getElementById("edit-tailscale-only").checked=t.tailscaleOnly||!1,document.getElementById("edit-logo-url").value=t.logo||"";const c=document.getElementById("edit-service-category");c&&(c.dataset.current=t.category||"",typeof window.populateCategorySelects=="function"&&window.populateCategorySelects()),h.classList.add("show")}function p(){closeModal("service-edit-modal"),a=null}async function v(){if(!a)return;const t=document.getElementById("edit-subdomain").value.trim().toLowerCase(),h=document.getElementById("edit-service-name").value.trim(),c=document.getElementById("edit-port").value.trim(),o=document.getElementById("edit-ip").value.trim()||"localhost",l=document.getElementById("edit-tailscale-only").checked,g=document.getElementById("edit-logo-url").value.trim(),e=document.getElementById("edit-service-category")?.value||"";if(!t){showNotification("Subdomain is required","warning");return}const r=a.id,f=[];if(t!==r&&f.push("subdomain"),h&&h!==a.name&&f.push("name"),c&&c!==String(a.port)&&f.push("port"),o!==a.ip&&f.push("ip"),l!==(a.tailscaleOnly||!1)&&f.push("tailscale"),g&&g!==a.logo&&f.push("logo"),e!==(a.category||"")&&f.push("category"),f.length===0){p();return}const n=document.getElementById("service-edit-save");n.textContent="Saving...",n.disabled=!0;try{const b=await(await secureFetch("/api/v1/services/update",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({oldSubdomain:r,newSubdomain:t,name:h||a.name,port:c||a.port,ip:o,tailscaleOnly:l,logo:g||void 0,category:e})})).json();if(!b.success)throw new Error(b.error||"Failed to update service");const d=window.APPS.findIndex(E=>E.id===r);d!==-1&&(window.APPS[d]={...window.APPS[d],id:t,name:h||window.APPS[d].name,port:c||window.APPS[d].port,ip:o,tailscaleOnly:l,logo:g||window.APPS[d].logo,category:e||void 0}),p(),window.buildGrid(),window.refreshAll()}catch(u){console.error("Error saving service changes:",u),showNotification(`Error saving changes: ${u.message}`,"error")}finally{n.textContent="Save Changes",n.disabled=!1}}document.getElementById("edit-logo-file")?.addEventListener("change",async t=>{const h=t.target.files[0];if(!h)return;if(!h.type.startsWith("image/")){showNotification("Please select an image file","warning");return}const c=new FileReader;c.onload=async o=>{const l=o.target.result;if(document.getElementById("edit-service-logo-preview").src=l,document.getElementById("edit-logo-url").value=l,a)try{const e=await(await secureFetch("/api/v1/assets/upload",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({filename:`${a.id}.png`,data:l})})).json();e.success&&e.path&&(document.getElementById("edit-logo-url").value=e.path)}catch{}},c.readAsDataURL(h)}),document.getElementById("service-edit-cancel")?.addEventListener("click",p),document.getElementById("service-edit-save")?.addEventListener("click",v),document.getElementById("service-edit-modal")?.addEventListener("click",t=>{t.target.id==="service-edit-modal"&&p()});function i(t,h,c){return new Promise(o=>{const l=document.getElementById("delete-service-modal"),g=document.getElementById("delete-modal-title"),e=document.getElementById("delete-modal-message"),r=document.getElementById("delete-modal-container-info"),f=document.getElementById("delete-modal-container-name"),n=document.getElementById("delete-modal-help"),u=document.getElementById("delete-modal-cancel"),b=document.getElementById("delete-modal-remove"),d=document.getElementById("delete-modal-delete");g.textContent=`Delete "${t}"`,h?(e.innerHTML="This service has an associated Docker container. Choose how to proceed:",r.style.display="block",f.textContent=`Container ID: ${c?.slice(0,12)||"Unknown"}`,n.style.display="block",d.style.display="block"):(e.textContent="Remove this service from the dashboard?",r.style.display="none",n.style.display="none",d.style.display="none");const E=()=>{l.classList.remove("show"),u.removeEventListener("click",T),b.removeEventListener("click",L),d.removeEventListener("click",P),l.removeEventListener("click",C)},T=()=>{E(),o(null)},L=()=>{E(),o(!1)},P=()=>{E(),o(!0)},C=B=>{B.target===l&&(E(),o(null))};u.addEventListener("click",T),b.addEventListener("click",L),d.addEventListener("click",P),l.addEventListener("click",C),l.classList.add("show")})}async function s(t,h,c){const o=document.getElementById(`update-btn-${c}`),l=o?.textContent;if(confirm(`Update ${h} to the latest version?
This will:
1. Pull the latest image
2. Stop the container
3. Recreate with same settings
-The service will be briefly unavailable.`))try{n&&(n.textContent="\u{1F504}",n.disabled=!0,n.title="Updating...");const e=await(await secureFetch(`/api/v1/containers/${a}/update`,{method:"POST"})).json();if(e.success){const s=window.APPS.find(p=>p.id===m);s&&e.newContainerId&&(s.containerId=e.newContainerId),n&&(n.textContent="\u2705",n.title="Updated successfully!",setTimeout(()=>{n.textContent=d,n.disabled=!1,n.title="Update container to latest version"},3e3)),setTimeout(()=>window.refreshAll(),2e3),showNotification(`${b} updated successfully!`,"success")}else throw new Error(e.error||"Update failed")}catch(g){console.error("Update error:",g),n&&(n.textContent="\u274C",n.title="Update failed",setTimeout(()=>{n.textContent=d,n.disabled=!1,n.title="Update container to latest version"},3e3)),showNotification(`Failed to update ${b}: ${g.message}`,"error")}}async function h(a,b){const m=window.APPS.find(i=>i.id===a),n=m?buildDomain(m.id):null,d=m?.containerId,g=await l(b||a,d,m?.containerId);if(g===null)return;let e={dashboard:!1,container:null,dns:null,caddy:null,service:null};if(g&&d)try{const i=new URLSearchParams({containerId:m.containerId,subdomain:m.id,ip:m.ip||"localhost",deleteContainer:"true"}),T=await(await secureFetch(`/api/v1/apps/${encodeURIComponent(m.id)}?${i.toString()}`,{method:"DELETE"})).json();T.success?e={...e,...T.results,dashboard:!1}:console.error("App removal failed:",T.error)}catch(i){console.error("App removal error:",i)}else if(g&&n){try{const i=m?.ip||"localhost",T=await(await secureFetch(`/api/v1/dns/record?domain=${encodeURIComponent(n)}&type=A&ipAddress=${encodeURIComponent(i)}&server=${SITE.dnsIp}`,{method:"DELETE"})).json();e.dns=T.success?"deleted":T.error||"failed"}catch(i){e.dns=i.message}try{const E=await(await secureFetch(`/api/v1/site/${encodeURIComponent(n)}`,{method:"DELETE"})).json();e.caddy=E.success||E.error&&E.error.includes("not found")?"removed":E.error||"failed"}catch(i){e.caddy=i.message}}const s=window.APPS.findIndex(i=>i.id===a);s>-1&&(window.APPS.splice(s,1),e.dashboard=!0);try{const i=safeGetJSON("custom-apps",[]),E=i.findIndex(T=>T.id===a);E>-1&&(i.splice(E,1),safeSet("custom-apps",JSON.stringify(i)))}catch{}try{const E=await(await secureFetch(`/api/v1/services/${encodeURIComponent(a)}`,{method:"DELETE"})).json();e.service=E.success?"removed":E.error||"failed"}catch(i){e.service=i.message}window.buildGrid(),window.refreshAll();let p=!1,t=[];e.dashboard||(p=!0,t.push("\u2717 Failed to remove from dashboard"));const c=["removed","already removed","not found","deleted","kept (user choice)","skipped","no such record","does not exist"],v=i=>!i||c.some(E=>i.toLowerCase().includes(E.toLowerCase()));e.container&&!v(e.container)&&(p=!0,t.push(`\u26A0 Container: ${e.container}`)),e.dns&&!v(e.dns)&&(p=!0,t.push(`\u26A0 DNS Record: ${e.dns}`)),e.caddy&&!v(e.caddy)&&(p=!0,t.push(`\u26A0 Caddy Config: ${e.caddy}`)),e.service&&!v(e.service)&&(p=!0,t.push(`\u26A0 Service File: ${e.service}`)),p&&showNotification(`Error deleting "${b||a}": ${t.join(", ")}`,"error",6e3)}window.openServiceEditModal=f,window.showDeleteModal=l,window.updateContainer=r,window.deleteService=h})(),(function(){function o(e){return e.toLowerCase().replace(/\s+/g,"-").replace(/[^a-z0-9-]/g,"").replace(/-+/g,"-").replace(/^-|-$/g,"")}function f(){return SITE.defaults?.sslType||(SITE.configurationType==="public"?"letsencrypt":"caddy-managed")}function u(){const e=document.getElementById("service-subdomain-input").value||"subdomain",s=document.getElementById("service-ip-input").value||y.lan||"localhost",p=document.getElementById("service-port-input").value||DC.DEFAULTS.SERVICE_PORT,t=document.getElementById("ssl-type-select").value,c=document.getElementById("ca-name-input").value||"sami-ca",v=document.getElementById("existing-ca-select").value,i=document.getElementById("enable-auth").checked,E=document.getElementById("enable-cors").checked,T=document.getElementById("custom-headers-input").value,L=document.getElementById("upstream-path-input").value||"/",P=document.getElementById("health-check-input").value,S=document.getElementById("timeout-input").value||30,B=document.getElementById("dns-preview");B&&(B.textContent=`${buildDomain(e)} \u2192 ${s}`);const $=document.getElementById("url-preview");$&&($.textContent=buildServiceUrl(e));const x={subdomain:e,port:p,ip:s,sslType:t,caName:c,existingCa:v,enableAuth:i,enableCors:E,customHeaders:T,upstreamPath:L,healthCheck:P,timeout:S},w=window.generateCaddyConfig(x),C=document.getElementById("caddy-config-preview");C&&(C.value=w)}const y={localhost:"127.0.0.1",lan:"",tailscale:""};async function l(){try{const t=await fetch("/api/v1/network/ips",{signal:AbortSignal.timeout(2e3)});if(t.ok){const c=await t.json();c.lan&&(y.lan=c.lan),c.tailscale&&(y.tailscale=c.tailscale)}}catch{}const e=document.getElementById("quick-ip-lan"),s=document.getElementById("quick-ip-tailscale");e&&(y.lan?(e.dataset.ip=y.lan,e.textContent=`LAN (${y.lan})`,e.title=`LAN IP: ${y.lan}`):e.style.display="none"),s&&(y.tailscale?(s.dataset.ip=y.tailscale,s.textContent=`Tailscale (${y.tailscale})`,s.title=`Tailscale IP: ${y.tailscale}`):s.style.display="none");const p=document.getElementById("service-ip-input");p&&!p.value&&y.lan&&(p.value=y.lan)}function r(){document.querySelectorAll(".quick-ip-btn").forEach(e=>{e.addEventListener("click",()=>{const s=e.dataset.ip;s&&(document.getElementById("service-ip-input").value=s,document.querySelectorAll(".quick-ip-btn").forEach(p=>p.classList.remove("active")),e.classList.add("active"),u())})}),document.getElementById("service-ip-input")?.addEventListener("input",e=>{const s=e.target.value;document.querySelectorAll(".quick-ip-btn").forEach(p=>{p.classList.toggle("active",p.dataset.ip===s)})})}async function h(){const e=document.getElementById("add-service-modal");e.classList.add("show");const s=e.querySelector(".weather-modal-content");s&&(s.scrollTop=0),document.body.style.overflow="hidden";const p=document.getElementById("ssl-type-select");p&&(p.value=f()),await l();const t=document.getElementById("caddyfile-path-input").value||DC.DEFAULTS.CADDYFILE;await window.loadExistingCAs(t);const c=document.getElementById("manual-tailscale-status"),v=document.getElementById("manual-tailscale-only");try{const E=await(await fetch("/api/v1/tailscale/status")).json();E.success&&E.installed&&E.connected?(c.innerHTML=`
+The service will be briefly unavailable.`))try{o&&(o.textContent="\u{1F504}",o.disabled=!0,o.title="Updating...");const e=await(await secureFetch(`/api/v1/containers/${t}/update`,{method:"POST"})).json();if(e.success){const r=window.APPS.find(f=>f.id===c);r&&e.newContainerId&&(r.containerId=e.newContainerId),o&&(o.textContent="\u2705",o.title="Updated successfully!",setTimeout(()=>{o.textContent=l,o.disabled=!1,o.title="Update container to latest version"},3e3)),setTimeout(()=>window.refreshAll(),2e3),showNotification(`${h} updated successfully!`,"success")}else throw new Error(e.error||"Update failed")}catch(g){console.error("Update error:",g),o&&(o.textContent="\u274C",o.title="Update failed",setTimeout(()=>{o.textContent=l,o.disabled=!1,o.title="Update container to latest version"},3e3)),showNotification(`Failed to update ${h}: ${g.message}`,"error")}}async function m(t,h){const c=window.APPS.find(d=>d.id===t),o=c?buildDomain(c.id):null,l=c?.containerId,g=await i(h||t,l,c?.containerId);if(g===null)return;let e={dashboard:!1,container:null,dns:null,caddy:null,service:null};if(g&&l)try{const d=new URLSearchParams({containerId:c.containerId,subdomain:c.id,ip:c.ip||"localhost",deleteContainer:"true"}),T=await(await secureFetch(`/api/v1/apps/${encodeURIComponent(c.id)}?${d.toString()}`,{method:"DELETE"})).json();T.success?e={...e,...T.results,dashboard:!1}:console.error("App removal failed:",T.error)}catch(d){console.error("App removal error:",d)}else if(g&&o){try{const d=c?.ip||"localhost",T=await(await secureFetch(`/api/v1/dns/record?domain=${encodeURIComponent(o)}&type=A&ipAddress=${encodeURIComponent(d)}&server=${SITE.dnsIp}`,{method:"DELETE"})).json();e.dns=T.success?"deleted":T.error||"failed"}catch(d){e.dns=d.message}try{const E=await(await secureFetch(`/api/v1/site/${encodeURIComponent(o)}`,{method:"DELETE"})).json();e.caddy=E.success||E.error&&E.error.includes("not found")?"removed":E.error||"failed"}catch(d){e.caddy=d.message}}const r=window.APPS.findIndex(d=>d.id===t);r>-1&&(window.APPS.splice(r,1),e.dashboard=!0);try{const d=safeGetJSON("custom-apps",[]),E=d.findIndex(T=>T.id===t);E>-1&&(d.splice(E,1),safeSet("custom-apps",JSON.stringify(d)))}catch{}try{const E=await(await secureFetch(`/api/v1/services/${encodeURIComponent(t)}`,{method:"DELETE"})).json();e.service=E.success?"removed":E.error||"failed"}catch(d){e.service=d.message}window.buildGrid(),window.refreshAll();let f=!1,n=[];e.dashboard||(f=!0,n.push("\u2717 Failed to remove from dashboard"));const u=["removed","already removed","not found","deleted","kept (user choice)","skipped","no such record","does not exist"],b=d=>!d||u.some(E=>d.toLowerCase().includes(E.toLowerCase()));e.container&&!b(e.container)&&(f=!0,n.push(`\u26A0 Container: ${e.container}`)),e.dns&&!b(e.dns)&&(f=!0,n.push(`\u26A0 DNS Record: ${e.dns}`)),e.caddy&&!b(e.caddy)&&(f=!0,n.push(`\u26A0 Caddy Config: ${e.caddy}`)),e.service&&!b(e.service)&&(f=!0,n.push(`\u26A0 Service File: ${e.service}`)),f&&showNotification(`Error deleting "${h||t}": ${n.join(", ")}`,"error",6e3)}window.openServiceEditModal=y,window.showDeleteModal=i,window.updateContainer=s,window.deleteService=m})(),(function(){function a(e){return e.toLowerCase().replace(/\s+/g,"-").replace(/[^a-z0-9-]/g,"").replace(/-+/g,"-").replace(/^-|-$/g,"")}function y(){return SITE.defaults?.sslType||(SITE.configurationType==="public"?"letsencrypt":"caddy-managed")}function p(){const e=document.getElementById("service-subdomain-input").value||"subdomain",r=document.getElementById("service-ip-input").value||v.lan||"localhost",f=document.getElementById("service-port-input").value||DC.DEFAULTS.SERVICE_PORT,n=document.getElementById("ssl-type-select").value,u=document.getElementById("ca-name-input").value||"sami-ca",b=document.getElementById("existing-ca-select").value,d=document.getElementById("enable-auth").checked,E=document.getElementById("enable-cors").checked,T=document.getElementById("custom-headers-input").value,L=document.getElementById("upstream-path-input").value||"/",P=document.getElementById("health-check-input").value,C=document.getElementById("timeout-input").value||30,B=document.getElementById("dns-preview");B&&(B.textContent=`${buildDomain(e)} \u2192 ${r}`);const $=document.getElementById("url-preview");$&&($.textContent=buildServiceUrl(e));const x={subdomain:e,port:f,ip:r,sslType:n,caName:u,existingCa:b,enableAuth:d,enableCors:E,customHeaders:T,upstreamPath:L,healthCheck:P,timeout:C},w=window.generateCaddyConfig(x),I=document.getElementById("caddy-config-preview");I&&(I.value=w)}const v={localhost:"127.0.0.1",lan:"",tailscale:""};async function i(){try{const n=await fetch("/api/v1/network/ips",{signal:AbortSignal.timeout(2e3)});if(n.ok){const u=await n.json();u.lan&&(v.lan=u.lan),u.tailscale&&(v.tailscale=u.tailscale)}}catch{}const e=document.getElementById("quick-ip-lan"),r=document.getElementById("quick-ip-tailscale");e&&(v.lan?(e.dataset.ip=v.lan,e.textContent=`LAN (${v.lan})`,e.title=`LAN IP: ${v.lan}`):e.style.display="none"),r&&(v.tailscale?(r.dataset.ip=v.tailscale,r.textContent=`Tailscale (${v.tailscale})`,r.title=`Tailscale IP: ${v.tailscale}`):r.style.display="none");const f=document.getElementById("service-ip-input");f&&!f.value&&v.lan&&(f.value=v.lan)}function s(){document.querySelectorAll(".quick-ip-btn").forEach(e=>{e.addEventListener("click",()=>{const r=e.dataset.ip;r&&(document.getElementById("service-ip-input").value=r,document.querySelectorAll(".quick-ip-btn").forEach(f=>f.classList.remove("active")),e.classList.add("active"),p())})}),document.getElementById("service-ip-input")?.addEventListener("input",e=>{const r=e.target.value;document.querySelectorAll(".quick-ip-btn").forEach(f=>{f.classList.toggle("active",f.dataset.ip===r)})})}async function m(){const e=document.getElementById("add-service-modal");e.classList.add("show");const r=e.querySelector(".weather-modal-content");r&&(r.scrollTop=0),document.body.style.overflow="hidden";const f=document.getElementById("ssl-type-select");f&&(f.value=y()),await i();const n=document.getElementById("caddyfile-path-input").value||DC.DEFAULTS.CADDYFILE;await window.loadExistingCAs(n);const u=document.getElementById("manual-tailscale-status"),b=document.getElementById("manual-tailscale-only");try{const E=await(await fetch("/api/v1/tailscale/status")).json();E.success&&E.installed&&E.connected?(u.innerHTML=`
\u2713 Connected${E.self?.hostname} (${E.self?.ip})
- `,v.disabled=!1):E.installed?(c.innerHTML='\u26A0 Not connected',v.disabled=!0):(c.innerHTML='Not available',v.disabled=!0)}catch{c.innerHTML='Could not check',v.disabled=!0}v.checked=!1,u()}function a(){const e=document.getElementById("service-type-local"),s=document.getElementById("service-type-external"),p=document.getElementById("local-service-config"),t=document.getElementById("external-service-config"),c=document.getElementById("tab-local"),v=document.getElementById("tab-external");function i(){e.checked?(p.style.display="grid",t.style.display="none",c&&(c.style.background="var(--accent)",c.style.color="var(--bg)"),v&&(v.style.background="transparent",v.style.color="var(--muted)")):(p.style.display="none",t.style.display="block",v&&(v.style.background="var(--accent)",v.style.color="var(--bg)"),c&&(c.style.background="transparent",c.style.color="var(--muted)"))}e?.addEventListener("change",i),s?.addEventListener("change",i)}function b(){const e=document.getElementById("service-name-input"),s=document.getElementById("service-subdomain-input"),p=document.getElementById("subdomain-preview");let t=!1;e?.addEventListener("input",()=>{const L=o(e.value);!t&&s&&(s.value=L),p&&(p.textContent=L?`\u2192 ${buildDomain(L)}`:""),u()}),s?.addEventListener("input",()=>{t=s.value!==o(e?.value||"");const L=s.value.trim()||o(e?.value||"");p&&(p.textContent=L?`\u2192 ${buildDomain(L)}`:""),u()});const c=document.getElementById("external-service-name"),v=document.getElementById("external-service-subdomain"),i=document.getElementById("external-subdomain-preview"),E=document.getElementById("external-domain-preview");let T=!1;c?.addEventListener("input",()=>{const L=o(c.value);!T&&v&&(v.value=L);const P=v?.value||L;i&&(i.textContent=P?`\u2192 ${buildDomain(P)}`:""),E&&(E.textContent=P?buildDomain(P):"")}),v?.addEventListener("input",()=>{T=v.value!==o(c?.value||"");const L=v.value.trim()||o(c?.value||"");i&&(i.textContent=L?`\u2192 ${buildDomain(L)}`:""),E&&(E.textContent=L?buildDomain(L):"")})}async function m(){const e=document.getElementById("external-service-name").value.trim(),s=document.getElementById("external-service-url").value.trim(),p=(document.getElementById("external-service-subdomain").value.trim()||o(e)).toLowerCase(),t=document.getElementById("external-service-logo").value.trim(),c=document.getElementById("external-service-icon").value.trim(),v=document.getElementById("external-create-dns").checked,i=document.getElementById("external-create-caddy").checked,E=document.getElementById("external-proxy-ip").value.trim()||SITE.dnsIp||"localhost",T=document.getElementById("external-preserve-host").checked,L=document.getElementById("external-follow-redirects").checked,P=document.getElementById("external-service-category")?.value||"";if(!e||!s){showNotification("Please fill in Name and External URL","warning");return}if(!p){showNotification("Could not derive subdomain from name. Please set one in Options.","warning");return}if(!s.startsWith("http://")&&!s.startsWith("https://")){showNotification("External URL must start with http:// or https://","warning");return}const S=buildDomain(p);try{const B={dns:null,caddy:null,dashboard:!1};if(v)if(window.getToken(getPrimaryDnsId(),"admin"))try{const A=await(await secureFetch("/api/v1/dns/record",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:S,ip:E,ttl:DC.DEFAULTS.TTL,server:SITE.dnsIp})})).json();B.dns=A.success?"created":A.error||"failed"}catch(I){B.dns=I.message}else B.dns="no admin token (configure in \u{1F511} Tokens)";if(i)try{const k={subdomain:p,externalUrl:s,preserveHost:T,followRedirects:L,sslType:"caddy-managed",caddyfilePath:DC.DEFAULTS.CADDYFILE,reloadCaddy:!0},A=await(await secureFetch("/api/v1/site/external",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(k)})).json();B.caddy=A.success?"created":A.error||"failed"}catch(k){B.caddy=k.message}const $={id:p,name:e,url:`https://${S}`,externalUrl:s,logo:t||c||"\u{1F310}",isExternal:!0,isCustom:!0};P&&($.category=P),window.APPS.push($),B.dashboard=!0;const x=["plex","router","chat","sync","torrent","radarr","sonarr","prowlarr","portainer","requests","jellyfin","emby"],w=window.APPS.filter(k=>!x.includes(k.id));safeSet("custom-services",JSON.stringify(w));try{await secureFetch("/api/v1/services",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(window.APPS)})}catch(k){console.warn("Failed to save to services.json:",k)}window.buildGrid(),window.refreshAll(),n();const C=[`External service "${e}" added!`];v&&C.push(`DNS: ${B.dns==="created"?"\u2713":"\u26A0 "+B.dns}`),i&&C.push(`Caddy: ${B.caddy==="created"?"\u2713":"\u26A0 "+B.caddy}`),C.push(`Access at: https://${S}`),showNotification(C.join(" | "),"success",6e3)}catch(B){console.error("Failed to create external service:",B),showNotification(`Failed to create external service: ${B.message}`,"error")}}function n(){closeModal("add-service-modal"),document.body.style.overflow="",document.getElementById("service-name-input").value="",document.getElementById("service-subdomain-input").value="",document.getElementById("service-port-input").value="",document.getElementById("service-ip-input").value=y.lan||"",document.getElementById("service-logo-input").value="",document.getElementById("dns-ttl-input").value=DC.DEFAULTS.TTL,document.getElementById("ssl-type-select").value=f(),document.getElementById("ca-name-input").value="",document.getElementById("enable-auth").checked=!1,document.getElementById("enable-cors").checked=!1,document.getElementById("custom-headers-input").value="",document.getElementById("upstream-path-input").value="/",document.getElementById("health-check-input").value="",document.getElementById("timeout-input").value="30";const e=document.getElementById("subdomain-preview");e&&(e.textContent="");const s=document.getElementById("external-subdomain-preview");s&&(s.textContent="");const p=document.getElementById("external-service-name");p&&(p.value="");const t=document.getElementById("external-service-subdomain");t&&(t.value="");const c=document.getElementById("external-service-url");c&&(c.value="");const v=document.getElementById("external-service-logo");v&&(v.value="");const i=document.getElementById("external-service-icon");i&&(i.value="");const E=document.getElementById("local-advanced-options");E&&E.removeAttribute("open");const T=document.getElementById("external-advanced-options");T&&T.removeAttribute("open");const L=document.getElementById("service-type-local");L&&(L.checked=!0);const P=document.getElementById("local-service-config"),S=document.getElementById("external-service-config");P&&(P.style.display="grid"),S&&(S.style.display="none");const B=document.getElementById("tab-local"),$=document.getElementById("tab-external");B&&(B.style.background="var(--accent)",B.style.color="var(--bg)"),$&&($.style.background="transparent",$.style.color="var(--muted)")}async function d(){const e=document.getElementById("service-name-input").value.trim(),s=(document.getElementById("service-subdomain-input").value.trim()||o(e)).toLowerCase(),p=document.getElementById("service-port-input").value.trim(),t=document.getElementById("service-ip-input").value.trim(),c=document.getElementById("service-logo-input").value.trim(),v=document.getElementById("create-dns-record").checked,i=parseInt(document.getElementById("dns-ttl-input").value)||DC.DEFAULTS.TTL,E=document.getElementById("manual-tailscale-only")?.checked||!1,T=document.getElementById("ssl-type-select")?.value||"caddy-managed",L=document.getElementById("ca-name-input")?.value||"",P=document.getElementById("existing-ca-select")?.value||"",S=document.getElementById("enable-auth")?.checked||!1,B=document.getElementById("enable-cors")?.checked||!1,$=document.getElementById("custom-headers-input")?.value||"",x=document.getElementById("upstream-path-input")?.value||"/",w=document.getElementById("health-check-input")?.value||"",C=document.getElementById("timeout-input")?.value||30,I=(document.getElementById("service-category-input")||document.getElementById("external-service-category"))?.value||"",A=window.getToken(getPrimaryDnsId(),"admin");if(!e||!p||!t){showNotification("Please fill in Name, Port, and IP Address","warning");return}if(!s){showNotification("Could not derive subdomain from name. Please set one in Options.","warning");return}if(v&&!A){showNotification("DNS Admin token required. Configure it in the Tokens menu first.","warning");return}const O={dns:null,caddy:null,dashboard:!1};try{if(v)try{await window.createDnsRecord(s,t,i),O.dns="created"}catch(N){throw console.error("DNS creation failed:",N),O.dns=N.message,new Error(`DNS creation failed: ${N.message}`)}else O.dns="skipped";const D=window.generateCaddyConfig({subdomain:s,port:p,ip:t,sslType:T,caName:L,existingCa:P,enableAuth:S,enableCors:B,customHeaders:$,upstreamPath:x,healthCheck:w,timeout:C,tailscaleOnly:E});try{const U=await(await secureFetch("/api/v1/site",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:buildDomain(s),upstream:`${t}:${p}`,config:D})})).json();if(U.success)O.caddy="added & reloaded";else throw console.error("Caddy configuration failed:",U.error),O.caddy=U.error||"failed",new Error(`Caddy configuration failed: ${U.error}`)}catch(N){throw console.error("Caddy API error:",N),O.caddy=N.message,new Error(`Caddy API error: ${N.message}`)}const M={name:e,subdomain:s,port:p,ip:t,logo:c||`/assets/${s}.png`,tailscaleOnly:E||!1};I&&(M.category=I),await window.addServiceToConfig(M),O.dashboard=!0;const R=[`DNS: ${O.dns==="created"?"\u2713":O.dns==="skipped"?"\u25CB":"\u2717"}`,`Caddy: ${O.caddy==="added & reloaded"?"\u2713":"\u2717"}`,`Dashboard: ${O.dashboard?"\u2713":"\u2717"}`];showNotification(`Service "${e}" created! ${R.join(" | ")} \u2014 ${buildServiceUrl(s)}${E?" (Tailscale)":""}`,"success",6e3),n(),window.buildGrid(),window.refreshAll()}catch(D){console.error("Error creating service:",D),showNotification(`Error creating "${e}": ${D.message}`,"error",6e3)}}document.getElementById("add-service")?.addEventListener("click",h),document.getElementById("add-service-cancel")?.addEventListener("click",n),document.getElementById("add-service-create")?.addEventListener("click",()=>{document.querySelector('input[name="service-type"]:checked')?.value==="external"?m():d()}),a(),b(),r(),document.getElementById("ssl-type-select")?.addEventListener("change",e=>{const s=document.getElementById("existing-ca-config"),p=document.getElementById("custom-ca-config");s.style.display="none",p.style.display="none",e.target.value==="existing-ca"?s.style.display="block":e.target.value==="custom-ca"&&(p.style.display="block"),u()}),document.getElementById("refresh-cas")?.addEventListener("click",async()=>{const e=document.getElementById("refresh-cas"),s=e.textContent;e.textContent="\u231B Loading...",e.disabled=!0;try{const p=document.getElementById("caddyfile-path-input").value||DC.DEFAULTS.CADDYFILE;await window.loadExistingCAs(p),e.textContent="\u2705 Refreshed"}catch(p){e.textContent="\u274C Failed",console.error("Failed to refresh CAs:",p)}setTimeout(()=>{e.textContent=s,e.disabled=!1},2e3)}),document.getElementById("create-dns-record")?.addEventListener("change",e=>{const s=document.getElementById("dns-config");s.style.display=e.target.checked?"block":"none"}),["service-subdomain-input","service-ip-input","service-port-input","ca-name-input","existing-ca-select","enable-auth","enable-cors","custom-headers-input","upstream-path-input","health-check-input","timeout-input"].forEach(e=>{const s=document.getElementById(e);s&&(s.addEventListener("input",u),s.addEventListener("change",u))});function g(){const e=safeGet("custom-services");if(e)try{JSON.parse(e).forEach(p=>{window.APPS.find(t=>t.id===p.id)||window.APPS.push(p)})}catch(s){console.warn("Failed to load custom services:",s)}}g(),window.openAddServiceModal=h,window.closeAddServiceModal=n})(),(function(){let o=null,f=1e3;const u=3e4;function y(){if(o)try{o.close()}catch{}o=new EventSource("/api/v1/events/stream"),o.addEventListener("connected",()=>{f=1e3,debug("[SSE] Connected to event stream")}),o.addEventListener("status-change",l=>{try{const r=JSON.parse(l.data);if(r.serviceId&&typeof window.setBadge=="function"){const h=r.status==="up"||r.status==="healthy";window.setBadge(r.serviceId,h,r.responseTime||null)}}catch{}}),o.addEventListener("resource-alert",l=>{try{const r=JSON.parse(l.data),h=`${r.containerName||r.containerId}: ${r.metric} at ${r.value}% (threshold: ${r.threshold}%)`;typeof showNotification=="function"&&showNotification(h,"warning")}catch{}}),o.addEventListener("auto-restart",l=>{try{const r=JSON.parse(l.data);typeof showNotification=="function"&&showNotification(`Container "${r.containerName}" was auto-restarted`,"info")}catch{}}),o.addEventListener("update-available",l=>{try{const r=JSON.parse(l.data),h=document.getElementById("updates-btn");if(h&&!h.querySelector(".sse-dot")){const a=document.createElement("span");a.className="sse-dot",a.style.cssText="display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--accent);margin-left:6px;vertical-align:middle;",h.appendChild(a)}typeof showNotification=="function"&&showNotification(`Update available for ${r.containerName||r.containerId}`,"info")}catch{}}),o.addEventListener("update-complete",l=>{try{const r=JSON.parse(l.data);typeof showNotification=="function"&&showNotification(`Update completed: ${r.containerName||r.containerId}`,"success"),typeof window.refreshAll=="function"&&window.refreshAll()}catch{}}),o.addEventListener("update-failed",l=>{try{const r=JSON.parse(l.data);typeof showNotification=="function"&&showNotification(`Update failed: ${r.containerName||r.containerId} \u2014 ${r.error||"unknown error"}`,"error")}catch{}}),o.addEventListener("incident",l=>{try{const r=JSON.parse(l.data);typeof showNotification=="function"&&(r.type==="created"?showNotification(`Incident: ${r.message||r.serviceId}`,"error"):r.type==="resolved"&&showNotification(`Resolved: ${r.serviceId||"incident"}`,"success"))}catch{}}),o.onerror=()=>{o.close(),console.warn(`[SSE] Disconnected, reconnecting in ${f/1e3}s...`),setTimeout(y,f),f=Math.min(f*2,u)}}y(),window._sseReconnect=y})(),(function(){const o=document.getElementById("service-filter-search"),f=document.getElementById("service-filter-status"),u=document.getElementById("service-filter-category"),y=document.getElementById("service-filter-count");function l(){const b=new Set,m=new Set;document.querySelectorAll("#cards .card[data-category]").forEach(g=>{const e=g.dataset.category.trim();e&&m.add(e)});const n=window.DC_CATEGORIES||typeof DC<"u"&&DC.CATEGORIES||{};return Object.keys(n).concat([...m].filter(g=>!n[g])).forEach(g=>b.add(g)),{list:[...b],apiCats:n}}function r(){if(!u)return;const{list:b,apiCats:m}=l(),n=u.value;u.innerHTML='',b.sort().forEach(d=>{const g=m[d],e=document.createElement("option");e.value=d,e.textContent=g?`${g.icon||""} ${d}`.trim():d,u.appendChild(e)}),n&&[...u.options].some(d=>d.value===n)?u.value=n:u.value="all"}function h(){r();const b=o.value.toLowerCase().trim(),m=f.value,n=u?u.value:"all",d=document.querySelectorAll("#cards .card");let g=0;if(d.forEach(e=>{const s=e.querySelector(".name")?.textContent?.toLowerCase()||"",p=e.dataset.app?.toLowerCase()||"",t=e.dataset.status||"off",c=e.dataset.category||"";(!b||s.includes(b)||p.includes(b))&&(m==="all"||t===m)&&(n==="all"||c===n)?(e.style.display="",g++):e.style.display="none"}),y){const e=d.length;y.textContent=`${g} of ${e} services`}}function a(b,m){let n;return function(...d){clearTimeout(n),n=setTimeout(()=>b.apply(this,d),m)}}o?.addEventListener("input",a(h,200)),f?.addEventListener("change",h),u?.addEventListener("change",h),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>setTimeout(h,500)):setTimeout(h,500),window.refreshServiceFilter=h,window.refreshCategoryDropdown=r})(),(function(){const o=document.getElementById("batch-operations-btn"),f=document.getElementById("batch-action-bar"),u=document.getElementById("batch-selected-count"),y=document.getElementById("batch-start-btn"),l=document.getElementById("batch-stop-btn"),r=document.getElementById("batch-restart-btn"),h=document.getElementById("batch-cancel-btn");let a=!1,b=new Set;function m(){a=!0,b.clear(),f.style.display="",o.textContent="\u2713 Exit Batch Mode",d(),document.querySelectorAll("#cards .card[data-app]").forEach(s=>{const p=s.dataset.containerId;if(!p)return;const t=s.querySelector(".batch-checkbox");t&&t.remove();const c=document.createElement("input");c.type="checkbox",c.className="batch-checkbox",c.dataset.containerId=p,c.dataset.serviceName=s.querySelector(".name")?.textContent||p,c.style.cssText="position: absolute; top: 8px; left: 8px; z-index: 10; width: 18px; height: 18px; cursor: pointer;",c.addEventListener("change",v=>{v.stopPropagation(),c.checked?b.add(p):b.delete(p),d()}),s.style.position="relative",s.insertBefore(c,s.firstChild)})}function n(){a=!1,b.clear(),f.style.display="none",o.textContent="\u2630 Batch Operations",document.querySelectorAll(".batch-checkbox").forEach(e=>e.remove())}function d(){const e=b.size;u.textContent=`${e} selected`,y.disabled=e===0,l.disabled=e===0,r.disabled=e===0}async function g(e){if(b.size===0)return;const s=Array.from(b),p={start:"Starting",stop:"Stopping",restart:"Restarting"}[e];if(!confirm(`${p} ${s.length} container(s)? This cannot be undone.`))return;const t=[y,l,r];t.forEach(E=>{E.disabled=!0,E.textContent="..."});let c=0,v=0;const i=[];for(const E of s)try{const T=await fetch(`/api/v1/containers/${encodeURIComponent(E)}/${e}`,{method:"POST"});if(T.ok)c++;else{v++;const L=await T.json().catch(()=>({}));i.push(`${E}: ${L.error||T.statusText}`)}}catch(T){v++,i.push(`${E}: ${T.message}`)}t[0].textContent="\u25B6 Start All",t[1].textContent="\u2B1B Stop All",t[2].textContent="\u{1F504} Restart All",d(),v===0?typeof showNotification=="function"&&showNotification(`${p} completed: ${c} container(s)`,"success"):(typeof showNotification=="function"&&showNotification(`${p}: ${c} succeeded, ${v} failed`,"warning"),console.error("Batch operation errors:",i)),setTimeout(()=>{typeof refreshAll=="function"&&refreshAll()},1500)}o?.addEventListener("click",()=>{a?n():m()}),y?.addEventListener("click",()=>g("start")),l?.addEventListener("click",()=>g("stop")),r?.addEventListener("click",()=>g("restart")),h?.addEventListener("click",n)})();
+ `,b.disabled=!1):E.installed?(u.innerHTML='\u26A0 Not connected',b.disabled=!0):(u.innerHTML='Not available',b.disabled=!0)}catch{u.innerHTML='Could not check',b.disabled=!0}b.checked=!1,p()}function t(){const e=document.getElementById("service-type-local"),r=document.getElementById("service-type-external"),f=document.getElementById("local-service-config"),n=document.getElementById("external-service-config"),u=document.getElementById("tab-local"),b=document.getElementById("tab-external");function d(){e.checked?(f.style.display="grid",n.style.display="none",u&&(u.style.background="var(--accent)",u.style.color="var(--bg)"),b&&(b.style.background="transparent",b.style.color="var(--muted)")):(f.style.display="none",n.style.display="block",b&&(b.style.background="var(--accent)",b.style.color="var(--bg)"),u&&(u.style.background="transparent",u.style.color="var(--muted)"))}e?.addEventListener("change",d),r?.addEventListener("change",d)}function h(){const e=document.getElementById("service-name-input"),r=document.getElementById("service-subdomain-input"),f=document.getElementById("subdomain-preview");let n=!1;e?.addEventListener("input",()=>{const L=a(e.value);!n&&r&&(r.value=L),f&&(f.textContent=L?`\u2192 ${buildDomain(L)}`:""),p()}),r?.addEventListener("input",()=>{n=r.value!==a(e?.value||"");const L=r.value.trim()||a(e?.value||"");f&&(f.textContent=L?`\u2192 ${buildDomain(L)}`:""),p()});const u=document.getElementById("external-service-name"),b=document.getElementById("external-service-subdomain"),d=document.getElementById("external-subdomain-preview"),E=document.getElementById("external-domain-preview");let T=!1;u?.addEventListener("input",()=>{const L=a(u.value);!T&&b&&(b.value=L);const P=b?.value||L;d&&(d.textContent=P?`\u2192 ${buildDomain(P)}`:""),E&&(E.textContent=P?buildDomain(P):"")}),b?.addEventListener("input",()=>{T=b.value!==a(u?.value||"");const L=b.value.trim()||a(u?.value||"");d&&(d.textContent=L?`\u2192 ${buildDomain(L)}`:""),E&&(E.textContent=L?buildDomain(L):"")})}async function c(){const e=document.getElementById("external-service-name").value.trim(),r=document.getElementById("external-service-url").value.trim(),f=(document.getElementById("external-service-subdomain").value.trim()||a(e)).toLowerCase(),n=document.getElementById("external-service-logo").value.trim(),u=document.getElementById("external-service-icon").value.trim(),b=document.getElementById("external-create-dns").checked,d=document.getElementById("external-create-caddy").checked,E=document.getElementById("external-proxy-ip").value.trim()||SITE.dnsIp||"localhost",T=document.getElementById("external-preserve-host").checked,L=document.getElementById("external-follow-redirects").checked,P=document.getElementById("external-service-category")?.value||"";if(!e||!r){showNotification("Please fill in Name and External URL","warning");return}if(!f){showNotification("Could not derive subdomain from name. Please set one in Options.","warning");return}if(!r.startsWith("http://")&&!r.startsWith("https://")){showNotification("External URL must start with http:// or https://","warning");return}const C=buildDomain(f);try{const B={dns:null,caddy:null,dashboard:!1};if(b)if(window.getToken(getPrimaryDnsId(),"admin"))try{const A=await(await secureFetch("/api/v1/dns/record",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:C,ip:E,ttl:DC.DEFAULTS.TTL,server:SITE.dnsIp})})).json();B.dns=A.success?"created":A.error||"failed"}catch(S){B.dns=S.message}else B.dns="no admin token (configure in \u{1F511} Tokens)";if(d)try{const k={subdomain:f,externalUrl:r,preserveHost:T,followRedirects:L,sslType:"caddy-managed",caddyfilePath:DC.DEFAULTS.CADDYFILE,reloadCaddy:!0},A=await(await secureFetch("/api/v1/site/external",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(k)})).json();B.caddy=A.success?"created":A.error||"failed"}catch(k){B.caddy=k.message}const $={id:f,name:e,url:`https://${C}`,externalUrl:r,logo:n||u||"\u{1F310}",isExternal:!0,isCustom:!0};P&&($.category=P),window.APPS.push($),B.dashboard=!0;const x=["plex","router","chat","sync","torrent","radarr","sonarr","prowlarr","portainer","requests","jellyfin","emby"],w=window.APPS.filter(k=>!x.includes(k.id));safeSet("custom-services",JSON.stringify(w));try{await secureFetch("/api/v1/services",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(window.APPS)})}catch(k){console.warn("Failed to save to services.json:",k)}window.buildGrid(),window.refreshAll(),o();const I=[`External service "${e}" added!`];b&&I.push(`DNS: ${B.dns==="created"?"\u2713":"\u26A0 "+B.dns}`),d&&I.push(`Caddy: ${B.caddy==="created"?"\u2713":"\u26A0 "+B.caddy}`),I.push(`Access at: https://${C}`),showNotification(I.join(" | "),"success",6e3)}catch(B){console.error("Failed to create external service:",B),showNotification(`Failed to create external service: ${B.message}`,"error")}}function o(){closeModal("add-service-modal"),document.body.style.overflow="",document.getElementById("service-name-input").value="",document.getElementById("service-subdomain-input").value="",document.getElementById("service-port-input").value="",document.getElementById("service-ip-input").value=v.lan||"",document.getElementById("service-logo-input").value="",document.getElementById("dns-ttl-input").value=DC.DEFAULTS.TTL,document.getElementById("ssl-type-select").value=y(),document.getElementById("ca-name-input").value="",document.getElementById("enable-auth").checked=!1,document.getElementById("enable-cors").checked=!1,document.getElementById("custom-headers-input").value="",document.getElementById("upstream-path-input").value="/",document.getElementById("health-check-input").value="",document.getElementById("timeout-input").value="30";const e=document.getElementById("subdomain-preview");e&&(e.textContent="");const r=document.getElementById("external-subdomain-preview");r&&(r.textContent="");const f=document.getElementById("external-service-name");f&&(f.value="");const n=document.getElementById("external-service-subdomain");n&&(n.value="");const u=document.getElementById("external-service-url");u&&(u.value="");const b=document.getElementById("external-service-logo");b&&(b.value="");const d=document.getElementById("external-service-icon");d&&(d.value="");const E=document.getElementById("local-advanced-options");E&&E.removeAttribute("open");const T=document.getElementById("external-advanced-options");T&&T.removeAttribute("open");const L=document.getElementById("service-type-local");L&&(L.checked=!0);const P=document.getElementById("local-service-config"),C=document.getElementById("external-service-config");P&&(P.style.display="grid"),C&&(C.style.display="none");const B=document.getElementById("tab-local"),$=document.getElementById("tab-external");B&&(B.style.background="var(--accent)",B.style.color="var(--bg)"),$&&($.style.background="transparent",$.style.color="var(--muted)")}async function l(){const e=document.getElementById("service-name-input").value.trim(),r=(document.getElementById("service-subdomain-input").value.trim()||a(e)).toLowerCase(),f=document.getElementById("service-port-input").value.trim(),n=document.getElementById("service-ip-input").value.trim(),u=document.getElementById("service-logo-input").value.trim(),b=document.getElementById("create-dns-record").checked,d=parseInt(document.getElementById("dns-ttl-input").value)||DC.DEFAULTS.TTL,E=document.getElementById("manual-tailscale-only")?.checked||!1,T=document.getElementById("ssl-type-select")?.value||"caddy-managed",L=document.getElementById("ca-name-input")?.value||"",P=document.getElementById("existing-ca-select")?.value||"",C=document.getElementById("enable-auth")?.checked||!1,B=document.getElementById("enable-cors")?.checked||!1,$=document.getElementById("custom-headers-input")?.value||"",x=document.getElementById("upstream-path-input")?.value||"/",w=document.getElementById("health-check-input")?.value||"",I=document.getElementById("timeout-input")?.value||30,S=(document.getElementById("service-category-input")||document.getElementById("external-service-category"))?.value||"",A=window.getToken(getPrimaryDnsId(),"admin");if(!e||!f||!n){showNotification("Please fill in Name, Port, and IP Address","warning");return}if(!r){showNotification("Could not derive subdomain from name. Please set one in Options.","warning");return}if(b&&!A){showNotification("DNS Admin token required. Configure it in the Tokens menu first.","warning");return}const O={dns:null,caddy:null,dashboard:!1};try{if(b)try{await window.createDnsRecord(r,n,d),O.dns="created"}catch(N){throw console.error("DNS creation failed:",N),O.dns=N.message,new Error(`DNS creation failed: ${N.message}`)}else O.dns="skipped";const D=window.generateCaddyConfig({subdomain:r,port:f,ip:n,sslType:T,caName:L,existingCa:P,enableAuth:C,enableCors:B,customHeaders:$,upstreamPath:x,healthCheck:w,timeout:I,tailscaleOnly:E});try{const U=await(await secureFetch("/api/v1/site",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:buildDomain(r),upstream:`${n}:${f}`,config:D})})).json();if(U.success)O.caddy="added & reloaded";else throw console.error("Caddy configuration failed:",U.error),O.caddy=U.error||"failed",new Error(`Caddy configuration failed: ${U.error}`)}catch(N){throw console.error("Caddy API error:",N),O.caddy=N.message,new Error(`Caddy API error: ${N.message}`)}const M={name:e,subdomain:r,port:f,ip:n,logo:u||`/assets/${r}.png`,tailscaleOnly:E||!1};S&&(M.category=S),await window.addServiceToConfig(M),O.dashboard=!0;const R=[`DNS: ${O.dns==="created"?"\u2713":O.dns==="skipped"?"\u25CB":"\u2717"}`,`Caddy: ${O.caddy==="added & reloaded"?"\u2713":"\u2717"}`,`Dashboard: ${O.dashboard?"\u2713":"\u2717"}`];showNotification(`Service "${e}" created! ${R.join(" | ")} \u2014 ${buildServiceUrl(r)}${E?" (Tailscale)":""}`,"success",6e3),o(),window.buildGrid(),window.refreshAll()}catch(D){console.error("Error creating service:",D),showNotification(`Error creating "${e}": ${D.message}`,"error",6e3)}}document.getElementById("add-service")?.addEventListener("click",m),document.getElementById("add-service-cancel")?.addEventListener("click",o),document.getElementById("add-service-create")?.addEventListener("click",()=>{document.querySelector('input[name="service-type"]:checked')?.value==="external"?c():l()}),t(),h(),s(),document.getElementById("ssl-type-select")?.addEventListener("change",e=>{const r=document.getElementById("existing-ca-config"),f=document.getElementById("custom-ca-config");r.style.display="none",f.style.display="none",e.target.value==="existing-ca"?r.style.display="block":e.target.value==="custom-ca"&&(f.style.display="block"),p()}),document.getElementById("refresh-cas")?.addEventListener("click",async()=>{const e=document.getElementById("refresh-cas"),r=e.textContent;e.textContent="\u231B Loading...",e.disabled=!0;try{const f=document.getElementById("caddyfile-path-input").value||DC.DEFAULTS.CADDYFILE;await window.loadExistingCAs(f),e.textContent="\u2705 Refreshed"}catch(f){e.textContent="\u274C Failed",console.error("Failed to refresh CAs:",f)}setTimeout(()=>{e.textContent=r,e.disabled=!1},2e3)}),document.getElementById("create-dns-record")?.addEventListener("change",e=>{const r=document.getElementById("dns-config");r.style.display=e.target.checked?"block":"none"}),["service-subdomain-input","service-ip-input","service-port-input","ca-name-input","existing-ca-select","enable-auth","enable-cors","custom-headers-input","upstream-path-input","health-check-input","timeout-input"].forEach(e=>{const r=document.getElementById(e);r&&(r.addEventListener("input",p),r.addEventListener("change",p))});function g(){const e=safeGet("custom-services");if(e)try{JSON.parse(e).forEach(f=>{window.APPS.find(n=>n.id===f.id)||window.APPS.push(f)})}catch(r){console.warn("Failed to load custom services:",r)}}g(),window.openAddServiceModal=m,window.closeAddServiceModal=o})(),(function(){let a=null,y=1e3;const p=3e4;function v(){if(a)try{a.close()}catch{}a=new EventSource("/api/v1/events/stream"),a.addEventListener("connected",()=>{y=1e3,debug("[SSE] Connected to event stream")}),a.addEventListener("status-change",i=>{try{const s=JSON.parse(i.data);if(s.serviceId&&typeof window.setBadge=="function"){const m=s.status==="up"||s.status==="healthy";window.setBadge(s.serviceId,m,s.responseTime||null)}}catch{}}),a.addEventListener("resource-alert",i=>{try{const s=JSON.parse(i.data),m=`${s.containerName||s.containerId}: ${s.metric} at ${s.value}% (threshold: ${s.threshold}%)`;typeof showNotification=="function"&&showNotification(m,"warning")}catch{}}),a.addEventListener("auto-restart",i=>{try{const s=JSON.parse(i.data);typeof showNotification=="function"&&showNotification(`Container "${s.containerName}" was auto-restarted`,"info")}catch{}}),a.addEventListener("update-available",i=>{try{const s=JSON.parse(i.data),m=document.getElementById("updates-btn");if(m&&!m.querySelector(".sse-dot")){const t=document.createElement("span");t.className="sse-dot",t.style.cssText="display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--accent);margin-left:6px;vertical-align:middle;",m.appendChild(t)}typeof showNotification=="function"&&showNotification(`Update available for ${s.containerName||s.containerId}`,"info")}catch{}}),a.addEventListener("update-complete",i=>{try{const s=JSON.parse(i.data);typeof showNotification=="function"&&showNotification(`Update completed: ${s.containerName||s.containerId}`,"success"),typeof window.refreshAll=="function"&&window.refreshAll()}catch{}}),a.addEventListener("update-failed",i=>{try{const s=JSON.parse(i.data);typeof showNotification=="function"&&showNotification(`Update failed: ${s.containerName||s.containerId} \u2014 ${s.error||"unknown error"}`,"error")}catch{}}),a.addEventListener("incident",i=>{try{const s=JSON.parse(i.data);typeof showNotification=="function"&&(s.type==="created"?showNotification(`Incident: ${s.message||s.serviceId}`,"error"):s.type==="resolved"&&showNotification(`Resolved: ${s.serviceId||"incident"}`,"success"))}catch{}}),a.onerror=()=>{a.close(),console.warn(`[SSE] Disconnected, reconnecting in ${y/1e3}s...`),setTimeout(v,y),y=Math.min(y*2,p)}}v(),window._sseReconnect=v})(),(function(){const a=document.getElementById("service-filter-search"),y=document.getElementById("service-filter-status"),p=document.getElementById("service-filter-category"),v=document.getElementById("service-filter-count");function i(){const h=new Set,c=new Set;document.querySelectorAll("#cards .card[data-category]").forEach(g=>{const e=g.dataset.category.trim();e&&c.add(e)});const o=window.DC_CATEGORIES||typeof DC<"u"&&DC.CATEGORIES||{};return Object.keys(o).concat([...c].filter(g=>!o[g])).forEach(g=>h.add(g)),{list:[...h],apiCats:o}}function s(){if(!p)return;const{list:h,apiCats:c}=i(),o=p.value;p.innerHTML='',h.sort().forEach(l=>{const g=c[l],e=document.createElement("option");e.value=l,e.textContent=g?`${g.icon||""} ${l}`.trim():l,p.appendChild(e)}),o&&[...p.options].some(l=>l.value===o)?p.value=o:p.value="all"}function m(){s();const h=a.value.toLowerCase().trim(),c=y.value,o=p?p.value:"all",l=document.querySelectorAll("#cards .card");let g=0;if(l.forEach(e=>{const r=e.querySelector(".name")?.textContent?.toLowerCase()||"",f=e.dataset.app?.toLowerCase()||"",n=e.dataset.status||"off",u=e.dataset.category||"";(!h||r.includes(h)||f.includes(h))&&(c==="all"||n===c)&&(o==="all"||u===o)?(e.style.display="",g++):e.style.display="none"}),v){const e=l.length;v.textContent=`${g} of ${e} services`}}function t(h,c){let o;return function(...l){clearTimeout(o),o=setTimeout(()=>h.apply(this,l),c)}}a?.addEventListener("input",t(m,200)),y?.addEventListener("change",m),p?.addEventListener("change",m),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>setTimeout(m,500)):setTimeout(m,500),window.refreshServiceFilter=m,window.refreshCategoryDropdown=s})(),(function(){const a=document.getElementById("batch-operations-btn"),y=document.getElementById("batch-action-bar"),p=document.getElementById("batch-selected-count"),v=document.getElementById("batch-start-btn"),i=document.getElementById("batch-stop-btn"),s=document.getElementById("batch-restart-btn"),m=document.getElementById("batch-cancel-btn");let t=!1,h=new Set;function c(){t=!0,h.clear(),y.style.display="",a.textContent="\u2713 Exit Batch Mode",l(),document.querySelectorAll("#cards .card[data-app]").forEach(r=>{const f=r.dataset.containerId;if(!f)return;const n=r.querySelector(".batch-checkbox");n&&n.remove();const u=document.createElement("input");u.type="checkbox",u.className="batch-checkbox",u.dataset.containerId=f,u.dataset.serviceName=r.querySelector(".name")?.textContent||f,u.style.cssText="position: absolute; top: 8px; left: 8px; z-index: 10; width: 18px; height: 18px; cursor: pointer;",u.addEventListener("change",b=>{b.stopPropagation(),u.checked?h.add(f):h.delete(f),l()}),r.style.position="relative",r.insertBefore(u,r.firstChild)})}function o(){t=!1,h.clear(),y.style.display="none",a.textContent="\u2630 Batch Operations",document.querySelectorAll(".batch-checkbox").forEach(e=>e.remove())}function l(){const e=h.size;p.textContent=`${e} selected`,v.disabled=e===0,i.disabled=e===0,s.disabled=e===0}async function g(e){if(h.size===0)return;const r=Array.from(h),f={start:"Starting",stop:"Stopping",restart:"Restarting"}[e];if(!confirm(`${f} ${r.length} container(s)? This cannot be undone.`))return;const n=[v,i,s];n.forEach(E=>{E.disabled=!0,E.textContent="..."});let u=0,b=0;const d=[];for(const E of r)try{const T=await fetch(`/api/v1/containers/${encodeURIComponent(E)}/${e}`,{method:"POST"});if(T.ok)u++;else{b++;const L=await T.json().catch(()=>({}));d.push(`${E}: ${L.error||T.statusText}`)}}catch(T){b++,d.push(`${E}: ${T.message}`)}n[0].textContent="\u25B6 Start All",n[1].textContent="\u2B1B Stop All",n[2].textContent="\u{1F504} Restart All",l(),b===0?typeof showNotification=="function"&&showNotification(`${f} completed: ${u} container(s)`,"success"):(typeof showNotification=="function"&&showNotification(`${f}: ${u} succeeded, ${b} failed`,"warning"),console.error("Batch operation errors:",d)),setTimeout(()=>{typeof refreshAll=="function"&&refreshAll()},1500)}a?.addEventListener("click",()=>{t?o():c()}),v?.addEventListener("click",()=>g("start")),i?.addEventListener("click",()=>g("stop")),s?.addEventListener("click",()=>g("restart")),m?.addEventListener("click",o)})();
diff --git a/status/dist/features.js b/status/dist/features.js
index 5c1e7d8..86dc01e 100644
--- a/status/dist/features.js
+++ b/status/dist/features.js
@@ -127,7 +127,7 @@ This will reset the logo, favicon, title, and position.`))try{if((await secureFe
Timezone: ${g.replace(/_/g," ")}
- `,D+="",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 A(){const x={setupComplete:!0,configurationType:h,timestamp:new Date().toISOString(),timezone:document.getElementById("setup-timezone")?.value||Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC"};h==="homelab"?(x.tld=document.getElementById("setup-tld")?.value?.trim()||".home",x.caName=document.getElementById("setup-ca-name")?.value?.trim()||"",x.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()||""},x.defaults={dnsType:"private",sslType:"internal",targetIP:"localhost"}):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 g=h==="homelab"?"Professional Home Lab":h==="simple"?"Simple Setup":"Public Server",u=D?"server (shared across all devices)":"locally (this browser only)";showNotification(`Setup Complete! Configured for: ${g}. Settings saved to: ${u}`,"success",5e3),setTimeout(()=>location.reload(),500)}const v=document.getElementById("setup-step-1-next");v&&(v.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 L=document.getElementById("setup-skip");L&&(L.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 b=document.getElementById("setup-tld");b&&(b.oninput=function(x){const D=x.target.value||".home",g=document.getElementById("tld-preview"),u=document.getElementById("tld-preview-2");g&&(g.textContent=D),u&&(u.textContent=D)});const M=document.getElementById("setup-homelab-back");M&&(M.onclick=function(x){x.preventDefault(),N("setup-step-1")});const k=document.getElementById("setup-homelab-next");k&&(k.onclick=function(x){x.preventDefault();const D=document.getElementById("setup-tld")?.value?.trim()||"",g=document.getElementById("setup-ca-name")?.value?.trim()||"",u=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(!g){showNotification("Please enter a Certificate Authority name","warning");return}if(!u){showNotification("Please enter your DNS server IP address","warning");return}O()});const B=document.getElementById("setup-simple-back");B&&(B.onclick=function(x){x.preventDefault(),N("setup-step-1")});const S=document.getElementById("setup-simple-next");S&&(S.onclick=function(x){x.preventDefault(),O()}),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 T=document.getElementById("setup-public-back");T&&(T.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()||"",g=document.getElementById("setup-public-email")?.value?.trim()||"";if(!D){showNotification("Please enter your domain name","warning");return}if(!g||!g.includes("@")){showNotification("Please enter a valid email address","warning");return}O()});const H=document.getElementById("setup-summary-back");H&&(H.onclick=function(x){x.preventDefault(),h==="homelab"?N("setup-step-homelab"):h==="simple"?N("setup-step-simple"):h==="public"&&N("setup-step-public")});const R=document.getElementById("setup-finish");R&&(R.onclick=function(x){x.preventDefault(),A()}),window.getGlobalConfig=async function(){try{const D=await fetch("/api/v1/config");if(D.ok){const g=await D.json();if(g&&g.setupComplete)return g}}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",`
+ `,D+="
",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 A(){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 f=document.getElementById("setup-dns-provider")?.value||"technitium";x.dns={provider:f,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 g=h==="homelab"?"Professional Home Lab":h==="simple"?"Simple Setup":"Public Server",u=D?"server (shared across all devices)":"locally (this browser only)";showNotification(`Setup Complete! Configured for: ${g}. Settings saved to: ${u}`,"success",5e3),setTimeout(()=>location.reload(),500)}const v=document.getElementById("setup-step-1-next");v&&(v.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 L=document.getElementById("setup-skip");L&&(L.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 b=document.getElementById("setup-tld");b&&(b.oninput=function(x){const D=x.target.value||".home",g=document.getElementById("tld-preview"),u=document.getElementById("tld-preview-2");g&&(g.textContent=D),u&&(u.textContent=D)});const M=document.getElementById("setup-homelab-back");M&&(M.onclick=function(x){x.preventDefault(),N("setup-step-1")});const k=document.getElementById("setup-homelab-next");k&&(k.onclick=function(x){x.preventDefault();const D=document.getElementById("setup-tld")?.value?.trim()||"",g=document.getElementById("setup-ca-name")?.value?.trim()||"",u=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(!g){showNotification("Please enter a Certificate Authority name","warning");return}if(!u){showNotification("Please enter your DNS server IP address","warning");return}O()});const B=document.getElementById("setup-simple-back");B&&(B.onclick=function(x){x.preventDefault(),N("setup-step-1")});const S=document.getElementById("setup-simple-next");S&&(S.onclick=function(x){x.preventDefault(),O()}),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 T=document.getElementById("setup-public-back");T&&(T.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()||"",g=document.getElementById("setup-public-email")?.value?.trim()||"";if(!D){showNotification("Please enter your domain name","warning");return}if(!g||!g.includes("@")){showNotification("Please enter a valid email address","warning");return}O()});const H=document.getElementById("setup-summary-back");H&&(H.onclick=function(x){x.preventDefault(),h==="homelab"?N("setup-step-homelab"):h==="simple"?N("setup-step-simple"):h==="public"&&N("setup-step-public")});const R=document.getElementById("setup-finish");R&&(R.onclick=function(x){x.preventDefault(),A()}),window.getGlobalConfig=async function(){try{const D=await fetch("/api/v1/config");if(D.ok){const g=await D.json();if(g&&g.setupComplete)return g}}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",`