diff --git a/dashcaddy-api/package.json b/dashcaddy-api/package.json index d003586..7f13a56 100644 --- a/dashcaddy-api/package.json +++ b/dashcaddy-api/package.json @@ -1,6 +1,6 @@ { "name": "dashcaddy-api", - "version": "1.4.1", + "version": "1.4.2", "description": "DashCaddy API server - Dashboard backend for Docker, Caddy & DNS management", "main": "server.js", "scripts": { diff --git a/status/dist/features.js b/status/dist/features.js index ba52e5c..3fc1e09 100644 --- a/status/dist/features.js +++ b/status/dist/features.js @@ -90,9 +90,9 @@ - `);const w=document.getElementById("logo-modal"),B=document.getElementById("logo-preview-dark"),z=document.getElementById("logo-preview-light"),I=document.getElementById("logo-status"),C=document.getElementById("logo-same-both"),P=document.getElementById("logo-dual-uploads"),$=document.getElementById("logo-single-upload"),L=document.getElementById("logo-upload-dark"),g=document.getElementById("logo-upload-light"),k=document.getElementById("logo-upload-single"),f=document.querySelector("#brand .brand-logo-dark"),x=document.querySelector("#brand .brand-logo-light"),E=document.querySelector(".top-row"),R=document.getElementById("dashboard-title"),O=DC.NAME;let D=null,N=null,M=null,H="left",S=O;C?.addEventListener("change",()=>{C.checked?(P.style.display="none",$.style.display="",D=null,N=null):(P.style.display="flex",$.style.display="none",M=null)});function T(n,e){if(!n||!n.type.startsWith("image/")){showNotification("Please select an image file","warning");return}const a=new FileReader;a.onload=t=>e(t.target.result),a.readAsDataURL(n)}L?.addEventListener("change",n=>{T(n.target.files[0],e=>{D=e,B.src=e,I.textContent="New dark logo ready to save"})}),g?.addEventListener("change",n=>{T(n.target.files[0],e=>{N=e,z.src=e,I.textContent="New light logo ready to save"})}),k?.addEventListener("change",n=>{T(n.target.files[0],e=>{M=e,B.src=e,z.src=e,I.textContent="New logo ready to save (both themes)"})});function b(n){E.setAttribute("data-logo-pos",n),document.querySelectorAll(".logo-pos-btn").forEach(e=>{e.style.background=e.dataset.pos===n?"var(--accent)":"var(--card-bg)",e.style.color=e.dataset.pos===n?"white":"var(--fg)"})}function u(n){S=n||O,document.title=S;const e=document.querySelector(".dashboard-title");e&&(e.textContent=S)}async function y(){try{const n=await fetch("/api/v1/logo");if(n.ok){const e=await n.json();e.customLogoDark&&(f.src=e.customLogoDark,B.src=e.customLogoDark),e.customLogoLight&&(x.src=e.customLogoLight,z.src=e.customLogoLight),!e.customLogoDark&&!e.customLogoLight&&e.customLogo&&(f.src=e.customLogo,x.src=e.customLogo,B.src=e.customLogo,z.src=e.customLogo),e.isDefault||(I.textContent="Using custom logo"),e.position&&(H=e.position,b(e.position)),e.dashboardTitle&&u(e.dashboardTitle)}}catch(n){console.warn("Could not load custom logo:",n.message)}}document.querySelectorAll(".logo-pos-btn").forEach(n=>{n.addEventListener("click",()=>{H=n.dataset.pos,b(H)})}),document.getElementById("brand")?.addEventListener("click",()=>{D=null,N=null,M=null,L&&(L.value=""),g&&(g.value=""),k&&(k.value=""),C&&(C.checked=!1),P.style.display="flex",$.style.display="none",B.src=f.src,z.src=x.src;const n=f.src.includes("custom-logo")||x.src.includes("custom-logo");I.textContent=n?"Using custom logo":"Using default logos",b(H),R.value=S,w.classList.add("show")}),document.getElementById("logo-save")?.addEventListener("click",async()=>{try{const n=R.value.trim()||O,e={position:H,dashboardTitle:n};C?.checked&&M?(e.dataDark=M,e.dataLight=M):(D&&(e.dataDark=D),N&&(e.dataLight=N));const a=await secureFetch("/api/v1/logo",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});if(a.ok){const t=await a.json(),s="?t="+Date.now();t.pathDark&&(f.src=t.pathDark+s,B.src=t.pathDark+s),t.pathLight&&(x.src=t.pathLight+s,z.src=t.pathLight+s),b(H),u(n),w.classList.remove("show")}else{const t=await a.json();showNotification("Failed to save: "+t.error,"error")}}catch(n){showNotification("Error saving: "+n.message,"error")}}),document.getElementById("logo-reset")?.addEventListener("click",async()=>{if(confirm(`Reset all branding to DashCaddy defaults? + `);const w=document.getElementById("logo-modal"),B=document.getElementById("logo-preview-dark"),z=document.getElementById("logo-preview-light"),I=document.getElementById("logo-status"),C=document.getElementById("logo-same-both"),P=document.getElementById("logo-dual-uploads"),$=document.getElementById("logo-single-upload"),L=document.getElementById("logo-upload-dark"),g=document.getElementById("logo-upload-light"),k=document.getElementById("logo-upload-single"),f=document.querySelector("#brand .brand-logo-dark"),x=document.querySelector("#brand .brand-logo-light"),E=document.querySelector(".top-row"),R=document.getElementById("dashboard-title"),O=DC.NAME;let D=null,N=null,M=null,H="left",S=O;C?.addEventListener("change",()=>{C.checked?(P.style.display="none",$.style.display="",D=null,N=null):(P.style.display="flex",$.style.display="none",M=null)});function T(a,e){if(!a||!a.type.startsWith("image/")){showNotification("Please select an image file","warning");return}const n=new FileReader;n.onload=t=>e(t.target.result),n.readAsDataURL(a)}L?.addEventListener("change",a=>{T(a.target.files[0],e=>{D=e,B.src=e,I.textContent="New dark logo ready to save"})}),g?.addEventListener("change",a=>{T(a.target.files[0],e=>{N=e,z.src=e,I.textContent="New light logo ready to save"})}),k?.addEventListener("change",a=>{T(a.target.files[0],e=>{M=e,B.src=e,z.src=e,I.textContent="New logo ready to save (both themes)"})});function b(a){E.setAttribute("data-logo-pos",a),document.querySelectorAll(".logo-pos-btn").forEach(e=>{e.style.background=e.dataset.pos===a?"var(--accent)":"var(--card-bg)",e.style.color=e.dataset.pos===a?"white":"var(--fg)"})}function u(a){S=a||O,document.title=S;const e=document.querySelector(".dashboard-title");e&&(e.textContent=S)}async function y(){try{const a=await fetch("/api/v1/logo");if(a.ok){const e=await a.json();e.customLogoDark&&(f.src=e.customLogoDark,B.src=e.customLogoDark),e.customLogoLight&&(x.src=e.customLogoLight,z.src=e.customLogoLight),!e.customLogoDark&&!e.customLogoLight&&e.customLogo&&(f.src=e.customLogo,x.src=e.customLogo,B.src=e.customLogo,z.src=e.customLogo),e.isDefault||(I.textContent="Using custom logo"),e.position&&(H=e.position,b(e.position)),e.dashboardTitle&&u(e.dashboardTitle)}}catch(a){console.warn("Could not load custom logo:",a.message)}}document.querySelectorAll(".logo-pos-btn").forEach(a=>{a.addEventListener("click",()=>{H=a.dataset.pos,b(H)})}),document.getElementById("brand")?.addEventListener("click",()=>{D=null,N=null,M=null,L&&(L.value=""),g&&(g.value=""),k&&(k.value=""),C&&(C.checked=!1),P.style.display="flex",$.style.display="none",B.src=f.src,z.src=x.src;const a=f.src.includes("custom-logo")||x.src.includes("custom-logo");I.textContent=a?"Using custom logo":"Using default logos",b(H),R.value=S,w.classList.add("show")}),document.getElementById("logo-save")?.addEventListener("click",async()=>{try{const a=R.value.trim()||O,e={position:H,dashboardTitle:a};C?.checked&&M?(e.dataDark=M,e.dataLight=M):(D&&(e.dataDark=D),N&&(e.dataLight=N));const n=await secureFetch("/api/v1/logo",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});if(n.ok){const t=await n.json(),s="?t="+Date.now();t.pathDark&&(f.src=t.pathDark+s,B.src=t.pathDark+s),t.pathLight&&(x.src=t.pathLight+s,z.src=t.pathLight+s),b(H),u(a),w.classList.remove("show")}else{const t=await n.json();showNotification("Failed to save: "+t.error,"error")}}catch(a){showNotification("Error saving: "+a.message,"error")}}),document.getElementById("logo-reset")?.addEventListener("click",async()=>{if(confirm(`Reset all branding to DashCaddy defaults? -This will reset the logo, favicon, title, and position.`))try{if((await secureFetch("/api/v1/logo",{method:"DELETE"})).ok&&(f.src="/assets/dashcaddy-logo-dark.png",x.src="/assets/dashcaddy-logo-light.png",B.src="/assets/dashcaddy-logo-dark.png",z.src="/assets/dashcaddy-logo-light.png",I.textContent="Using default logos",D=null,N=null,M=null,R.value=O,u(O),H="left",b("left")),(await secureFetch("/api/v1/favicon",{method:"DELETE"})).ok){const a=document.querySelector('link[rel="icon"]'),t=document.getElementById("favicon-preview"),s=document.getElementById("favicon-status");a&&(a.href="/assets/dashcaddy-favicon.ico?t="+Date.now()),t&&(t.src="/assets/dashcaddy-favicon.ico?t="+Date.now()),s&&(s.textContent="Using DashCaddy favicon"),r=null}}catch(n){showNotification("Error resetting branding: "+n.message,"error")}}),wireModal(w,document.getElementById("logo-cancel"));const c=document.getElementById("favicon-preview"),o=document.getElementById("favicon-status"),l=document.getElementById("favicon-upload"),v=document.querySelector('link[rel="icon"]')||document.createElement("link");let r=null;document.querySelector('link[rel="icon"]')||(v.rel="icon",v.href="/assets/dashcaddy-favicon.ico",document.head.appendChild(v));async function p(){try{const n=await fetch("/api/v1/favicon");if(n.ok){const e=await n.json();e.customFavicon&&(v.href=e.customFavicon+"?t="+Date.now(),c.src=e.customFavicon+"?t="+Date.now(),o.textContent="Using custom favicon")}}catch(n){console.warn("Could not load custom favicon:",n.message)}}l?.addEventListener("change",n=>{const e=n.target.files[0];if(!e)return;if(!e.type.match(/^image\/(png|svg\+xml)$/)){showNotification("Please select a PNG or SVG file","warning"),l.value="";return}const a=new FileReader;a.onload=t=>{r=t.target.result,c.src=r,o.textContent="New favicon ready to save"},a.readAsDataURL(e)}),document.getElementById("logo-save")?.addEventListener("click",async()=>{if(r)try{const n=await secureFetch("/api/v1/favicon",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({data:r})});if(n.ok){const e=await n.json();v.href=e.path+"?t="+Date.now(),c.src=e.path+"?t="+Date.now(),o.textContent="Using custom favicon",r=null}else{const e=await n.json();showNotification("Failed to save favicon: "+e.error,"error")}}catch(n){showNotification("Error saving favicon: "+n.message,"error")}}),p(),y();const m=document.getElementById("settings-timezone");m&&(new MutationObserver(()=>{w.classList.contains("show")&&m.options.length===0&&(async()=>{let e;try{const a=await fetch("/api/v1/config");a.ok&&(e=(await a.json()).timezone)}catch{}window.populateTimezoneSelect(m,e)})()}).observe(w,{attributes:!0,attributeFilter:["class"]}),document.getElementById("logo-save")?.addEventListener("click",async()=>{const e=m.value;if(e)try{const a=await fetch("/api/v1/config");if(!a.ok)return;const t=await a.json();t.timezone=e,t.updatedAt=new Date().toISOString(),await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})}catch(a){console.warn("Failed to save timezone:",a.message)}}))})(),window.populateTimezoneSelect=function(w,B){const z=Intl.supportedValuesOf("timeZone"),I=B||Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC";w.innerHTML="";for(const C of z){const P=document.createElement("option");P.value=C,P.textContent=C.replace(/_/g," "),C===I&&(P.selected=!0),w.appendChild(P)}},(function(){let w="homelab",B=null;async function z(){try{const T=await fetch("/api/v1/config");if(T.ok&&(B=await T.json(),B&&B.setupComplete))return document.getElementById("setup-wizard").style.display="none",!0}catch(T){console.warn("Could not fetch server config, checking localStorage fallback:",T.message)}return safeGet("dashcaddy-setup")?(document.getElementById("setup-wizard").style.display="none",!0):(document.getElementById("setup-wizard").style.display="flex",!1)}z();const I=document.getElementById("setup-timezone");I&&window.populateTimezoneSelect(I);function C(S){document.querySelectorAll(".setup-step").forEach(b=>{b.style.display="none"});const T=document.getElementById(S);T&&(T.style.display="block")}function P(){const S=document.getElementById("setup-summary-content");if(!S)return;let T='
| Service | Status | ',c+='Uptime 24h | Uptime 7d | ',c+='Avg Response | Last Check |
|---|---|---|---|---|---|
| ${escapeHtml(o.name||o.serviceId)} | `,c+=`${l?"Up":"Down"} | `,c+=`${typeof r=="number"?r.toFixed(1)+"%":r} | `,c+=`${typeof p=="number"?p.toFixed(1)+"%":p} | `,c+=`${m} | `,c+=`${n} | `,c+="
| Service | Status | ',c+='Uptime 24h | Uptime 7d | ',c+='Avg Response | Last Check |
|---|---|---|---|---|---|
| ${escapeHtml(o.name||o.serviceId)} | `,c+=`${l?"Up":"Down"} | `,c+=`${typeof r=="number"?r.toFixed(1)+"%":r} | `,c+=`${typeof p=="number"?p.toFixed(1)+"%":p} | `,c+=`${m} | `,c+=`${a} | `,c+="
| Container | Image | Current | Latest | Actions |
|---|---|---|---|---|
| ${escapeHtml(a.containerName)} | `,e+=`${escapeHtml(a.imageName)} | `,e+=`${escapeHtml(a.currentDigest)} | `,e+=`${escapeHtml(a.latestDigest)} | `,e+='',e+=``,e+=``,e+=" |
| When | Container | Image | Duration | Status |
|---|---|---|---|---|
| ${timeAgo(a.timestamp)} | `,e+=`${escapeHtml(a.containerName)} | `,e+=`${escapeHtml(a.imageName)} | `,e+=`${s} | `,e+=`${t?"\u2713 success":"\u2717 failed"} | `,e+="
| ${escapeHtml(a.error)} | ||||
| Container | Schedule | Window | Rollback | Last Run | Actions | |||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ${escapeHtml(h)} | `,s+=`
+ `);const w=document.getElementById("updates-modal"),B=document.getElementById("updates-btn"),z=document.getElementById("updates-cancel"),I=document.getElementById("updates-check-btn"),C=document.getElementById("updates-available-container"),P=document.getElementById("updates-history-container"),$=document.getElementById("updates-auto-container"),L=document.getElementById("updates-last-check");async function g(){try{const m=await(await fetch("/api/v1/updates/available")).json();if(!m.success)throw new Error(m.error);const a=m.updates||[];if(a.length===0){C.innerHTML=' All containers are up to date. ',L.textContent="";return}let e='
Failed: ${escapeHtml(p.message)} `}}async function k(){I.textContent="\u{1F50D} Checking...",I.disabled=!0;try{const m=await(await secureFetch("/api/v1/updates/check",{method:"POST"})).json();if(!m.success)throw new Error(m.error);I.textContent="\u2705 Done!",await g()}catch(p){I.textContent="\u274C Failed",showNotification("Check error: "+p.message,"error")}setTimeout(()=>{I.textContent="\u{1F50D} Check for Updates",I.disabled=!1},3e3)}async function f(){try{P.innerHTML=' Loading... ';const m=await(await fetch("/api/v1/updates/history?limit=50")).json(),a=m.success&&m.history?m.history:[];if(a.length===0){P.innerHTML='No update history yet. ';return}let e='
Failed: ${escapeHtml(p.message)} `}}async function x(){try{$.innerHTML=' Loading... ';const[p,m]=await Promise.all([fetch("/api/v1/stats/containers"),fetch("/api/v1/updates/auto-update")]),a=await p.json(),e=await m.json(),n=a.success&&a.stats?a.stats:[],t=e.success&&e.config?e.config:{};if(n.length===0){$.innerHTML='No running containers found. ';return}let s='Auto-updates run during maintenance window (default 2AM-4AM). Daily = every day, Weekly = Sundays, Monthly = 1st of month. ';s+='
Failed: ${escapeHtml(p.message)} `}}const E=document.getElementById("dashcaddy-current-version"),R=document.getElementById("dashcaddy-update-badge"),O=document.getElementById("dashcaddy-update-details"),D=document.getElementById("dashcaddy-new-version"),N=document.getElementById("dashcaddy-changelog"),M=document.getElementById("dashcaddy-apply-btn"),H=document.getElementById("dashcaddy-check-btn"),S=document.getElementById("dashcaddy-rollback-btn"),T=document.getElementById("dashcaddy-status-bar"),b=document.getElementById("dashcaddy-history-container");let u=null;function y(p,m){T&&(T.style.display="block",T.style.background=m==="error"?"var(--bad-bg)":m==="success"?"var(--ok-bg)":"var(--bg)",T.style.color=m==="error"?"var(--bad-fg)":m==="success"?"var(--ok-fg)":"var(--fg)",T.textContent=p)}async function c(){try{const m=await(await fetch("/api/v1/system/version")).json();m.success&&(E.textContent="v"+m.version+(m.commit?" ("+m.commit.substring(0,7)+")":""))}catch{E.textContent="Unable to fetch version"}}async function o(p){p||(H.textContent="Checking...",H.disabled=!0);try{const n=await(await fetch("/api/v1/system/update-check")).json();if(u=n,n.success&&n.available&&n.remote){R.style.display="",O.style.display="",D.textContent="v"+n.remote.version,N.textContent=n.remote.changelog||"No changelog available.";const e=document.getElementById("updates-btn");if(e&&!e.querySelector(".update-dot")){const t=document.createElement("span");t.className="update-dot",t.style.cssText="position:absolute;top:2px;right:2px;width:8px;height:8px;border-radius:50%;background:var(--accent);",e.style.position="relative",e.appendChild(t)}const a=document.getElementById("updates-dashcaddy-tab");if(a&&!a.querySelector(".update-dot")){const t=document.createElement("span");t.className="update-dot",t.style.cssText="display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--accent);margin-left:4px;vertical-align:middle;",a.appendChild(t)}}else R.style.display="none",O.style.display="none",await c(),p||y("You are running the latest version.","success");p||(H.textContent="Check for Updates",H.disabled=!1)}catch(m){p||(y("Failed to check: "+m.message,"error"),H.textContent="Check for Updates",H.disabled=!1)}}async function l(){if(confirm("Apply DashCaddy update? The API container will restart.")){M.textContent="Updating...",M.disabled=!0,y("Downloading and applying update...","info");try{const m=await(await secureFetch("/api/v1/system/update-apply",{method:"POST"})).json();if(m.success)y("Update initiated: v"+(m.fromVersion||"?")+" \u2192 v"+(m.toVersion||"?")+". The container will restart shortly.","success"),M.textContent="Applied!",document.querySelectorAll(".update-dot").forEach(n=>n.remove());else throw new Error(m.error||"Update failed")}catch(p){y("Update failed: "+p.message,"error"),M.textContent="Update Now",M.disabled=!1}}}async function v(){try{const m=await(await fetch("/api/v1/system/update-history")).json(),n=m.success&&m.history?m.history:[];if(n.length===0){b.innerHTML='No self-update history. ';return}let e='
Failed: '+escapeHtml(p.message)+" "}}async function r(){try{const m=await(await fetch("/api/v1/system/rollback-versions")).json(),n=m.success&&m.versions?m.versions:[];if(n.length===0){showNotification("No rollback versions available.","info");return}const e=prompt(`Available rollback versions:
-`+n.join(`
+ | `,s+=``,s+=` | `,s+=` | ${U} | `,s+=``,s+=" | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| When | Version | From | Status |
|---|---|---|---|
| '+timeAgo(n.timestamp)+" | ",e+='v'+escapeHtml(n.version)+(n.rollback?" (rollback)":"")+" | ",e+='v'+escapeHtml(n.fromVersion||"?")+" | ",e+=''+t+" | ",e+="
| '+escapeHtml(n.error)+" | |||
| '+escapeHtml(n.note)+" | |||