Replace service and delivery screenshots with live status

This commit is contained in:
Keith Casko
2026-10-02 11:05:45 -04:00
parent 62fc9fe0f4
commit 77d8cdd357
4 changed files with 154 additions and 21 deletions
+36 -21
View File
@@ -306,16 +306,19 @@
<div class="service-row"><strong>Uptime Kuma</strong><span>Availability</span><span>SSO</span><i>CONFIGURED</i></div> <div class="service-row"><strong>Uptime Kuma</strong><span>Availability</span><span>SSO</span><i>CONFIGURED</i></div>
</div> </div>
<figure class="portal-shot"> <div class="portal-shot live-service-panel" aria-label="Live service health">
<figcaption> <figcaption>
<span>SERVICE PORTAL</span> <span>LIVE SERVICE HEALTH</span>
<small>Dashy / internal access layer</small> <small>SANITIZED ENDPOINT CHECKS</small>
</figcaption> </figcaption>
<a href="assets/images/portfolio-cloud-dashboard-overview.png" target="_blank"> <div class="service-health-grid">
<img src="assets/images/portfolio-cloud-dashboard-overview.png" alt="ByteGeist Dashy service portal snapshot" /> <div><span>GITEA</span><strong id="health-gitea">CHECKING</strong><small id="gitea-version">VERSION --</small></div>
</a> <div><span>WOODPECKER</span><strong id="health-woodpecker">CHECKING</strong><small>CI / DELIVERY</small></div>
<p class="evidence-note">CAPTURED OCT 2026 · SERVICE PORTAL CONFIGURATION SNAPSHOT</p> <div><span>UPTIME KUMA</span><strong id="health-kuma">CHECKING</strong><small>AVAILABILITY</small></div>
</figure> <div><span>PROMETHEUS</span><strong id="health-prometheus">CHECKING</strong><small>METRICS</small></div>
</div>
<p class="evidence-note">LIVE HEALTH CHECKS · REFRESHED WITH /API/STATUS EVERY 15s</p>
</div>
</div> </div>
</section> </section>
@@ -340,19 +343,31 @@
<div><span>04</span><strong>DEPLOY</strong><small>Docker host</small></div> <div><span>04</span><strong>DEPLOY</strong><small>Docker host</small></div>
</div> </div>
<div class="pipeline-shots"> <div class="pipeline-shots live-pipeline-shots">
<figure> <div class="live-delivery-card">
<figcaption>GITEA / REPOSITORIES · OCT 2026 SNAPSHOT</figcaption> <figcaption>GITEA / SOURCE CONTROL</figcaption>
<a href="assets/images/portfolio-gitea-dashboard.png" target="_blank"> <div class="delivery-state">
<img src="assets/images/portfolio-gitea-dashboard.png" alt="Gitea repositories dashboard" /> <span class="delivery-kicker">SERVICE STATE</span>
</a> <strong id="pipeline-gitea-state">CHECKING</strong>
</figure> <small id="pipeline-gitea-version">VERSION --</small>
<figure> </div>
<figcaption>WOODPECKER / PIPELINE · DEPLOYMENT EVIDENCE</figcaption> <div class="delivery-meta">
<a href="assets/images/portfolio-woodpecker-pipeline.png" target="_blank"> <span>ROLE</span><strong>REPOSITORY HOST</strong>
<img src="assets/images/portfolio-woodpecker-pipeline.png" alt="Successful Woodpecker CI pipeline" /> <span>TRANSPORT</span><strong>SSH / 2222</strong>
</a> </div>
</figure> </div>
<div class="live-delivery-card">
<figcaption>WOODPECKER / DELIVERY</figcaption>
<div class="delivery-state">
<span class="delivery-kicker">SERVICE STATE</span>
<strong id="pipeline-woodpecker-state">CHECKING</strong>
<small>AUTOMATED DEPLOYMENT</small>
</div>
<div class="delivery-meta">
<span>DEPLOYED COMMIT</span><strong id="deployed-commit">-------</strong>
<span>TARGET</span><strong>BYTEGEIST.CASKO.DEV</strong>
</div>
</div>
</div> </div>
</div> </div>
</section> </section>
+1
View File
@@ -0,0 +1 @@
.live-service-panel{min-width:0}.service-health-grid{display:grid;grid-template-columns:1fr 1fr;gap:1px;background:var(--line);border:1px solid var(--line)}.service-health-grid div{min-height:104px;padding:15px;background:#080c12}.service-health-grid span,.delivery-kicker,.delivery-meta span{display:block;color:var(--dim);font:.54rem var(--mono);letter-spacing:.08em}.service-health-grid strong,.delivery-state strong{display:block;margin-top:14px;color:var(--green);font:500 1rem var(--mono)}.service-health-grid small,.delivery-state small{display:block;margin-top:8px;color:var(--muted);font:.52rem var(--mono)}.status-down{color:var(--amber)!important}.live-pipeline-shots{align-items:stretch}.live-delivery-card{border:1px solid var(--line);background:#080c12;min-height:250px}.live-delivery-card figcaption{padding:11px 14px;border-bottom:1px solid var(--line);color:var(--dim);font:.58rem var(--mono);letter-spacing:.06em}.delivery-state{padding:22px 18px;border-bottom:1px solid var(--line)}.delivery-state strong{font-size:1.35rem}.delivery-meta{display:grid;grid-template-columns:1fr auto;gap:11px 16px;padding:18px}.delivery-meta strong{color:var(--muted);font:500 .64rem var(--mono);text-align:right}@media(max-width:560px){.delivery-meta{grid-template-columns:1fr}.delivery-meta strong{text-align:left}}
+56
View File
@@ -1,3 +1,5 @@
const livePanelStyles=document.createElement("link");livePanelStyles.rel="stylesheet";livePanelStyles.href="live-panels.css";document.head.appendChild(livePanelStyles);
const timeElement = document.querySelector("#local-time"); const timeElement = document.querySelector("#local-time");
const refreshElement = document.querySelector("#refresh-indicator"); const refreshElement = document.querySelector("#refresh-indicator");
@@ -13,6 +15,15 @@ const fields = {
opsUptime: document.querySelector("#ops-uptime-value"), opsUptime: document.querySelector("#ops-uptime-value"),
opsContainers: document.querySelector("#ops-containers-value"), opsContainers: document.querySelector("#ops-containers-value"),
opsTargets: document.querySelector("#ops-targets-value"), opsTargets: document.querySelector("#ops-targets-value"),
healthGitea: document.querySelector("#health-gitea"),
healthWoodpecker: document.querySelector("#health-woodpecker"),
healthKuma: document.querySelector("#health-kuma"),
healthPrometheus: document.querySelector("#health-prometheus"),
giteaVersion: document.querySelector("#gitea-version"),
pipelineGiteaState: document.querySelector("#pipeline-gitea-state"),
pipelineGiteaVersion: document.querySelector("#pipeline-gitea-version"),
pipelineWoodpeckerState: document.querySelector("#pipeline-woodpecker-state"),
deployedCommit: document.querySelector("#deployed-commit"),
}; };
const telemetryHistory = { const telemetryHistory = {
@@ -56,6 +67,22 @@ function setUnavailable() {
fields.opsUptime.textContent = "--"; fields.opsUptime.textContent = "--";
fields.opsContainers.textContent = "--"; fields.opsContainers.textContent = "--";
fields.opsTargets.textContent = "--"; fields.opsTargets.textContent = "--";
for (const element of [
fields.healthGitea,
fields.healthWoodpecker,
fields.healthKuma,
fields.healthPrometheus,
fields.pipelineGiteaState,
fields.pipelineWoodpeckerState,
]) {
if (element) {
element.textContent = "UNAVAILABLE";
element.classList.add("status-down");
}
}
fields.giteaVersion.textContent = "VERSION --";
fields.pipelineGiteaVersion.textContent = "VERSION --";
fields.deployedCommit.textContent = "-------";
refreshElement.textContent = "TELEMETRY UNAVAILABLE"; refreshElement.textContent = "TELEMETRY UNAVAILABLE";
refreshElement.classList.add("telemetry-error"); refreshElement.classList.add("telemetry-error");
} }
@@ -77,6 +104,17 @@ function pushHistory(series, value) {
if (series.length > 24) series.shift(); if (series.length > 24) series.shift();
} }
function serviceLabel(service) {
return service && service.up ? "ONLINE" : "UNAVAILABLE";
}
function setServiceState(element, service) {
if (!element) return;
const up = Boolean(service && service.up);
element.textContent = up ? "ONLINE" : "UNAVAILABLE";
element.classList.toggle("status-down", !up);
}
function renderStatus(data) { function renderStatus(data) {
const cpu = Number(data.host.cpu_percent); const cpu = Number(data.host.cpu_percent);
const memory = Number(data.host.memory_percent); const memory = Number(data.host.memory_percent);
@@ -97,6 +135,23 @@ function renderStatus(data) {
fields.opsContainers.textContent = containers; fields.opsContainers.textContent = containers;
fields.opsTargets.textContent = targets; fields.opsTargets.textContent = targets;
const services = data.services || {};
setServiceState(fields.healthGitea, services.gitea);
setServiceState(fields.healthWoodpecker, services.woodpecker);
setServiceState(fields.healthKuma, services.uptime_kuma);
setServiceState(fields.healthPrometheus, services.prometheus);
setServiceState(fields.pipelineGiteaState, services.gitea);
setServiceState(fields.pipelineWoodpeckerState, services.woodpecker);
const giteaVersion = services.gitea && services.gitea.version
? "VERSION " + services.gitea.version
: "VERSION --";
fields.giteaVersion.textContent = giteaVersion;
fields.pipelineGiteaVersion.textContent = giteaVersion;
fields.deployedCommit.textContent = data.deployment && data.deployment.commit
? data.deployment.commit
: "-------";
pushHistory(telemetryHistory.cpu, cpu); pushHistory(telemetryHistory.cpu, cpu);
pushHistory(telemetryHistory.memory, memory); pushHistory(telemetryHistory.memory, memory);
renderOpsBars("#ops-cpu-bars", telemetryHistory.cpu); renderOpsBars("#ops-cpu-bars", telemetryHistory.cpu);
@@ -143,3 +198,4 @@ refreshTelemetry();
window.setInterval(updateClock, 1000); window.setInterval(updateClock, 1000);
window.setInterval(updateRefreshLabel, 1000); window.setInterval(updateRefreshLabel, 1000);
window.setInterval(refreshTelemetry, 15000); window.setInterval(refreshTelemetry, 15000);
+61
View File
@@ -3,10 +3,12 @@ import os
import time import time
import urllib.parse import urllib.parse
import urllib.request import urllib.request
from pathlib import Path
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
PROMETHEUS_URL = os.getenv("PROMETHEUS_URL", "http://host.docker.internal:9090").rstrip("/") PROMETHEUS_URL = os.getenv("PROMETHEUS_URL", "http://host.docker.internal:9090").rstrip("/")
TIMEOUT = float(os.getenv("PROMETHEUS_TIMEOUT", "2.5")) TIMEOUT = float(os.getenv("PROMETHEUS_TIMEOUT", "2.5"))
SITE_GIT_DIR = Path(os.getenv("SITE_GIT_DIR", "/site/.git"))
QUERIES = { QUERIES = {
"cpu_percent": '100-(avg(rate(node_cpu_seconds_total{mode="idle"}[2m]))*100)', "cpu_percent": '100-(avg(rate(node_cpu_seconds_total{mode="idle"}[2m]))*100)',
@@ -15,11 +17,23 @@ QUERIES = {
"running_containers": 'count(count by (name) (container_last_seen{name!=""}))', "running_containers": 'count(count by (name) (container_last_seen{name!=""}))',
} }
HTTP_CHECKS = {
"gitea": "http://host.docker.internal:3000/api/v1/version",
"woodpecker": "http://host.docker.internal:8010/",
"uptime_kuma": "http://host.docker.internal:3001/",
"prometheus": "http://host.docker.internal:9090/-/healthy",
}
def get_json(url): def get_json(url):
request = urllib.request.Request(url, headers={"Accept": "application/json"}) request = urllib.request.Request(url, headers={"Accept": "application/json"})
with urllib.request.urlopen(request, timeout=TIMEOUT) as response: with urllib.request.urlopen(request, timeout=TIMEOUT) as response:
return json.load(response) return json.load(response)
def get_text(url):
request = urllib.request.Request(url, headers={"Accept": "*/*"})
with urllib.request.urlopen(request, timeout=TIMEOUT) as response:
return response.status, response.read(1024).decode("utf-8", "replace")
def prometheus_query(expression): def prometheus_query(expression):
url = f"{PROMETHEUS_URL}/api/v1/query?{urllib.parse.urlencode({'query': expression})}" url = f"{PROMETHEUS_URL}/api/v1/query?{urllib.parse.urlencode({'query': expression})}"
payload = get_json(url) payload = get_json(url)
@@ -43,9 +57,51 @@ def target_health():
"jobs": jobs, "jobs": jobs,
} }
def check_services():
services = {}
for name, url in HTTP_CHECKS.items():
try:
status, body = get_text(url)
services[name] = {"up": 200 <= status < 400, "status": status}
if name == "gitea":
try:
services[name]["version"] = json.loads(body).get("version")
except Exception:
pass
except Exception:
services[name] = {"up": False, "status": None}
return services
def deployed_commit():
try:
head = (SITE_GIT_DIR / "HEAD").read_text(encoding="utf-8").strip()
if head.startswith("ref: "):
ref = head[5:]
ref_path = SITE_GIT_DIR / ref
if ref_path.exists():
value = ref_path.read_text(encoding="utf-8").strip()
else:
value = None
packed = SITE_GIT_DIR / "packed-refs"
if packed.exists():
for line in packed.read_text(encoding="utf-8").splitlines():
if line and not line.startswith(("#", "^")):
sha, name = line.split(" ", 1)
if name == ref:
value = sha
break
if not value:
return None
else:
value = head
return value[:7]
except Exception:
return None
def build_status(): def build_status():
values = {name: prometheus_query(query) for name, query in QUERIES.items()} values = {name: prometheus_query(query) for name, query in QUERIES.items()}
targets = target_health() targets = target_health()
services = check_services()
return { return {
"ok": True, "ok": True,
"source": "Prometheus / node_exporter / cAdvisor", "source": "Prometheus / node_exporter / cAdvisor",
@@ -57,6 +113,11 @@ def build_status():
}, },
"containers": {"running": int(values["running_containers"])}, "containers": {"running": int(values["running_containers"])},
"prometheus": targets, "prometheus": targets,
"services": services,
"deployment": {
"commit": deployed_commit(),
"site": "bytegeist.casko.dev",
},
} }
class Handler(BaseHTTPRequestHandler): class Handler(BaseHTTPRequestHandler):