This commit is contained in:
Neale Pickett 2023-03-11 08:54:24 -07:00
parent 1fe69cd88f
commit f9c74c2aac
7 changed files with 332 additions and 31 deletions

79
web/index.css Normal file
View File

@ -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;
}

View File

@ -1,18 +1,18 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>Webstat</title> <title>Deer Grove</title>
<meta charset="utf-8"> <meta charset="utf-8">
<script src="main.mjs" type="module"></script> <meta name="viewport" content="width=device-width, initial-scale=1">
<style> <link rel="icon" href="deergrove.png">
#pie { <link rel="stylesheet" href="index.css">
max-width: 50px; <script src="index.mjs" type="module"></script>
} </head>
</style> <body>
</head> <nav>
</nav>
<body> <section id="app">
<div id="pie"></div> <iframe></iframe>
<div id="chart"></div> </section>
</body> </body>
</html> </html>

133
web/index.mjs Normal file
View File

@ -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()
}

70
web/portal.json Normal file
View File

@ -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"
}
]

19
web/stat.html Normal file
View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<title>Webstat</title>
<meta charset="utf-8">
<script src="stat.mjs" type="module"></script>
<style>
#pie {
max-width: 50px;
position: absolute;
}
</style>
</head>
<body>
<div id="pie"></div>
<div id="chart"></div>
</body>
</html>

View File

@ -7,16 +7,16 @@ const Minute = 60 * Second
class App { class App {
constructor() { constructor() {
this.stat = new WebStat.Stat() this.stat = new WebStat.Stat()
this.areaChart = new WebStat.AreaChart(document.querySelector("#chart")) this.areaChart = new WebStat.AreaChart(document.querySelector("#chart"), this.stat)
this.pieChart = new WebStat.PieChart(document.querySelector("#pie")) this.pieChart = new WebStat.PieChart(document.querySelector("#pie"), this.stat)
setInterval(()=>this.update(), 2 * Second) setInterval(()=>this.update(), 2 * Second)
} }
async update() { async update() {
this.stat.update() this.stat.update()
this.areaChart.update(this.stat) this.areaChart.update()
this.pieChart.update(this.stat) this.pieChart.update()
} }
} }

View File

@ -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" import Chart from "https://cdn.jsdelivr.net/npm/chart.js@4.2.1/auto/+esm"
function qpush(arr, val, len) { function qpush(arr, val, len) {
@ -53,7 +52,8 @@ class Stat {
} }
class AreaChart { class AreaChart {
constructor(element, width=60) { constructor(element, stat, width=60) {
this.stat = stat
this.width = width this.width = width
this.canvas = element.appendChild(document.createElement("canvas")) this.canvas = element.appendChild(document.createElement("canvas"))
this.data ={ this.data ={
@ -93,10 +93,9 @@ class AreaChart {
) )
} }
async update(stat) { async update() {
let now = stat.Date let cpu = this.stat.cpu()
let cpu = stat.cpu() qpush(this.data.labels, this.stat.Date, this.width)
qpush(this.data.labels, now, this.width)
qpush(this.datasets.user, cpu.user, this.width) qpush(this.datasets.user, cpu.user, this.width)
qpush(this.datasets.nice, cpu.nice, this.width) qpush(this.datasets.nice, cpu.nice, this.width)
qpush(this.datasets.sys, cpu.sys, this.width) qpush(this.datasets.sys, cpu.sys, this.width)
@ -107,7 +106,8 @@ class AreaChart {
} }
class PieChart { class PieChart {
constructor(element) { constructor(element, stat) {
this.stat = stat
this.canvas = element.appendChild(document.createElement("canvas")) this.canvas = element.appendChild(document.createElement("canvas"))
this.data ={ this.data ={
labels: ["user", "nice", "sys", "idle", "wait"], labels: ["user", "nice", "sys", "idle", "wait"],
@ -122,11 +122,11 @@ class PieChart {
animation: false, animation: false,
borderWidth: 0, borderWidth: 0,
backgroundColor: [ backgroundColor: [
"blue",
"red", "red",
"green",
"orange", "orange",
"rgba(0, 64, 0, 0.2)", "rgba(255, 255, 0, 0.2)",
"magenta", "cyan",
], ],
plugins: { plugins: {
legend: { display: false }, legend: { display: false },
@ -136,8 +136,8 @@ class PieChart {
) )
} }
async update(stat) { async update() {
let cpu = stat.cpu() let cpu = this.stat.cpu()
this.data.datasets = [{ this.data.datasets = [{
label: "Current", label: "Current",
data: [cpu.user, cpu.nice, cpu.sys, cpu.idle, cpu.wait], data: [cpu.user, cpu.nice, cpu.sys, cpu.idle, cpu.wait],