diff --git a/backend/app.py b/backend/app.py index b857724..a65a618 100644 --- a/backend/app.py +++ b/backend/app.py @@ -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"]}, + ]}, + ], + }, + ], + }) diff --git a/site/home-leaderboard.js b/site/home-leaderboard.js index c5c5e3f..324d74c 100644 --- a/site/home-leaderboard.js +++ b/site/home-leaderboard.js @@ -65,7 +65,7 @@ return; } - const top = rows.slice(0, 5); + const top = rows.slice(0, 10); if (!top.length) { list.innerHTML = `
  • 1.
  • `; diff --git a/site/index.html b/site/index.html index 935531d..4c1ac1c 100644 --- a/site/index.html +++ b/site/index.html @@ -8,8 +8,9 @@ - + +
    @@ -52,30 +53,48 @@

    Server Status

    -
    -
    US Server
    +
    +
    +
    + US ServerLoading… +
    +
    +
    Alis
    +
    V2
    +
    V3
    +
    BB
    +
    +
    +
    Abion
    +
    HC/BB
    +
    +
    +
    AdHoc-US
    +
    PSP1
    +
    PSP2i
    +
    +
    -
    Alis0 Players
    -
    V20 Players
    -
    V30 Players
    -
    BB0 Players
    - -
    Abion
    -
    HC0 Players
    - -
    EU Server
    - -
    Palma0 Players
    -
    V20 Players
    -
    V30 Players
    -
    BB0 Players
    - -
    Aiedo
    -
    HC0 Players
    - -
    PSP Ship
    -
    PSP10 Players
    -
    PSP2i0 Players
    +
    +
    + EU ServerLoading… +
    +
    +
    Palma
    +
    V2
    +
    V3
    +
    BB
    +
    +
    +
    Aiedo
    +
    HC/BB
    +
    +
    +
    AdHoc-EU
    +
    PSP1
    +
    PSP2i
    +
    +
    @@ -90,22 +109,11 @@

    Hardcore Leaderboard

    -
      +
      1. 1.Loading...
      more
    - -
    -

    C Rank Points

    -
      -
    1. 1.
    2. -
    3. 2.
    4. -
    5. 3.
    6. -
    7. 4.
    8. -
    9. 5.
    10. -
    -
    @@ -139,6 +147,6 @@
    - + diff --git a/site/server-status.js b/site/server-status.js new file mode 100644 index 0000000..e59ea6c --- /dev/null +++ b/site/server-status.js @@ -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) => ` +
    +
    + ${esc(server.label)}${players(server.players)} +
    + ${(server.ships || []).map((ship) => ` +
    + ${esc(ship.label)} +
    + ${(ship.rows || []).map((row) => ` +
    + ${esc(row.label)}${players(row.players)} +
    + `).join("")} + `).join("")} +
    + `).join(""); + }; + + fetch("/api/server-status", { cache: "no-store" }) + .then((res) => res.ok ? res.json() : null) + .then((data) => data && render(data)) + .catch(() => {}); +})(); diff --git a/site/style.css b/site/style.css index 52ede35..d5eb819 100644 --- a/site/style.css +++ b/site/style.css @@ -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; + } +} +