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 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
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 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",`
\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 b of u)n.insertAdjacentHTML("beforeend",`
${y(b)}
`)}function v(){let n=safeSessionGet("dashcaddy-encryption-key");if(n)return n;const u=safeGet("dashcaddy-encryption-key");if(u)return safeSessionSet("dashcaddy-encryption-key",u),safeRemove("dashcaddy-encryption-key"),u;const b=new Uint8Array(32);return crypto.getRandomValues(b),n=Array.from(b,d=>d.toString(16).padStart(2,"0")).join(""),safeSessionSet("dashcaddy-encryption-key",n),n}const i=v();function s(n,u){if(!n)return"";const b=crypto.getRandomValues(new Uint8Array(8)),d=Array.from(b,L=>L.toString(16).padStart(2,"0")).join(""),E=new TextEncoder().encode(u+d);let T="";for(let L=0;LparseInt($,16))),P=atob(n.substring(17)),C=new TextEncoder().encode(u+T);let B="";for(let $=0;${["readonly","admin"].forEach(u=>{["token","username"].forEach(b=>{safeRemove(`${n}-${u}-${b}-enc`)})}),safeRemove(`${n}-token-enc`),safeRemove(`${n}-username-enc`)})}function f(n){const u=c(n,"readonly"),b=o(n,"readonly"),d=c(n,"admin"),E=o(n,"admin"),T=m(safeGet(`${n}-token-enc`),i),L=m(safeGet(`${n}-username-enc`),i);return{username:E||b||L,token:d||u||T,readonlyToken:u||T,readonlyUsername:b||L,adminToken:d||T,adminUsername:E||L}}document.getElementById("manage-tokens")?.addEventListener("click",()=>{p();const n=document.getElementById("token-management-modal"),u=e();a().forEach(b=>{const d=u[b];document.getElementById(`${b}-readonly-username`).value=d.readonly.username,document.getElementById(`${b}-readonly-token`).value=d.readonly.token,document.getElementById(`${b}-admin-username`).value=d.admin.username,document.getElementById(`${b}-admin-token`).value=d.admin.token,document.getElementById(`${b}-token-status`).textContent=""}),n.classList.add("show")}),document.getElementById("token-management-modal")?.addEventListener("click",n=>{const u=n.target.closest(".token-toggle");if(u){const b=u.dataset.target,d=document.getElementById(b);d.type==="password"?(d.type="text",u.textContent="\u{1F648}"):(d.type="password",u.textContent="\u{1F441}");return}n.target.id==="token-management-modal"&&n.target.classList.remove("show")}),document.getElementById("token-save")?.addEventListener("click",async()=>{const n=a();n.forEach(d=>{g(d,"readonly",document.getElementById(`${d}-readonly-username`).value.trim()),l(d,"readonly",document.getElementById(`${d}-readonly-token`).value.trim()),g(d,"admin",document.getElementById(`${d}-admin-username`).value.trim()),l(d,"admin",document.getElementById(`${d}-admin-token`).value.trim())});const u={};let b=!1;if(n.forEach(d=>{const E={},T=document.getElementById(`${d}-readonly-username`).value.trim(),L=document.getElementById(`${d}-readonly-token`).value.trim(),P=document.getElementById(`${d}-admin-username`).value.trim(),C=document.getElementById(`${d}-admin-token`).value.trim();T&&L&&(E.readonly={username:T,password:L},b=!0),P&&C&&(E.admin={username:P,password:C},b=!0),Object.keys(E).length>0&&(u[d]=E)}),b){n.forEach(d=>{u[d]&&(document.getElementById(`${d}-token-status`).textContent="Verifying...",document.getElementById(`${d}-token-status`).className="token-status")});try{const E=await(await secureFetch("/api/v1/dns/credentials",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({servers:u})})).json();E.results?n.forEach(T=>{const L=document.getElementById(`${T}-token-status`);if(!u[T]){L.textContent="";return}const P=E.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")}):E.success?n.forEach(T=>{u[T]&&(document.getElementById(`${T}-token-status`).textContent="\u2713 Saved",document.getElementById(`${T}-token-status`).className="token-status success")}):n.forEach(T=>{u[T]&&(document.getElementById(`${T}-token-status`).textContent="\u2717 "+(E.error||"Failed"),document.getElementById(`${T}-token-status`).className="token-status error")})}catch(d){console.error("Failed to sync DNS credentials to backend:",d),n.forEach(E=>{u[E]&&(document.getElementById(`${E}-token-status`).textContent="\u2713 Saved locally (sync failed)",document.getElementById(`${E}-token-status`).className="token-status")})}}else n.forEach(d=>{document.getElementById(`${d}-token-status`).textContent=""});setTimeout(()=>{n.every(E=>{const T=document.getElementById(`${E}-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.")){r(),a().forEach(n=>{document.getElementById(`${n}-readonly-username`).value="",document.getElementById(`${n}-readonly-token`).value="",document.getElementById(`${n}-admin-username`).value="",document.getElementById(`${n}-admin-token`).value="",document.getElementById(`${n}-token-status`).textContent="\u2713 Cleared",document.getElementById(`${n}-token-status`).className="token-status success"});try{await secureFetch("/api/v1/dns/credentials",{method:"DELETE"})}catch{}}}),window.getToken=c,window.getUsername=o,window.setToken=l,window.setUsername=g,window.getAllCredentials=e,window.getCredential=t,window.setCredential=h,window.getEncryptionKey=v,window.getDnsIds=a,window.getDnsDisplayName=y})(),(function(){function a(l,g,e=null){const r=document.getElementById(l+"-dot"),f=document.getElementById(l+"-pill"),n=document.getElementById(l+"-time"),u=document.querySelector(`[data-app="${l}"]`);r&&(r.classList.toggle("ok",g),r.classList.toggle("bad",!g)),f&&(f.textContent=g?"ON":"OFF",f.classList.toggle("on",g),f.classList.toggle("off",!g)),n&&e!==null&&(n.textContent=g?`${e}ms`:"timeout",n.className=`response-time ${y(e,g)}`),u&&u.setAttribute("data-status",g?"on":"off")}function y(l,g){return g?l<200?"excellent":l<500?"good":l<1e3?"fair":"slow":"timeout"}async function p(l){const g=performance.now();try{const e=await fetch("/probe/"+l,{cache:"no-store"}),r=performance.now(),f=Math.round(r-g);return{isUp:e.status>=200&&e.status<400||e.status===401||e.status===403,responseTime:f}}catch{const e=performance.now();return{isUp:!1,responseTime:Math.round(e-g)}}}window.APPS=[];let v=null,i=!1;async function s(){try{window.SkeletonLoader&&window.SkeletonLoader.show(6);const l=await fetch("/api/v1/services",{cache:"no-store"});if(l.ok){const g=await l.json();window.APPS=g.services||[],window.SkeletonLoader&&window.SkeletonLoader.hide()}else console.error("Failed to load services:",l.status),window.SkeletonLoader&&window.SkeletonLoader.hide()}catch(l){console.error("Failed to load services:",l),window.SkeletonLoader&&window.SkeletonLoader.hide()}}function m(l){const g=window.APPS?.find(r=>r.id===l);if(g?.url)return g.url.startsWith("http")?g.url:"https://"+g.url;if(g?.isExternal&&g.externalUrl)return g.externalUrl;const e=SITE.dnsServers?.[l];return e?"http://"+e.ip+":"+(e.port||5380):buildServiceUrl(l)}function t(l,g,e){const r=document.createElement(l);return g&&(r.className=g),e&&(r.textContent=e),r}function h(){const l=document.getElementById("cards");l.innerHTML="";for(let g=0;g{O.stopPropagation(),window.openContainerLogsModal(e.containerId,e.name)},w.appendChild(k);const S=t("button","update-btn","\u2B06\uFE0F");S.title="Update container to latest version",S.id=`update-btn-${e.id}`,S.onclick=O=>{O.stopPropagation(),window.updateContainer(e.containerId,e.name,e.id)},w.appendChild(S);const A=t("button","exec-btn",">_");A.title="Open terminal",A.onclick=O=>{O.stopPropagation(),window.openExecModal&&window.openExecModal(e.containerId,e.name)},w.appendChild(A)}if(e.logPath&&!e.containerId){const k=t("button","logs-btn","\u{1F4CB}");k.title="View application logs",k.onclick=S=>{S.stopPropagation(),window.openFileLogsModal(e.logPath,e.name)},w.appendChild(k)}if(e.isExternal||e.appTemplate||e.url){const k=t("button","creds-btn","\u{1F511}");k.title="Auto-login credentials",k.id=`creds-btn-${e.id}`,k.onclick=S=>{S.stopPropagation(),window.openServiceCredsModal(e)},w.appendChild(k)}if(e.id!=="internet"){const k=t("button","options-btn","\u2699\uFE0F");k.title="Edit service settings",k.onclick=S=>{S.stopPropagation(),window.openServiceEditModal(e)},w.appendChild(k)}if(e.id!=="internet"){const k=t("button","delete-btn","\u{1F5D1}\uFE0F");k.title="Delete this service",k.onclick=S=>{S.stopPropagation(),window.deleteService(e.id,e.name)},w.appendChild(k)}const I=t("button",null,"Open");I.onclick=()=>window.open(m(e.id),"_blank","noopener"),w.appendChild(I),r.appendChild(w),r.style.transitionDelay=`${Math.min(g*45,270)}ms`,l.appendChild(r)}requestAnimationFrame(()=>{l.querySelectorAll(".card").forEach(g=>g.classList.add("loaded"))}),window.groupRecipeCards&&requestAnimationFrame(()=>window.groupRecipeCards()),window.refreshServiceFilter&&window.refreshServiceFilter()}function c(l,g,e=null){const r=document.getElementById("dot-"+l+"-grid"),f=document.getElementById("badge-"+l),n=document.getElementById("time-"+l),u=document.querySelector(`[data-app="${l}"]`);r&&(r.classList.toggle("ok",g),r.classList.toggle("bad",!g)),f&&(f.textContent=g?"ON":"OFF",f.classList.toggle("on",g),f.classList.toggle("off",!g)),n&&e!==null&&(n.textContent=g?`${e}ms`:"timeout",n.className=`response-time ${y(e,g)}`),u&&u.setAttribute("data-status",g?"on":"off")}async function o(){if(v)return i=!0,v;function l(r,f=new Date){const n=document.getElementById("stamp");n&&(n.textContent=`${r}: ${new Date(f).toLocaleTimeString()}`)}function g(r){Object.keys(SITE.dnsServers).forEach(n=>{const u=r[n];u&&a(n,u.isUp,u.responseTime)}),r.internet&&a("internet",r.internet.isUp,r.internet.responseTime),window.APPS.forEach(n=>{const u=r[n.id];u&&c(n.id,u.isUp,u.responseTime)})}async function e(){const r=Object.keys(SITE.dnsServers),f=r.map(d=>p(d));f.push(p("internet"));const n=await Promise.all(f);r.forEach((d,E)=>a(d,n[E].isUp,n[E].responseTime));const u=n[n.length-1];a("internet",u.isUp,u.responseTime),(await Promise.all(window.APPS.map(async d=>{const E=await p(d.id);return{id:d.id,...E}}))).forEach(d=>{c(d.id,d.isUp,d.responseTime)})}return v=(async()=>{try{const r=await fetch("/api/v1/services/status",{cache:"no-store"});if(!r.ok)throw new Error(`Status refresh failed (${r.status})`);const f=await r.json();g(f.statuses||{}),l("last check",f.checkedAt||new Date)}catch(r){console.warn("Batched status refresh failed, falling back to direct probes:",r);try{await e(),l("last check")}catch(f){console.error("Dashboard refresh failed:",f),l("last failed")}}finally{v=null,i&&(i=!1,setTimeout(()=>{window.refreshAll()},0))}})(),v}document.querySelector(".top")?.addEventListener("click",l=>{const g=l.target.closest('[id$="-open"]');if(!g)return;const e=g.id.replace("-open","");SITE.dnsServers[e]&&window.open(m(e),"_blank","noopener")}),document.getElementById("ca-open")?.addEventListener("click",()=>window.open(m("ca"),"_blank","noopener")),document.getElementById("creds-btn-ca")?.addEventListener("click",l=>{l.stopPropagation();const g=window.APPS.find(e=>e.id==="ca");g&&window.openServiceCredsModal&&window.openServiceCredsModal(g)}),document.getElementById("options-btn-ca")?.addEventListener("click",l=>{l.stopPropagation();const g=window.APPS.find(e=>e.id==="ca");g&&window.openServiceEditModal&&window.openServiceEditModal(g)}),document.getElementById("delete-btn-ca")?.addEventListener("click",l=>{l.stopPropagation(),window.deleteService&&window.deleteService("ca","DashCA")}),window.loadServices=s,window.buildGrid=h,window.refreshAll=o,window.setQuick=a,window.setBadge=c,window.getResponseTimeClass=y,window.checkServiceWithTiming=p,window.serviceUrl=m,window.el=t})(),(function(){async function a(t){const c=await(await secureFetch(`/api/v1/dns/restart/${t}`,{method:"POST"})).json();if(!c.success)throw new Error(c.error||"Restart failed");return c}document.querySelector(".top")?.addEventListener("click",async t=>{const h=t.target.closest('[id$="-restart"]');if(!h)return;const c=h.id.replace("-restart","");if(SITE.dnsServers[c]&&confirm(`Restart ${c.toUpperCase()} service?`))try{await withButton(h,"...",()=>a(c)),setTimeout(window.refreshAll,DC.DELAYS.RELOAD)}catch(o){showNotification("Restart failed: "+o.message,"error")}});async function y(t,h){const c=document.getElementById(`${t}-update`),o=c?.textContent||"\u2B06\uFE0F";try{c.textContent="\u{1F50D}",c.disabled=!0,c.title="Checking for updates...";const g=await(await fetch(`/api/v1/dns/check-update?server=${encodeURIComponent(h)}`)).json();if(!g.success)throw new Error(g.error||"Failed to check for updates");if(!g.updateAvailable){c.textContent="\u2705",c.title=`Already on latest version (${g.currentVersion})`,showNotification(`${t.toUpperCase()} is already up to date! Current version: ${g.currentVersion}`,"info"),setTimeout(()=>{c.textContent=o,c.disabled=!1,c.title="Update DNS server"},3e3);return}if(!confirm(`Update available for ${t.toUpperCase()}!
Current: ${g.currentVersion}
New: ${g.updateVersion}
`+(g.updateTitle?`${g.updateTitle}
`:"")+`The DNS server will restart during the update.
Proceed?`)){c.textContent=o,c.disabled=!1,c.title="Update DNS server";return}c.textContent="\u{1F504}",c.title="Updating...";const f=await(await secureFetch(`/api/v1/dns/update?server=${encodeURIComponent(h)}`,{method:"POST"})).json();if(!f.success)throw new Error(f.error||"Update failed");if(f.manualUpdateRequired){c.textContent="\u2B06\uFE0F",c.title=`Update available: ${f.newVersion}`;const n=f.downloadLink?`
Download: ${f.downloadLink}`:"",u=f.instructionsLink?`
Instructions: ${f.instructionsLink}`:"";showNotification(`${t.toUpperCase()} update requires manual installation. Current: ${f.previousVersion} \u2192 ${f.newVersion}. Please update manually on the host machine.`,"warning",8e3),c.disabled=!1;return}c.textContent="\u2705",c.title="Updated successfully!",showNotification(`${t.toUpperCase()} updated successfully! ${f.previousVersion} \u2192 ${f.newVersion}. Server is restarting...`,"success"),setTimeout(()=>{c.textContent=o,c.disabled=!1,c.title="Update DNS server",window.refreshAll()},1e4)}catch(l){console.error("DNS update error:",l),c.textContent="\u274C",c.title="Update failed",showNotification(`Failed to update ${t.toUpperCase()}: ${l.message}`,"error"),setTimeout(()=>{c.textContent=o,c.disabled=!1,c.title="Update DNS server"},3e3)}}document.querySelector(".top")?.addEventListener("click",t=>{const h=t.target.closest('[id$="-update"]');if(!h)return;const c=h.id.replace("-update","");SITE.dnsServers[c]&&y(c,SITE.dnsServers[c]?.ip)}),injectModal("dns-settings-modal",`
DNS Settings
Manage credentials via Tokens in the toolbar
`);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",`