const DC={NAME:"DashCaddy",POLL:{DASHBOARD:1e4,LOGS:3e3,STATS:5e3,WEATHER:6e5,HEALTH:1e3,DEPLOY_SSL:5e3},DELAYS:{BTN_RESET:2e3,RELOAD:5e3,MODAL_CLOSE:500,PORT_CHECK:500,DEPLOY_INIT:3e3},DEFAULTS:{DNS_PORT:"5380",SERVICE_PORT:"8080",TTL:300,CADDYFILE:"C:\\caddy\\Caddyfile"}},_cachedCfg=JSON.parse(localStorage.getItem("dashcaddy_site_config")||"null"),SITE={tld:_cachedCfg&&_cachedCfg.tld||".home",dnsIp:"",dnsPort:DC.DEFAULTS.DNS_PORT,dnsServers:{},configurationType:_cachedCfg&&_cachedCfg.configurationType||"homelab",domain:_cachedCfg&&_cachedCfg.domain||"",defaults:_cachedCfg&&_cachedCfg.defaults||{},routingMode:_cachedCfg&&_cachedCfg.routingMode||"subdomain",onboardingCompleted:!1};window.__dashcaddySiteConfigLoaded=(async function(){try{const f=await fetch("/api/v1/config");if(f.ok){const m=await f.json();if(m.tld&&(SITE.tld=m.tld.startsWith(".")?m.tld:"."+m.tld),m.dns&&(SITE.dnsIp=m.dns.ip||"",SITE.dnsPort=m.dns.port||DC.DEFAULTS.DNS_PORT),m.dnsServers&&typeof m.dnsServers=="object")for(const[b,a]of Object.entries(m.dnsServers))b!=="__proto__"&&b!=="constructor"&&b!=="prototype"&&(SITE.dnsServers[b]=a);m.configurationType&&(SITE.configurationType=m.configurationType),m.domain&&(SITE.domain=m.domain),m.defaults&&(SITE.defaults=m.defaults),m.routingMode&&(SITE.routingMode=m.routingMode),SITE.onboardingCompleted=m.onboardingCompleted===!0,localStorage.setItem("dashcaddy_site_config",JSON.stringify({tld:SITE.tld,configurationType:SITE.configurationType,domain:SITE.domain,routingMode:SITE.routingMode})),renderDnsCards();const l=document.getElementById("manage-tokens");l&&(l.style.display=Object.keys(SITE.dnsServers).length?"":"none")}}catch{}document.querySelectorAll("[data-tld]").forEach(f=>f.textContent=SITE.tld);const i=document.getElementById("edit-tld-suffix");i&&(i.textContent=SITE.tld);const v=document.getElementById("external-proxy-ip");v&&SITE.dnsIp&&(v.value=SITE.dnsIp,v.placeholder=SITE.dnsIp)})();function buildDomain(o){return o+SITE.tld}function buildServiceUrl(o){return SITE.routingMode==="subdirectory"&&SITE.domain?"https://"+SITE.domain+"/"+o:SITE.configurationType==="public"&&SITE.domain?"https://"+o+"."+SITE.domain:"https://"+buildDomain(o)}function getDnsServerAddr(o){const i=SITE.dnsServers[o];return i?`${i.ip}:${i.port}`:buildDomain(o)}function getPrimaryDnsId(){if(!SITE.dnsIp)return null;for(const[o,i]of Object.entries(SITE.dnsServers))if(i.ip===SITE.dnsIp)return o;return null}function renderDnsCards(){const o=document.querySelector(".top");if(!o)return;const i=Object.keys(SITE.dnsServers);if(!i.length)return;const v='',f=o.firstElementChild;i.forEach(m=>{const l=escapeHtml(m),b=escapeHtml((SITE.dnsServers[m].name||m).toUpperCase()),a=document.createElement("div");a.className="card",a.setAttribute("data-app",m),a.setAttribute("data-status","off"),a.innerHTML=`
${v}
${b}OFF
--
--
`,o.insertBefore(a,f)})}window.renderDnsCards=renderDnsCards;let csrfToken=null;async function getCSRFToken(){if(csrfToken)return csrfToken;try{const o=await fetch("/api/v1/csrf-token");if(!o.ok)throw new Error("Failed to fetch CSRF token");return csrfToken=(await o.json()).token,csrfToken}catch(o){throw console.error("Failed to get CSRF token:",o),o}}async function secureFetch(o,i={}){const v=(i.method||"GET").toUpperCase(),f=!["GET","HEAD","OPTIONS"].includes(v);if(f)try{const l=await getCSRFToken();i.headers={...i.headers,"X-CSRF-Token":l}}catch(l){console.error("Failed to add CSRF token to request:",l)}i.signal||(i={...i,signal:AbortSignal.timeout(15e3)});const m=await fetch(o,i);if(f&&m.status===403)try{const l=await m.clone().json();if(l.error&&(l.error.includes("DC-100")||l.error.includes("DC-101"))){csrfToken=null;const b=await getCSRFToken();return i.headers={...i.headers,"X-CSRF-Token":b},i.signal=AbortSignal.timeout(15e3),fetch(o,i)}}catch{}return m}async function postJSON(o,i){const v=await secureFetch(o,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)}),f=await v.json();if(!v.ok||f.success===!1)throw new Error(f.error||`Request failed (${v.status})`);return f}async function getJSON(o){const i=await secureFetch(o);if(!i.ok){let v=`Request failed (${i.status})`;try{v=(await i.json()).error||v}catch{}throw new Error(v)}return i.json()}async function deleteAPI(o){const i=await secureFetch(o,{method:"DELETE"}),v=await i.json();if(!i.ok||v.success===!1)throw new Error(v.error||`Delete failed (${i.status})`);return v}async function withButton(o,i,v,f={}){const m=o.innerHTML,{successText:l="\u2705",resetDelay:b=DC.DELAYS.BTN_RESET}=f;o.disabled=!0,o.innerHTML=i;try{const a=await v();return o.innerHTML=l,setTimeout(()=>{o.innerHTML=m,o.disabled=!1},b),a}catch(a){throw o.innerHTML=m,o.disabled=!1,a}}function openModal(o){document.getElementById(o)?.classList.add("show")}function closeModal(o){document.getElementById(o)?.classList.remove("show")}function wireModal(o,...i){o&&(o.addEventListener("click",v=>{v.target===o&&o.classList.remove("show")}),i.forEach(v=>v?.addEventListener("click",()=>o.classList.remove("show"))))}function showNotification(o,i="info",v=3e3){const f=document.querySelector(".deploy-notification");f&&f.remove();const m={info:{bg:"#2196F3",fg:"#fff"},success:{bg:"var(--ok-bg)",fg:"var(--ok-fg)"},error:{bg:"#f44336",fg:"#fff"},warning:{bg:"#ff9800",fg:"#fff"}},l=m[i]||m.info,b=document.createElement("div");b.className="deploy-notification",b.textContent=o,b.style.cssText=` position: fixed; top: 20px; right: 20px; background: ${l.bg}; color: ${l.fg}; padding: 16px 24px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,.3); z-index: 10000; animation: slideIn 0.3s ease-out; max-width: 400px; white-space: pre-line; font-size: 14px; `,document.body.appendChild(b),v>0&&setTimeout(()=>b.remove(),v)}function timeAgo(o){const i=Date.now()-new Date(o).getTime();return i<6e4?"just now":i<36e5?Math.floor(i/6e4)+"m ago":i<864e5?Math.floor(i/36e5)+"h ago":Math.floor(i/864e5)+"d ago"}function safeGet(o,i=null){try{const v=localStorage.getItem(o);return v!==null?v:i}catch{return i}}function safeSet(o,i){try{localStorage.setItem(o,i)}catch{}}function safeRemove(o){try{localStorage.removeItem(o)}catch{}}function safeSessionGet(o,i=null){try{const v=sessionStorage.getItem(o);return v!==null?v:i}catch{return i}}function safeSessionSet(o,i){try{sessionStorage.setItem(o,i)}catch{}}function safeGetJSON(o,i=null){try{const v=localStorage.getItem(o);return v?JSON.parse(v):i}catch{return i}}function escapeHtml(o){return String(o??"").replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function injectModal(o,i){document.getElementById(o)||document.body.insertAdjacentHTML("beforeend",i)}const DC_BUS={_handlers:{},on(o,i){var v;((v=this._handlers)[o]||(v[o]=[])).push(i)},off(o,i){this._handlers[o]=this._handlers[o]?.filter(v=>v!==i)},emit(o,i){this._handlers[o]?.forEach(v=>v(i))}},AppState={_apps:[],getApps(){return this._apps},setApps(o){this._apps=o,window.APPS=o,DC_BUS.emit("apps:changed",o)},findApp(o){return this._apps.find(i=>i.id===o)},addApp(o){this._apps.push(o),window.APPS=this._apps,DC_BUS.emit("apps:changed",this._apps)},removeApp(o){const i=this._apps.findIndex(v=>v.id===o);return i>-1&&(this._apps.splice(i,1),window.APPS=this._apps,DC_BUS.emit("apps:changed",this._apps)),i>-1},updateApp(o,i){const v=this._apps.find(f=>f.id===o);if(v){for(const[f,m]of Object.entries(i))f!=="__proto__"&&f!=="constructor"&&f!=="prototype"&&(v[f]=m);DC_BUS.emit("apps:changed",this._apps)}return v}};(function(){function o(){const f=document.createElement("div");return f.className="skeleton-card",f.innerHTML='
',f}function i(f){const m=document.getElementById("cards");if(!(!m||m.querySelector(".card"))){f=f||6;for(let l=0;l.4,P={};return P.hover=S?u(g,T,.35):u(g,$,.08),P["card-hover"]=u(g,P.hover,.5),P.base=u(T,g,.6),P["fg-muted"]=u(w,T,.35),P.success=C,P.error=E,P.warning=S?"#d68a00":"#f39c12",P}function s(x,T){var $=T.lightBg||T.bg&&h(T.bg)>.4,w=T.accent||T["accent-strong"]||"#888888",g=t(w);return $?":root."+x+` body { background: radial-gradient(1200px 800px at 10% -10%, rgba(`+g.r+","+g.g+","+g.b+`, .08), transparent 60%), radial-gradient(1000px 700px at 110% 10%, rgba(`+g.r+","+g.g+","+g.b+`, .05), transparent 55%), var(--bg); } `:":root."+x+` body { background: radial-gradient(1200px 900px at 8% -12%, rgba(`+g.r+","+g.g+","+g.b+`, .10), transparent 60%), radial-gradient(1000px 700px at 110% -10%, rgba(`+g.r+","+g.g+","+g.b+`, .07), transparent 55%), var(--bg); } `}function p(x,T){var $=T.lightBg||T.bg&&h(T.bg)>.4;return $?":root."+x+` button:hover { background: color-mix(in srgb, var(--accent-strong) 12%, white 88%); border-color: rgba(0, 0, 0, .15); box-shadow: 0 1px 6px rgba(0, 0, 0, .08), inset 0 1px 0 rgba(255, 255, 255, .8); } `:":root."+x+` button:hover { background: color-mix(in srgb, var(--accent) 18%, transparent); border-color: color-mix(in srgb, var(--accent) 35%, var(--border)); } `}function n(){return window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}function c(){l.forEach(function(x){document.documentElement.style.removeProperty("--"+x)})}function y(x,T){var $=x.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"");$||($="custom"),f.indexOf($)!==-1&&($=$+"-custom");for(var w=safeGetJSON(i,{}),g=$,C=2;w[$]&&$!==T;)$=g+"-"+C++;return $}function r(x){var T=document.getElementById("user-theme-styles");T&&T.remove(),m.length=f.length,Object.keys(k).forEach(function(E){f.indexOf(E)===-1&&delete k[E]});var $=x||safeGetJSON(i,{}),w=Object.keys($);if(w=w.filter(function(E){return f.indexOf(E)===-1}),!!w.length){var g="";w.forEach(function(E){var S=$[E];m.indexOf(E)===-1&&m.push(E);var P={};l.forEach(function(O){S[O]&&(P[O]=S[O])}),P["card-bg"]=S["card-base"]||S.bg,S.lightBg&&(P.lightBg=!0);var N=e(P);a.forEach(function(O){!P[O]&&N[O]&&(P[O]=N[O])}),k[E]=P,g+=":root."+E+` { `,l.forEach(function(O){P[O]&&(g+=" --"+O+": "+P[O]+`; `)}),g+=`} `,g+=s(E,P),g+=p(E,P)});var C=document.createElement("style");C.id="user-theme-styles",C.textContent=g,document.head.appendChild(C)}}function I(){secureFetch("/api/v1/themes").then(function(x){return x.json()}).then(function(x){if(!(!x.success||!x.themes)){var T=x.themes,$=safeGetJSON(i,{});if(JSON.stringify(T)!==JSON.stringify($)){safeSet(i,JSON.stringify(T)),r(T);var w=safeGet(o);w&&m.indexOf(w)!==-1&&L(w)}}}).catch(function(){})}function B(){var x=safeGetJSON(v);if(x){var T=x.name||"Custom",$=y(T),w={name:T};l.forEach(function(E){x[E]&&(w[E]=x[E])});var g=safeGetJSON(i,{});g[$]=w,safeSet(i,JSON.stringify(g)),safeGet(o)==="custom"&&safeSet(o,$),safeRemove(v);var C={};l.forEach(function(E){w[E]&&(C[E]=w[E])}),fetch("/api/v1/themes/"+$,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:T,colors:C})}).catch(function(){})}}function L(x){document.documentElement.classList.add("theme-transitioning"),m.forEach(function(g){g!=="dark"&&document.documentElement.classList.remove(g)}),c(),x!=="dark"&&document.documentElement.classList.add(x),safeSet(o,x);var T=k[x],$=document.querySelector('meta[name="theme-color"]');$&&T&&$.setAttribute("content",T.bg);var w=T&&T.lightBg;!w&&T&&T.bg&&(w=h(T.bg)>.4),w?document.documentElement.classList.add("light-bg"):document.documentElement.classList.remove("light-bg"),setTimeout(function(){document.documentElement.classList.remove("theme-transitioning")},300)}B(),r();var A=safeGet(o);A==="red"&&(A="black",safeSet(o,"black")),A&&A!=="dark"&&m.indexOf(A)===-1&&(A=null),L(A||n()),I(),window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",function(x){safeGet(o)||L(x.matches?"dark":"light")}),window.THEMES=m,window.BUILTIN_THEMES=f,window.THEME_COLORS=k,window.THEME_PROPS=l,window.BASE_PROPS=b,window.DERIVED_PROPS=a,window.USER_THEMES_KEY=i,window.applyTheme=L,window.clearCustomProperties=c,window.injectUserThemeStyles=r,window.syncThemesFromServer=I,window.slugifyThemeName=y,window.getActiveTheme=function(){return safeGet(o)||n()},window.deriveExtendedColors=e,window.hexToRgb=t,window.rgbToHex=d,window.blendColors=u})(),(function(){function o(){const b=document.querySelector(".totp-card");if(!b)return;const k=getComputedStyle(b).backgroundColor.match(/\d+/g);if(!k)return;const t=(.299*+k[0]+.587*+k[1]+.114*+k[2])/255,d=b.querySelector(".totp-logo-dark"),u=b.querySelector(".totp-logo-light");d&&(d.style.display=t>.5?"none":""),u&&(u.style.display=t>.5?"":"none")}function i(){const b=document.getElementById("totp-overlay");if(b){b.classList.add("show"),setTimeout(o,50);const a=b.querySelector(".totp-digits input");a&&setTimeout(()=>a.focus(),100)}}function v(){const b=document.getElementById("totp-overlay");b&&b.classList.remove("show")}const f=document.getElementById("totp-digits");if(f){const b=f.querySelectorAll("input");b.forEach((a,k)=>{a.addEventListener("input",t=>{const d=t.target.value.replace(/\D/g,"");t.target.value=d.slice(0,1),d&&kh.value).join("");u.length===6&&m(u)}),a.addEventListener("keydown",t=>{t.key==="Backspace"&&!t.target.value&&k>0&&(b[k-1].focus(),b[k-1].value="")}),a.addEventListener("paste",t=>{t.preventDefault();const d=(t.clipboardData.getData("text")||"").replace(/\D/g,"");d.length>=6&&(b.forEach((u,h)=>{u.value=d[h]||""}),b[5].focus(),m(d.slice(0,6)))})})}async function m(b){const a=document.getElementById("totp-error");a.textContent="Verifying...",a.className="totp-error verifying";try{const t=await(await secureFetch("/api/v1/totp/verify",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:b})})).json();if(t.success){a.textContent="",t.csrfToken&&(csrfToken=t.csrfToken),v();const d=safeSessionGet("totp_redirect");if(d){try{sessionStorage.removeItem("totp_redirect")}catch{}window.location.href=d;return}typeof window.initializeDashboard=="function"&&window.initializeDashboard()}else{a.textContent=t.error||"Invalid code",a.className="totp-error";const d=document.querySelectorAll("#totp-digits input");d.forEach(u=>{u.value=""}),d[0]?.focus()}}catch{a.textContent="Connection error",a.className="totp-error"}}const l=new URLSearchParams(window.location.search);if(l.get("auth")==="required"){const b=l.get("return");if(b)try{const a=new URL(b,window.location.origin),k=a.hostname,t=a.origin===window.location.origin,d=SITE.tld.startsWith(".")?SITE.tld:"."+SITE.tld,u=k.endsWith(d)||k===d.substring(1);(t||u)&&safeSessionSet("totp_redirect",b)}catch{}window.history.replaceState({},"",window.location.pathname)}window._showTotpOverlay=i})(),(function(){injectModal("folder-browser-modal",`

\u{1F4C2} Browse for Media Folders

/
Loading...
`),injectModal("service-creds-modal",`

Service Credentials

Credentials are injected automatically when accessing this service.

No credentials stored
`);const o=document.getElementById("service-creds-modal");let i=null;const v=["sonarr","radarr","prowlarr","overseerr"],f=["sonarr","radarr"];function m(t){return t.externalUrl||t.url||""}function l(t){const d=document.getElementById("svc-creds-error");d.textContent=t,d.style.display=""}function b(){const t=document.getElementById("svc-creds-error");t.textContent="",t.style.display="none"}window.openServiceCredsModal=async function(t){i=t,b();const d=document.getElementById("svc-creds-title"),u=document.getElementById("svc-creds-desc"),h=document.getElementById("svc-creds-seedhost"),e=document.getElementById("svc-creds-apikey"),s=document.getElementById("svc-creds-basic"),p=document.getElementById("svc-creds-quality");d.textContent=t.name+" Credentials";const n=!!t.isExternal,c=v.includes(t.id)||v.includes(t.appTemplate),y=f.includes(t.id)||f.includes(t.appTemplate);h.style.display=n?"":"none",e.style.display=c?"":"none",p.style.display=y?"":"none",s.style.display=n?"none":"";const r=document.getElementById("svc-quality-select");r.innerHTML='',document.getElementById("svc-quality-status").textContent="",n?(u.textContent="Seedhost credentials auto-login past the HTTP prompt. API key bypasses the app login.",document.getElementById("svc-seedhost-pass").placeholder=`Password for ${t.name}`):c?u.textContent="API key bypasses the app login screen automatically.":u.textContent="Credentials are injected automatically when accessing this service.",await a(t),o.classList.add("show")};async function a(t){const d=document.getElementById("svc-creds-dot"),u=document.getElementById("svc-creds-status"),h=document.getElementById("svc-creds-clear");let e=!1;if(t.isExternal){try{const n=await(await fetch(`/api/v1/seedhost-creds?serviceId=${t.id}`)).json();n.success?(document.getElementById("svc-seedhost-user").value=n.username||"",n.hasCredentials&&(e=!0)):document.getElementById("svc-seedhost-user").value=""}catch{}document.getElementById("svc-seedhost-pass").value=""}try{const n=await(await fetch(`/api/v1/services/${t.id}/credentials`)).json();n.success&&(n.hasApiKey?(document.getElementById("svc-apikey-input").value="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",e=!0):document.getElementById("svc-apikey-input").value="",n.hasBasicAuth&&!t.isExternal?(document.getElementById("svc-basic-user").value=n.username||"",e=!0):document.getElementById("svc-basic-user").value="")}catch{}document.getElementById("svc-basic-pass")&&(document.getElementById("svc-basic-pass").value="");const s=t.id||t.appTemplate;if(f.includes(s)&&await k(t),e){d.style.background="var(--ok-fg, #74dfc4)",u.style.color="var(--ok-fg, #74dfc4)",u.textContent="Credentials stored",h.style.display="";const p=document.getElementById(`creds-btn-${t.id}`);p&&p.classList.add("has-creds")}else d.style.background="var(--muted)",u.style.color="var(--muted)",u.textContent="No credentials stored",h.style.display="none"}async function k(t){const d=document.getElementById("svc-quality-select"),u=document.getElementById("svc-quality-status"),h=t.id||t.appTemplate,e=m(t);if(!e){d.innerHTML='';return}d.innerHTML='',u.textContent="";try{const s=new URLSearchParams({service:h,url:e}),n=await(await fetch(`/api/v1/arr/quality-profiles?${s}`)).json();if(!n.success||!n.profiles?.length){d.innerHTML='';return}d.innerHTML="";for(const c of n.profiles){const y=document.createElement("option");y.value=c.id,y.textContent=c.name,d.appendChild(y)}if(n.storedProfileId&&(d.value=String(n.storedProfileId)),!d.value){const c=n.profiles.find(y=>/720/i.test(y.name));c&&(d.value=String(c.id))}!d.value&&n.profiles.length&&(d.value=String(n.profiles[0].id)),u.innerHTML=`${n.profiles.length} profiles loaded`}catch(s){d.innerHTML='',u.innerHTML=`Error: ${s.message}`}}document.getElementById("svc-quality-fetch")?.addEventListener("click",async()=>{if(!i)return;const t=i.id||i.appTemplate,d=m(i),h=document.getElementById("svc-apikey-input")?.value.trim(),e=document.getElementById("svc-quality-select"),s=document.getElementById("svc-quality-status");if(!d){s.innerHTML='No service URL available';return}if(!h||h==="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022"){s.innerHTML='Enter an API key first';return}e.innerHTML='',s.textContent="";try{const p=new URLSearchParams({service:t,url:d,apiKey:h}),c=await(await fetch(`/api/v1/arr/quality-profiles?${p}`)).json();if(!c.success){e.innerHTML='',s.innerHTML=`${c.error||"Failed to fetch profiles"}`;return}if(!c.profiles?.length){e.innerHTML='';return}e.innerHTML="";for(const r of c.profiles){const I=document.createElement("option");I.value=r.id,I.textContent=r.name,e.appendChild(I)}const y=c.profiles.find(r=>/720/i.test(r.name));y?e.value=String(y.id):c.profiles.length&&(e.value=String(c.profiles[0].id)),s.innerHTML=`${c.profiles.length} profiles loaded`}catch(p){e.innerHTML='',s.innerHTML=`${p.message}`}}),document.getElementById("svc-creds-save")?.addEventListener("click",async()=>{if(!i)return;const t=document.getElementById("svc-creds-save");t.textContent="Saving...",t.disabled=!0,b();try{const d=v.includes(i.id)||v.includes(i.appTemplate),u=i.id||i.appTemplate;if(i.isExternal){const s=document.getElementById("svc-seedhost-user").value.trim(),p=document.getElementById("svc-seedhost-pass").value;s&&await secureFetch("/api/v1/seedhost-creds",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:s,password:p||void 0,serviceId:i.id})})}const e=document.getElementById("svc-apikey-input")?.value.trim();if(e&&e!=="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022")if(d){const s=m(i),p=document.getElementById("svc-quality-select"),n=p?.value?parseInt(p.value):void 0,c=p?.selectedOptions?.[0]?.textContent||void 0,r=await(await secureFetch("/api/v1/arr/credentials",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({service:u,apiKey:e,url:s||void 0,qualityProfileId:n||void 0,qualityProfileName:c||void 0})})).json();if(!r.success){l(r.error||"Failed to save API key"),t.textContent="Save",t.disabled=!1;return}r.connectionTest&&!r.connectionTest.success&&l(`API key saved but connection test failed: ${r.connectionTest.error}`)}else await secureFetch(`/api/v1/services/${i.id}/credentials`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({apiKey:e})});else if(d&&f.includes(u)){const s=document.getElementById("svc-quality-select"),p=s?.value?parseInt(s.value):void 0,n=s?.selectedOptions?.[0]?.textContent||void 0;p&&await secureFetch("/api/v1/arr/quality-profiles",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({service:u,qualityProfileId:p,qualityProfileName:n})})}if(!i.isExternal){const s=document.getElementById("svc-basic-user").value.trim(),p=document.getElementById("svc-basic-pass").value;s&&p&&await secureFetch(`/api/v1/services/${i.id}/credentials`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:s,password:p})})}await a(i)}catch(d){console.error("Failed to save credentials:",d),l("Failed to save: "+(d.message||"Unknown error"))}t.textContent="Save",t.disabled=!1}),document.getElementById("svc-creds-clear")?.addEventListener("click",async()=>{if(i&&confirm(`Remove stored credentials for ${i.name}?`)){b();try{const t=i.id||i.appTemplate,d=v.includes(t);i.isExternal&&await secureFetch(`/api/v1/seedhost-creds?serviceId=${i.id}`,{method:"DELETE"}),await secureFetch(`/api/v1/services/${i.id}/credentials`,{method:"DELETE"}),d&&await secureFetch(`/api/v1/arr/credentials/${t}`,{method:"DELETE"});const u=document.getElementById(`creds-btn-${i.id}`);u&&u.classList.remove("has-creds"),await a(i)}catch(t){console.error("Failed to clear credentials:",t),l("Failed to clear: "+(t.message||"Unknown error"))}}}),document.getElementById("svc-creds-close")?.addEventListener("click",()=>{o.classList.remove("show"),i=null}),o?.addEventListener("click",t=>{t.target===o&&(o.classList.remove("show"),i=null)}),window.refreshCredsButtons=async function(){try{for(const t of window.APPS||[]){if(!t.isExternal&&!t.appTemplate&&!t.url)continue;let d=!1;if(t.isExternal)try{const e=await(await fetch(`/api/v1/seedhost-creds?serviceId=${t.id}`)).json();e.success&&e.hasCredentials&&(d=!0)}catch{}try{const e=await(await fetch(`/api/v1/services/${t.id}/credentials`)).json();e.success&&(e.hasApiKey||e.hasBasicAuth)&&(d=!0)}catch{}const u=document.getElementById(`creds-btn-${t.id}`);u&&u.classList.toggle("has-creds",d)}}catch{}}})(),(function(){injectModal("totp-settings-modal",`

Authentication Settings

TOTP is not configured
or
`);async function o(){try{const m=await(await fetch("/api/v1/totp/config")).json();if(!m.success)return;const{enabled:l,sessionDuration:b,isSetUp:a}=m.config,k=document.getElementById("totp-status-dot"),t=document.getElementById("totp-status-text"),d=document.getElementById("totp-status-banner"),u=document.getElementById("totp-setup-section"),h=document.getElementById("totp-qr-section"),e=document.getElementById("totp-duration-section"),s=document.getElementById("totp-disable-section");l&&a?(k.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)",t.textContent="TOTP is active",t.style.color="var(--ok-fg, #7ef2ff)",u.style.display="none",h.style.display="none",e.style.display="block",s.style.display="block",document.getElementById("totp-duration-select").value=b):(k.style.background="var(--muted)",d.style.borderColor="var(--border)",d.style.background="transparent",t.textContent="TOTP is not configured",t.style.color="var(--muted)",u.style.display="block",h.style.display="none",e.style.display="none",s.style.display="none"),v(l&&a,b)}catch(f){console.warn("Failed to load TOTP settings:",f)}}const i={"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(f,m){const l=document.getElementById("auth-card"),b=document.getElementById("auth-pill"),a=document.getElementById("auth-dot"),k=document.getElementById("auth-status-text");l&&(f?(l.setAttribute("data-status","on"),b.className="badge on",b.textContent="YES",a.className="dot ok at-bl",k.textContent="Session: "+(i[m]||m)):(l.setAttribute("data-status","off"),b.className="badge off",b.textContent="NO",a.className="dot bad at-bl",k.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(f){console.error("TOTP setup failed:",f)}}),document.getElementById("totp-import-btn")?.addEventListener("click",async()=>{const f=document.getElementById("totp-import-key").value.trim(),m=document.getElementById("totp-import-error");if(m.textContent="",!f){m.textContent="Paste a Base32 secret key first";return}try{const b=await(await secureFetch("/api/v1/totp/setup",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({secret:f})})).json();b.success?(m.textContent="",document.getElementById("totp-qr-image").src=b.qrCode,document.getElementById("totp-manual-key").textContent=b.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=b.error||b.message||"Import failed"}catch{m.textContent="Connection error \u2014 try refreshing the page"}}),document.getElementById("totp-copy-key")?.addEventListener("click",()=>{const f=document.getElementById("totp-manual-key").textContent;navigator.clipboard.writeText(f).then(()=>{const m=document.getElementById("totp-copy-key");m.textContent="\u2705",setTimeout(()=>{m.textContent="\u{1F4CB}"},2e3)})}),document.getElementById("totp-confirm-setup")?.addEventListener("click",async()=>{const f=document.getElementById("totp-setup-code").value,m=document.getElementById("totp-setup-error");if(!/^\d{6}$/.test(f)){m.textContent="Enter a 6-digit code";return}try{const b=await(await secureFetch("/api/v1/totp/verify-setup",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:f})})).json();b.success?(m.textContent="",o()):m.textContent=b.error||"Invalid code"}catch{m.textContent="Connection error"}}),document.getElementById("totp-setup-code")?.addEventListener("keydown",f=>{f.key==="Enter"&&document.getElementById("totp-confirm-setup")?.click()}),document.getElementById("totp-duration-select")?.addEventListener("change",async f=>{try{await secureFetch("/api/v1/totp/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sessionDuration:f.target.value})}),o()}catch(m){console.error("Failed to update session duration:",m)}}),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&&o()}catch(f){console.error("Failed to disable TOTP:",f)}}),document.getElementById("auth-settings-btn")?.addEventListener("click",()=>{o(),openModal("totp-settings-modal")}),document.getElementById("totp-modal-close")?.addEventListener("click",()=>{closeModal("totp-settings-modal")}),document.getElementById("totp-settings-modal")?.addEventListener("click",f=>{f.target.id==="totp-settings-modal"&&closeModal("totp-settings-modal")}),window._updateAuthCard=v,(async()=>{try{const m=await(await fetch("/api/v1/totp/config")).json();if(m.success){const l=m.config.enabled&&m.config.isSetUp;v(l,m.config.sessionDuration)}}catch(f){console.error("[AuthCard] Failed to update:",f)}})()})(),(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.

`);function o(){return Object.keys(SITE.dnsServers||{})}function i(n){return(SITE.dnsServers||{})[n]?.name||n.toUpperCase()}function v(){const n=document.getElementById("dns-cred-sections");if(!n)return;n.innerHTML="";const c=o();if(c.length===0){n.innerHTML='

No DNS servers configured.

';return}for(const y of c)n.insertAdjacentHTML("beforeend",`

${i(y)}

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

DNS Settings

Manage credentials via Tokens in the toolbar
`);let v=null;function f(a){v=a;const k=SITE.dnsServers[a]||{},t=document.getElementById("dns-settings-modal");document.getElementById("dns-settings-title").textContent=`${(k.name||a).toUpperCase()} Settings`,document.getElementById("dns-edit-ip").value=k.ip||"",document.getElementById("dns-edit-port").value=k.port||DC.DEFAULTS.DNS_PORT,document.getElementById("dns-edit-name").value=k.name||"",t.classList.add("show")}async function m(){if(!v)return;const a=document.getElementById("dns-edit-ip").value.trim(),k=document.getElementById("dns-edit-port").value.trim()||DC.DEFAULTS.DNS_PORT,t=document.getElementById("dns-edit-name").value.trim();if(!a){showNotification("Server IP is required","warning");return}const d={dnsServers:{}};d.dnsServers[v]={ip:a,port:String(k)},t&&(d.dnsServers[v].name=t);try{const h=await(await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(d)})).json();h.success?(SITE.dnsServers[v]=d.dnsServers[v],showNotification(`${v.toUpperCase()} settings saved`,"success"),b(),window.refreshAll()):showNotification(h.error||"Failed to save settings","error")}catch(u){showNotification("Failed to save: "+u.message,"error")}}async function l(){if(v&&confirm(`Remove ${v.toUpperCase()} from dashboard? This won't affect the actual DNS server.`))try{const k=await(await secureFetch("/api/v1/config")).json();k.dnsServers&&delete k.dnsServers[v];const d=await(await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({dnsServers:k.dnsServers||{}})})).json();if(d.success){delete SITE.dnsServers[v];const u=document.querySelector(`.top [data-app="${v}"]`);u&&u.remove(),showNotification(`${v.toUpperCase()} removed from dashboard`,"success"),b()}else showNotification(d.error||"Failed to remove","error")}catch(a){showNotification("Failed to remove: "+a.message,"error")}}function b(){closeModal("dns-settings-modal"),v=null}document.getElementById("dns-settings-cancel")?.addEventListener("click",b),document.getElementById("dns-settings-save")?.addEventListener("click",m),document.getElementById("dns-settings-delete")?.addEventListener("click",l),document.getElementById("dns-settings-modal")?.addEventListener("click",a=>{a.target.id==="dns-settings-modal"&&b()}),document.querySelector(".top")?.addEventListener("click",a=>{const k=a.target.closest('[id$="-settings"]');if(!k)return;const t=k.id.replace("-settings","");SITE.dnsServers[t]&&(a.stopPropagation(),f(t))}),document.getElementById("refresh")?.addEventListener("click",window.refreshAll)})(),(function(){injectModal("logs-modal",`

DNS Logs

Loading logs...
`);let o=null,i=null,v=!1,f=null,m=null,l=!1,b=null,a=null,k=!1,t=null,d=!1;async function u(w,g=25){try{const C=getDnsServerAddr(w),E=await fetch(`/api/v1/dns/logs?server=${C}&limit=${g}`,{cache:"no-store",headers:{Accept:"application/json","Cache-Control":"no-cache"}});if(E.ok){const S=await E.json();return S.success&&S.logs?{logs:S.logs,count:S.count,server:S.server}:{error:S.error||"Failed to fetch logs"}}else return E.status===401?{error:"DNS auto-auth failed - check credentials in settings"}:{error:`HTTP ${E.status}`}}catch(C){return console.error("DNS logs fetch failed:",C),{error:C.message}}}function h(w){return{NoError:"var(--ok-fg)",NOERROR:"var(--ok-fg)",NxDomain:"var(--muted)",NXDOMAIN:"var(--muted)",Refused:"var(--bad-fg)",REFUSED:"var(--bad-fg)",ServerFailure:"#f39c12",SERVFAIL:"#f39c12"}[w]||"var(--fg)"}function e(w){const g=document.createElement("div");if(g.className="log-entry",g.style.cssText="display: grid; grid-template-columns: 140px 110px 1fr 70px 80px; gap: 8px; padding: 6px 10px; border-bottom: 1px solid var(--border); font-size: 0.8rem; align-items: center;",w.parsed===!1)return g.style.gridTemplateColumns="1fr",g.innerHTML=`${escapeHtml(w.raw)}`,g;const C=h(w.rcode),E=w.rcode==="Refused"||w.rcode==="REFUSED";return g.innerHTML=` ${escapeHtml(w.timestamp)} ${escapeHtml(w.client)} ${escapeHtml(w.domain)} ${escapeHtml(w.type)} ${escapeHtml(w.rcode)} `,g}async function s(){if(k){await T();return}if(l){await B();return}if(v||!o)return;const w=parseInt(document.getElementById("log-lines").value),g=document.getElementById("logs-content");try{const C=await u(o,w);if(C.error){g.innerHTML=`
\u26A0\uFE0F Error
${escapeHtml(C.error)}
`;return}g.innerHTML=`
Time Client Domain Type Status
`,C.logs&&C.logs.length>0?C.logs.forEach(E=>{const S=e(E);g.appendChild(S)}):g.innerHTML+=`
No DNS queries logged yet
`}catch(C){g.innerHTML=`
Failed to fetch logs: ${escapeHtml(C.message)}
`}}function p(w){o=w,v=!1,l=!1;const g=document.getElementById("logs-modal"),C=document.getElementById("logs-title"),E=document.getElementById("logs-pause"),S=document.getElementById("logs-stream");C.textContent=`${w.toUpperCase()} DNS Logs`,E.textContent="\u23F8\uFE0F Pause",E.classList.remove("paused"),S&&(S.style.display="none"),g.classList.add("show"),s(),i=setInterval(s,DC.POLL.LOGS)}function n(){document.getElementById("logs-modal").classList.remove("show"),i&&(clearInterval(i),i=null),y(),o=null,l=!1,f=null,m=null,k=!1,b=null,a=null,v=!1}function c(w){t&&y();const g=document.getElementById("logs-stream"),C=document.getElementById("logs-pause"),E=document.getElementById("logs-content");i&&(clearInterval(i),i=null);try{t=new EventSource(`/api/v1/logs/stream/${w}`),d=!0,g.classList.add("active"),g.textContent="\u{1F534} Live",g.title="Streaming - click to stop",C.style.display="none";const S=document.getElementById("logs-title");S.textContent.includes("\u{1F534}")||(S.innerHTML=S.textContent.replace("\u{1F4CB}","\u{1F4CB} \u{1F534}")),t.onmessage=P=>{try{const N=JSON.parse(P.data);if(N.error){console.error("Stream error:",N.error),y();return}const O=document.createElement("div");O.className="log-entry",O.style.cssText="display: flex; gap: 12px; padding: 6px 10px; border-bottom: 1px solid var(--border); font-size: 0.8rem; align-items: flex-start; font-family: monospace;";const R=(N.stream||"stdout")==="stderr",U=R?"var(--bad-fg)":"var(--fg)",M=`${R?"STDERR":"STDOUT"}`;for(O.innerHTML=`
${M}
${escapeHtml(N.text)}
`,E.appendChild(O),E.scrollTop=E.scrollHeight;E.children.length>500;)E.removeChild(E.firstChild)}catch(N){console.error("Error parsing stream data:",N)}},t.onerror=P=>{console.error("EventSource error:",P),y()}}catch(S){console.error("Failed to start streaming:",S),y()}}function y(){t&&(t.close(),t=null),d=!1;const w=document.getElementById("logs-stream"),g=document.getElementById("logs-pause"),C=document.getElementById("logs-title");w&&(w.classList.remove("active"),w.textContent="\u{1F4E1} Live",w.title="Enable real-time streaming"),g&&(g.style.display=""),C&&(C.textContent=C.textContent.replace(" \u{1F534}","")),l&&f&&!i&&(i=setInterval(B,DC.POLL.LOGS))}async function r(w,g=100){try{const C=`/api/v1/logs/container/${w}?tail=${g}×tamps=true`,E=await fetch(C,{cache:"no-store",headers:{Accept:"application/json","Cache-Control":"no-cache"}});if(E.ok){const S=await E.json();return S.success&&S.logs?{logs:S.logs,count:S.count,containerName:S.containerName,containerId:S.containerId}:{error:S.error||"Failed to fetch container logs"}}else return{error:`HTTP ${E.status}: ${E.statusText}`}}catch(C){return console.error("Container logs fetch failed:",C),{error:C.message}}}function I(w){const g=document.createElement("div");g.className="log-entry",g.style.cssText="display: flex; gap: 12px; padding: 6px 10px; border-bottom: 1px solid var(--border); font-size: 0.8rem; align-items: flex-start; font-family: monospace;";const C=w.stream==="stderr"?"var(--bad-fg)":"var(--fg)",E=w.stream==="stderr"?'STDERR':'STDOUT';return g.innerHTML=`
${E}
${escapeHtml(w.text)}
`,g}async function B(){if(v||!f||!l)return;const w=parseInt(document.getElementById("log-lines").value),g=document.getElementById("logs-content");try{const C=await r(f,w);if(C.error){g.innerHTML=`
\u26A0\uFE0F Error
${escapeHtml(C.error)}
`;return}g.innerHTML=`
Stream Log Output
`,C.logs&&C.logs.length>0?(C.logs.forEach(E=>{const S=I(E);g.appendChild(S)}),g.scrollTop=g.scrollHeight):g.innerHTML+=`
No logs available for this container
`}catch(C){g.innerHTML=`
Failed to fetch logs: ${escapeHtml(C.message)}
`}}function L(w,g){f=w,m=g,l=!0,k=!1,v=!1,y();const C=document.getElementById("logs-modal"),E=document.getElementById("logs-title"),S=document.getElementById("logs-pause"),P=document.getElementById("logs-stream");E.textContent=`\u{1F4CB} ${g} - Container Logs`,S.textContent="\u23F8\uFE0F Pause",S.classList.remove("paused"),P&&(P.style.display=""),C.classList.add("show"),B(),i=setInterval(B,DC.POLL.LOGS)}async function A(w,g=100){try{const C=`/api/v1/logs/file?path=${encodeURIComponent(w)}&tail=${g}`,E=await fetch(C,{cache:"no-store",headers:{Accept:"application/json","Cache-Control":"no-cache"}});if(E.ok){const S=await E.json();return S.success&&S.logs?{logs:S.logs,count:S.count,logPath:S.logPath,totalLines:S.totalLines}:{error:S.error||"Failed to fetch file logs"}}else return{error:(await E.json().catch(()=>({}))).error||`HTTP ${E.status}`}}catch(C){return console.error("File logs fetch failed:",C),{error:C.message}}}function x(w){const g=document.createElement("div");g.className="log-entry",g.style.cssText="display: flex; gap: 12px; padding: 6px 10px; border-bottom: 1px solid var(--border); font-size: 0.8rem; align-items: flex-start; font-family: monospace;";const C=w.text;let E="INFO",S="var(--fg)";C.match(/ERROR|FATAL|CRITICAL/i)?(E="ERROR",S="var(--bad-fg)"):C.match(/WARN|WARNING/i)?(E="WARN",S="#f39c12"):C.match(/DEBUG/i)&&(E="DEBUG",S="var(--muted)");const N=`${E}`;return g.innerHTML=`
${N}
${escapeHtml(C)}
`,g}async function T(){if(v||!b||!k)return;const w=parseInt(document.getElementById("log-lines").value),g=document.getElementById("logs-content");try{const C=await A(b,w);if(C.error){g.innerHTML=`
\u26A0\uFE0F Error
${escapeHtml(C.error)}
`;return}g.innerHTML=`
Log Output (${C.count} of ${C.totalLines} lines)
`,C.logs&&C.logs.length>0?(C.logs.forEach(E=>{const S=x(E);g.appendChild(S)}),g.scrollTop=g.scrollHeight):g.innerHTML+=`
No logs available in this file
`}catch(C){g.innerHTML=`
Failed to fetch logs: ${escapeHtml(C.message)}
`}}function $(w,g){b=w,a=g,k=!0,l=!1,v=!1;const C=document.getElementById("logs-modal"),E=document.getElementById("logs-title"),S=document.getElementById("logs-pause"),P=document.getElementById("logs-stream");E.textContent=`\u{1F4CB} ${g} - Application Logs`,S.textContent="\u23F8\uFE0F Pause",S.classList.remove("paused"),P&&(P.style.display="none"),C.classList.add("show"),T(),i=setInterval(T,DC.POLL.LOGS)}document.querySelector(".top")?.addEventListener("click",w=>{const g=w.target.closest('[id$="-logs"]');if(!g)return;const C=g.id.replace("-logs","");SITE.dnsServers[C]&&p(C)}),document.getElementById("logs-close")?.addEventListener("click",n),document.getElementById("logs-pause")?.addEventListener("click",()=>{v=!v;const w=document.getElementById("logs-pause");v?(w.textContent="\u25B6\uFE0F Resume",w.classList.add("paused")):(w.textContent="\u23F8\uFE0F Pause",w.classList.remove("paused"),s())}),document.getElementById("log-lines")?.addEventListener("change",()=>{v||s()}),document.getElementById("logs-stream")?.addEventListener("click",()=>{!l||!f||(d?y():c(f))}),document.getElementById("logs-modal")?.addEventListener("click",w=>{w.target.id==="logs-modal"&&n()}),document.addEventListener("keydown",w=>{w.key==="Escape"&&document.getElementById("logs-modal")?.classList.contains("show")&&n()}),window.openContainerLogsModal=L,window.openFileLogsModal=$,window.openLogsModal=p})(),(function(){injectModal("service-edit-modal",`

Edit Service

.home
The port Caddy will proxy to (container's exposed port)
Enter a URL or upload an image file (PNG, JPG, SVG)
`),injectModal("delete-service-modal",`

Delete Service

`),injectModal("add-service-modal",`

Add Service

Options
Checking Tailscale...

`)})(),(function(){async function o(l){try{const b=await fetch(`/api/v1/caddy/cas?caddyfilePath=${encodeURIComponent(l)}`);if(!b.ok)throw new Error(`Failed to load CAs: ${b.status}`);const a=await b.json();if(a.status==="success"){const k=document.getElementById("existing-ca-select");return k.innerHTML="",a.data.cas.length===0?k.innerHTML='':(k.innerHTML='',a.data.cas.forEach(t=>{const d=document.createElement("option");typeof t=="object"?(d.value=t.id,d.textContent=t.displayName||t.name):(d.value=t,d.textContent=t),k.appendChild(d)})),a.data.cas}else throw new Error(a.message)}catch(b){console.error("Error loading CAs:",b);const a=document.getElementById("existing-ca-select");return a.innerHTML='',[]}}function i(l){const{subdomain:b,port:a,ip:k,sslType:t,caName:d,existingCa:u,enableAuth:h,enableCors:e,customHeaders:s,upstreamPath:p,healthCheck:n,timeout:c,tailscaleOnly:y}=l;let r=`${buildDomain(b)} { `;switch(y&&(r+=` @blocked not remote_ip 100.64.0.0/10 `,r+=` respond @blocked "Access denied. Tailscale connection required." 403 `),t){case"letsencrypt":break;case"caddy-managed":r+=` tls internal `;break;case"existing-ca":u&&(r+=` tls { ca ${u} } `);break;case"custom-ca":d&&(r+=` tls { ca ${d} } `);break}if(h&&(r+=` basicauth { admin $2a$14$hashed_password_here } `),e&&(r+=` header { `,r+=` Access-Control-Allow-Origin "*" `,r+=` Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" `,r+=` Access-Control-Allow-Headers "Content-Type, Authorization" `,r+=` } `),s)try{const I=JSON.parse(s);r+=` header { `,Object.entries(I).forEach(([B,L])=>{r+=` ${B} "${L}" `}),r+=` } `}catch{console.warn("Invalid JSON in custom headers")}return n&&(r+=` health_uri ${n} `),r+=` reverse_proxy ${k}:${a} { `,p&&p!=="/"&&(r+=` rewrite ${p} `),c&&c!==30&&(r+=` transport http { `,r+=` dial_timeout ${c}s `,r+=` response_header_timeout ${c}s `,r+=` } `),r+=` } `,r+=`} `,r}async function v(l,b,a=DC.DEFAULTS.TTL){const k=window.getToken(getPrimaryDnsId(),"admin");if(!k)throw new Error("DNS admin token not configured. Please set it in the Tokens menu.");const t=buildDomain(l),d=await secureFetch("/api/v1/dns/record",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:t,ip:b,ttl:a,token:k,server:SITE.dnsIp})});if(!d.ok){const h=await d.text();throw new Error(`DNS API Error: ${d.status} - ${h}`)}const u=await d.json();if(!u.success)throw new Error(`DNS Error: ${u.error||"Unknown error"}`);return u}async function f(l){const b={id:l.subdomain,name:l.name,logo:l.logo||`/assets/${l.subdomain}.png`};try{const a=await secureFetch("/api/v1/services",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(b)});if(!a.ok){const k=await a.json();throw new Error(k.error||"Failed to save service")}return await window.loadServices(),window.buildGrid(),b}catch(a){throw console.error("Failed to add service to config:",a),a}}async function m(l){const b=document.getElementById("service-subdomain-input").value.trim(),a=document.getElementById("service-ip-input").value.trim()||"localhost",k=document.getElementById("service-port-input").value.trim()||"80",t=await secureFetch("/api/v1/site",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:buildDomain(b),upstream:`${a}:${k}`,config:l})}),d=await t.json();if(!t.ok||!d.success)throw new Error(d.error||`Caddy API Error: ${t.status}`);return d}window.loadExistingCAs=o,window.generateCaddyConfig=i,window.createDnsRecord=v,window.addServiceToConfig=f,window.addToCaddyfile=m})(),(function(){let o=null;function i(a){o=a;const k=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||"",k.classList.add("show")}function v(){closeModal("service-edit-modal"),o=null}async function f(){if(!o)return;const a=document.getElementById("edit-subdomain").value.trim().toLowerCase(),k=document.getElementById("edit-service-name").value.trim(),t=document.getElementById("edit-port").value.trim(),d=document.getElementById("edit-ip").value.trim()||"localhost",u=document.getElementById("edit-tailscale-only").checked,h=document.getElementById("edit-logo-url").value.trim();if(!a){showNotification("Subdomain is required","warning");return}const e=o.id,s=[];if(a!==e&&s.push("subdomain"),k&&k!==o.name&&s.push("name"),t&&t!==String(o.port)&&s.push("port"),d!==o.ip&&s.push("ip"),u!==(o.tailscaleOnly||!1)&&s.push("tailscale"),h&&h!==o.logo&&s.push("logo"),s.length===0){v();return}const p=document.getElementById("service-edit-save");p.textContent="Saving...",p.disabled=!0;try{const c=await(await secureFetch("/api/v1/services/update",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({oldSubdomain:e,newSubdomain:a,name:k||o.name,port:t||o.port,ip:d,tailscaleOnly:u,logo:h||void 0})})).json();if(!c.success)throw new Error(c.error||"Failed to update service");const y=window.APPS.findIndex(r=>r.id===e);y!==-1&&(window.APPS[y]={...window.APPS[y],id:a,name:k||window.APPS[y].name,port:t||window.APPS[y].port,ip:d,tailscaleOnly:u,logo:h||window.APPS[y].logo}),v(),window.buildGrid(),window.refreshAll()}catch(n){console.error("Error saving service changes:",n),showNotification(`Error saving changes: ${n.message}`,"error")}finally{p.textContent="Save Changes",p.disabled=!1}}document.getElementById("edit-logo-file")?.addEventListener("change",async a=>{const k=a.target.files[0];if(!k)return;if(!k.type.startsWith("image/")){showNotification("Please select an image file","warning");return}const t=new FileReader;t.onload=async d=>{const u=d.target.result;if(document.getElementById("edit-service-logo-preview").src=u,document.getElementById("edit-logo-url").value=u,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:u})})).json();e.success&&e.path&&(document.getElementById("edit-logo-url").value=e.path)}catch{}},t.readAsDataURL(k)}),document.getElementById("service-edit-cancel")?.addEventListener("click",v),document.getElementById("service-edit-save")?.addEventListener("click",f),document.getElementById("service-edit-modal")?.addEventListener("click",a=>{a.target.id==="service-edit-modal"&&v()});function m(a,k,t){return new Promise(d=>{const u=document.getElementById("delete-service-modal"),h=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"),n=document.getElementById("delete-modal-help"),c=document.getElementById("delete-modal-cancel"),y=document.getElementById("delete-modal-remove"),r=document.getElementById("delete-modal-delete");h.textContent=`Delete "${a}"`,k?(e.innerHTML="This service has an associated Docker container.
Choose how to proceed:",s.style.display="block",p.textContent=`Container ID: ${t?.slice(0,12)||"Unknown"}`,n.style.display="block",r.style.display="block"):(e.textContent="Remove this service from the dashboard?",s.style.display="none",n.style.display="none",r.style.display="none");const I=()=>{u.classList.remove("show"),c.removeEventListener("click",B),y.removeEventListener("click",L),r.removeEventListener("click",A),u.removeEventListener("click",x)},B=()=>{I(),d(null)},L=()=>{I(),d(!1)},A=()=>{I(),d(!0)},x=T=>{T.target===u&&(I(),d(null))};c.addEventListener("click",B),y.addEventListener("click",L),r.addEventListener("click",A),u.addEventListener("click",x),u.classList.add("show")})}async function l(a,k,t){const d=document.getElementById(`update-btn-${t}`),u=d?.textContent;if(confirm(`Update ${k} 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{d&&(d.textContent="\u{1F504}",d.disabled=!0,d.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===t);s&&e.newContainerId&&(s.containerId=e.newContainerId),d&&(d.textContent="\u2705",d.title="Updated successfully!",setTimeout(()=>{d.textContent=u,d.disabled=!1,d.title="Update container to latest version"},3e3)),setTimeout(()=>window.refreshAll(),2e3),showNotification(`${k} updated successfully!`,"success")}else throw new Error(e.error||"Update failed")}catch(h){console.error("Update error:",h),d&&(d.textContent="\u274C",d.title="Update failed",setTimeout(()=>{d.textContent=u,d.disabled=!1,d.title="Update container to latest version"},3e3)),showNotification(`Failed to update ${k}: ${h.message}`,"error")}}async function b(a,k){const t=window.APPS.find(r=>r.id===a),d=t?buildDomain(t.id):null,u=t?.containerId,h=await m(k||a,u,t?.containerId);if(h===null)return;let e={dashboard:!1,container:null,dns:null,caddy:null,service:null};if(h&&u)try{const r=new URLSearchParams({containerId:t.containerId,subdomain:t.id,ip:t.ip||"localhost",deleteContainer:"true"}),B=await(await secureFetch(`/api/v1/apps/${encodeURIComponent(t.id)}?${r.toString()}`,{method:"DELETE"})).json();B.success?e={...e,...B.results,dashboard:!1}:console.error("App removal failed:",B.error)}catch(r){console.error("App removal error:",r)}else if(h&&d){try{const r=t?.ip||"localhost",B=await(await secureFetch(`/api/v1/dns/record?domain=${encodeURIComponent(d)}&type=A&ipAddress=${encodeURIComponent(r)}&server=${SITE.dnsIp}`,{method:"DELETE"})).json();e.dns=B.success?"deleted":B.error||"failed"}catch(r){e.dns=r.message}try{const I=await(await secureFetch(`/api/v1/site/${encodeURIComponent(d)}`,{method:"DELETE"})).json();e.caddy=I.success||I.error&&I.error.includes("not found")?"removed":I.error||"failed"}catch(r){e.caddy=r.message}}const s=window.APPS.findIndex(r=>r.id===a);s>-1&&(window.APPS.splice(s,1),e.dashboard=!0);try{const r=safeGetJSON("custom-apps",[]),I=r.findIndex(B=>B.id===a);I>-1&&(r.splice(I,1),safeSet("custom-apps",JSON.stringify(r)))}catch{}try{const I=await(await secureFetch(`/api/v1/services/${encodeURIComponent(a)}`,{method:"DELETE"})).json();e.service=I.success?"removed":I.error||"failed"}catch(r){e.service=r.message}window.buildGrid(),window.refreshAll();let p=!1,n=[];e.dashboard||(p=!0,n.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"],y=r=>!r||c.some(I=>r.toLowerCase().includes(I.toLowerCase()));e.container&&!y(e.container)&&(p=!0,n.push(`\u26A0 Container: ${e.container}`)),e.dns&&!y(e.dns)&&(p=!0,n.push(`\u26A0 DNS Record: ${e.dns}`)),e.caddy&&!y(e.caddy)&&(p=!0,n.push(`\u26A0 Caddy Config: ${e.caddy}`)),e.service&&!y(e.service)&&(p=!0,n.push(`\u26A0 Service File: ${e.service}`)),p&&showNotification(`Error deleting "${k||a}": ${n.join(", ")}`,"error",6e3)}window.openServiceEditModal=i,window.showDeleteModal=m,window.updateContainer=l,window.deleteService=b})(),(function(){function o(e){return e.toLowerCase().replace(/\s+/g,"-").replace(/[^a-z0-9-]/g,"").replace(/-+/g,"-").replace(/^-|-$/g,"")}function i(){return SITE.defaults?.sslType||(SITE.configurationType==="public"?"letsencrypt":"caddy-managed")}function v(){const e=document.getElementById("service-subdomain-input").value||"subdomain",s=document.getElementById("service-ip-input").value||f.lan||"localhost",p=document.getElementById("service-port-input").value||DC.DEFAULTS.SERVICE_PORT,n=document.getElementById("ssl-type-select").value,c=document.getElementById("ca-name-input").value||"sami-ca",y=document.getElementById("existing-ca-select").value,r=document.getElementById("enable-auth").checked,I=document.getElementById("enable-cors").checked,B=document.getElementById("custom-headers-input").value,L=document.getElementById("upstream-path-input").value||"/",A=document.getElementById("health-check-input").value,x=document.getElementById("timeout-input").value||30,T=document.getElementById("dns-preview");T&&(T.textContent=`${buildDomain(e)} \u2192 ${s}`);const $=document.getElementById("url-preview");$&&($.textContent=buildServiceUrl(e));const w={subdomain:e,port:p,ip:s,sslType:n,caName:c,existingCa:y,enableAuth:r,enableCors:I,customHeaders:B,upstreamPath:L,healthCheck:A,timeout:x},g=window.generateCaddyConfig(w),C=document.getElementById("caddy-config-preview");C&&(C.value=g)}const f={localhost:"127.0.0.1",lan:"",tailscale:""};async function m(){try{const n=await fetch("/api/v1/network/ips",{signal:AbortSignal.timeout(2e3)});if(n.ok){const c=await n.json();c.lan&&(f.lan=c.lan),c.tailscale&&(f.tailscale=c.tailscale)}}catch{}const e=document.getElementById("quick-ip-lan"),s=document.getElementById("quick-ip-tailscale");e&&(f.lan?(e.dataset.ip=f.lan,e.textContent=`LAN (${f.lan})`,e.title=`LAN IP: ${f.lan}`):e.style.display="none"),s&&(f.tailscale?(s.dataset.ip=f.tailscale,s.textContent=`Tailscale (${f.tailscale})`,s.title=`Tailscale IP: ${f.tailscale}`):s.style.display="none");const p=document.getElementById("service-ip-input");p&&!p.value&&f.lan&&(p.value=f.lan)}function l(){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"),v())})}),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 b(){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=i()),await m();const n=document.getElementById("caddyfile-path-input").value||DC.DEFAULTS.CADDYFILE;await window.loadExistingCAs(n);const c=document.getElementById("manual-tailscale-status"),y=document.getElementById("manual-tailscale-only");try{const I=await(await fetch("/api/v1/tailscale/status")).json();I.success&&I.installed&&I.connected?(c.innerHTML=` \u2713 Connected ${I.self?.hostname} (${I.self?.ip}) `,y.disabled=!1):I.installed?(c.innerHTML='\u26A0 Not connected',y.disabled=!0):(c.innerHTML='Not available',y.disabled=!0)}catch{c.innerHTML='Could not check',y.disabled=!0}y.checked=!1,v()}function a(){const e=document.getElementById("service-type-local"),s=document.getElementById("service-type-external"),p=document.getElementById("local-service-config"),n=document.getElementById("external-service-config"),c=document.getElementById("tab-local"),y=document.getElementById("tab-external");function r(){e.checked?(p.style.display="grid",n.style.display="none",c&&(c.style.background="var(--accent)",c.style.color="var(--bg)"),y&&(y.style.background="transparent",y.style.color="var(--muted)")):(p.style.display="none",n.style.display="block",y&&(y.style.background="var(--accent)",y.style.color="var(--bg)"),c&&(c.style.background="transparent",c.style.color="var(--muted)"))}e?.addEventListener("change",r),s?.addEventListener("change",r)}function k(){const e=document.getElementById("service-name-input"),s=document.getElementById("service-subdomain-input"),p=document.getElementById("subdomain-preview");let n=!1;e?.addEventListener("input",()=>{const L=o(e.value);!n&&s&&(s.value=L),p&&(p.textContent=L?`\u2192 ${buildDomain(L)}`:""),v()}),s?.addEventListener("input",()=>{n=s.value!==o(e?.value||"");const L=s.value.trim()||o(e?.value||"");p&&(p.textContent=L?`\u2192 ${buildDomain(L)}`:""),v()});const c=document.getElementById("external-service-name"),y=document.getElementById("external-service-subdomain"),r=document.getElementById("external-subdomain-preview"),I=document.getElementById("external-domain-preview");let B=!1;c?.addEventListener("input",()=>{const L=o(c.value);!B&&y&&(y.value=L);const A=y?.value||L;r&&(r.textContent=A?`\u2192 ${buildDomain(A)}`:""),I&&(I.textContent=A?buildDomain(A):"")}),y?.addEventListener("input",()=>{B=y.value!==o(c?.value||"");const L=y.value.trim()||o(c?.value||"");r&&(r.textContent=L?`\u2192 ${buildDomain(L)}`:""),I&&(I.textContent=L?buildDomain(L):"")})}async function t(){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(),n=document.getElementById("external-service-logo").value.trim(),c=document.getElementById("external-service-icon").value.trim(),y=document.getElementById("external-create-dns").checked,r=document.getElementById("external-create-caddy").checked,I=document.getElementById("external-proxy-ip").value.trim()||SITE.dnsIp||"localhost",B=document.getElementById("external-preserve-host").checked,L=document.getElementById("external-follow-redirects").checked;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 A=buildDomain(p);try{const x={dns:null,caddy:null,dashboard:!1};if(y)if(window.getToken(getPrimaryDnsId(),"admin"))try{const S=await(await secureFetch("/api/v1/dns/record",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:A,ip:I,ttl:DC.DEFAULTS.TTL,server:SITE.dnsIp})})).json();x.dns=S.success?"created":S.error||"failed"}catch(E){x.dns=E.message}else x.dns="no admin token (configure in \u{1F511} Tokens)";if(r)try{const C={subdomain:p,externalUrl:s,preserveHost:B,followRedirects:L,sslType:"caddy-managed",caddyfilePath:DC.DEFAULTS.CADDYFILE,reloadCaddy:!0},S=await(await secureFetch("/api/v1/site/external",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(C)})).json();x.caddy=S.success?"created":S.error||"failed"}catch(C){x.caddy=C.message}const T={id:p,name:e,url:`https://${A}`,externalUrl:s,logo:n||c||"\u{1F310}",isExternal:!0,isCustom:!0};window.APPS.push(T),x.dashboard=!0;const $=["plex","router","chat","sync","torrent","radarr","sonarr","prowlarr","portainer","requests","jellyfin","emby"],w=window.APPS.filter(C=>!$.includes(C.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(C){console.warn("Failed to save to services.json:",C)}window.buildGrid(),window.refreshAll(),d();const g=[`External service "${e}" added!`];y&&g.push(`DNS: ${x.dns==="created"?"\u2713":"\u26A0 "+x.dns}`),r&&g.push(`Caddy: ${x.caddy==="created"?"\u2713":"\u26A0 "+x.caddy}`),g.push(`Access at: https://${A}`),showNotification(g.join(" | "),"success",6e3)}catch(x){console.error("Failed to create external service:",x),showNotification(`Failed to create external service: ${x.message}`,"error")}}function d(){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=f.lan||"",document.getElementById("service-logo-input").value="",document.getElementById("dns-ttl-input").value=DC.DEFAULTS.TTL,document.getElementById("ssl-type-select").value=i(),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 n=document.getElementById("external-service-subdomain");n&&(n.value="");const c=document.getElementById("external-service-url");c&&(c.value="");const y=document.getElementById("external-service-logo");y&&(y.value="");const r=document.getElementById("external-service-icon");r&&(r.value="");const I=document.getElementById("local-advanced-options");I&&I.removeAttribute("open");const B=document.getElementById("external-advanced-options");B&&B.removeAttribute("open");const L=document.getElementById("service-type-local");L&&(L.checked=!0);const A=document.getElementById("local-service-config"),x=document.getElementById("external-service-config");A&&(A.style.display="grid"),x&&(x.style.display="none");const T=document.getElementById("tab-local"),$=document.getElementById("tab-external");T&&(T.style.background="var(--accent)",T.style.color="var(--bg)"),$&&($.style.background="transparent",$.style.color="var(--muted)")}async function u(){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(),n=document.getElementById("service-ip-input").value.trim(),c=document.getElementById("service-logo-input").value.trim(),y=document.getElementById("create-dns-record").checked,r=parseInt(document.getElementById("dns-ttl-input").value)||DC.DEFAULTS.TTL,I=document.getElementById("manual-tailscale-only")?.checked||!1,B=document.getElementById("ssl-type-select")?.value||"caddy-managed",L=document.getElementById("ca-name-input")?.value||"",A=document.getElementById("existing-ca-select")?.value||"",x=document.getElementById("enable-auth")?.checked||!1,T=document.getElementById("enable-cors")?.checked||!1,$=document.getElementById("custom-headers-input")?.value||"",w=document.getElementById("upstream-path-input")?.value||"/",g=document.getElementById("health-check-input")?.value||"",C=document.getElementById("timeout-input")?.value||30,E=window.getToken(getPrimaryDnsId(),"admin");if(!e||!p||!n){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(y&&!E){showNotification("DNS Admin token required. Configure it in the Tokens menu first.","warning");return}const S={dns:null,caddy:null,dashboard:!1};try{if(y)try{await window.createDnsRecord(s,n,r),S.dns="created"}catch(D){throw console.error("DNS creation failed:",D),S.dns=D.message,new Error(`DNS creation failed: ${D.message}`)}else S.dns="skipped";const P=window.generateCaddyConfig({subdomain:s,port:p,ip:n,sslType:B,caName:L,existingCa:A,enableAuth:x,enableCors:T,customHeaders:$,upstreamPath:w,healthCheck:g,timeout:C,tailscaleOnly:I});try{const R=await(await secureFetch("/api/v1/site",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:buildDomain(s),upstream:`${n}:${p}`,config:P})})).json();if(R.success)S.caddy="added & reloaded";else throw console.error("Caddy configuration failed:",R.error),S.caddy=R.error||"failed",new Error(`Caddy configuration failed: ${R.error}`)}catch(D){throw console.error("Caddy API error:",D),S.caddy=D.message,new Error(`Caddy API error: ${D.message}`)}const N={name:e,subdomain:s,port:p,ip:n,logo:c||`/assets/${s}.png`,tailscaleOnly:I||!1};await window.addServiceToConfig(N),S.dashboard=!0;const O=[`DNS: ${S.dns==="created"?"\u2713":S.dns==="skipped"?"\u25CB":"\u2717"}`,`Caddy: ${S.caddy==="added & reloaded"?"\u2713":"\u2717"}`,`Dashboard: ${S.dashboard?"\u2713":"\u2717"}`];showNotification(`Service "${e}" created! ${O.join(" | ")} \u2014 ${buildServiceUrl(s)}${I?" (Tailscale)":""}`,"success",6e3),d(),window.buildGrid(),window.refreshAll()}catch(P){console.error("Error creating service:",P),showNotification(`Error creating "${e}": ${P.message}`,"error",6e3)}}document.getElementById("add-service")?.addEventListener("click",b),document.getElementById("add-service-cancel")?.addEventListener("click",d),document.getElementById("add-service-create")?.addEventListener("click",()=>{document.querySelector('input[name="service-type"]:checked')?.value==="external"?t():u()}),a(),k(),l(),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"),v()}),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",v),s.addEventListener("change",v))});function h(){const e=safeGet("custom-services");if(e)try{JSON.parse(e).forEach(p=>{window.APPS.find(n=>n.id===p.id)||window.APPS.push(p)})}catch(s){console.warn("Failed to load custom services:",s)}}h(),window.openAddServiceModal=b,window.closeAddServiceModal=d})(),(function(){let o=null,i=1e3;const v=3e4;function f(){if(o)try{o.close()}catch{}o=new EventSource("/api/v1/events/stream"),o.addEventListener("connected",()=>{i=1e3,console.log("[SSE] Connected to event stream")}),o.addEventListener("status-change",m=>{try{const l=JSON.parse(m.data);if(l.serviceId&&typeof window.setBadge=="function"){const b=l.status==="up"||l.status==="healthy";window.setBadge(l.serviceId,b,l.responseTime||null)}}catch{}}),o.addEventListener("resource-alert",m=>{try{const l=JSON.parse(m.data),b=`${l.containerName||l.containerId}: ${l.metric} at ${l.value}% (threshold: ${l.threshold}%)`;typeof showNotification=="function"&&showNotification(b,"warning")}catch{}}),o.addEventListener("auto-restart",m=>{try{const l=JSON.parse(m.data);typeof showNotification=="function"&&showNotification(`Container "${l.containerName}" was auto-restarted`,"info")}catch{}}),o.addEventListener("update-available",m=>{try{const l=JSON.parse(m.data),b=document.getElementById("updates-btn");if(b&&!b.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;",b.appendChild(a)}typeof showNotification=="function"&&showNotification(`Update available for ${l.containerName||l.containerId}`,"info")}catch{}}),o.addEventListener("update-complete",m=>{try{const l=JSON.parse(m.data);typeof showNotification=="function"&&showNotification(`Update completed: ${l.containerName||l.containerId}`,"success"),typeof window.refreshAll=="function"&&window.refreshAll()}catch{}}),o.addEventListener("update-failed",m=>{try{const l=JSON.parse(m.data);typeof showNotification=="function"&&showNotification(`Update failed: ${l.containerName||l.containerId} \u2014 ${l.error||"unknown error"}`,"error")}catch{}}),o.addEventListener("incident",m=>{try{const l=JSON.parse(m.data);typeof showNotification=="function"&&(l.type==="created"?showNotification(`Incident: ${l.message||l.serviceId}`,"error"):l.type==="resolved"&&showNotification(`Resolved: ${l.serviceId||"incident"}`,"success"))}catch{}}),o.onerror=()=>{o.close(),console.warn(`[SSE] Disconnected, reconnecting in ${i/1e3}s...`),setTimeout(f,i),i=Math.min(i*2,v)}}f(),window._sseReconnect=f})();