-
Notifications
You must be signed in to change notification settings - Fork 16
Expand file tree
/
Copy pathadmin.html
More file actions
1 lines (1 loc) · 22.4 KB
/
Copy pathadmin.html
File metadata and controls
1 lines (1 loc) · 22.4 KB
1
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Monitoring admin area</title><style>.header,h1,th,footer,button{user-select:none}*,.url-prefix{box-sizing:border-box}.tab,button:hover{background:#34495e}#login,table{background:#fff}.tab,button,h1,th{color:#ecf0f1}#error,h1,label.page-public,label.url{margin-bottom:1rem}#error,#save,.hidden{display:none}.close,.tab,button,label{cursor:pointer}.threshold-header,label{user-select:none;font-weight:700}*{margin:0;padding:0;font-family:sans-serif}body{background:#ebebeb;color:#333;padding:15px}#login{max-width:20rem;margin:5rem auto;padding:2rem}a{color:#333!important;text-decoration:none;font-weight:700}a:hover{text-decoration:underline}#container{max-width:100%}#login,button,h1,table{box-shadow:0 1px 3px rgba(0,0,0,.1)}h1{padding:12px;font-size:1.5rem}.tabs{margin-bottom:.75rem}.tab{padding:10px 20px;border:none;border-radius:0;margin-right:5px}.tab.active{background:#1c6bb8}button,h1{background-color:#2c3e50}#login,.modal .content,button,h1,table{border-radius:5px}#save{align-items:center;gap:10px;float:right;font-size:1rem;font-weight:400}button{border:none;padding:8px 12px;margin:2px}.modal button,table button{padding:6px 8px}table{width:100%;border-collapse:collapse}td,th{padding:1px 6px;text-align:left;border-bottom:1px solid #ddd}th{background:#2c3e50;padding-top:.3rem;padding-bottom:.3rem}tr:nth-child(2n){background:#f7f7f7}h1 button{background-color:#243342}#login-form{display:flex;flex-direction:column;gap:1rem}input[type=checkbox]{width:auto}.grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:1rem}.grid label{display:flex;flex-direction:column}.modal{position:fixed;z-index:1000;left:0;top:0;width:100%;height:100%;background:rgba(0,0,0,.5)}.modal-content{background:#fff;margin:2.5% auto;padding:2rem;width:80%;max-width:40rem;max-height:90vh;overflow-y:auto}.header{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}.close{font-size:28px;color:#aaa}.close:hover{color:#000}#error{padding:1rem;background:#f44336;color:#fff;border-radius:3px}.list{max-height:50vh;overflow-y:auto;border:1px solid #ddd;padding:10px;border-radius:3px}input,select,textarea{padding:8px;border-radius:3px;transition:.2s;outline:0;font-size:.9rem;border:1px solid #ddd;background-color:#fff}.url-input-group:hover .url-prefix,input:hover,label.url:hover .url-input-group:not(:focus-within) .url-prefix,select:hover,textarea:hover{border-color:#bbb}input:focus,select:focus,textarea:focus{border-color:#1c6bb8;box-shadow:0 0 0 2px rgba(28,107,184,.2);background:#fafafa}textarea{resize:vertical}.url-input-group{display:flex;transition:.2s}.url-prefix{padding:8px 0 8px 6px;border:1px solid #ddd;border-right:none;border-radius:3px 0 0 3px;color:#666;font-size:.9rem;white-space:nowrap;transition:.2s}.url-input-group input{border-radius:0 3px 3px 0;border-left:none;box-shadow:none}.url-input-group:focus-within .url-prefix{border-color:#1c6bb8}.url-input-group:focus-within{box-shadow:0 0 0 2px rgba(28,107,184,.2);border-radius:3px}footer,footer a{text-align:center;margin-top:1rem;color:#999!important;font-size:.7rem;font-weight:400}footer a:hover{color:#666!important}@media (prefers-color-scheme:dark){body{background:#121212;color:#e0e0e0}a{color:#e0e0e0!important}#login,.modal-content,table{background:#282828;color:#e0e0e0}h1 button{background-color:#1a2732}td,th{border-color:#444}tr:nth-child(2n){background:#333}button,h1{background-color:#233443}.list,input,select,textarea{background:#333;color:#e0e0e0;border-color:#555}.close:hover{color:#fff}#monitors tr button:nth-of-type(2){background-color:#2c4154}#monitors tr button:nth-of-type(2):hover{background-color:#354e64}.url-prefix,input,select,textarea{background-color:#333;color:#e0e0e0;border-color:#555}.url-input-group:hover .url-prefix,input:hover,label.url:hover .url-input-group:not(:focus-within) .url-prefix,select:hover,textarea:hover{border-color:#666}input:focus,select:focus,textarea:focus{background-color:#3a3a3a;border-color:#1c6bb8}.url-prefix{color:#bbb}.url-input-group:focus-within .url-prefix{border-color:#1c6bb8;background-color:#3a3a3a}footer,footer a{color:#555!important}footer a:hover{color:#777!important}}#monitors tr button:nth-of-type(3),#pages tr button:nth-of-type(2),h1 button:nth-of-type(2){background-color:#f44336}#monitors tr button:nth-of-type(3):hover,#pages tr button:nth-of-type(2):hover,h1 button:nth-of-type(2):hover{background-color:#f66055}table tr button:first-of-type{background-color:#1c6bb8}table tr button:first-of-type:hover{background-color:#2180de}#monitors tr button:nth-of-type(4){background-color:#062}#monitors tr button:nth-of-type(4):hover{background-color:#007025}label input[type=number],label textarea{display:block;width:100%}.line{display:block;margin-top:.5rem}code{font-family:monospace}#notification-settings-modal textarea,input.seperate,select{width:100%;margin-top:.2rem;margin-bottom:.3rem}.add{margin:0 0 .5rem}.threshold-header{font-size:1.1rem;margin-top:.85rem}#page-path{padding-left:0;margin:0}body:has(.modal:not(.hidden)){overflow:hidden}</style></head><body><div id="container" class="hidden"><div id="login"><h1>Admin login</h1><div id="error"></div><div id="login-form"><label for="password">Password:</label><input type="password" id="password" onkeypress="'Enter'===event.key&&login();"><button onclick="login();">Login</button></div></div><div id="admin" class="hidden"><h1>Monitoring admin area<span id="save">Unsaved changes<button onclick="save();">Save</button><button onclick="discard();">Discard</button><button onclick="forceSave();" id="force">Force save</button></span></h1><div class="tabs"><button class="tab active" onclick="showTab('monitors');">Monitors</button><button class="tab" onclick="showTab('pages');">Status pages</button><button class="tab" onclick="showTab('settings');">Settings</button></div><div id="monitors" class="content"><button onclick="editMonitor();" class="add">Add monitor</button><table><thead><tr><th>Name</th><th>Public</th><th>Actions</th></tr></thead><tbody></tbody></table></div><div id="pages" class="content hidden"><button onclick="editPage();" class="add">Add status page</button><table><thead><tr><th>Name</th><th>URL</th><th>Public</th><th>Monitors</th><th>Actions</th></tr></thead><tbody></tbody></table></div><div id="settings" class="content hidden"><label class="line">Change password: <input type="password" placeholder="New password" oninput="changePassword(this.value);"></label><label class="line">Copy command (<code>DOMAIN</code> will be replaced with the domain, <code>TOKEN</code> will be replaced with the token, <code>ADDITIONAL_PATHS</code> will be replaced with the additional paths to be monitored and <code>NAME</code> will be replaced with the name): <select onchange="$('copy').value=this.value;data.copy=this.value;possiblyChanged();" style="width:auto;padding:.05rem .5rem"><option disabled selected>-- Select --</option><option value="curl -s https://ltstats.de/v1.3/systemd:agent | tee install.sh | sha256sum -c <(echo 123bdcc123d39dfe915eb3ed9223ea75c845a8bef5c79b994f4b2de20530085c -) && bash install.sh DOMAIN TOKEN ntp ADDITIONAL_PATHS # NAME">systemd + ntp client</option><option value="curl -s https://ltstats.de/v1.3/systemd:agent | tee install.sh | sha256sum -c <(echo 123bdcc123d39dfe915eb3ed9223ea75c845a8bef5c79b994f4b2de20530085c -) && bash install.sh DOMAIN TOKEN no-ntp ADDITIONAL_PATHS # NAME">systemd</option><option value="">Custom</option></select><input class="seperate" id="copy" oninput="data.copy=this.value,possiblyChanged();"></label><div class="line">Hide settings (for private monitors): <button onclick="editHide();">Edit</button></div><div class="line">Notification settings: <button onclick="editNotifications();">Edit</button></div></div></div></div><div id="monitor-modal" class="modal hidden"><div class="modal-content"><div class="header"><h2></h2><span class="close" onclick="closeModals();">×</span></div><label class="line">Name:<br><input id="monitor-name" class="seperate"></label><label class="line"><input type="checkbox" id="monitor-public"> Public</label><label class="line">Note:<textarea id="monitor-note" rows="3"></textarea></label><label class="line"><input type="checkbox" id="monitor-note-public"> Note is public</label><div class="line threshold-header">Monitoring thresholds:<button type="button" onclick="applyDefaults();">Apply defaults</button><button type="button" onclick="applyToAll();">Apply to all</button></div><div class="line"><div class="grid"><label>Offline (minutes):<input type="number" min="0"></label><label>CPU Usage (%):<input type="number" min="1" max="100"></label><label>CPU IOWait (%):<input type="number" min="1" max="100"></label><label>CPU Steal (%):<input type="number" min="1" max="100"></label><label>RAM Usage (%):<input type="number" min="1" max="100"></label><label>Swap Usage (%):<input type="number" min="1" max="100"></label><label>Disk Usage (%):<input type="number" min="1" max="100"></label><label>Net RX (bps):<input type="number" min="1"></label><label>Net TX (bps):<input type="number" min="1"></label><label>Disk Read (Bps):<input type="number" min="1"></label><label>Disk Write (Bps):<input type="number" min="1"></label></div></div><label class="line">Additional filesystems/partitions to be monitored (space seperated, specify any path within the mounted fileystem):<br><input id="monitor-additional-paths" class="seperate"></label><div class="line"><button onclick="saveMonitor();">Save</button><button onclick="closeModals();">Cancel</button></div></div></div><div id="page-modal" class="modal hidden"><div class="modal-content"><div class="header"><h2></h2><span class="close" onclick="closeModals();">×</span></div><label class="line">Name:<input id="page-name" class="seperate"></label><label class="line url">URL:<div class="url-input-group"><span class="url-prefix" id="domain"></span><input id="page-path" class="seperate"></div></label><label class="line page-public"><input type="checkbox" id="page-public"> Public</label><label class="line">Monitors:</label><div id="page-modal-monitors" class="list"></div><div class="line"><button onclick="savePage();">Save</button><button onclick="closeModals();">Cancel</button></div></div></div><div id="add-to-page-modal" class="modal hidden"><div class="modal-content"><div class="header"><h2>Add monitor to status page</h2><span class="close" onclick="closeModals();">×</span></div><label class="line">Select status page:<select id="page-select"></select></label><div class="line"><button onclick="addMonitorToPage();">Add</button><button onclick="closeModals();">Cancel</button></div></div></div><div id="hide-private-modal" class="modal hidden"><div class="modal-content"><div class="header"><h2>Hide settings (for private monitors):</h2><span class="close" onclick="closeModals();">×</span></div><div class="line" id="hide-select"></div><div class="line"><button onclick="saveHide();">Save</button><button onclick="closeModals();">Cancel</button></div></div></div><div id="notification-settings-modal" class="modal hidden"><div class="modal-content"><div class="header"><h2>Notification settings</h2><span class="close" onclick="closeModals();">×</span></div><label class="line">Check every (seconds):<input type="number" id="notification-every" min="1"></label><label class="line">Sample period (minutes):<input type="number" id="notification-sample" min="1"></label><label class="line">Command (space seperated, <code>NAME</code>, <code>TYPE</code>, <code>PUBLIC_TOKEN</code> and <code>STATUS</code> will be replaced, <code>TYPE</code> is one of <code>{DOWN, CPU_USAGE, CPU_IOWAIT, CPU_STEAL, RAM_USAGE, SWAP_USAGE, DISK_USAGE, NET_RX, NET_TX, DISK_READ, DISK_WRITE}</code> and <code>STILL_MET</code> is <code>TRUE</code> or <code>FALSE</code>):<textarea id="notification-exec" rows="3"></textarea></label><div class="line threshold-header">Default monitoring thresholds:</div><div class="line"><div class="grid"><label>Offline (minutes):<input type="number" min="0"></label><label>CPU Usage (%):<input type="number" min="1" max="100"></label><label>CPU IOWait (%):<input type="number" min="1" max="100"></label><label>CPU Steal (%):<input type="number" min="1" max="100"></label><label>RAM Usage (%):<input type="number" min="1" max="100"></label><label>Swap Usage (%):<input type="number" min="1" max="100"></label><label>Disk Usage (%):<input type="number" min="1" max="100"></label><label>Net RX (bps):<input type="number" min="1"></label><label>Net TX (bps):<input type="number" min="1"></label><label>Disk Read (Bps):<input type="number" min="1"></label><label>Disk Write (Bps):<input type="number" min="1"></label></div></div><div class="line"><button onclick="saveNotifications();">Save</button><button onclick="closeModals();">Cancel</button></div></div></div><script>var data,original,editing,$=e=>document.getElementById(e),hex=e=>e.map(e=>e.toString(16).padStart(2,"0")).join("");async function hash(e){return hex(Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256",(new TextEncoder).encode(e)))))}function newToken(){for(var t;t=hex(Array.from(crypto.getRandomValues(new Uint8Array(16)))),void 0!==data.monitors[t]||0!==Object.entries(data.monitors).filter(e=>e[1][0]===t).length;);return t}function compare(e,t){if(e!==t){if(null===e||null===t||"object"!=typeof e||"object"!=typeof t)return!1;var a,o=Object.keys(e),n=Object.keys(t);if(o.length!==n.length)return!1;for(a of o)if(!Object.hasOwn(t,a)||!compare(e[a],t[a]))return!1}return!0}function possiblyChanged(){compare(original,data)?$("save").style="":($("save").style.display="flex",$("force").style.display="none")}function showTab(e){document.querySelectorAll(".tab").forEach(e=>e.classList.remove("active")),document.querySelectorAll(".content").forEach(e=>e.classList.add("hidden")),event.target.classList.add("active"),$(e).classList.remove("hidden")}var closeModals=()=>document.querySelectorAll(".modal").forEach(e=>e.classList.add("hidden"));function checkAuth(){fetch("/admin/logged_in").then(e=>{if(!e.ok)throw new Error;$("login").style.display="none",$("admin").style.display="block",loadData()}).catch(()=>$("container").classList.remove("hidden"))}function login(){hash($("password").value).then(e=>{fetch("/admin/login",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({hash:e})}).then(e=>{e.ok?($("login").style.display="none",$("admin").style.display="block",loadData()):((e=$("error")).textContent="Invalid password",e.style.display="block")}).catch(()=>{var e=$("error");e.textContent="Login failed",e.style.display="block"})})}function loadData(){fetch("/admin/data").then(e=>{if(200!=e.status)throw new Error;return e.json()}).then(e=>{data=e,original=structuredClone(data),refresh(),$("container").classList.remove("hidden")}).catch(()=>alert("Failed to load data"))}function save(){fetch("/admin/data",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(data)}).then(e=>{409===e.status?(alert("There is a conflict. You can force saving."),$("force").style.display="inline"):e.ok?(original=structuredClone(data),$("save").style.display="none",loadData()):alert("Saving failed")}).catch(()=>alert("Saving failed"))}function forceSave(){data.time=0,save(),$("force").style.display="none"}function discard(){data=structuredClone(original),$("save").style.display="none",refresh()}function refresh(){refreshMonitors(),refreshPages(),$("copy").value=data.copy}function refreshMonitors(){var e,t,a=$("monitors").querySelector("tbody");a.innerHTML="";for([e,t]of Object.entries(data.monitors).reverse())a.insertRow().innerHTML=`<td><a href="/monitor/${e}">${t[1]}</a></td><td>${t[2]?"Yes":"No"}</td><td><button onclick="editMonitor('${e}');">Edit</button><button onclick="copy('${e}');">Copy command</button><button onclick="deleteMonitor('${e}');">Delete</button><button onclick="showAddToPageModal('${e}');">Add to status page</button></td>`}function refreshPages(){var e,t,a=$("pages").querySelector("tbody");a.innerHTML="";for([e,t]of Object.entries(data.pages))a.insertRow().innerHTML=`<td><a href="/${e}">${t[0]}</a></td><td><a style="font-weight:normal" href="/${"main"===e?"":e}">https://${window.location.host}${"main"===e?"":"/"+e}</a>${"main"===e?'<span style="user-select:none"> (when not logged in)</span>':""}</td><td>${t[1]?"Yes":"No"}</td><td>${t[2].length}</td><td><button onclick="editPage('${e}');">Edit</button>${"main"===e?"":`<button onclick="deletePage('${e}');">Delete</button>`}</td>`}function editMonitor(e=null){var a;editing=e,$("monitor-modal").querySelector("h2").textContent=e?"Edit monitor":"Add monitor",document.querySelectorAll("input[type=number],#monitor-note,#monitor-name").forEach(e=>e.value=""),$("monitor-public").checked=!0,$("monitor-note-public").checked=!1,$("monitor-additional-paths").value="",e?(a=data.monitors[e],$("monitor-name").value=a[1],$("monitor-public").checked=a[2],a[3]?($("monitor-note").value=a[3][0],$("monitor-note-public").checked=a[3][1]):($("monitor-note").value="",$("monitor-note-public").checked=!1),a[4]&&$("monitor-modal").querySelectorAll("input[type=number]").forEach((e,t)=>e.value=a[4][t]),$("monitor-additional-paths").value=a[5].join(" ")):applyDefaults(),$("monitor-modal").classList.remove("hidden")}function parseGrid(e){var a=[];return $(e).querySelectorAll(".grid input[type=number]").forEach((e,t)=>a[t]=0===parseInt(e.value)?0:parseInt(e.value)||!1),!!a.some(e=>!1!==e)&&a}function saveMonitor(){var e,t,a=$("monitor-name").value;a?(e=editing||newToken(),t=data.monitors[e]||[],editing||(t[0]=newToken()),t[1]=a,t[2]=$("monitor-public").checked,a=$("monitor-note").value,t[3]=!!a&&[a,$("monitor-note-public").checked],t[4]=parseGrid("monitor-modal"),t[5]=$("monitor-additional-paths").value.split(" "),data.monitors[e]=t,possiblyChanged(),closeModals(),refreshMonitors()):alert("Name is required")}function deleteMonitor(t){if(confirm("Delete monitor?")){delete data.monitors[t];for(var e of Object.values(data.pages))e[2]=e[2].filter(e=>e!==t);possiblyChanged(),refresh()}}function copy(e){var t=data.monitors[e][5].join(" ");""!==t&&(t=`"${t}"`),navigator.clipboard.writeText(data.copy.replaceAll("TOKEN",data.monitors[e][0]).replaceAll("ADDITIONAL_PATHS",t).replaceAll("DOMAIN",window.location.host).replaceAll("NAME",data.monitors[e][1]).trim())}function applyDefaults(){data.notifications.default&&$("monitor-modal").querySelectorAll("input[type=number]").forEach((e,t)=>e.value=data.notifications.default[t])}function applyToAll(){if(confirm("Apply to all monitors?")){var e,t=parseGrid("monitor-modal");for(e of Object.values(data.monitors))e[4]=t;possiblyChanged()}}function showAddToPageModal(e){editing=e;var t,a,o,n=$("page-select");n.innerHTML="";for([t,a]of Object.entries(data.pages))a[2].includes(e)||((o=document.createElement("option")).value=t,o.textContent=a[0]+` (${t})`,n.appendChild(o));0===n.children.length?alert("Monitor is already on all available status pages."):$("add-to-page-modal").classList.remove("hidden")}function addMonitorToPage(){var e=$("page-select").value;e&&(data.pages[e][2].push(editing),possiblyChanged(),closeModals(),refreshPages())}function editPage(e=null){var t;editing=e,$("page-modal").querySelector("h2").textContent=e?"Edit status page":"Add status page",$("domain").innerHTML=`https://${window.location.host}/`,e?(t=data.pages[e],$("page-path").value="main"===e?"":e,$("page-path").disabled="main"===e,$("page-name").value=t[0],$("page-public").checked=t[1],populatePageMonitors(t[2])):($("page-path").value="",$("page-name").value="",$("page-public").checked=!1,populatePageMonitors([])),$("page-modal").classList.remove("hidden")}function populatePageMonitors(e=[]){var t,a,o=$("page-modal-monitors");o.innerHTML="";for([t,a]of Object.entries(data.monitors||{})){var n=document.createElement("div");n.innerHTML=`<label><input type="checkbox" value="${t}" ${e.includes(t)?"checked":""}> ${a[1]}</label>`,o.appendChild(n)}}function savePage(){var e,t=$("page-path").value,a=("main"===editing&&(t="main"),$("page-name").value);t&&a?data.pages[t]&&editing!=t&&!confirm("This path already exists. Do you want to overwrite that status page?")||(editing&&editing!==t&&delete data.pages[editing],e=Array.from($("page-modal-monitors").querySelectorAll("input:checked")).map(e=>e.value),data.pages[t]=[a,$("page-public").checked,e],possiblyChanged(),closeModals(),refreshPages()):alert("Path and name required!")}function deletePage(e){confirm("Delete page?")&&(delete data.pages[e],possiblyChanged(),refreshPages())}function editHide(){var a=$("hide-select");a.innerHTML="",["TOTAL_IO","TOTAL_TRAFFIC","KERNEL","CPU_MODEL","CPU_CORES","UPTIME","CPU_USAGE","CPU_IOWAIT","CPU_STEAL","RAM_SIZE","RAM_USAGE","SWAP_SIZE","SWAP_USAGE","DISK_SIZE","DISK_USAGE","NET","IO"].forEach(e=>{var t=document.createElement("div");t.innerHTML=`<label><input type="checkbox" value="${e}"${data.hide.includes(e)?" checked":""}> ${e}</label>`,a.appendChild(t)}),$("hide-private-modal").classList.remove("hidden")}function saveHide(){data.hide=Array.from($("hide-select").querySelectorAll("input:checked")).map(e=>e.value),possiblyChanged(),closeModals(),refreshSettings()}function editNotifications(){var a=data.notifications;$("notification-every").value=a.every,$("notification-sample").value=a.sample,$("notification-exec").value=a.exec.join(" "),a.default&&$("notification-settings-modal").querySelectorAll(".grid input[type=number]").forEach((e,t)=>e.value=a.default[t]),$("notification-settings-modal").classList.remove("hidden")}function saveNotifications(){var e=parseInt($("notification-every").value),t=parseInt($("notification-sample").value);e&&t?(data.notifications.every=e,data.notifications.sample=t,data.notifications.exec=$("notification-exec").value.split(" "),data.notifications.default=parseGrid("notification-settings-modal"),possiblyChanged(),closeModals()):alert("Every and sample required to be an integer bigger than 0!")}function changePassword(e){""===e?(data.hash=original.hash,possiblyChanged()):hash(e).then(e=>{data.hash=e,possiblyChanged()})}document.addEventListener("keydown",e=>{"Escape"===e.key&&closeModals()}),window.addEventListener("beforeunload",e=>{compare(original,data)||(e.preventDefault(),e.returnValue="")}),window.addEventListener("click",()=>{"modal"===event.target.className&&closeModals()}),window.addEventListener("DOMContentLoaded",()=>{document.querySelectorAll("input[type=number]").forEach(e=>{void 0===e.min&&void 0===e.max||e.addEventListener("change",()=>{""!==e.value&&(parseInt(e.value)>parseInt(e.max)&&(e.value=e.max),parseInt(e.value)<parseInt(e.min))&&(e.value=e.min)})}),checkAuth()});</script><footer>Powered by <a href="https://ltstats.de">LTstats</a></footer></body></html>