Compare commits

...

15 Commits

Author SHA1 Message Date
incentive 222d493496 Merge pull request 'change draining state' (#12) from feature/account-lock-session-end-fix into main
Reviewed-on: #12
2026-06-14 03:43:09 -04:00
Your Name 5e663c68be change draining state 2026-06-14 03:28:16 -04:00
incentive 759a1a4089 Merge pull request 'server status' (#11) from feature/home-server-status-prometheus into main
Reviewed-on: #11
2026-06-14 01:07:21 -04:00
Your Name 922c63f13d server status 2026-06-14 01:07:03 -04:00
incentive 9e76165638 Merge pull request 'Add Prometheus-backed home server status' (#10) from feature/home-server-status-prometheus into main
Reviewed-on: #10
2026-06-14 01:06:28 -04:00
Your Name 8ae301ab2d Wire homepage server status to Prometheus 2026-06-13 21:52:27 -04:00
Your Name c04356f6a0 Add Caddy-compatible server status route 2026-06-13 21:46:37 -04:00
Your Name d757468b7c Split home server status by region 2026-06-13 21:14:59 -04:00
Your Name 79915431fa Narrow home server status list 2026-06-13 21:11:19 -04:00
Your Name 2a0b162a02 Stretch home right column 2026-06-13 21:10:03 -04:00
Your Name 7ce98d6275 Stretch home hardcore leaderboard card 2026-06-13 21:08:44 -04:00
Your Name 3fdc4fda38 Show top ten hardcore leaderboard on home 2026-06-13 21:07:21 -04:00
Your Name 8d4d1772ca Fix home server status markup 2026-06-13 21:04:12 -04:00
Your Name 733e3149c4 Add Prometheus-backed home server status 2026-06-13 20:55:19 -04:00
incentive 1865a385ad Merge pull request 'feature/fix-bestiary-xp-bonus-bp-tier' (#9) from feature/fix-bestiary-xp-bonus-bp-tier into main
Reviewed-on: #9
2026-06-13 20:40:42 -04:00
7 changed files with 377 additions and 39 deletions
+2
View File
@@ -42,3 +42,5 @@ yarn-error.log*
source-bestiary/
source-drops/
site/server-status.json
site/server-status.json.tmp
+115 -1
View File
@@ -780,7 +780,7 @@ def newserv_account_lock_heartbeat():
SET updated_at = now(),
expires_at = %s
WHERE holder_source = %s
AND state IN ('active', 'draining')
AND state = 'active'
""", (expires_at, source))
refreshed = cur.rowcount
@@ -1939,3 +1939,117 @@ def hardcore_leaderboard_points_combined():
return jsonify(rows[:100])
# --- 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"]},
]},
],
},
],
})
+103
View File
@@ -0,0 +1,103 @@
#!/usr/bin/env python3
import json
import os
import urllib.parse
import urllib.request
from datetime import datetime, timezone
from pathlib import Path
PROMETHEUS_URL = os.environ.get("PROMETHEUS_URL", "http://5.0.0.20:9090").rstrip("/")
SOURCE_URL = os.environ.get("SERVER_STATUS_SOURCE_URL", "").strip()
OUTPUT_PATH = Path(os.environ.get("SERVER_STATUS_JSON", "site/server-status.json"))
TIMEOUT_SECONDS = float(os.environ.get("PROMETHEUS_TIMEOUT_SECONDS", "5"))
def write_data(data):
if not isinstance(data.get("servers"), list):
raise SystemExit("server status JSON missing servers array")
data["generated_at"] = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
tmp_path = OUTPUT_PATH.with_name(OUTPUT_PATH.name + ".tmp")
tmp_path.write_text(json.dumps(data, separators=(",", ":")) + "\n")
os.replace(tmp_path, OUTPUT_PATH)
print(f"wrote {OUTPUT_PATH}")
def prom_value(query):
url = PROMETHEUS_URL + "/api/v1/query?" + urllib.parse.urlencode({"query": query})
with urllib.request.urlopen(url, timeout=TIMEOUT_SECONDS) as response:
body = json.loads(response.read().decode("utf-8"))
result = body.get("data", {}).get("result", [])
if not result:
return 0
return int(float(result[0]["value"][1]))
def q(metric, labels):
label_text = ",".join(f'{key}="{value}"' for key, value 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,
}))
def row(label, players):
return {"label": label, "players": int(players)}
def main():
if SOURCE_URL:
with urllib.request.urlopen(SOURCE_URL, timeout=TIMEOUT_SECONDS) as response:
write_data(json.loads(response.read().decode("utf-8")))
return
us_alis_v2 = newserv("us", "us-newserv-live", "live", "v2")
us_alis_v3 = newserv("us", "us-newserv-live", "live", "v3")
us_alis_bb = newserv("us", "us-newserv-live", "live", "v4")
us_abion_hcbb = newserv("us", "us-newserv-hardcore", "hardcore", "v4")
us_adhoc_psp1 = adhoc("us", "psp1")
us_adhoc_psp2i = adhoc("us", "psp2i")
eu_palma_v2 = newserv("eu", "eu-newserv-live", "live", "v2")
eu_palma_v3 = newserv("eu", "eu-newserv-live", "live", "v3")
eu_palma_bb = newserv("eu", "eu-newserv-live", "live", "v4")
eu_aiedo_hcbb = newserv("eu", "eu-newserv-hardcore", "hardcore", "v4")
eu_adhoc_psp1 = adhoc("eu", "psp1")
eu_adhoc_psp2i = adhoc("eu", "psp2i")
write_data({
"servers": [
{
"label": "US Server",
"players": us_alis_v2 + us_alis_v3 + us_alis_bb + us_abion_hcbb + us_adhoc_psp1 + us_adhoc_psp2i,
"ships": [
{"label": "Alis", "rows": [row("V2", us_alis_v2), row("V3", us_alis_v3), row("BB", us_alis_bb)]},
{"label": "Abion", "rows": [row("HC/BB", us_abion_hcbb)]},
{"label": "AdHoc-US", "rows": [row("PSP1", us_adhoc_psp1), row("PSP2i", us_adhoc_psp2i)]},
],
},
{
"label": "EU Server",
"players": eu_palma_v2 + eu_palma_v3 + eu_palma_bb + eu_aiedo_hcbb + eu_adhoc_psp1 + eu_adhoc_psp2i,
"ships": [
{"label": "Palma", "rows": [row("V2", eu_palma_v2), row("V3", eu_palma_v3), row("BB", eu_palma_bb)]},
{"label": "Aiedo", "rows": [row("HC/BB", eu_aiedo_hcbb)]},
{"label": "AdHoc-EU", "rows": [row("PSP1", eu_adhoc_psp1), row("PSP2i", eu_adhoc_psp2i)]},
],
},
],
})
if __name__ == "__main__":
main()
+1 -1
View File
@@ -65,7 +65,7 @@
return;
}
const top = rows.slice(0, 5);
const top = rows.slice(0, 10);
if (!top.length) {
list.innerHTML = `<li><span class="rank">1.</span><span>—</span></li>`;
+45 -37
View File
@@ -8,8 +8,9 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<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 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="server-status.js?v=home-status-prometheus-2" defer></script>
</head>
<body>
<div class="site-shell">
@@ -52,30 +53,48 @@
<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-row status-parent" role="listitem"><span>US Server</span><span></span></div>
<div class="status-list status-list--regions" role="list" aria-label="Current server player counts" data-server-status="loading">
<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-row status-child" role="listitem"><span>V2</span><span>0 Players</span></div>
<div class="status-row status-child" role="listitem"><span>V3</span><span>0 Players</span></div>
<div class="status-row status-child" role="listitem"><span>BB</span><span>0 Players</span></div>
<div class="status-row status-parent" role="listitem"><span>Abion</span><span></span></div>
<div class="status-row status-child" role="listitem"><span>HC</span><span>0 Players</span></div>
<div class="status-row status-parent" role="listitem"><span>EU Server</span><span></span></div>
<div class="status-row status-parent" role="listitem"><span>Palma</span><span>0 Players</span></div>
<div class="status-row status-child" role="listitem"><span>V2</span><span>0 Players</span></div>
<div class="status-row status-child" role="listitem"><span>V3</span><span>0 Players</span></div>
<div class="status-row status-child" role="listitem"><span>BB</span><span>0 Players</span></div>
<div class="status-row status-parent" role="listitem"><span>Aiedo</span><span></span></div>
<div class="status-row status-child" role="listitem"><span>HC</span><span>0 Players</span></div>
<div class="status-row status-parent" role="listitem"><span>PSP Ship</span><span></span></div>
<div class="status-row status-child" role="listitem"><span>PSP1</span><span>0 Players</span></div>
<div class="status-row status-child" role="listitem"><span>PSP2i</span><span>0 Players</span></div>
<div class="status-region" role="group" aria-label="EU Server player counts">
<div class="status-region-header">
<span>EU Server</span><span>Loading…</span>
</div>
<div class="status-ship">
<div class="status-ship-name">Palma</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">Aiedo</div>
<div class="status-row"><span>HC/BB</span><span></span></div>
</div>
<div class="status-ship">
<div class="status-ship-name">AdHoc-EU</div>
<div class="status-row"><span>PSP1</span><span></span></div>
<div class="status-row"><span>PSP2i</span><span></span></div>
</div>
</div>
</div>
</section>
@@ -90,22 +109,11 @@
<section class="card leaderboard-card" aria-labelledby="hardcore-heading">
<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>
</ol>
<a class="small-link home-leaderboard-more" href="leaderboards.html">more</a>
</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>
</main>
@@ -139,6 +147,6 @@
</footer>
</div>
<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>
</html>
+54
View File
@@ -0,0 +1,54 @@
(() => {
const list = document.querySelector(".status-list");
if (!list) return;
const esc = (value) => String(value ?? "").replace(/[&<>"']/g, (ch) => ({
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
"\"": "&quot;",
"'": "&#39;",
}[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("");
};
const loadStatus = (url) => fetch(url, { cache: "no-store" })
.then((res) => {
if (!res.ok) {
throw new Error(`status ${res.status}`);
}
return res.json();
});
loadStatus(`/server-status.json?ts=${Date.now()}`)
.catch(() => loadStatus("/api/server-status"))
.then((data) => data && render(data))
.catch(() => {});
})();
+57
View File
@@ -1857,4 +1857,61 @@ button.inline-link,
overflow-x: auto;
overflow-y: hidden;
-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;
}
}