diff --git a/dashcaddy-api/routes/services.js b/dashcaddy-api/routes/services.js index fcb9569..0976c47 100644 --- a/dashcaddy-api/routes/services.js +++ b/dashcaddy-api/routes/services.js @@ -422,7 +422,7 @@ module.exports = function(ctx) { return ctx.errorResponse(res, 400, 'Invalid port number (must be 1-65535)'); } - if (ip && !validatorLib.isIP(ip)) { + if (ip && ip !== 'localhost' && !validatorLib.isIP(ip)) { return ctx.errorResponse(res, 400, '[DC-210] Invalid IP address'); } @@ -473,7 +473,7 @@ module.exports = function(ctx) { if (await exists(ctx.SERVICES_FILE)) { await ctx.servicesStateManager.update(services => { - const serviceIndex = services.findIndex(s => s.id === oldSubdomain); + const serviceIndex = services.findIndex(s => s.id === oldSubdomain || s.url?.includes(oldSubdomain)); if (serviceIndex !== -1) { const existing = services[serviceIndex]; const finalPort = port || existing.port; @@ -481,7 +481,7 @@ module.exports = function(ctx) { services[serviceIndex] = { ...existing, - id: newSubdomain, + // Keep the original ID — don't change it to the subdomain port: finalPort, ip: finalIp, tailscaleOnly: tailscaleOnly || false, diff --git a/status/dist/core.js b/status/dist/core.js index f0a0a7f..c9920cb 100644 --- a/status/dist/core.js +++ b/status/dist/core.js @@ -1,35 +1,35 @@ -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 p=await fetch("/api/v1/config");if(p.ok){const r=await p.json();if(r.tld&&(SITE.tld=r.tld.startsWith(".")?r.tld:"."+r.tld),r.dns&&(SITE.dnsIp=r.dns.ip||"",SITE.dnsPort=r.dns.port||DC.DEFAULTS.DNS_PORT),r.dnsServers&&typeof r.dnsServers=="object")for(const[f,t]of Object.entries(r.dnsServers))f!=="__proto__"&&f!=="constructor"&&f!=="prototype"&&(SITE.dnsServers[f]=t);r.configurationType&&(SITE.configurationType=r.configurationType),r.domain&&(SITE.domain=r.domain),r.defaults&&(SITE.defaults=r.defaults),r.routingMode&&(SITE.routingMode=r.routingMode),SITE.onboardingCompleted=r.onboardingCompleted===!0,localStorage.setItem("dashcaddy_site_config",JSON.stringify({tld:SITE.tld,configurationType:SITE.configurationType,domain:SITE.domain,routingMode:SITE.routingMode})),renderDnsCards();const E=document.getElementById("manage-tokens");E&&(E.style.display=Object.keys(SITE.dnsServers).length?"":"none")}}catch{}document.querySelectorAll("[data-tld]").forEach(p=>p.textContent=SITE.tld);const i=document.getElementById("edit-tld-suffix");i&&(i.textContent=SITE.tld);const g=document.getElementById("external-proxy-ip");g&&SITE.dnsIp&&(g.value=SITE.dnsIp,g.placeholder=SITE.dnsIp)})();function buildDomain(n){return n+SITE.tld}function buildServiceUrl(n){return SITE.routingMode==="subdirectory"&&SITE.domain?"https://"+SITE.domain+"/"+n:SITE.configurationType==="public"&&SITE.domain?"https://"+n+"."+SITE.domain:"https://"+buildDomain(n)}function getDnsServerAddr(n){const i=SITE.dnsServers[n];return i?`${i.ip}:${i.port}`:buildDomain(n)}function getPrimaryDnsId(){if(!SITE.dnsIp)return null;for(const[n,i]of Object.entries(SITE.dnsServers))if(i.ip===SITE.dnsIp)return n;return null}function renderDnsCards(){const n=document.querySelector(".top");if(!n)return;const i=Object.keys(SITE.dnsServers);if(!i.length)return;const g='',p=n.firstElementChild;i.forEach(r=>{const E=escapeHtml(r),f=escapeHtml((SITE.dnsServers[r].name||r).toUpperCase()),t=document.createElement("div");t.className="card",t.setAttribute("data-app",r),t.setAttribute("data-status","off"),t.innerHTML=`
${g}
${f}OFF
--
--
`,n.insertBefore(t,p)})}window.renderDnsCards=renderDnsCards;let csrfToken=null;async function getCSRFToken(){if(csrfToken)return csrfToken;try{const n=await fetch("/api/v1/csrf-token");if(!n.ok)throw new Error("Failed to fetch CSRF token");return csrfToken=(await n.json()).token,csrfToken}catch(n){throw console.error("Failed to get CSRF token:",n),n}}async function secureFetch(n,i={}){const g=(i.method||"GET").toUpperCase();if(!["GET","HEAD","OPTIONS"].includes(g))try{const p=await getCSRFToken();i.headers={...i.headers,"X-CSRF-Token":p}}catch(p){console.error("Failed to add CSRF token to request:",p)}return i.signal||(i={...i,signal:AbortSignal.timeout(15e3)}),fetch(n,i)}async function postJSON(n,i){const g=await secureFetch(n,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)}),p=await g.json();if(!g.ok||p.success===!1)throw new Error(p.error||`Request failed (${g.status})`);return p}async function getJSON(n){const i=await secureFetch(n);if(!i.ok){let g=`Request failed (${i.status})`;try{g=(await i.json()).error||g}catch{}throw new Error(g)}return i.json()}async function deleteAPI(n){const i=await secureFetch(n,{method:"DELETE"}),g=await i.json();if(!i.ok||g.success===!1)throw new Error(g.error||`Delete failed (${i.status})`);return g}async function withButton(n,i,g,p={}){const r=n.innerHTML,{successText:E="\u2705",resetDelay:f=DC.DELAYS.BTN_RESET}=p;n.disabled=!0,n.innerHTML=i;try{const t=await g();return n.innerHTML=E,setTimeout(()=>{n.innerHTML=r,n.disabled=!1},f),t}catch(t){throw n.innerHTML=r,n.disabled=!1,t}}function openModal(n){document.getElementById(n)?.classList.add("show")}function closeModal(n){document.getElementById(n)?.classList.remove("show")}function wireModal(n,...i){n&&(n.addEventListener("click",g=>{g.target===n&&n.classList.remove("show")}),i.forEach(g=>g?.addEventListener("click",()=>n.classList.remove("show"))))}function showNotification(n,i="info",g=3e3){const p=document.querySelector(".deploy-notification");p&&p.remove();const r={info:{bg:"#2196F3",fg:"#fff"},success:{bg:"var(--ok-bg)",fg:"var(--ok-fg)"},error:{bg:"#f44336",fg:"#fff"},warning:{bg:"#ff9800",fg:"#fff"}},E=r[i]||r.info,f=document.createElement("div");f.className="deploy-notification",f.textContent=n,f.style.cssText=` +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 p=await fetch("/api/v1/config");if(p.ok){const r=await p.json();if(r.tld&&(SITE.tld=r.tld.startsWith(".")?r.tld:"."+r.tld),r.dns&&(SITE.dnsIp=r.dns.ip||"",SITE.dnsPort=r.dns.port||DC.DEFAULTS.DNS_PORT),r.dnsServers&&typeof r.dnsServers=="object")for(const[f,t]of Object.entries(r.dnsServers))f!=="__proto__"&&f!=="constructor"&&f!=="prototype"&&(SITE.dnsServers[f]=t);r.configurationType&&(SITE.configurationType=r.configurationType),r.domain&&(SITE.domain=r.domain),r.defaults&&(SITE.defaults=r.defaults),r.routingMode&&(SITE.routingMode=r.routingMode),SITE.onboardingCompleted=r.onboardingCompleted===!0,localStorage.setItem("dashcaddy_site_config",JSON.stringify({tld:SITE.tld,configurationType:SITE.configurationType,domain:SITE.domain,routingMode:SITE.routingMode})),renderDnsCards();const x=document.getElementById("manage-tokens");x&&(x.style.display=Object.keys(SITE.dnsServers).length?"":"none")}}catch{}document.querySelectorAll("[data-tld]").forEach(p=>p.textContent=SITE.tld);const i=document.getElementById("edit-tld-suffix");i&&(i.textContent=SITE.tld);const g=document.getElementById("external-proxy-ip");g&&SITE.dnsIp&&(g.value=SITE.dnsIp,g.placeholder=SITE.dnsIp)})();function buildDomain(n){return n+SITE.tld}function buildServiceUrl(n){return SITE.routingMode==="subdirectory"&&SITE.domain?"https://"+SITE.domain+"/"+n:SITE.configurationType==="public"&&SITE.domain?"https://"+n+"."+SITE.domain:"https://"+buildDomain(n)}function getDnsServerAddr(n){const i=SITE.dnsServers[n];return i?`${i.ip}:${i.port}`:buildDomain(n)}function getPrimaryDnsId(){if(!SITE.dnsIp)return null;for(const[n,i]of Object.entries(SITE.dnsServers))if(i.ip===SITE.dnsIp)return n;return null}function renderDnsCards(){const n=document.querySelector(".top");if(!n)return;const i=Object.keys(SITE.dnsServers);if(!i.length)return;const g='',p=n.firstElementChild;i.forEach(r=>{const x=escapeHtml(r),f=escapeHtml((SITE.dnsServers[r].name||r).toUpperCase()),t=document.createElement("div");t.className="card",t.setAttribute("data-app",r),t.setAttribute("data-status","off"),t.innerHTML=`
${g}
${f}OFF
--
--
`,n.insertBefore(t,p)})}window.renderDnsCards=renderDnsCards;let csrfToken=null;async function getCSRFToken(){if(csrfToken)return csrfToken;try{const n=await fetch("/api/v1/csrf-token");if(!n.ok)throw new Error("Failed to fetch CSRF token");return csrfToken=(await n.json()).token,csrfToken}catch(n){throw console.error("Failed to get CSRF token:",n),n}}async function secureFetch(n,i={}){const g=(i.method||"GET").toUpperCase(),p=!["GET","HEAD","OPTIONS"].includes(g);if(p)try{const x=await getCSRFToken();i.headers={...i.headers,"X-CSRF-Token":x}}catch(x){console.error("Failed to add CSRF token to request:",x)}i.signal||(i={...i,signal:AbortSignal.timeout(15e3)});const r=await fetch(n,i);if(p&&r.status===403)try{const x=await r.clone().json();if(x.error&&(x.error.includes("DC-100")||x.error.includes("DC-101"))){csrfToken=null;const f=await getCSRFToken();return i.headers={...i.headers,"X-CSRF-Token":f},i.signal=AbortSignal.timeout(15e3),fetch(n,i)}}catch{}return r}async function postJSON(n,i){const g=await secureFetch(n,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)}),p=await g.json();if(!g.ok||p.success===!1)throw new Error(p.error||`Request failed (${g.status})`);return p}async function getJSON(n){const i=await secureFetch(n);if(!i.ok){let g=`Request failed (${i.status})`;try{g=(await i.json()).error||g}catch{}throw new Error(g)}return i.json()}async function deleteAPI(n){const i=await secureFetch(n,{method:"DELETE"}),g=await i.json();if(!i.ok||g.success===!1)throw new Error(g.error||`Delete failed (${i.status})`);return g}async function withButton(n,i,g,p={}){const r=n.innerHTML,{successText:x="\u2705",resetDelay:f=DC.DELAYS.BTN_RESET}=p;n.disabled=!0,n.innerHTML=i;try{const t=await g();return n.innerHTML=x,setTimeout(()=>{n.innerHTML=r,n.disabled=!1},f),t}catch(t){throw n.innerHTML=r,n.disabled=!1,t}}function openModal(n){document.getElementById(n)?.classList.add("show")}function closeModal(n){document.getElementById(n)?.classList.remove("show")}function wireModal(n,...i){n&&(n.addEventListener("click",g=>{g.target===n&&n.classList.remove("show")}),i.forEach(g=>g?.addEventListener("click",()=>n.classList.remove("show"))))}function showNotification(n,i="info",g=3e3){const p=document.querySelector(".deploy-notification");p&&p.remove();const r={info:{bg:"#2196F3",fg:"#fff"},success:{bg:"var(--ok-bg)",fg:"var(--ok-fg)"},error:{bg:"#f44336",fg:"#fff"},warning:{bg:"#ff9800",fg:"#fff"}},x=r[i]||r.info,f=document.createElement("div");f.className="deploy-notification",f.textContent=n,f.style.cssText=` position: fixed; top: 20px; right: 20px; - background: ${E.bg}; color: ${E.fg}; + background: ${x.bg}; color: ${x.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(f),g>0&&setTimeout(()=>f.remove(),g)}function timeAgo(n){const i=Date.now()-new Date(n).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(n,i=null){try{const g=localStorage.getItem(n);return g!==null?g:i}catch{return i}}function safeSet(n,i){try{localStorage.setItem(n,i)}catch{}}function safeRemove(n){try{localStorage.removeItem(n)}catch{}}function safeSessionGet(n,i=null){try{const g=sessionStorage.getItem(n);return g!==null?g:i}catch{return i}}function safeSessionSet(n,i){try{sessionStorage.setItem(n,i)}catch{}}function safeGetJSON(n,i=null){try{const g=localStorage.getItem(n);return g?JSON.parse(g):i}catch{return i}}function escapeHtml(n){return String(n??"").replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function injectModal(n,i){document.getElementById(n)||document.body.insertAdjacentHTML("beforeend",i)}const DC_BUS={_handlers:{},on(n,i){var g;((g=this._handlers)[n]||(g[n]=[])).push(i)},off(n,i){this._handlers[n]=this._handlers[n]?.filter(g=>g!==i)},emit(n,i){this._handlers[n]?.forEach(g=>g(i))}},AppState={_apps:[],getApps(){return this._apps},setApps(n){this._apps=n,window.APPS=n,DC_BUS.emit("apps:changed",n)},findApp(n){return this._apps.find(i=>i.id===n)},addApp(n){this._apps.push(n),window.APPS=this._apps,DC_BUS.emit("apps:changed",this._apps)},removeApp(n){const i=this._apps.findIndex(g=>g.id===n);return i>-1&&(this._apps.splice(i,1),window.APPS=this._apps,DC_BUS.emit("apps:changed",this._apps)),i>-1},updateApp(n,i){const g=this._apps.find(p=>p.id===n);if(g){for(const[p,r]of Object.entries(i))p!=="__proto__"&&p!=="constructor"&&p!=="prototype"&&(g[p]=r);DC_BUS.emit("apps:changed",this._apps)}return g}};(function(){function n(){const p=document.createElement("div");return p.className="skeleton-card",p.innerHTML='
',p}function i(p){const r=document.getElementById("cards");if(!(!r||r.querySelector(".card"))){p=p||6;for(let E=0;E.4,P={};return P.hover=C?y(l,T,.35):y(l,$,.08),P["card-hover"]=y(l,P.hover,.5),P.base=y(T,l,.6),P["fg-muted"]=y(b,T,.35),P.success=S,P.error=x,P.warning=C?"#d68a00":"#f39c12",P}function d(h,T){var $=T.lightBg||T.bg&&k(T.bg)>.4,b=T.accent||T["accent-strong"]||"#888888",l=s(b);return $?":root."+h+` body { + `,document.body.appendChild(f),g>0&&setTimeout(()=>f.remove(),g)}function timeAgo(n){const i=Date.now()-new Date(n).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(n,i=null){try{const g=localStorage.getItem(n);return g!==null?g:i}catch{return i}}function safeSet(n,i){try{localStorage.setItem(n,i)}catch{}}function safeRemove(n){try{localStorage.removeItem(n)}catch{}}function safeSessionGet(n,i=null){try{const g=sessionStorage.getItem(n);return g!==null?g:i}catch{return i}}function safeSessionSet(n,i){try{sessionStorage.setItem(n,i)}catch{}}function safeGetJSON(n,i=null){try{const g=localStorage.getItem(n);return g?JSON.parse(g):i}catch{return i}}function escapeHtml(n){return String(n??"").replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function injectModal(n,i){document.getElementById(n)||document.body.insertAdjacentHTML("beforeend",i)}const DC_BUS={_handlers:{},on(n,i){var g;((g=this._handlers)[n]||(g[n]=[])).push(i)},off(n,i){this._handlers[n]=this._handlers[n]?.filter(g=>g!==i)},emit(n,i){this._handlers[n]?.forEach(g=>g(i))}},AppState={_apps:[],getApps(){return this._apps},setApps(n){this._apps=n,window.APPS=n,DC_BUS.emit("apps:changed",n)},findApp(n){return this._apps.find(i=>i.id===n)},addApp(n){this._apps.push(n),window.APPS=this._apps,DC_BUS.emit("apps:changed",this._apps)},removeApp(n){const i=this._apps.findIndex(g=>g.id===n);return i>-1&&(this._apps.splice(i,1),window.APPS=this._apps,DC_BUS.emit("apps:changed",this._apps)),i>-1},updateApp(n,i){const g=this._apps.find(p=>p.id===n);if(g){for(const[p,r]of Object.entries(i))p!=="__proto__"&&p!=="constructor"&&p!=="prototype"&&(g[p]=r);DC_BUS.emit("apps:changed",this._apps)}return g}};(function(){function n(){const p=document.createElement("div");return p.className="skeleton-card",p.innerHTML='
',p}function i(p){const r=document.getElementById("cards");if(!(!r||r.querySelector(".card"))){p=p||6;for(let x=0;x.4,P={};return P.hover=C?v(c,T,.35):v(c,$,.08),P["card-hover"]=v(c,P.hover,.5),P.base=v(T,c,.6),P["fg-muted"]=v(b,T,.35),P.success=S,P.error=k,P.warning=C?"#d68a00":"#f39c12",P}function d(w,T){var $=T.lightBg||T.bg&&E(T.bg)>.4,b=T.accent||T["accent-strong"]||"#888888",c=s(b);return $?":root."+w+` body { background: - radial-gradient(1200px 800px at 10% -10%, rgba(`+l.r+","+l.g+","+l.b+`, .08), transparent 60%), - radial-gradient(1000px 700px at 110% 10%, rgba(`+l.r+","+l.g+","+l.b+`, .05), transparent 55%), + radial-gradient(1200px 800px at 10% -10%, rgba(`+c.r+","+c.g+","+c.b+`, .08), transparent 60%), + radial-gradient(1000px 700px at 110% 10%, rgba(`+c.r+","+c.g+","+c.b+`, .05), transparent 55%), var(--bg); } -`:":root."+h+` body { +`:":root."+w+` body { background: - radial-gradient(1200px 900px at 8% -12%, rgba(`+l.r+","+l.g+","+l.b+`, .10), transparent 60%), - radial-gradient(1000px 700px at 110% -10%, rgba(`+l.r+","+l.g+","+l.b+`, .07), transparent 55%), + radial-gradient(1200px 900px at 8% -12%, rgba(`+c.r+","+c.g+","+c.b+`, .10), transparent 60%), + radial-gradient(1000px 700px at 110% -10%, rgba(`+c.r+","+c.g+","+c.b+`, .07), transparent 55%), var(--bg); } -`}function v(h,T){var $=T.lightBg||T.bg&&k(T.bg)>.4;return $?":root."+h+` button:hover { +`}function h(w,T){var $=T.lightBg||T.bg&&E(T.bg)>.4;return $?":root."+w+` 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."+h+` button:hover { +`:":root."+w+` button:hover { background: color-mix(in srgb, var(--accent) 18%, transparent); border-color: color-mix(in srgb, var(--accent) 35%, var(--border)); } -`}function o(){return window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}function u(){E.forEach(function(h){document.documentElement.style.removeProperty("--"+h)})}function w(h,T){var $=h.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"");$||($="custom"),p.indexOf($)!==-1&&($=$+"-custom");for(var b=safeGetJSON(i,{}),l=$,S=2;b[$]&&$!==T;)$=l+"-"+S++;return $}function a(h){var T=document.getElementById("user-theme-styles");T&&T.remove(),r.length=p.length,Object.keys(m).forEach(function(x){p.indexOf(x)===-1&&delete m[x]});var $=h||safeGetJSON(i,{}),b=Object.keys($);if(b=b.filter(function(x){return p.indexOf(x)===-1}),!!b.length){var l="";b.forEach(function(x){var C=$[x];r.indexOf(x)===-1&&r.push(x);var P={};E.forEach(function(D){C[D]&&(P[D]=C[D])}),P["card-bg"]=C["card-base"]||C.bg,C.lightBg&&(P.lightBg=!0);var O=e(P);t.forEach(function(D){!P[D]&&O[D]&&(P[D]=O[D])}),m[x]=P,l+=":root."+x+` { -`,E.forEach(function(D){P[D]&&(l+=" --"+D+": "+P[D]+`; -`)}),l+=`} -`,l+=d(x,P),l+=v(x,P)});var S=document.createElement("style");S.id="user-theme-styles",S.textContent=l,document.head.appendChild(S)}}function I(){secureFetch("/api/v1/themes").then(function(h){return h.json()}).then(function(h){if(!(!h.success||!h.themes)){var T=h.themes,$=safeGetJSON(i,{});if(JSON.stringify(T)!==JSON.stringify($)){safeSet(i,JSON.stringify(T)),a(T);var b=safeGet(n);b&&r.indexOf(b)!==-1&&L(b)}}}).catch(function(){})}function B(){var h=safeGetJSON(g);if(h){var T=h.name||"Custom",$=w(T),b={name:T};E.forEach(function(x){h[x]&&(b[x]=h[x])});var l=safeGetJSON(i,{});l[$]=b,safeSet(i,JSON.stringify(l)),safeGet(n)==="custom"&&safeSet(n,$),safeRemove(g);var S={};E.forEach(function(x){b[x]&&(S[x]=b[x])}),fetch("/api/v1/themes/"+$,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:T,colors:S})}).catch(function(){})}}function L(h){document.documentElement.classList.add("theme-transitioning"),r.forEach(function(l){l!=="dark"&&document.documentElement.classList.remove(l)}),u(),h!=="dark"&&document.documentElement.classList.add(h),safeSet(n,h);var T=m[h],$=document.querySelector('meta[name="theme-color"]');$&&T&&$.setAttribute("content",T.bg);var b=T&&T.lightBg;!b&&T&&T.bg&&(b=k(T.bg)>.4),b?document.documentElement.classList.add("light-bg"):document.documentElement.classList.remove("light-bg"),setTimeout(function(){document.documentElement.classList.remove("theme-transitioning")},300)}B(),a();var A=safeGet(n);A==="red"&&(A="black",safeSet(n,"black")),A&&A!=="dark"&&r.indexOf(A)===-1&&(A=null),L(A||o()),I(),window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",function(h){safeGet(n)||L(h.matches?"dark":"light")}),window.THEMES=r,window.BUILTIN_THEMES=p,window.THEME_COLORS=m,window.THEME_PROPS=E,window.BASE_PROPS=f,window.DERIVED_PROPS=t,window.USER_THEMES_KEY=i,window.applyTheme=L,window.clearCustomProperties=u,window.injectUserThemeStyles=a,window.syncThemesFromServer=I,window.slugifyThemeName=w,window.getActiveTheme=function(){return safeGet(n)||o()},window.deriveExtendedColors=e,window.hexToRgb=s,window.rgbToHex=c,window.blendColors=y})(),(function(){function n(){const f=document.querySelector(".totp-card");if(!f)return;const m=getComputedStyle(f).backgroundColor.match(/\d+/g);if(!m)return;const s=(.299*+m[0]+.587*+m[1]+.114*+m[2])/255,c=f.querySelector(".totp-logo-dark"),y=f.querySelector(".totp-logo-light");c&&(c.style.display=s>.5?"none":""),y&&(y.style.display=s>.5?"":"none")}function i(){const f=document.getElementById("totp-overlay");if(f){f.classList.add("show"),setTimeout(n,50);const t=f.querySelector(".totp-digits input");t&&setTimeout(()=>t.focus(),100)}}function g(){const f=document.getElementById("totp-overlay");f&&f.classList.remove("show")}const p=document.getElementById("totp-digits");if(p){const f=p.querySelectorAll("input");f.forEach((t,m)=>{t.addEventListener("input",s=>{const c=s.target.value.replace(/\D/g,"");s.target.value=c.slice(0,1),c&&mk.value).join("");y.length===6&&r(y)}),t.addEventListener("keydown",s=>{s.key==="Backspace"&&!s.target.value&&m>0&&(f[m-1].focus(),f[m-1].value="")}),t.addEventListener("paste",s=>{s.preventDefault();const c=(s.clipboardData.getData("text")||"").replace(/\D/g,"");c.length>=6&&(f.forEach((y,k)=>{y.value=c[k]||""}),f[5].focus(),r(c.slice(0,6)))})})}async function r(f){const t=document.getElementById("totp-error");t.textContent="Verifying...",t.className="totp-error verifying";try{const s=await(await secureFetch("/api/v1/totp/verify",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:f})})).json();if(s.success){t.textContent="",g();const c=safeSessionGet("totp_redirect");if(c){try{sessionStorage.removeItem("totp_redirect")}catch{}window.location.href=c;return}typeof window.initializeDashboard=="function"&&window.initializeDashboard()}else{t.textContent=s.error||"Invalid code",t.className="totp-error";const c=document.querySelectorAll("#totp-digits input");c.forEach(y=>{y.value=""}),c[0]?.focus()}}catch{t.textContent="Connection error",t.className="totp-error"}}const E=new URLSearchParams(window.location.search);if(E.get("auth")==="required"){const f=E.get("return");if(f)try{const t=new URL(f,window.location.origin),m=t.hostname,s=t.origin===window.location.origin,c=SITE.tld.startsWith(".")?SITE.tld:"."+SITE.tld,y=m.endsWith(c)||m===c.substring(1);(s||y)&&safeSessionSet("totp_redirect",f)}catch{}window.history.replaceState({},"",window.location.pathname)}window._showTotpOverlay=i})(),(function(){injectModal("folder-browser-modal",`
+`}function o(){return window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}function u(){x.forEach(function(w){document.documentElement.style.removeProperty("--"+w)})}function y(w,T){var $=w.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"");$||($="custom"),p.indexOf($)!==-1&&($=$+"-custom");for(var b=safeGetJSON(i,{}),c=$,S=2;b[$]&&$!==T;)$=c+"-"+S++;return $}function a(w){var T=document.getElementById("user-theme-styles");T&&T.remove(),r.length=p.length,Object.keys(m).forEach(function(k){p.indexOf(k)===-1&&delete m[k]});var $=w||safeGetJSON(i,{}),b=Object.keys($);if(b=b.filter(function(k){return p.indexOf(k)===-1}),!!b.length){var c="";b.forEach(function(k){var C=$[k];r.indexOf(k)===-1&&r.push(k);var P={};x.forEach(function(D){C[D]&&(P[D]=C[D])}),P["card-bg"]=C["card-base"]||C.bg,C.lightBg&&(P.lightBg=!0);var O=e(P);t.forEach(function(D){!P[D]&&O[D]&&(P[D]=O[D])}),m[k]=P,c+=":root."+k+` { +`,x.forEach(function(D){P[D]&&(c+=" --"+D+": "+P[D]+`; +`)}),c+=`} +`,c+=d(k,P),c+=h(k,P)});var S=document.createElement("style");S.id="user-theme-styles",S.textContent=c,document.head.appendChild(S)}}function I(){secureFetch("/api/v1/themes").then(function(w){return w.json()}).then(function(w){if(!(!w.success||!w.themes)){var T=w.themes,$=safeGetJSON(i,{});if(JSON.stringify(T)!==JSON.stringify($)){safeSet(i,JSON.stringify(T)),a(T);var b=safeGet(n);b&&r.indexOf(b)!==-1&&L(b)}}}).catch(function(){})}function B(){var w=safeGetJSON(g);if(w){var T=w.name||"Custom",$=y(T),b={name:T};x.forEach(function(k){w[k]&&(b[k]=w[k])});var c=safeGetJSON(i,{});c[$]=b,safeSet(i,JSON.stringify(c)),safeGet(n)==="custom"&&safeSet(n,$),safeRemove(g);var S={};x.forEach(function(k){b[k]&&(S[k]=b[k])}),fetch("/api/v1/themes/"+$,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:T,colors:S})}).catch(function(){})}}function L(w){document.documentElement.classList.add("theme-transitioning"),r.forEach(function(c){c!=="dark"&&document.documentElement.classList.remove(c)}),u(),w!=="dark"&&document.documentElement.classList.add(w),safeSet(n,w);var T=m[w],$=document.querySelector('meta[name="theme-color"]');$&&T&&$.setAttribute("content",T.bg);var b=T&&T.lightBg;!b&&T&&T.bg&&(b=E(T.bg)>.4),b?document.documentElement.classList.add("light-bg"):document.documentElement.classList.remove("light-bg"),setTimeout(function(){document.documentElement.classList.remove("theme-transitioning")},300)}B(),a();var A=safeGet(n);A==="red"&&(A="black",safeSet(n,"black")),A&&A!=="dark"&&r.indexOf(A)===-1&&(A=null),L(A||o()),I(),window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",function(w){safeGet(n)||L(w.matches?"dark":"light")}),window.THEMES=r,window.BUILTIN_THEMES=p,window.THEME_COLORS=m,window.THEME_PROPS=x,window.BASE_PROPS=f,window.DERIVED_PROPS=t,window.USER_THEMES_KEY=i,window.applyTheme=L,window.clearCustomProperties=u,window.injectUserThemeStyles=a,window.syncThemesFromServer=I,window.slugifyThemeName=y,window.getActiveTheme=function(){return safeGet(n)||o()},window.deriveExtendedColors=e,window.hexToRgb=s,window.rgbToHex=l,window.blendColors=v})(),(function(){function n(){const f=document.querySelector(".totp-card");if(!f)return;const m=getComputedStyle(f).backgroundColor.match(/\d+/g);if(!m)return;const s=(.299*+m[0]+.587*+m[1]+.114*+m[2])/255,l=f.querySelector(".totp-logo-dark"),v=f.querySelector(".totp-logo-light");l&&(l.style.display=s>.5?"none":""),v&&(v.style.display=s>.5?"":"none")}function i(){const f=document.getElementById("totp-overlay");if(f){f.classList.add("show"),setTimeout(n,50);const t=f.querySelector(".totp-digits input");t&&setTimeout(()=>t.focus(),100)}}function g(){const f=document.getElementById("totp-overlay");f&&f.classList.remove("show")}const p=document.getElementById("totp-digits");if(p){const f=p.querySelectorAll("input");f.forEach((t,m)=>{t.addEventListener("input",s=>{const l=s.target.value.replace(/\D/g,"");s.target.value=l.slice(0,1),l&&mE.value).join("");v.length===6&&r(v)}),t.addEventListener("keydown",s=>{s.key==="Backspace"&&!s.target.value&&m>0&&(f[m-1].focus(),f[m-1].value="")}),t.addEventListener("paste",s=>{s.preventDefault();const l=(s.clipboardData.getData("text")||"").replace(/\D/g,"");l.length>=6&&(f.forEach((v,E)=>{v.value=l[E]||""}),f[5].focus(),r(l.slice(0,6)))})})}async function r(f){const t=document.getElementById("totp-error");t.textContent="Verifying...",t.className="totp-error verifying";try{const s=await(await secureFetch("/api/v1/totp/verify",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:f})})).json();if(s.success){t.textContent="",s.csrfToken&&(csrfToken=s.csrfToken),g();const l=safeSessionGet("totp_redirect");if(l){try{sessionStorage.removeItem("totp_redirect")}catch{}window.location.href=l;return}typeof window.initializeDashboard=="function"&&window.initializeDashboard()}else{t.textContent=s.error||"Invalid code",t.className="totp-error";const l=document.querySelectorAll("#totp-digits input");l.forEach(v=>{v.value=""}),l[0]?.focus()}}catch{t.textContent="Connection error",t.className="totp-error"}}const x=new URLSearchParams(window.location.search);if(x.get("auth")==="required"){const f=x.get("return");if(f)try{const t=new URL(f,window.location.origin),m=t.hostname,s=t.origin===window.location.origin,l=SITE.tld.startsWith(".")?SITE.tld:"."+SITE.tld,v=m.endsWith(l)||m===l.substring(1);(s||v)&&safeSessionSet("totp_redirect",f)}catch{}window.history.replaceState({},"",window.location.pathname)}window._showTotpOverlay=i})(),(function(){injectModal("folder-browser-modal",`

\u{1F4C2} Browse for Media Folders

@@ -107,7 +107,7 @@ const DC={NAME:"DashCaddy",POLL:{DASHBOARD:1e4,LOGS:3e3,STATS:5e3,WEATHER:6e5,HE
-
`);const n=document.getElementById("service-creds-modal");let i=null;const g=["sonarr","radarr","prowlarr","overseerr"];window.openServiceCredsModal=async function(r){i=r;const E=document.getElementById("svc-creds-title"),f=document.getElementById("svc-creds-desc"),t=document.getElementById("svc-creds-seedhost"),m=document.getElementById("svc-creds-apikey"),s=document.getElementById("svc-creds-basic");E.textContent=r.name+" Credentials";const c=!!r.isExternal,y=g.includes(r.id)||g.includes(r.appTemplate);t.style.display=c?"":"none",m.style.display=y?"":"none",s.style.display=c?"none":"",c?(f.textContent="Seedhost credentials auto-login past the HTTP prompt. API key bypasses the app login.",document.getElementById("svc-seedhost-pass").placeholder=`Password for ${r.name}`):y?f.textContent="API key bypasses the app login screen automatically.":f.textContent="Credentials are injected automatically when accessing this service.",await p(r),n.classList.add("show")};async function p(r){const E=document.getElementById("svc-creds-dot"),f=document.getElementById("svc-creds-status"),t=document.getElementById("svc-creds-clear");let m=!1;if(r.isExternal){try{const c=await(await fetch(`/api/v1/seedhost-creds?serviceId=${r.id}`)).json();c.success?(document.getElementById("svc-seedhost-user").value=c.username||"",c.hasCredentials&&(m=!0)):document.getElementById("svc-seedhost-user").value=""}catch{}document.getElementById("svc-seedhost-pass").value=""}try{const c=await(await fetch(`/api/v1/services/${r.id}/credentials`)).json();c.success&&(c.hasApiKey?(document.getElementById("svc-apikey-input").value="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",m=!0):document.getElementById("svc-apikey-input").value="",c.hasBasicAuth&&!r.isExternal?(document.getElementById("svc-basic-user").value=c.username||"",m=!0):document.getElementById("svc-basic-user").value="")}catch{}if(document.getElementById("svc-basic-pass")&&(document.getElementById("svc-basic-pass").value=""),m){E.style.background="var(--ok-fg, #74dfc4)",f.style.color="var(--ok-fg, #74dfc4)",f.textContent="Credentials stored",t.style.display="";const s=document.getElementById(`creds-btn-${r.id}`);s&&s.classList.add("has-creds")}else E.style.background="var(--muted)",f.style.color="var(--muted)",f.textContent="No credentials stored",t.style.display="none"}document.getElementById("svc-creds-save")?.addEventListener("click",async()=>{if(!i)return;const r=document.getElementById("svc-creds-save");r.textContent="Saving...",r.disabled=!0;try{if(i.isExternal){const t=document.getElementById("svc-seedhost-user").value.trim(),m=document.getElementById("svc-seedhost-pass").value;t&&await secureFetch("/api/v1/seedhost-creds",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:t,password:m||void 0,serviceId:i.id})})}const f=document.getElementById("svc-apikey-input").value.trim();if(f&&f!=="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022"&&await secureFetch(`/api/v1/services/${i.id}/credentials`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({apiKey:f})}),!i.isExternal){const t=document.getElementById("svc-basic-user").value.trim(),m=document.getElementById("svc-basic-pass").value;t&&m&&await secureFetch(`/api/v1/services/${i.id}/credentials`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:t,password:m})})}await p(i)}catch(E){console.error("Failed to save credentials:",E)}r.textContent="Save",r.disabled=!1}),document.getElementById("svc-creds-clear")?.addEventListener("click",async()=>{if(i&&confirm(`Remove stored credentials for ${i.name}?`))try{i.isExternal&&await secureFetch(`/api/v1/seedhost-creds?serviceId=${i.id}`,{method:"DELETE"}),await secureFetch(`/api/v1/services/${i.id}/credentials`,{method:"DELETE"});const r=document.getElementById(`creds-btn-${i.id}`);r&&r.classList.remove("has-creds"),await p(i)}catch(r){console.error("Failed to clear credentials:",r)}}),document.getElementById("svc-creds-close")?.addEventListener("click",()=>{n.classList.remove("show"),i=null}),n?.addEventListener("click",r=>{r.target===n&&(n.classList.remove("show"),i=null)}),window.refreshCredsButtons=async function(){try{for(const r of window.APPS||[]){if(!r.isExternal&&!r.appTemplate&&!r.url)continue;let E=!1;if(r.isExternal)try{const m=await(await fetch(`/api/v1/seedhost-creds?serviceId=${r.id}`)).json();m.success&&m.hasCredentials&&(E=!0)}catch{}try{const m=await(await fetch(`/api/v1/services/${r.id}/credentials`)).json();m.success&&(m.hasApiKey||m.hasBasicAuth)&&(E=!0)}catch{}const f=document.getElementById(`creds-btn-${r.id}`);f&&f.classList.toggle("has-creds",E)}}catch{}}})(),(function(){injectModal("totp-settings-modal",`
+
`);const n=document.getElementById("service-creds-modal");let i=null;const g=["sonarr","radarr","prowlarr","overseerr"];window.openServiceCredsModal=async function(r){i=r;const x=document.getElementById("svc-creds-title"),f=document.getElementById("svc-creds-desc"),t=document.getElementById("svc-creds-seedhost"),m=document.getElementById("svc-creds-apikey"),s=document.getElementById("svc-creds-basic");x.textContent=r.name+" Credentials";const l=!!r.isExternal,v=g.includes(r.id)||g.includes(r.appTemplate);t.style.display=l?"":"none",m.style.display=v?"":"none",s.style.display=l?"none":"",l?(f.textContent="Seedhost credentials auto-login past the HTTP prompt. API key bypasses the app login.",document.getElementById("svc-seedhost-pass").placeholder=`Password for ${r.name}`):v?f.textContent="API key bypasses the app login screen automatically.":f.textContent="Credentials are injected automatically when accessing this service.",await p(r),n.classList.add("show")};async function p(r){const x=document.getElementById("svc-creds-dot"),f=document.getElementById("svc-creds-status"),t=document.getElementById("svc-creds-clear");let m=!1;if(r.isExternal){try{const l=await(await fetch(`/api/v1/seedhost-creds?serviceId=${r.id}`)).json();l.success?(document.getElementById("svc-seedhost-user").value=l.username||"",l.hasCredentials&&(m=!0)):document.getElementById("svc-seedhost-user").value=""}catch{}document.getElementById("svc-seedhost-pass").value=""}try{const l=await(await fetch(`/api/v1/services/${r.id}/credentials`)).json();l.success&&(l.hasApiKey?(document.getElementById("svc-apikey-input").value="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",m=!0):document.getElementById("svc-apikey-input").value="",l.hasBasicAuth&&!r.isExternal?(document.getElementById("svc-basic-user").value=l.username||"",m=!0):document.getElementById("svc-basic-user").value="")}catch{}if(document.getElementById("svc-basic-pass")&&(document.getElementById("svc-basic-pass").value=""),m){x.style.background="var(--ok-fg, #74dfc4)",f.style.color="var(--ok-fg, #74dfc4)",f.textContent="Credentials stored",t.style.display="";const s=document.getElementById(`creds-btn-${r.id}`);s&&s.classList.add("has-creds")}else x.style.background="var(--muted)",f.style.color="var(--muted)",f.textContent="No credentials stored",t.style.display="none"}document.getElementById("svc-creds-save")?.addEventListener("click",async()=>{if(!i)return;const r=document.getElementById("svc-creds-save");r.textContent="Saving...",r.disabled=!0;try{if(i.isExternal){const t=document.getElementById("svc-seedhost-user").value.trim(),m=document.getElementById("svc-seedhost-pass").value;t&&await secureFetch("/api/v1/seedhost-creds",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:t,password:m||void 0,serviceId:i.id})})}const f=document.getElementById("svc-apikey-input").value.trim();if(f&&f!=="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022"&&await secureFetch(`/api/v1/services/${i.id}/credentials`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({apiKey:f})}),!i.isExternal){const t=document.getElementById("svc-basic-user").value.trim(),m=document.getElementById("svc-basic-pass").value;t&&m&&await secureFetch(`/api/v1/services/${i.id}/credentials`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:t,password:m})})}await p(i)}catch(x){console.error("Failed to save credentials:",x)}r.textContent="Save",r.disabled=!1}),document.getElementById("svc-creds-clear")?.addEventListener("click",async()=>{if(i&&confirm(`Remove stored credentials for ${i.name}?`))try{i.isExternal&&await secureFetch(`/api/v1/seedhost-creds?serviceId=${i.id}`,{method:"DELETE"}),await secureFetch(`/api/v1/services/${i.id}/credentials`,{method:"DELETE"});const r=document.getElementById(`creds-btn-${i.id}`);r&&r.classList.remove("has-creds"),await p(i)}catch(r){console.error("Failed to clear credentials:",r)}}),document.getElementById("svc-creds-close")?.addEventListener("click",()=>{n.classList.remove("show"),i=null}),n?.addEventListener("click",r=>{r.target===n&&(n.classList.remove("show"),i=null)}),window.refreshCredsButtons=async function(){try{for(const r of window.APPS||[]){if(!r.isExternal&&!r.appTemplate&&!r.url)continue;let x=!1;if(r.isExternal)try{const m=await(await fetch(`/api/v1/seedhost-creds?serviceId=${r.id}`)).json();m.success&&m.hasCredentials&&(x=!0)}catch{}try{const m=await(await fetch(`/api/v1/services/${r.id}/credentials`)).json();m.success&&(m.hasApiKey||m.hasBasicAuth)&&(x=!0)}catch{}const f=document.getElementById(`creds-btn-${r.id}`);f&&f.classList.toggle("has-creds",x)}}catch{}}})(),(function(){injectModal("totp-settings-modal",`

Authentication Settings

@@ -201,7 +201,7 @@ const DC={NAME:"DashCaddy",POLL:{DASHBOARD:1e4,LOGS:3e3,STATS:5e3,WEATHER:6e5,HE
- `);async function n(){try{const r=await(await fetch("/api/v1/totp/config")).json();if(!r.success)return;const{enabled:E,sessionDuration:f,isSetUp:t}=r.config,m=document.getElementById("totp-status-dot"),s=document.getElementById("totp-status-text"),c=document.getElementById("totp-status-banner"),y=document.getElementById("totp-setup-section"),k=document.getElementById("totp-qr-section"),e=document.getElementById("totp-duration-section"),d=document.getElementById("totp-disable-section");E&&t?(m.style.background="var(--ok-fg, #7ef2ff)",c.style.borderColor="var(--ok-fg, #7ef2ff)",c.style.background="color-mix(in srgb, var(--ok-fg) 8%, transparent)",s.textContent="TOTP is active",s.style.color="var(--ok-fg, #7ef2ff)",y.style.display="none",k.style.display="none",e.style.display="block",d.style.display="block",document.getElementById("totp-duration-select").value=f):(m.style.background="var(--muted)",c.style.borderColor="var(--border)",c.style.background="transparent",s.textContent="TOTP is not configured",s.style.color="var(--muted)",y.style.display="block",k.style.display="none",e.style.display="none",d.style.display="none"),g(E&&t,f)}catch(p){console.warn("Failed to load TOTP settings:",p)}}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 g(p,r){const E=document.getElementById("auth-card"),f=document.getElementById("auth-pill"),t=document.getElementById("auth-dot"),m=document.getElementById("auth-status-text");E&&(p?(E.setAttribute("data-status","on"),f.className="badge on",f.textContent="YES",t.className="dot ok at-bl",m.textContent="Session: "+(i[r]||r)):(E.setAttribute("data-status","off"),f.className="badge off",f.textContent="NO",t.className="dot bad at-bl",m.textContent="Not configured"))}document.getElementById("totp-setup-btn")?.addEventListener("click",async()=>{try{const r=await(await secureFetch("/api/v1/totp/setup",{method:"POST"})).json();r.success&&(document.getElementById("totp-qr-image").src=r.qrCode,document.getElementById("totp-manual-key").textContent=r.manualKey,document.getElementById("totp-setup-section").style.display="none",document.getElementById("totp-qr-section").style.display="block",document.getElementById("totp-setup-code").value="",document.getElementById("totp-setup-error").textContent="",document.getElementById("totp-setup-code").focus())}catch(p){console.error("TOTP setup failed:",p)}}),document.getElementById("totp-import-btn")?.addEventListener("click",async()=>{const p=document.getElementById("totp-import-key").value.trim();if(p)try{const E=await(await secureFetch("/api/v1/totp/setup",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({secret:p})})).json();E.success?(document.getElementById("totp-qr-image").src=E.qrCode,document.getElementById("totp-manual-key").textContent=E.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()):(document.getElementById("totp-import-key").style.borderColor="var(--bad-fg)",setTimeout(()=>{document.getElementById("totp-import-key").style.borderColor=""},2e3))}catch(r){console.error("TOTP import failed:",r)}}),document.getElementById("totp-copy-key")?.addEventListener("click",()=>{const p=document.getElementById("totp-manual-key").textContent;navigator.clipboard.writeText(p).then(()=>{const r=document.getElementById("totp-copy-key");r.textContent="\u2705",setTimeout(()=>{r.textContent="\u{1F4CB}"},2e3)})}),document.getElementById("totp-confirm-setup")?.addEventListener("click",async()=>{const p=document.getElementById("totp-setup-code").value,r=document.getElementById("totp-setup-error");if(!/^\d{6}$/.test(p)){r.textContent="Enter a 6-digit code";return}try{const f=await(await secureFetch("/api/v1/totp/verify-setup",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:p})})).json();f.success?(r.textContent="",n()):r.textContent=f.error||"Invalid code"}catch{r.textContent="Connection error"}}),document.getElementById("totp-setup-code")?.addEventListener("keydown",p=>{p.key==="Enter"&&document.getElementById("totp-confirm-setup")?.click()}),document.getElementById("totp-duration-select")?.addEventListener("change",async p=>{try{await secureFetch("/api/v1/totp/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sessionDuration:p.target.value})}),n()}catch(r){console.error("Failed to update session duration:",r)}}),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&&n()}catch(p){console.error("Failed to disable TOTP:",p)}}),document.getElementById("auth-settings-btn")?.addEventListener("click",()=>{n(),openModal("totp-settings-modal")}),document.getElementById("totp-modal-close")?.addEventListener("click",()=>{closeModal("totp-settings-modal")}),document.getElementById("totp-settings-modal")?.addEventListener("click",p=>{p.target.id==="totp-settings-modal"&&closeModal("totp-settings-modal")}),window._updateAuthCard=g,(async()=>{try{const r=await(await fetch("/api/v1/totp/config")).json();if(r.success){const E=r.config.enabled&&r.config.isSetUp;g(E,r.config.sessionDuration)}}catch(p){console.error("[AuthCard] Failed to update:",p)}})()})(),(function(){injectModal("token-management-modal",` + `);async function n(){try{const r=await(await fetch("/api/v1/totp/config")).json();if(!r.success)return;const{enabled:x,sessionDuration:f,isSetUp:t}=r.config,m=document.getElementById("totp-status-dot"),s=document.getElementById("totp-status-text"),l=document.getElementById("totp-status-banner"),v=document.getElementById("totp-setup-section"),E=document.getElementById("totp-qr-section"),e=document.getElementById("totp-duration-section"),d=document.getElementById("totp-disable-section");x&&t?(m.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)",s.textContent="TOTP is active",s.style.color="var(--ok-fg, #7ef2ff)",v.style.display="none",E.style.display="none",e.style.display="block",d.style.display="block",document.getElementById("totp-duration-select").value=f):(m.style.background="var(--muted)",l.style.borderColor="var(--border)",l.style.background="transparent",s.textContent="TOTP is not configured",s.style.color="var(--muted)",v.style.display="block",E.style.display="none",e.style.display="none",d.style.display="none"),g(x&&t,f)}catch(p){console.warn("Failed to load TOTP settings:",p)}}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 g(p,r){const x=document.getElementById("auth-card"),f=document.getElementById("auth-pill"),t=document.getElementById("auth-dot"),m=document.getElementById("auth-status-text");x&&(p?(x.setAttribute("data-status","on"),f.className="badge on",f.textContent="YES",t.className="dot ok at-bl",m.textContent="Session: "+(i[r]||r)):(x.setAttribute("data-status","off"),f.className="badge off",f.textContent="NO",t.className="dot bad at-bl",m.textContent="Not configured"))}document.getElementById("totp-setup-btn")?.addEventListener("click",async()=>{try{const r=await(await secureFetch("/api/v1/totp/setup",{method:"POST"})).json();r.success&&(document.getElementById("totp-qr-image").src=r.qrCode,document.getElementById("totp-manual-key").textContent=r.manualKey,document.getElementById("totp-setup-section").style.display="none",document.getElementById("totp-qr-section").style.display="block",document.getElementById("totp-setup-code").value="",document.getElementById("totp-setup-error").textContent="",document.getElementById("totp-setup-code").focus())}catch(p){console.error("TOTP setup failed:",p)}}),document.getElementById("totp-import-btn")?.addEventListener("click",async()=>{const p=document.getElementById("totp-import-key").value.trim();if(p)try{const x=await(await secureFetch("/api/v1/totp/setup",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({secret:p})})).json();x.success?(document.getElementById("totp-qr-image").src=x.qrCode,document.getElementById("totp-manual-key").textContent=x.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()):(document.getElementById("totp-import-key").style.borderColor="var(--bad-fg)",setTimeout(()=>{document.getElementById("totp-import-key").style.borderColor=""},2e3))}catch(r){console.error("TOTP import failed:",r)}}),document.getElementById("totp-copy-key")?.addEventListener("click",()=>{const p=document.getElementById("totp-manual-key").textContent;navigator.clipboard.writeText(p).then(()=>{const r=document.getElementById("totp-copy-key");r.textContent="\u2705",setTimeout(()=>{r.textContent="\u{1F4CB}"},2e3)})}),document.getElementById("totp-confirm-setup")?.addEventListener("click",async()=>{const p=document.getElementById("totp-setup-code").value,r=document.getElementById("totp-setup-error");if(!/^\d{6}$/.test(p)){r.textContent="Enter a 6-digit code";return}try{const f=await(await secureFetch("/api/v1/totp/verify-setup",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:p})})).json();f.success?(r.textContent="",n()):r.textContent=f.error||"Invalid code"}catch{r.textContent="Connection error"}}),document.getElementById("totp-setup-code")?.addEventListener("keydown",p=>{p.key==="Enter"&&document.getElementById("totp-confirm-setup")?.click()}),document.getElementById("totp-duration-select")?.addEventListener("change",async p=>{try{await secureFetch("/api/v1/totp/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sessionDuration:p.target.value})}),n()}catch(r){console.error("Failed to update session duration:",r)}}),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&&n()}catch(p){console.error("Failed to disable TOTP:",p)}}),document.getElementById("auth-settings-btn")?.addEventListener("click",()=>{n(),openModal("totp-settings-modal")}),document.getElementById("totp-modal-close")?.addEventListener("click",()=>{closeModal("totp-settings-modal")}),document.getElementById("totp-settings-modal")?.addEventListener("click",p=>{p.target.id==="totp-settings-modal"&&closeModal("totp-settings-modal")}),window._updateAuthCard=g,(async()=>{try{const r=await(await fetch("/api/v1/totp/config")).json();if(r.success){const x=r.config.enabled&&r.config.isSetUp;g(x,r.config.sessionDuration)}}catch(p){console.error("[AuthCard] Failed to update:",p)}})()})(),(function(){injectModal("token-management-modal",`

\u{1F511} DNS Credentials

@@ -219,40 +219,40 @@ const DC={NAME:"DashCaddy",POLL:{DASHBOARD:1e4,LOGS:3e3,STATS:5e3,WEATHER:6e5,HE
- `);function n(){return Object.keys(SITE.dnsServers||{})}function i(o){return(SITE.dnsServers||{})[o]?.name||o.toUpperCase()}function g(){const o=document.getElementById("dns-cred-sections");if(!o)return;o.innerHTML="";const u=n();if(u.length===0){o.innerHTML='

No DNS servers configured.

';return}for(const w of u)o.insertAdjacentHTML("beforeend",` + `);function n(){return Object.keys(SITE.dnsServers||{})}function i(o){return(SITE.dnsServers||{})[o]?.name||o.toUpperCase()}function g(){const o=document.getElementById("dns-cred-sections");if(!o)return;o.innerHTML="";const u=n();if(u.length===0){o.innerHTML='

No DNS servers configured.

';return}for(const y of u)o.insertAdjacentHTML("beforeend",`
-

${i(w)}

+

${i(y)}

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

DNS Settings

@@ -279,7 +279,7 @@ Instructions: ${v.instructionsLink}`:"";showNotification(`${t.toUpperCase()} upd
- `);let g=null;function p(t){g=t;const m=SITE.dnsServers[t]||{},s=document.getElementById("dns-settings-modal");document.getElementById("dns-settings-title").textContent=`${(m.name||t).toUpperCase()} Settings`,document.getElementById("dns-edit-ip").value=m.ip||"",document.getElementById("dns-edit-port").value=m.port||DC.DEFAULTS.DNS_PORT,document.getElementById("dns-edit-name").value=m.name||"",s.classList.add("show")}async function r(){if(!g)return;const t=document.getElementById("dns-edit-ip").value.trim(),m=document.getElementById("dns-edit-port").value.trim()||DC.DEFAULTS.DNS_PORT,s=document.getElementById("dns-edit-name").value.trim();if(!t){showNotification("Server IP is required","warning");return}const c={dnsServers:{}};c.dnsServers[g]={ip:t,port:String(m)},s&&(c.dnsServers[g].name=s);try{const k=await(await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(c)})).json();k.success?(SITE.dnsServers[g]=c.dnsServers[g],showNotification(`${g.toUpperCase()} settings saved`,"success"),f(),window.refreshAll()):showNotification(k.error||"Failed to save settings","error")}catch(y){showNotification("Failed to save: "+y.message,"error")}}async function E(){if(g&&confirm(`Remove ${g.toUpperCase()} from dashboard? This won't affect the actual DNS server.`))try{const m=await(await secureFetch("/api/v1/config")).json();m.dnsServers&&delete m.dnsServers[g];const c=await(await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({dnsServers:m.dnsServers||{}})})).json();if(c.success){delete SITE.dnsServers[g];const y=document.querySelector(`.top [data-app="${g}"]`);y&&y.remove(),showNotification(`${g.toUpperCase()} removed from dashboard`,"success"),f()}else showNotification(c.error||"Failed to remove","error")}catch(t){showNotification("Failed to remove: "+t.message,"error")}}function f(){closeModal("dns-settings-modal"),g=null}document.getElementById("dns-settings-cancel")?.addEventListener("click",f),document.getElementById("dns-settings-save")?.addEventListener("click",r),document.getElementById("dns-settings-delete")?.addEventListener("click",E),document.getElementById("dns-settings-modal")?.addEventListener("click",t=>{t.target.id==="dns-settings-modal"&&f()}),document.querySelector(".top")?.addEventListener("click",t=>{const m=t.target.closest('[id$="-settings"]');if(!m)return;const s=m.id.replace("-settings","");SITE.dnsServers[s]&&(t.stopPropagation(),p(s))}),document.getElementById("refresh")?.addEventListener("click",window.refreshAll)})(),(function(){injectModal("logs-modal",` + `);let g=null;function p(t){g=t;const m=SITE.dnsServers[t]||{},s=document.getElementById("dns-settings-modal");document.getElementById("dns-settings-title").textContent=`${(m.name||t).toUpperCase()} Settings`,document.getElementById("dns-edit-ip").value=m.ip||"",document.getElementById("dns-edit-port").value=m.port||DC.DEFAULTS.DNS_PORT,document.getElementById("dns-edit-name").value=m.name||"",s.classList.add("show")}async function r(){if(!g)return;const t=document.getElementById("dns-edit-ip").value.trim(),m=document.getElementById("dns-edit-port").value.trim()||DC.DEFAULTS.DNS_PORT,s=document.getElementById("dns-edit-name").value.trim();if(!t){showNotification("Server IP is required","warning");return}const l={dnsServers:{}};l.dnsServers[g]={ip:t,port:String(m)},s&&(l.dnsServers[g].name=s);try{const E=await(await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(l)})).json();E.success?(SITE.dnsServers[g]=l.dnsServers[g],showNotification(`${g.toUpperCase()} settings saved`,"success"),f(),window.refreshAll()):showNotification(E.error||"Failed to save settings","error")}catch(v){showNotification("Failed to save: "+v.message,"error")}}async function x(){if(g&&confirm(`Remove ${g.toUpperCase()} from dashboard? This won't affect the actual DNS server.`))try{const m=await(await secureFetch("/api/v1/config")).json();m.dnsServers&&delete m.dnsServers[g];const l=await(await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({dnsServers:m.dnsServers||{}})})).json();if(l.success){delete SITE.dnsServers[g];const v=document.querySelector(`.top [data-app="${g}"]`);v&&v.remove(),showNotification(`${g.toUpperCase()} removed from dashboard`,"success"),f()}else showNotification(l.error||"Failed to remove","error")}catch(t){showNotification("Failed to remove: "+t.message,"error")}}function f(){closeModal("dns-settings-modal"),g=null}document.getElementById("dns-settings-cancel")?.addEventListener("click",f),document.getElementById("dns-settings-save")?.addEventListener("click",r),document.getElementById("dns-settings-delete")?.addEventListener("click",x),document.getElementById("dns-settings-modal")?.addEventListener("click",t=>{t.target.id==="dns-settings-modal"&&f()}),document.querySelector(".top")?.addEventListener("click",t=>{const m=t.target.closest('[id$="-settings"]');if(!m)return;const s=m.id.replace("-settings","");SITE.dnsServers[s]&&(t.stopPropagation(),p(s))}),document.getElementById("refresh")?.addEventListener("click",window.refreshAll)})(),(function(){injectModal("logs-modal",`
@@ -303,66 +303,66 @@ Instructions: ${v.instructionsLink}`:"";showNotification(`${t.toUpperCase()} upd
- `);let n=null,i=null,g=!1,p=null,r=null,E=!1,f=null,t=null,m=!1,s=null,c=!1;async function y(b,l=25){try{const S=getDnsServerAddr(b),x=await fetch(`/api/v1/dns/logs?server=${S}&limit=${l}`,{cache:"no-store",headers:{Accept:"application/json","Cache-Control":"no-cache"}});if(x.ok){const C=await x.json();return C.success&&C.logs?{logs:C.logs,count:C.count,server:C.server}:{error:C.error||"Failed to fetch logs"}}else return x.status===401?{error:"DNS auto-auth failed - check credentials in settings"}:{error:`HTTP ${x.status}`}}catch(S){return console.error("DNS logs fetch failed:",S),{error:S.message}}}function k(b){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"}[b]||"var(--fg)"}function e(b){const l=document.createElement("div");if(l.className="log-entry",l.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;",b.parsed===!1)return l.style.gridTemplateColumns="1fr",l.innerHTML=`${escapeHtml(b.raw)}`,l;const S=k(b.rcode),x=b.rcode==="Refused"||b.rcode==="REFUSED";return l.innerHTML=` + `);let n=null,i=null,g=!1,p=null,r=null,x=!1,f=null,t=null,m=!1,s=null,l=!1;async function v(b,c=25){try{const S=getDnsServerAddr(b),k=await fetch(`/api/v1/dns/logs?server=${S}&limit=${c}`,{cache:"no-store",headers:{Accept:"application/json","Cache-Control":"no-cache"}});if(k.ok){const C=await k.json();return C.success&&C.logs?{logs:C.logs,count:C.count,server:C.server}:{error:C.error||"Failed to fetch logs"}}else return k.status===401?{error:"DNS auto-auth failed - check credentials in settings"}:{error:`HTTP ${k.status}`}}catch(S){return console.error("DNS logs fetch failed:",S),{error:S.message}}}function E(b){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"}[b]||"var(--fg)"}function e(b){const c=document.createElement("div");if(c.className="log-entry",c.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;",b.parsed===!1)return c.style.gridTemplateColumns="1fr",c.innerHTML=`${escapeHtml(b.raw)}`,c;const S=E(b.rcode),k=b.rcode==="Refused"||b.rcode==="REFUSED";return c.innerHTML=` ${escapeHtml(b.timestamp)} ${escapeHtml(b.client)} - ${escapeHtml(b.domain)} + ${escapeHtml(b.domain)} ${escapeHtml(b.type)} ${escapeHtml(b.rcode)} - `,l}async function d(){if(m){await T();return}if(E){await B();return}if(g||!n)return;const b=parseInt(document.getElementById("log-lines").value),l=document.getElementById("logs-content");try{const S=await y(n,b);if(S.error){l.innerHTML=` + `,c}async function d(){if(m){await T();return}if(x){await B();return}if(g||!n)return;const b=parseInt(document.getElementById("log-lines").value),c=document.getElementById("logs-content");try{const S=await v(n,b);if(S.error){c.innerHTML=`
\u26A0\uFE0F Error
${escapeHtml(S.error)}
-
`;return}l.innerHTML=` + `;return}c.innerHTML=`
Time Client Domain Type Status -
`,S.logs&&S.logs.length>0?S.logs.forEach(x=>{const C=e(x);l.appendChild(C)}):l.innerHTML+=` + `,S.logs&&S.logs.length>0?S.logs.forEach(k=>{const C=e(k);c.appendChild(C)}):c.innerHTML+=`
No DNS queries logged yet -
`}catch(S){l.innerHTML=` + `}catch(S){c.innerHTML=`
Failed to fetch logs: ${escapeHtml(S.message)} -
`}}function v(b){n=b,g=!1,E=!1;const l=document.getElementById("logs-modal"),S=document.getElementById("logs-title"),x=document.getElementById("logs-pause"),C=document.getElementById("logs-stream");S.textContent=`${b.toUpperCase()} DNS Logs`,x.textContent="\u23F8\uFE0F Pause",x.classList.remove("paused"),C&&(C.style.display="none"),l.classList.add("show"),d(),i=setInterval(d,DC.POLL.LOGS)}function o(){document.getElementById("logs-modal").classList.remove("show"),i&&(clearInterval(i),i=null),w(),n=null,E=!1,p=null,r=null,m=!1,f=null,t=null,g=!1}function u(b){s&&w();const l=document.getElementById("logs-stream"),S=document.getElementById("logs-pause"),x=document.getElementById("logs-content");i&&(clearInterval(i),i=null);try{s=new EventSource(`/api/v1/logs/stream/${b}`),c=!0,l.classList.add("active"),l.textContent="\u{1F534} Live",l.title="Streaming - click to stop",S.style.display="none";const C=document.getElementById("logs-title");C.textContent.includes("\u{1F534}")||(C.innerHTML=C.textContent.replace("\u{1F4CB}","\u{1F4CB} \u{1F534}")),s.onmessage=P=>{try{const O=JSON.parse(P.data);if(O.error){console.error("Stream error:",O.error),w();return}const D=document.createElement("div");D.className="log-entry",D.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=(O.stream||"stdout")==="stderr",U=R?"var(--bad-fg)":"var(--fg)",F=`${R?"STDERR":"STDOUT"}`;for(D.innerHTML=` + `}}function h(b){n=b,g=!1,x=!1;const c=document.getElementById("logs-modal"),S=document.getElementById("logs-title"),k=document.getElementById("logs-pause"),C=document.getElementById("logs-stream");S.textContent=`${b.toUpperCase()} DNS Logs`,k.textContent="\u23F8\uFE0F Pause",k.classList.remove("paused"),C&&(C.style.display="none"),c.classList.add("show"),d(),i=setInterval(d,DC.POLL.LOGS)}function o(){document.getElementById("logs-modal").classList.remove("show"),i&&(clearInterval(i),i=null),y(),n=null,x=!1,p=null,r=null,m=!1,f=null,t=null,g=!1}function u(b){s&&y();const c=document.getElementById("logs-stream"),S=document.getElementById("logs-pause"),k=document.getElementById("logs-content");i&&(clearInterval(i),i=null);try{s=new EventSource(`/api/v1/logs/stream/${b}`),l=!0,c.classList.add("active"),c.textContent="\u{1F534} Live",c.title="Streaming - click to stop",S.style.display="none";const C=document.getElementById("logs-title");C.textContent.includes("\u{1F534}")||(C.innerHTML=C.textContent.replace("\u{1F4CB}","\u{1F4CB} \u{1F534}")),s.onmessage=P=>{try{const O=JSON.parse(P.data);if(O.error){console.error("Stream error:",O.error),y();return}const D=document.createElement("div");D.className="log-entry",D.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=(O.stream||"stdout")==="stderr",U=R?"var(--bad-fg)":"var(--fg)",F=`${R?"STDERR":"STDOUT"}`;for(D.innerHTML=`
${F}
${escapeHtml(O.text)}
- `,x.appendChild(D),x.scrollTop=x.scrollHeight;x.children.length>500;)x.removeChild(x.firstChild)}catch(O){console.error("Error parsing stream data:",O)}},s.onerror=P=>{console.error("EventSource error:",P),w()}}catch(C){console.error("Failed to start streaming:",C),w()}}function w(){s&&(s.close(),s=null),c=!1;const b=document.getElementById("logs-stream"),l=document.getElementById("logs-pause"),S=document.getElementById("logs-title");b&&(b.classList.remove("active"),b.textContent="\u{1F4E1} Live",b.title="Enable real-time streaming"),l&&(l.style.display=""),S&&(S.textContent=S.textContent.replace(" \u{1F534}","")),E&&p&&!i&&(i=setInterval(B,DC.POLL.LOGS))}async function a(b,l=100){try{const S=`/api/v1/logs/container/${b}?tail=${l}×tamps=true`,x=await fetch(S,{cache:"no-store",headers:{Accept:"application/json","Cache-Control":"no-cache"}});if(x.ok){const C=await x.json();return C.success&&C.logs?{logs:C.logs,count:C.count,containerName:C.containerName,containerId:C.containerId}:{error:C.error||"Failed to fetch container logs"}}else return{error:`HTTP ${x.status}: ${x.statusText}`}}catch(S){return console.error("Container logs fetch failed:",S),{error:S.message}}}function I(b){const l=document.createElement("div");l.className="log-entry",l.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 S=b.stream==="stderr"?"var(--bad-fg)":"var(--fg)",x=b.stream==="stderr"?'STDERR':'STDOUT';return l.innerHTML=` -
${x}
+ `,k.appendChild(D),k.scrollTop=k.scrollHeight;k.children.length>500;)k.removeChild(k.firstChild)}catch(O){console.error("Error parsing stream data:",O)}},s.onerror=P=>{console.error("EventSource error:",P),y()}}catch(C){console.error("Failed to start streaming:",C),y()}}function y(){s&&(s.close(),s=null),l=!1;const b=document.getElementById("logs-stream"),c=document.getElementById("logs-pause"),S=document.getElementById("logs-title");b&&(b.classList.remove("active"),b.textContent="\u{1F4E1} Live",b.title="Enable real-time streaming"),c&&(c.style.display=""),S&&(S.textContent=S.textContent.replace(" \u{1F534}","")),x&&p&&!i&&(i=setInterval(B,DC.POLL.LOGS))}async function a(b,c=100){try{const S=`/api/v1/logs/container/${b}?tail=${c}×tamps=true`,k=await fetch(S,{cache:"no-store",headers:{Accept:"application/json","Cache-Control":"no-cache"}});if(k.ok){const C=await k.json();return C.success&&C.logs?{logs:C.logs,count:C.count,containerName:C.containerName,containerId:C.containerId}:{error:C.error||"Failed to fetch container logs"}}else return{error:`HTTP ${k.status}: ${k.statusText}`}}catch(S){return console.error("Container logs fetch failed:",S),{error:S.message}}}function I(b){const c=document.createElement("div");c.className="log-entry",c.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 S=b.stream==="stderr"?"var(--bad-fg)":"var(--fg)",k=b.stream==="stderr"?'STDERR':'STDOUT';return c.innerHTML=` +
${k}
${escapeHtml(b.text)}
- `,l}async function B(){if(g||!p||!E)return;const b=parseInt(document.getElementById("log-lines").value),l=document.getElementById("logs-content");try{const S=await a(p,b);if(S.error){l.innerHTML=` + `,c}async function B(){if(g||!p||!x)return;const b=parseInt(document.getElementById("log-lines").value),c=document.getElementById("logs-content");try{const S=await a(p,b);if(S.error){c.innerHTML=`
\u26A0\uFE0F Error
${escapeHtml(S.error)}
-
`;return}l.innerHTML=` + `;return}c.innerHTML=`
Stream Log Output -
`,S.logs&&S.logs.length>0?(S.logs.forEach(x=>{const C=I(x);l.appendChild(C)}),l.scrollTop=l.scrollHeight):l.innerHTML+=` + `,S.logs&&S.logs.length>0?(S.logs.forEach(k=>{const C=I(k);c.appendChild(C)}),c.scrollTop=c.scrollHeight):c.innerHTML+=`
No logs available for this container -
`}catch(S){l.innerHTML=` + `}catch(S){c.innerHTML=`
Failed to fetch logs: ${escapeHtml(S.message)} -
`}}function L(b,l){p=b,r=l,E=!0,m=!1,g=!1,w();const S=document.getElementById("logs-modal"),x=document.getElementById("logs-title"),C=document.getElementById("logs-pause"),P=document.getElementById("logs-stream");x.textContent=`\u{1F4CB} ${l} - Container Logs`,C.textContent="\u23F8\uFE0F Pause",C.classList.remove("paused"),P&&(P.style.display=""),S.classList.add("show"),B(),i=setInterval(B,DC.POLL.LOGS)}async function A(b,l=100){try{const S=`/api/v1/logs/file?path=${encodeURIComponent(b)}&tail=${l}`,x=await fetch(S,{cache:"no-store",headers:{Accept:"application/json","Cache-Control":"no-cache"}});if(x.ok){const C=await x.json();return C.success&&C.logs?{logs:C.logs,count:C.count,logPath:C.logPath,totalLines:C.totalLines}:{error:C.error||"Failed to fetch file logs"}}else return{error:(await x.json().catch(()=>({}))).error||`HTTP ${x.status}`}}catch(S){return console.error("File logs fetch failed:",S),{error:S.message}}}function h(b){const l=document.createElement("div");l.className="log-entry",l.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 S=b.text;let x="INFO",C="var(--fg)";S.match(/ERROR|FATAL|CRITICAL/i)?(x="ERROR",C="var(--bad-fg)"):S.match(/WARN|WARNING/i)?(x="WARN",C="#f39c12"):S.match(/DEBUG/i)&&(x="DEBUG",C="var(--muted)");const O=`${x}`;return l.innerHTML=` + `}}function L(b,c){p=b,r=c,x=!0,m=!1,g=!1,y();const S=document.getElementById("logs-modal"),k=document.getElementById("logs-title"),C=document.getElementById("logs-pause"),P=document.getElementById("logs-stream");k.textContent=`\u{1F4CB} ${c} - Container Logs`,C.textContent="\u23F8\uFE0F Pause",C.classList.remove("paused"),P&&(P.style.display=""),S.classList.add("show"),B(),i=setInterval(B,DC.POLL.LOGS)}async function A(b,c=100){try{const S=`/api/v1/logs/file?path=${encodeURIComponent(b)}&tail=${c}`,k=await fetch(S,{cache:"no-store",headers:{Accept:"application/json","Cache-Control":"no-cache"}});if(k.ok){const C=await k.json();return C.success&&C.logs?{logs:C.logs,count:C.count,logPath:C.logPath,totalLines:C.totalLines}:{error:C.error||"Failed to fetch file logs"}}else return{error:(await k.json().catch(()=>({}))).error||`HTTP ${k.status}`}}catch(S){return console.error("File logs fetch failed:",S),{error:S.message}}}function w(b){const c=document.createElement("div");c.className="log-entry",c.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 S=b.text;let k="INFO",C="var(--fg)";S.match(/ERROR|FATAL|CRITICAL/i)?(k="ERROR",C="var(--bad-fg)"):S.match(/WARN|WARNING/i)?(k="WARN",C="#f39c12"):S.match(/DEBUG/i)&&(k="DEBUG",C="var(--muted)");const O=`${k}`;return c.innerHTML=`
${O}
${escapeHtml(S)}
- `,l}async function T(){if(g||!f||!m)return;const b=parseInt(document.getElementById("log-lines").value),l=document.getElementById("logs-content");try{const S=await A(f,b);if(S.error){l.innerHTML=` + `,c}async function T(){if(g||!f||!m)return;const b=parseInt(document.getElementById("log-lines").value),c=document.getElementById("logs-content");try{const S=await A(f,b);if(S.error){c.innerHTML=`
\u26A0\uFE0F Error
${escapeHtml(S.error)}
-
`;return}l.innerHTML=` + `;return}c.innerHTML=`
Log Output (${S.count} of ${S.totalLines} lines) -
`,S.logs&&S.logs.length>0?(S.logs.forEach(x=>{const C=h(x);l.appendChild(C)}),l.scrollTop=l.scrollHeight):l.innerHTML+=` + `,S.logs&&S.logs.length>0?(S.logs.forEach(k=>{const C=w(k);c.appendChild(C)}),c.scrollTop=c.scrollHeight):c.innerHTML+=`
No logs available in this file -
`}catch(S){l.innerHTML=` + `}catch(S){c.innerHTML=`
Failed to fetch logs: ${escapeHtml(S.message)} -
`}}function $(b,l){f=b,t=l,m=!0,E=!1,g=!1;const S=document.getElementById("logs-modal"),x=document.getElementById("logs-title"),C=document.getElementById("logs-pause"),P=document.getElementById("logs-stream");x.textContent=`\u{1F4CB} ${l} - Application Logs`,C.textContent="\u23F8\uFE0F Pause",C.classList.remove("paused"),P&&(P.style.display="none"),S.classList.add("show"),T(),i=setInterval(T,DC.POLL.LOGS)}document.querySelector(".top")?.addEventListener("click",b=>{const l=b.target.closest('[id$="-logs"]');if(!l)return;const S=l.id.replace("-logs","");SITE.dnsServers[S]&&v(S)}),document.getElementById("logs-close")?.addEventListener("click",o),document.getElementById("logs-pause")?.addEventListener("click",()=>{g=!g;const b=document.getElementById("logs-pause");g?(b.textContent="\u25B6\uFE0F Resume",b.classList.add("paused")):(b.textContent="\u23F8\uFE0F Pause",b.classList.remove("paused"),d())}),document.getElementById("log-lines")?.addEventListener("change",()=>{g||d()}),document.getElementById("logs-stream")?.addEventListener("click",()=>{!E||!p||(c?w():u(p))}),document.getElementById("logs-modal")?.addEventListener("click",b=>{b.target.id==="logs-modal"&&o()}),document.addEventListener("keydown",b=>{b.key==="Escape"&&document.getElementById("logs-modal")?.classList.contains("show")&&o()}),window.openContainerLogsModal=L,window.openFileLogsModal=$,window.openLogsModal=v})(),(function(){injectModal("service-edit-modal",` + `}}function $(b,c){f=b,t=c,m=!0,x=!1,g=!1;const S=document.getElementById("logs-modal"),k=document.getElementById("logs-title"),C=document.getElementById("logs-pause"),P=document.getElementById("logs-stream");k.textContent=`\u{1F4CB} ${c} - Application Logs`,C.textContent="\u23F8\uFE0F Pause",C.classList.remove("paused"),P&&(P.style.display="none"),S.classList.add("show"),T(),i=setInterval(T,DC.POLL.LOGS)}document.querySelector(".top")?.addEventListener("click",b=>{const c=b.target.closest('[id$="-logs"]');if(!c)return;const S=c.id.replace("-logs","");SITE.dnsServers[S]&&h(S)}),document.getElementById("logs-close")?.addEventListener("click",o),document.getElementById("logs-pause")?.addEventListener("click",()=>{g=!g;const b=document.getElementById("logs-pause");g?(b.textContent="\u25B6\uFE0F Resume",b.classList.add("paused")):(b.textContent="\u23F8\uFE0F Pause",b.classList.remove("paused"),d())}),document.getElementById("log-lines")?.addEventListener("change",()=>{g||d()}),document.getElementById("logs-stream")?.addEventListener("click",()=>{!x||!p||(l?y():u(p))}),document.getElementById("logs-modal")?.addEventListener("click",b=>{b.target.id==="logs-modal"&&o()}),document.addEventListener("keydown",b=>{b.key==="Escape"&&document.getElementById("logs-modal")?.classList.contains("show")&&o()}),window.openContainerLogsModal=L,window.openFileLogsModal=$,window.openLogsModal=h})(),(function(){injectModal("service-edit-modal",`

Edit Service

@@ -371,12 +371,19 @@ Instructions: ${v.instructionsLink}`:"";showNotification(`${t.toUpperCase()} upd
-
-
+
+ +
+ + +
+
-
`)})(),(function(){async function n(E){try{const f=await fetch(`/api/v1/caddy/cas?caddyfilePath=${encodeURIComponent(E)}`);if(!f.ok)throw new Error(`Failed to load CAs: ${f.status}`);const t=await f.json();if(t.status==="success"){const m=document.getElementById("existing-ca-select");return m.innerHTML="",t.data.cas.length===0?m.innerHTML='':(m.innerHTML='',t.data.cas.forEach(s=>{const c=document.createElement("option");typeof s=="object"?(c.value=s.id,c.textContent=s.displayName||s.name):(c.value=s,c.textContent=s),m.appendChild(c)})),t.data.cas}else throw new Error(t.message)}catch(f){console.error("Error loading CAs:",f);const t=document.getElementById("existing-ca-select");return t.innerHTML='',[]}}function i(E){const{subdomain:f,port:t,ip:m,sslType:s,caName:c,existingCa:y,enableAuth:k,enableCors:e,customHeaders:d,upstreamPath:v,healthCheck:o,timeout:u,tailscaleOnly:w}=E;let a=`${buildDomain(f)} { -`;switch(w&&(a+=` @blocked not remote_ip 100.64.0.0/10 +
`)})(),(function(){async function n(x){try{const f=await fetch(`/api/v1/caddy/cas?caddyfilePath=${encodeURIComponent(x)}`);if(!f.ok)throw new Error(`Failed to load CAs: ${f.status}`);const t=await f.json();if(t.status==="success"){const m=document.getElementById("existing-ca-select");return m.innerHTML="",t.data.cas.length===0?m.innerHTML='':(m.innerHTML='',t.data.cas.forEach(s=>{const l=document.createElement("option");typeof s=="object"?(l.value=s.id,l.textContent=s.displayName||s.name):(l.value=s,l.textContent=s),m.appendChild(l)})),t.data.cas}else throw new Error(t.message)}catch(f){console.error("Error loading CAs:",f);const t=document.getElementById("existing-ca-select");return t.innerHTML='',[]}}function i(x){const{subdomain:f,port:t,ip:m,sslType:s,caName:l,existingCa:v,enableAuth:E,enableCors:e,customHeaders:d,upstreamPath:h,healthCheck:o,timeout:u,tailscaleOnly:y}=x;let a=`${buildDomain(f)} { +`;switch(y&&(a+=` @blocked not remote_ip 100.64.0.0/10 `,a+=` respond @blocked "Access denied. Tailscale connection required." 403 `),s){case"letsencrypt":break;case"caddy-managed":a+=` tls internal -`;break;case"existing-ca":y&&(a+=` tls { - ca ${y} +`;break;case"existing-ca":v&&(a+=` tls { + ca ${v} } -`);break;case"custom-ca":c&&(a+=` tls { - ca ${c} +`);break;case"custom-ca":l&&(a+=` tls { + ca ${l} } -`);break}if(k&&(a+=` basicauth { +`);break}if(E&&(a+=` basicauth { admin $2a$14$hashed_password_here } `),e&&(a+=` header { @@ -707,21 +714,21 @@ Instructions: ${v.instructionsLink}`:"";showNotification(`${t.toUpperCase()} upd `}),a+=` } `}catch{console.warn("Invalid JSON in custom headers")}return o&&(a+=` health_uri ${o} `),a+=` reverse_proxy ${m}:${t} { -`,v&&v!=="/"&&(a+=` rewrite ${v} +`,h&&h!=="/"&&(a+=` rewrite ${h} `),u&&u!==30&&(a+=` transport http { `,a+=` dial_timeout ${u}s `,a+=` response_header_timeout ${u}s `,a+=` } `),a+=` } `,a+=`} -`,a}async function g(E,f,t=DC.DEFAULTS.TTL){const m=window.getToken(getPrimaryDnsId(),"admin");if(!m)throw new Error("DNS admin token not configured. Please set it in the Tokens menu.");const s=buildDomain(E),c=await secureFetch("/api/v1/dns/record",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:s,ip:f,ttl:t,token:m,server:SITE.dnsIp})});if(!c.ok){const k=await c.text();throw new Error(`DNS API Error: ${c.status} - ${k}`)}const y=await c.json();if(!y.success)throw new Error(`DNS Error: ${y.error||"Unknown error"}`);return y}async function p(E){const f={id:E.subdomain,name:E.name,logo:E.logo||`/assets/${E.subdomain}.png`};try{const t=await secureFetch("/api/v1/services",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(f)});if(!t.ok){const m=await t.json();throw new Error(m.error||"Failed to save service")}return await window.loadServices(),window.buildGrid(),f}catch(t){throw console.error("Failed to add service to config:",t),t}}async function r(E){const f=document.getElementById("service-subdomain-input").value.trim(),t=document.getElementById("service-ip-input").value.trim()||"localhost",m=document.getElementById("service-port-input").value.trim()||"80",s=await secureFetch("/api/v1/site",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:buildDomain(f),upstream:`${t}:${m}`,config:E})}),c=await s.json();if(!s.ok||!c.success)throw new Error(c.error||`Caddy API Error: ${s.status}`);return c}window.loadExistingCAs=n,window.generateCaddyConfig=i,window.createDnsRecord=g,window.addServiceToConfig=p,window.addToCaddyfile=r})(),(function(){let n=null;function i(t){n=t;const m=document.getElementById("service-edit-modal");document.getElementById("service-edit-title").textContent=`Edit ${t.name}`,document.getElementById("edit-service-name-display").textContent=t.name,document.getElementById("edit-service-url-display").textContent=t.url||buildServiceUrl(t.id),document.getElementById("edit-service-logo-preview").src=t.logo||`/assets/${t.id}.png`,document.getElementById("edit-subdomain").value=t.id,document.getElementById("edit-port").value=t.port||"",document.getElementById("edit-ip").value=t.ip||"localhost",document.getElementById("edit-tailscale-only").checked=t.tailscaleOnly||!1,document.getElementById("edit-logo-url").value=t.logo||"",m.classList.add("show")}function g(){closeModal("service-edit-modal"),n=null}async function p(){if(!n)return;const t=document.getElementById("edit-subdomain").value.trim().toLowerCase(),m=document.getElementById("edit-port").value.trim(),s=document.getElementById("edit-ip").value.trim()||"localhost",c=document.getElementById("edit-tailscale-only").checked,y=document.getElementById("edit-logo-url").value.trim();if(!t){showNotification("Subdomain is required","warning");return}const k=n.id,e=[];if(t!==k&&e.push("subdomain"),m&&m!==String(n.port)&&e.push("port"),s!==n.ip&&e.push("ip"),c!==(n.tailscaleOnly||!1)&&e.push("tailscale"),y!==n.logo&&e.push("logo"),e.length===0){g();return}const d=document.getElementById("service-edit-save");d.textContent="Saving...",d.disabled=!0;try{if(e.includes("subdomain")||e.includes("port")||e.includes("ip")||e.includes("tailscale")){const u=await(await secureFetch("/api/v1/services/update",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({oldSubdomain:k,newSubdomain:t,port:m||n.port,ip:s,tailscaleOnly:c})})).json();if(!u.success)throw new Error(u.error||"Failed to update service")}const v=window.APPS.findIndex(o=>o.id===k);v!==-1&&(window.APPS[v]={...window.APPS[v],id:t,port:m||window.APPS[v].port,ip:s,tailscaleOnly:c,logo:y||window.APPS[v].logo}),await secureFetch("/api/v1/services",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:t,name:n.name,port:m||n.port,ip:s,logo:y||n.logo,tailscaleOnly:c,containerId:n.containerId,appTemplate:n.appTemplate})}),t!==k&&await secureFetch(`/api/v1/services/${k}`,{method:"DELETE"}),g(),window.buildGrid(),window.refreshAll()}catch(v){console.error("Error saving service changes:",v),showNotification(`Error saving changes: ${v.message}`,"error")}finally{d.textContent="Save Changes",d.disabled=!1}}document.getElementById("edit-logo-file")?.addEventListener("change",async t=>{const m=t.target.files[0];if(!m)return;if(!m.type.startsWith("image/")){showNotification("Please select an image file","warning");return}const s=new FileReader;s.onload=async c=>{const y=c.target.result;if(document.getElementById("edit-service-logo-preview").src=y,document.getElementById("edit-logo-url").value=y,n)try{const e=await(await secureFetch("/api/v1/assets/upload",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({filename:`${n.id}.png`,data:y})})).json();e.success&&e.path&&(document.getElementById("edit-logo-url").value=e.path)}catch{}},s.readAsDataURL(m)}),document.getElementById("service-edit-cancel")?.addEventListener("click",g),document.getElementById("service-edit-save")?.addEventListener("click",p),document.getElementById("service-edit-modal")?.addEventListener("click",t=>{t.target.id==="service-edit-modal"&&g()});function r(t,m,s){return new Promise(c=>{const y=document.getElementById("delete-service-modal"),k=document.getElementById("delete-modal-title"),e=document.getElementById("delete-modal-message"),d=document.getElementById("delete-modal-container-info"),v=document.getElementById("delete-modal-container-name"),o=document.getElementById("delete-modal-help"),u=document.getElementById("delete-modal-cancel"),w=document.getElementById("delete-modal-remove"),a=document.getElementById("delete-modal-delete");k.textContent=`Delete "${t}"`,m?(e.innerHTML="This service has an associated Docker container.
Choose how to proceed:",d.style.display="block",v.textContent=`Container ID: ${s?.slice(0,12)||"Unknown"}`,o.style.display="block",a.style.display="block"):(e.textContent="Remove this service from the dashboard?",d.style.display="none",o.style.display="none",a.style.display="none");const I=()=>{y.classList.remove("show"),u.removeEventListener("click",B),w.removeEventListener("click",L),a.removeEventListener("click",A),y.removeEventListener("click",h)},B=()=>{I(),c(null)},L=()=>{I(),c(!1)},A=()=>{I(),c(!0)},h=T=>{T.target===y&&(I(),c(null))};u.addEventListener("click",B),w.addEventListener("click",L),a.addEventListener("click",A),y.addEventListener("click",h),y.classList.add("show")})}async function E(t,m,s){const c=document.getElementById(`update-btn-${s}`),y=c?.textContent;if(confirm(`Update ${m} to the latest version? +`,a}async function g(x,f,t=DC.DEFAULTS.TTL){const m=window.getToken(getPrimaryDnsId(),"admin");if(!m)throw new Error("DNS admin token not configured. Please set it in the Tokens menu.");const s=buildDomain(x),l=await secureFetch("/api/v1/dns/record",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:s,ip:f,ttl:t,token:m,server:SITE.dnsIp})});if(!l.ok){const E=await l.text();throw new Error(`DNS API Error: ${l.status} - ${E}`)}const v=await l.json();if(!v.success)throw new Error(`DNS Error: ${v.error||"Unknown error"}`);return v}async function p(x){const f={id:x.subdomain,name:x.name,logo:x.logo||`/assets/${x.subdomain}.png`};try{const t=await secureFetch("/api/v1/services",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(f)});if(!t.ok){const m=await t.json();throw new Error(m.error||"Failed to save service")}return await window.loadServices(),window.buildGrid(),f}catch(t){throw console.error("Failed to add service to config:",t),t}}async function r(x){const f=document.getElementById("service-subdomain-input").value.trim(),t=document.getElementById("service-ip-input").value.trim()||"localhost",m=document.getElementById("service-port-input").value.trim()||"80",s=await secureFetch("/api/v1/site",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:buildDomain(f),upstream:`${t}:${m}`,config:x})}),l=await s.json();if(!s.ok||!l.success)throw new Error(l.error||`Caddy API Error: ${s.status}`);return l}window.loadExistingCAs=n,window.generateCaddyConfig=i,window.createDnsRecord=g,window.addServiceToConfig=p,window.addToCaddyfile=r})(),(function(){let n=null;function i(t){n=t;const m=document.getElementById("service-edit-modal");document.getElementById("service-edit-title").textContent=`Edit ${t.name}`,document.getElementById("edit-service-name").value=t.name,document.getElementById("edit-service-url-display").textContent=t.url||buildServiceUrl(t.id),document.getElementById("edit-service-logo-preview").src=t.logo||`/assets/${t.id}.png`,document.getElementById("edit-subdomain").value=t.id,document.getElementById("edit-port").value=t.port||"",document.getElementById("edit-ip").value=t.ip||"localhost",document.getElementById("edit-tailscale-only").checked=t.tailscaleOnly||!1,document.getElementById("edit-logo-url").value=t.logo||"",m.classList.add("show")}function g(){closeModal("service-edit-modal"),n=null}async function p(){if(!n)return;const t=document.getElementById("edit-subdomain").value.trim().toLowerCase(),m=document.getElementById("edit-service-name").value.trim(),s=document.getElementById("edit-port").value.trim(),l=document.getElementById("edit-ip").value.trim()||"localhost",v=document.getElementById("edit-tailscale-only").checked,E=document.getElementById("edit-logo-url").value.trim();if(!t){showNotification("Subdomain is required","warning");return}const e=n.id,d=[];if(t!==e&&d.push("subdomain"),m&&m!==n.name&&d.push("name"),s&&s!==String(n.port)&&d.push("port"),l!==n.ip&&d.push("ip"),v!==(n.tailscaleOnly||!1)&&d.push("tailscale"),E&&E!==n.logo&&d.push("logo"),d.length===0){g();return}const h=document.getElementById("service-edit-save");h.textContent="Saving...",h.disabled=!0;try{const u=await(await secureFetch("/api/v1/services/update",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({oldSubdomain:e,newSubdomain:t,name:m||n.name,port:s||n.port,ip:l,tailscaleOnly:v,logo:E||void 0})})).json();if(!u.success)throw new Error(u.error||"Failed to update service");const y=window.APPS.findIndex(a=>a.id===e);y!==-1&&(window.APPS[y]={...window.APPS[y],id:t,name:m||window.APPS[y].name,port:s||window.APPS[y].port,ip:l,tailscaleOnly:v,logo:E||window.APPS[y].logo}),g(),window.buildGrid(),window.refreshAll()}catch(o){console.error("Error saving service changes:",o),showNotification(`Error saving changes: ${o.message}`,"error")}finally{h.textContent="Save Changes",h.disabled=!1}}document.getElementById("edit-logo-file")?.addEventListener("change",async t=>{const m=t.target.files[0];if(!m)return;if(!m.type.startsWith("image/")){showNotification("Please select an image file","warning");return}const s=new FileReader;s.onload=async l=>{const v=l.target.result;if(document.getElementById("edit-service-logo-preview").src=v,document.getElementById("edit-logo-url").value=v,n)try{const e=await(await secureFetch("/api/v1/assets/upload",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({filename:`${n.id}.png`,data:v})})).json();e.success&&e.path&&(document.getElementById("edit-logo-url").value=e.path)}catch{}},s.readAsDataURL(m)}),document.getElementById("service-edit-cancel")?.addEventListener("click",g),document.getElementById("service-edit-save")?.addEventListener("click",p),document.getElementById("service-edit-modal")?.addEventListener("click",t=>{t.target.id==="service-edit-modal"&&g()});function r(t,m,s){return new Promise(l=>{const v=document.getElementById("delete-service-modal"),E=document.getElementById("delete-modal-title"),e=document.getElementById("delete-modal-message"),d=document.getElementById("delete-modal-container-info"),h=document.getElementById("delete-modal-container-name"),o=document.getElementById("delete-modal-help"),u=document.getElementById("delete-modal-cancel"),y=document.getElementById("delete-modal-remove"),a=document.getElementById("delete-modal-delete");E.textContent=`Delete "${t}"`,m?(e.innerHTML="This service has an associated Docker container.
Choose how to proceed:",d.style.display="block",h.textContent=`Container ID: ${s?.slice(0,12)||"Unknown"}`,o.style.display="block",a.style.display="block"):(e.textContent="Remove this service from the dashboard?",d.style.display="none",o.style.display="none",a.style.display="none");const I=()=>{v.classList.remove("show"),u.removeEventListener("click",B),y.removeEventListener("click",L),a.removeEventListener("click",A),v.removeEventListener("click",w)},B=()=>{I(),l(null)},L=()=>{I(),l(!1)},A=()=>{I(),l(!0)},w=T=>{T.target===v&&(I(),l(null))};u.addEventListener("click",B),y.addEventListener("click",L),a.addEventListener("click",A),v.addEventListener("click",w),v.classList.add("show")})}async function x(t,m,s){const l=document.getElementById(`update-btn-${s}`),v=l?.textContent;if(confirm(`Update ${m} 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{c&&(c.textContent="\u{1F504}",c.disabled=!0,c.title="Updating...");const e=await(await secureFetch(`/api/v1/containers/${t}/update`,{method:"POST"})).json();if(e.success){const d=window.APPS.find(v=>v.id===s);d&&e.newContainerId&&(d.containerId=e.newContainerId),c&&(c.textContent="\u2705",c.title="Updated successfully!",setTimeout(()=>{c.textContent=y,c.disabled=!1,c.title="Update container to latest version"},3e3)),setTimeout(()=>window.refreshAll(),2e3),showNotification(`${m} updated successfully!`,"success")}else throw new Error(e.error||"Update failed")}catch(k){console.error("Update error:",k),c&&(c.textContent="\u274C",c.title="Update failed",setTimeout(()=>{c.textContent=y,c.disabled=!1,c.title="Update container to latest version"},3e3)),showNotification(`Failed to update ${m}: ${k.message}`,"error")}}async function f(t,m){const s=window.APPS.find(a=>a.id===t),c=s?buildDomain(s.id):null,y=s?.containerId,k=await r(m||t,y,s?.containerId);if(k===null)return;let e={dashboard:!1,container:null,dns:null,caddy:null,service:null};if(k&&y)try{const a=new URLSearchParams({containerId:s.containerId,subdomain:s.id,ip:s.ip||"localhost",deleteContainer:"true"}),B=await(await secureFetch(`/api/v1/apps/${encodeURIComponent(s.id)}?${a.toString()}`,{method:"DELETE"})).json();B.success?e={...e,...B.results,dashboard:!1}:console.error("App removal failed:",B.error)}catch(a){console.error("App removal error:",a)}else if(k&&c){try{const a=s?.ip||"localhost",B=await(await secureFetch(`/api/v1/dns/record?domain=${encodeURIComponent(c)}&type=A&ipAddress=${encodeURIComponent(a)}&server=${SITE.dnsIp}`,{method:"DELETE"})).json();e.dns=B.success?"deleted":B.error||"failed"}catch(a){e.dns=a.message}try{const I=await(await secureFetch(`/api/v1/site/${encodeURIComponent(c)}`,{method:"DELETE"})).json();e.caddy=I.success||I.error&&I.error.includes("not found")?"removed":I.error||"failed"}catch(a){e.caddy=a.message}}const d=window.APPS.findIndex(a=>a.id===t);d>-1&&(window.APPS.splice(d,1),e.dashboard=!0);try{const a=safeGetJSON("custom-apps",[]),I=a.findIndex(B=>B.id===t);I>-1&&(a.splice(I,1),safeSet("custom-apps",JSON.stringify(a)))}catch{}try{const I=await(await secureFetch(`/api/v1/services/${encodeURIComponent(t)}`,{method:"DELETE"})).json();e.service=I.success?"removed":I.error||"failed"}catch(a){e.service=a.message}window.buildGrid(),window.refreshAll();let v=!1,o=[];e.dashboard||(v=!0,o.push("\u2717 Failed to remove from dashboard"));const u=["removed","already removed","not found","deleted","kept (user choice)","skipped","no such record","does not exist"],w=a=>!a||u.some(I=>a.toLowerCase().includes(I.toLowerCase()));e.container&&!w(e.container)&&(v=!0,o.push(`\u26A0 Container: ${e.container}`)),e.dns&&!w(e.dns)&&(v=!0,o.push(`\u26A0 DNS Record: ${e.dns}`)),e.caddy&&!w(e.caddy)&&(v=!0,o.push(`\u26A0 Caddy Config: ${e.caddy}`)),e.service&&!w(e.service)&&(v=!0,o.push(`\u26A0 Service File: ${e.service}`)),v&&showNotification(`Error deleting "${m||t}": ${o.join(", ")}`,"error",6e3)}window.openServiceEditModal=i,window.showDeleteModal=r,window.updateContainer=E,window.deleteService=f})(),(function(){function n(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 g(){const e=document.getElementById("service-subdomain-input").value||"subdomain",d=document.getElementById("service-ip-input").value||p.lan||"localhost",v=document.getElementById("service-port-input").value||DC.DEFAULTS.SERVICE_PORT,o=document.getElementById("ssl-type-select").value,u=document.getElementById("ca-name-input").value||"sami-ca",w=document.getElementById("existing-ca-select").value,a=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,h=document.getElementById("timeout-input").value||30,T=document.getElementById("dns-preview");T&&(T.textContent=`${buildDomain(e)} \u2192 ${d}`);const $=document.getElementById("url-preview");$&&($.textContent=buildServiceUrl(e));const b={subdomain:e,port:v,ip:d,sslType:o,caName:u,existingCa:w,enableAuth:a,enableCors:I,customHeaders:B,upstreamPath:L,healthCheck:A,timeout:h},l=window.generateCaddyConfig(b),S=document.getElementById("caddy-config-preview");S&&(S.value=l)}const p={localhost:"127.0.0.1",lan:"",tailscale:""};async function r(){try{const o=await fetch("/api/v1/network/ips",{signal:AbortSignal.timeout(2e3)});if(o.ok){const u=await o.json();u.lan&&(p.lan=u.lan),u.tailscale&&(p.tailscale=u.tailscale)}}catch{}const e=document.getElementById("quick-ip-lan"),d=document.getElementById("quick-ip-tailscale");e&&(p.lan?(e.dataset.ip=p.lan,e.textContent=`LAN (${p.lan})`,e.title=`LAN IP: ${p.lan}`):e.style.display="none"),d&&(p.tailscale?(d.dataset.ip=p.tailscale,d.textContent=`Tailscale (${p.tailscale})`,d.title=`Tailscale IP: ${p.tailscale}`):d.style.display="none");const v=document.getElementById("service-ip-input");v&&!v.value&&p.lan&&(v.value=p.lan)}function E(){document.querySelectorAll(".quick-ip-btn").forEach(e=>{e.addEventListener("click",()=>{const d=e.dataset.ip;d&&(document.getElementById("service-ip-input").value=d,document.querySelectorAll(".quick-ip-btn").forEach(v=>v.classList.remove("active")),e.classList.add("active"),g())})}),document.getElementById("service-ip-input")?.addEventListener("input",e=>{const d=e.target.value;document.querySelectorAll(".quick-ip-btn").forEach(v=>{v.classList.toggle("active",v.dataset.ip===d)})})}async function f(){const e=document.getElementById("add-service-modal");e.classList.add("show");const d=e.querySelector(".weather-modal-content");d&&(d.scrollTop=0),document.body.style.overflow="hidden";const v=document.getElementById("ssl-type-select");v&&(v.value=i()),await r();const o=document.getElementById("caddyfile-path-input").value||DC.DEFAULTS.CADDYFILE;await window.loadExistingCAs(o);const u=document.getElementById("manual-tailscale-status"),w=document.getElementById("manual-tailscale-only");try{const I=await(await fetch("/api/v1/tailscale/status")).json();I.success&&I.installed&&I.connected?(u.innerHTML=` +The service will be briefly unavailable.`))try{l&&(l.textContent="\u{1F504}",l.disabled=!0,l.title="Updating...");const e=await(await secureFetch(`/api/v1/containers/${t}/update`,{method:"POST"})).json();if(e.success){const d=window.APPS.find(h=>h.id===s);d&&e.newContainerId&&(d.containerId=e.newContainerId),l&&(l.textContent="\u2705",l.title="Updated successfully!",setTimeout(()=>{l.textContent=v,l.disabled=!1,l.title="Update container to latest version"},3e3)),setTimeout(()=>window.refreshAll(),2e3),showNotification(`${m} updated successfully!`,"success")}else throw new Error(e.error||"Update failed")}catch(E){console.error("Update error:",E),l&&(l.textContent="\u274C",l.title="Update failed",setTimeout(()=>{l.textContent=v,l.disabled=!1,l.title="Update container to latest version"},3e3)),showNotification(`Failed to update ${m}: ${E.message}`,"error")}}async function f(t,m){const s=window.APPS.find(a=>a.id===t),l=s?buildDomain(s.id):null,v=s?.containerId,E=await r(m||t,v,s?.containerId);if(E===null)return;let e={dashboard:!1,container:null,dns:null,caddy:null,service:null};if(E&&v)try{const a=new URLSearchParams({containerId:s.containerId,subdomain:s.id,ip:s.ip||"localhost",deleteContainer:"true"}),B=await(await secureFetch(`/api/v1/apps/${encodeURIComponent(s.id)}?${a.toString()}`,{method:"DELETE"})).json();B.success?e={...e,...B.results,dashboard:!1}:console.error("App removal failed:",B.error)}catch(a){console.error("App removal error:",a)}else if(E&&l){try{const a=s?.ip||"localhost",B=await(await secureFetch(`/api/v1/dns/record?domain=${encodeURIComponent(l)}&type=A&ipAddress=${encodeURIComponent(a)}&server=${SITE.dnsIp}`,{method:"DELETE"})).json();e.dns=B.success?"deleted":B.error||"failed"}catch(a){e.dns=a.message}try{const I=await(await secureFetch(`/api/v1/site/${encodeURIComponent(l)}`,{method:"DELETE"})).json();e.caddy=I.success||I.error&&I.error.includes("not found")?"removed":I.error||"failed"}catch(a){e.caddy=a.message}}const d=window.APPS.findIndex(a=>a.id===t);d>-1&&(window.APPS.splice(d,1),e.dashboard=!0);try{const a=safeGetJSON("custom-apps",[]),I=a.findIndex(B=>B.id===t);I>-1&&(a.splice(I,1),safeSet("custom-apps",JSON.stringify(a)))}catch{}try{const I=await(await secureFetch(`/api/v1/services/${encodeURIComponent(t)}`,{method:"DELETE"})).json();e.service=I.success?"removed":I.error||"failed"}catch(a){e.service=a.message}window.buildGrid(),window.refreshAll();let h=!1,o=[];e.dashboard||(h=!0,o.push("\u2717 Failed to remove from dashboard"));const u=["removed","already removed","not found","deleted","kept (user choice)","skipped","no such record","does not exist"],y=a=>!a||u.some(I=>a.toLowerCase().includes(I.toLowerCase()));e.container&&!y(e.container)&&(h=!0,o.push(`\u26A0 Container: ${e.container}`)),e.dns&&!y(e.dns)&&(h=!0,o.push(`\u26A0 DNS Record: ${e.dns}`)),e.caddy&&!y(e.caddy)&&(h=!0,o.push(`\u26A0 Caddy Config: ${e.caddy}`)),e.service&&!y(e.service)&&(h=!0,o.push(`\u26A0 Service File: ${e.service}`)),h&&showNotification(`Error deleting "${m||t}": ${o.join(", ")}`,"error",6e3)}window.openServiceEditModal=i,window.showDeleteModal=r,window.updateContainer=x,window.deleteService=f})(),(function(){function n(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 g(){const e=document.getElementById("service-subdomain-input").value||"subdomain",d=document.getElementById("service-ip-input").value||p.lan||"localhost",h=document.getElementById("service-port-input").value||DC.DEFAULTS.SERVICE_PORT,o=document.getElementById("ssl-type-select").value,u=document.getElementById("ca-name-input").value||"sami-ca",y=document.getElementById("existing-ca-select").value,a=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,w=document.getElementById("timeout-input").value||30,T=document.getElementById("dns-preview");T&&(T.textContent=`${buildDomain(e)} \u2192 ${d}`);const $=document.getElementById("url-preview");$&&($.textContent=buildServiceUrl(e));const b={subdomain:e,port:h,ip:d,sslType:o,caName:u,existingCa:y,enableAuth:a,enableCors:I,customHeaders:B,upstreamPath:L,healthCheck:A,timeout:w},c=window.generateCaddyConfig(b),S=document.getElementById("caddy-config-preview");S&&(S.value=c)}const p={localhost:"127.0.0.1",lan:"",tailscale:""};async function r(){try{const o=await fetch("/api/v1/network/ips",{signal:AbortSignal.timeout(2e3)});if(o.ok){const u=await o.json();u.lan&&(p.lan=u.lan),u.tailscale&&(p.tailscale=u.tailscale)}}catch{}const e=document.getElementById("quick-ip-lan"),d=document.getElementById("quick-ip-tailscale");e&&(p.lan?(e.dataset.ip=p.lan,e.textContent=`LAN (${p.lan})`,e.title=`LAN IP: ${p.lan}`):e.style.display="none"),d&&(p.tailscale?(d.dataset.ip=p.tailscale,d.textContent=`Tailscale (${p.tailscale})`,d.title=`Tailscale IP: ${p.tailscale}`):d.style.display="none");const h=document.getElementById("service-ip-input");h&&!h.value&&p.lan&&(h.value=p.lan)}function x(){document.querySelectorAll(".quick-ip-btn").forEach(e=>{e.addEventListener("click",()=>{const d=e.dataset.ip;d&&(document.getElementById("service-ip-input").value=d,document.querySelectorAll(".quick-ip-btn").forEach(h=>h.classList.remove("active")),e.classList.add("active"),g())})}),document.getElementById("service-ip-input")?.addEventListener("input",e=>{const d=e.target.value;document.querySelectorAll(".quick-ip-btn").forEach(h=>{h.classList.toggle("active",h.dataset.ip===d)})})}async function f(){const e=document.getElementById("add-service-modal");e.classList.add("show");const d=e.querySelector(".weather-modal-content");d&&(d.scrollTop=0),document.body.style.overflow="hidden";const h=document.getElementById("ssl-type-select");h&&(h.value=i()),await r();const o=document.getElementById("caddyfile-path-input").value||DC.DEFAULTS.CADDYFILE;await window.loadExistingCAs(o);const u=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?(u.innerHTML=` \u2713 Connected ${I.self?.hostname} (${I.self?.ip}) - `,w.disabled=!1):I.installed?(u.innerHTML='\u26A0 Not connected',w.disabled=!0):(u.innerHTML='Not available',w.disabled=!0)}catch{u.innerHTML='Could not check',w.disabled=!0}w.checked=!1,g()}function t(){const e=document.getElementById("service-type-local"),d=document.getElementById("service-type-external"),v=document.getElementById("local-service-config"),o=document.getElementById("external-service-config"),u=document.getElementById("tab-local"),w=document.getElementById("tab-external");function a(){e.checked?(v.style.display="grid",o.style.display="none",u&&(u.style.background="var(--accent)",u.style.color="var(--bg)"),w&&(w.style.background="transparent",w.style.color="var(--muted)")):(v.style.display="none",o.style.display="block",w&&(w.style.background="var(--accent)",w.style.color="var(--bg)"),u&&(u.style.background="transparent",u.style.color="var(--muted)"))}e?.addEventListener("change",a),d?.addEventListener("change",a)}function m(){const e=document.getElementById("service-name-input"),d=document.getElementById("service-subdomain-input"),v=document.getElementById("subdomain-preview");let o=!1;e?.addEventListener("input",()=>{const L=n(e.value);!o&&d&&(d.value=L),v&&(v.textContent=L?`\u2192 ${buildDomain(L)}`:""),g()}),d?.addEventListener("input",()=>{o=d.value!==n(e?.value||"");const L=d.value.trim()||n(e?.value||"");v&&(v.textContent=L?`\u2192 ${buildDomain(L)}`:""),g()});const u=document.getElementById("external-service-name"),w=document.getElementById("external-service-subdomain"),a=document.getElementById("external-subdomain-preview"),I=document.getElementById("external-domain-preview");let B=!1;u?.addEventListener("input",()=>{const L=n(u.value);!B&&w&&(w.value=L);const A=w?.value||L;a&&(a.textContent=A?`\u2192 ${buildDomain(A)}`:""),I&&(I.textContent=A?buildDomain(A):"")}),w?.addEventListener("input",()=>{B=w.value!==n(u?.value||"");const L=w.value.trim()||n(u?.value||"");a&&(a.textContent=L?`\u2192 ${buildDomain(L)}`:""),I&&(I.textContent=L?buildDomain(L):"")})}async function s(){const e=document.getElementById("external-service-name").value.trim(),d=document.getElementById("external-service-url").value.trim(),v=(document.getElementById("external-service-subdomain").value.trim()||n(e)).toLowerCase(),o=document.getElementById("external-service-logo").value.trim(),u=document.getElementById("external-service-icon").value.trim(),w=document.getElementById("external-create-dns").checked,a=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||!d){showNotification("Please fill in Name and External URL","warning");return}if(!v){showNotification("Could not derive subdomain from name. Please set one in Options.","warning");return}if(!d.startsWith("http://")&&!d.startsWith("https://")){showNotification("External URL must start with http:// or https://","warning");return}const A=buildDomain(v);try{const h={dns:null,caddy:null,dashboard:!1};if(w)if(window.getToken(getPrimaryDnsId(),"admin"))try{const C=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();h.dns=C.success?"created":C.error||"failed"}catch(x){h.dns=x.message}else h.dns="no admin token (configure in \u{1F511} Tokens)";if(a)try{const S={subdomain:v,externalUrl:d,preserveHost:B,followRedirects:L,sslType:"caddy-managed",caddyfilePath:DC.DEFAULTS.CADDYFILE,reloadCaddy:!0},C=await(await secureFetch("/api/v1/site/external",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(S)})).json();h.caddy=C.success?"created":C.error||"failed"}catch(S){h.caddy=S.message}const T={id:v,name:e,url:`https://${A}`,externalUrl:d,logo:o||u||"\u{1F310}",isExternal:!0,isCustom:!0};window.APPS.push(T),h.dashboard=!0;const $=["plex","router","chat","sync","torrent","radarr","sonarr","prowlarr","portainer","requests","jellyfin","emby"],b=window.APPS.filter(S=>!$.includes(S.id));safeSet("custom-services",JSON.stringify(b));try{await secureFetch("/api/v1/services",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(window.APPS)})}catch(S){console.warn("Failed to save to services.json:",S)}window.buildGrid(),window.refreshAll(),c();const l=[`External service "${e}" added!`];w&&l.push(`DNS: ${h.dns==="created"?"\u2713":"\u26A0 "+h.dns}`),a&&l.push(`Caddy: ${h.caddy==="created"?"\u2713":"\u26A0 "+h.caddy}`),l.push(`Access at: https://${A}`),showNotification(l.join(" | "),"success",6e3)}catch(h){console.error("Failed to create external service:",h),showNotification(`Failed to create external service: ${h.message}`,"error")}}function c(){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=p.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 d=document.getElementById("external-subdomain-preview");d&&(d.textContent="");const v=document.getElementById("external-service-name");v&&(v.value="");const o=document.getElementById("external-service-subdomain");o&&(o.value="");const u=document.getElementById("external-service-url");u&&(u.value="");const w=document.getElementById("external-service-logo");w&&(w.value="");const a=document.getElementById("external-service-icon");a&&(a.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"),h=document.getElementById("external-service-config");A&&(A.style.display="grid"),h&&(h.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 y(){const e=document.getElementById("service-name-input").value.trim(),d=(document.getElementById("service-subdomain-input").value.trim()||n(e)).toLowerCase(),v=document.getElementById("service-port-input").value.trim(),o=document.getElementById("service-ip-input").value.trim(),u=document.getElementById("service-logo-input").value.trim(),w=document.getElementById("create-dns-record").checked,a=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||"",h=document.getElementById("enable-auth")?.checked||!1,T=document.getElementById("enable-cors")?.checked||!1,$=document.getElementById("custom-headers-input")?.value||"",b=document.getElementById("upstream-path-input")?.value||"/",l=document.getElementById("health-check-input")?.value||"",S=document.getElementById("timeout-input")?.value||30,x=window.getToken(getPrimaryDnsId(),"admin");if(!e||!v||!o){showNotification("Please fill in Name, Port, and IP Address","warning");return}if(!d){showNotification("Could not derive subdomain from name. Please set one in Options.","warning");return}if(w&&!x){showNotification("DNS Admin token required. Configure it in the Tokens menu first.","warning");return}const C={dns:null,caddy:null,dashboard:!1};try{if(w)try{await window.createDnsRecord(d,o,a),C.dns="created"}catch(N){throw console.error("DNS creation failed:",N),C.dns=N.message,new Error(`DNS creation failed: ${N.message}`)}else C.dns="skipped";const P=window.generateCaddyConfig({subdomain:d,port:v,ip:o,sslType:B,caName:L,existingCa:A,enableAuth:h,enableCors:T,customHeaders:$,upstreamPath:b,healthCheck:l,timeout:S,tailscaleOnly:I});try{const R=await(await secureFetch("/api/v1/site",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:buildDomain(d),upstream:`${o}:${v}`,config:P})})).json();if(R.success)C.caddy="added & reloaded";else throw console.error("Caddy configuration failed:",R.error),C.caddy=R.error||"failed",new Error(`Caddy configuration failed: ${R.error}`)}catch(N){throw console.error("Caddy API error:",N),C.caddy=N.message,new Error(`Caddy API error: ${N.message}`)}const O={name:e,subdomain:d,port:v,ip:o,logo:u||`/assets/${d}.png`,tailscaleOnly:I||!1};await window.addServiceToConfig(O),C.dashboard=!0;const D=[`DNS: ${C.dns==="created"?"\u2713":C.dns==="skipped"?"\u25CB":"\u2717"}`,`Caddy: ${C.caddy==="added & reloaded"?"\u2713":"\u2717"}`,`Dashboard: ${C.dashboard?"\u2713":"\u2717"}`];showNotification(`Service "${e}" created! ${D.join(" | ")} \u2014 ${buildServiceUrl(d)}${I?" (Tailscale)":""}`,"success",6e3),c(),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",f),document.getElementById("add-service-cancel")?.addEventListener("click",c),document.getElementById("add-service-create")?.addEventListener("click",()=>{document.querySelector('input[name="service-type"]:checked')?.value==="external"?s():y()}),t(),m(),E(),document.getElementById("ssl-type-select")?.addEventListener("change",e=>{const d=document.getElementById("existing-ca-config"),v=document.getElementById("custom-ca-config");d.style.display="none",v.style.display="none",e.target.value==="existing-ca"?d.style.display="block":e.target.value==="custom-ca"&&(v.style.display="block"),g()}),document.getElementById("refresh-cas")?.addEventListener("click",async()=>{const e=document.getElementById("refresh-cas"),d=e.textContent;e.textContent="\u231B Loading...",e.disabled=!0;try{const v=document.getElementById("caddyfile-path-input").value||DC.DEFAULTS.CADDYFILE;await window.loadExistingCAs(v),e.textContent="\u2705 Refreshed"}catch(v){e.textContent="\u274C Failed",console.error("Failed to refresh CAs:",v)}setTimeout(()=>{e.textContent=d,e.disabled=!1},2e3)}),document.getElementById("create-dns-record")?.addEventListener("change",e=>{const d=document.getElementById("dns-config");d.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 d=document.getElementById(e);d&&(d.addEventListener("input",g),d.addEventListener("change",g))});function k(){const e=safeGet("custom-services");if(e)try{JSON.parse(e).forEach(v=>{window.APPS.find(o=>o.id===v.id)||window.APPS.push(v)})}catch(d){console.warn("Failed to load custom services:",d)}}k(),window.openAddServiceModal=f,window.closeAddServiceModal=c})(); + `,y.disabled=!1):I.installed?(u.innerHTML='\u26A0 Not connected',y.disabled=!0):(u.innerHTML='Not available',y.disabled=!0)}catch{u.innerHTML='Could not check',y.disabled=!0}y.checked=!1,g()}function t(){const e=document.getElementById("service-type-local"),d=document.getElementById("service-type-external"),h=document.getElementById("local-service-config"),o=document.getElementById("external-service-config"),u=document.getElementById("tab-local"),y=document.getElementById("tab-external");function a(){e.checked?(h.style.display="grid",o.style.display="none",u&&(u.style.background="var(--accent)",u.style.color="var(--bg)"),y&&(y.style.background="transparent",y.style.color="var(--muted)")):(h.style.display="none",o.style.display="block",y&&(y.style.background="var(--accent)",y.style.color="var(--bg)"),u&&(u.style.background="transparent",u.style.color="var(--muted)"))}e?.addEventListener("change",a),d?.addEventListener("change",a)}function m(){const e=document.getElementById("service-name-input"),d=document.getElementById("service-subdomain-input"),h=document.getElementById("subdomain-preview");let o=!1;e?.addEventListener("input",()=>{const L=n(e.value);!o&&d&&(d.value=L),h&&(h.textContent=L?`\u2192 ${buildDomain(L)}`:""),g()}),d?.addEventListener("input",()=>{o=d.value!==n(e?.value||"");const L=d.value.trim()||n(e?.value||"");h&&(h.textContent=L?`\u2192 ${buildDomain(L)}`:""),g()});const u=document.getElementById("external-service-name"),y=document.getElementById("external-service-subdomain"),a=document.getElementById("external-subdomain-preview"),I=document.getElementById("external-domain-preview");let B=!1;u?.addEventListener("input",()=>{const L=n(u.value);!B&&y&&(y.value=L);const A=y?.value||L;a&&(a.textContent=A?`\u2192 ${buildDomain(A)}`:""),I&&(I.textContent=A?buildDomain(A):"")}),y?.addEventListener("input",()=>{B=y.value!==n(u?.value||"");const L=y.value.trim()||n(u?.value||"");a&&(a.textContent=L?`\u2192 ${buildDomain(L)}`:""),I&&(I.textContent=L?buildDomain(L):"")})}async function s(){const e=document.getElementById("external-service-name").value.trim(),d=document.getElementById("external-service-url").value.trim(),h=(document.getElementById("external-service-subdomain").value.trim()||n(e)).toLowerCase(),o=document.getElementById("external-service-logo").value.trim(),u=document.getElementById("external-service-icon").value.trim(),y=document.getElementById("external-create-dns").checked,a=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||!d){showNotification("Please fill in Name and External URL","warning");return}if(!h){showNotification("Could not derive subdomain from name. Please set one in Options.","warning");return}if(!d.startsWith("http://")&&!d.startsWith("https://")){showNotification("External URL must start with http:// or https://","warning");return}const A=buildDomain(h);try{const w={dns:null,caddy:null,dashboard:!1};if(y)if(window.getToken(getPrimaryDnsId(),"admin"))try{const C=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();w.dns=C.success?"created":C.error||"failed"}catch(k){w.dns=k.message}else w.dns="no admin token (configure in \u{1F511} Tokens)";if(a)try{const S={subdomain:h,externalUrl:d,preserveHost:B,followRedirects:L,sslType:"caddy-managed",caddyfilePath:DC.DEFAULTS.CADDYFILE,reloadCaddy:!0},C=await(await secureFetch("/api/v1/site/external",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(S)})).json();w.caddy=C.success?"created":C.error||"failed"}catch(S){w.caddy=S.message}const T={id:h,name:e,url:`https://${A}`,externalUrl:d,logo:o||u||"\u{1F310}",isExternal:!0,isCustom:!0};window.APPS.push(T),w.dashboard=!0;const $=["plex","router","chat","sync","torrent","radarr","sonarr","prowlarr","portainer","requests","jellyfin","emby"],b=window.APPS.filter(S=>!$.includes(S.id));safeSet("custom-services",JSON.stringify(b));try{await secureFetch("/api/v1/services",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(window.APPS)})}catch(S){console.warn("Failed to save to services.json:",S)}window.buildGrid(),window.refreshAll(),l();const c=[`External service "${e}" added!`];y&&c.push(`DNS: ${w.dns==="created"?"\u2713":"\u26A0 "+w.dns}`),a&&c.push(`Caddy: ${w.caddy==="created"?"\u2713":"\u26A0 "+w.caddy}`),c.push(`Access at: https://${A}`),showNotification(c.join(" | "),"success",6e3)}catch(w){console.error("Failed to create external service:",w),showNotification(`Failed to create external service: ${w.message}`,"error")}}function l(){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=p.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 d=document.getElementById("external-subdomain-preview");d&&(d.textContent="");const h=document.getElementById("external-service-name");h&&(h.value="");const o=document.getElementById("external-service-subdomain");o&&(o.value="");const u=document.getElementById("external-service-url");u&&(u.value="");const y=document.getElementById("external-service-logo");y&&(y.value="");const a=document.getElementById("external-service-icon");a&&(a.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"),w=document.getElementById("external-service-config");A&&(A.style.display="grid"),w&&(w.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 v(){const e=document.getElementById("service-name-input").value.trim(),d=(document.getElementById("service-subdomain-input").value.trim()||n(e)).toLowerCase(),h=document.getElementById("service-port-input").value.trim(),o=document.getElementById("service-ip-input").value.trim(),u=document.getElementById("service-logo-input").value.trim(),y=document.getElementById("create-dns-record").checked,a=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||"",w=document.getElementById("enable-auth")?.checked||!1,T=document.getElementById("enable-cors")?.checked||!1,$=document.getElementById("custom-headers-input")?.value||"",b=document.getElementById("upstream-path-input")?.value||"/",c=document.getElementById("health-check-input")?.value||"",S=document.getElementById("timeout-input")?.value||30,k=window.getToken(getPrimaryDnsId(),"admin");if(!e||!h||!o){showNotification("Please fill in Name, Port, and IP Address","warning");return}if(!d){showNotification("Could not derive subdomain from name. Please set one in Options.","warning");return}if(y&&!k){showNotification("DNS Admin token required. Configure it in the Tokens menu first.","warning");return}const C={dns:null,caddy:null,dashboard:!1};try{if(y)try{await window.createDnsRecord(d,o,a),C.dns="created"}catch(N){throw console.error("DNS creation failed:",N),C.dns=N.message,new Error(`DNS creation failed: ${N.message}`)}else C.dns="skipped";const P=window.generateCaddyConfig({subdomain:d,port:h,ip:o,sslType:B,caName:L,existingCa:A,enableAuth:w,enableCors:T,customHeaders:$,upstreamPath:b,healthCheck:c,timeout:S,tailscaleOnly:I});try{const R=await(await secureFetch("/api/v1/site",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:buildDomain(d),upstream:`${o}:${h}`,config:P})})).json();if(R.success)C.caddy="added & reloaded";else throw console.error("Caddy configuration failed:",R.error),C.caddy=R.error||"failed",new Error(`Caddy configuration failed: ${R.error}`)}catch(N){throw console.error("Caddy API error:",N),C.caddy=N.message,new Error(`Caddy API error: ${N.message}`)}const O={name:e,subdomain:d,port:h,ip:o,logo:u||`/assets/${d}.png`,tailscaleOnly:I||!1};await window.addServiceToConfig(O),C.dashboard=!0;const D=[`DNS: ${C.dns==="created"?"\u2713":C.dns==="skipped"?"\u25CB":"\u2717"}`,`Caddy: ${C.caddy==="added & reloaded"?"\u2713":"\u2717"}`,`Dashboard: ${C.dashboard?"\u2713":"\u2717"}`];showNotification(`Service "${e}" created! ${D.join(" | ")} \u2014 ${buildServiceUrl(d)}${I?" (Tailscale)":""}`,"success",6e3),l(),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",f),document.getElementById("add-service-cancel")?.addEventListener("click",l),document.getElementById("add-service-create")?.addEventListener("click",()=>{document.querySelector('input[name="service-type"]:checked')?.value==="external"?s():v()}),t(),m(),x(),document.getElementById("ssl-type-select")?.addEventListener("change",e=>{const d=document.getElementById("existing-ca-config"),h=document.getElementById("custom-ca-config");d.style.display="none",h.style.display="none",e.target.value==="existing-ca"?d.style.display="block":e.target.value==="custom-ca"&&(h.style.display="block"),g()}),document.getElementById("refresh-cas")?.addEventListener("click",async()=>{const e=document.getElementById("refresh-cas"),d=e.textContent;e.textContent="\u231B Loading...",e.disabled=!0;try{const h=document.getElementById("caddyfile-path-input").value||DC.DEFAULTS.CADDYFILE;await window.loadExistingCAs(h),e.textContent="\u2705 Refreshed"}catch(h){e.textContent="\u274C Failed",console.error("Failed to refresh CAs:",h)}setTimeout(()=>{e.textContent=d,e.disabled=!1},2e3)}),document.getElementById("create-dns-record")?.addEventListener("change",e=>{const d=document.getElementById("dns-config");d.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 d=document.getElementById(e);d&&(d.addEventListener("input",g),d.addEventListener("change",g))});function E(){const e=safeGet("custom-services");if(e)try{JSON.parse(e).forEach(h=>{window.APPS.find(o=>o.id===h.id)||window.APPS.push(h)})}catch(d){console.warn("Failed to load custom services:",d)}}E(),window.openAddServiceModal=f,window.closeAddServiceModal=l})(); diff --git a/status/js/core/grid.js b/status/js/core/grid.js index 89d8008..e6e42ef 100644 --- a/status/js/core/grid.js +++ b/status/js/core/grid.js @@ -77,7 +77,14 @@ } } - function serviceUrl(id) { return buildServiceUrl(id); } + function serviceUrl(id) { + const svc = window.APPS?.find(a => a.id === id); + if (svc?.url) return svc.url.startsWith('http') ? svc.url : 'https://' + svc.url; + if (svc?.isExternal && svc.externalUrl) return svc.externalUrl; + const dns = SITE.dnsServers?.[id]; + if (dns) return 'http://' + dns.ip + ':' + (dns.port || 5380); + return buildServiceUrl(id); + } function el(tag, cls, txt) { const n = document.createElement(tag); if (cls) n.className = cls; if (txt) n.textContent = txt; return n; } function buildGrid() {