`,o.classList.add("show");const s=p.querySelector("#auth-gate-email-input"),e=p.querySelector("#auth-gate-email-submit"),n=p.querySelector("#auth-gate-email-status"),l=p.querySelector("#auth-gate-back");e.addEventListener("click",async()=>{const t=(s.value||"").trim();if(!t||!t.includes("@")){n.textContent="Enter a valid email address.",n.style.color="var(--error, #d33)";return}e.disabled=!0,n.textContent="Sending\u2026",n.style.color="";try{const y=await(await fetch("/api/v1/auth/login/email/initiate",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:t})})).json();if(y.success){const u=y.deliveredVia||"email",w=y.maskedEmail||t;u==="dev-console"?n.innerHTML=`
Check the server logs for your one-time link.
(Dev mode: no SMTP configured. In production this would email ${w}.)`:n.innerHTML=`Sign-in link sent to ${w}.
Check your inbox (and spam folder).`,n.style.color="var(--success, #2a7)"}else n.textContent=y.error||"Could not send link.",n.style.color="var(--error, #d33)",e.disabled=!1}catch{n.textContent="Connection error. Try again.",n.style.color="var(--error, #d33)",e.disabled=!1}}),s.addEventListener("keydown",t=>{t.key==="Enter"&&e.click()}),l.addEventListener("click",t=>{t.preventDefault(),f(providers)});return}console.warn("[auth-gate] unknown provider",d.name),window.location.reload()}}async function m(){if(!document.getElementById("totp-overlay"))return;const o=await a();if(o.length===0){typeof window._showTotpOverlay=="function"&&window._showTotpOverlay();return}if(o.length===1&&o[0].name==="totp"){b(o[0]);return}f(o)}function b(d){const o=c&&c.find(n=>n.name==="email");if(!o){typeof window._showTotpOverlay=="function"&&window._showTotpOverlay();return}const p=document.getElementById("totp-overlay"),s=p.querySelector(".totp-card");if(!s)return;if(s.dataset.originalBody||(s.dataset.originalBody=s.innerHTML),!s.querySelector("#auth-gate-email-alt")){const n=document.createElement("div");n.id="auth-gate-email-alt",n.style.cssText="margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--border); font-size: 0.85rem;",n.innerHTML=`
Or sign in with email instead \u2192
`,s.appendChild(n),n.querySelector("#auth-gate-email-alt-link").addEventListener("click",l=>{l.preventDefault(),g(o)})}p.classList.add("show")}window.__dc_049_handled=!0;function r(d){try{const o=new URL(d,window.location.origin);if(!["http:","https:"].includes(o.protocol))return!1;if(o.origin===window.location.origin)return!0;if(o.protocol!=="https:")return!1;const p=SITE.tld.startsWith(".")?SITE.tld:`.${SITE.tld}`;return o.hostname===p.slice(1)||o.hostname.endsWith(p)}catch{return!1}}const v=new URLSearchParams(window.location.search);if(v.get("auth")==="required"){const d=v.get("return");if(d&&r(d))try{sessionStorage.setItem("totp_redirect",d)}catch{}window.history.replaceState({},"",window.location.pathname),setTimeout(m,0)}window._showAuthGate=m,window._authGateMethods=a})(),(function(){function c(){const r=document.querySelector(".totp-card");if(!r)return;const d=getComputedStyle(r).backgroundColor.match(/\d+/g);if(!d)return;const o=(.299*+d[0]+.587*+d[1]+.114*+d[2])/255,p=r.querySelector(".totp-logo-dark"),s=r.querySelector(".totp-logo-light");p&&(p.style.display=o>.5?"none":""),s&&(s.style.display=o>.5?"":"none")}function a(){const r=document.getElementById("totp-overlay");if(r){r.classList.add("show"),setTimeout(c,50);const v=r.querySelector(".totp-digits input");v&&setTimeout(()=>v.focus(),100)}typeof window._refreshRecoveryLink=="function"&&window._refreshRecoveryLink()}function f(){const r=document.getElementById("totp-overlay");r&&r.classList.remove("show")}function h(r,v){const d=new URL(r,window.location.origin);if(d.origin===window.location.origin)return d.toString();const o=SITE.tld.startsWith(".")?SITE.tld:`.${SITE.tld}`,p=d.hostname===o.slice(1)||d.hostname.endsWith(o);if(d.protocol!=="https:"||!p)return null;if(!v)return d.toString();const s=`${d.pathname}${d.search}${d.hash}`;return d.pathname="/dashcaddy-sso",d.search="",d.hash="",d.searchParams.set("token",v),d.searchParams.set("return",s),d.toString()}const g=document.getElementById("totp-digits");if(g){const r=g.querySelectorAll("input");r.forEach((v,d)=>{v.addEventListener("input",o=>{const p=o.target.value.replace(/\D/g,"");o.target.value=p.slice(0,1),p&&de.value).join("");s.length===6&&m(s)}),v.addEventListener("keydown",o=>{o.key==="Backspace"&&!o.target.value&&d>0&&(r[d-1].focus(),r[d-1].value="")}),v.addEventListener("paste",o=>{o.preventDefault();const p=(o.clipboardData.getData("text")||"").replace(/\D/g,"");p.length>=6&&(r.forEach((s,e)=>{s.value=p[e]||""}),r[5].focus(),m(p.slice(0,6)))})})}async function m(r){const v=document.getElementById("totp-error");v.textContent="Verifying...",v.className="totp-error verifying";try{const o=await(await secureFetch("/api/v1/totp/verify",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:r})})).json();if(o.success){v.textContent="",o.csrfToken&&(csrfToken=o.csrfToken),f();const p=safeSessionGet("totp_redirect");if(p){try{sessionStorage.removeItem("totp_redirect")}catch{}const s=h(p,o.ssoToken);if(!s)return;window.location.href=s;return}typeof window.initializeDashboard=="function"&&window.initializeDashboard()}else{v.textContent=o.error||"Invalid code",v.className="totp-error";const p=document.querySelectorAll("#totp-digits input");p.forEach(s=>{s.value=""}),p[0]?.focus()}}catch{v.textContent="Connection error",v.className="totp-error"}}if(!!!window.__dc_049_handled&&urlParams.get("auth")==="required"){const r=urlParams.get("return");if(r)try{const v=new URL(r,window.location.origin),d=v.hostname,o=v.origin===window.location.origin,p=SITE.tld.startsWith(".")?SITE.tld:"."+SITE.tld,s=d.endsWith(p)||d===p.substring(1);(o||s)&&safeSessionSet("totp_redirect",r)}catch{}window.history.replaceState({},"",window.location.pathname)}window._showTotpOverlay=a})(),(function(){"use strict";async function c(){try{return await(await fetch("/api/v1/totp/recovery-info",{cache:"no-store"})).json()}catch{return{success:!1,status:"unknown",hint:"Could not contact server"}}}function a(b){const r=document.getElementById("totp-recovery-link");r&&(r.style.display=b?"":"none")}function f(){const b=document.getElementById("totp-recovery-panel");b&&(b.style.display="");const r=document.getElementById("totp-recovery-status"),v=document.getElementById("totp-recovery-import"),d=document.getElementById("totp-recovery-verify");v&&(v.style.display=""),d&&(d.style.display="none"),document.getElementById("totp-recovery-error").textContent="",document.getElementById("totp-recovery-confirm-error").textContent="",document.getElementById("totp-recovery-secret").value="",document.getElementById("totp-recovery-code").value="",c().then(o=>{r.textContent=o.hint||"",o.status==="healthy"?r.style.borderColor="var(--ok-fg, #7ef2ff)":o.status==="unreadable"?(r.style.borderColor="var(--bad-fg, #ff9aa3)",r.style.background="color-mix(in srgb, var(--bad-fg) 6%, transparent)"):o.status==="not_configured"?r.style.borderColor="var(--muted)":r.style.borderColor="var(--border)"}),setTimeout(()=>{document.getElementById("totp-recovery-secret")?.focus()},100)}function h(){const b=document.getElementById("totp-recovery-panel");b&&(b.style.display="none")}async function g(){const b=document.getElementById("totp-recovery-secret").value.trim(),r=document.getElementById("totp-recovery-error");if(r.textContent="",!b){r.textContent="Paste your Base32 key first";return}if(!/^[A-Za-z2-7\s]+=*$/.test(b)){r.textContent="Invalid Base32 format \u2014 should be letters A-Z and digits 2-7 only";return}try{const v=await fetch("/api/v1/totp/setup",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({secret:b})}),d=await v.json();if(!v.ok||!d.success){r.textContent=d.error||d.message||"Restore failed";return}document.getElementById("totp-recovery-import").style.display="none",document.getElementById("totp-recovery-verify").style.display="",setTimeout(()=>document.getElementById("totp-recovery-code")?.focus(),100)}catch{r.textContent="Network error \u2014 try again"}}async function m(){const b=document.getElementById("totp-recovery-code").value.trim(),r=document.getElementById("totp-recovery-confirm-error");if(r.textContent="",!/^\d{6}$/.test(b)){r.textContent="Enter a 6-digit code";return}try{const v=await fetch("/api/v1/totp/verify-setup",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:b})}),d=await v.json();if(!v.ok||!d.success){r.textContent=d.error||d.message||"Invalid code",document.getElementById("totp-recovery-code").value="",document.getElementById("totp-recovery-code")?.focus();return}h();const o=document.getElementById("totp-overlay");o&&o.classList.remove("show"),typeof window.initializeDashboard=="function"&&window.initializeDashboard()}catch{r.textContent="Network error \u2014 try again"}}document.getElementById("totp-show-recovery")?.addEventListener("click",b=>{b.preventDefault(),f()}),document.getElementById("totp-recovery-close")?.addEventListener("click",h),document.getElementById("totp-recovery-submit")?.addEventListener("click",g),document.getElementById("totp-recovery-confirm")?.addEventListener("click",m),document.getElementById("totp-recovery-secret")?.addEventListener("keydown",b=>{b.key==="Enter"&&(b.preventDefault(),g())}),document.getElementById("totp-recovery-code")?.addEventListener("keydown",b=>{b.key==="Enter"&&(b.preventDefault(),m())}),window._refreshRecoveryLink=async function(){const b=await c();return b&&b.success&&b.status&&b.status!=="healthy"?a(!0):a(!1),b}})(),(function(){const c=new ErrorHandler;injectModal("folder-browser-modal",`
\u{1F4C2} Browse for Media Folders
/
Loading...
Selected folders:
`),injectModal("service-creds-modal",`
Service Credentials
Credentials are injected automatically when accessing this service.
No credentials stored
Username shared across all services. Password is per-service.
Bypasses the app's own login screen
Used when requesting via Seerr
`);const a=document.getElementById("service-creds-modal");let f=null;const h=["sonarr","radarr","prowlarr","overseerr"],g=["sonarr","radarr"];function m(o){return o.externalUrl||o.url||""}function b(o){const p=document.getElementById("svc-creds-error");p.textContent=o,p.style.display=""}function r(){const o=document.getElementById("svc-creds-error");o.textContent="",o.style.display="none"}window.openServiceCredsModal=async function(o){f=o,r();const p=document.getElementById("svc-creds-title"),s=document.getElementById("svc-creds-desc"),e=document.getElementById("svc-creds-seedhost"),n=document.getElementById("svc-creds-apikey"),l=document.getElementById("svc-creds-basic"),t=document.getElementById("svc-creds-quality");p.textContent=o.name+" Credentials";const i=!!o.isExternal,y=h.includes(o.id)||h.includes(o.appTemplate),u=g.includes(o.id)||g.includes(o.appTemplate);e.style.display=i?"":"none",n.style.display=y?"":"none",t.style.display=u?"":"none",l.style.display=i?"none":"";const w=document.getElementById("svc-quality-select");w.innerHTML='',document.getElementById("svc-quality-status").textContent="",i?(s.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}`):y?s.textContent="API key bypasses the app login screen automatically.":s.textContent="Credentials are injected automatically when accessing this service.",await v(o),a.classList.add("show")};async function v(o){const p=document.getElementById("svc-creds-dot"),s=document.getElementById("svc-creds-status"),e=document.getElementById("svc-creds-clear");let n=!1;if(o.isExternal){try{const i=await(await fetch(`/api/v1/seedhost-creds?serviceId=${o.id}`)).json();i.success?(document.getElementById("svc-seedhost-user").value=i.username||"",i.hasCredentials&&(n=!0)):document.getElementById("svc-seedhost-user").value=""}catch{}document.getElementById("svc-seedhost-pass").value=""}try{const i=await(await fetch(`/api/v1/services/${o.id}/credentials`)).json();i.success&&(i.hasApiKey?(document.getElementById("svc-apikey-input").value="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",n=!0):document.getElementById("svc-apikey-input").value="",i.hasBasicAuth&&!o.isExternal?(document.getElementById("svc-basic-user").value=i.username||"",n=!0):document.getElementById("svc-basic-user").value="")}catch{}document.getElementById("svc-basic-pass")&&(document.getElementById("svc-basic-pass").value="");const l=o.id||o.appTemplate;if(g.includes(l)&&await d(o),n){p.style.background="var(--ok-fg, #74dfc4)",s.style.color="var(--ok-fg, #74dfc4)",s.textContent="Credentials stored",e.style.display="";const t=document.getElementById(`creds-btn-${o.id}`);t&&t.classList.add("has-creds")}else p.style.background="var(--muted)",s.style.color="var(--muted)",s.textContent="No credentials stored",e.style.display="none"}async function d(o){const p=document.getElementById("svc-quality-select"),s=document.getElementById("svc-quality-status"),e=o.id||o.appTemplate,n=m(o);if(!n){p.innerHTML='';return}p.innerHTML='',s.textContent="";try{const l=new URLSearchParams({service:e,url:n}),i=await(await fetch(`/api/v1/arr/quality-profiles?${l}`)).json();if(!i.success||!i.profiles?.length){p.innerHTML='';return}p.innerHTML="";for(const y of i.profiles){const u=document.createElement("option");u.value=y.id,u.textContent=y.name,p.appendChild(u)}if(i.storedProfileId&&(p.value=String(i.storedProfileId)),!p.value){const y=i.profiles.find(u=>/720/i.test(u.name));y&&(p.value=String(y.id))}!p.value&&i.profiles.length&&(p.value=String(i.profiles[0].id)),s.innerHTML=`${i.profiles.length} profiles loaded`}catch(l){p.innerHTML='',s.innerHTML=`Error: ${l.message}`}}document.getElementById("svc-quality-fetch")?.addEventListener("click",async()=>{if(!f)return;const o=f.id||f.appTemplate,p=m(f),e=document.getElementById("svc-apikey-input")?.value.trim(),n=document.getElementById("svc-quality-select"),l=document.getElementById("svc-quality-status");if(!p){l.innerHTML='No service URL available';return}if(!e||e==="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022"){l.innerHTML='Enter an API key first';return}n.innerHTML='',l.textContent="";try{const t=new URLSearchParams({service:o,url:p,apiKey:e}),y=await(await fetch(`/api/v1/arr/quality-profiles?${t}`)).json();if(!y.success){n.innerHTML='',l.innerHTML=`${y.error||"Failed to fetch profiles"}`;return}if(!y.profiles?.length){n.innerHTML='';return}n.innerHTML="";for(const w of y.profiles){const T=document.createElement("option");T.value=w.id,T.textContent=w.name,n.appendChild(T)}const u=y.profiles.find(w=>/720/i.test(w.name));u?n.value=String(u.id):y.profiles.length&&(n.value=String(y.profiles[0].id)),l.innerHTML=`${y.profiles.length} profiles loaded`}catch(t){n.innerHTML='',l.innerHTML=`${t.message}`}}),document.getElementById("svc-creds-save")?.addEventListener("click",async()=>{if(!f)return;const o=document.getElementById("svc-creds-save");o.textContent="Saving...",o.disabled=!0,r();try{const p=h.includes(f.id)||h.includes(f.appTemplate),s=f.id||f.appTemplate;if(f.isExternal){const l=document.getElementById("svc-seedhost-user").value.trim(),t=document.getElementById("svc-seedhost-pass").value;l&&await secureFetch("/api/v1/seedhost-creds",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:l,password:t||void 0,serviceId:f.id})})}const n=document.getElementById("svc-apikey-input")?.value.trim();if(n&&n!=="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022")if(p){const l=m(f),t=document.getElementById("svc-quality-select"),i=t?.value?parseInt(t.value):void 0,y=t?.selectedOptions?.[0]?.textContent||void 0,w=await(await secureFetch("/api/v1/arr/credentials",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({service:s,apiKey:n,url:l||void 0,qualityProfileId:i||void 0,qualityProfileName:y||void 0})})).json();if(!w.success){b(w.error||"Failed to save API key"),o.textContent="Save",o.disabled=!1;return}w.connectionTest&&!w.connectionTest.success&&b(`API key saved but connection test failed: ${w.connectionTest.error}`)}else await secureFetch(`/api/v1/services/${f.id}/credentials`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({apiKey:n})});else if(p&&g.includes(s)){const l=document.getElementById("svc-quality-select"),t=l?.value?parseInt(l.value):void 0,i=l?.selectedOptions?.[0]?.textContent||void 0;t&&await secureFetch("/api/v1/arr/quality-profiles",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({service:s,qualityProfileId:t,qualityProfileName:i})})}if(!f.isExternal){const l=document.getElementById("svc-basic-user").value.trim(),t=document.getElementById("svc-basic-pass").value;l&&t&&await secureFetch(`/api/v1/services/${f.id}/credentials`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:l,password:t})})}await v(f)}catch(p){c.logError("[ServiceCredentials] Save",p,{function:"saveCredentials"}),b("Failed to save: "+(p.message||"Unknown error"))}o.textContent="Save",o.disabled=!1}),document.getElementById("svc-creds-clear")?.addEventListener("click",async()=>{if(f&&confirm(`Remove stored credentials for ${f.name}?`)){r();try{const o=f.id||f.appTemplate,p=h.includes(o);f.isExternal&&await secureFetch(`/api/v1/seedhost-creds?serviceId=${f.id}`,{method:"DELETE"}),await secureFetch(`/api/v1/services/${f.id}/credentials`,{method:"DELETE"}),p&&await secureFetch(`/api/v1/arr/credentials/${o}`,{method:"DELETE"});const s=document.getElementById(`creds-btn-${f.id}`);s&&s.classList.remove("has-creds"),await v(f)}catch(o){c.logError("[ServiceCredentials] Clear",o,{function:"clearCredentials"}),b("Failed to clear: "+(o.message||"Unknown error"))}}}),document.getElementById("svc-creds-close")?.addEventListener("click",()=>{a.classList.remove("show"),f=null}),a?.addEventListener("click",o=>{o.target===a&&(a.classList.remove("show"),f=null)}),window.refreshCredsButtons=async function(){try{for(const o of window.APPS||[]){if(!o.isExternal&&!o.appTemplate&&!o.url)continue;let p=!1;if(o.isExternal)try{const n=await(await fetch(`/api/v1/seedhost-creds?serviceId=${o.id}`)).json();n.success&&n.hasCredentials&&(p=!0)}catch{}try{const n=await(await fetch(`/api/v1/services/${o.id}/credentials`)).json();n.success&&(n.hasApiKey||n.hasBasicAuth)&&(p=!0)}catch{}const s=document.getElementById(`creds-btn-${o.id}`);s&&s.classList.toggle("has-creds",p)}}catch{}}})(),(function(){const c=new ErrorHandler;injectModal("totp-settings-modal",`
Authentication Settings
TOTP is not configured
or
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)
How long before you need to re-enter your code
`);async function a(){try{const m=await(await fetch("/api/v1/totp/config")).json();if(!m.success)return;const{enabled:b,sessionDuration:r,isSetUp:v}=m.config,d=document.getElementById("totp-status-dot"),o=document.getElementById("totp-status-text"),p=document.getElementById("totp-status-banner"),s=document.getElementById("totp-setup-section"),e=document.getElementById("totp-qr-section"),n=document.getElementById("totp-duration-section"),l=document.getElementById("totp-disable-section");if(b&&v){d.style.background="var(--ok-fg, #7ef2ff)",p.style.borderColor="var(--ok-fg, #7ef2ff)",p.style.background="color-mix(in srgb, var(--ok-fg) 8%, transparent)",o.textContent="TOTP is active",o.style.color="var(--ok-fg, #7ef2ff)",s.style.display="block";const t=document.getElementById("totp-setup-btn");t&&(t.textContent="Generate New Secret"),e.style.display="none",n.style.display="block",l.style.display="block",document.getElementById("totp-duration-select").value=r}else d.style.background="var(--muted)",p.style.borderColor="var(--border)",p.style.background="transparent",o.textContent="TOTP is not configured",o.style.color="var(--muted)",s.style.display="block",e.style.display="none",n.style.display="none",l.style.display="none";h(b&&v,r)}catch(g){console.warn("Failed to load TOTP settings:",g)}}const f={"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 h(g,m){const b=document.getElementById("auth-card"),r=document.getElementById("auth-pill"),v=document.getElementById("auth-dot"),d=document.getElementById("auth-status-text");b&&(g?(b.setAttribute("data-status","on"),r.className="badge on",r.textContent="YES",v.className="dot ok at-bl",d.textContent="Session: "+(f[m]||m)):(b.setAttribute("data-status","off"),r.className="badge off",r.textContent="NO",v.className="dot bad at-bl",d.textContent="Not configured"))}document.getElementById("totp-setup-btn")?.addEventListener("click",async()=>{try{const m=await(await secureFetch("/api/v1/totp/setup",{method:"POST"})).json();m.success&&(document.getElementById("totp-qr-image").src=m.qrCode,document.getElementById("totp-manual-key").textContent=m.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(g){c.logError("[TOTP] Setup Failed",g,{function:"setupTOTP"})}}),document.getElementById("totp-import-btn")?.addEventListener("click",async()=>{const g=document.getElementById("totp-import-key").value.trim(),m=document.getElementById("totp-import-error");if(m.textContent="",!g){m.textContent="Paste a Base32 secret key first";return}try{const r=await(await secureFetch("/api/v1/totp/setup",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({secret:g})})).json();r.success?(m.textContent="",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()):m.textContent=r.error||r.message||"Import failed"}catch{m.textContent="Connection error \u2014 try refreshing the page"}}),document.getElementById("totp-copy-key")?.addEventListener("click",()=>{const g=document.getElementById("totp-manual-key").textContent;navigator.clipboard.writeText(g).then(()=>{const m=document.getElementById("totp-copy-key");m.textContent="\u2705",setTimeout(()=>{m.textContent="\u{1F4CB}"},2e3)})}),document.getElementById("totp-download-backup")?.addEventListener("click",()=>{const g=document.getElementById("totp-manual-key").textContent.trim();if(!g)return;const m={service:"DashCaddy",type:"totp-secret",secret:g,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.'},b=new Blob([JSON.stringify(m,null,2)],{type:"application/json"}),r=URL.createObjectURL(b),v=document.createElement("a");v.href=r,v.download=`dashcaddy-totp-backup-${new Date().toISOString().slice(0,10)}.json`,document.body.appendChild(v),v.click(),document.body.removeChild(v),URL.revokeObjectURL(r);const d=document.getElementById("totp-download-backup");d.textContent="\u2705 Saved",setTimeout(()=>{d.textContent="\u2B07 Download"},2e3)}),document.getElementById("totp-confirm-setup")?.addEventListener("click",async()=>{const g=document.getElementById("totp-setup-code").value,m=document.getElementById("totp-setup-error");if(!/^\d{6}$/.test(g)){m.textContent="Enter a 6-digit code";return}try{const r=await(await secureFetch("/api/v1/totp/verify-setup",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:g})})).json();r.success?(m.textContent="",a()):m.textContent=r.error||"Invalid code"}catch{m.textContent="Connection error"}}),document.getElementById("totp-setup-code")?.addEventListener("keydown",g=>{g.key==="Enter"&&document.getElementById("totp-confirm-setup")?.click()}),document.getElementById("totp-duration-select")?.addEventListener("change",async g=>{try{await secureFetch("/api/v1/totp/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sessionDuration:g.target.value})}),a()}catch(m){c.logError("[TOTP] Update Session Duration",m,{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&&a()}catch(g){c.logError("[TOTP] Disable Failed",g,{function:"disableTOTP"})}}),document.getElementById("auth-settings-btn")?.addEventListener("click",()=>{a(),openModal("totp-settings-modal")}),document.getElementById("totp-modal-close")?.addEventListener("click",()=>{closeModal("totp-settings-modal")}),document.getElementById("totp-settings-modal")?.addEventListener("click",g=>{g.target.id==="totp-settings-modal"&&closeModal("totp-settings-modal")}),window._updateAuthCard=h,(async()=>{try{const m=await(await fetch("/api/v1/totp/config")).json();if(m.success){const b=m.config.enabled&&m.config.isSetUp;h(b,m.config.sessionDuration)}}catch(g){c.logError("[TOTP] AuthCard Update",g,{function:"authCardUpdate"})}})()})(),(function(){"use strict";const c={me:"/api/v1/auth/me",users:"/api/v1/auth/admin/users",allowlist:"/api/v1/auth/admin/allowlist",invites:"/api/v1/auth/admin/invites"};function a(s,e,...n){const l=document.createElement(s);if(e)for(const t of Object.keys(e)){const i=e[t];i==null||i===!1||(t==="class"?l.className=i:t==="text"?l.textContent=i:t==="html"?l.innerHTML=i:t.startsWith("on")&&typeof i=="function"?l.addEventListener(t.slice(2).toLowerCase(),i):l.setAttribute(t,i))}for(const t of n)t==null||t===!1||(typeof t=="string"?l.appendChild(document.createTextNode(t)):l.appendChild(t));return l}async function f(s,e){const n=window.SITE&&window.SITE.csrfToken||"";e=e||{},e.headers=Object.assign({"Content-Type":"application/json"},e.headers||{},n?{"X-CSRF-Token":n}:{}),e.body&&typeof e.body!="string"&&(e.body=JSON.stringify(e.body));const l=await fetch(s,e),t=await l.json().catch(()=>({}));if(!l.ok){const i=t&&(t.message||t.error)||"HTTP "+l.status,y=new Error(i);throw y.status=l.status,y}return t}function h(s){const e={admin:"background:#7c3aed;color:#fff",operator:"background:#2563eb;color:#fff",viewer:"background:#6b7280;color:#fff"};return a("span",{class:"role-badge",style:"display:inline-block;padding:2px 8px;border-radius:4px;font-size:0.75rem;font-weight:600;text-transform:uppercase;"+(e[s]||e.viewer),text:s})}function g(s,e,n){if(s.innerHTML="",!e||e.length===0){s.appendChild(a("p",{style:"color:var(--muted)",text:"No users yet."}));return}const l=a("table",{style:"width:100%;border-collapse:collapse;font-size:0.9rem"});l.appendChild(a("thead",null,a("tr",{style:"border-bottom:1px solid var(--border)"},a("th",{style:"text-align:left;padding:8px",text:"Email"}),a("th",{style:"text-align:left;padding:8px",text:"Role"}),a("th",{style:"text-align:left;padding:8px",text:"Created"}),a("th",{style:"text-align:left;padding:8px",text:"Last login"}),a("th",{style:"text-align:right;padding:8px",text:"Actions"}))));const t=a("tbody");for(const i of e){const y=a("tr",{style:"border-bottom:1px solid var(--border)"}),u=a("td",{style:"padding:8px"});u.appendChild(a("span",{text:i.email||"(no email)"})),i.displayName&&i.displayName!==(i.email||"").split("@")[0]&&(u.appendChild(a("br")),u.appendChild(a("small",{style:"color:var(--muted)",text:i.displayName}))),y.appendChild(u);const w=a("td",{style:"padding:8px"});w.appendChild(h(i.role)),y.appendChild(w),y.appendChild(a("td",{style:"padding:8px;color:var(--muted);font-size:0.85rem",text:i.createdAt?new Date(i.createdAt).toLocaleDateString():"\u2014"})),y.appendChild(a("td",{style:"padding:8px;color:var(--muted);font-size:0.85rem",text:i.lastLoginAt?new Date(i.lastLoginAt).toLocaleString():"\u2014"}));const T=a("td",{style:"padding:8px;text-align:right"}),L=a("select",{style:"padding:2px 6px;margin-right:6px",onchange:async C=>{try{await f(c.users+"/"+encodeURIComponent(i.id),{method:"PATCH",body:{role:C.target.value}}),n&&n()}catch(B){window.errorHandler&&window.errorHandler.show("Role update failed: "+B.message),C.target.value=i.role}}});for(const C of["admin","operator","viewer"]){const B=a("option",{value:C,text:C});C===i.role&&(B.selected=!0),L.appendChild(B)}T.appendChild(L);const P=a("button",{class:"btn-sm",style:"padding:2px 8px",text:"Delete",onclick:async()=>{if(confirm("Delete user "+(i.email||i.id)+"? This cannot be undone."))try{await f(c.users+"/"+encodeURIComponent(i.id),{method:"DELETE"}),n&&n()}catch(C){window.errorHandler&&window.errorHandler.show("Delete failed: "+C.message)}}});T.appendChild(P),y.appendChild(T),t.appendChild(y)}l.appendChild(t),s.appendChild(l)}function m(s,e){const n=a("form",{style:"display:flex;gap:8px;flex-wrap:wrap;align-items:end",onsubmit:async t=>{t.preventDefault();const i=new FormData(t.target),y={email:i.get("email"),role:i.get("role"),ttlHours:parseInt(i.get("ttlHours"),10)||24,sendEmail:i.get("sendEmail")==="on"};try{const u=await f(c.invites,{method:"POST",body:y});t.target.reset(),e&&e(u)}catch(u){window.errorHandler&&window.errorHandler.show("Invite failed: "+u.message)}}});n.appendChild(a("label",{style:"display:flex;flex-direction:column;gap:2px;font-size:0.85rem"},a("span",{text:"Email"}),a("input",{name:"email",type:"email",required:!0,placeholder:"user@example.com",style:"padding:6px"})));const l=a("select",{name:"role",style:"padding:6px"});for(const t of["operator","viewer","admin"])l.appendChild(a("option",{value:t,text:t}));n.appendChild(a("label",{style:"display:flex;flex-direction:column;gap:2px;font-size:0.85rem"},a("span",{text:"Role"}),l)),n.appendChild(a("label",{style:"display:flex;flex-direction:column;gap:2px;font-size:0.85rem"},a("span",{text:"TTL (hours)"}),a("input",{name:"ttlHours",type:"number",min:"1",max:"168",value:"24",style:"padding:6px;width:80px"}))),n.appendChild(a("label",{style:"display:flex;gap:4px;align-items:center;font-size:0.85rem"},a("input",{name:"sendEmail",type:"checkbox",checked:!0}),a("span",{text:"Send email"}))),n.appendChild(a("button",{type:"submit",class:"btn-sm",style:"padding:6px 12px",text:"Issue invite"})),s.appendChild(n)}function b(s,e,n){if(s.innerHTML="",!e||e.length===0){s.appendChild(a("p",{style:"color:var(--muted)",text:"No outstanding invites."}));return}const l=a("table",{style:"width:100%;border-collapse:collapse;font-size:0.9rem"});l.appendChild(a("thead",null,a("tr",{style:"border-bottom:1px solid var(--border)"},a("th",{style:"text-align:left;padding:8px",text:"Email"}),a("th",{style:"text-align:left;padding:8px",text:"Role"}),a("th",{style:"text-align:left;padding:8px",text:"Invited by"}),a("th",{style:"text-align:left;padding:8px",text:"Expires"}),a("th",{style:"text-align:right;padding:8px",text:"Actions"}))));const t=a("tbody");for(const i of e){const y=a("tr",{style:"border-bottom:1px solid var(--border)"});y.appendChild(a("td",{style:"padding:8px",text:i.email})),y.appendChild(a("td",{style:"padding:8px"},h(i.role))),y.appendChild(a("td",{style:"padding:8px;color:var(--muted)",text:i.invitedBy||"\u2014"})),y.appendChild(a("td",{style:"padding:8px;color:var(--muted);font-size:0.85rem",text:i.expiresAt?new Date(i.expiresAt).toLocaleString():"\u2014"}));const u=a("td",{style:"padding:8px;text-align:right"});u.appendChild(a("button",{class:"btn-sm",style:"padding:2px 8px",text:"Revoke",onclick:async()=>{if(confirm("Revoke invite for "+i.email+"?"))try{await f(c.invites+"/"+encodeURIComponent(i.id),{method:"DELETE"}),n&&n()}catch(w){window.errorHandler&&window.errorHandler.show("Revoke failed: "+w.message)}}})),y.appendChild(u),t.appendChild(y)}l.appendChild(t),s.appendChild(l)}function r(s,e){const n=a("div",{style:"margin-top:12px;padding:12px;border:1px solid #16a34a;border-radius:6px;background:#052e1a;color:#bbf7d0;font-size:0.85rem"});n.appendChild(a("strong",{text:"Invite issued \u2014 copy the link below. It will not be shown again."})),n.appendChild(a("br")),n.appendChild(a("code",{style:"display:block;margin-top:8px;padding:8px;background:#000;border-radius:4px;word-break:break-all;color:#d1fae5",text:s.acceptUrl}));const l=a("button",{class:"btn-sm",style:"margin-top:8px;padding:4px 10px",text:"Copy link",onclick:async()=>{try{await navigator.clipboard.writeText(s.acceptUrl),l.textContent="Copied!",setTimeout(()=>{l.textContent="Copy link"},2e3)}catch{window.errorHandler&&window.errorHandler.show("Clipboard blocked: select the link manually.")}}});n.appendChild(l),s.deliveredVia==="dev-console"?n.appendChild(a("p",{style:"margin-top:8px;color:#fbbf24;font-size:0.8rem",text:"SMTP not configured \u2014 the invite was logged to the server console (search for [DC-048-DEV-INVITE-LINK])."})):s.deliveredVia==="email"&&n.appendChild(a("p",{style:"margin-top:8px;color:#86efac;font-size:0.8rem",text:"Email sent to "+s.email+"."})),e.appendChild(n)}async function v(s){s.innerHTML="",s.appendChild(a("h2",{style:"margin:0 0 16px",text:"Admin \xB7 Users & Invites"}));const e=await f(c.me).catch(()=>({}));if(!e||!e.user||e.user.role!=="admin"){s.appendChild(a("p",{style:"color:var(--muted)",text:"Admin role required to view this panel. If multi-user mode is enabled and you should have access, check /api/v1/auth/me."}));return}const n=a("button",{class:"btn-sm",style:"float:right;padding:4px 10px",text:"Refresh",onclick:()=>v(s)});s.appendChild(n);const l=a("h3",{style:"margin:24px 0 8px;clear:both",text:"Users"});s.appendChild(l);const t=a("div",{id:"admin-users-list"});s.appendChild(t);const i=await f(c.users).catch(()=>({users:[]}));g(t,i.users,()=>v(s)),s.appendChild(a("h4",{style:"margin:24px 0 8px;font-size:0.95rem",text:"Pre-authorize email"}));const y=a("form",{style:"display:flex;gap:8px;align-items:end",onsubmit:async L=>{L.preventDefault();const P=L.target.email.value.trim();if(P)try{await f(c.users,{method:"POST",body:{email:P}}),L.target.reset(),v(s)}catch(C){window.errorHandler&&window.errorHandler.show("Add failed: "+C.message)}}});y.appendChild(a("input",{name:"email",type:"email",required:!0,placeholder:"user@example.com",style:"padding:6px"})),y.appendChild(a("button",{type:"submit",class:"btn-sm",style:"padding:6px 12px",text:"Add to allowlist"})),s.appendChild(y),s.appendChild(a("h3",{style:"margin:24px 0 8px",text:"Issue invite"}));const u=a("div");s.appendChild(u);const w=a("div",{id:"admin-invites-list",style:"margin-top:16px"});s.appendChild(w);const T=await f(c.invites).catch(()=>({invites:[]}));b(w,T.invites,()=>v(s)),m(u,L=>{r(L,u),v(s)})}async function d(){if(document.getElementById("admin-panel-root"))return;const e=a("div",{id:"admin-panel-root",style:"position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:1000;display:flex;align-items:center;justify-content:center;",onclick:t=>{t.target===e&&o()}}),n=a("div",{style:"background:var(--card-base,#1f2937);color:var(--text,#f3f4f6);border-radius:8px;padding:24px;max-width:900px;width:90%;max-height:85vh;overflow:auto;position:relative;box-shadow:0 10px 30px rgba(0,0,0,0.3)"});n.appendChild(a("button",{class:"btn-sm",style:"position:absolute;top:12px;right:12px;padding:4px 10px",text:"Close",onclick:o}));const l=a("div",{id:"admin-panel-body"});n.appendChild(l),e.appendChild(n),document.body.appendChild(e);try{await v(l)}catch(t){l.innerHTML='
Failed to load admin panel: '+(t.message||t)+"
"}}function o(){const s=document.getElementById("admin-panel-root");s&&s.remove()}async function p(s){async function e(){const n=await f(c.me).catch(()=>({})),l=document.getElementById("admin-trigger-btn");if(n&&n.user&&n.user.role==="admin"){if(l)return;const t=a("button",{id:"admin-trigger-btn",class:"btn-sm",style:"margin-left:8px;padding:6px 12px",text:"Admin",onclick:d});s?s.appendChild(t):document.body&&document.body.appendChild(t)}else l&&l.remove()}await e(),setInterval(e,6e4)}window.AdminPanel={open:d,close:o,attachTrigger:p}})(),(function(){injectModal("token-management-modal",`
\u{1F511} DNS Credentials
Enter Technitium DNS login credentials. Read-only accounts are used for logs; admin accounts for restarts, records, and updates.
';return}for(const y of i)t.insertAdjacentHTML("beforeend",`
${a(y)}
`)}function h(){let t=safeSessionGet("dashcaddy-encryption-key");if(t)return t;const i=safeGet("dashcaddy-encryption-key");if(i)return safeSessionSet("dashcaddy-encryption-key",i),safeRemove("dashcaddy-encryption-key"),i;const y=new Uint8Array(32);return crypto.getRandomValues(y),t=Array.from(y,u=>u.toString(16).padStart(2,"0")).join(""),safeSessionSet("dashcaddy-encryption-key",t),t}const g=h();function m(t,i){if(!t)return"";const y=crypto.getRandomValues(new Uint8Array(8)),u=Array.from(y,L=>L.toString(16).padStart(2,"0")).join(""),w=new TextEncoder().encode(i+u);let T="";for(let L=0;LparseInt($,16))),P=atob(t.substring(17)),C=new TextEncoder().encode(i+T);let B="";for(let $=0;${["readonly","admin"].forEach(i=>{["token","username"].forEach(y=>{safeRemove(`${t}-${i}-${y}-enc`)})}),safeRemove(`${t}-token-enc`),safeRemove(`${t}-username-enc`)})}function l(t){const i=d(t,"readonly"),y=o(t,"readonly"),u=d(t,"admin"),w=o(t,"admin"),T=b(safeGet(`${t}-token-enc`),g),L=b(safeGet(`${t}-username-enc`),g);return{username:w||y||L,token:u||i||T,readonlyToken:i||T,readonlyUsername:y||L,adminToken:u||T,adminUsername:w||L}}document.getElementById("manage-tokens")?.addEventListener("click",()=>{f();const t=document.getElementById("token-management-modal"),i=e();c().forEach(y=>{const u=i[y];document.getElementById(`${y}-readonly-username`).value=u.readonly.username,document.getElementById(`${y}-readonly-token`).value=u.readonly.token,document.getElementById(`${y}-admin-username`).value=u.admin.username,document.getElementById(`${y}-admin-token`).value=u.admin.token,document.getElementById(`${y}-token-status`).textContent=""}),t.classList.add("show")}),document.getElementById("token-management-modal")?.addEventListener("click",t=>{const i=t.target.closest(".token-toggle");if(i){const y=i.dataset.target,u=document.getElementById(y);u.type==="password"?(u.type="text",i.textContent="\u{1F648}"):(u.type="password",i.textContent="\u{1F441}");return}t.target.id==="token-management-modal"&&t.target.classList.remove("show")}),document.getElementById("token-save")?.addEventListener("click",async()=>{const t=c();t.forEach(u=>{s(u,"readonly",document.getElementById(`${u}-readonly-username`).value.trim()),p(u,"readonly",document.getElementById(`${u}-readonly-token`).value.trim()),s(u,"admin",document.getElementById(`${u}-admin-username`).value.trim()),p(u,"admin",document.getElementById(`${u}-admin-token`).value.trim())});const i={};let y=!1;if(t.forEach(u=>{const w={},T=document.getElementById(`${u}-readonly-username`).value.trim(),L=document.getElementById(`${u}-readonly-token`).value.trim(),P=document.getElementById(`${u}-admin-username`).value.trim(),C=document.getElementById(`${u}-admin-token`).value.trim();T&&L&&(w.readonly={username:T,password:L},y=!0),P&&C&&(w.admin={username:P,password:C},y=!0),Object.keys(w).length>0&&(i[u]=w)}),y){t.forEach(u=>{i[u]&&(document.getElementById(`${u}-token-status`).textContent="Verifying...",document.getElementById(`${u}-token-status`).className="token-status")});try{const w=await(await secureFetch("/api/v1/dns/credentials",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({servers:i})})).json();w.results?t.forEach(T=>{const L=document.getElementById(`${T}-token-status`);if(!i[T]){L.textContent="";return}const P=w.results[T];P?.success?(L.textContent="\u2713 Verified & saved",L.className="token-status success"):P?.partial?(L.textContent="\u2713 "+P.partial,L.className="token-status success"):(L.textContent="\u2717 "+(P?.error||"Login failed"),L.className="token-status error")}):w.success?t.forEach(T=>{i[T]&&(document.getElementById(`${T}-token-status`).textContent="\u2713 Saved",document.getElementById(`${T}-token-status`).className="token-status success")}):t.forEach(T=>{i[T]&&(document.getElementById(`${T}-token-status`).textContent="\u2717 "+(w.error||"Failed"),document.getElementById(`${T}-token-status`).className="token-status error")})}catch(u){console.error("Failed to sync DNS credentials to backend:",u),t.forEach(w=>{i[w]&&(document.getElementById(`${w}-token-status`).textContent="\u2713 Saved locally (sync failed)",document.getElementById(`${w}-token-status`).className="token-status")})}}else t.forEach(u=>{document.getElementById(`${u}-token-status`).textContent=""});setTimeout(()=>{t.every(w=>{const T=document.getElementById(`${w}-token-status`)?.textContent;return!T||T.includes("\u2713")})&&closeModal("token-management-modal")},1500)}),document.getElementById("token-cancel")?.addEventListener("click",()=>{closeModal("token-management-modal")}),document.getElementById("token-clear-all")?.addEventListener("click",async()=>{if(confirm("Clear all stored DNS credentials? This cannot be undone.")){n(),c().forEach(t=>{document.getElementById(`${t}-readonly-username`).value="",document.getElementById(`${t}-readonly-token`).value="",document.getElementById(`${t}-admin-username`).value="",document.getElementById(`${t}-admin-token`).value="",document.getElementById(`${t}-token-status`).textContent="\u2713 Cleared",document.getElementById(`${t}-token-status`).className="token-status success"});try{await secureFetch("/api/v1/dns/credentials",{method:"DELETE"})}catch{}}}),window.getToken=d,window.getUsername=o,window.setToken=p,window.setUsername=s,window.getAllCredentials=e,window.getCredential=r,window.setCredential=v,window.getEncryptionKey=h,window.getDnsIds=c,window.getDnsDisplayName=a})(),(function(){function c(p,s,e=null){const n=document.getElementById(p+"-dot"),l=document.getElementById(p+"-pill"),t=document.getElementById(p+"-time"),i=document.querySelector(`[data-app="${p}"]`);n&&(n.classList.toggle("ok",s),n.classList.toggle("bad",!s)),l&&(l.textContent=s?"ON":"OFF",l.classList.toggle("on",s),l.classList.toggle("off",!s)),t&&e!==null&&(t.textContent=s?`${e}ms`:"timeout",t.className=`response-time ${a(e,s)}`),i&&i.setAttribute("data-status",s?"on":"off")}function a(p,s){return s?p<200?"excellent":p<500?"good":p<1e3?"fair":"slow":"timeout"}async function f(p){const s=performance.now();try{const e=await fetch("/probe/"+p,{cache:"no-store"}),n=performance.now(),l=Math.round(n-s);return{isUp:e.status>=200&&e.status<400||e.status===401||e.status===403,responseTime:l}}catch{const e=performance.now();return{isUp:!1,responseTime:Math.round(e-s)}}}window.APPS=[];let h=null,g=!1;async function m(){try{window.SkeletonLoader&&window.SkeletonLoader.show(6);const p=await fetch("/api/v1/services",{cache:"no-store"});if(p.ok){const s=await p.json();window.APPS=s.services||[],window.SkeletonLoader&&window.SkeletonLoader.hide()}else console.error("Failed to load services:",p.status),window.SkeletonLoader&&window.SkeletonLoader.hide()}catch(p){console.error("Failed to load services:",p),window.SkeletonLoader&&window.SkeletonLoader.hide()}}function b(p){const s=window.APPS?.find(n=>n.id===p);if(s?.url)return s.url.startsWith("http")?s.url:"https://"+s.url;if(s?.isExternal&&s.externalUrl)return s.externalUrl;const e=SITE.dnsServers?.[p];return e?"http://"+e.ip+":"+(e.port||5380):buildServiceUrl(p)}function r(p,s,e){const n=document.createElement(p);return s&&(n.className=s),e&&(n.textContent=e),n}function v(){const p=document.getElementById("cards");p.innerHTML="";for(let s=0;s{D.stopPropagation(),window.openContainerLogsModal(e.containerId,e.name)},x.appendChild(E);const S=r("button","update-btn","\u2B06\uFE0F");S.title="Update container to latest version",S.id=`update-btn-${e.id}`,S.onclick=D=>{D.stopPropagation(),window.updateContainer(e.containerId,e.name,e.id)},x.appendChild(S);const A=r("button","exec-btn",">_");A.title="Open terminal",A.onclick=D=>{D.stopPropagation(),window.openExecModal&&window.openExecModal(e.containerId,e.name)},x.appendChild(A)}if(e.logPath&&!e.containerId){const E=r("button","logs-btn","\u{1F4CB}");E.title="View application logs",E.onclick=S=>{S.stopPropagation(),window.openFileLogsModal(e.logPath,e.name)},x.appendChild(E)}if(e.isExternal||e.appTemplate||e.url){const E=r("button","creds-btn","\u{1F511}");E.title="Auto-login credentials",E.id=`creds-btn-${e.id}`,E.onclick=S=>{S.stopPropagation(),window.openServiceCredsModal(e)},x.appendChild(E)}if(e.id!=="internet"){const E=r("button","options-btn","\u2699\uFE0F");E.title="Edit service settings",E.onclick=S=>{S.stopPropagation(),window.openServiceEditModal(e)},x.appendChild(E)}if(e.id!=="internet"){const E=r("button","share-btn","\u{1F517}");E.title="Share this service (Pro)",E.onclick=S=>{S.stopPropagation(),typeof window.openShareModal=="function"?window.openShareModal(e):typeof window.showNotification=="function"&&window.showNotification("Share modal not loaded. Refresh the page.","error")},x.appendChild(E)}if(e.id!=="internet"){const E=r("button","delete-btn","\u{1F5D1}\uFE0F");E.title="Delete this service",E.onclick=S=>{S.stopPropagation(),window.deleteService(e.id,e.name)},x.appendChild(E)}const I=r("button",null,"Open");I.onclick=()=>window.open(b(e.id),"_blank","noopener"),x.appendChild(I),n.appendChild(x),n.style.transitionDelay=`${Math.min(s*45,270)}ms`,p.appendChild(n)}requestAnimationFrame(()=>{p.querySelectorAll(".card").forEach(s=>s.classList.add("loaded"))}),window.groupRecipeCards&&requestAnimationFrame(()=>window.groupRecipeCards()),window.refreshServiceFilter&&window.refreshServiceFilter()}function d(p,s,e=null){const n=document.getElementById("dot-"+p+"-grid"),l=document.getElementById("badge-"+p),t=document.getElementById("time-"+p),i=document.querySelector(`[data-app="${p}"]`);n&&(n.classList.toggle("ok",s),n.classList.toggle("bad",!s)),l&&(l.textContent=s?"ON":"OFF",l.classList.toggle("on",s),l.classList.toggle("off",!s)),t&&e!==null&&(t.textContent=s?`${e}ms`:"timeout",t.className=`response-time ${a(e,s)}`),i&&i.setAttribute("data-status",s?"on":"off")}async function o(){if(h)return g=!0,h;function p(n,l=new Date){const t=document.getElementById("stamp");t&&(t.textContent=`${n}: ${new Date(l).toLocaleTimeString()}`)}function s(n){Object.keys(SITE.dnsServers).forEach(t=>{const i=n[t];i&&c(t,i.isUp,i.responseTime)}),n.internet&&c("internet",n.internet.isUp,n.internet.responseTime),window.APPS.forEach(t=>{const i=n[t.id];i&&d(t.id,i.isUp,i.responseTime)})}async function e(){const n=Object.keys(SITE.dnsServers),l=n.map(u=>f(u));l.push(f("internet"));const t=await Promise.all(l);n.forEach((u,w)=>c(u,t[w].isUp,t[w].responseTime));const i=t[t.length-1];c("internet",i.isUp,i.responseTime),(await Promise.all(window.APPS.map(async u=>{const w=await f(u.id);return{id:u.id,...w}}))).forEach(u=>{d(u.id,u.isUp,u.responseTime)})}return h=(async()=>{try{const n=await fetch("/api/v1/services/status",{cache:"no-store"});if(!n.ok)throw new Error(`Status refresh failed (${n.status})`);const l=await n.json();s(l.statuses||{}),p("last check",l.checkedAt||new Date)}catch(n){console.warn("Batched status refresh failed, falling back to direct probes:",n);try{await e(),p("last check")}catch(l){console.error("Dashboard refresh failed:",l),p("last failed")}}finally{h=null,g&&(g=!1,setTimeout(()=>{window.refreshAll()},0))}})(),h}document.querySelector(".top")?.addEventListener("click",p=>{const s=p.target.closest('[id$="-open"]');if(!s)return;const e=s.id.replace("-open","");SITE.dnsServers[e]&&window.open(b(e),"_blank","noopener")}),document.getElementById("ca-open")?.addEventListener("click",()=>window.open(b("ca"),"_blank","noopener")),document.getElementById("creds-btn-ca")?.addEventListener("click",p=>{p.stopPropagation();const s=window.APPS.find(e=>e.id==="ca");s&&window.openServiceCredsModal&&window.openServiceCredsModal(s)}),document.getElementById("options-btn-ca")?.addEventListener("click",p=>{p.stopPropagation();const s=window.APPS.find(e=>e.id==="ca");s&&window.openServiceEditModal&&window.openServiceEditModal(s)}),document.getElementById("delete-btn-ca")?.addEventListener("click",p=>{p.stopPropagation(),window.deleteService&&window.deleteService("ca","DashCA")}),window.loadServices=m,window.buildGrid=v,window.refreshAll=o,window.setQuick=c,window.setBadge=d,window.getResponseTimeClass=a,window.checkServiceWithTiming=f,window.serviceUrl=b,window.el=r})(),(function(){async function c(r){const d=await(await secureFetch(`/api/v1/dns/restart/${r}`,{method:"POST"})).json();if(!d.success)throw new Error(d.error||"Restart failed");return d}document.querySelector(".top")?.addEventListener("click",async r=>{const v=r.target.closest('[id$="-restart"]');if(!v)return;const d=v.id.replace("-restart","");if(SITE.dnsServers[d]&&confirm(`Restart ${d.toUpperCase()} service?`))try{await withButton(v,"...",()=>c(d)),setTimeout(window.refreshAll,DC.DELAYS.RELOAD)}catch(o){showNotification("Restart failed: "+o.message,"error")}});async function a(r,v){const d=document.getElementById(`${r}-update`),o=d?.textContent||"\u2B06\uFE0F";try{d.textContent="\u{1F50D}",d.disabled=!0,d.title="Checking for updates...";const s=await(await fetch(`/api/v1/dns/check-update?server=${encodeURIComponent(v)}`)).json();if(!s.success)throw new Error(s.error||"Failed to check for updates");if(!s.updateAvailable){d.textContent="\u2705",d.title=`Already on latest version (${s.currentVersion})`,showNotification(`${r.toUpperCase()} is already up to date! Current version: ${s.currentVersion}`,"info"),setTimeout(()=>{d.textContent=o,d.disabled=!1,d.title="Update DNS server"},3e3);return}if(!confirm(`Update available for ${r.toUpperCase()}!
Current: ${s.currentVersion}
New: ${s.updateVersion}
`+(s.updateTitle?`${s.updateTitle}
`:"")+`The DNS server will restart during the update.
Proceed?`)){d.textContent=o,d.disabled=!1,d.title="Update DNS server";return}d.textContent="\u{1F504}",d.title="Updating...";const l=await(await secureFetch(`/api/v1/dns/update?server=${encodeURIComponent(v)}`,{method:"POST"})).json();if(!l.success)throw new Error(l.error||"Update failed");if(l.manualUpdateRequired){d.textContent="\u2B06\uFE0F",d.title=`Update available: ${l.newVersion}`;const t=l.downloadLink?`
Download: ${l.downloadLink}`:"",i=l.instructionsLink?`
Instructions: ${l.instructionsLink}`:"";showNotification(`${r.toUpperCase()} update requires manual installation. Current: ${l.previousVersion} \u2192 ${l.newVersion}. Please update manually on the host machine.`,"warning",8e3),d.disabled=!1;return}d.textContent="\u2705",d.title="Updated successfully!",showNotification(`${r.toUpperCase()} updated successfully! ${l.previousVersion} \u2192 ${l.newVersion}. Server is restarting...`,"success"),setTimeout(()=>{d.textContent=o,d.disabled=!1,d.title="Update DNS server",window.refreshAll()},1e4)}catch(p){console.error("DNS update error:",p),d.textContent="\u274C",d.title="Update failed",showNotification(`Failed to update ${r.toUpperCase()}: ${p.message}`,"error"),setTimeout(()=>{d.textContent=o,d.disabled=!1,d.title="Update DNS server"},3e3)}}document.querySelector(".top")?.addEventListener("click",r=>{const v=r.target.closest('[id$="-update"]');if(!v)return;const d=v.id.replace("-update","");SITE.dnsServers[d]&&a(d,SITE.dnsServers[d]?.ip)}),injectModal("dns-settings-modal",`
DNS Settings
Manage credentials via Tokens in the toolbar
`);let f=null;function h(r){f=r;const v=SITE.dnsServers[r]||{},d=document.getElementById("dns-settings-modal");document.getElementById("dns-settings-title").textContent=`${(v.name||r).toUpperCase()} Settings`,document.getElementById("dns-edit-ip").value=v.ip||"",document.getElementById("dns-edit-port").value=v.port||DC.DEFAULTS.DNS_PORT,document.getElementById("dns-edit-name").value=v.name||"",d.classList.add("show")}async function g(){if(!f)return;const r=document.getElementById("dns-edit-ip").value.trim(),v=document.getElementById("dns-edit-port").value.trim()||DC.DEFAULTS.DNS_PORT,d=document.getElementById("dns-edit-name").value.trim();if(!r){showNotification("Server IP is required","warning");return}const o={dnsServers:{}};o.dnsServers[f]={ip:r,port:String(v)},d&&(o.dnsServers[f].name=d);try{const s=await(await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)})).json();s.success?(SITE.dnsServers[f]=o.dnsServers[f],showNotification(`${f.toUpperCase()} settings saved`,"success"),b(),window.refreshAll()):showNotification(s.error||"Failed to save settings","error")}catch(p){showNotification("Failed to save: "+p.message,"error")}}async function m(){if(f&&confirm(`Remove ${f.toUpperCase()} from dashboard? This won't affect the actual DNS server.`))try{const v=await(await secureFetch("/api/v1/config")).json();v.dnsServers&&delete v.dnsServers[f];const o=await(await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({dnsServers:v.dnsServers||{}})})).json();if(o.success){delete SITE.dnsServers[f];const p=document.querySelector(`.top [data-app="${f}"]`);p&&p.remove(),showNotification(`${f.toUpperCase()} removed from dashboard`,"success"),b()}else showNotification(o.error||"Failed to remove","error")}catch(r){showNotification("Failed to remove: "+r.message,"error")}}function b(){closeModal("dns-settings-modal"),f=null}document.getElementById("dns-settings-cancel")?.addEventListener("click",b),document.getElementById("dns-settings-save")?.addEventListener("click",g),document.getElementById("dns-settings-delete")?.addEventListener("click",m),document.getElementById("dns-settings-modal")?.addEventListener("click",r=>{r.target.id==="dns-settings-modal"&&b()}),document.querySelector(".top")?.addEventListener("click",r=>{const v=r.target.closest('[id$="-settings"]');if(!v)return;const d=v.id.replace("-settings","");SITE.dnsServers[d]&&(r.stopPropagation(),h(d))}),document.getElementById("refresh")?.addEventListener("click",window.refreshAll)})(),(function(){injectModal("logs-modal",`