diff --git a/web/index.css b/web/index.css new file mode 100644 index 0000000..f6cd826 --- /dev/null +++ b/web/index.css @@ -0,0 +1,79 @@ +body { + display: flex; + flex-flow: column; + height: 100vh; + margin: 0; + font-family: Helvetica, sans-serif; + background-color: #222; + color: white; +} + +.hidden { + display: none; +} + +nav { + overflow-x: auto; + font-weight: bold; + font-size: 12pt; + display: flex; +} +nav img { + max-height: 2em; +} +nav a { + padding: 0.5em 1.5em; + color: #aaa; + text-decoration: none; + white-space: nowrap; +} +nav a[data-no-menu] { + display: none; +} +nav a:hover { + background: #555; +} +nav a.active { + background: #8b8; + color: black; +} + +#app { + flex-grow: 1; +} +iframe { + display: block; + border: none; + width: 100%; + height: 100%; +} + +.icons { + margin: auto 2em; +} +.icons { + display: flex; + flex-wrap: wrap; + justify-content: center; + max-width: 40em; + margin: auto; + gap: 20px; +} +.icons a { + background-color: #444; + color: #fff; + width: 120px; + height: 120px; + border-radius: 10px; + display: flex; + justify-content: center; + align-items: center; +} +.icons a img, +.icons a canvas { + max-width: 75%; + max-height: 75%; + width: 100%; + height: 100%; + image-rendering: pixelated; +} diff --git a/web/index.html b/web/index.html index 0056901..cce5201 100644 --- a/web/index.html +++ b/web/index.html @@ -1,18 +1,18 @@ - - Webstat - - - - - - -
-
- + + Deer Grove + + + + + + + + +
+ +
+ diff --git a/web/index.mjs b/web/index.mjs new file mode 100644 index 0000000..3ecafba --- /dev/null +++ b/web/index.mjs @@ -0,0 +1,133 @@ +import * as WebStat from "./webstat.mjs" + +const Millisecond = 1 +const Second = 1000 * Millisecond +const Minute = 60 * Second + +class StatApp { + constructor(parent) { + this.stat = new WebStat.Stat() + this.chart = new WebStat.PieChart(parent, this.stat) + + setInterval(()=>this.update(), 2 * Second) + this.update() + } + + async update() { + this.stat.update() + this.chart.update() + } +} + +let frames = {} +function activate(event, element) { + event.preventDefault() + + let parent = element.parentElement + for (let e of parent.getElementsByClassName("active")) { + e.classList.remove("active") + } + element.classList.add("active") + let href = element.href + let app = document.querySelector("#app") + + let frame = frames[href] + if (frame) { + frame.dispatchEvent(new Event("load")) + } else { + frame = app.appendChild(document.createElement("iframe")) + frame.addEventListener("load", e => frameLoaded(frame)) + frame.src = href + frames[href] = frame + } + + for (let fhref in frames) { + let f = frames[fhref] + if (fhref == href) { + f.classList.remove("hidden") + } else { + f.classList.add("hidden") + } + } +} + + +function frameLoaded(frame) { + let doc = frame.contentDocument + if (doc.title.length > 0) { + document.title = doc.title + } + let icon = document.querySelector("link[rel~='icon']") + let dicon = doc.querySelector("link[rel~='icon']") + if (dicon) { + icon.href = dicon.href + } else { + icon.href = defaultIcon + } +} + +let defaultIcon = null +async function init() { + let doc = document.querySelector("iframe").contentDocument + + defaultIcon = document.querySelector("link[rel~='icon']").href + + for (let l of document.head.querySelectorAll("style")) { + doc.head.appendChild(l.cloneNode(true)) + } + for (let l of document.head.querySelectorAll("link[rel='stylesheet']")) { + doc.head.appendChild(l.cloneNode()) + } + for (let f of document.querySelectorAll("#app iframe")) { + frames[f.src] = f + } + + + let icons = doc.body.appendChild(doc.createElement("section")) + icons.classList.add("icons") + + let nav = document.querySelector("nav") + let resp = await fetch("portal.json") + let obj = await resp.json() + for (let app of obj) { + let hlink = null + if (app.target != "_blank") { + hlink = nav.appendChild(document.createElement("a")) + hlink.href = app.href + hlink.textContent = app.title + if (app.target) { + hlink.target = app.target + } else { + hlink.addEventListener("click", event => activate(event, hlink)) + } + } + + let dlink = icons.appendChild(doc.createElement("a")) + dlink.href = app.href + if (app.target) { + dlink.target = app.target + } else { + dlink.addEventListener("click", event => activate(event, hlink)) + } + if (app.icon) { + let icon = dlink.appendChild(doc.createElement("img")) + icon.src = app.icon + icon.alt = app.title + icon.title = app.title + icon.style.objectFit = "cover" + } else if (app.app) { + if (app.app == "stat") { + new StatApp(dlink) + } + } else { + let text = dlink.appendChild(doc.createElement("div")) + text.textContent = app.title + } + } +} + +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init) +} else { + init() +} diff --git a/web/portal.json b/web/portal.json new file mode 100644 index 0000000..aa9dc6f --- /dev/null +++ b/web/portal.json @@ -0,0 +1,70 @@ +[ + { + "title": "Movies", + "href": "https://deergrove.woozle.org/radarr/", + "icon": "/radarr/Content/Images/logo.svg" + }, + { + "title": "Episodes", + "href": "https://deergrove.woozle.org/sonarr/", + "icon": "/sonarr/Content/Images/logo.svg" + }, + { + "title": "Music", + "href": "https://deergrove.woozle.org/lidarr/", + "icon": "/lidarr/Content/Images/logo.svg" + }, + { + "title": "Books", + "href": "https://deergrove.woozle.org/readarr/", + "icon": "/readarr/Content/Images/logo.svg" + }, + { + "title": "Media Sucker", + "href": "https://deergrove.woozle.org/sucker/", + "icon": "/sucker/cd-dvd.svg" + }, + { + "title": "Searcher", + "href": "https://deergrove.woozle.org/prowlarr/", + "icon": "/prowlarr/Content/Images/logo.png" + }, + { + "title": "Usenet", + "href": "https://deergrove.woozle.org/nzbget/", + "icon": "/nzbget/img/favicon-256x256.png" + }, + { + "title": "BitTorrent", + "href": "https://deergrove.woozle.org/transmission/web/", + "icon": "/transmission/web/images/webclip-icon.png" + }, + { + "title": "3D Printer", + "href": "https://deergrove.woozle.org/octoprint/", + "icon": "/octoprint/static/img/logo.png" + }, + { + "title": "Git", + "href": "https://git.woozle.org/", + "icon": "https://git.woozle.org/assets/img/logo.svg", + "target": "_blank" + }, + { + "title": "Storage", + "href": "https://drive.woozle.org/", + "icon": "https://drive.woozle.org/storage/public/icons/cloud-folder.png", + "target": "_blank" + }, + { + "title": "Genealogy", + "href": "https://ancestry.woozle.org/", + "icon": "https://ancestry.woozle.org/images/arbre_start.png", + "target": "_blank" + }, + { + "title": "Host Stats", + "href": "/stat.html", + "app": "stat" + } +] diff --git a/web/stat.html b/web/stat.html new file mode 100644 index 0000000..3cda6bf --- /dev/null +++ b/web/stat.html @@ -0,0 +1,19 @@ + + + + Webstat + + + + + + +
+
+ + diff --git a/web/main.mjs b/web/stat.mjs similarity index 83% rename from web/main.mjs rename to web/stat.mjs index 4eae58c..e8fb62c 100644 --- a/web/main.mjs +++ b/web/stat.mjs @@ -7,16 +7,16 @@ const Minute = 60 * Second class App { constructor() { this.stat = new WebStat.Stat() - this.areaChart = new WebStat.AreaChart(document.querySelector("#chart")) - this.pieChart = new WebStat.PieChart(document.querySelector("#pie")) + this.areaChart = new WebStat.AreaChart(document.querySelector("#chart"), this.stat) + this.pieChart = new WebStat.PieChart(document.querySelector("#pie"), this.stat) setInterval(()=>this.update(), 2 * Second) } async update() { this.stat.update() - this.areaChart.update(this.stat) - this.pieChart.update(this.stat) + this.areaChart.update() + this.pieChart.update() } } diff --git a/web/webstat.mjs b/web/webstat.mjs index dc6d8b7..9b99d2e 100644 --- a/web/webstat.mjs +++ b/web/webstat.mjs @@ -1,4 +1,3 @@ -//import Chart from "https://esm.run/chart.js@4.2.1/auto" import Chart from "https://cdn.jsdelivr.net/npm/chart.js@4.2.1/auto/+esm" function qpush(arr, val, len) { @@ -53,7 +52,8 @@ class Stat { } class AreaChart { - constructor(element, width=60) { + constructor(element, stat, width=60) { + this.stat = stat this.width = width this.canvas = element.appendChild(document.createElement("canvas")) this.data ={ @@ -93,10 +93,9 @@ class AreaChart { ) } - async update(stat) { - let now = stat.Date - let cpu = stat.cpu() - qpush(this.data.labels, now, this.width) + async update() { + let cpu = this.stat.cpu() + qpush(this.data.labels, this.stat.Date, this.width) qpush(this.datasets.user, cpu.user, this.width) qpush(this.datasets.nice, cpu.nice, this.width) qpush(this.datasets.sys, cpu.sys, this.width) @@ -107,7 +106,8 @@ class AreaChart { } class PieChart { - constructor(element) { + constructor(element, stat) { + this.stat = stat this.canvas = element.appendChild(document.createElement("canvas")) this.data ={ labels: ["user", "nice", "sys", "idle", "wait"], @@ -122,11 +122,11 @@ class PieChart { animation: false, borderWidth: 0, backgroundColor: [ + "blue", "red", - "green", "orange", - "rgba(0, 64, 0, 0.2)", - "magenta", + "rgba(255, 255, 0, 0.2)", + "cyan", ], plugins: { legend: { display: false }, @@ -136,8 +136,8 @@ class PieChart { ) } - async update(stat) { - let cpu = stat.cpu() + async update() { + let cpu = this.stat.cpu() this.data.datasets = [{ label: "Current", data: [cpu.user, cpu.nice, cpu.sys, cpu.idle, cpu.wait],