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],