Add Prometheus-backed home server status #10
+114
@@ -1939,3 +1939,117 @@ def hardcore_leaderboard_points_combined():
|
|||||||
return jsonify(rows[:100])
|
return jsonify(rows[:100])
|
||||||
|
|
||||||
# --- end Hardcore stats aggregator -------------------------------------------
|
# --- end Hardcore stats aggregator -------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/server-status")
|
||||||
|
@app.get("/api/server-status")
|
||||||
|
def public_server_status():
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
prometheus_url = os.environ.get("PROMETHEUS_URL", "http://5.0.0.20:9090").rstrip("/")
|
||||||
|
|
||||||
|
def prom_value(query):
|
||||||
|
url = prometheus_url + "/api/v1/query?" + urllib.parse.urlencode({"query": query})
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(url, timeout=2.5) as resp:
|
||||||
|
data = json.loads(resp.read().decode("utf-8"))
|
||||||
|
except Exception:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if data.get("status") != "success":
|
||||||
|
return 0
|
||||||
|
|
||||||
|
total = 0.0
|
||||||
|
for result in data.get("data", {}).get("result", []):
|
||||||
|
value = result.get("value", [None, "0"])[1]
|
||||||
|
try:
|
||||||
|
total += float(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return int(total)
|
||||||
|
|
||||||
|
def q(metric, labels):
|
||||||
|
label_text = ",".join(f'{k}="{v}"' for k, v in labels.items())
|
||||||
|
return f'sum({metric}{{{label_text}}}) or vector(0)'
|
||||||
|
|
||||||
|
def newserv(region, service, ship, version):
|
||||||
|
return prom_value(q("pso_newserv_clients_connected", {
|
||||||
|
"region": region,
|
||||||
|
"service": service,
|
||||||
|
"ship": ship,
|
||||||
|
"version": version,
|
||||||
|
}))
|
||||||
|
|
||||||
|
def adhoc(region, game):
|
||||||
|
return prom_value(q("psppeeps_adhoc_connected_clients_by_product", {
|
||||||
|
"region": region,
|
||||||
|
"service": f"{region}-psppeeps-adhoc",
|
||||||
|
"ship": "psp",
|
||||||
|
"game": game,
|
||||||
|
}))
|
||||||
|
|
||||||
|
us = {
|
||||||
|
"alis_v2": newserv("us", "us-newserv-live", "live", "v2"),
|
||||||
|
"alis_v3": newserv("us", "us-newserv-live", "live", "v3"),
|
||||||
|
"alis_bb": newserv("us", "us-newserv-live", "live", "v4"),
|
||||||
|
"abion_hcbb": newserv("us", "us-newserv-hardcore", "hardcore", "v4"),
|
||||||
|
"adhoc_psp1": adhoc("us", "psp1"),
|
||||||
|
"adhoc_psp2i": adhoc("us", "psp2i"),
|
||||||
|
}
|
||||||
|
|
||||||
|
eu = {
|
||||||
|
"palma_v2": newserv("eu", "eu-newserv-live", "live", "v2"),
|
||||||
|
"palma_v3": newserv("eu", "eu-newserv-live", "live", "v3"),
|
||||||
|
"palma_bb": newserv("eu", "eu-newserv-live", "live", "v4"),
|
||||||
|
"aiedo_hcbb": newserv("eu", "eu-newserv-hardcore", "hardcore", "v4"),
|
||||||
|
"adhoc_psp1": adhoc("eu", "psp1"),
|
||||||
|
"adhoc_psp2i": adhoc("eu", "psp2i"),
|
||||||
|
}
|
||||||
|
|
||||||
|
us_total = sum(us.values())
|
||||||
|
eu_total = sum(eu.values())
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"label": "US Server",
|
||||||
|
"players": us_total,
|
||||||
|
"ships": [
|
||||||
|
{"label": "Alis", "rows": [
|
||||||
|
{"label": "V2", "players": us["alis_v2"]},
|
||||||
|
{"label": "V3", "players": us["alis_v3"]},
|
||||||
|
{"label": "BB", "players": us["alis_bb"]},
|
||||||
|
]},
|
||||||
|
{"label": "Abion", "rows": [
|
||||||
|
{"label": "HC/BB", "players": us["abion_hcbb"]},
|
||||||
|
]},
|
||||||
|
{"label": "AdHoc-US", "rows": [
|
||||||
|
{"label": "PSP1", "players": us["adhoc_psp1"]},
|
||||||
|
{"label": "PSP2i", "players": us["adhoc_psp2i"]},
|
||||||
|
]},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "EU Server",
|
||||||
|
"players": eu_total,
|
||||||
|
"ships": [
|
||||||
|
{"label": "Palma", "rows": [
|
||||||
|
{"label": "V2", "players": eu["palma_v2"]},
|
||||||
|
{"label": "V3", "players": eu["palma_v3"]},
|
||||||
|
{"label": "BB", "players": eu["palma_bb"]},
|
||||||
|
]},
|
||||||
|
{"label": "Aiedo", "rows": [
|
||||||
|
{"label": "HC/BB", "players": eu["aiedo_hcbb"]},
|
||||||
|
]},
|
||||||
|
{"label": "AdHoc-EU", "rows": [
|
||||||
|
{"label": "PSP1", "players": eu["adhoc_psp1"]},
|
||||||
|
{"label": "PSP2i", "players": eu["adhoc_psp2i"]},
|
||||||
|
]},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const top = rows.slice(0, 5);
|
const top = rows.slice(0, 10);
|
||||||
|
|
||||||
if (!top.length) {
|
if (!top.length) {
|
||||||
list.innerHTML = `<li><span class="rank">1.</span><span>—</span></li>`;
|
list.innerHTML = `<li><span class="rank">1.</span><span>—</span></li>`;
|
||||||
|
|||||||
+45
-37
@@ -8,8 +8,9 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="style.css?v=home-hardcore-preline-20260610-1">
|
<link rel="stylesheet" href="style.css?v=home-status-two-boxes-2">
|
||||||
<script src="app.js?v=account-status-label-20260609" defer></script>
|
<script src="app.js?v=account-status-label-20260609" defer></script>
|
||||||
|
<script src="server-status.js?v=home-status-prometheus-2" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="site-shell">
|
<div class="site-shell">
|
||||||
@@ -52,30 +53,48 @@
|
|||||||
|
|
||||||
<h1 id="server-status-heading" class="section-title">Server Status</h1>
|
<h1 id="server-status-heading" class="section-title">Server Status</h1>
|
||||||
|
|
||||||
<div class="status-list" role="list" aria-label="Current server player counts">
|
<div class="status-list status-list--regions" role="list" aria-label="Current server player counts" data-server-status="loading">
|
||||||
<div class="status-row status-parent" role="listitem"><span>US Server</span><span></span></div>
|
<div class="status-region" role="group" aria-label="US Server player counts">
|
||||||
|
<div class="status-region-header">
|
||||||
|
<span>US Server</span><span>Loading…</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-ship">
|
||||||
|
<div class="status-ship-name">Alis</div>
|
||||||
|
<div class="status-row"><span>V2</span><span>—</span></div>
|
||||||
|
<div class="status-row"><span>V3</span><span>—</span></div>
|
||||||
|
<div class="status-row"><span>BB</span><span>—</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="status-ship">
|
||||||
|
<div class="status-ship-name">Abion</div>
|
||||||
|
<div class="status-row"><span>HC/BB</span><span>—</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="status-ship">
|
||||||
|
<div class="status-ship-name">AdHoc-US</div>
|
||||||
|
<div class="status-row"><span>PSP1</span><span>—</span></div>
|
||||||
|
<div class="status-row"><span>PSP2i</span><span>—</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="status-row status-parent" role="listitem"><span>Alis</span><span>0 Players</span></div>
|
<div class="status-region" role="group" aria-label="EU Server player counts">
|
||||||
<div class="status-row status-child" role="listitem"><span>V2</span><span>0 Players</span></div>
|
<div class="status-region-header">
|
||||||
<div class="status-row status-child" role="listitem"><span>V3</span><span>0 Players</span></div>
|
<span>EU Server</span><span>Loading…</span>
|
||||||
<div class="status-row status-child" role="listitem"><span>BB</span><span>0 Players</span></div>
|
</div>
|
||||||
|
<div class="status-ship">
|
||||||
<div class="status-row status-parent" role="listitem"><span>Abion</span><span></span></div>
|
<div class="status-ship-name">Palma</div>
|
||||||
<div class="status-row status-child" role="listitem"><span>HC</span><span>0 Players</span></div>
|
<div class="status-row"><span>V2</span><span>—</span></div>
|
||||||
|
<div class="status-row"><span>V3</span><span>—</span></div>
|
||||||
<div class="status-row status-parent" role="listitem"><span>EU Server</span><span></span></div>
|
<div class="status-row"><span>BB</span><span>—</span></div>
|
||||||
|
</div>
|
||||||
<div class="status-row status-parent" role="listitem"><span>Palma</span><span>0 Players</span></div>
|
<div class="status-ship">
|
||||||
<div class="status-row status-child" role="listitem"><span>V2</span><span>0 Players</span></div>
|
<div class="status-ship-name">Aiedo</div>
|
||||||
<div class="status-row status-child" role="listitem"><span>V3</span><span>0 Players</span></div>
|
<div class="status-row"><span>HC/BB</span><span>—</span></div>
|
||||||
<div class="status-row status-child" role="listitem"><span>BB</span><span>0 Players</span></div>
|
</div>
|
||||||
|
<div class="status-ship">
|
||||||
<div class="status-row status-parent" role="listitem"><span>Aiedo</span><span></span></div>
|
<div class="status-ship-name">AdHoc-EU</div>
|
||||||
<div class="status-row status-child" role="listitem"><span>HC</span><span>0 Players</span></div>
|
<div class="status-row"><span>PSP1</span><span>—</span></div>
|
||||||
|
<div class="status-row"><span>PSP2i</span><span>—</span></div>
|
||||||
<div class="status-row status-parent" role="listitem"><span>PSP Ship</span><span></span></div>
|
</div>
|
||||||
<div class="status-row status-child" role="listitem"><span>PSP1</span><span>0 Players</span></div>
|
</div>
|
||||||
<div class="status-row status-child" role="listitem"><span>PSP2i</span><span>0 Players</span></div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -90,22 +109,11 @@
|
|||||||
|
|
||||||
<section class="card leaderboard-card" aria-labelledby="hardcore-heading">
|
<section class="card leaderboard-card" aria-labelledby="hardcore-heading">
|
||||||
<h2 id="hardcore-heading" class="section-title">Hardcore Leaderboard</h2>
|
<h2 id="hardcore-heading" class="section-title">Hardcore Leaderboard</h2>
|
||||||
<ol class="leaderboard-list leaderboard-list--home-hardcore" id="home-hardcore-leaderboard-body" aria-label="Top five hardcore players">
|
<ol class="leaderboard-list leaderboard-list--home-hardcore" id="home-hardcore-leaderboard-body" aria-label="Top ten hardcore players">
|
||||||
<li><span class="rank">1.</span><span>Loading...</span></li>
|
<li><span class="rank">1.</span><span>Loading...</span></li>
|
||||||
</ol>
|
</ol>
|
||||||
<a class="small-link home-leaderboard-more" href="leaderboards.html">more</a>
|
<a class="small-link home-leaderboard-more" href="leaderboards.html">more</a>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="card leaderboard-card" aria-labelledby="crank-heading">
|
|
||||||
<h2 id="crank-heading" class="section-title">C Rank Points</h2>
|
|
||||||
<ol class="leaderboard-list" aria-label="Top five C Rank point totals">
|
|
||||||
<li><span class="rank">1.</span><span>—</span></li>
|
|
||||||
<li><span class="rank">2.</span><span>—</span></li>
|
|
||||||
<li><span class="rank">3.</span><span>—</span></li>
|
|
||||||
<li><span class="rank">4.</span><span>—</span></li>
|
|
||||||
<li><span class="rank">5.</span><span>—</span></li>
|
|
||||||
</ol>
|
|
||||||
</section>
|
|
||||||
</aside>
|
</aside>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@@ -139,6 +147,6 @@
|
|||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
<script src="hero-cycle.js?v=force-cycle-3" defer></script>
|
<script src="hero-cycle.js?v=force-cycle-3" defer></script>
|
||||||
<script src="home-leaderboard.js?v=home-hardcore-preline-20260610-1" defer></script>
|
<script src="home-leaderboard.js?v=home-hardcore-top10-1" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
(() => {
|
||||||
|
const list = document.querySelector(".status-list");
|
||||||
|
if (!list) return;
|
||||||
|
|
||||||
|
const esc = (value) => String(value ?? "").replace(/[&<>"']/g, (ch) => ({
|
||||||
|
"&": "&",
|
||||||
|
"<": "<",
|
||||||
|
">": ">",
|
||||||
|
"\"": """,
|
||||||
|
"'": "'",
|
||||||
|
}[ch]));
|
||||||
|
|
||||||
|
const players = (value) => {
|
||||||
|
const n = Number(value || 0);
|
||||||
|
return `${n.toLocaleString()} ${n === 1 ? "Player" : "Players"}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const render = (data) => {
|
||||||
|
const servers = Array.isArray(data?.servers) ? data.servers : [];
|
||||||
|
if (!servers.length) return;
|
||||||
|
|
||||||
|
list.classList.add("status-list--regions");
|
||||||
|
list.innerHTML = servers.map((server) => `
|
||||||
|
<div class="status-region" role="group" aria-label="${esc(server.label)} player counts">
|
||||||
|
<div class="status-row status-parent status-server" role="listitem">
|
||||||
|
<span>${esc(server.label)}</span><span>${players(server.players)}</span>
|
||||||
|
</div>
|
||||||
|
${(server.ships || []).map((ship) => `
|
||||||
|
<div class="status-row status-parent status-ship" role="listitem">
|
||||||
|
<span>${esc(ship.label)}</span><span></span>
|
||||||
|
</div>
|
||||||
|
${(ship.rows || []).map((row) => `
|
||||||
|
<div class="status-row status-child" role="listitem">
|
||||||
|
<span>${esc(row.label)}</span><span>${players(row.players)}</span>
|
||||||
|
</div>
|
||||||
|
`).join("")}
|
||||||
|
`).join("")}
|
||||||
|
</div>
|
||||||
|
`).join("");
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch("/api/server-status", { cache: "no-store" })
|
||||||
|
.then((res) => res.ok ? res.json() : null)
|
||||||
|
.then((data) => data && render(data))
|
||||||
|
.catch(() => {});
|
||||||
|
})();
|
||||||
@@ -1857,4 +1857,61 @@ button.inline-link,
|
|||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}\n\n
|
||||||
|
/* Home right column: stretch Hardcore leaderboard after C Rank removal */
|
||||||
|
.right-stack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.right-stack .leaderboard-card {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
\n
|
||||||
|
|
||||||
|
|
||||||
|
/* Home layout: stretch right column to match server status card */
|
||||||
|
.home-grid {
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-stack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-stack .leaderboard-card {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Home server status: split US/EU into two inner boxes */
|
||||||
|
.server-card .status-list.status-list--regions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-card .status-region {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.35rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 0.45rem;
|
||||||
|
background: rgba(0, 0, 0, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-card .status-region .status-row {
|
||||||
|
grid-template-columns: minmax(0, 1fr) max-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.server-card .status-list.status-list--regions {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user