Replace service and delivery screenshots with live status
This commit is contained in:
+36
-21
@@ -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>
|
||||||
|
|||||||
@@ -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}}
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user