Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 88fee8a7d1 | |||
|
d10054477f
|
|||
| 74ab464497 | |||
|
a4a7bb265d
|
|||
| 572ca5c2b2 | |||
|
acadbbe5d2
|
|||
|
429228bf18
|
|||
| 8dc372b8a9 | |||
|
90fefa30d3
|
|||
| 8685303673 |
+118
-14
@@ -445,13 +445,13 @@ def bb_sync_info(account_id):
|
|||||||
regions = {
|
regions = {
|
||||||
"us": {
|
"us": {
|
||||||
"host": "psopeeps_us",
|
"host": "psopeeps_us",
|
||||||
"targets": ["us-live", "us-test", "us-hardcore"],
|
"targets": ["us-live", "us-hardcore"],
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
"applied_at": None,
|
"applied_at": None,
|
||||||
},
|
},
|
||||||
"eu": {
|
"eu": {
|
||||||
"host": "psopeeps_eu",
|
"host": "psopeeps_eu",
|
||||||
"targets": ["eu-live", "eu-test", "eu-hardcore"],
|
"targets": ["eu-live", "eu-hardcore"],
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
"applied_at": None,
|
"applied_at": None,
|
||||||
},
|
},
|
||||||
@@ -490,6 +490,11 @@ def bb_sync_info(account_id):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def account_sync_current_on_all_regions(account_id):
|
||||||
|
sync = bb_sync_info(account_id)
|
||||||
|
return sync.get("status") == "current", sync
|
||||||
|
|
||||||
|
|
||||||
def bb_payload(account_id, username):
|
def bb_payload(account_id, username):
|
||||||
sync = bb_sync_info(account_id)
|
sync = bb_sync_info(account_id)
|
||||||
return {
|
return {
|
||||||
@@ -1050,8 +1055,16 @@ def local_syncer_save_summary(account_id):
|
|||||||
applied_dir = root / "state" / "applied"
|
applied_dir = root / "state" / "applied"
|
||||||
|
|
||||||
paths = {
|
paths = {
|
||||||
"us": applied_dir / f"psopeeps_us.site.{account}.json",
|
"us": [
|
||||||
"eu": applied_dir / f"psopeeps_eu.site.{account}.json",
|
applied_dir / f"psopeeps_us.us-live.site.{account}.json",
|
||||||
|
applied_dir / f"psopeeps_us.us-test.site.{account}.json",
|
||||||
|
applied_dir / f"psopeeps_us.us-hardcore.site.{account}.json",
|
||||||
|
],
|
||||||
|
"eu": [
|
||||||
|
applied_dir / f"psopeeps_eu.eu-live.site.{account}.json",
|
||||||
|
applied_dir / f"psopeeps_eu.eu-test.site.{account}.json",
|
||||||
|
applied_dir / f"psopeeps_eu.eu-hardcore.site.{account}.json",
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
def parse_time(value):
|
def parse_time(value):
|
||||||
@@ -1074,19 +1087,36 @@ def local_syncer_save_summary(account_id):
|
|||||||
"manifest_sha256": None,
|
"manifest_sha256": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
if path.exists():
|
states = []
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
for path in paths[region]:
|
||||||
|
if not path.exists():
|
||||||
|
continue
|
||||||
try:
|
try:
|
||||||
data = json.loads(path.read_text())
|
data = json.loads(path.read_text())
|
||||||
info.update({
|
data["_path"] = str(path)
|
||||||
"status": "seen",
|
states.append(data)
|
||||||
"label": "Seen",
|
|
||||||
"style": "warn",
|
|
||||||
"host": data.get("host"),
|
|
||||||
"applied_at": data.get("applied_at"),
|
|
||||||
"manifest_sha256": data.get("manifest_sha256"),
|
|
||||||
})
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
info["error"] = str(e)
|
errors.append(f"{path.name}: {e}")
|
||||||
|
|
||||||
|
if states:
|
||||||
|
latest_state = max(states, key=lambda x: str(x.get("applied_at") or ""))
|
||||||
|
hashes = {x.get("manifest_sha256") for x in states if x.get("manifest_sha256")}
|
||||||
|
info.update({
|
||||||
|
"status": "seen",
|
||||||
|
"label": "Seen",
|
||||||
|
"style": "warn",
|
||||||
|
"host": latest_state.get("host"),
|
||||||
|
"applied_at": latest_state.get("applied_at"),
|
||||||
|
"manifest_sha256": latest_state.get("manifest_sha256"),
|
||||||
|
"targets_seen": len(states),
|
||||||
|
"targets_expected": len(paths[region]),
|
||||||
|
"all_targets_same_hash": len(hashes) == 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
info["errors"] = errors
|
||||||
|
|
||||||
regions[region] = info
|
regions[region] = info
|
||||||
|
|
||||||
@@ -1697,6 +1727,7 @@ def _hc_merge_character_rows(rows):
|
|||||||
merged["total_enemies_killed"] = total_kills
|
merged["total_enemies_killed"] = total_kills
|
||||||
merged["alive"] = alive
|
merged["alive"] = alive
|
||||||
merged["dead_at"] = dead_at
|
merged["dead_at"] = dead_at
|
||||||
|
_hc_apply_canonical_death_overlay(merged)
|
||||||
merged["last_seen_at"] = last_seen_at or None
|
merged["last_seen_at"] = last_seen_at or None
|
||||||
merged["updated_at"] = updated_at or None
|
merged["updated_at"] = updated_at or None
|
||||||
|
|
||||||
@@ -1704,6 +1735,79 @@ def _hc_merge_character_rows(rows):
|
|||||||
|
|
||||||
return combined
|
return combined
|
||||||
|
|
||||||
|
|
||||||
|
def _hc_death_overlay_int(value):
|
||||||
|
try:
|
||||||
|
if value is None or value == "":
|
||||||
|
return None
|
||||||
|
return int(value)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _hc_canonical_accounts_root():
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
return Path(os.environ.get(
|
||||||
|
"PSOPEEPS_HC_CANONICAL_ACCOUNTS_ROOT",
|
||||||
|
"/home/rbatty/.local/share/psopeeps_account_sync/canonical/accounts",
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
def _hc_canonical_slot_is_dead(guild_card, character_slot):
|
||||||
|
import json
|
||||||
|
|
||||||
|
account_id = _hc_death_overlay_int(guild_card)
|
||||||
|
slot = _hc_death_overlay_int(character_slot)
|
||||||
|
if account_id is None or slot is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
players_dir = _hc_canonical_accounts_root() / f"{account_id:010d}" / "system" / "players"
|
||||||
|
if not players_dir.exists():
|
||||||
|
return False
|
||||||
|
|
||||||
|
if any(players_dir.glob(f"player_*_{slot}.psochar.hardcore-dead")):
|
||||||
|
return True
|
||||||
|
|
||||||
|
deaths_log = players_dir / "hardcore-deaths.jsonl"
|
||||||
|
if not deaths_log.exists():
|
||||||
|
return False
|
||||||
|
|
||||||
|
suffix = f"_{slot}.psochar"
|
||||||
|
try:
|
||||||
|
lines = deaths_log.read_text(errors="replace").splitlines()
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
entry = json.loads(line)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if _hc_death_overlay_int(entry.get("account_id")) != account_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if str(entry.get("character_file") or "").endswith(suffix):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _hc_apply_canonical_death_overlay(row):
|
||||||
|
if _hc_canonical_slot_is_dead(
|
||||||
|
row.get("guild_card") or row.get("account_id"),
|
||||||
|
row.get("character_slot"),
|
||||||
|
):
|
||||||
|
row["alive"] = False
|
||||||
|
row.setdefault("dead_at", "canonical-hardcore-dead")
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
def _hc_combined_payload():
|
def _hc_combined_payload():
|
||||||
source_rows, errors = _hc_get_source_characters()
|
source_rows, errors = _hc_get_source_characters()
|
||||||
combined = _hc_merge_character_rows(source_rows)
|
combined = _hc_merge_character_rows(source_rows)
|
||||||
|
|||||||
+25
-13
@@ -32,13 +32,9 @@
|
|||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Account Dashboard</p>
|
<p class="eyebrow">Account Dashboard</p>
|
||||||
<h1 id="account-title">chuudoku</h1>
|
<h1 id="account-title">chuudoku</h1>
|
||||||
<p>
|
|
||||||
Manage your Blue Burst login and the serial/access keys you use for DC V2, PC V2, and GC V3.
|
|
||||||
Linked saves are mirrored between US and EU automatically.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="status-badges" aria-label="Account setup status">
|
<div class="status-badges" aria-label="Account setup status">
|
||||||
<span class="badge badge--ok">BB account ready</span>
|
<span class="badge badge--ok">Account ready</span>
|
||||||
<span class="badge badge--ok">Saves synced</span>
|
<span class="badge badge--ok">Saves synced</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -54,12 +50,25 @@
|
|||||||
|
|
||||||
<section class="dashboard-grid dashboard-grid--setup">
|
<section class="dashboard-grid dashboard-grid--setup">
|
||||||
<section class="card setup-card setup-card--bb" aria-labelledby="bb-heading">
|
<section class="card setup-card setup-card--bb" aria-labelledby="bb-heading">
|
||||||
<h2 id="bb-heading" class="section-title">Blue Burst Account</h2>
|
<h2 id="bb-heading" class="section-title">Blue Burst</h2>
|
||||||
<dl class="account-summary account-summary--large">
|
<p>BB username <strong>chuudoku</strong><br>BB account ID <strong>0126326509</strong></p>
|
||||||
<div><dt>BB username</dt><dd>chuudoku</dd></div>
|
|
||||||
<div><dt>BB account ID</dt><dd>0126326509</dd></div>
|
<form class="bb-account-form" data-bb-action="change-password">
|
||||||
</dl>
|
<p class="muted">Change your Blue Burst login password. This updates the account file, then it needs to sync to the ships.</p>
|
||||||
<p class="fine-print">Blue Burst is limited to one account per website account. Password reset can come later.</p>
|
|
||||||
|
<label>
|
||||||
|
New BB password
|
||||||
|
<input name="password" type="password" autocomplete="new-password" maxlength="16" required>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Confirm new BB password
|
||||||
|
<input name="confirm_password" type="password" autocomplete="new-password" maxlength="16" required>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button class="button" type="submit">Change Blue Burst Password</button>
|
||||||
|
<div class="bb-message" role="status"></div>
|
||||||
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="card setup-card setup-card--key-sync" aria-labelledby="key-sync-heading">
|
<section class="card setup-card setup-card--key-sync" aria-labelledby="key-sync-heading">
|
||||||
@@ -88,10 +97,13 @@
|
|||||||
<input id="key-label" name="key-label" type="text" placeholder="Dreamcast US disc, GameCube JP, etc.">
|
<input id="key-label" name="key-label" type="text" placeholder="Dreamcast US disc, GameCube JP, etc.">
|
||||||
|
|
||||||
<label for="key-serial">Serial Number</label>
|
<label for="key-serial">Serial Number</label>
|
||||||
<input id="key-serial" name="key-serial" type="text" inputmode="numeric">
|
<input id="key-serial" name="key-serial" type="text" inputmode="numeric" placeholder="DC V2 serial number">
|
||||||
|
|
||||||
|
<label for="key-display-serial">Confirm Serial Number</label>
|
||||||
|
<input id="key-display-serial" name="display_serial" autocomplete="off" type="text" inputmode="numeric" placeholder="confirm serial number">
|
||||||
|
|
||||||
<label for="key-access">Access Key</label>
|
<label for="key-access">Access Key</label>
|
||||||
<input id="key-access" name="key-access" type="text">
|
<input id="key-access" name="key-access" type="text" placeholder="access key">
|
||||||
<button type="button">Register Key Profile</button>
|
<button type="button">Register Key Profile</button>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
+8
-2
@@ -388,7 +388,10 @@
|
|||||||
if (!hero || !title) return;
|
if (!hero || !title) return;
|
||||||
|
|
||||||
for (const p of Array.from(hero.querySelectorAll("p"))) {
|
for (const p of Array.from(hero.querySelectorAll("p"))) {
|
||||||
if (p.textContent.includes("Manage your Blue Burst login")) {
|
if (
|
||||||
|
p.textContent.includes("Manage your Blue Burst login") ||
|
||||||
|
p.classList.contains("account-email-line")
|
||||||
|
) {
|
||||||
p.remove();
|
p.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -575,7 +578,10 @@
|
|||||||
|
|
||||||
renderAccountEmail(accountData);
|
renderAccountEmail(accountData);
|
||||||
updateAccountStatusBadges(accountData);
|
updateAccountStatusBadges(accountData);
|
||||||
renderBBCard(accountData);
|
|
||||||
|
// Account dashboard BB card is server-rendered.
|
||||||
|
// Do not let the generic app bootstrap rewrite it into a stale layout.
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function boot() {
|
async function boot() {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<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=leaderboard-table-restore-20260610-1">
|
<link rel="stylesheet" href="style.css?v=leaderboard-level-column-20260613-1">
|
||||||
<script src="app.js?v=saves-synced-20260609-2" defer></script>
|
<script src="app.js?v=saves-synced-20260609-2" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -83,6 +83,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
<script src="placeholder-pages.js?v=hardcore-leaderboard-table-restore-20260610-1" defer></script>
|
<script src="placeholder-pages.js?v=hardcore-leaderboard-level-column-20260613-1" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
{ key: "rank", label: "Rank", numeric: true },
|
{ key: "rank", label: "Rank", numeric: true },
|
||||||
{ key: "name", label: "Player Name" },
|
{ key: "name", label: "Player Name" },
|
||||||
{ key: "points", label: "Points", numeric: true },
|
{ key: "points", label: "Points", numeric: true },
|
||||||
|
{ key: "level", label: "Level", numeric: true },
|
||||||
{ key: "status", label: "Status" },
|
{ key: "status", label: "Status" },
|
||||||
{ key: "class", label: "Class" },
|
{ key: "class", label: "Class" },
|
||||||
{ key: "secid", label: "SecID" },
|
{ key: "secid", label: "SecID" },
|
||||||
@@ -61,6 +62,7 @@
|
|||||||
originalRank: index + 1,
|
originalRank: index + 1,
|
||||||
name: row.PlayerName || row.CharacterName || row.character_name || "",
|
name: row.PlayerName || row.CharacterName || row.character_name || "",
|
||||||
points: Number(row.Points ?? row.TotalPoints ?? 0),
|
points: Number(row.Points ?? row.TotalPoints ?? 0),
|
||||||
|
level: Number(row.Level ?? row.level ?? 0),
|
||||||
class: row.Class || row.character_class || "",
|
class: row.Class || row.character_class || "",
|
||||||
secid: row.SecID || row.section_id || "",
|
secid: row.SecID || row.section_id || "",
|
||||||
kills: Number(row.Kills ?? row.TotalKills ?? row.total_enemies_killed ?? 0),
|
kills: Number(row.Kills ?? row.TotalKills ?? row.total_enemies_killed ?? 0),
|
||||||
@@ -168,6 +170,7 @@
|
|||||||
<td data-label="Rank">${rank}</td>
|
<td data-label="Rank">${rank}</td>
|
||||||
<td data-label="Player Name">${escapeHtml(row.name)}</td>
|
<td data-label="Player Name">${escapeHtml(row.name)}</td>
|
||||||
<td data-label="Points">${fmtNumber(row.points)}</td>
|
<td data-label="Points">${fmtNumber(row.points)}</td>
|
||||||
|
<td data-label="Level">${fmtNumber(row.level)}</td>
|
||||||
<td data-label="Status">${escapeHtml(row.status)}</td>
|
<td data-label="Status">${escapeHtml(row.status)}</td>
|
||||||
<td data-label="Class">${escapeHtml(row.class || "—")}</td>
|
<td data-label="Class">${escapeHtml(row.class || "—")}</td>
|
||||||
<td data-label="SecID">${escapeHtml(row.secid || "—")}</td>
|
<td data-label="SecID">${escapeHtml(row.secid || "—")}</td>
|
||||||
@@ -180,7 +183,7 @@
|
|||||||
<div class="leaderboard-table-wrap">
|
<div class="leaderboard-table-wrap">
|
||||||
<table class="leaderboard-table">
|
<table class="leaderboard-table">
|
||||||
<thead><tr>${head}</tr></thead>
|
<thead><tr>${head}</tr></thead>
|
||||||
<tbody>${body || `<tr><td colspan="8">No Hardcore leaderboard rows yet.</td></tr>`}</tbody>
|
<tbody>${body || `<tr><td colspan="9">No Hardcore leaderboard rows yet.</td></tr>`}</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="leaderboard-pager">
|
<div class="leaderboard-pager">
|
||||||
|
|||||||
+4
-2
@@ -2084,8 +2084,9 @@ button.inline-link,
|
|||||||
|
|
||||||
.leaderboard-table td:nth-child(1),
|
.leaderboard-table td:nth-child(1),
|
||||||
.leaderboard-table td:nth-child(3),
|
.leaderboard-table td:nth-child(3),
|
||||||
.leaderboard-table td:nth-child(6),
|
.leaderboard-table td:nth-child(4),
|
||||||
.leaderboard-table td:nth-child(7) {
|
.leaderboard-table td:nth-child(7),
|
||||||
|
.leaderboard-table td:nth-child(8) {
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2153,6 +2154,7 @@ button.inline-link,
|
|||||||
|
|
||||||
.leaderboard-table td:nth-child(1),
|
.leaderboard-table td:nth-child(1),
|
||||||
.leaderboard-table td:nth-child(3),
|
.leaderboard-table td:nth-child(3),
|
||||||
|
.leaderboard-table td:nth-child(4),
|
||||||
.leaderboard-table td:nth-child(7),
|
.leaderboard-table td:nth-child(7),
|
||||||
.leaderboard-table td:nth-child(8) {
|
.leaderboard-table td:nth-child(8) {
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
|
|||||||
Reference in New Issue
Block a user