|
|
|
@@ -1,4 +1,4 @@
|
|
|
|
|
(function(c){"use strict";class a{constructor(){this.errors=[],this.maxErrors=50}logError(h,b,g={}){const v={timestamp:new Date().toISOString(),context:h,message:b instanceof Error?b.message:b,stack:b instanceof Error?b.stack:null,metadata:g};this.errors.push(v),this.errors.length>this.maxErrors&&this.errors.shift(),console.error(`[Onboarding Error] ${h}:`,b,g)}recoverFromError(h,b){switch(this.classifyError(h)){case"ELEMENT_NOT_FOUND":return this.logError("Element Not Found",h,{currentStep:b}),{action:"SKIP_STEP",nextStep:b+1,message:"Target element not found, skipping to next step"};case"STORAGE_UNAVAILABLE":return this.logError("Storage Unavailable",h),{action:"USE_MEMORY_STORAGE",message:"Local storage unavailable, using in-memory storage"};case"DRIVER_NOT_LOADED":return this.logError("Driver.js Not Loaded",h),{action:"ABORT_TOUR",message:"Driver.js library not loaded, cannot start tour"};case"INVALID_TOOLTIP":return this.logError("Invalid Tooltip Configuration",h,{currentStep:b}),{action:"SKIP_STEP",nextStep:b+1,message:"Invalid tooltip configuration, skipping"};case"THEME_DETECTION_FAILED":return this.logError("Theme Detection Failed",h),{action:"USE_DEFAULT_THEME",message:"Using default dark theme"};default:return this.logError("Unknown Error",h,{currentStep:b}),{action:"ABORT_TOUR",message:"Unexpected error occurred, aborting tour"}}}classifyError(h){const b=h.message||h.toString();return b.includes("element")&&b.includes("not found")?"ELEMENT_NOT_FOUND":b.includes("storage")||b.includes("quota")?"STORAGE_UNAVAILABLE":b.includes("driver")||b.includes("undefined")?"DRIVER_NOT_LOADED":b.includes("invalid")||b.includes("validation")?"INVALID_TOOLTIP":b.includes("theme")?"THEME_DETECTION_FAILED":"UNKNOWN"}getErrors(){return[...this.errors]}clearErrors(){this.errors=[]}getStatistics(){const h={total:this.errors.length,byContext:{},byType:{},recent:this.errors.slice(-10)};return this.errors.forEach(b=>{h.byContext[b.context]=(h.byContext[b.context]||0)+1;const g=this.classifyError({message:b.message});h.byType[g]=(h.byType[g]||0)+1}),h}handleDriverLoadFailure(){this.logError("Driver.js Load Failure","Driver.js library failed to load");const h=document.createElement("div");return h.id="onboarding-fallback",h.style.cssText=`
|
|
|
|
|
(function(u){"use strict";class o{constructor(){this.errors=[],this.maxErrors=50}logError(h,b,g={}){const v={timestamp:new Date().toISOString(),context:h,message:b instanceof Error?b.message:b,stack:b instanceof Error?b.stack:null,metadata:g};this.errors.push(v),this.errors.length>this.maxErrors&&this.errors.shift(),console.error(`[Onboarding Error] ${h}:`,b,g)}recoverFromError(h,b){switch(this.classifyError(h)){case"ELEMENT_NOT_FOUND":return this.logError("Element Not Found",h,{currentStep:b}),{action:"SKIP_STEP",nextStep:b+1,message:"Target element not found, skipping to next step"};case"STORAGE_UNAVAILABLE":return this.logError("Storage Unavailable",h),{action:"USE_MEMORY_STORAGE",message:"Local storage unavailable, using in-memory storage"};case"DRIVER_NOT_LOADED":return this.logError("Driver.js Not Loaded",h),{action:"ABORT_TOUR",message:"Driver.js library not loaded, cannot start tour"};case"INVALID_TOOLTIP":return this.logError("Invalid Tooltip Configuration",h,{currentStep:b}),{action:"SKIP_STEP",nextStep:b+1,message:"Invalid tooltip configuration, skipping"};case"THEME_DETECTION_FAILED":return this.logError("Theme Detection Failed",h),{action:"USE_DEFAULT_THEME",message:"Using default dark theme"};default:return this.logError("Unknown Error",h,{currentStep:b}),{action:"ABORT_TOUR",message:"Unexpected error occurred, aborting tour"}}}classifyError(h){const b=h.message||h.toString();return b.includes("element")&&b.includes("not found")?"ELEMENT_NOT_FOUND":b.includes("storage")||b.includes("quota")?"STORAGE_UNAVAILABLE":b.includes("driver")||b.includes("undefined")?"DRIVER_NOT_LOADED":b.includes("invalid")||b.includes("validation")?"INVALID_TOOLTIP":b.includes("theme")?"THEME_DETECTION_FAILED":"UNKNOWN"}getErrors(){return[...this.errors]}clearErrors(){this.errors=[]}getStatistics(){const h={total:this.errors.length,byContext:{},byType:{},recent:this.errors.slice(-10)};return this.errors.forEach(b=>{h.byContext[b.context]=(h.byContext[b.context]||0)+1;const g=this.classifyError({message:b.message});h.byType[g]=(h.byType[g]||0)+1}),h}handleDriverLoadFailure(){this.logError("Driver.js Load Failure","Driver.js library failed to load");const h=document.createElement("div");return h.id="onboarding-fallback",h.style.cssText=`
|
|
|
|
|
position: fixed;
|
|
|
|
|
bottom: 20px;
|
|
|
|
|
right: 20px;
|
|
|
|
@@ -16,48 +16,48 @@
|
|
|
|
|
The interactive tour is unavailable, but you can explore the dashboard freely.
|
|
|
|
|
Check the documentation for help getting started.
|
|
|
|
|
</p>
|
|
|
|
|
`,document.body.appendChild(h),setTimeout(()=>{h.parentNode&&h.parentNode.removeChild(h)},1e4),!0}handleStorageUnavailable(){this.logError("Storage Unavailable","Local storage is not available");const h={data:{},getItem(b){return this.data[b]||null},setItem(b,g){this.data[b]=g},removeItem(b){delete this.data[b]},clear(){this.data={}}};return console.warn("[ErrorHandler] Using in-memory storage - progress will not persist"),h}sendToErrorTracking(h){}}c.ErrorHandler=a,console.log("[ErrorHandler] Module loaded")})(window),(function(){try{var a=typeof localStorage<"u"&&localStorage.getItem("dashcaddy-health-settings")||null;if(a){var m=JSON.parse(a);m.statsPollingInterval&&m.statsPollingInterval>=5&&m.statsPollingInterval<=3600&&(window.__DC_STATS_OVERRIDE=m.statsPollingInterval*1e3)}}catch{}})();const DC={NAME:"DashCaddy",POLL:{DASHBOARD:1e4,LOGS:3e3,STATS:typeof window<"u"&&window.__DC_STATS_OVERRIDE||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"}},errorHandler=new ErrorHandler,_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 h=await fetch("/api/v1/config");if(h.ok){const b=await h.json();if(b.tld&&(SITE.tld=b.tld.startsWith(".")?b.tld:"."+b.tld),b.dns&&(SITE.dnsIp=b.dns.ip||"",SITE.dnsPort=b.dns.port||DC.DEFAULTS.DNS_PORT),b.dnsServers&&typeof b.dnsServers=="object")for(const[v,p]of Object.entries(b.dnsServers))v!=="__proto__"&&v!=="constructor"&&v!=="prototype"&&(SITE.dnsServers[v]=p);b.configurationType&&(SITE.configurationType=b.configurationType),b.domain&&(SITE.domain=b.domain),b.defaults&&(SITE.defaults=b.defaults),b.routingMode&&(SITE.routingMode=b.routingMode),SITE.onboardingCompleted=b.onboardingCompleted===!0,localStorage.setItem("dashcaddy_site_config",JSON.stringify({tld:SITE.tld,configurationType:SITE.configurationType,domain:SITE.domain,routingMode:SITE.routingMode})),renderDnsCards();const g=document.getElementById("manage-tokens");g&&(g.style.display=Object.keys(SITE.dnsServers).length?"":"none")}}catch{}document.querySelectorAll("[data-tld]").forEach(h=>h.textContent=SITE.tld);const a=document.getElementById("edit-tld-suffix");a&&(a.textContent=SITE.tld);const m=document.getElementById("external-proxy-ip");m&&SITE.dnsIp&&(m.value=SITE.dnsIp,m.placeholder=SITE.dnsIp)})();function buildDomain(c){return c+SITE.tld}function buildServiceUrl(c){return SITE.routingMode==="subdirectory"&&SITE.domain?"https://"+SITE.domain+"/"+c:SITE.configurationType==="public"&&SITE.domain?"https://"+c+"."+SITE.domain:"https://"+buildDomain(c)}function getDnsServerAddr(c){const a=SITE.dnsServers[c];return a?`${a.ip}:${a.port}`:buildDomain(c)}function getPrimaryDnsId(){if(!SITE.dnsIp)return null;for(const[c,a]of Object.entries(SITE.dnsServers))if(a.ip===SITE.dnsIp)return c;return null}function renderDnsCards(){const c=document.querySelector(".top");if(!c)return;const a=Object.keys(SITE.dnsServers);if(!a.length)return;const m='<svg viewBox="0 0 24 24" class="service-icon"><rect x="3" y="4" width="18" height="16" rx="2" fill="#34495e"/><rect x="5" y="6" width="14" height="2" rx="1" fill="#3498db"/><rect x="5" y="9" width="10" height="1" fill="#ecf0f1"/><rect x="5" y="11" width="12" height="1" fill="#ecf0f1"/><rect x="5" y="13" width="8" height="1" fill="#ecf0f1"/><rect x="5" y="15" width="14" height="1" fill="#ecf0f1"/><circle cx="17" cy="11" r="2" fill="#e74c3c"/><path d="M17 9v4M15 11h4" stroke="white" stroke-width="1"/></svg>',h=c.firstElementChild;a.forEach(b=>{const g=escapeHtml(b),v=escapeHtml((SITE.dnsServers[b].name||b).toUpperCase()),p=document.createElement("div");p.className="card",p.setAttribute("data-app",b),p.setAttribute("data-status","off"),p.innerHTML=`<span id="${g}-dot" class="dot bad at-bl"></span><div class="row"><div class="logo-wrap">${m}</div><span class="name">${v}</span><span class="spacer"></span><span id="${g}-pill" class="badge off" data-i18n-live-status="binary">OFF</span></div><div class="response-row"><span id="${g}-time" class="response-time">--</span></div><div class="health-row" id="health-${g}"><span id="uptime-${g}" class="uptime-chip">--</span><div class="uptime-mini-bar"><div class="fill" id="uptime-bar-${g}" style="width: 0%"></div></div></div><div class="btn-row"><button id="${g}-restart" class="restart-btn" data-i18n="action.restart">Restart</button><button id="${g}-update" class="update-btn" title="Update DNS server">\u2B06\uFE0F</button><button id="${g}-open" data-i18n="action.open">OpLine truncated
|
|
|
|
|
`,document.body.appendChild(h),setTimeout(()=>{h.parentNode&&h.parentNode.removeChild(h)},1e4),!0}handleStorageUnavailable(){this.logError("Storage Unavailable","Local storage is not available");const h={data:{},getItem(b){return this.data[b]||null},setItem(b,g){this.data[b]=g},removeItem(b){delete this.data[b]},clear(){this.data={}}};return console.warn("[ErrorHandler] Using in-memory storage - progress will not persist"),h}sendToErrorTracking(h){}}u.ErrorHandler=o,console.log("[ErrorHandler] Module loaded")})(window),(function(){try{var o=typeof localStorage<"u"&&localStorage.getItem("dashcaddy-health-settings")||null;if(o){var m=JSON.parse(o);m.statsPollingInterval&&m.statsPollingInterval>=5&&m.statsPollingInterval<=3600&&(window.__DC_STATS_OVERRIDE=m.statsPollingInterval*1e3)}}catch{}})();const DC={NAME:"DashCaddy",POLL:{DASHBOARD:1e4,LOGS:3e3,STATS:typeof window<"u"&&window.__DC_STATS_OVERRIDE||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"}},errorHandler=new ErrorHandler,_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 h=await fetch("/api/v1/config");if(h.ok){const b=await h.json();if(b.tld&&(SITE.tld=b.tld.startsWith(".")?b.tld:"."+b.tld),b.dns&&(SITE.dnsIp=b.dns.ip||"",SITE.dnsPort=b.dns.port||DC.DEFAULTS.DNS_PORT),b.dnsServers&&typeof b.dnsServers=="object")for(const[v,p]of Object.entries(b.dnsServers))v!=="__proto__"&&v!=="constructor"&&v!=="prototype"&&(SITE.dnsServers[v]=p);b.configurationType&&(SITE.configurationType=b.configurationType),b.domain&&(SITE.domain=b.domain),b.defaults&&(SITE.defaults=b.defaults),b.routingMode&&(SITE.routingMode=b.routingMode),SITE.onboardingCompleted=b.onboardingCompleted===!0,localStorage.setItem("dashcaddy_site_config",JSON.stringify({tld:SITE.tld,configurationType:SITE.configurationType,domain:SITE.domain,routingMode:SITE.routingMode})),renderDnsCards();const g=document.getElementById("manage-tokens");g&&(g.style.display=Object.keys(SITE.dnsServers).length?"":"none")}}catch{}document.querySelectorAll("[data-tld]").forEach(h=>h.textContent=SITE.tld);const o=document.getElementById("edit-tld-suffix");o&&(o.textContent=SITE.tld);const m=document.getElementById("external-proxy-ip");m&&SITE.dnsIp&&(m.value=SITE.dnsIp,m.placeholder=SITE.dnsIp)})();function buildDomain(u){return u+SITE.tld}function buildServiceUrl(u){return SITE.routingMode==="subdirectory"&&SITE.domain?"https://"+SITE.domain+"/"+u:SITE.configurationType==="public"&&SITE.domain?"https://"+u+"."+SITE.domain:"https://"+buildDomain(u)}function getDnsServerAddr(u){const o=SITE.dnsServers[u];return o?`${o.ip}:${o.port}`:buildDomain(u)}function getPrimaryDnsId(){if(!SITE.dnsIp)return null;for(const[u,o]of Object.entries(SITE.dnsServers))if(o.ip===SITE.dnsIp)return u;return null}function renderDnsCards(){const u=document.querySelector(".top");if(!u)return;const o=Object.keys(SITE.dnsServers);if(!o.length)return;const m='<svg viewBox="0 0 24 24" class="service-icon"><rect x="3" y="4" width="18" height="16" rx="2" fill="#34495e"/><rect x="5" y="6" width="14" height="2" rx="1" fill="#3498db"/><rect x="5" y="9" width="10" height="1" fill="#ecf0f1"/><rect x="5" y="11" width="12" height="1" fill="#ecf0f1"/><rect x="5" y="13" width="8" height="1" fill="#ecf0f1"/><rect x="5" y="15" width="14" height="1" fill="#ecf0f1"/><circle cx="17" cy="11" r="2" fill="#e74c3c"/><path d="M17 9v4M15 11h4" stroke="white" stroke-width="1"/></svg>',h=u.firstElementChild;o.forEach(b=>{const g=escapeHtml(b),v=escapeHtml((SITE.dnsServers[b].name||b).toUpperCase()),p=document.createElement("div");p.className="card",p.setAttribute("data-app",b),p.setAttribute("data-status","off"),p.innerHTML=`<span id="${g}-dot" class="dot bad at-bl"></span><div class="row"><div class="logo-wrap">${m}</div><span class="name">${v}</span><span class="spacer"></span><span id="${g}-pill" class="badge off" data-i18n-live-status="binary">OFF</span></div><div class="response-row"><span id="${g}-time" class="response-time">--</span></div><div class="health-row" id="health-${g}"><span id="uptime-${g}" class="uptime-chip">--</span><div class="uptime-mini-bar"><div class="fill" id="uptime-bar-${g}" style="width: 0%"></div></div></div><div class="btn-row"><button id="${g}-restart" class="restart-btn" data-i18n="action.restart">Restart</button><button id="${g}-update" class="update-btn" title="Update DNS server">\u2B06\uFE0F</button><button id="${g}-open" data-i18n="action.open">OpLine truncated
|
|
|
|
|
position: fixed; top: 20px; right: 20px;
|
|
|
|
|
background: ${g.bg}; color: ${g.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(v),m>0&&setTimeout(()=>v.remove(),m)}function timeAgo(c){const a=Date.now()-new Date(c).getTime();return a<6e4?"just now":a<36e5?Math.floor(a/6e4)+"m ago":a<864e5?Math.floor(a/36e5)+"h ago":Math.floor(a/864e5)+"d ago"}function safeGet(c,a=null){try{const m=localStorage.getItem(c);return m!==null?m:a}catch{return a}}function safeSet(c,a){try{localStorage.setItem(c,a)}catch{}}function safeRemove(c){try{localStorage.removeItem(c)}catch{}}function safeSessionGet(c,a=null){try{const m=sessionStorage.getItem(c);return m!==null?m:a}catch{return a}}function safeSessionSet(c,a){try{sessionStorage.setItem(c,a)}catch{}}function safeGetJSON(c,a=null){try{const m=localStorage.getItem(c);return m?JSON.parse(m):a}catch{return a}}function escapeHtml(c){return String(c??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}function injectModal(c,a){document.getElementById(c)||document.body.insertAdjacentHTML("beforeend",a)}const DC_BUS={_handlers:{},on(c,a){var m;((m=this._handlers)[c]||(m[c]=[])).push(a)},off(c,a){this._handlers[c]=this._handlers[c]?.filter(m=>m!==a)},emit(c,a){this._handlers[c]?.forEach(m=>m(a))}},AppState={_apps:[],getApps(){return this._apps},setApps(c){this._apps=c,window.APPS=c,DC_BUS.emit("apps:changed",c)},findApp(c){return this._apps.find(a=>a.id===c)},addApp(c){this._apps.push(c),window.APPS=this._apps,DC_BUS.emit("apps:changed",this._apps)},removeApp(c){const a=this._apps.findIndex(m=>m.id===c);return a>-1&&(this._apps.splice(a,1),window.APPS=this._apps,DC_BUS.emit("apps:changed",this._apps)),a>-1},updateApp(c,a){const m=this._apps.find(h=>h.id===c);if(m){for(const[h,b]of Object.entries(a))h!=="__proto__"&&h!=="constructor"&&h!=="prototype"&&(m[h]=b);DC_BUS.emit("apps:changed",this._apps)}return m}};(function(){function c(){const h=document.createElement("div");return h.className="skeleton-card",h.innerHTML='<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px"><div class="skeleton-bar" style="width:48px;height:48px;border-radius:8px;flex-shrink:0"></div><div style="flex:1"><div class="skeleton-bar" style="width:60%;height:14px;margin-bottom:6px"></div><div class="skeleton-bar" style="width:35%;height:10px"></div></div><div class="skeleton-bar" style="width:42px;height:22px;border-radius:11px"></div></div><div class="skeleton-bar" style="width:45%;height:12px;margin-bottom:10px"></div><div style="display:flex;gap:8px;margin-top:auto"><div class="skeleton-bar" style="width:64px;height:28px;border-radius:8px"></div><div class="skeleton-bar" style="width:64px;height:28px;border-radius:8px"></div></div>',h}function a(h){const b=document.getElementById("cards");if(!(!b||b.querySelector(".card"))){h=h||6;for(let g=0;g<h;g++){const v=c();b.appendChild(v),setTimeout(function(){v.classList.add("loaded")},g*60)}}}function m(){const h=document.getElementById("cards");if(h){var b=h.querySelectorAll(".skeleton-card");b.length&&(b.forEach(function(g,v){setTimeout(function(){g.style.opacity="0",g.style.transform="translateY(-10px)"},v*25)}),setTimeout(function(){b.forEach(function(g){g.parentNode&&g.remove()})},b.length*25+300))}}window.SkeletonLoader={show:a,hide:m}})(),(function(){var c="theme",a="user-themes",m="custom-theme",h=["dark","light","blue","black","nord","dracula","solarized-dark","solarized-light","taxi","ocean"],b=h.slice(),g=["bg","fg","muted","fg-muted","card-base","card-bg","border","hover","card-hover","base","ok-bg","ok-fg","bad-bg","bad-fg","dot-ok","dot-bad","uptime","success","error","warning","accent","accent-strong"],v=["bg","fg","muted","card-base","card-bg","border","ok-bg","ok-fg","bad-bg","bad-fg","dot-ok","dot-bad","uptime","accent","accent-strong"],p=["fg-muted","hover","card-hover","base","success","error","warning"],d={dark:{bg:"#0b0f1a",fg:"#e8ecf5",muted:"#9aa6bf","fg-muted":"#6b7a94","card-base":"#121826","card-bg":"#121826",border:"#263552",hover:"#1a2235","card-hover":"#161e2e",base:"#151c2b","ok-bg":"#0c2430","ok-fg":"#7ef2ff","bad-bg":"#2a121a","bad-fg":"#ff9aa3","dot-ok":"#35d1ff","dot-bad":"#ff5f7a",uptime:"#35d1ff",success:"#4caf50",error:"#e74c3c",warning:"#f39c12",accent:"#8FD6FF","accent-strong":"#1F7BFF"},light:{bg:"#f6f7fb",fg:"#0f1115",muted:"#5f6b7a","fg-muted":"#8993a4","card-base":"#ffffff","card-bg":"#ffffff",border:"#e2e7ef",hover:"#eef1f6","card-hover":"#f5f6fa",base:"#ebeef3","ok-bg":"#eafff1","ok-fg":"#0a7c3a","bad-bg":"#ffefef","bad-fg":"#b00020","dot-ok":"#0fb15a","dot-bad":"#d93b3b",uptime:"#0fb15a",success:"#0a7c3a",error:"#b00020",warning:"#d68a00",accent:"#4a90d9","accent-strong":"#2563eb",lightBg:!0},blue:{bg:"#1908AC",fg:"#e8f1ff",muted:"#d6e2ff","fg-muted":"#9eafdb","card-base":"#0d1533","card-bg":"#0d1533",border:"#1c2d6a",hover:"#141f4a","card-hover":"#111a3e",base:"#0f1840","ok-bg":"#162040","ok-fg":"#edffff","bad-bg":"#0a0e24","bad-fg":"#ffb3c0","dot-ok":"#c7e5ff","dot-bad":"#ffd6dc",uptime:"#7ec8ff",success:"#7ec8ff",error:"#ffb3c0",
Line truncated
|
|
|
|
|
`,document.body.appendChild(v),m>0&&setTimeout(()=>v.remove(),m)}function timeAgo(u){const o=Date.now()-new Date(u).getTime();return o<6e4?"just now":o<36e5?Math.floor(o/6e4)+"m ago":o<864e5?Math.floor(o/36e5)+"h ago":Math.floor(o/864e5)+"d ago"}function safeGet(u,o=null){try{const m=localStorage.getItem(u);return m!==null?m:o}catch{return o}}function safeSet(u,o){try{localStorage.setItem(u,o)}catch{}}function safeRemove(u){try{localStorage.removeItem(u)}catch{}}function safeSessionGet(u,o=null){try{const m=sessionStorage.getItem(u);return m!==null?m:o}catch{return o}}function safeSessionSet(u,o){try{sessionStorage.setItem(u,o)}catch{}}function safeGetJSON(u,o=null){try{const m=localStorage.getItem(u);return m?JSON.parse(m):o}catch{return o}}function escapeHtml(u){return String(u??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}function injectModal(u,o){document.getElementById(u)||document.body.insertAdjacentHTML("beforeend",o)}const DC_BUS={_handlers:{},on(u,o){var m;((m=this._handlers)[u]||(m[u]=[])).push(o)},off(u,o){this._handlers[u]=this._handlers[u]?.filter(m=>m!==o)},emit(u,o){this._handlers[u]?.forEach(m=>m(o))}},AppState={_apps:[],getApps(){return this._apps},setApps(u){this._apps=u,window.APPS=u,DC_BUS.emit("apps:changed",u)},findApp(u){return this._apps.find(o=>o.id===u)},addApp(u){this._apps.push(u),window.APPS=this._apps,DC_BUS.emit("apps:changed",this._apps)},removeApp(u){const o=this._apps.findIndex(m=>m.id===u);return o>-1&&(this._apps.splice(o,1),window.APPS=this._apps,DC_BUS.emit("apps:changed",this._apps)),o>-1},updateApp(u,o){const m=this._apps.find(h=>h.id===u);if(m){for(const[h,b]of Object.entries(o))h!=="__proto__"&&h!=="constructor"&&h!=="prototype"&&(m[h]=b);DC_BUS.emit("apps:changed",this._apps)}return m}};(function(){function u(){const h=document.createElement("div");return h.className="skeleton-card",h.innerHTML='<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px"><div class="skeleton-bar" style="width:48px;height:48px;border-radius:8px;flex-shrink:0"></div><div style="flex:1"><div class="skeleton-bar" style="width:60%;height:14px;margin-bottom:6px"></div><div class="skeleton-bar" style="width:35%;height:10px"></div></div><div class="skeleton-bar" style="width:42px;height:22px;border-radius:11px"></div></div><div class="skeleton-bar" style="width:45%;height:12px;margin-bottom:10px"></div><div style="display:flex;gap:8px;margin-top:auto"><div class="skeleton-bar" style="width:64px;height:28px;border-radius:8px"></div><div class="skeleton-bar" style="width:64px;height:28px;border-radius:8px"></div></div>',h}function o(h){const b=document.getElementById("cards");if(!(!b||b.querySelector(".card"))){h=h||6;for(let g=0;g<h;g++){const v=u();b.appendChild(v),setTimeout(function(){v.classList.add("loaded")},g*60)}}}function m(){const h=document.getElementById("cards");if(h){var b=h.querySelectorAll(".skeleton-card");b.length&&(b.forEach(function(g,v){setTimeout(function(){g.style.opacity="0",g.style.transform="translateY(-10px)"},v*25)}),setTimeout(function(){b.forEach(function(g){g.parentNode&&g.remove()})},b.length*25+300))}}window.SkeletonLoader={show:o,hide:m}})(),(function(){var u="theme",o="user-themes",m="custom-theme",h=["dark","light","blue","black","nord","dracula","solarized-dark","solarized-light","taxi","ocean"],b=h.slice(),g=["bg","fg","muted","fg-muted","card-base","card-bg","border","hover","card-hover","base","ok-bg","ok-fg","bad-bg","bad-fg","dot-ok","dot-bad","uptime","success","error","warning","accent","accent-strong"],v=["bg","fg","muted","card-base","card-bg","border","ok-bg","ok-fg","bad-bg","bad-fg","dot-ok","dot-bad","uptime","accent","accent-strong"],p=["fg-muted","hover","card-hover","base","success","error","warning"],c={dark:{bg:"#0b0f1a",fg:"#e8ecf5",muted:"#9aa6bf","fg-muted":"#6b7a94","card-base":"#121826","card-bg":"#121826",border:"#263552",hover:"#1a2235","card-hover":"#161e2e",base:"#151c2b","ok-bg":"#0c2430","ok-fg":"#7ef2ff","bad-bg":"#2a121a","bad-fg":"#ff9aa3","dot-ok":"#35d1ff","dot-bad":"#ff5f7a",uptime:"#35d1ff",success:"#4caf50",error:"#e74c3c",warning:"#f39c12",accent:"#8FD6FF","accent-strong":"#1F7BFF"},light:{bg:"#f6f7fb",fg:"#0f1115",muted:"#5f6b7a","fg-muted":"#8993a4","card-base":"#ffffff","card-bg":"#ffffff",border:"#e2e7ef",hover:"#eef1f6","card-hover":"#f5f6fa",base:"#ebeef3","ok-bg":"#eafff1","ok-fg":"#0a7c3a","bad-bg":"#ffefef","bad-fg":"#b00020","dot-ok":"#0fb15a","dot-bad":"#d93b3b",uptime:"#0fb15a",success:"#0a7c3a",error:"#b00020",warning:"#d68a00",accent:"#4a90d9","accent-strong":"#2563eb",lightBg:!0},blue:{bg:"#1908AC",fg:"#e8f1ff",muted:"#d6e2ff","fg-muted":"#9eafdb","card-base":"#0d1533","card-bg":"#0d1533",border:"#1c2d6a",hover:"#141f4a","card-hover":"#111a3e",base:"#0f1840","ok-bg":"#162040","ok-fg":"#edffff","bad-bg":"#0a0e24","bad-fg":"#ffb3c0","dot-ok":"#c7e5ff","dot-bad":"#ffd6dc",uptime:"#7ec8ff",success:"#7ec8ff",error:"#ffb3c0",
Line truncated
|
|
|
|
|
background:
|
|
|
|
|
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%),
|
|
|
|
|
radial-gradient(1200px 800px at 10% -10%, rgba(`+k.r+","+k.g+","+k.b+`, .08), transparent 60%),
|
|
|
|
|
radial-gradient(1000px 700px at 110% 10%, rgba(`+k.r+","+k.g+","+k.b+`, .05), transparent 55%),
|
|
|
|
|
var(--bg);
|
|
|
|
|
}
|
|
|
|
|
`:":root."+x+` body {
|
|
|
|
|
`:":root."+w+` body {
|
|
|
|
|
background:
|
|
|
|
|
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%),
|
|
|
|
|
radial-gradient(1200px 900px at 8% -12%, rgba(`+k.r+","+k.g+","+k.b+`, .10), transparent 60%),
|
|
|
|
|
radial-gradient(1000px 700px at 110% -10%, rgba(`+k.r+","+k.g+","+k.b+`, .07), transparent 55%),
|
|
|
|
|
var(--bg);
|
|
|
|
|
}
|
|
|
|
|
`}function o(x,B){var A=B.lightBg||B.bg&&s(B.bg)>.4;return A?":root."+x+` button:hover {
|
|
|
|
|
`}function t(w,L){var A=L.lightBg||L.bg&&r(L.bg)>.4;return A?":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."+x+` 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 u(){return window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}function n(){g.forEach(function(x){document.documentElement.style.removeProperty("--"+x)})}function i(x,B){var A=x.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"");A||(A="custom"),h.indexOf(A)!==-1&&(A=A+"-custom");for(var E=safeGetJSON(a,{}),C=A,T=2;E[A]&&A!==B;)A=C+"-"+T++;return A}function l(x){var B=document.getElementById("user-theme-styles");B&&B.remove(),b.length=h.length,Object.keys(d).forEach(function($){h.indexOf($)===-1&&delete d[$]});var A=x||safeGetJSON(a,{}),E=Object.keys(A);if(E=E.filter(function($){return h.indexOf($)===-1}),!!E.length){var C="";E.forEach(function($){var I=A[$];b.indexOf($)===-1&&b.push($);var P={};g.forEach(function(O){I[O]&&(P[O]=I[O])}),P["card-bg"]=I["card-base"]||I.bg,I.lightBg&&(P.lightBg=!0);var D=t(P);p.forEach(function(O){!P[O]&&D[O]&&(P[O]=D[O])}),d[$]=P,C+=":root."+$+` {
|
|
|
|
|
`,g.forEach(function(O){P[O]&&(C+=" --"+O+": "+P[O]+`;
|
|
|
|
|
`)}),C+=`}
|
|
|
|
|
`,C+=e($,P),C+=o($,P)});var T=document.createElement("style");T.id="user-theme-styles",T.textContent=C,document.head.appendChild(T)}}function y(){secureFetch("/api/v1/themes").then(function(x){return x.json()}).then(function(x){if(!(!x.success||!x.themes)){var B=x.themes,A=safeGetJSON(a,{});if(JSON.stringify(B)!==JSON.stringify(A)){safeSet(a,JSON.stringify(B)),l(B);var E=safeGet(c);E&&b.indexOf(E)!==-1&&k(E)}}}).catch(function(){})}function S(){var x=safeGetJSON(m);if(x){var B=x.name||"Custom",A=i(B),E={name:B};g.forEach(function($){x[$]&&(E[$]=x[$])});var C=safeGetJSON(a,{});C[A]=E,safeSet(a,JSON.stringify(C)),safeGet(c)==="custom"&&safeSet(c,A),safeRemove(m);var T={};g.forEach(function($){E[$]&&(T[$]=E[$])}),fetch("/api/v1/themes/"+A,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:B,colors:T})}).catch(function(){})}}function k(x){document.documentElement.classList.add("theme-transitioning"),b.forEach(function(C){C!=="dark"&&document.documentElement.classList.remove(C)}),n(),x!=="dark"&&document.documentElement.classList.add(x),safeSet(c,x);var B=d[x],A=document.querySelector('meta[name="theme-color"]');A&&B&&A.setAttribute("content",B.bg);var E=B&&B.lightBg;!E&&B&&B.bg&&(E=s(B.bg)>.4),E?document.documentElement.classList.add("light-bg"):document.documentElement.classList.remove("light-bg"),setTimeout(function(){document.documentElement.classList.remove("theme-transitioning")},300)}S(),l();var L=safeGet(c);L==="red"&&(L="black",safeSet(c,"black")),L&&L!=="dark"&&b.indexOf(L)===-1&&(L=null),k(L||u()),y(),window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",function(x){safeGet(c)||k(x.matches?"dark":"light")}),window.THEMES=b,window.BUILTIN_THEMES=h,window.THEME_COLORS=d,window.THEME_PROPS=g,window.BASE_PROPS=v,window.DERIVED_PROPS=p,window.USER_THEMES_KEY=a,window.applyTheme=k,window.clearCustomProperties=n,window.injectUserThemeStyles=l,window.syncThemesFromServer=y,window.slugifyThemeName=i,window.getActiveTheme=function(){return safeGet(c)||u()},window.deriveExtendedColors=t,window.hexToRgb=r,window.rgbToHex=f,window.blendColors=w})(),(function(){let c=null;async function a(){if(c)return c;try{const w=await fetch("/api/v1/auth/login/methods",{cache:"no-store"});if(!w.ok)throw new Error(`methods HTTP ${w.status}`);const s=await w.json();return c=Array.isArray(s.providers)?s.providers:[],c}catch(w){return console.warn("[auth-gate] methods fetch failed; falling back to TOTP-only",w),[]}}function m(w){const s=document.getElementById("totp-overlay");if(!s)return;const t=s.querySelector(".totp-card");if(!t)return;const e=t.innerHTML;t.dataset.originalBody||(t.dataset.originalBody=e);const o=w.map(u=>{const n=u.config&&(u.config.label||u.name)||u.name;return`<button class="provider-btn" data-provider="${u.name}"
|
|
|
|
|
`}function i(){return window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}function n(){g.forEach(function(w){document.documentElement.style.removeProperty("--"+w)})}function d(w,L){var A=w.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"");A||(A="custom"),h.indexOf(A)!==-1&&(A=A+"-custom");for(var E=safeGetJSON(o,{}),k=A,B=2;E[A]&&A!==L;)A=k+"-"+B++;return A}function l(w){var L=document.getElementById("user-theme-styles");L&&L.remove(),b.length=h.length,Object.keys(c).forEach(function($){h.indexOf($)===-1&&delete c[$]});var A=w||safeGetJSON(o,{}),E=Object.keys(A);if(E=E.filter(function($){return h.indexOf($)===-1}),!!E.length){var k="";E.forEach(function($){var I=A[$];b.indexOf($)===-1&&b.push($);var P={};g.forEach(function(O){I[O]&&(P[O]=I[O])}),P["card-bg"]=I["card-base"]||I.bg,I.lightBg&&(P.lightBg=!0);var D=a(P);p.forEach(function(O){!P[O]&&D[O]&&(P[O]=D[O])}),c[$]=P,k+=":root."+$+` {
|
|
|
|
|
`,g.forEach(function(O){P[O]&&(k+=" --"+O+": "+P[O]+`;
|
|
|
|
|
`)}),k+=`}
|
|
|
|
|
`,k+=e($,P),k+=t($,P)});var B=document.createElement("style");B.id="user-theme-styles",B.textContent=k,document.head.appendChild(B)}}function f(){secureFetch("/api/v1/themes").then(function(w){return w.json()}).then(function(w){if(!(!w.success||!w.themes)){var L=w.themes,A=safeGetJSON(o,{});if(JSON.stringify(L)!==JSON.stringify(A)){safeSet(o,JSON.stringify(L)),l(L);var E=safeGet(u);E&&b.indexOf(E)!==-1&&S(E)}}}).catch(function(){})}function C(){var w=safeGetJSON(m);if(w){var L=w.name||"Custom",A=d(L),E={name:L};g.forEach(function($){w[$]&&(E[$]=w[$])});var k=safeGetJSON(o,{});k[A]=E,safeSet(o,JSON.stringify(k)),safeGet(u)==="custom"&&safeSet(u,A),safeRemove(m);var B={};g.forEach(function($){E[$]&&(B[$]=E[$])}),fetch("/api/v1/themes/"+A,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:L,colors:B})}).catch(function(){})}}function S(w){document.documentElement.classList.add("theme-transitioning"),b.forEach(function(k){k!=="dark"&&document.documentElement.classList.remove(k)}),n(),w!=="dark"&&document.documentElement.classList.add(w),safeSet(u,w);var L=c[w],A=document.querySelector('meta[name="theme-color"]');A&&L&&A.setAttribute("content",L.bg);var E=L&&L.lightBg;!E&&L&&L.bg&&(E=r(L.bg)>.4),E?document.documentElement.classList.add("light-bg"):document.documentElement.classList.remove("light-bg"),setTimeout(function(){document.documentElement.classList.remove("theme-transitioning")},300)}C(),l();var T=safeGet(u);T==="red"&&(T="black",safeSet(u,"black")),T&&T!=="dark"&&b.indexOf(T)===-1&&(T=null),S(T||i()),f(),window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",function(w){safeGet(u)||S(w.matches?"dark":"light")}),window.THEMES=b,window.BUILTIN_THEMES=h,window.THEME_COLORS=c,window.THEME_PROPS=g,window.BASE_PROPS=v,window.DERIVED_PROPS=p,window.USER_THEMES_KEY=o,window.applyTheme=S,window.clearCustomProperties=n,window.injectUserThemeStyles=l,window.syncThemesFromServer=f,window.slugifyThemeName=d,window.getActiveTheme=function(){return safeGet(u)||i()},window.deriveExtendedColors=a,window.hexToRgb=s,window.rgbToHex=y,window.blendColors=x})(),(function(){let u=null;async function o(){if(u)return u;try{const x=await fetch("/api/v1/auth/login/methods",{cache:"no-store"});if(!x.ok)throw new Error(`methods HTTP ${x.status}`);const r=await x.json();return u=Array.isArray(r.providers)?r.providers:[],u}catch(x){return console.warn("[auth-gate] methods fetch failed; falling back to TOTP-only",x),[]}}function m(x){const r=document.getElementById("totp-overlay");if(!r)return;const a=r.querySelector(".totp-card");if(!a)return;const e=a.innerHTML;a.dataset.originalBody||(a.dataset.originalBody=e);const t=x.map(i=>{const n=i.config&&(i.config.label||i.name)||i.name;return`<button class="provider-btn" data-provider="${i.name}"
|
|
|
|
|
style="margin: 6px 4px; padding: 10px 16px; background: var(--accent); color: white; border: 0; border-radius: 6px; cursor: pointer; font-size: 0.95rem;">
|
|
|
|
|
Sign in with ${n}
|
|
|
|
|
</button>`}).join(`
|
|
|
|
|
`);t.innerHTML=`
|
|
|
|
|
`);a.innerHTML=`
|
|
|
|
|
<img class="totp-logo totp-logo-dark" src="/assets/dashcaddy-logo-dark.png" alt="DashCaddy" onerror="this.style.display='none'">
|
|
|
|
|
<img class="totp-logo totp-logo-light" src="/assets/dashcaddy-logo-light.png" alt="DashCaddy" onerror="this.style.display='none'">
|
|
|
|
|
<p class="subtitle">Choose how to sign in</p>
|
|
|
|
|
<div id="auth-gate-providers" style="margin: 14px 0 10px;">${o}</div>
|
|
|
|
|
<div id="auth-gate-providers" style="margin: 14px 0 10px;">${t}</div>
|
|
|
|
|
<div id="auth-gate-message" style="margin-top: 8px; color: var(--muted); font-size: 0.8rem;"></div>
|
|
|
|
|
`,s.classList.add("show"),t.querySelectorAll(".provider-btn").forEach(u=>{u.addEventListener("click",()=>{const n=u.dataset.provider,i=w.find(l=>l.name===n);b(i)})})}function h(){const w=document.getElementById("totp-overlay");if(!w)return;const s=w.querySelector(".totp-card");!s||!s.dataset.originalBody||(s.innerHTML=s.dataset.originalBody,window.location.reload())}function b(w){const s=document.getElementById("totp-overlay");if(!s)return;const t=s.querySelector(".totp-card");if(t){if(w.name==="totp"){window.location.reload();return}if(w.name==="email"){t.innerHTML=`
|
|
|
|
|
`,r.classList.add("show"),a.querySelectorAll(".provider-btn").forEach(i=>{i.addEventListener("click",()=>{const n=i.dataset.provider,d=x.find(l=>l.name===n);b(d)})})}function h(){const x=document.getElementById("totp-overlay");if(!x)return;const r=x.querySelector(".totp-card");!r||!r.dataset.originalBody||(r.innerHTML=r.dataset.originalBody,window.location.reload())}function b(x){const r=document.getElementById("totp-overlay");if(!r)return;const a=r.querySelector(".totp-card");if(a){if(x.name==="totp"){window.location.reload();return}if(x.name==="email"){a.innerHTML=`
|
|
|
|
|
<img class="totp-logo totp-logo-dark" src="/assets/dashcaddy-logo-dark.png" alt="DashCaddy" onerror="this.style.display='none'">
|
|
|
|
|
<img class="totp-logo totp-logo-light" src="/assets/dashcaddy-logo-light.png" alt="DashCaddy" onerror="this.style.display='none'">
|
|
|
|
|
<p class="subtitle">Sign in with email</p>
|
|
|
|
@@ -75,13 +75,13 @@
|
|
|
|
|
<div style="margin-top: 16px;">
|
|
|
|
|
<a href="#" id="auth-gate-back" style="color: var(--muted); font-size: 0.8rem;">\u2190 Back</a>
|
|
|
|
|
</div>
|
|
|
|
|
`,s.classList.add("show");const e=t.querySelector("#auth-gate-email-input"),o=t.querySelector("#auth-gate-email-submit"),u=t.querySelector("#auth-gate-email-status"),n=t.querySelector("#auth-gate-back");o.addEventListener("click",async()=>{const i=(e.value||"").trim();if(!i||!i.includes("@")){u.textContent="Enter a valid email address.",u.style.color="var(--error, #d33)";return}o.disabled=!0,u.textContent="Sending\u2026",u.style.color="";try{const y=await(await fetch("/api/v1/auth/login/email/initiate",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:i})})).json();if(y.success){const S=y.deliveredVia||"email",k=y.maskedEmail||i;S==="dev-console"?u.innerHTML=`
|
|
|
|
|
`,r.classList.add("show");const e=a.querySelector("#auth-gate-email-input"),t=a.querySelector("#auth-gate-email-submit"),i=a.querySelector("#auth-gate-email-status"),n=a.querySelector("#auth-gate-back");t.addEventListener("click",async()=>{const d=(e.value||"").trim();if(!d||!d.includes("@")){i.textContent="Enter a valid email address.",i.style.color="var(--error, #d33)";return}t.disabled=!0,i.textContent="Sending\u2026",i.style.color="";try{const f=await(await fetch("/api/v1/auth/login/email/initiate",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:d})})).json();if(f.success){const C=f.deliveredVia||"email",S=f.maskedEmail||d;C==="dev-console"?i.innerHTML=`
|
|
|
|
|
Check the server logs for your one-time link.
|
|
|
|
|
(Dev mode: no SMTP configured. In production this would email <strong>${k}</strong>.)`:u.innerHTML=`Sign-in link sent to <strong>${k}</strong>.
|
|
|
|
|
Check your inbox (and spam folder).`,u.style.color="var(--success, #2a7)"}else u.textContent=y.error||"Could not send link.",u.style.color="var(--error, #d33)",o.disabled=!1}catch{u.textContent="Connection error. Try again.",u.style.color="var(--error, #d33)",o.disabled=!1}}),e.addEventListener("keydown",i=>{i.key==="Enter"&&o.click()}),n.addEventListener("click",i=>{i.preventDefault(),m(providers)});return}console.warn("[auth-gate] unknown provider",w.name),window.location.reload()}}async function g(){if(!document.getElementById("totp-overlay"))return;const s=await a();if(s.length===0){typeof window._showTotpOverlay=="function"&&window._showTotpOverlay();return}if(s.length===1&&s[0].name==="totp"){v(s[0]);return}m(s)}function v(w){const s=c&&c.find(u=>u.name==="email");if(!s){typeof window._showTotpOverlay=="function"&&window._showTotpOverlay();return}const t=document.getElementById("totp-overlay"),e=t.querySelector(".totp-card");if(!e)return;if(e.dataset.originalBody||(e.dataset.originalBody=e.innerHTML),!e.querySelector("#auth-gate-email-alt")){const u=document.createElement("div");u.id="auth-gate-email-alt",u.style.cssText="margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--border); font-size: 0.85rem;",u.innerHTML=`<a href="#" id="auth-gate-email-alt-link"
|
|
|
|
|
(Dev mode: no SMTP configured. In production this would email <strong>${S}</strong>.)`:i.innerHTML=`Sign-in link sent to <strong>${S}</strong>.
|
|
|
|
|
Check your inbox (and spam folder).`,i.style.color="var(--success, #2a7)"}else i.textContent=f.error||"Could not send link.",i.style.color="var(--error, #d33)",t.disabled=!1}catch{i.textContent="Connection error. Try again.",i.style.color="var(--error, #d33)",t.disabled=!1}}),e.addEventListener("keydown",d=>{d.key==="Enter"&&t.click()}),n.addEventListener("click",d=>{d.preventDefault(),m(providers)});return}console.warn("[auth-gate] unknown provider",x.name),window.location.reload()}}async function g(){if(!document.getElementById("totp-overlay"))return;const r=await o();if(r.length===0){typeof window._showTotpOverlay=="function"&&window._showTotpOverlay();return}if(r.length===1&&r[0].name==="totp"){v(r[0]);return}m(r)}function v(x){const r=u&&u.find(i=>i.name==="email");if(!r){typeof window._showTotpOverlay=="function"&&window._showTotpOverlay();return}const a=document.getElementById("totp-overlay"),e=a.querySelector(".totp-card");if(!e)return;if(e.dataset.originalBody||(e.dataset.originalBody=e.innerHTML),!e.querySelector("#auth-gate-email-alt")){const i=document.createElement("div");i.id="auth-gate-email-alt",i.style.cssText="margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--border); font-size: 0.85rem;",i.innerHTML=`<a href="#" id="auth-gate-email-alt-link"
|
|
|
|
|
style="color: var(--accent); text-decoration: none;">
|
|
|
|
|
Or sign in with email instead \u2192
|
|
|
|
|
</a>`,e.appendChild(u),u.querySelector("#auth-gate-email-alt-link").addEventListener("click",n=>{n.preventDefault(),b(s)})}t.classList.add("show")}window.__dc_049_handled=!0;function p(w){try{const s=new URL(w,window.location.origin);if(!["http:","https:"].includes(s.protocol))return!1;if(s.origin===window.location.origin)return!0;if(s.protocol!=="https:")return!1;const t=SITE.tld.startsWith(".")?SITE.tld:`.${SITE.tld}`;return s.hostname===t.slice(1)||s.hostname.endsWith(t)}catch{return!1}}function d(w,s){const t=new URL(w,window.location.origin);if(t.origin===window.location.origin)return t.toString();const e=SITE.tld.startsWith(".")?SITE.tld:`.${SITE.tld}`,o=t.hostname===e.slice(1)||t.hostname.endsWith(e);if(t.protocol!=="https:"||!o||!s)return null;const u=`${t.pathname}${t.search}${t.hash}`;return t.pathname="/dashcaddy-sso",t.search="",t.hash="",t.searchParams.set("token",s),t.searchParams.set("return",u),t.toString()}async function r(w){if(!w||!p(w))return!1;try{const s=new URL(w,window.location.origin),t=SITE.tld.startsWith(".")?SITE.tld:`.${SITE.tld}`,e=s.hostname.slice(0,-t.length);if(!/^[a-z0-9][a-z0-9-]*$/.test(e))return!1;const o=await fetch(`/api/v1/auth/sso-handoff?serviceId=${encodeURIComponent(e)}`,{credentials:"include",cache:"no-store"});if(!o.ok)return!1;const u=await o.json(),n=u.success&&d(w,u.ssoToken);if(!n)return!1;try{sessionStorage.removeItem("totp_redirect")}catch{}return window.location.replace(n),!0}catch{return!1}}const f=new URLSearchParams(window.location.search);if(f.get("auth")==="required"){const w=f.get("return");if(w&&p(w))try{sessionStorage.setItem("totp_redirect",w)}catch{}window.history.replaceState({},"",window.location.pathname),setTimeout(async()=>{await r(w)||await g()},0)}window._showAuthGate=g,window._authGateMethods=a})(),(function(){function c(){const p=document.querySelector(".totp-card");if(!p)return;const r=getComputedStyle(p).backgroundColor.match(/\d+/g);if(!r)return;const f=(.299*+r[0]+.587*+r[1]+.114*+r[2])/255,w=p.querySelector(".totp-logo-dark"),s=p.querySelector(".totp-logo-light");w&&(w.style.display=f>.5?"none":""),s&&(s.style.display=f>.5?"":"none")}function a(){const p=document.getElementById("totp-overlay");if(p){p.classList.add("show"),setTimeout(c,50);const d=p.querySelector(".totp-digits input");d&&setTimeout(()=>d.focus(),100)}typeof window._refreshRecoveryLink=="function"&&window._refreshRecoveryLink()}function m(){const p=document.getElementById("totp-overlay");p&&p.classList.remove("show")}function h(p,d){const r=new URL(p,window.location.origin);if(r.origin===window.location.origin)return r.toString();const f=SITE.tld.startsWith(".")?SITE.tld:`.${SITE.tld}`,w=r.hostname===f.slice(1)||r.hostname.endsWith(f);if(r.protocol!=="https:"||!w)return null;if(!d)return r.toString();const s=`${r.pathname}${r.search}${r.hash}`;return r.pathname="/dashcaddy-sso",r.search="",r.hash="",r.searchParams.set("token",d),r.searchParams.set("return",s),r.toString()}const b=document.getElementById("totp-digits");if(b){const p=b.querySelectorAll("input");p.forEach((d,r)=>{d.addEventListener("input",f=>{const w=f.target.value.replace(/\D/g,"");f.target.value=w.slice(0,1),w&&r<p.length-1&&p[r+1].focus();const s=Array.from(p).map(t=>t.value).join("");s.length===6&&g(s)}),d.addEventListener("keydown",f=>{f.key==="Backspace"&&!f.target.value&&r>0&&(p[r-1].focus(),p[r-1].value="")}),d.addEventListener("paste",f=>{f.preventDefault();const w=(f.clipboardData.getData("text")||"").replace(/\D/g,"");w.length>=6&&(p.forEach((s,t)=>{s.value=w[t]||""}),p[5].focus(),g(w.slice(0,6)))})})}async function g(p){const d=document.getElementById("totp-error");d.textContent="Verifying...",d.className="totp-error verifying";const r=safeSessionGet("totp_redirect");let f=null;if(r)try{const w=new URL(r,window.location.origin),s=SITE.tld.startsWith(".")?SITE.tld:`.${SITE.tld}`,t=w.hostname.slice(0,-s.length);w.hostname.endsWith(s)&&/^[a-z0-9][a-z0-9-]*$/.test(t)&&(f=t)}catch{}try{const s=await(await secureFetch("/api/v1/totp/verify",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:p,serviceId:f})})).json();if(s.success){if(d.textContent="",s.csrfToken&&(csrfToken=s.csrfToken),m(),r){try{sessionStorage.removeItem("totp_redirect")}catch{}const t=h(r,s.ssoToken);if(!t)return;window.location.href=t;return}typeof window.initializeDashboard=="function"&&window.initializeDashboard(),window._dcAuthLost=!1,typeof window._sseResume=="function"?window._sseResume():typeof window._sseReconnect=="function"&&window._sseReconnect(),typeof window.refreshAll=="function"&&window.refreshAll()}else{d.textContent=s.error||"Invalid code",d.className="totp-error";const t=document.querySelectorAll("#totp-digits input");t.forEach(e=>{e.value=""}),t[0]?.focus()}}catch{d.textContent="Connection error",d.className="totp-error"}}if(!!!window.__dc_049_handled&&urlParams.get("auth")==="required"){const p=urlParams.get("return");if(p)try{const d=new URL(p,window.location.origin)Line truncated
|
|
|
|
|
</a>`,e.appendChild(i),i.querySelector("#auth-gate-email-alt-link").addEventListener("click",n=>{n.preventDefault(),b(r)})}a.classList.add("show")}window.__dc_049_handled=!0;function p(x){try{const r=new URL(x,window.location.origin);if(!["http:","https:"].includes(r.protocol))return!1;if(r.origin===window.location.origin)return!0;if(r.protocol!=="https:")return!1;const a=SITE.tld.startsWith(".")?SITE.tld:`.${SITE.tld}`;return r.hostname===a.slice(1)||r.hostname.endsWith(a)}catch{return!1}}function c(x,r){const a=new URL(x,window.location.origin);if(a.origin===window.location.origin)return a.toString();const e=SITE.tld.startsWith(".")?SITE.tld:`.${SITE.tld}`,t=a.hostname===e.slice(1)||a.hostname.endsWith(e);if(a.protocol!=="https:"||!t||!r)return null;const i=`${a.pathname}${a.search}${a.hash}`;return a.pathname="/dashcaddy-sso",a.search="",a.hash="",a.searchParams.set("token",r),a.searchParams.set("return",i),a.toString()}async function s(x){if(!x||!p(x))return!1;try{const r=new URL(x,window.location.origin),a=SITE.tld.startsWith(".")?SITE.tld:`.${SITE.tld}`,e=r.hostname.slice(0,-a.length);if(!/^[a-z0-9][a-z0-9-]*$/.test(e))return!1;const t=await fetch(`/api/v1/auth/sso-handoff?serviceId=${encodeURIComponent(e)}`,{credentials:"include",cache:"no-store"});if(!t.ok)return!1;const i=await t.json(),n=i.success&&c(x,i.ssoToken);if(!n)return!1;try{sessionStorage.removeItem("totp_redirect")}catch{}return window.location.replace(n),!0}catch{return!1}}const y=new URLSearchParams(window.location.search);if(y.get("auth")==="required"){const x=y.get("return");if(x&&p(x))try{sessionStorage.setItem("totp_redirect",x)}catch{}window.history.replaceState({},"",window.location.pathname),setTimeout(async()=>{await s(x)||await g()},0)}window._showAuthGate=g,window._authGateMethods=o})(),(function(){function u(){const p=document.querySelector(".totp-card");if(!p)return;const s=getComputedStyle(p).backgroundColor.match(/\d+/g);if(!s)return;const y=(.299*+s[0]+.587*+s[1]+.114*+s[2])/255,x=p.querySelector(".totp-logo-dark"),r=p.querySelector(".totp-logo-light");x&&(x.style.display=y>.5?"none":""),r&&(r.style.display=y>.5?"":"none")}function o(){const p=document.getElementById("totp-overlay");if(p){p.classList.add("show"),setTimeout(u,50);const c=p.querySelector(".totp-digits input");c&&setTimeout(()=>c.focus(),100)}typeof window._refreshRecoveryLink=="function"&&window._refreshRecoveryLink()}function m(){const p=document.getElementById("totp-overlay");p&&p.classList.remove("show")}function h(p,c){const s=new URL(p,window.location.origin);if(s.origin===window.location.origin)return s.toString();const y=SITE.tld.startsWith(".")?SITE.tld:`.${SITE.tld}`,x=s.hostname===y.slice(1)||s.hostname.endsWith(y);if(s.protocol!=="https:"||!x)return null;if(!c)return s.toString();const r=`${s.pathname}${s.search}${s.hash}`;return s.pathname="/dashcaddy-sso",s.search="",s.hash="",s.searchParams.set("token",c),s.searchParams.set("return",r),s.toString()}const b=document.getElementById("totp-digits");if(b){const p=b.querySelectorAll("input");p.forEach((c,s)=>{c.addEventListener("input",y=>{const x=y.target.value.replace(/\D/g,"");y.target.value=x.slice(0,1),x&&s<p.length-1&&p[s+1].focus();const r=Array.from(p).map(a=>a.value).join("");r.length===6&&g(r)}),c.addEventListener("keydown",y=>{y.key==="Backspace"&&!y.target.value&&s>0&&(p[s-1].focus(),p[s-1].value="")}),c.addEventListener("paste",y=>{y.preventDefault();const x=(y.clipboardData.getData("text")||"").replace(/\D/g,"");x.length>=6&&(p.forEach((r,a)=>{r.value=x[a]||""}),p[5].focus(),g(x.slice(0,6)))})})}async function g(p){const c=document.getElementById("totp-error");c.textContent="Verifying...",c.className="totp-error verifying";const s=safeSessionGet("totp_redirect");let y=null;if(s)try{const x=new URL(s,window.location.origin),r=SITE.tld.startsWith(".")?SITE.tld:`.${SITE.tld}`,a=x.hostname.slice(0,-r.length);x.hostname.endsWith(r)&&/^[a-z0-9][a-z0-9-]*$/.test(a)&&(y=a)}catch{}try{const r=await(await secureFetch("/api/v1/totp/verify",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:p,serviceId:y})})).json();if(r.success){if(c.textContent="",r.csrfToken&&(csrfToken=r.csrfToken),m(),s){try{sessionStorage.removeItem("totp_redirect")}catch{}const a=h(s,r.ssoToken);if(!a)return;window.location.href=a;return}typeof window.initializeDashboard=="function"&&window.initializeDashboard(),window._dcAuthLost=!1,typeof window._sseResume=="function"?window._sseResume():typeof window._sseReconnect=="function"&&window._sseReconnect(),typeof window.refreshAll=="function"&&window.refreshAll()}else{c.textContent=r.error||"Invalid code",c.className="totp-error";const a=document.querySelectorAll("#totp-digits input");a.forEach(e=>{e.value=""}),a[0]?.focus()}}catch{c.textContent="Connection error",c.className="totp-error"}}if(!!!window.__dc_049_handled&&urlParams.get("auth")==="required"){const p=urlParams.get("return");if(p)try{const c=new URL(p,window.location.origin)Line truncated
|
|
|
|
|
<div class="weather-modal-content" style="min-width: 500px; max-width: 700px; max-height: 80vh;">
|
|
|
|
|
<h3>\u{1F4C2} Browse for Media Folders</h3>
|
|
|
|
|
|
|
|
|
@@ -179,7 +179,7 @@
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>`);const a=document.getElementById("service-creds-modal");let m=null,h=null,b=!1;const g=["sonarr","radarr","prowlarr","overseerr"],v=["sonarr","radarr"];function p(e){return e.externalUrl||e.url||""}function d(e){const o=document.getElementById("svc-creds-error");o.textContent=e,o.style.display=""}function r(){const e=document.getElementById("svc-creds-error");e.textContent="",e.style.display="none"}async function f(e,o){if(!e)throw new Error(`${o} failed: no response`);let u;try{u=await e.json()}catch{throw new Error(`${o} failed: invalid server response`)}if(!e.ok||u?.success!==!0)throw new Error(u?.error||`${o} failed (${e.status||"unknown status"})`);return u}function w(e,o){return!!window.DCCredentialVault?.isAllowedReturnUrl(e,o)}window.openServiceCredsModal=async function(e,o={}){m=e,h=w(o.returnUrl,e.id)?o.returnUrl:null,b=!1,r();const u=document.getElementById("svc-creds-title"),n=document.getElementById("svc-creds-desc"),i=document.getElementById("svc-creds-seedhost"),l=document.getElementById("svc-creds-apikey"),y=document.getElementById("svc-creds-basic"),S=document.getElementById("svc-creds-quality");u.textContent=e.name+" \u2014 Encrypted Vault",document.getElementById("svc-creds-save").textContent=h?"Save to vault & open service":"Save to encrypted vault";const k=!!e.isExternal,L=g.includes(e.id)||g.includes(e.appTemplate),x=v.includes(e.id)||v.includes(e.appTemplate);i.style.display=k?"":"none",l.style.display=L?"":"none",S.style.display=x?"":"none",y.style.display=k?"none":"";const B=document.getElementById("svc-quality-select");B.innerHTML='<option value="">-- Enter API key first --</option>',document.getElementById("svc-quality-status").textContent="",k?(n.textContent="Seedhost credentials auto-login past the HTTP prompt. API key bypasses the app login.",document.getElementById("svc-seedhost-pass").placeholder=`Password for ${e.name}`):L?n.textContent="API key bypasses the app login screen automatically.":n.textContent="Credentials are injected automatically when accessing this service.",await s(e),a.classList.add("show")};async function s(e){const o=document.getElementById("svc-creds-dot"),u=document.getElementById("svc-creds-status"),n=document.getElementById("svc-creds-clear");let i=!1;if(e.isExternal){try{const S=await(await fetch(`/api/v1/seedhost-creds?serviceId=${e.id}`)).json();S.success?(document.getElementById("svc-seedhost-user").value=S.username||"",S.hasCredentials&&(i=!0)):document.getElementById("svc-seedhost-user").value=""}catch{}document.getElementById("svc-seedhost-pass").value=""}try{const S=await(await fetch(`/api/v1/services/${e.id}/credentials`)).json();S.success&&(S.hasApiKey?(document.getElementById("svc-apikey-input").value="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",i=!0):document.getElementById("svc-apikey-input").value="",S.hasBasicAuth&&!e.isExternal?(document.getElementById("svc-basic-user").value=S.username||"",i=!0):document.getElementById("svc-basic-user").value="")}catch{}document.getElementById("svc-basic-pass")&&(document.getElementById("svc-basic-pass").value="");const l=e.id||e.appTemplate;if(v.includes(l)&&await t(e),i){b=!0,o.style.background="var(--ok-fg, #74dfc4)",u.style.color="var(--ok-fg, #74dfc4)",u.textContent="Credentials stored",n.style.display="";const y=document.getElementById(`creds-btn-${e.id}`);y&&y.classList.add("has-creds")}else o.style.background="var(--muted)",u.style.color="var(--muted)",u.textContent="No credentials stored",n.style.display="none"}async function t(e){const o=document.getElementById("svc-quality-select"),u=document.getElementById("svc-quality-status"),n=e.id||e.appTemplate,i=p(e);if(!i){o.innerHTML='<option value="">-- No service URL --</option>';return}o.innerHTML='<option value="">Loading...</option>',u.textContent="";try{const l=new URLSearchParams({service:n,url:i}),S=await(await fetch(`/api/v1/arr/quality-profiles?${l}`)).json();if(!S.success||!S.profiles?.length){o.innerHTML='<option value="">-- No profiles found (enter API key and click Fetch) --</option>';return}o.innerHTML="";for(const k of S.profiles){const L=document.createElement("option");L.value=k.id,L.textContent=k.name,o.appendChild(L)}if(S.storedProfileId&&(o.value=String(S.storedProfileId)),!o.value){const k=S.profiles.find(L=>/720/i.test(L.name));k&&(o.value=String(k.id))}!o.value&&S.profiles.length&&(o.value=String(S.profiles[0].id)),u.innerHTML=`<span style="color: var(--ok-fg);">${S.profiles.length} profiles loaded</span>`}catch(l){o.innerHTML='<option value="">-- Failed to load --</option>',u.innerHTML=`<span style="color: var(--error, #c62828);">Error: ${l.message}</span>`}}document.getElementById("svc-quality-fetch")?.addEventListener("click",async()=>{if(!m)return;const e=m.id||m.appTemplate,o=p(m),n=document.getElementById("svc-apikey-input")?.value.trim(),i=document.getElementById("svc-quality-select"),l=document.getElementById("svc-quality-status");if(!o){l.innerHTML='<span style="color: var(--error, #c62828);">No servLine truncated
|
|
|
|
|
</div>`);const o=document.getElementById("service-creds-modal");let m=null,h=null,b=!1;const g=["sonarr","radarr","prowlarr","overseerr"],v=["sonarr","radarr"];function p(e){return e.externalUrl||e.url||""}function c(e){const t=document.getElementById("svc-creds-error");t.textContent=e,t.style.display=""}function s(){const e=document.getElementById("svc-creds-error");e.textContent="",e.style.display="none"}async function y(e,t){if(!e)throw new Error(`${t} failed: no response`);let i;try{i=await e.json()}catch{throw new Error(`${t} failed: invalid server response`)}if(!e.ok||i?.success!==!0)throw new Error(i?.error||`${t} failed (${e.status||"unknown status"})`);return i}function x(e,t){return!!window.DCCredentialVault?.isAllowedReturnUrl(e,t)}window.openServiceCredsModal=async function(e,t={}){m=e,h=x(t.returnUrl,e.id)?t.returnUrl:null,b=!1,s();const i=document.getElementById("svc-creds-title"),n=document.getElementById("svc-creds-desc"),d=document.getElementById("svc-creds-seedhost"),l=document.getElementById("svc-creds-apikey"),f=document.getElementById("svc-creds-basic"),C=document.getElementById("svc-creds-quality");i.textContent=e.name+" \u2014 Encrypted Vault",document.getElementById("svc-creds-save").textContent=h?"Save to vault & open service":"Save to encrypted vault";const S=!!e.isExternal,T=g.includes(e.id)||g.includes(e.appTemplate),w=v.includes(e.id)||v.includes(e.appTemplate);d.style.display=S?"":"none",l.style.display=T?"":"none",C.style.display=w?"":"none",f.style.display=S?"none":"";const L=document.getElementById("svc-quality-select");L.innerHTML='<option value="">-- Enter API key first --</option>',document.getElementById("svc-quality-status").textContent="",S?(n.textContent="Seedhost credentials auto-login past the HTTP prompt. API key bypasses the app login.",document.getElementById("svc-seedhost-pass").placeholder=`Password for ${e.name}`):T?n.textContent="API key bypasses the app login screen automatically.":n.textContent="Credentials are injected automatically when accessing this service.",await r(e),o.classList.add("show")};async function r(e){const t=document.getElementById("svc-creds-dot"),i=document.getElementById("svc-creds-status"),n=document.getElementById("svc-creds-clear");let d=!1;if(e.isExternal){try{const C=await(await fetch(`/api/v1/seedhost-creds?serviceId=${e.id}`)).json();C.success?(document.getElementById("svc-seedhost-user").value=C.username||"",C.hasCredentials&&(d=!0)):document.getElementById("svc-seedhost-user").value=""}catch{}document.getElementById("svc-seedhost-pass").value=""}try{const C=await(await fetch(`/api/v1/services/${e.id}/credentials`)).json();C.success&&(C.hasApiKey?(document.getElementById("svc-apikey-input").value="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",d=!0):document.getElementById("svc-apikey-input").value="",C.hasBasicAuth&&!e.isExternal?(document.getElementById("svc-basic-user").value=C.username||"",d=!0):document.getElementById("svc-basic-user").value="")}catch{}document.getElementById("svc-basic-pass")&&(document.getElementById("svc-basic-pass").value="");const l=e.id||e.appTemplate;if(v.includes(l)&&await a(e),d){b=!0,t.style.background="var(--ok-fg, #74dfc4)",i.style.color="var(--ok-fg, #74dfc4)",i.textContent="Credentials stored",n.style.display="";const f=document.getElementById(`creds-btn-${e.id}`);f&&f.classList.add("has-creds")}else t.style.background="var(--muted)",i.style.color="var(--muted)",i.textContent="No credentials stored",n.style.display="none"}async function a(e){const t=document.getElementById("svc-quality-select"),i=document.getElementById("svc-quality-status"),n=e.id||e.appTemplate,d=p(e);if(!d){t.innerHTML='<option value="">-- No service URL --</option>';return}t.innerHTML='<option value="">Loading...</option>',i.textContent="";try{const l=new URLSearchParams({service:n,url:d}),C=await(await fetch(`/api/v1/arr/quality-profiles?${l}`)).json();if(!C.success||!C.profiles?.length){t.innerHTML='<option value="">-- No profiles found (enter API key and click Fetch) --</option>';return}t.innerHTML="";for(const S of C.profiles){const T=document.createElement("option");T.value=S.id,T.textContent=S.name,t.appendChild(T)}if(C.storedProfileId&&(t.value=String(C.storedProfileId)),!t.value){const S=C.profiles.find(T=>/720/i.test(T.name));S&&(t.value=String(S.id))}!t.value&&C.profiles.length&&(t.value=String(C.profiles[0].id)),i.innerHTML=`<span style="color: var(--ok-fg);">${C.profiles.length} profiles loaded</span>`}catch(l){t.innerHTML='<option value="">-- Failed to load --</option>',i.innerHTML=`<span style="color: var(--error, #c62828);">Error: ${l.message}</span>`}}document.getElementById("svc-quality-fetch")?.addEventListener("click",async()=>{if(!m)return;const e=m.id||m.appTemplate,t=p(m),n=document.getElementById("svc-apikey-input")?.value.trim(),d=document.getElementById("svc-quality-select"),l=document.getElementById("svc-quality-status");if(!t){l.innerHTML='<span style="color: var(--error, #c62828);">No servLine truncated
|
|
|
|
|
<div class="weather-modal-content" style="min-width: 420px; max-width: 520px;">
|
|
|
|
|
<h3 style="margin: 0 0 16px; font-size: 1.1rem;">Authentication Settings</h3>
|
|
|
|
|
|
|
|
|
@@ -288,7 +288,7 @@
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>`);async function a(){try{const v=await(await fetch("/api/v1/totp/config")).json();if(!v.success)return;const{enabled:p,sessionDuration:d,isSetUp:r}=v.config,f=document.getElementById("totp-status-dot"),w=document.getElementById("totp-status-text"),s=document.getElementById("totp-status-banner"),t=document.getElementById("totp-setup-section"),e=document.getElementById("totp-qr-section"),o=document.getElementById("totp-duration-section"),u=document.getElementById("totp-disable-section");if(p&&r){f.style.background="var(--ok-fg, #7ef2ff)",s.style.borderColor="var(--ok-fg, #7ef2ff)",s.style.background="color-mix(in srgb, var(--ok-fg) 8%, transparent)",w.textContent="TOTP is active",w.style.color="var(--ok-fg, #7ef2ff)",t.style.display="block";const n=document.getElementById("totp-setup-btn");n&&(n.textContent="Generate New Secret"),e.style.display="none",o.style.display="block",u.style.display="block",document.getElementById("totp-duration-select").value=d}else f.style.background="var(--muted)",s.style.borderColor="var(--border)",s.style.background="transparent",w.textContent="TOTP is not configured",w.style.color="var(--muted)",t.style.display="block",e.style.display="none",o.style.display="none",u.style.display="none";b(p&&r,d)}catch(g){console.warn("Failed to load TOTP settings:",g)}}const m={"15m":"15 min","30m":"30 min","1h":"1 hour","2h":"2 hours","4h":"4 hours","8h":"8 hours","12h":"12 hours","24h":"24 hours",never:"Disabled"};function h(g,v){const p=window.DCI18n;return p&&p.isLoaded()?p.t(g):v}function b(g,v){const p=document.getElementById("auth-card"),d=document.getElementById("auth-pill"),r=document.getElementById("auth-dot"),f=document.getElementById("auth-status-text");p&&(g?(p.setAttribute("data-status","on"),d.className="badge on",d.textContent=h("card.status.yes","YES"),r.className="dot ok at-bl",f.textContent="Session: "+(m[v]||v)):(p.setAttribute("data-status","off"),d.className="badge off",d.textContent=h("card.status.no","NO"),r.className="dot bad at-bl",f.textContent=h("card.auth.not_configured","Not configured")))}document.getElementById("totp-setup-btn")?.addEventListener("click",async()=>{try{const v=await(await secureFetch("/api/v1/totp/setup",{method:"POST"})).json();v.success&&(document.getElementById("totp-qr-image").src=v.qrCode,document.getElementById("totp-manual-key").textContent=v.manualKey,document.getElementById("totp-setup-section").style.display="none",document.getElementById("totp-qr-section").style.display="block",document.getElementById("totp-setup-code").value="",document.getElementById("totp-setup-error").textContent="",document.getElementById("totp-setup-code").focus())}catch(g){c.logError("[TOTP] Setup Failed",g,{function:"setupTOTP"})}}),document.getElementById("totp-import-btn")?.addEventListener("click",async()=>{const g=document.getElementById("totp-import-key").value.trim(),v=document.getElementById("totp-import-error");if(v.textContent="",!g){v.textContent="Paste a Base32 secret key first";return}try{const d=await(await secureFetch("/api/v1/totp/setup",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({secret:g})})).json();d.success?(v.textContent="",document.getElementById("totp-qr-image").src=d.qrCode,document.getElementById("totp-manual-key").textContent=d.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()):v.textContent=d.error||d.message||"Import failed"}catch{v.textContent="Connection error \u2014 try refreshing the page"}}),document.getElementById("totp-copy-key")?.addEventListener("click",()=>{const g=document.getElementById("totp-manual-key").textContent;navigator.clipboard.writeText(g).then(()=>{const v=document.getElementById("totp-copy-key");v.textContent="\u2705",setTimeout(()=>{v.textContent="\u{1F4CB}"},2e3)})}),document.getElementById("totp-download-backup")?.addEventListener("click",()=>{const g=document.getElementById("totp-manual-key").textContent.trim();if(!g)return;const v={service:"DashCaddy",type:"totp-secret",secret:g,issuer:"DashCaddy",algorithm:"SHA1",digits:6,period:30,issued:new Date().toISOString(),recovery_url:`${window.location.origin}/ (login screen \u2192 "Lost access?")`,note:'Keep this file somewhere safe. Anyone with this secret can generate your login codes. Use it ONLY to recover TOTP access via the "Lost access?" link on the DashCaddy login screen.'},p=new Blob([JSON.stringify(v,null,2)],{type:"application/json"}),d=URL.createObjectURL(p),r=document.createElement("a");r.href=d,r.download=`dashcaddy-totp-backup-${new Date().toISOString().slice(0,10)}.json`,document.body.appendChild(r),r.click(),document.body.removeChild(r),URL.revokeObjectURL(d);const f=document.getElementById("totp-download-backup");f.textContent="\u2705 Saved",setTimeLine truncated
|
|
|
|
|
</div>`);async function o(){try{const v=await(await fetch("/api/v1/totp/config")).json();if(!v.success)return;const{enabled:p,sessionDuration:c,isSetUp:s}=v.config,y=document.getElementById("totp-status-dot"),x=document.getElementById("totp-status-text"),r=document.getElementById("totp-status-banner"),a=document.getElementById("totp-setup-section"),e=document.getElementById("totp-qr-section"),t=document.getElementById("totp-duration-section"),i=document.getElementById("totp-disable-section");if(p&&s){y.style.background="var(--ok-fg, #7ef2ff)",r.style.borderColor="var(--ok-fg, #7ef2ff)",r.style.background="color-mix(in srgb, var(--ok-fg) 8%, transparent)",x.textContent="TOTP is active",x.style.color="var(--ok-fg, #7ef2ff)",a.style.display="block";const n=document.getElementById("totp-setup-btn");n&&(n.textContent="Generate New Secret"),e.style.display="none",t.style.display="block",i.style.display="block",document.getElementById("totp-duration-select").value=c}else y.style.background="var(--muted)",r.style.borderColor="var(--border)",r.style.background="transparent",x.textContent="TOTP is not configured",x.style.color="var(--muted)",a.style.display="block",e.style.display="none",t.style.display="none",i.style.display="none";b(p&&s,c)}catch(g){console.warn("Failed to load TOTP settings:",g)}}const m={"15m":"15 min","30m":"30 min","1h":"1 hour","2h":"2 hours","4h":"4 hours","8h":"8 hours","12h":"12 hours","24h":"24 hours",never:"Disabled"};function h(g,v){const p=window.DCI18n;return p&&p.isLoaded()?p.t(g):v}function b(g,v){const p=document.getElementById("auth-card"),c=document.getElementById("auth-pill"),s=document.getElementById("auth-dot"),y=document.getElementById("auth-status-text");p&&(g?(p.setAttribute("data-status","on"),c.className="badge on",c.textContent=h("card.status.yes","YES"),s.className="dot ok at-bl",y.textContent="Session: "+(m[v]||v)):(p.setAttribute("data-status","off"),c.className="badge off",c.textContent=h("card.status.no","NO"),s.className="dot bad at-bl",y.textContent=h("card.auth.not_configured","Not configured")))}document.getElementById("totp-setup-btn")?.addEventListener("click",async()=>{try{const v=await(await secureFetch("/api/v1/totp/setup",{method:"POST"})).json();v.success&&(document.getElementById("totp-qr-image").src=v.qrCode,document.getElementById("totp-manual-key").textContent=v.manualKey,document.getElementById("totp-setup-section").style.display="none",document.getElementById("totp-qr-section").style.display="block",document.getElementById("totp-setup-code").value="",document.getElementById("totp-setup-error").textContent="",document.getElementById("totp-setup-code").focus())}catch(g){u.logError("[TOTP] Setup Failed",g,{function:"setupTOTP"})}}),document.getElementById("totp-import-btn")?.addEventListener("click",async()=>{const g=document.getElementById("totp-import-key").value.trim(),v=document.getElementById("totp-import-error");if(v.textContent="",!g){v.textContent="Paste a Base32 secret key first";return}try{const c=await(await secureFetch("/api/v1/totp/setup",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({secret:g})})).json();c.success?(v.textContent="",document.getElementById("totp-qr-image").src=c.qrCode,document.getElementById("totp-manual-key").textContent=c.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()):v.textContent=c.error||c.message||"Import failed"}catch{v.textContent="Connection error \u2014 try refreshing the page"}}),document.getElementById("totp-copy-key")?.addEventListener("click",()=>{const g=document.getElementById("totp-manual-key").textContent;navigator.clipboard.writeText(g).then(()=>{const v=document.getElementById("totp-copy-key");v.textContent="\u2705",setTimeout(()=>{v.textContent="\u{1F4CB}"},2e3)})}),document.getElementById("totp-download-backup")?.addEventListener("click",()=>{const g=document.getElementById("totp-manual-key").textContent.trim();if(!g)return;const v={service:"DashCaddy",type:"totp-secret",secret:g,issuer:"DashCaddy",algorithm:"SHA1",digits:6,period:30,issued:new Date().toISOString(),recovery_url:`${window.location.origin}/ (login screen \u2192 "Lost access?")`,note:'Keep this file somewhere safe. Anyone with this secret can generate your login codes. Use it ONLY to recover TOTP access via the "Lost access?" link on the DashCaddy login screen.'},p=new Blob([JSON.stringify(v,null,2)],{type:"application/json"}),c=URL.createObjectURL(p),s=document.createElement("a");s.href=c,s.download=`dashcaddy-totp-backup-${new Date().toISOString().slice(0,10)}.json`,document.body.appendChild(s),s.click(),document.body.removeChild(s),URL.revokeObjectURL(c);const y=document.getElementById("totp-download-backup");y.textContent="\u2705 Saved",setTimeLine truncated
|
|
|
|
|
<div id="token-management-modal" class="weather-modal">
|
|
|
|
|
<div class="weather-modal-content" style="min-width: 500px; max-width: 600px;">
|
|
|
|
|
<h3>\u{1F511} DNS Credentials</h3>
|
|
|
|
@@ -306,9 +306,9 @@
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`);function c(){return Object.keys(SITE.dnsServers||{})}function a(n){return(SITE.dnsServers||{})[n]?.name||n.toUpperCase()}async function m(n,i){if(!n)throw new Error(`${i} failed: no response`);let l;try{l=await n.json()}catch{throw new Error(`${i} failed: invalid server response`)}if(!n.ok||l?.success!==!0)throw new Error(l?.error||`${i} failed (${n.status||"unknown status"})`);return l}function h(){const n=document.getElementById("dns-cred-sections");if(!n)return;n.innerHTML="";const i=c();if(i.length===0){n.innerHTML='<p style="color: var(--muted); text-align: center; padding: 20px;">No DNS servers configured.</p>';return}for(const l of i)n.insertAdjacentHTML("beforeend",`
|
|
|
|
|
`);function u(){return Object.keys(SITE.dnsServers||{})}function o(n){return(SITE.dnsServers||{})[n]?.name||n.toUpperCase()}async function m(n,d){if(!n)throw new Error(`${d} failed: no response`);let l;try{l=await n.json()}catch{throw new Error(`${d} failed: invalid server response`)}if(!n.ok||l?.success!==!0)throw new Error(l?.error||`${d} failed (${n.status||"unknown status"})`);return l}function h(){const n=document.getElementById("dns-cred-sections");if(!n)return;n.innerHTML="";const d=u();if(d.length===0){n.innerHTML='<p style="color: var(--muted); text-align: center; padding: 20px;">No DNS servers configured.</p>';return}for(const l of d)n.insertAdjacentHTML("beforeend",`
|
|
|
|
|
<div class="token-section">
|
|
|
|
|
<h4 class="token-section-title">${a(l)}</h4>
|
|
|
|
|
<h4 class="token-section-title">${o(l)}</h4>
|
|
|
|
|
<div class="token-grid">
|
|
|
|
|
<div class="token-field">
|
|
|
|
|
<label for="${l}-readonly-username">\u{1F4D6} Read-Only (Logs):</label>
|
|
|
|
@@ -329,17 +329,17 @@
|
|
|
|
|
</div>
|
|
|
|
|
<div class="token-status" id="${l}-token-status"></div>
|
|
|
|
|
</div>
|
|
|
|
|
`)}function b(){let n=safeSessionGet("dashcaddy-encryption-key");if(n)return n;const i=safeGet("dashcaddy-encryption-key");if(i)return safeSessionSet("dashcaddy-encryption-key",i),safeRemove("dashcaddy-encryption-key"),i;const l=new Uint8Array(32);return crypto.getRandomValues(l),n=Array.from(l,y=>y.toString(16).padStart(2,"0")).join(""),safeSessionSet("dashcaddy-encryption-key",n),n}const g=b();function v(n,i){if(!n)return"";const l=crypto.getRandomValues(new Uint8Array(8)),y=Array.from(l,L=>L.toString(16).padStart(2,"0")).join(""),S=new TextEncoder().encode(i+y);let k="";for(let L=0;L<n.length;L++){const x=n.charCodeAt(L)^S[L%S.length]^l[L%l.length]^L*31+17&255;k+=String.fromCharCode(x)}return y+":"+btoa(k)}function p(n,i){if(!n)return"";try{if(n.indexOf(":")===16){const k=n.substring(0,16),L=new Uint8Array(k.match(/.{2}/g).map(E=>parseInt(E,16))),x=atob(n.substring(17)),B=new TextEncoder().encode(i+k);let A="";for(let E=0;E<x.length;E++){const C=x.charCodeAt(E)^B[E%B.length]^L[E%L.length]^E*31+17&255;A+=String.fromCharCode(C)}return A}const y=atob(n);let S="";for(let k=0;k<y.length;k++){const L=y.charCodeAt(k)^i.charCodeAt(k%i.length);S+=String.fromCharCode(L)}return S}catch{return""}}function d(n,i,l){const y=safeGet(`${n}-${i}-${l}-enc`);return p(y,g)}function r(n,i,l,y){const S=`${n}-${i}-${l}-enc`;y?safeSet(S,v(y,g)):safeRemove(S)}function f(n,i){return d(n,i,"token")}function w(n,i){return d(n,i,"username")}function s(n,i,l){r(n,i,"token",l)}function t(n,i,l){r(n,i,"username",l)}function e(){const n={};for(const i of c())n[i]={readonly:{username:w(i,"readonly"),token:f(i,"readonly")},admin:{username:w(i,"admin"),token:f(i,"admin")}};return n}function o(){c().forEach(n=>{["readonly","admin"].forEach(i=>{["token","username"].forEach(l=>{safeRemove(`${n}-${i}-${l}-enc`)})}),safeRemove(`${n}-token-enc`),safeRemove(`${n}-username-enc`)})}function u(n){const i=f(n,"readonly"),l=w(n,"readonly"),y=f(n,"admin"),S=w(n,"admin"),k=p(safeGet(`${n}-token-enc`),g),L=p(safeGet(`${n}-username-enc`),g);return{username:S||l||L,token:y||i||k,readonlyToken:i||k,readonlyUsername:l||L,adminToken:y||k,adminUsername:S||L}}document.getElementById("manage-tokens")?.addEventListener("click",()=>{h();const n=document.getElementById("token-management-modal"),i=e();c().forEach(l=>{const y=i[l];document.getElementById(`${l}-readonly-username`).value=y.readonly.username,document.getElementById(`${l}-readonly-token`).value=y.readonly.token,document.getElementById(`${l}-admin-username`).value=y.admin.username,document.getElementById(`${l}-admin-token`).value=y.admin.token,document.getElementById(`${l}-token-status`).textContent=""}),n.classList.add("show")}),document.getElementById("token-management-modal")?.addEventListener("click",n=>{const i=n.target.closest(".token-toggle");if(i){const l=i.dataset.target,y=document.getElementById(l);y.type==="password"?(y.type="text",i.textContent="\u{1F648}"):(y.type="password",i.textContent="\u{1F441}");return}n.target.id==="token-management-modal"&&n.target.classList.remove("show")}),document.getElementById("token-save")?.addEventListener("click",async()=>{const n=c(),i={};let l=!1;if(n.forEach(y=>{const S={},k=document.getElementById(`${y}-readonly-username`).value.trim(),L=document.getElementById(`${y}-readonly-token`).value.trim(),x=document.getElementById(`${y}-admin-username`).value.trim(),B=document.getElementById(`${y}-admin-token`).value.trim();k&&L&&(S.readonly={username:k,password:L},l=!0),x&&B&&(S.admin={username:x,password:B},l=!0),Object.keys(S).length>0&&(i[y]=S)}),l){n.forEach(y=>{i[y]&&(document.getElementById(`${y}-token-status`).textContent="Verifying...",document.getElementById(`${y}-token-status`).className="token-status")});try{const y=await secureFetch("/api/v1/dns/credentials",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({servers:i})}),S=await m(y,"DNS credential save");if(S.results){const k=Object.keys(i).filter(L=>S.results[L]?.success!==!0);if(k.length){const L=k.map(x=>S.results[x]?.error||`${x} failed`).join("; ");throw new Error(L)}}n.forEach(k=>{t(k,"readonly",document.getElementById(`${k}-readonly-username`).value.trim()),s(k,"readonly",document.getElementById(`${k}-readonly-token`).value.trim()),t(k,"admin",document.getElementById(`${k}-admin-username`).value.trim()),s(k,"admin",document.getElementById(`${k}-admin-token`).value.trim())}),n.forEach(k=>{const L=document.getElementById(`${k}-token-status`);if(!i[k]){L.textContent="";return}const x=S.results?.[k];L.textContent=x?.partial?"\u2713 "+x.partial:"\u2713 Verified & saved",L.className="token-status success"})}catch(y){console.error("Failed to sync DNS credentials to backend:",y),n.forEach(S=>{i[S]&&(document.getElementById(`${S}-token-status`).textContent="\u2717 "+(y.message||"Save failed"),document.getElementById(`${S}-token-status`).className="token-status error")})}}else n.forEach(y=>{document.getElementById(`${y}-token-status`).textContent=""});setTimeout(()=>{
Line truncated
|
|
|
|
|
`)}function b(){let n=safeSessionGet("dashcaddy-encryption-key");if(n)return n;const d=safeGet("dashcaddy-encryption-key");if(d)return safeSessionSet("dashcaddy-encryption-key",d),safeRemove("dashcaddy-encryption-key"),d;const l=new Uint8Array(32);return crypto.getRandomValues(l),n=Array.from(l,f=>f.toString(16).padStart(2,"0")).join(""),safeSessionSet("dashcaddy-encryption-key",n),n}const g=b();function v(n,d){if(!n)return"";const l=crypto.getRandomValues(new Uint8Array(8)),f=Array.from(l,T=>T.toString(16).padStart(2,"0")).join(""),C=new TextEncoder().encode(d+f);let S="";for(let T=0;T<n.length;T++){const w=n.charCodeAt(T)^C[T%C.length]^l[T%l.length]^T*31+17&255;S+=String.fromCharCode(w)}return f+":"+btoa(S)}function p(n,d){if(!n)return"";try{if(n.indexOf(":")===16){const S=n.substring(0,16),T=new Uint8Array(S.match(/.{2}/g).map(E=>parseInt(E,16))),w=atob(n.substring(17)),L=new TextEncoder().encode(d+S);let A="";for(let E=0;E<w.length;E++){const k=w.charCodeAt(E)^L[E%L.length]^T[E%T.length]^E*31+17&255;A+=String.fromCharCode(k)}return A}const f=atob(n);let C="";for(let S=0;S<f.length;S++){const T=f.charCodeAt(S)^d.charCodeAt(S%d.length);C+=String.fromCharCode(T)}return C}catch{return""}}function c(n,d,l){const f=safeGet(`${n}-${d}-${l}-enc`);return p(f,g)}function s(n,d,l,f){const C=`${n}-${d}-${l}-enc`;f?safeSet(C,v(f,g)):safeRemove(C)}function y(n,d){return c(n,d,"token")}function x(n,d){return c(n,d,"username")}function r(n,d,l){s(n,d,"token",l)}function a(n,d,l){s(n,d,"username",l)}function e(){const n={};for(const d of u())n[d]={readonly:{username:x(d,"readonly"),token:y(d,"readonly")},admin:{username:x(d,"admin"),token:y(d,"admin")}};return n}function t(){u().forEach(n=>{["readonly","admin"].forEach(d=>{["token","username"].forEach(l=>{safeRemove(`${n}-${d}-${l}-enc`)})}),safeRemove(`${n}-token-enc`),safeRemove(`${n}-username-enc`)})}function i(n){const d=y(n,"readonly"),l=x(n,"readonly"),f=y(n,"admin"),C=x(n,"admin"),S=p(safeGet(`${n}-token-enc`),g),T=p(safeGet(`${n}-username-enc`),g);return{username:C||l||T,token:f||d||S,readonlyToken:d||S,readonlyUsername:l||T,adminToken:f||S,adminUsername:C||T}}document.getElementById("manage-tokens")?.addEventListener("click",()=>{h();const n=document.getElementById("token-management-modal"),d=e();u().forEach(l=>{const f=d[l];document.getElementById(`${l}-readonly-username`).value=f.readonly.username,document.getElementById(`${l}-readonly-token`).value=f.readonly.token,document.getElementById(`${l}-admin-username`).value=f.admin.username,document.getElementById(`${l}-admin-token`).value=f.admin.token,document.getElementById(`${l}-token-status`).textContent=""}),n.classList.add("show")}),document.getElementById("token-management-modal")?.addEventListener("click",n=>{const d=n.target.closest(".token-toggle");if(d){const l=d.dataset.target,f=document.getElementById(l);f.type==="password"?(f.type="text",d.textContent="\u{1F648}"):(f.type="password",d.textContent="\u{1F441}");return}n.target.id==="token-management-modal"&&n.target.classList.remove("show")}),document.getElementById("token-save")?.addEventListener("click",async()=>{const n=u(),d={};let l=!1;if(n.forEach(f=>{const C={},S=document.getElementById(`${f}-readonly-username`).value.trim(),T=document.getElementById(`${f}-readonly-token`).value.trim(),w=document.getElementById(`${f}-admin-username`).value.trim(),L=document.getElementById(`${f}-admin-token`).value.trim();S&&T&&(C.readonly={username:S,password:T},l=!0),w&&L&&(C.admin={username:w,password:L},l=!0),Object.keys(C).length>0&&(d[f]=C)}),l){n.forEach(f=>{d[f]&&(document.getElementById(`${f}-token-status`).textContent="Verifying...",document.getElementById(`${f}-token-status`).className="token-status")});try{const f=await secureFetch("/api/v1/dns/credentials",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({servers:d})}),C=await m(f,"DNS credential save");if(C.results){const S=Object.keys(d).filter(T=>C.results[T]?.success!==!0);if(S.length){const T=S.map(w=>C.results[w]?.error||`${w} failed`).join("; ");throw new Error(T)}}n.forEach(S=>{a(S,"readonly",document.getElementById(`${S}-readonly-username`).value.trim()),r(S,"readonly",document.getElementById(`${S}-readonly-token`).value.trim()),a(S,"admin",document.getElementById(`${S}-admin-username`).value.trim()),r(S,"admin",document.getElementById(`${S}-admin-token`).value.trim())}),n.forEach(S=>{const T=document.getElementById(`${S}-token-status`);if(!d[S]){T.textContent="";return}const w=C.results?.[S];T.textContent=w?.partial?"\u2713 "+w.partial:"\u2713 Verified & saved",T.className="token-status success"})}catch(f){console.error("Failed to sync DNS credentials to backend:",f),n.forEach(C=>{d[C]&&(document.getElementById(`${C}-token-status`).textContent="\u2717 "+(f.message||"Save failed"),document.getElementById(`${C}-token-status`).className="token-status error")})}}else n.forEach(f=>{document.getElementById(`${f}-token-status`).textContent=""});setTimeout(()=>{
Line truncated
|
|
|
|
|
|
|
|
|
|
Current: ${s.currentVersion}
|
|
|
|
|
New: ${s.updateVersion}
|
|
|
|
|
Current: ${r.currentVersion}
|
|
|
|
|
New: ${r.updateVersion}
|
|
|
|
|
|
|
|
|
|
`+(s.updateTitle?`${s.updateTitle}
|
|
|
|
|
`+(r.updateTitle?`${r.updateTitle}
|
|
|
|
|
|
|
|
|
|
`:"")+`The DNS server will restart during the update.
|
|
|
|
|
Proceed?`)){r.textContent=f,r.disabled=!1,r.title="Update DNS server";return}r.textContent="\u{1F504}",r.title="Updating...";const o=await(await secureFetch(`/api/v1/dns/update?server=${encodeURIComponent(d)}`,{method:"POST"})).json();if(!o.success)throw new Error(o.error||"Update failed");if(o.manualUpdateRequired){r.textContent="\u2B06\uFE0F",r.title=`Update available: ${o.newVersion}`;const u=o.downloadLink?`
|
|
|
|
|
Download: ${o.downloadLink}`:"",n=o.instructionsLink?`
|
|
|
|
|
Instructions: ${o.instructionsLink}`:"";showNotification(`${p.toUpperCase()} update requires manual installation. Current: ${o.previousVersion} \u2192 ${o.newVersion}. Please update manually on the host machine.`,"warning",8e3),r.disabled=!1;return}r.textContent="\u2705",r.title="Updated successfully!",showNotification(`${p.toUpperCase()} updated successfully! ${o.previousVersion} \u2192 ${o.newVersion}. Server is restarting...`,"success"),setTimeout(()=>{r.textContent=f,r.disabled=!1,r.title="Update DNS server",window.refreshAll()},1e4)}catch(w){console.error("DNS update error:",w),r.textContent="\u274C",r.title="Update failed",showNotification(`Failed to update ${p.toUpperCase()}: ${w.message}`,"error"),setTimeout(()=>{r.textContent=f,r.disabled=!1,r.title="Update DNS server"},3e3)}}document.querySelector(".top")?.addEventListener("click",p=>{const d=p.target.closest('[id$="-update"]');if(!d)return;const r=d.id.replace("-update","");SITE.dnsServers[r]&&a(r,SITE.dnsServers[r]?.ip)}),injectModal("dns-settings-modal",`
|
|
|
|
|
Proceed?`)){s.textContent=y,s.disabled=!1,s.title="Update DNS server";return}s.textContent="\u{1F504}",s.title="Updating...";const t=await(await secureFetch(`/api/v1/dns/update?server=${encodeURIComponent(c)}`,{method:"POST"})).json();if(!t.success)throw new Error(t.error||"Update failed");if(t.manualUpdateRequired){s.textContent="\u2B06\uFE0F",s.title=`Update available: ${t.newVersion}`;const i=t.downloadLink?`
|
|
|
|
|
Download: ${t.downloadLink}`:"",n=t.instructionsLink?`
|
|
|
|
|
Instructions: ${t.instructionsLink}`:"";showNotification(`${p.toUpperCase()} update requires manual installation. Current: ${t.previousVersion} \u2192 ${t.newVersion}. Please update manually on the host machine.`,"warning",8e3),s.disabled=!1;return}s.textContent="\u2705",s.title="Updated successfully!",showNotification(`${p.toUpperCase()} updated successfully! ${t.previousVersion} \u2192 ${t.newVersion}. Server is restarting...`,"success"),setTimeout(()=>{s.textContent=y,s.disabled=!1,s.title="Update DNS server",window.refreshAll()},1e4)}catch(x){console.error("DNS update error:",x),s.textContent="\u274C",s.title="Update failed",showNotification(`Failed to update ${p.toUpperCase()}: ${x.message}`,"error"),setTimeout(()=>{s.textContent=y,s.disabled=!1,s.title="Update DNS server"},3e3)}}document.querySelector(".top")?.addEventListener("click",p=>{const c=p.target.closest('[id$="-update"]');if(!c)return;const s=c.id.replace("-update","");SITE.dnsServers[s]&&o(s,SITE.dnsServers[s]?.ip)}),injectModal("dns-settings-modal",`
|
|
|
|
|
<div id="dns-settings-modal" class="weather-modal">
|
|
|
|
|
<div class="weather-modal-content" style="min-width: 420px; max-width: 520px;">
|
|
|
|
|
<h3 id="dns-settings-title">DNS Settings</h3>
|
|
|
|
@@ -366,7 +366,7 @@ Instructions: ${o.instructionsLink}`:"";showNotification(`${p.toUpperCase()} upd
|
|
|
|
|
<button id="dns-settings-save" class="btn-accent">Save</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>`);let m=null;function h(p){m=p;const d=SITE.dnsServers[p]||{},r=document.getElementById("dns-settings-modal");document.getElementById("dns-settings-title").textContent=`${(d.name||p).toUpperCase()} Settings`,document.getElementById("dns-edit-ip").value=d.ip||"",document.getElementById("dns-edit-port").value=d.port||DC.DEFAULTS.DNS_PORT,document.getElementById("dns-edit-name").value=d.name||"",r.classList.add("show")}async function b(){if(!m)return;const p=document.getElementById("dns-edit-ip").value.trim(),d=document.getElementById("dns-edit-port").value.trim()||DC.DEFAULTS.DNS_PORT,r=document.getElementById("dns-edit-name").value.trim();if(!p){showNotification("Server IP is required","warning");return}const f={dnsServers:{}};f.dnsServers[m]={ip:p,port:String(d)},r&&(f.dnsServers[m].name=r);try{const s=await(await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(f)})).json();s.success?(SITE.dnsServers[m]=f.dnsServers[m],showNotification(`${m.toUpperCase()} settings saved`,"success"),v(),window.refreshAll()):showNotification(s.error||"Failed to save settings","error")}catch(w){showNotification("Failed to save: "+w.message,"error")}}async function g(){if(m&&confirm(`Remove ${m.toUpperCase()} from dashboard? This won't affect the actual DNS server.`))try{const d=await(await secureFetch("/api/v1/config")).json();d.dnsServers&&delete d.dnsServers[m];const f=await(await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({dnsServers:d.dnsServers||{}})})).json();if(f.success){delete SITE.dnsServers[m];const w=document.querySelector(`.top [data-app="${m}"]`);w&&w.remove(),showNotification(`${m.toUpperCase()} removed from dashboard`,"success"),v()}else showNotification(f.error||"Failed to remove","error")}catch(p){showNotification("Failed to remove: "+p.message,"error")}}function v(){closeModal("dns-settings-modal"),m=null}document.getElementById("dns-settings-cancel")?.addEventListener("click",v),document.getElementById("dns-settings-save")?.addEventListener("click",b),document.getElementById("dns-settings-delete")?.addEventListener("click",g),document.getElementById("dns-settings-modal")?.addEventListener("click",p=>{p.target.id==="dns-settings-modal"&&v()}),document.querySelector(".top")?.addEventListener("click",p=>{const d=p.target.closest('[id$="-settings"]');if(!d)return;const r=d.id.replace("-settings","");SITE.dnsServers[r]&&(p.stopPropagation(),h(r))}),document.getElementById("refresh")?.addEventListener("click",window.refreshAll)})(),(function(){injectModal("logs-modal",`
|
|
|
|
|
</div>`);let m=null;function h(p){m=p;const c=SITE.dnsServers[p]||{},s=document.getElementById("dns-settings-modal");document.getElementById("dns-settings-title").textContent=`${(c.name||p).toUpperCase()} Settings`,document.getElementById("dns-edit-ip").value=c.ip||"",document.getElementById("dns-edit-port").value=c.port||DC.DEFAULTS.DNS_PORT,document.getElementById("dns-edit-name").value=c.name||"",s.classList.add("show")}async function b(){if(!m)return;const p=document.getElementById("dns-edit-ip").value.trim(),c=document.getElementById("dns-edit-port").value.trim()||DC.DEFAULTS.DNS_PORT,s=document.getElementById("dns-edit-name").value.trim();if(!p){showNotification("Server IP is required","warning");return}const y={dnsServers:{}};y.dnsServers[m]={ip:p,port:String(c)},s&&(y.dnsServers[m].name=s);try{const r=await(await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(y)})).json();r.success?(SITE.dnsServers[m]=y.dnsServers[m],showNotification(`${m.toUpperCase()} settings saved`,"success"),v(),window.refreshAll()):showNotification(r.error||"Failed to save settings","error")}catch(x){showNotification("Failed to save: "+x.message,"error")}}async function g(){if(m&&confirm(`Remove ${m.toUpperCase()} from dashboard? This won't affect the actual DNS server.`))try{const c=await(await secureFetch("/api/v1/config")).json();c.dnsServers&&delete c.dnsServers[m];const y=await(await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({dnsServers:c.dnsServers||{}})})).json();if(y.success){delete SITE.dnsServers[m];const x=document.querySelector(`.top [data-app="${m}"]`);x&&x.remove(),showNotification(`${m.toUpperCase()} removed from dashboard`,"success"),v()}else showNotification(y.error||"Failed to remove","error")}catch(p){showNotification("Failed to remove: "+p.message,"error")}}function v(){closeModal("dns-settings-modal"),m=null}document.getElementById("dns-settings-cancel")?.addEventListener("click",v),document.getElementById("dns-settings-save")?.addEventListener("click",b),document.getElementById("dns-settings-delete")?.addEventListener("click",g),document.getElementById("dns-settings-modal")?.addEventListener("click",p=>{p.target.id==="dns-settings-modal"&&v()}),document.querySelector(".top")?.addEventListener("click",p=>{const c=p.target.closest('[id$="-settings"]');if(!c)return;const s=c.id.replace("-settings","");SITE.dnsServers[s]&&(p.stopPropagation(),h(s))}),document.getElementById("refresh")?.addEventListener("click",window.refreshAll)})(),(function(){injectModal("logs-modal",`
|
|
|
|
|
<div id="logs-modal" class="logs-modal">
|
|
|
|
|
<div class="logs-modal-content" style="min-width: 800px; max-width: 1000px;">
|
|
|
|
|
<div class="logs-header">
|
|
|
|
@@ -390,66 +390,66 @@ Instructions: ${o.instructionsLink}`:"";showNotification(`${p.toUpperCase()} upd
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>`);let c=null,a=null,m=!1,h=null,b=null,g=!1,v=null,p=null,d=!1,r=null,f=!1;async function w(E,C=25){try{const T=getDnsServerAddr(E),$=await fetch(`/api/v1/dns/logs?server=${T}&limit=${C}`,{cache:"no-store",headers:{Accept:"application/json","Cache-Control":"no-cache"}});if($.ok){const I=await $.json();return I.success&&I.logs?{logs:I.logs,count:I.count,server:I.server}:{error:I.error||"Failed to fetch logs"}}else return $.status===401?{error:"DNS auto-auth failed - check credentials in settings"}:{error:`HTTP ${$.status}`}}catch(T){return console.error("DNS logs fetch failed:",T),{error:T.message}}}function s(E){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"}[E]||"var(--fg)"}function t(E){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;",E.parsed===!1)return C.style.gridTemplateColumns="1fr",C.innerHTML=`<span style="color: var(--muted); font-family: monospace; font-size: 0.75rem;">${escapeHtml(E.raw)}</span>`,C;const T=s(E.rcode),$=E.rcode==="Refused"||E.rcode==="REFUSED";return C.innerHTML=`
|
|
|
|
|
</div>`);let u=null,o=null,m=!1,h=null,b=null,g=!1,v=null,p=null,c=!1,s=null,y=!1;async function x(E,k=25){try{const B=getDnsServerAddr(E),$=await fetch(`/api/v1/dns/logs?server=${B}&limit=${k}`,{cache:"no-store",headers:{Accept:"application/json","Cache-Control":"no-cache"}});if($.ok){const I=await $.json();return I.success&&I.logs?{logs:I.logs,count:I.count,server:I.server}:{error:I.error||"Failed to fetch logs"}}else return $.status===401?{error:"DNS auto-auth failed - check credentials in settings"}:{error:`HTTP ${$.status}`}}catch(B){return console.error("DNS logs fetch failed:",B),{error:B.message}}}function r(E){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"}[E]||"var(--fg)"}function a(E){const k=document.createElement("div");if(k.className="log-entry",k.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;",E.parsed===!1)return k.style.gridTemplateColumns="1fr",k.innerHTML=`<span style="color: var(--muted); font-family: monospace; font-size: 0.75rem;">${escapeHtml(E.raw)}</span>`,k;const B=r(E.rcode),$=E.rcode==="Refused"||E.rcode==="REFUSED";return k.innerHTML=`
|
|
|
|
|
<span style="color: var(--muted); font-size: 0.75rem;">${escapeHtml(E.timestamp)}</span>
|
|
|
|
|
<span style="color: var(--accent); font-size: 0.75rem;" title="${escapeHtml(E.client)}">${escapeHtml(E.client)}</span>
|
|
|
|
|
<span style="font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; ${$?"text-decoration: line-through; opacity: 0.6;":""}" title="${escapeHtml(E.domain)}">${escapeHtml(E.domain)}</span>
|
|
|
|
|
<span style="color: var(--muted); font-size: 0.75rem;">${escapeHtml(E.type)}</span>
|
|
|
|
|
<span style="color: ${T}; font-weight: 500; font-size: 0.75rem;">${escapeHtml(E.rcode)}</span>
|
|
|
|
|
`,C}async function e(){if(d){await B();return}if(g){await S();return}if(m||!c)return;const E=parseInt(document.getElementById("log-lines").value),C=document.getElementById("logs-content");try{const T=await w(c,E);if(T.error){C.innerHTML=`
|
|
|
|
|
<span style="color: ${B}; font-weight: 500; font-size: 0.75rem;">${escapeHtml(E.rcode)}</span>
|
|
|
|
|
`,k}async function e(){if(c){await L();return}if(g){await C();return}if(m||!u)return;const E=parseInt(document.getElementById("log-lines").value),k=document.getElementById("logs-content");try{const B=await x(u,E);if(B.error){k.innerHTML=`
|
|
|
|
|
<div style="padding: 20px; text-align: center; color: var(--bad-fg);">
|
|
|
|
|
<div style="font-size: 1.2rem; margin-bottom: 8px;">\u26A0\uFE0F Error</div>
|
|
|
|
|
<div>${escapeHtml(T.error)}</div>
|
|
|
|
|
</div>`;return}C.innerHTML=`
|
|
|
|
|
<div>${escapeHtml(B.error)}</div>
|
|
|
|
|
</div>`;return}k.innerHTML=`
|
|
|
|
|
<div style="display: grid; grid-template-columns: 140px 110px 1fr 70px 80px; gap: 8px; padding: 8px 10px; background: var(--card-base); border-bottom: 2px solid var(--border); font-size: 0.75rem; font-weight: 600; color: var(--muted); position: sticky; top: 0;">
|
|
|
|
|
<span>Time</span>
|
|
|
|
|
<span>Client</span>
|
|
|
|
|
<span>Domain</span>
|
|
|
|
|
<span>Type</span>
|
|
|
|
|
<span>Status</span>
|
|
|
|
|
</div>`,T.logs&&T.logs.length>0?T.logs.forEach($=>{const I=t($);C.appendChild(I)}):C.innerHTML+=`
|
|
|
|
|
</div>`,B.logs&&B.logs.length>0?B.logs.forEach($=>{const I=a($);k.appendChild(I)}):k.innerHTML+=`
|
|
|
|
|
<div style="padding: 20px; text-align: center; color: var(--muted);">
|
|
|
|
|
No DNS queries logged yet
|
|
|
|
|
</div>`}catch(T){C.innerHTML=`
|
|
|
|
|
</div>`}catch(B){k.innerHTML=`
|
|
|
|
|
<div style="padding: 20px; text-align: center; color: var(--bad-fg);">
|
|
|
|
|
Failed to fetch logs: ${escapeHtml(T.message)}
|
|
|
|
|
</div>`}}function o(E){c=E,m=!1,g=!1;const C=document.getElementById("logs-modal"),T=document.getElementById("logs-title"),$=document.getElementById("logs-pause"),I=document.getElementById("logs-stream");T.textContent=`${E.toUpperCase()} DNS Logs`,$.textContent="\u23F8\uFE0F Pause",$.classList.remove("paused"),I&&(I.style.display="none"),C.classList.add("show"),e(),a=setInterval(e,DC.POLL.LOGS)}function u(){document.getElementById("logs-modal").classList.remove("show"),a&&(clearInterval(a),a=null),i(),c=null,g=!1,h=null,b=null,d=!1,v=null,p=null,m=!1}function n(E){r&&i();const C=document.getElementById("logs-stream"),T=document.getElementById("logs-pause"),$=document.getElementById("logs-content");a&&(clearInterval(a),a=null);try{r=new EventSource(`/api/v1/logs/stream/${E}`),f=!0,C.classList.add("active"),C.textContent="\u{1F534} Live",C.title="Streaming - click to stop",T.style.display="none";const I=document.getElementById("logs-title");I.textContent.includes("\u{1F534}")||(I.innerHTML=I.textContent.replace("\u{1F4CB}","\u{1F4CB} \u{1F534}")),r.onmessage=P=>{try{const D=JSON.parse(P.data);if(D.error){console.error("Stream error:",D.error),i();return}const O=document.createElement("div");O.className="log-entry",O.style.cssText="display: flex; gap: 12px; padding: 6px 10px; border-bottom: 1px solid var(--border); font-size: 0.8rem; align-items: flex-start; font-family: monospace;";const _=(D.stream||"stdout")==="stderr",R=_?"var(--bad-fg)":"var(--fg)",M=`<span style="background: ${_?"var(--bad-bg)":"var(--ok-bg)"}; color: ${R}; padding: 2px 6px; border-radius: 4px; font-size: 0.7rem; font-weight: 600; min-width: 50px; text-align: center;">${_?"STDERR":"STDOUT"}</span>`;for(O.innerHTML=`
|
|
|
|
|
<div style="flex-shrink: 0;">${M}</div>
|
|
|
|
|
<div style="flex: 1; color: ${R}; word-break: break-all; white-space: pre-wrap;">${escapeHtml(D.text)}</div>
|
|
|
|
|
`,$.appendChild(O),$.scrollTop=$.scrollHeight;$.children.length>500;)$.removeChild($.firstChild)}catch(D){console.error("Error parsing stream data:",D)}},r.onerror=P=>{console.error("EventSource error:",P),i()}}catch(I){console.error("Failed to start streaming:",I),i()}}function i(){r&&(r.close(),r=null),f=!1;const E=document.getElementById("logs-stream"),C=document.getElementById("logs-pause"),T=document.getElementById("logs-title");E&&(E.classList.remove("active"),E.textContent="\u{1F4E1} Live",E.title="Enable real-time streaming"),C&&(C.style.display=""),T&&(T.textContent=T.textContent.replace(" \u{1F534}","")),g&&h&&!a&&(a=setInterval(S,DC.POLL.LOGS))}async function l(E,C=100){try{const T=`/api/v1/logs/container/${E}?tail=${C}×tamps=true`,$=await fetch(T,{cache:"no-store",headers:{Accept:"application/json","Cache-Control":"no-cache"}});if($.ok){const I=await $.json();return I.success&&I.logs?{logs:I.logs,count:I.count,containerName:I.containerName,containerId:I.containerId}:{error:I.error||"Failed to fetch container logs"}}else return{error:`HTTP ${$.status}: ${$.statusText}`}}catch(T){return console.error("Container logs fetch failed:",T),{error:T.message}}}function y(E){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 T=E.stream==="stderr"?"var(--bad-fg)":"var(--fg)",$=E.stream==="stderr"?'<span style="background: var(--bad-bg); color: var(--bad-fg); padding: 2px 6px; border-radius: 4px; font-size: 0.7rem; font-weight: 600;">STDERR</span>':'<span style="background: var(--ok-bg); color: var(--ok-fg); padding: 2px 6px; border-radius: 4px; font-size: 0.7rem; font-weight: 600;">STDOUT</span>';return C.innerHTML=`
|
|
|
|
|
Failed to fetch logs: ${escapeHtml(B.message)}
|
|
|
|
|
</div>`}}function t(E){u=E,m=!1,g=!1;const k=document.getElementById("logs-modal"),B=document.getElementById("logs-title"),$=document.getElementById("logs-pause"),I=document.getElementById("logs-stream");B.textContent=`${E.toUpperCase()} DNS Logs`,$.textContent="\u23F8\uFE0F Pause",$.classList.remove("paused"),I&&(I.style.display="none"),k.classList.add("show"),e(),o=setInterval(e,DC.POLL.LOGS)}function i(){document.getElementById("logs-modal").classList.remove("show"),o&&(clearInterval(o),o=null),d(),u=null,g=!1,h=null,b=null,c=!1,v=null,p=null,m=!1}function n(E){s&&d();const k=document.getElementById("logs-stream"),B=document.getElementById("logs-pause"),$=document.getElementById("logs-content");o&&(clearInterval(o),o=null);try{s=new EventSource(`/api/v1/logs/stream/${E}`),y=!0,k.classList.add("active"),k.textContent="\u{1F534} Live",k.title="Streaming - click to stop",B.style.display="none";const I=document.getElementById("logs-title");I.textContent.includes("\u{1F534}")||(I.innerHTML=I.textContent.replace("\u{1F4CB}","\u{1F4CB} \u{1F534}")),s.onmessage=P=>{try{const D=JSON.parse(P.data);if(D.error){console.error("Stream error:",D.error),d();return}const O=document.createElement("div");O.className="log-entry",O.style.cssText="display: flex; gap: 12px; padding: 6px 10px; border-bottom: 1px solid var(--border); font-size: 0.8rem; align-items: flex-start; font-family: monospace;";const N=(D.stream||"stdout")==="stderr",_=N?"var(--bad-fg)":"var(--fg)",U=`<span style="background: ${N?"var(--bad-bg)":"var(--ok-bg)"}; color: ${_}; padding: 2px 6px; border-radius: 4px; font-size: 0.7rem; font-weight: 600; min-width: 50px; text-align: center;">${N?"STDERR":"STDOUT"}</span>`;for(O.innerHTML=`
|
|
|
|
|
<div style="flex-shrink: 0;">${U}</div>
|
|
|
|
|
<div style="flex: 1; color: ${_}; word-break: break-all; white-space: pre-wrap;">${escapeHtml(D.text)}</div>
|
|
|
|
|
`,$.appendChild(O),$.scrollTop=$.scrollHeight;$.children.length>500;)$.removeChild($.firstChild)}catch(D){console.error("Error parsing stream data:",D)}},s.onerror=P=>{console.error("EventSource error:",P),d()}}catch(I){console.error("Failed to start streaming:",I),d()}}function d(){s&&(s.close(),s=null),y=!1;const E=document.getElementById("logs-stream"),k=document.getElementById("logs-pause"),B=document.getElementById("logs-title");E&&(E.classList.remove("active"),E.textContent="\u{1F4E1} Live",E.title="Enable real-time streaming"),k&&(k.style.display=""),B&&(B.textContent=B.textContent.replace(" \u{1F534}","")),g&&h&&!o&&(o=setInterval(C,DC.POLL.LOGS))}async function l(E,k=100){try{const B=`/api/v1/logs/container/${E}?tail=${k}×tamps=true`,$=await fetch(B,{cache:"no-store",headers:{Accept:"application/json","Cache-Control":"no-cache"}});if($.ok){const I=await $.json();return I.success&&I.logs?{logs:I.logs,count:I.count,containerName:I.containerName,containerId:I.containerId}:{error:I.error||"Failed to fetch container logs"}}else return{error:`HTTP ${$.status}: ${$.statusText}`}}catch(B){return console.error("Container logs fetch failed:",B),{error:B.message}}}function f(E){const k=document.createElement("div");k.className="log-entry",k.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 B=E.stream==="stderr"?"var(--bad-fg)":"var(--fg)",$=E.stream==="stderr"?'<span style="background: var(--bad-bg); color: var(--bad-fg); padding: 2px 6px; border-radius: 4px; font-size: 0.7rem; font-weight: 600;">STDERR</span>':'<span style="background: var(--ok-bg); color: var(--ok-fg); padding: 2px 6px; border-radius: 4px; font-size: 0.7rem; font-weight: 600;">STDOUT</span>';return k.innerHTML=`
|
|
|
|
|
<div style="flex-shrink: 0;">${$}</div>
|
|
|
|
|
<div style="flex: 1; color: ${T}; word-break: break-all; white-space: pre-wrap;">${escapeHtml(E.text)}</div>
|
|
|
|
|
`,C}async function S(){if(m||!h||!g)return;const E=parseInt(document.getElementById("log-lines").value),C=document.getElementById("logs-content");try{const T=await l(h,E);if(T.error){C.innerHTML=`
|
|
|
|
|
<div style="flex: 1; color: ${B}; word-break: break-all; white-space: pre-wrap;">${escapeHtml(E.text)}</div>
|
|
|
|
|
`,k}async function C(){if(m||!h||!g)return;const E=parseInt(document.getElementById("log-lines").value),k=document.getElementById("logs-content");try{const B=await l(h,E);if(B.error){k.innerHTML=`
|
|
|
|
|
<div style="padding: 20px; text-align: center; color: var(--bad-fg);">
|
|
|
|
|
<div style="font-size: 1.2rem; margin-bottom: 8px;">\u26A0\uFE0F Error</div>
|
|
|
|
|
<div>${escapeHtml(T.error)}</div>
|
|
|
|
|
</div>`;return}C.innerHTML=`
|
|
|
|
|
<div>${escapeHtml(B.error)}</div>
|
|
|
|
|
</div>`;return}k.innerHTML=`
|
|
|
|
|
<div style="display: flex; gap: 12px; padding: 8px 10px; background: var(--card-base); border-bottom: 2px solid var(--border); font-size: 0.75rem; font-weight: 600; color: var(--muted); position: sticky; top: 0;">
|
|
|
|
|
<span style="flex-shrink: 0; width: 80px;">Stream</span>
|
|
|
|
|
<span style="flex: 1;">Log Output</span>
|
|
|
|
|
</div>`,T.logs&&T.logs.length>0?(T.logs.forEach($=>{const I=y($);C.appendChild(I)}),C.scrollTop=C.scrollHeight):C.innerHTML+=`
|
|
|
|
|
</div>`,B.logs&&B.logs.length>0?(B.logs.forEach($=>{const I=f($);k.appendChild(I)}),k.scrollTop=k.scrollHeight):k.innerHTML+=`
|
|
|
|
|
<div style="padding: 20px; text-align: center; color: var(--muted);">
|
|
|
|
|
No logs available for this container
|
|
|
|
|
</div>`}catch(T){C.innerHTML=`
|
|
|
|
|
</div>`}catch(B){k.innerHTML=`
|
|
|
|
|
<div style="padding: 20px; text-align: center; color: var(--bad-fg);">
|
|
|
|
|
Failed to fetch logs: ${escapeHtml(T.message)}
|
|
|
|
|
</div>`}}function k(E,C){h=E,b=C,g=!0,d=!1,m=!1,i();const T=document.getElementById("logs-modal"),$=document.getElementById("logs-title"),I=document.getElementById("logs-pause"),P=document.getElementById("logs-stream");$.textContent=`\u{1F4CB} ${C} - Container Logs`,I.textContent="\u23F8\uFE0F Pause",I.classList.remove("paused"),P&&(P.style.display=""),T.classList.add("show"),S(),a=setInterval(S,DC.POLL.LOGS)}async function L(E,C=100){try{const T=`/api/v1/logs/file?path=${encodeURIComponent(E)}&tail=${C}`,$=await fetch(T,{cache:"no-store",headers:{Accept:"application/json","Cache-Control":"no-cache"}});if($.ok){const I=await $.json();return I.success&&I.logs?{logs:I.logs,count:I.count,logPath:I.logPath,totalLines:I.totalLines}:{error:I.error||"Failed to fetch file logs"}}else return{error:(await $.json().catch(()=>({}))).error||`HTTP ${$.status}`}}catch(T){return console.error("File logs fetch failed:",T),{error:T.message}}}function x(E){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 T=E.text;let $="INFO",I="var(--fg)";T.match(/ERROR|FATAL|CRITICAL/i)?($="ERROR",I="var(--bad-fg)"):T.match(/WARN|WARNING/i)?($="WARN",I="#f39c12"):T.match(/DEBUG/i)&&($="DEBUG",I="var(--muted)");const D=`<span style="background: ${I==="var(--bad-fg)"?"var(--bad-bg)":"var(--ok-bg)"}; color: ${I}; padding: 2px 6px; border-radius: 4px; font-size: 0.7rem; font-weight: 600; min-width: 50px; text-align: center;">${$}</span>`;return C.innerHTML=`
|
|
|
|
|
Failed to fetch logs: ${escapeHtml(B.message)}
|
|
|
|
|
</div>`}}function S(E,k){h=E,b=k,g=!0,c=!1,m=!1,d();const B=document.getElementById("logs-modal"),$=document.getElementById("logs-title"),I=document.getElementById("logs-pause"),P=document.getElementById("logs-stream");$.textContent=`\u{1F4CB} ${k} - Container Logs`,I.textContent="\u23F8\uFE0F Pause",I.classList.remove("paused"),P&&(P.style.display=""),B.classList.add("show"),C(),o=setInterval(C,DC.POLL.LOGS)}async function T(E,k=100){try{const B=`/api/v1/logs/file?path=${encodeURIComponent(E)}&tail=${k}`,$=await fetch(B,{cache:"no-store",headers:{Accept:"application/json","Cache-Control":"no-cache"}});if($.ok){const I=await $.json();return I.success&&I.logs?{logs:I.logs,count:I.count,logPath:I.logPath,totalLines:I.totalLines}:{error:I.error||"Failed to fetch file logs"}}else return{error:(await $.json().catch(()=>({}))).error||`HTTP ${$.status}`}}catch(B){return console.error("File logs fetch failed:",B),{error:B.message}}}function w(E){const k=document.createElement("div");k.className="log-entry",k.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 B=E.text;let $="INFO",I="var(--fg)";B.match(/ERROR|FATAL|CRITICAL/i)?($="ERROR",I="var(--bad-fg)"):B.match(/WARN|WARNING/i)?($="WARN",I="#f39c12"):B.match(/DEBUG/i)&&($="DEBUG",I="var(--muted)");const D=`<span style="background: ${I==="var(--bad-fg)"?"var(--bad-bg)":"var(--ok-bg)"}; color: ${I}; padding: 2px 6px; border-radius: 4px; font-size: 0.7rem; font-weight: 600; min-width: 50px; text-align: center;">${$}</span>`;return k.innerHTML=`
|
|
|
|
|
<div style="flex-shrink: 0;">${D}</div>
|
|
|
|
|
<div style="flex: 1; color: ${I}; word-break: break-all; white-space: pre-wrap;">${escapeHtml(T)}</div>
|
|
|
|
|
`,C}async function B(){if(m||!v||!d)return;const E=parseInt(document.getElementById("log-lines").value),C=document.getElementById("logs-content");try{const T=await L(v,E);if(T.error){C.innerHTML=`
|
|
|
|
|
<div style="flex: 1; color: ${I}; word-break: break-all; white-space: pre-wrap;">${escapeHtml(B)}</div>
|
|
|
|
|
`,k}async function L(){if(m||!v||!c)return;const E=parseInt(document.getElementById("log-lines").value),k=document.getElementById("logs-content");try{const B=await T(v,E);if(B.error){k.innerHTML=`
|
|
|
|
|
<div style="padding: 20px; text-align: center; color: var(--bad-fg);">
|
|
|
|
|
<div style="font-size: 1.2rem; margin-bottom: 8px;">\u26A0\uFE0F Error</div>
|
|
|
|
|
<div>${escapeHtml(T.error)}</div>
|
|
|
|
|
</div>`;return}C.innerHTML=`
|
|
|
|
|
<div>${escapeHtml(B.error)}</div>
|
|
|
|
|
</div>`;return}k.innerHTML=`
|
|
|
|
|
<div style="display: flex; gap: 12px; padding: 8px 10px; background: var(--card-base); border-bottom: 2px solid var(--border); font-size: 0.75rem; font-weight: 600; color: var(--muted); position: sticky; top: 0;">
|
|
|
|
|
<span style="flex: 1;">Log Output (${T.count} of ${T.totalLines} lines)</span>
|
|
|
|
|
</div>`,T.logs&&T.logs.length>0?(T.logs.forEach($=>{const I=x($);C.appendChild(I)}),C.scrollTop=C.scrollHeight):C.innerHTML+=`
|
|
|
|
|
<span style="flex: 1;">Log Output (${B.count} of ${B.totalLines} lines)</span>
|
|
|
|
|
</div>`,B.logs&&B.logs.length>0?(B.logs.forEach($=>{const I=w($);k.appendChild(I)}),k.scrollTop=k.scrollHeight):k.innerHTML+=`
|
|
|
|
|
<div style="padding: 20px; text-align: center; color: var(--muted);">
|
|
|
|
|
No logs available in this file
|
|
|
|
|
</div>`}catch(T){C.innerHTML=`
|
|
|
|
|
</div>`}catch(B){k.innerHTML=`
|
|
|
|
|
<div style="padding: 20px; text-align: center; color: var(--bad-fg);">
|
|
|
|
|
Failed to fetch logs: ${escapeHtml(T.message)}
|
|
|
|
|
</div>`}}function A(E,C){v=E,p=C,d=!0,g=!1,m=!1;const T=document.getElementById("logs-modal"),$=document.getElementById("logs-title"),I=document.getElementById("logs-pause"),P=document.getElementById("logs-stream");$.textContent=`\u{1F4CB} ${C} - Application Logs`,I.textContent="\u23F8\uFE0F Pause",I.classList.remove("paused"),P&&(P.style.display="none"),T.classList.add("show"),B(),a=setInterval(B,DC.POLL.LOGS)}document.querySelector(".top")?.addEventListener("click",E=>{const C=E.target.closest('[id$="-logs"]');if(!C)return;const T=C.id.replace("-logs","");SITE.dnsServers[T]&&o(T)}),document.getElementById("logs-close")?.addEventListener("click",u),document.getElementById("logs-pause")?.addEventListener("click",()=>{m=!m;const E=document.getElementById("logs-pause");m?(E.textContent="\u25B6\uFE0F Resume",E.classList.add("paused")):(E.textContent="\u23F8\uFE0F Pause",E.classList.remove("paused"),e())}),document.getElementById("log-lines")?.addEventListener("change",()=>{m||e()}),document.getElementById("logs-stream")?.addEventListener("click",()=>{!g||!h||(f?i():n(h))}),document.getElementById("logs-modal")?.addEventListener("click",E=>{E.target.id==="logs-modal"&&u()}),document.addEventListener("keydown",E=>{E.key==="Escape"&&document.getElementById("logs-modal")?.classList.contains("show")&&u()}),window.openContainerLogsModal=k,window.openFileLogsModal=A,window.openLogsModal=o})(),(function(){injectModal("service-edit-modal",`
|
|
|
|
|
Failed to fetch logs: ${escapeHtml(B.message)}
|
|
|
|
|
</div>`}}function A(E,k){v=E,p=k,c=!0,g=!1,m=!1;const B=document.getElementById("logs-modal"),$=document.getElementById("logs-title"),I=document.getElementById("logs-pause"),P=document.getElementById("logs-stream");$.textContent=`\u{1F4CB} ${k} - Application Logs`,I.textContent="\u23F8\uFE0F Pause",I.classList.remove("paused"),P&&(P.style.display="none"),B.classList.add("show"),L(),o=setInterval(L,DC.POLL.LOGS)}document.querySelector(".top")?.addEventListener("click",E=>{const k=E.target.closest('[id$="-logs"]');if(!k)return;const B=k.id.replace("-logs","");SITE.dnsServers[B]&&t(B)}),document.getElementById("logs-close")?.addEventListener("click",i),document.getElementById("logs-pause")?.addEventListener("click",()=>{m=!m;const E=document.getElementById("logs-pause");m?(E.textContent="\u25B6\uFE0F Resume",E.classList.add("paused")):(E.textContent="\u23F8\uFE0F Pause",E.classList.remove("paused"),e())}),document.getElementById("log-lines")?.addEventListener("change",()=>{m||e()}),document.getElementById("logs-stream")?.addEventListener("click",()=>{!g||!h||(y?d():n(h))}),document.getElementById("logs-modal")?.addEventListener("click",E=>{E.target.id==="logs-modal"&&i()}),document.addEventListener("keydown",E=>{E.key==="Escape"&&document.getElementById("logs-modal")?.classList.contains("show")&&i()}),window.openContainerLogsModal=S,window.openFileLogsModal=A,window.openLogsModal=t})(),(function(){injectModal("service-edit-modal",`
|
|
|
|
|
<div id="service-edit-modal" class="weather-modal">
|
|
|
|
|
<div class="weather-modal-content" style="min-width: 500px; max-width: 600px;">
|
|
|
|
|
<h3 id="service-edit-title">Edit Service</h3>
|
|
|
|
@@ -604,36 +604,53 @@ Instructions: ${o.instructionsLink}`:"";showNotification(`${p.toUpperCase()} upd
|
|
|
|
|
|
|
|
|
|
<div class="grid-2col">
|
|
|
|
|
<div>
|
|
|
|
|
<label for="service-port-input" style="font-size: 0.8rem; color: var(--muted); margin-bottom: 4px; display: block;">Port</label>
|
|
|
|
|
<input type="number" id="service-port-input" placeholder="e.g., 8096" style="font-size: 1rem;" />
|
|
|
|
|
<label for="service-subdomain-input" style="font-size: 0.8rem; color: var(--muted); margin-bottom: 4px; display: block;">Subdomain</label>
|
|
|
|
|
<input type="text" id="service-subdomain-input" placeholder="auto-derived from name" required />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label for="service-ip-input" style="font-size: 0.8rem; color: var(--muted); margin-bottom: 4px; display: block;">IP Address</label>
|
|
|
|
|
<input type="text" id="service-ip-input" placeholder="Auto-detected" style="font-size: 1rem;" />
|
|
|
|
|
<div class="quick-ip-buttons" style="display: flex; gap: 4px; margin-top: 4px; flex-wrap: wrap;">
|
|
|
|
|
<button type="button" class="quick-ip-btn" data-ip="127.0.0.1" title="Localhost" style="font-size: 0.7rem; padding: 2px 6px;">localhost</button>
|
|
|
|
|
<button type="button" class="quick-ip-btn" data-ip="" id="quick-ip-lan" title="LAN IP" style="font-size: 0.7rem; padding: 2px 6px;">LAN</button>
|
|
|
|
|
<button type="button" class="quick-ip-btn" data-ip="" id="quick-ip-tailscale" title="Tailscale IP" style="font-size: 0.7rem; padding: 2px 6px;">Tailscale</button>
|
|
|
|
|
</div>
|
|
|
|
|
<label for="service-port-input" style="font-size: 0.8rem; color: var(--muted); margin-bottom: 4px; display: block;">Port</label>
|
|
|
|
|
<input type="number" id="service-port-input" placeholder="e.g., 8096" min="1" max="65535" required style="font-size: 1rem;" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label for="service-source-url" style="font-size: 0.8rem; color: var(--muted); margin-bottom: 4px; display: block;">Source URL</label>
|
|
|
|
|
<input type="url" id="service-source-url" placeholder="https://github.com/owner/repo" required style="font-size: 1rem;" />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="grid-2col">
|
|
|
|
|
<div>
|
|
|
|
|
<label for="service-sha256-input">Sha256 pin (optional)</label>
|
|
|
|
|
<input type="text" id="service-sha256-input" maxlength="64" autocomplete="off" placeholder="64 lowercase hex characters" />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label for="service-git-token">Token (optional)</label>
|
|
|
|
|
<input type="password" id="service-git-token" maxlength="512" autocomplete="off" placeholder="Private repositories" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="grid-2col">
|
|
|
|
|
<div>
|
|
|
|
|
<label for="service-ip-input" style="font-size: 0.8rem; color: var(--muted); margin-bottom: 4px; display: block;">Deployed Host / IP</label>
|
|
|
|
|
<input type="text" id="service-ip-input" value="localhost" placeholder="localhost" style="font-size: 1rem;" />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label for="service-logo-input" style="font-size: 0.8rem; color: var(--muted); margin-bottom: 4px; display: block;">Logo URL</label>
|
|
|
|
|
<input type="text" id="service-logo-input" placeholder="/assets/name.png" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<details id="shipdeckfile-preview">
|
|
|
|
|
<summary id="shipdeckfile-toggle" style="cursor: pointer; color: var(--accent); font-size: 0.8rem; user-select: none;">Show Shipdeckfile</summary>
|
|
|
|
|
<pre id="shipdeckfile-content" style="white-space: pre-wrap; max-height: 220px; overflow: auto; font-size: 0.72rem; background: var(--card-bg); padding: 10px; border-radius: 6px;">Deploy the service to render its immutable Shipdeckfile.</pre>
|
|
|
|
|
</details>
|
|
|
|
|
<div id="shipdeck-deploy-status" aria-live="polite" style="font-size: 0.78rem; color: var(--accent); min-height: 1.2em;"></div>
|
|
|
|
|
|
|
|
|
|
<!-- Options (collapsed by default) -->
|
|
|
|
|
<details id="local-advanced-options">
|
|
|
|
|
<summary style="cursor: pointer; color: var(--accent); font-size: 0.8rem; user-select: none;">Options</summary>
|
|
|
|
|
<div style="margin-top: 10px; display: grid; gap: 10px; font-size: 0.8rem;">
|
|
|
|
|
|
|
|
|
|
<div class="grid-2col">
|
|
|
|
|
<div>
|
|
|
|
|
<label for="service-subdomain-input">Subdomain:</label>
|
|
|
|
|
<input type="text" id="service-subdomain-input" placeholder="auto-derived from name" />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label for="service-logo-input">Logo URL:</label>
|
|
|
|
|
<input type="text" id="service-logo-input" placeholder="/assets/name.png" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; align-items: start;">
|
|
|
|
|
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer;">
|
|
|
|
|
<input type="checkbox" id="create-dns-record" checked />
|
|
|
|
@@ -805,45 +822,45 @@ Instructions: ${o.instructionsLink}`:"";showNotification(`${p.toUpperCase()} upd
|
|
|
|
|
<button id="add-service-create" class="btn-accent">Create Service</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>`)})(),(function(){async function c(g){try{const v=await fetch(`/api/v1/caddy/cas?caddyfilePath=${encodeURIComponent(g)}`);if(!v.ok)throw new Error(`Failed to load CAs: ${v.status}`);const p=await v.json();if(p.success){const d=document.getElementById("existing-ca-select");return d.innerHTML="",p.cas.length===0?d.innerHTML='<option value="">No CAs found in Caddyfile</option>':(d.innerHTML='<option value="">Select existing CA...</option>',p.cas.forEach(r=>{const f=document.createElement("option");typeof r=="object"?(f.value=r.id,f.textContent=r.displayName||r.name):(f.value=r,f.textContent=r),d.appendChild(f)})),p.data.cas}else throw new Error(p.message)}catch(v){console.error("Error loading CAs:",v);const p=document.getElementById("existing-ca-select");return p.innerHTML='<option value="">Error loading CAs</option>',[]}}function a(g){const{subdomain:v,port:p,ip:d,sslType:r,caName:f,existingCa:w,enableAuth:s,enableCors:t,customHeaders:e,upstreamPath:o,healthCheck:u,timeout:n,tailscaleOnly:i}=g;let l=`${buildDomain(v)} {
|
|
|
|
|
`;switch(i&&(l+=` @blocked not remote_ip 100.64.0.0/10
|
|
|
|
|
</div>`)})(),(function(){async function u(g){try{const v=await fetch(`/api/v1/caddy/cas?caddyfilePath=${encodeURIComponent(g)}`);if(!v.ok)throw new Error(`Failed to load CAs: ${v.status}`);const p=await v.json();if(p.success){const c=document.getElementById("existing-ca-select");return c.innerHTML="",p.cas.length===0?c.innerHTML='<option value="">No CAs found in Caddyfile</option>':(c.innerHTML='<option value="">Select existing CA...</option>',p.cas.forEach(s=>{const y=document.createElement("option");typeof s=="object"?(y.value=s.id,y.textContent=s.displayName||s.name):(y.value=s,y.textContent=s),c.appendChild(y)})),p.data.cas}else throw new Error(p.message)}catch(v){console.error("Error loading CAs:",v);const p=document.getElementById("existing-ca-select");return p.innerHTML='<option value="">Error loading CAs</option>',[]}}function o(g){const{subdomain:v,port:p,ip:c,sslType:s,caName:y,existingCa:x,enableAuth:r,enableCors:a,customHeaders:e,upstreamPath:t,healthCheck:i,timeout:n,tailscaleOnly:d}=g;let l=`${buildDomain(v)} {
|
|
|
|
|
`;switch(d&&(l+=` @blocked not remote_ip 100.64.0.0/10
|
|
|
|
|
`,l+=` respond @blocked "Access denied. Tailscale connection required." 403
|
|
|
|
|
`),r){case"letsencrypt":break;case"caddy-managed":l+=` tls internal
|
|
|
|
|
`;break;case"existing-ca":w&&(l+=` tls {
|
|
|
|
|
ca ${w}
|
|
|
|
|
`),s){case"letsencrypt":break;case"caddy-managed":l+=` tls internal
|
|
|
|
|
`;break;case"existing-ca":x&&(l+=` tls {
|
|
|
|
|
ca ${x}
|
|
|
|
|
}
|
|
|
|
|
`);break;case"custom-ca":f&&(l+=` tls {
|
|
|
|
|
ca ${f}
|
|
|
|
|
`);break;case"custom-ca":y&&(l+=` tls {
|
|
|
|
|
ca ${y}
|
|
|
|
|
}
|
|
|
|
|
`);break}if(s&&(l+=` basicauth {
|
|
|
|
|
`);break}if(r&&(l+=` basicauth {
|
|
|
|
|
admin $2a$14$hashed_password_here
|
|
|
|
|
}
|
|
|
|
|
`),t&&(l+=` header {
|
|
|
|
|
`),a&&(l+=` header {
|
|
|
|
|
`,l+=` Access-Control-Allow-Origin "*"
|
|
|
|
|
`,l+=` Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
|
|
|
|
|
`,l+=` Access-Control-Allow-Headers "Content-Type, Authorization"
|
|
|
|
|
`,l+=` }
|
|
|
|
|
`),e)try{const y=JSON.parse(e);l+=` header {
|
|
|
|
|
`,Object.entries(y).forEach(([S,k])=>{l+=` ${S} "${k}"
|
|
|
|
|
`),e)try{const f=JSON.parse(e);l+=` header {
|
|
|
|
|
`,Object.entries(f).forEach(([C,S])=>{l+=` ${C} "${S}"
|
|
|
|
|
`}),l+=` }
|
|
|
|
|
`}catch{console.warn("Invalid JSON in custom headers")}return u&&(l+=` health_uri ${u}
|
|
|
|
|
`),l+=` reverse_proxy ${d}:${p} {
|
|
|
|
|
`,o&&o!=="/"&&(l+=` rewrite ${o}
|
|
|
|
|
`}catch{console.warn("Invalid JSON in custom headers")}return i&&(l+=` health_uri ${i}
|
|
|
|
|
`),l+=` reverse_proxy ${c}:${p} {
|
|
|
|
|
`,t&&t!=="/"&&(l+=` rewrite ${t}
|
|
|
|
|
`),n&&n!==30&&(l+=` transport http {
|
|
|
|
|
`,l+=` dial_timeout ${n}s
|
|
|
|
|
`,l+=` response_header_timeout ${n}s
|
|
|
|
|
`,l+=` }
|
|
|
|
|
`),l+=` }
|
|
|
|
|
`,l+=`}
|
|
|
|
|
`,l}async function m(g,v,p=DC.DEFAULTS.TTL){const d=window.getToken(getPrimaryDnsId(),"admin");if(!d)throw new Error("DNS admin token not configured. Please set it in the Tokens menu.");const r=buildDomain(g),f=await secureFetch("/api/v1/dns/record",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:r,ip:v,ttl:p,token:d,server:SITE.dnsIp})});if(!f.ok){const s=await f.text();throw new Error(`DNS API Error: ${f.status} - ${s}`)}const w=await f.json();if(!w.success)throw new Error(`DNS Error: ${w.error||"Unknown error"}`);return w}async function h(g){const v={id:g.subdomain,name:g.name,logo:g.logo||`/assets/${g.subdomain}.png`};g.category&&(v.category=g.category),g.containerId&&(v.containerId=g.containerId);try{const p=await secureFetch("/api/v1/services",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(v)});if(!p.ok){const d=await p.json();throw new Error(d.error||"Failed to save service")}return await window.loadServices(),window.buildGrid(),v}catch(p){throw console.error("Failed to add service to config:",p),p}}async function b(g){const v=document.getElementById("service-subdomain-input").value.trim(),p=document.getElementById("service-ip-input").value.trim()||"localhost",d=document.getElementById("service-port-input").value.trim()||"80",r=await secureFetch("/api/v1/site",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:buildDomain(v),upstream:`${p}:${d}`,config:g})}),f=await r.json();if(!r.ok||!f.success)throw new Error(f.error||`Caddy API Error: ${r.status}`);return f}window.loadExistingCAs=c,window.generateCaddyConfig=a,window.createDnsRecord=m,window.addServiceToConfig=h,window.addToCaddyfile=b})(),(function(){let c=null;function a(p){c=p;const d=document.getElementById("service-edit-modal");document.getElementById("service-edit-title").textContent=`Edit ${p.name}`,document.getElementById("edit-service-name").value=p.name,document.getElementById("edit-service-url-display").textContent=p.url||buildServiceUrl(p.id),document.getElementById("edit-service-logo-preview").src=p.logo||`/assets/${p.id}.png`,document.getElementById("edit-subdomain").value=p.id,document.getElementById("edit-port").value=p.port||"",document.getElementById("edit-ip").value=p.ip||"localhost",document.getElementById("edit-tailscale-only").checked=p.tailscaleOnly||!1,document.getElementById("edit-logo-url").value=p.logo||"";const r=document.getElementById("edit-service-category");r&&(r.dataset.current=p.category||"",typeof window.populateCategorySelects=="function"&&window.populateCategorySelects()),d.classList.add("show")}function m(){closeModal("service-edit-modal"),c=null}async function h(){if(!c)return;const p=document.getElementById("edit-subdomain").value.trim().toLowerCase(),d=document.getElementById("edit-service-name").value.trim(),r=document.getElementById("edit-port").value.trim(),f=document.getElementById("edit-ip").value.trim()||"localhost",w=document.getElementById("edit-tailscale-only").checked,s=document.getElementById("edit-logo-url").value.trim(),t=document.getElementById("edit-service-category")?.value||"";if(!p){showNotification("Subdomain is required","warning");return}const e=c.id,o=[];if(p!==e&&o.push("subdomain"),d&&d!==c.name&&o.push("name"),r&&r!==String(c.port)&&o.push("port"),f!==c.ip&&o.push("ip"),w!==(c.tailscaleOnly||!1)&&o.push("tailscale"),s&&s!==c.logo&&o.push("logo"),t!==(c.category||"")&&o.push("category"),o.length===0){m();return}const u=document.getElementById("service-edit-save");u.textContent="Saving...",u.disabled=!0;try{const i=await(await secureFetch("/api/v1/services/update",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({oldSubdomain:e,newSubdomain:p,name:d||c.name,port:r||c.port,ip:f,tailscaleOnly:w,logo:s||void 0,category:t})})).json();if(!i.success)throw new Error(i.error||"Failed to update service");const l=window.APPS.findIndex(y=>y.id===e);l!==-1&&(window.APPS[l]={...window.APPS[l],id:p,name:d||window.APPS[l].name,port:r||window.APPS[l].port,ip:f,tailscaleOnly:w,logo:s||window.APPS[l].logo,category:t||void 0}),m(),window.buildGrid(),window.refreshAll()}catch(n){console.error("Error saving service changes:",n),showNotification(`Error saving changes: ${n.message}`,"error")}finally{u.textContent="Save Changes",u.disabled=!1}}document.getElementById("edit-logo-file")?.addEventListener("change",async p=>{const d=p.target.files[0];if(!d)return;if(!d.type.startsWith("image/")){showNotification("Please select an image file","warning");return}const r=new FileReader;r.onload=async f=>{const w=f.target.result;if(document.getElementById("edit-service-logo-preview").src=w,document.getElementById("edit-logo-url").value=w,c)try{const t=await(await secureFetch("/api/v1/assets/upload",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({filename:`${c.id}.png`,data:w})})).json();t.success&&t.path&&(document.getElementById("edit-logo-url").val
Line truncated
|
|
|
|
|
`,l}async function m(g,v,p=DC.DEFAULTS.TTL){const c=window.getToken(getPrimaryDnsId(),"admin");if(!c)throw new Error("DNS admin token not configured. Please set it in the Tokens menu.");const s=buildDomain(g),y=await secureFetch("/api/v1/dns/record",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:s,ip:v,ttl:p,token:c,server:SITE.dnsIp})});if(!y.ok){const r=await y.text();throw new Error(`DNS API Error: ${y.status} - ${r}`)}const x=await y.json();if(!x.success)throw new Error(`DNS Error: ${x.error||"Unknown error"}`);return x}async function h(g){const v={id:g.subdomain,name:g.name,logo:g.logo||`/assets/${g.subdomain}.png`};g.category&&(v.category=g.category),g.containerId&&(v.containerId=g.containerId);try{const p=await secureFetch("/api/v1/services",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(v)});if(!p.ok){const c=await p.json();throw new Error(c.error||"Failed to save service")}return await window.loadServices(),window.buildGrid(),v}catch(p){throw console.error("Failed to add service to config:",p),p}}async function b(g){const v=document.getElementById("service-subdomain-input").value.trim(),p=document.getElementById("service-ip-input").value.trim()||"localhost",c=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(v),upstream:`${p}:${c}`,config:g})}),y=await s.json();if(!s.ok||!y.success)throw new Error(y.error||`Caddy API Error: ${s.status}`);return y}window.loadExistingCAs=u,window.generateCaddyConfig=o,window.createDnsRecord=m,window.addServiceToConfig=h,window.addToCaddyfile=b})(),(function(){let u=null;function o(p){u=p;const c=document.getElementById("service-edit-modal");document.getElementById("service-edit-title").textContent=`Edit ${p.name}`,document.getElementById("edit-service-name").value=p.name,document.getElementById("edit-service-url-display").textContent=p.url||buildServiceUrl(p.id),document.getElementById("edit-service-logo-preview").src=p.logo||`/assets/${p.id}.png`,document.getElementById("edit-subdomain").value=p.id,document.getElementById("edit-port").value=p.port||"",document.getElementById("edit-ip").value=p.ip||"localhost",document.getElementById("edit-tailscale-only").checked=p.tailscaleOnly||!1,document.getElementById("edit-logo-url").value=p.logo||"";const s=document.getElementById("edit-service-category");s&&(s.dataset.current=p.category||"",typeof window.populateCategorySelects=="function"&&window.populateCategorySelects()),c.classList.add("show")}function m(){closeModal("service-edit-modal"),u=null}async function h(){if(!u)return;const p=document.getElementById("edit-subdomain").value.trim().toLowerCase(),c=document.getElementById("edit-service-name").value.trim(),s=document.getElementById("edit-port").value.trim(),y=document.getElementById("edit-ip").value.trim()||"localhost",x=document.getElementById("edit-tailscale-only").checked,r=document.getElementById("edit-logo-url").value.trim(),a=document.getElementById("edit-service-category")?.value||"";if(!p){showNotification("Subdomain is required","warning");return}const e=u.id,t=[];if(p!==e&&t.push("subdomain"),c&&c!==u.name&&t.push("name"),s&&s!==String(u.port)&&t.push("port"),y!==u.ip&&t.push("ip"),x!==(u.tailscaleOnly||!1)&&t.push("tailscale"),r&&r!==u.logo&&t.push("logo"),a!==(u.category||"")&&t.push("category"),t.length===0){m();return}const i=document.getElementById("service-edit-save");i.textContent="Saving...",i.disabled=!0;try{const d=await(await secureFetch("/api/v1/services/update",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({oldSubdomain:e,newSubdomain:p,name:c||u.name,port:s||u.port,ip:y,tailscaleOnly:x,logo:r||void 0,category:a})})).json();if(!d.success)throw new Error(d.error||"Failed to update service");const l=window.APPS.findIndex(f=>f.id===e);l!==-1&&(window.APPS[l]={...window.APPS[l],id:p,name:c||window.APPS[l].name,port:s||window.APPS[l].port,ip:y,tailscaleOnly:x,logo:r||window.APPS[l].logo,category:a||void 0}),m(),window.buildGrid(),window.refreshAll()}catch(n){console.error("Error saving service changes:",n),showNotification(`Error saving changes: ${n.message}`,"error")}finally{i.textContent="Save Changes",i.disabled=!1}}document.getElementById("edit-logo-file")?.addEventListener("change",async p=>{const c=p.target.files[0];if(!c)return;if(!c.type.startsWith("image/")){showNotification("Please select an image file","warning");return}const s=new FileReader;s.onload=async y=>{const x=y.target.result;if(document.getElementById("edit-service-logo-preview").src=x,document.getElementById("edit-logo-url").value=x,u)try{const a=await(await secureFetch("/api/v1/assets/upload",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({filename:`${u.id}.png`,data:x})})).json();a.success&&a.path&&(document.getElementById("edit-logo-url").val
Line truncated
|
|
|
|
|
|
|
|
|
|
This will:
|
|
|
|
|
1. Pull the latest image
|
|
|
|
|
2. Stop the container
|
|
|
|
|
3. Recreate with same settings
|
|
|
|
|
|
|
|
|
|
The service will be briefly unavailable.`))try{f&&(f.textContent="\u{1F504}",f.disabled=!0,f.title="Updating...");const t=await(await secureFetch(`/api/v1/containers/${p}/update`,{method:"POST"})).json();if(t.success){const e=window.APPS.find(o=>o.id===r);e&&t.newContainerId&&(e.containerId=t.newContainerId),f&&(f.textContent="\u2705",f.title="Updated successfully!",setTimeout(()=>{f.textContent=w,f.disabled=!1,f.title="Update container to latest version"},3e3)),setTimeout(()=>window.refreshAll(),2e3),showNotification(`${d} updated successfully!`,"success")}else throw new Error(t.error||"Update failed")}catch(s){console.error("Update error:",s),f&&(f.textContent="\u274C",f.title="Update failed",setTimeout(()=>{f.textContent=w,f.disabled=!1,f.title="Update container to latest version"},3e3)),showNotification(`Failed to update ${d}: ${s.message}`,"error")}}async function v(p,d){const r=window.APPS.find(l=>l.id===p),f=r?buildDomain(r.id):null,w=r?.containerId,s=await b(d||p,w,r?.containerId);if(s===null)return;let t={dashboard:!1,container:null,dns:null,caddy:null,service:null};if(s&&w)try{const l=new URLSearchParams({containerId:r.containerId,subdomain:r.id,ip:r.ip||"localhost",deleteContainer:"true"}),S=await(await secureFetch(`/api/v1/apps/${encodeURIComponent(r.id)}?${l.toString()}`,{method:"DELETE"})).json();S.success?t={...t,...S.results,dashboard:!1}:console.error("App removal failed:",S.error)}catch(l){console.error("App removal error:",l)}else if(s&&f){try{const l=r?.ip||"localhost",S=await(await secureFetch(`/api/v1/dns/record?domain=${encodeURIComponent(f)}&type=A&ipAddress=${encodeURIComponent(l)}&server=${SITE.dnsIp}`,{method:"DELETE"})).json();t.dns=S.success?"deleted":S.error||"failed"}catch(l){t.dns=l.message}try{const y=await(await secureFetch(`/api/v1/site/${encodeURIComponent(f)}`,{method:"DELETE"})).json();t.caddy=y.success||y.error&&y.error.includes("not found")?"removed":y.error||"failed"}catch(l){t.caddy=l.message}}const e=window.APPS.findIndex(l=>l.id===p);e>-1&&(window.APPS.splice(e,1),t.dashboard=!0);try{const l=safeGetJSON("custom-apps",[]),y=l.findIndex(S=>S.id===p);y>-1&&(l.splice(y,1),safeSet("custom-apps",JSON.stringify(l)))}catch{}try{const y=await(await secureFetch(`/api/v1/services/${encodeURIComponent(p)}`,{method:"DELETE"})).json();t.service=y.success?"removed":y.error||"failed"}catch(l){t.service=l.message}window.buildGrid(),window.refreshAll();let o=!1,u=[];t.dashboard||(o=!0,u.push("\u2717 Failed to remove from dashboard"));const n=["removed","already removed","not found","deleted","kept (user choice)","skipped","no such record","does not exist"],i=l=>!l||n.some(y=>l.toLowerCase().includes(y.toLowerCase()));t.container&&!i(t.container)&&(o=!0,u.push(`\u26A0 Container: ${t.container}`)),t.dns&&!i(t.dns)&&(o=!0,u.push(`\u26A0 DNS Record: ${t.dns}`)),t.caddy&&!i(t.caddy)&&(o=!0,u.push(`\u26A0 Caddy Config: ${t.caddy}`)),t.service&&!i(t.service)&&(o=!0,u.push(`\u26A0 Service File: ${t.service}`)),o&&showNotification(`Error deleting "${d||p}": ${u.join(", ")}`,"error",6e3)}window.openServiceEditModal=a,window.showDeleteModal=b,window.updateContainer=g,window.deleteService=v})(),(function(){function c(t){return t.toLowerCase().replace(/\s+/g,"-").replace(/[^a-z0-9-]/g,"").replace(/-+/g,"-").replace(/^-|-$/g,"")}function a(){return SITE.defaults?.sslType||(SITE.configurationType==="public"?"letsencrypt":"caddy-managed")}function m(){const t=document.getElementById("service-subdomain-input").value||"subdomain",e=document.getElementById("service-ip-input").value||h.lan||"localhost",o=document.getElementById("service-port-input").value||DC.DEFAULTS.SERVICE_PORT,u=document.getElementById("ssl-type-select").value,n=document.getElementById("ca-name-input").value||"sami-ca",i=document.getElementById("existing-ca-select").value,l=document.getElementById("enable-auth").checked,y=document.getElementById("enable-cors").checked,S=document.getElementById("custom-headers-input").value,k=document.getElementById("upstream-path-input").value||"/",L=document.getElementById("health-check-input").value,x=document.getElementById("timeout-input").value||30,B=document.getElementById("dns-preview");B&&(B.textContent=`${buildDomain(t)} \u2192 ${e}`);const A=document.getElementById("url-preview");A&&(A.textContent=buildServiceUrl(t));const E={subdomain:t,port:o,ip:e,sslType:u,caName:n,existingCa:i,enableAuth:l,enableCors:y,customHeaders:S,upstreamPath:k,healthCheck:L,timeout:x},C=window.generateCaddyConfig(E),T=document.getElementById("caddy-config-preview");T&&(T.value=C)}const h={localhost:"127.0.0.1",lan:"",tailscale:""};async function b(){try{const u=await fetch("/api/v1/network/ips",{signal:AbortSignal.timeout(2e3)});if(u.ok){const n=await u.json();n.lan&&(h.lan=n.lan),n.tailscale&&(h.tailscale=n.tailscale)}}catch{}const t=document.getElementById("quick-ip-lan"),e=document.getElementById("quick-ip-tailscale");t&&(h.lan?(t.dataset.ip=h.lan,t.textContent=`LAN (${h.lan})`,t.title=`LAN IP: ${h.lan}`):t
Line truncated
|
|
|
|
|
The service will be briefly unavailable.`))try{y&&(y.textContent="\u{1F504}",y.disabled=!0,y.title="Updating...");const a=await(await secureFetch(`/api/v1/containers/${p}/update`,{method:"POST"})).json();if(a.success){const e=window.APPS.find(t=>t.id===s);e&&a.newContainerId&&(e.containerId=a.newContainerId),y&&(y.textContent="\u2705",y.title="Updated successfully!",setTimeout(()=>{y.textContent=x,y.disabled=!1,y.title="Update container to latest version"},3e3)),setTimeout(()=>window.refreshAll(),2e3),showNotification(`${c} updated successfully!`,"success")}else throw new Error(a.error||"Update failed")}catch(r){console.error("Update error:",r),y&&(y.textContent="\u274C",y.title="Update failed",setTimeout(()=>{y.textContent=x,y.disabled=!1,y.title="Update container to latest version"},3e3)),showNotification(`Failed to update ${c}: ${r.message}`,"error")}}async function v(p,c){const s=window.APPS.find(l=>l.id===p),y=s?buildDomain(s.id):null,x=s?.containerId,r=await b(c||p,x,s?.containerId);if(r===null)return;let a={dashboard:!1,container:null,dns:null,caddy:null,service:null};if(r&&x)try{const l=new URLSearchParams({containerId:s.containerId,subdomain:s.id,ip:s.ip||"localhost",deleteContainer:"true"}),C=await(await secureFetch(`/api/v1/apps/${encodeURIComponent(s.id)}?${l.toString()}`,{method:"DELETE"})).json();C.success?a={...a,...C.results,dashboard:!1}:console.error("App removal failed:",C.error)}catch(l){console.error("App removal error:",l)}else if(r&&y){try{const l=s?.ip||"localhost",C=await(await secureFetch(`/api/v1/dns/record?domain=${encodeURIComponent(y)}&type=A&ipAddress=${encodeURIComponent(l)}&server=${SITE.dnsIp}`,{method:"DELETE"})).json();a.dns=C.success?"deleted":C.error||"failed"}catch(l){a.dns=l.message}try{const f=await(await secureFetch(`/api/v1/site/${encodeURIComponent(y)}`,{method:"DELETE"})).json();a.caddy=f.success||f.error&&f.error.includes("not found")?"removed":f.error||"failed"}catch(l){a.caddy=l.message}}const e=window.APPS.findIndex(l=>l.id===p);e>-1&&(window.APPS.splice(e,1),a.dashboard=!0);try{const l=safeGetJSON("custom-apps",[]),f=l.findIndex(C=>C.id===p);f>-1&&(l.splice(f,1),safeSet("custom-apps",JSON.stringify(l)))}catch{}try{const f=await(await secureFetch(`/api/v1/services/${encodeURIComponent(p)}`,{method:"DELETE"})).json();a.service=f.success?"removed":f.error||"failed"}catch(l){a.service=l.message}window.buildGrid(),window.refreshAll();let t=!1,i=[];a.dashboard||(t=!0,i.push("\u2717 Failed to remove from dashboard"));const n=["removed","already removed","not found","deleted","kept (user choice)","skipped","no such record","does not exist"],d=l=>!l||n.some(f=>l.toLowerCase().includes(f.toLowerCase()));a.container&&!d(a.container)&&(t=!0,i.push(`\u26A0 Container: ${a.container}`)),a.dns&&!d(a.dns)&&(t=!0,i.push(`\u26A0 DNS Record: ${a.dns}`)),a.caddy&&!d(a.caddy)&&(t=!0,i.push(`\u26A0 Caddy Config: ${a.caddy}`)),a.service&&!d(a.service)&&(t=!0,i.push(`\u26A0 Service File: ${a.service}`)),t&&showNotification(`Error deleting "${c||p}": ${i.join(", ")}`,"error",6e3)}window.openServiceEditModal=o,window.showDeleteModal=b,window.updateContainer=g,window.deleteService=v})(),(function(){function u(e){return e.toLowerCase().replace(/\s+/g,"-").replace(/[^a-z0-9-]/g,"").replace(/-+/g,"-").replace(/^-|-$/g,"")}function o(){return SITE.defaults?.sslType||(SITE.configurationType==="public"?"letsencrypt":"caddy-managed")}function m(){const e=document.getElementById("service-subdomain-input").value||"subdomain",t=document.getElementById("service-ip-input").value||h.lan||"localhost",i=document.getElementById("service-port-input").value||DC.DEFAULTS.SERVICE_PORT,n=document.getElementById("ssl-type-select").value,d=document.getElementById("ca-name-input").value||"sami-ca",l=document.getElementById("existing-ca-select").value,f=document.getElementById("enable-auth").checked,C=document.getElementById("enable-cors").checked,S=document.getElementById("custom-headers-input").value,T=document.getElementById("upstream-path-input").value||"/",w=document.getElementById("health-check-input").value,L=document.getElementById("timeout-input").value||30,A=document.getElementById("dns-preview");A&&(A.textContent=`${buildDomain(e)} \u2192 ${t}`);const E=document.getElementById("url-preview");E&&(E.textContent=buildServiceUrl(e));const k={subdomain:e,port:i,ip:t,sslType:n,caName:d,existingCa:l,enableAuth:f,enableCors:C,customHeaders:S,upstreamPath:T,healthCheck:w,timeout:L},B=window.generateCaddyConfig(k),$=document.getElementById("caddy-config-preview");$&&($.value=B)}const h={localhost:"127.0.0.1",lan:"",tailscale:""};async function b(){try{const n=await fetch("/api/v1/network/ips",{signal:AbortSignal.timeout(2e3)});if(n.ok){const d=await n.json();d.lan&&(h.lan=d.lan),d.tailscale&&(h.tailscale=d.tailscale)}}catch{}const e=document.getElementById("quick-ip-lan"),t=document.getElementById("quick-ip-tailscale");e&&(h.lan?(e.dataset.ip=h.lan,e.textContent=`LAN (${h.lan})`,e.title=`LAN IP: ${h.lan}`):e
Line truncated
|
|
|
|
|
<span style="color: #4caf50;">\u2713 Connected</span>
|
|
|
|
|
<span style="color: var(--muted); margin-left: 6px;">${y.self?.hostname} (${y.self?.ip})</span>
|
|
|
|
|
`,i.disabled=!1):y.installed?(n.innerHTML='<span style="color: #ff9800;">\u26A0 Not connected</span>',i.disabled=!0):(n.innerHTML='<span style="color: var(--muted);">Not available</span>',i.disabled=!0)}catch{n.innerHTML='<span style="color: var(--muted);">Could not check</span>',i.disabled=!0}i.checked=!1,m()}function p(){const t=document.getElementById("service-type-local"),e=document.getElementById("service-type-external"),o=document.getElementById("local-service-config"),u=document.getElementById("external-service-config"),n=document.getElementById("tab-local"),i=document.getElementById("tab-external");function l(){t.checked?(o.style.display="grid",u.style.display="none",n&&(n.style.background="var(--accent)",n.style.color="var(--bg)"),i&&(i.style.background="transparent",i.style.color="var(--muted)")):(o.style.display="none",u.style.display="block",i&&(i.style.background="var(--accent)",i.style.color="var(--bg)"),n&&(n.style.background="transparent",n.style.color="var(--muted)"))}t?.addEventListener("change",l),e?.addEventListener("change",l)}function d(){const t=document.getElementById("service-name-input"),e=document.getElementById("service-subdomain-input"),o=document.getElementById("subdomain-preview");let u=!1;t?.addEventListener("input",()=>{const k=c(t.value);!u&&e&&(e.value=k),o&&(o.textContent=k?`\u2192 ${buildDomain(k)}`:""),m()}),e?.addEventListener("input",()=>{u=e.value!==c(t?.value||"");const k=e.value.trim()||c(t?.value||"");o&&(o.textContent=k?`\u2192 ${buildDomain(k)}`:""),m()});const n=document.getElementById("external-service-name"),i=document.getElementById("external-service-subdomain"),l=document.getElementById("external-subdomain-preview"),y=document.getElementById("external-domain-preview");let S=!1;n?.addEventListener("input",()=>{const k=c(n.value);!S&&i&&(i.value=k);const L=i?.value||k;l&&(l.textContent=L?`\u2192 ${buildDomain(L)}`:""),y&&(y.textContent=L?buildDomain(L):"")}),i?.addEventListener("input",()=>{S=i.value!==c(n?.value||"");const k=i.value.trim()||c(n?.value||"");l&&(l.textContent=k?`\u2192 ${buildDomain(k)}`:""),y&&(y.textContent=k?buildDomain(k):"")})}async function r(){const t=document.getElementById("external-service-name").value.trim(),e=document.getElementById("external-service-url").value.trim(),o=(document.getElementById("external-service-subdomain").value.trim()||c(t)).toLowerCase(),u=document.getElementById("external-service-logo").value.trim(),n=document.getElementById("external-service-icon").value.trim(),i=document.getElementById("external-create-dns").checked,l=document.getElementById("external-create-caddy").checked,y=document.getElementById("external-proxy-ip").value.trim()||SITE.dnsIp||"localhost",S=document.getElementById("external-preserve-host").checked,k=document.getElementById("external-follow-redirects").checked,L=document.getElementById("external-service-category")?.value||"";if(!t||!e){showNotification("Please fill in Name and External URL","warning");return}if(!o){showNotification("Could not derive subdomain from name. Please set one in Options.","warning");return}if(!e.startsWith("http://")&&!e.startsWith("https://")){showNotification("External URL must start with http:// or https://","warning");return}const x=buildDomain(o);try{const B={dns:null,caddy:null,dashboard:!1};if(i)if(window.getToken(getPrimaryDnsId(),"admin"))try{const P=await(await secureFetch("/api/v1/dns/record",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:x,ip:y,ttl:DC.DEFAULTS.TTL,server:SITE.dnsIp})})).json();B.dns=P.success?"created":P.error||"failed"}catch(I){B.dns=I.message}else B.dns="no admin token (configure in \u{1F511} Tokens)";if(l)try{const $={subdomain:o,externalUrl:e,preserveHost:S,followRedirects:k,sslType:"caddy-managed",caddyfilePath:DC.DEFAULTS.CADDYFILE,reloadCaddy:!0},P=await(await secureFetch("/api/v1/site/external",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify($)})).json();B.caddy=P.success?"created":P.error||"failed"}catch($){B.caddy=$.message}const A={id:o,name:t,url:`https://${x}`,externalUrl:e,logo:u||n||"\u{1F310}",isExternal:!0,isCustom:!0};L&&(A.category=L),window.APPS.push(A),B.dashboard=!0;const E=["plex","router","chat","sync","torrent","radarr","sonarr","prowlarr","portainer","requests","jellyfin","emby"],C=window.APPS.filter($=>!E.includes($.id));safeSet("custom-services",JSON.stringify(C));try{await secureFetch("/api/v1/services",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(window.APPS)})}catch($){console.warn("Failed to save to services.json:",$)}window.buildGrid(),window.refreshAll(),f();const T=[`External service "${t}" added!`];i&&T.push(`DNS: ${B.dns==="created"?"\u2713":"\u26A0 "+B.dns}`),l&&T.push(`Caddy: ${B.caddy==="created"?"\u2713":"\u26A0 "+B.caddy}`),T.push(`Access at: https://${x}`),showNotification(T.join(" | "),"success",6e3)}catch(B){console.error("Failed to create external service:",B),showNotification(`F
Line truncated
|
|
|
|
|
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsiPHN0ZGluPiJdLAogICJzb3VyY2VzQ29udGVudCI6IFsiLyoqXG4gKiBFcnJvciBIYW5kbGVyXG4gKiBIYW5kbGVzIGVycm9ycyBncmFjZWZ1bGx5IHdpdGhvdXQgYnJlYWtpbmcgdGhlIG9uYm9hcmRpbmcgdG91clxuICovXG5cbihmdW5jdGlvbih3aW5kb3cpIHtcbiAgJ3VzZSBzdHJpY3QnO1xuXG4gIGNsYXNzIEVycm9ySGFuZGxlciB7XG4gICAgY29uc3RydWN0b3IoKSB7XG4gICAgICB0aGlzLmVycm9ycyA9IFtdO1xuICAgICAgdGhpcy5tYXhFcnJvcnMgPSA1MDsgLy8gS2VlcCBsYXN0IDUwIGVycm9yc1xuICAgIH1cblxuICAgIC8qKlxuICAgICAqIExvZyBhbiBlcnJvciB3aXRob3V0IGJyZWFraW5nIHRoZSB0b3VyXG4gICAgICogQHBhcmFtIHtzdHJpbmd9IGNvbnRleHQgLSBDb250ZXh0IHdoZXJlIGVycm9yIG9jY3VycmVkXG4gICAgICogQHBhcmFtIHtFcnJvcnxzdHJpbmd9IGVycm9yIC0gVGhlIGVycm9yIG9iamVjdCBvciBtZXNzYWdlXG4gICAgICogQHBhcmFtIHtPYmplY3R9IG1ldGFkYXRhIC0gQWRkaXRpb25hbCBtZXRhZGF0YVxuICAgICAqL1xuICAgIGxvZ0Vycm9yKGNvbnRleHQsIGVycm9yLCBtZXRhZGF0YSA9IHt9KSB7XG4gICAgICBjb25zdCBlcnJvckVudHJ5ID0ge1xuICAgICAgICB0aW1lc3RhbXA6IG5ldyBEYXRlKCkudG9JU09TdHJpbmcoKSxcbiAgICAgICAgY29udGV4dCxcbiAgICAgICAgbWVzc2FnZTogZXJyb3IgaW5zdGFuY2VvZiBFcnJvciA/IGVycm9yLm1lc3NhZ2UgOiBlcnJvcixcbiAgICAgICAgc3RhY2s6IGVycm9yIGluc3RhbmNlb2YgRXJyb3IgPyBlcnJvci5zdGFjayA6IG51bGwsXG4gICAgICAgIG1ldGFkYXRhXG4gICAgICB9O1xuXG4gICAgICAvLyBBZGQgdG8gZXJyb3JzIGFycmF5XG4gICAgICB0aGlzLmVycm9ycy5wdXNoKGVycm9yRW50cnkpO1xuXG4gICAgICAvLyBLZWVwIG9ubHkgbGFzdCBtYXhFcnJvcnNcbiAgICAgIGlmICh0aGlzLmVycm9ycy5sZW5ndGggPiB0aGlzLm1heEVycm9ycykge1xuICAgICAgICB0aGlzLmVycm9ycy5zaGlmdCgpO1xuICAgICAgfVxuXG4gICAgICAvLyBMb2cgdG8gY29uc29sZVxuICAgICAgY29uc29sZS5lcnJvcihgW09uYm9hcmRpbmcgRXJyb3JdICR7Y29udGV4dH06YCwgZXJyb3IsIG1ldGFkYXRhKTtcblxuICAgICAgLy8gT3B0aW9uYWxseSBzZW5kIHRvIGVycm9yIHRyYWNraW5nIHNlcnZpY2VcbiAgICAgIC8vIHRoaXMuc2VuZFRvRXJyb3JUcmFja2luZyhlcnJvckVudHJ5KTtcbiAgICB9XG5cbiAgICAvKipcbiAgICAgKiBBdHRlbXB0IHRvIHJlY292ZXIgZnJvbSBhbiBlcnJvciBhbmQgY29udGludWUgdG91clxuICAgICAqIEBwYXJhbSB7RXJyb3J9IGVycm9yIC0gVGhlIGVycm9yIG9iamVjdFxuICAgICAqIEBwYXJhbSB7bnVtYmVyfSBjdXJyZW50U3RlcCAtIEN1cnJlbnQgc3RlcCBpbmRleFxuICAgICAqIEByZXR1cm5zIHtPYmplY3R9IFJlY292ZXJ5IGFjdGlvblxuICAgICAqL1xuICAgIHJlY292ZXJGcm9tRXJyb3IoZXJyb3IsIGN1cnJlbnRTdGVwKSB7XG4gICAgICBjb25zdCBlcnJvclR5cGUgPSB0aGlzLmNsYXNzaWZ5RXJyb3IoZXJyb3IpO1xuXG4gICAgICBzd2l0Y2ggKGVycm9yVHlwZSkge1xuICAgICAgICBjYXNlICdFTEVNRU5UX05PVF9GT1VORCc6XG4gICAgICAgICAgdGhpcy5sb2dFcnJvcignRWxlbWVudCBOb3QgRm91bmQnLCBlcnJvciwgeyBjdXJyZW50U3RlcCB9KTtcbiAgICAgICAgICByZXR1cm4geyBcbiAgICAgICAgICAgIGFjdGlvbjogJ1NLSVBfU1RFUCcsIFxuICAgICAgICAgICAgbmV4dFN0ZXA6IGN1cnJlbnRTdGVwICsgMSxcbiAgICAgICAgICAgIG1lc3NhZ2U6ICdUYXJnZXQgZWxlbWVudCBub3QgZm91bmQsIHNraXBwaW5nIHRvIG5leHQgc3RlcCdcbiAgICAgICAgICB9O1xuXG4gICAgICAgIGNhc2UgJ1NUT1JBR0VfVU5BVkFJTEFCTEUnOlxuICAgICAgICAgIHRoaXMubG9nRXJyb3IoJ1N0b3JhZ2UgVW5hdmFpbGFibGUnLCBlcnJvcik7XG4gICAgICAgICAgcmV0dXJuIHsgXG4gICAgICAgICAgICBhY3Rpb246ICdVU0VfTUVNT1JZX1NUT1JBR0UnLFxuICAgICAgICAgICAgbWVzc2FnZTogJ0xvY2FsIHN0b3JhZ2UgdW5hdmFpbGFibGUsIHVzaW5nIGluLW1lbW9yeSBzdG9yYWdlJ1xuICAgICAgICAgIH07XG5cbiAgICAgICAgY2FzZSAnRFJJVkVSX05PVF9MT0FERUQnOlxuICAgICAgICAgIHRoaXMubG9nRXJyb3IoJ0RyaXZlci5qcyBOb3QgTG9hZGVkJywgZXJyb3IpO1xuICAgICAgICAgIHJldHVybiB7XG4gICAgICAgICAgICBhY3Rpb246ICdBQk9SVF9UT1VSJyxcbiAgICAgICAgICAgIG1lc3NhZ2U6ICdEcml2ZXIuanMgbGlicmFyeSBub3QgbG9hZGVkLCBjYW5ub3Qgc3RhcnQgdG91cidcbiAgICAgICAgICB9O1xuXG4gICAgICAgIGNhc2UgJ0lOVkFMSURfVE9PTFRJUCc6XG4gICAgICAgICAgdGhpcy5sb2dFcnJvcignSW52YWxpZCBUb29sdGlwIENvbmZpZ3VyYXRpb24nLCBlcnJvciwgeyBjdXJyZW50U3RlcCB9KTtcbiAgICAgICAgICByZXR1cm4ge1xuICAgICAgICAgICAgYWN0aW9uOiAnU0tJUF9TVEVQJyxcbiAgICAgICAgICAgIG5leHRTdGVwOiBjdXJyZW50U3RlcCArIDEsXG4gICAgICAgICAgICBtZXNzYWdlOiAnSW52YWxpZCB0b29sdGlwIGNvbmZpZ3VyYXRpb24sIHNraXBwaW5nJ1xuICAgICAgICAgIH07XG5cbiAgICAgICAgY2FzZSAnVEhFTUVfREVURUNUSU9OX0ZBSUxFRCc6XG4gICAgICAgICAgdGhpcy5sb2dFcnJvcignVGhlbWUgRGV0ZWN0aW9uIEZhaWxlZCcsIGVycm9yKTtcbiAgICAgICAgICByZXR1cm4ge1xuICAgICAgICAgICAgYWN0aW9uOiAnVVNFX0RFRkFVTFRfVEhFTUUnLFxuICAgICAgICAgICAgbWVzc2FnZTogJ1VzaW5nIGRlZmF1bHQgZGFyayB0aGVtZSdcbiAgICAgICAgICB9O1xuXG4gICAgICAgIGRlZmF1bHQ6XG4gICAgICAgICAgdGhpcy5sb2dFcnJvcignVW5rbm93biBFcnJvcicsIGVycm9yLCB7IGN1cnJlbnRTdGVwIH0pO1xuICAgICAgICAgIHJldHVybiB7XG4gICAgICAgICAgICBhY3Rpb246ICdBQk9SVF9UT1VSJyxcbiAgICAgICAgICAgIG1lc3NhZ2U6ICdVbmV4cGVjdGVkIGVycm9yIG9jY3VycmVkLCBhYm9ydGluZyB0b3VyJ1xuICAgICAgICAgIH07XG4gICAgICB9XG4gICAgfVxuXG4gICAgLyoqXG4gICAgICogQ2xhc3NpZnkgZXJyb3IgdHlwZVxuICAgICAqIEBwcml2YXRlXG4gICAgICogQHBhcmFtIHtFcnJvcn0gZXJyb3IgLSBUaGUgZXJyb3Igb2JqZWN0XG4gICAgICogQHJldHVybnMge3N0cmluZ30gRXJyb3IgdHlwZVxuICAgICAqL1xuICAgIGNsYXNzaWZ5RXJyb3IoZXJyb3IpIHtcbiAgICAgIGNvbnN0IG1lc3NhZ2UgPSBlcnJvci5tZXNzYWdlIHx8IGVycm9yLnRvU3RyaW5nKCk7XG5cbiAgICAgIGlmIChtZXNzYWdlLmluY2x1ZGVzKCdlbGVtZW50JykgJiYgbWVzc2FnZS5pbmNsdWRlcygnbm90IGZvdW5kJykpIHtcbiAgICAgICAgcmV0dXJuICdFTEVNRU5UX05PVF9GT1VORCc7XG4gICAgICB9XG4gICAgICBpZiAobWVzc2FnZS5pbmNsdWRlcygnc3RvcmFnZScpIHx8IG1lc3NhZ2UuaW5jbHVkZXMoJ3F1b3RhJykpIHtcbiAgICAgICAgcmV0dXJuICdTVE9SQUdFX1VOQVZBSUxBQkxFJztcbiAgICAgIH1cbiAgICAgIGlmIChtZXNzYWdlLmluY2x1ZGVzKCdkcml2ZXInKSB8fCBtZXNzYWdlLmluY2x1ZGVzKCd1bmRlZmluZWQnKSkge1xuICAgI
Line truncated
|
|
|
|
|
<span style="color: var(--muted); margin-left: 6px;">${S.self?.hostname} (${S.self?.ip})</span>
|
|
|
|
|
`,f.disabled=!1):S.installed?(l.innerHTML='<span style="color: #ff9800;">\u26A0 Not connected</span>',f.disabled=!0):(l.innerHTML='<span style="color: var(--muted);">Not available</span>',f.disabled=!0)}catch{l.innerHTML='<span style="color: var(--muted);">Could not check</span>',f.disabled=!0}f.checked=!1,m()}function p(){const e=document.getElementById("service-type-local"),t=document.getElementById("service-type-external"),i=document.getElementById("local-service-config"),n=document.getElementById("external-service-config"),d=document.getElementById("tab-local"),l=document.getElementById("tab-external");function f(){const C=document.getElementById("add-service-create");e.checked?(i.style.display="grid",n.style.display="none",C&&(C.textContent="Deploy with Shipdeck"),d&&(d.style.background="var(--accent)",d.style.color="var(--bg)"),l&&(l.style.background="transparent",l.style.color="var(--muted)")):(i.style.display="none",n.style.display="block",C&&(C.textContent="Create Service"),l&&(l.style.background="var(--accent)",l.style.color="var(--bg)"),d&&(d.style.background="transparent",d.style.color="var(--muted)"))}e?.addEventListener("change",f),t?.addEventListener("change",f)}function c(){const e=document.getElementById("service-name-input"),t=document.getElementById("service-subdomain-input"),i=document.getElementById("subdomain-preview");let n=!1;e?.addEventListener("input",()=>{const T=u(e.value);!n&&t&&(t.value=T),i&&(i.textContent=T?`\u2192 ${buildDomain(T)}`:""),m()}),t?.addEventListener("input",()=>{n=t.value!==u(e?.value||"");const T=t.value.trim()||u(e?.value||"");i&&(i.textContent=T?`\u2192 ${buildDomain(T)}`:""),m()});const d=document.getElementById("external-service-name"),l=document.getElementById("external-service-subdomain"),f=document.getElementById("external-subdomain-preview"),C=document.getElementById("external-domain-preview");let S=!1;d?.addEventListener("input",()=>{const T=u(d.value);!S&&l&&(l.value=T);const w=l?.value||T;f&&(f.textContent=w?`\u2192 ${buildDomain(w)}`:""),C&&(C.textContent=w?buildDomain(w):"")}),l?.addEventListener("input",()=>{S=l.value!==u(d?.value||"");const T=l.value.trim()||u(d?.value||"");f&&(f.textContent=T?`\u2192 ${buildDomain(T)}`:""),C&&(C.textContent=T?buildDomain(T):"")})}async function s(){const e=document.getElementById("external-service-name").value.trim(),t=document.getElementById("external-service-url").value.trim(),i=(document.getElementById("external-service-subdomain").value.trim()||u(e)).toLowerCase(),n=document.getElementById("external-service-logo").value.trim(),d=document.getElementById("external-service-icon").value.trim(),l=document.getElementById("external-create-dns").checked,f=document.getElementById("external-create-caddy").checked,C=document.getElementById("external-proxy-ip").value.trim()||SITE.dnsIp||"localhost",S=document.getElementById("external-preserve-host").checked,T=document.getElementById("external-follow-redirects").checked,w=document.getElementById("external-service-category")?.value||"";if(!e||!t){showNotification("Please fill in Name and External URL","warning");return}if(!i){showNotification("Could not derive subdomain from name. Please set one in Options.","warning");return}if(!t.startsWith("http://")&&!t.startsWith("https://")){showNotification("External URL must start with http:// or https://","warning");return}const L=buildDomain(i);try{const A={dns:null,caddy:null,dashboard:!1};if(l)if(window.getToken(getPrimaryDnsId(),"admin"))try{const D=await(await secureFetch("/api/v1/dns/record",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:L,ip:C,ttl:DC.DEFAULTS.TTL,server:SITE.dnsIp})})).json();A.dns=D.success?"created":D.error||"failed"}catch(P){A.dns=P.message}else A.dns="no admin token (configure in \u{1F511} Tokens)";if(f)try{const I={subdomain:i,externalUrl:t,preserveHost:S,followRedirects:T,sslType:"caddy-managed",caddyfilePath:DC.DEFAULTS.CADDYFILE,reloadCaddy:!0},D=await(await secureFetch("/api/v1/site/external",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(I)})).json();A.caddy=D.success?"created":D.error||"failed"}catch(I){A.caddy=I.message}const E={id:i,name:e,url:`https://${L}`,externalUrl:t,logo:n||d||"\u{1F310}",isExternal:!0,isCustom:!0};w&&(E.category=w),window.APPS.push(E),A.dashboard=!0;const k=["plex","router","chat","sync","torrent","radarr","sonarr","prowlarr","portainer","requests","jellyfin","emby"],B=window.APPS.filter(I=>!k.includes(I.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(I){console.warn("Failed to save to services.json:",I)}window.buildGrid(),window.refreshAll(),y();const $=[`External service "${e}" added!`];l&&$.push(`DNS: ${A.dns==="created"?"\u2713":"\u26A0 "+A.dns}`),f&&$.push(`Caddy: ${A.caddy==="created"?"\u2713":"\u26A0 "+A.caddy}`),$.push(`Access at: https://${L
Line truncated
|
|
|
|
|
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsiPHN0ZGluPiJdLAogICJzb3VyY2VzQ29udGVudCI6IFsiLyoqXG4gKiBFcnJvciBIYW5kbGVyXG4gKiBIYW5kbGVzIGVycm9ycyBncmFjZWZ1bGx5IHdpdGhvdXQgYnJlYWtpbmcgdGhlIG9uYm9hcmRpbmcgdG91clxuICovXG5cbihmdW5jdGlvbih3aW5kb3cpIHtcbiAgJ3VzZSBzdHJpY3QnO1xuXG4gIGNsYXNzIEVycm9ySGFuZGxlciB7XG4gICAgY29uc3RydWN0b3IoKSB7XG4gICAgICB0aGlzLmVycm9ycyA9IFtdO1xuICAgICAgdGhpcy5tYXhFcnJvcnMgPSA1MDsgLy8gS2VlcCBsYXN0IDUwIGVycm9yc1xuICAgIH1cblxuICAgIC8qKlxuICAgICAqIExvZyBhbiBlcnJvciB3aXRob3V0IGJyZWFraW5nIHRoZSB0b3VyXG4gICAgICogQHBhcmFtIHtzdHJpbmd9IGNvbnRleHQgLSBDb250ZXh0IHdoZXJlIGVycm9yIG9jY3VycmVkXG4gICAgICogQHBhcmFtIHtFcnJvcnxzdHJpbmd9IGVycm9yIC0gVGhlIGVycm9yIG9iamVjdCBvciBtZXNzYWdlXG4gICAgICogQHBhcmFtIHtPYmplY3R9IG1ldGFkYXRhIC0gQWRkaXRpb25hbCBtZXRhZGF0YVxuICAgICAqL1xuICAgIGxvZ0Vycm9yKGNvbnRleHQsIGVycm9yLCBtZXRhZGF0YSA9IHt9KSB7XG4gICAgICBjb25zdCBlcnJvckVudHJ5ID0ge1xuICAgICAgICB0aW1lc3RhbXA6IG5ldyBEYXRlKCkudG9JU09TdHJpbmcoKSxcbiAgICAgICAgY29udGV4dCxcbiAgICAgICAgbWVzc2FnZTogZXJyb3IgaW5zdGFuY2VvZiBFcnJvciA/IGVycm9yLm1lc3NhZ2UgOiBlcnJvcixcbiAgICAgICAgc3RhY2s6IGVycm9yIGluc3RhbmNlb2YgRXJyb3IgPyBlcnJvci5zdGFjayA6IG51bGwsXG4gICAgICAgIG1ldGFkYXRhXG4gICAgICB9O1xuXG4gICAgICAvLyBBZGQgdG8gZXJyb3JzIGFycmF5XG4gICAgICB0aGlzLmVycm9ycy5wdXNoKGVycm9yRW50cnkpO1xuXG4gICAgICAvLyBLZWVwIG9ubHkgbGFzdCBtYXhFcnJvcnNcbiAgICAgIGlmICh0aGlzLmVycm9ycy5sZW5ndGggPiB0aGlzLm1heEVycm9ycykge1xuICAgICAgICB0aGlzLmVycm9ycy5zaGlmdCgpO1xuICAgICAgfVxuXG4gICAgICAvLyBMb2cgdG8gY29uc29sZVxuICAgICAgY29uc29sZS5lcnJvcihgW09uYm9hcmRpbmcgRXJyb3JdICR7Y29udGV4dH06YCwgZXJyb3IsIG1ldGFkYXRhKTtcblxuICAgICAgLy8gT3B0aW9uYWxseSBzZW5kIHRvIGVycm9yIHRyYWNraW5nIHNlcnZpY2VcbiAgICAgIC8vIHRoaXMuc2VuZFRvRXJyb3JUcmFja2luZyhlcnJvckVudHJ5KTtcbiAgICB9XG5cbiAgICAvKipcbiAgICAgKiBBdHRlbXB0IHRvIHJlY292ZXIgZnJvbSBhbiBlcnJvciBhbmQgY29udGludWUgdG91clxuICAgICAqIEBwYXJhbSB7RXJyb3J9IGVycm9yIC0gVGhlIGVycm9yIG9iamVjdFxuICAgICAqIEBwYXJhbSB7bnVtYmVyfSBjdXJyZW50U3RlcCAtIEN1cnJlbnQgc3RlcCBpbmRleFxuICAgICAqIEByZXR1cm5zIHtPYmplY3R9IFJlY292ZXJ5IGFjdGlvblxuICAgICAqL1xuICAgIHJlY292ZXJGcm9tRXJyb3IoZXJyb3IsIGN1cnJlbnRTdGVwKSB7XG4gICAgICBjb25zdCBlcnJvclR5cGUgPSB0aGlzLmNsYXNzaWZ5RXJyb3IoZXJyb3IpO1xuXG4gICAgICBzd2l0Y2ggKGVycm9yVHlwZSkge1xuICAgICAgICBjYXNlICdFTEVNRU5UX05PVF9GT1VORCc6XG4gICAgICAgICAgdGhpcy5sb2dFcnJvcignRWxlbWVudCBOb3QgRm91bmQnLCBlcnJvciwgeyBjdXJyZW50U3RlcCB9KTtcbiAgICAgICAgICByZXR1cm4geyBcbiAgICAgICAgICAgIGFjdGlvbjogJ1NLSVBfU1RFUCcsIFxuICAgICAgICAgICAgbmV4dFN0ZXA6IGN1cnJlbnRTdGVwICsgMSxcbiAgICAgICAgICAgIG1lc3NhZ2U6ICdUYXJnZXQgZWxlbWVudCBub3QgZm91bmQsIHNraXBwaW5nIHRvIG5leHQgc3RlcCdcbiAgICAgICAgICB9O1xuXG4gICAgICAgIGNhc2UgJ1NUT1JBR0VfVU5BVkFJTEFCTEUnOlxuICAgICAgICAgIHRoaXMubG9nRXJyb3IoJ1N0b3JhZ2UgVW5hdmFpbGFibGUnLCBlcnJvcik7XG4gICAgICAgICAgcmV0dXJuIHsgXG4gICAgICAgICAgICBhY3Rpb246ICdVU0VfTUVNT1JZX1NUT1JBR0UnLFxuICAgICAgICAgICAgbWVzc2FnZTogJ0xvY2FsIHN0b3JhZ2UgdW5hdmFpbGFibGUsIHVzaW5nIGluLW1lbW9yeSBzdG9yYWdlJ1xuICAgICAgICAgIH07XG5cbiAgICAgICAgY2FzZSAnRFJJVkVSX05PVF9MT0FERUQnOlxuICAgICAgICAgIHRoaXMubG9nRXJyb3IoJ0RyaXZlci5qcyBOb3QgTG9hZGVkJywgZXJyb3IpO1xuICAgICAgICAgIHJldHVybiB7XG4gICAgICAgICAgICBhY3Rpb246ICdBQk9SVF9UT1VSJyxcbiAgICAgICAgICAgIG1lc3NhZ2U6ICdEcml2ZXIuanMgbGlicmFyeSBub3QgbG9hZGVkLCBjYW5ub3Qgc3RhcnQgdG91cidcbiAgICAgICAgICB9O1xuXG4gICAgICAgIGNhc2UgJ0lOVkFMSURfVE9PTFRJUCc6XG4gICAgICAgICAgdGhpcy5sb2dFcnJvcignSW52YWxpZCBUb29sdGlwIENvbmZpZ3VyYXRpb24nLCBlcnJvciwgeyBjdXJyZW50U3RlcCB9KTtcbiAgICAgICAgICByZXR1cm4ge1xuICAgICAgICAgICAgYWN0aW9uOiAnU0tJUF9TVEVQJyxcbiAgICAgICAgICAgIG5leHRTdGVwOiBjdXJyZW50U3RlcCArIDEsXG4gICAgICAgICAgICBtZXNzYWdlOiAnSW52YWxpZCB0b29sdGlwIGNvbmZpZ3VyYXRpb24sIHNraXBwaW5nJ1xuICAgICAgICAgIH07XG5cbiAgICAgICAgY2FzZSAnVEhFTUVfREVURUNUSU9OX0ZBSUxFRCc6XG4gICAgICAgICAgdGhpcy5sb2dFcnJvcignVGhlbWUgRGV0ZWN0aW9uIEZhaWxlZCcsIGVycm9yKTtcbiAgICAgICAgICByZXR1cm4ge1xuICAgICAgICAgICAgYWN0aW9uOiAnVVNFX0RFRkFVTFRfVEhFTUUnLFxuICAgICAgICAgICAgbWVzc2FnZTogJ1VzaW5nIGRlZmF1bHQgZGFyayB0aGVtZSdcbiAgICAgICAgICB9O1xuXG4gICAgICAgIGRlZmF1bHQ6XG4gICAgICAgICAgdGhpcy5sb2dFcnJvcignVW5rbm93biBFcnJvcicsIGVycm9yLCB7IGN1cnJlbnRTdGVwIH0pO1xuICAgICAgICAgIHJldHVybiB7XG4gICAgICAgICAgICBhY3Rpb246ICdBQk9SVF9UT1VSJyxcbiAgICAgICAgICAgIG1lc3NhZ2U6ICdVbmV4cGVjdGVkIGVycm9yIG9jY3VycmVkLCBhYm9ydGluZyB0b3VyJ1xuICAgICAgICAgIH07XG4gICAgICB9XG4gICAgfVxuXG4gICAgLyoqXG4gICAgICogQ2xhc3NpZnkgZXJyb3IgdHlwZVxuICAgICAqIEBwcml2YXRlXG4gICAgICogQHBhcmFtIHtFcnJvcn0gZXJyb3IgLSBUaGUgZXJyb3Igb2JqZWN0XG4gICAgICogQHJldHVybnMge3N0cmluZ30gRXJyb3IgdHlwZVxuICAgICAqL1xuICAgIGNsYXNzaWZ5RXJyb3IoZXJyb3IpIHtcbiAgICAgIGNvbnN0IG1lc3NhZ2UgPSBlcnJvci5tZXNzYWdlIHx8IGVycm9yLnRvU3RyaW5nKCk7XG5cbiAgICAgIGlmIChtZXNzYWdlLmluY2x1ZGVzKCdlbGVtZW50JykgJiYgbWVzc2FnZS5pbmNsdWRlcygnbm90IGZvdW5kJykpIHtcbiAgICAgICAgcmV0dXJuICdFTEVNRU5UX05PVF9GT1VORCc7XG4gICAgICB9XG4gICAgICBpZiAobWVzc2FnZS5pbmNsdWRlcygnc3RvcmFnZScpIHx8IG1lc3NhZ2UuaW5jbHVkZXMoJ3F1b3RhJykpIHtcbiAgICAgICAgcmV0dXJuICdTVE9SQUdFX1VOQVZBSUxBQkxFJztcbiAgICAgIH1cbiAgICAgIGlmIChtZXNzYWdlLmluY2x1ZGVzKCdkcml2ZXInKSB8fCBtZXNzYWdlLmluY2x1ZGVzKCd1bmRlZmluZWQnKSkge1xuICAgI
Line truncated
|