Add all bp
This commit is contained in:
32
css/grafana.css
Normal file
32
css/grafana.css
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Grafana embed wrappers for rx.kitsunehosting.net
|
||||
*
|
||||
* Cross-origin iframes cannot be styled from this page — Grafana renders on
|
||||
* grafana.kitsunehosting.net. Use URL params on the iframe src instead:
|
||||
* theme=dark|light panel colors
|
||||
* kiosk=tv hide Grafana chrome (nav, menus)
|
||||
* refresh=30s auto-refresh interval
|
||||
*
|
||||
* Custom Grafana themes require server-side config on the Grafana instance.
|
||||
*/
|
||||
|
||||
.grafana-embed {
|
||||
margin-bottom: 12px;
|
||||
background: #000000;
|
||||
border: 2px inset #808080;
|
||||
padding: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.grafana-embed iframe {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
border: 0;
|
||||
background: #000000;
|
||||
}
|
||||
|
||||
/* Match the retro terminal vibe when Grafana is in dark mode */
|
||||
.grafana-embed--dark {
|
||||
box-shadow: inset 0 0 12px rgba(0, 255, 0, 0.08);
|
||||
}
|
||||
208
css/style.css
Normal file
208
css/style.css
Normal file
@@ -0,0 +1,208 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
background: #c0c0c0;
|
||||
color: #000000;
|
||||
font-family: "Times New Roman", Times, serif;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0000ee;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: #551a8b;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #ff0000;
|
||||
}
|
||||
|
||||
.banner {
|
||||
text-align: center;
|
||||
background: #000080;
|
||||
color: #ffff00;
|
||||
padding: 12px;
|
||||
border: 4px outset #808080;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 2px;
|
||||
text-shadow: 2px 2px #000000;
|
||||
}
|
||||
|
||||
.nav {
|
||||
text-align: center;
|
||||
margin: 12px 0;
|
||||
padding: 8px;
|
||||
background: #d4d0c8;
|
||||
border: 2px inset #808080;
|
||||
}
|
||||
|
||||
.nav a {
|
||||
margin: 0 16px;
|
||||
font-weight: bold;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.nav a.active {
|
||||
color: #000000;
|
||||
text-decoration: none;
|
||||
background: #ffffff;
|
||||
padding: 2px 6px;
|
||||
border: 1px inset #808080;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
height: 2px;
|
||||
background: #808080;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: #d4d0c8;
|
||||
border: 2px inset #808080;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
margin: 0 0 12px 0;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.live-feed {
|
||||
text-align: center;
|
||||
margin: 0 auto;
|
||||
max-width: 960px;
|
||||
}
|
||||
|
||||
.live-feed img {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
aspect-ratio: 16 / 9;
|
||||
object-fit: contain;
|
||||
border: 3px inset #808080;
|
||||
background: #000000;
|
||||
}
|
||||
|
||||
.live-label {
|
||||
margin-top: 8px;
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
font-size: 14px;
|
||||
color: #008000;
|
||||
}
|
||||
|
||||
.live-timestamp {
|
||||
margin: 4px 0 0;
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
font-size: 12px;
|
||||
color: #404040;
|
||||
}
|
||||
|
||||
.blink {
|
||||
animation: blink 1s step-end infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.content-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.content-table td {
|
||||
vertical-align: top;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
.graphs-panel {
|
||||
min-height: 280px;
|
||||
}
|
||||
|
||||
.graph-placeholder {
|
||||
height: 200px;
|
||||
background: #000000;
|
||||
border: 2px inset #808080;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #00ff00;
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
font-size: 14px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.stats-panel {
|
||||
min-height: 280px;
|
||||
}
|
||||
|
||||
.stat-block {
|
||||
text-align: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background: #ffffff;
|
||||
border: 2px outset #808080;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 48px;
|
||||
font-weight: bold;
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
color: #000080;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.stat-value--ok {
|
||||
color: #008000;
|
||||
}
|
||||
|
||||
.stat-value--bad {
|
||||
color: #cc0000;
|
||||
}
|
||||
|
||||
#stat-status {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
margin-top: 4px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.placeholder-page {
|
||||
text-align: center;
|
||||
padding: 48px 16px;
|
||||
}
|
||||
|
||||
.placeholder-page h2 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.under-construction {
|
||||
font-size: 48px;
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
margin-top: 24px;
|
||||
font-size: 12px;
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
color: #404040;
|
||||
}
|
||||
91
index.html
Normal file
91
index.html
Normal file
@@ -0,0 +1,91 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>RX.KITSUNEHOSTING.NET - Live</title>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<link rel="stylesheet" href="css/grafana.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="banner">RX.KITSUNEHOSTING.NET</div>
|
||||
|
||||
<nav class="nav">
|
||||
<a href="index.html" class="active">Home/Live</a>
|
||||
<a href="tools.html">Tools</a>
|
||||
<a href="radio.html">Radio</a>
|
||||
</nav>
|
||||
|
||||
<hr>
|
||||
|
||||
<section class="panel live-feed">
|
||||
<h2 class="panel-title">Live</h2>
|
||||
<img id="live-feed" src="live/live.png" alt="Live feed" width="1920" height="1080">
|
||||
<p class="live-label"><span class="blink">●</span> LIVE</p>
|
||||
<p id="live-timestamp" class="live-timestamp"></p>
|
||||
</section>
|
||||
|
||||
<hr>
|
||||
|
||||
<table class="content-table" role="presentation">
|
||||
<tr>
|
||||
<td width="60%">
|
||||
<section class="panel graphs-panel">
|
||||
<h2 class="panel-title">Graphs</h2>
|
||||
<div class="grafana-embed">
|
||||
<iframe
|
||||
src="https://grafana.kitsunehosting.net/d-solo/ffp84u9sdyccgb/amateur-radio?orgId=2&timezone=browser&refresh=auto&theme=light&panelId=1&__feature.dashboardSceneSolo"
|
||||
width="450"
|
||||
height="200"
|
||||
title="Amateur radio graph 1"
|
||||
loading="lazy"
|
||||
></iframe>
|
||||
</div>
|
||||
<div class="grafana-embed">
|
||||
<iframe
|
||||
src="https://grafana.kitsunehosting.net/d-solo/ffp84u9sdyccgb/amateur-radio?orgId=2&timezone=browser&refresh=auto&theme=light&panelId=1&__feature.dashboardSceneSolo"
|
||||
width="450"
|
||||
height="200"
|
||||
title="Amateur radio graph 2"
|
||||
loading="lazy"
|
||||
></iframe>
|
||||
</div>
|
||||
<div class="grafana-embed">
|
||||
<iframe
|
||||
src="https://grafana.kitsunehosting.net/d-solo/ffp84u9sdyccgb/amateur-radio?orgId=2&timezone=browser&refresh=auto&theme=light&panelId=1&__feature.dashboardSceneSolo"
|
||||
width="450"
|
||||
height="200"
|
||||
title="Amateur radio graph 3"
|
||||
loading="lazy"
|
||||
></iframe>
|
||||
</div>
|
||||
</section>
|
||||
</td>
|
||||
<td width="40%">
|
||||
<section class="panel stats-panel">
|
||||
<h2 class="panel-title">Overall Stats</h2>
|
||||
<div class="stat-block">
|
||||
<div id="stat-uptime" class="stat-value">---</div>
|
||||
<div class="stat-label">Uptime</div>
|
||||
</div>
|
||||
<div class="stat-block">
|
||||
<div id="stat-listeners" class="stat-value">---</div>
|
||||
<div class="stat-label">Live Listeners</div>
|
||||
</div>
|
||||
<div class="stat-block">
|
||||
<div id="stat-status" class="stat-value">---</div>
|
||||
<div class="stat-label">Status</div>
|
||||
</div>
|
||||
</section>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="footer">
|
||||
rx.kitsunehosting.net
|
||||
</div>
|
||||
|
||||
<script src="js/live.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
161
js/live.js
Normal file
161
js/live.js
Normal file
@@ -0,0 +1,161 @@
|
||||
(function () {
|
||||
var LIVE_IMAGE = "live/live.png";
|
||||
var LIVE_DATA = "live/live.json";
|
||||
var REFRESH_MS = 60 * 1000;
|
||||
var TICK_MS = 1000;
|
||||
|
||||
var img = document.getElementById("live-feed");
|
||||
var timestampEl = document.getElementById("live-timestamp");
|
||||
var statUptime = document.getElementById("stat-uptime");
|
||||
var statListeners = document.getElementById("stat-listeners");
|
||||
var statStatus = document.getElementById("stat-status");
|
||||
var lastCapturedAt = null;
|
||||
|
||||
if (!img) {
|
||||
return;
|
||||
}
|
||||
|
||||
function formatTimeAgo(unixSeconds) {
|
||||
var diff = Math.floor(Date.now() / 1000) - unixSeconds;
|
||||
if (diff < 0) {
|
||||
diff = 0;
|
||||
}
|
||||
|
||||
if (diff < 60) {
|
||||
return diff === 1 ? "1 second ago" : diff + " seconds ago";
|
||||
}
|
||||
|
||||
var minutes = Math.floor(diff / 60);
|
||||
if (minutes < 60) {
|
||||
return minutes === 1 ? "1 minute ago" : minutes + " minutes ago";
|
||||
}
|
||||
|
||||
var hours = Math.floor(diff / 3600);
|
||||
if (hours < 24) {
|
||||
return hours === 1 ? "1 hour ago" : hours + " hours ago";
|
||||
}
|
||||
|
||||
var days = Math.floor(diff / 86400);
|
||||
return days === 1 ? "1 day ago" : days + " days ago";
|
||||
}
|
||||
|
||||
function formatDuration(seconds) {
|
||||
var diff = Math.max(0, Math.floor(seconds));
|
||||
var days = Math.floor(diff / 86400);
|
||||
var hours = Math.floor((diff % 86400) / 3600);
|
||||
var minutes = Math.floor((diff % 3600) / 60);
|
||||
|
||||
if (days > 0) {
|
||||
return days + "d " + hours + "h";
|
||||
}
|
||||
if (hours > 0) {
|
||||
return hours + "h " + minutes + "m";
|
||||
}
|
||||
if (minutes > 0) {
|
||||
return minutes + "m";
|
||||
}
|
||||
return diff + "s";
|
||||
}
|
||||
|
||||
function formatUptime(value) {
|
||||
if (value === null || value === undefined || value === "") {
|
||||
return "---";
|
||||
}
|
||||
if (typeof value === "number") {
|
||||
return formatDuration(value);
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function formatListeners(value) {
|
||||
if (value === null || value === undefined || value === "") {
|
||||
return "---";
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function formatStatus(value) {
|
||||
if (value === null || value === undefined || value === "") {
|
||||
return "---";
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function updateTimeAgo() {
|
||||
if (!timestampEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!lastCapturedAt) {
|
||||
timestampEl.textContent = "";
|
||||
return;
|
||||
}
|
||||
|
||||
timestampEl.textContent = "Captured " + formatTimeAgo(lastCapturedAt);
|
||||
}
|
||||
|
||||
function updateStats(data) {
|
||||
if (statUptime) {
|
||||
statUptime.textContent = formatUptime(data.uptime);
|
||||
}
|
||||
if (statListeners) {
|
||||
var listeners = data.listeners;
|
||||
if (listeners === undefined) {
|
||||
listeners = data.live_listeners;
|
||||
}
|
||||
statListeners.textContent = formatListeners(listeners);
|
||||
}
|
||||
if (statStatus) {
|
||||
statStatus.textContent = formatStatus(data.status);
|
||||
statStatus.classList.remove("stat-value--ok", "stat-value--bad");
|
||||
var status = String(data.status || "").toLowerCase();
|
||||
if (status === "online" || status === "ok" || status === "up") {
|
||||
statStatus.classList.add("stat-value--ok");
|
||||
} else if (status === "offline" || status === "down" || status === "error") {
|
||||
statStatus.classList.add("stat-value--bad");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearStats() {
|
||||
updateStats({});
|
||||
}
|
||||
|
||||
function fetchLiveData() {
|
||||
fetch(LIVE_DATA + "?t=" + Date.now())
|
||||
.then(function (response) {
|
||||
if (!response.ok) {
|
||||
throw new Error("live data unavailable");
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(function (data) {
|
||||
if (data && data.timestamp) {
|
||||
lastCapturedAt = data.timestamp;
|
||||
updateTimeAgo();
|
||||
} else {
|
||||
lastCapturedAt = null;
|
||||
updateTimeAgo();
|
||||
}
|
||||
updateStats(data || {});
|
||||
})
|
||||
.catch(function () {
|
||||
lastCapturedAt = null;
|
||||
updateTimeAgo();
|
||||
clearStats();
|
||||
});
|
||||
}
|
||||
|
||||
function refreshLiveImage() {
|
||||
img.src = LIVE_IMAGE + "?t=" + Date.now();
|
||||
fetchLiveData();
|
||||
}
|
||||
|
||||
img.addEventListener("error", function () {
|
||||
img.alt = "Live feed unavailable";
|
||||
});
|
||||
|
||||
refreshLiveImage();
|
||||
setInterval(refreshLiveImage, REFRESH_MS);
|
||||
setInterval(updateTimeAgo, TICK_MS);
|
||||
})();
|
||||
6
live/live.json
Normal file
6
live/live.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"timestamp": 1781667629,
|
||||
"uptime": 86412,
|
||||
"listeners": 0,
|
||||
"status": "online"
|
||||
}
|
||||
BIN
live/live.png
Normal file
BIN
live/live.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 MiB |
32
radio.html
Normal file
32
radio.html
Normal file
@@ -0,0 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>RX.KITSUNEHOSTING.NET - Radio</title>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="banner">RX.KITSUNEHOSTING.NET</div>
|
||||
|
||||
<nav class="nav">
|
||||
<a href="index.html">Home/Live</a>
|
||||
<a href="tools.html">Tools</a>
|
||||
<a href="radio.html" class="active">Radio</a>
|
||||
</nav>
|
||||
|
||||
<hr>
|
||||
|
||||
<section class="panel placeholder-page">
|
||||
<div class="under-construction">♫</div>
|
||||
<h2>Radio</h2>
|
||||
<p>Coming soon.</p>
|
||||
</section>
|
||||
|
||||
<div class="footer">
|
||||
rx.kitsunehosting.net
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
32
tools.html
Normal file
32
tools.html
Normal file
@@ -0,0 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>RX.KITSUNEHOSTING.NET - Tools</title>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="banner">RX.KITSUNEHOSTING.NET</div>
|
||||
|
||||
<nav class="nav">
|
||||
<a href="index.html">Home/Live</a>
|
||||
<a href="tools.html" class="active">Tools</a>
|
||||
<a href="radio.html">Radio</a>
|
||||
</nav>
|
||||
|
||||
<hr>
|
||||
|
||||
<section class="panel placeholder-page">
|
||||
<div class="under-construction">⚙</div>
|
||||
<h2>Tools</h2>
|
||||
<p>Coming soon.</p>
|
||||
</section>
|
||||
|
||||
<div class="footer">
|
||||
rx.kitsunehosting.net
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user