Fix player sync empty overwrites and force CSS cache refresh.

Keep D1 recent/heroes/peers/rank when OpenDota is partial; bump site to 0.6.55 and shorten style.css cache so「我」layout is not stuck on stale CSS.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
voson
2026-08-01 01:46:13 +08:00
co-authored by Cursor
parent 41d272ba29
commit cafd0651b1
9 changed files with 102 additions and 20 deletions
+46
View File
@@ -0,0 +1,46 @@
"""Upload web/assets/ui_icons/* to OSS ui-icon/ (no secret echo)."""
from __future__ import annotations
import mimetypes
import os
import sys
from pathlib import Path
import oss2
ROOT = Path(__file__).resolve().parents[1]
SRC = ROOT / "web" / "assets" / "ui_icons"
def main() -> int:
ak = os.environ.get("KEYZOO_ASSET_META_ACCESSKEY_ID") or os.environ.get(
"OSS_ACCESS_KEY_ID"
)
sk = os.environ.get("KEYZOO_ASSET_SECRET_ACCESSKEY_SECRET") or os.environ.get(
"OSS_ACCESS_KEY_SECRET"
)
if not ak or not sk:
print("missing OSS credentials", file=sys.stderr)
return 2
bucket = oss2.Bucket(oss2.Auth(ak, sk), "https://oss-cn-shanghai.aliyuncs.com", "climperor")
n = 0
for f in sorted(SRC.iterdir()):
if not f.is_file() or f.name.startswith("."):
continue
key = f"ui-icon/{f.name}"
ctype = mimetypes.guess_type(f.name)[0] or "application/octet-stream"
bucket.put_object_from_file(
key,
str(f),
headers={"Content-Type": ctype, "Cache-Control": "public, max-age=86400"},
)
n += 1
print(f"uploaded {n} ui-icon files", flush=True)
for name in ("dota2_logo_wordmark.png", "dota2_logo.png"):
print(f" {name} exists={bucket.object_exists('ui-icon/' + name)}", flush=True)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+8 -4
View File
@@ -37,8 +37,8 @@ export async function upsertProfile(db, accountId, profile) {
availability_complete, source, fetched_at, enriched_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(account_id) DO UPDATE SET
rank_tier=excluded.rank_tier,
leaderboard_rank=excluded.leaderboard_rank,
rank_tier=COALESCE(excluded.rank_tier, player_profiles.rank_tier),
leaderboard_rank=COALESCE(excluded.leaderboard_rank, player_profiles.leaderboard_rank),
availability_status=excluded.availability_status,
availability_note=excluded.availability_note,
availability_complete=excluded.availability_complete,
@@ -102,9 +102,11 @@ export async function upsertStats(db, accountId, scope, stats) {
}
export async function replaceHeroes(db, accountId, heroes) {
// Empty OpenDota heroes must not wipe a good cache.
if (!Array.isArray(heroes) || heroes.length === 0) return;
const now = utcNow();
await db.prepare(`DELETE FROM player_heroes WHERE account_id = ?`).bind(accountId).run();
for (const h of heroes || []) {
for (const h of heroes) {
await db
.prepare(
`INSERT INTO player_heroes (
@@ -127,9 +129,11 @@ export async function replaceHeroes(db, accountId, heroes) {
}
export async function replacePeers(db, accountId, peers) {
// Empty OpenDota peers must not wipe a good cache.
if (!Array.isArray(peers) || peers.length === 0) return;
const now = utcNow();
await db.prepare(`DELETE FROM player_peers WHERE account_id = ?`).bind(accountId).run();
for (const p of peers || []) {
for (const p of peers) {
await db
.prepare(
`INSERT INTO player_peers (
+31 -7
View File
@@ -193,9 +193,17 @@ async function syncAccount(env, msg) {
};
}
const existing = await loadPlayerBundle(env.DB, accountId);
const existingRecentN = Array.isArray(existing && existing.recent)
? existing.recent.length
: 0;
const existingR20 =
existing && existing.recent_20 ? Number(existing.recent_20.sample) || 0 : 0;
const odRecentOk = Array.isArray(recentRaw) && recentRaw.length > 0;
const fetched = utcNow();
const availability = mergeAvailability({
opendotaRecentN: Array.isArray(recentRaw) ? recentRaw.length : 0,
opendotaRecentN: odRecentOk ? recentRaw.length : 0,
career,
steamHistoryStatus: steamStatus,
fetchedAt: fetched,
@@ -204,11 +212,13 @@ async function syncAccount(env, msg) {
availability.status = "private";
availability.note = "未公开比赛数据";
}
// Steam public but OpenDota empty → almost always rate-limit; retry queue.
// Cold account + Steam public + empty OpenDota rate-limit; retry queue.
if (
availability.status === "syncing" &&
(!Array.isArray(recentRaw) || recentRaw.length === 0) &&
!career
!odRecentOk &&
!career &&
existingRecentN === 0 &&
!(existing && existing.career && existing.career.games > 0) &&
steamStatus === 1
) {
throw new Error("OpenDota empty while Steam public — retry");
}
@@ -222,14 +232,28 @@ async function syncAccount(env, msg) {
availability,
enriched_at: fetched,
};
// Partial OpenDota (career ok, recent empty): keep prior freshness so /me
// stays soft-stale and do not pretend the row is fully refreshed.
if (!odRecentOk && (existingR20 > 0 || existingRecentN > 0)) {
profile.enriched_at =
(existing && existing.enriched_at) || profile.enriched_at;
if (existing && existing.availability && existing.availability.fetched_at) {
availability.fetched_at = existing.availability.fetched_at;
}
}
await upsertProfile(env.DB, accountId, profile);
if (career) await upsertStats(env.DB, accountId, "career", career);
await upsertStats(env.DB, accountId, "recent20", aggregateFromRows(recent20, 20));
const recentAgg = aggregateFromRows(recent20, 20);
if ((recentAgg.sample || 0) > 0) {
await upsertStats(env.DB, accountId, "recent20", recentAgg);
await upsertMatches(env.DB, accountId, recent20);
} else if (existingR20 <= 0 && existingRecentN <= 0) {
await upsertStats(env.DB, accountId, "recent20", recentAgg);
}
if (activity180) await upsertStats(env.DB, accountId, "recent180", activity180);
await replaceHeroes(env.DB, accountId, topHeroes);
await replacePeers(env.DB, accountId, peerRows);
await upsertMatches(env.DB, accountId, recent20);
// Optional: store a published match detail into R2 (deduped by match_id).
if (msg.kind === "publish_match" && msg.match_id && env.MATCHES) {
+1 -1
View File
@@ -55,7 +55,7 @@ from shared.paths import (
from seo_prerender import DEFAULT_SITE_ORIGIN, write_seo_bundle
from serve_relations import WEB_DIR, build_payload
SITE_VERSION = "0.6.54"
SITE_VERSION = "0.6.55"
DEFAULT_OSS_BASE = "https://climperor.oss-cn-shanghai.aliyuncs.com"
+1 -1
View File
@@ -20,7 +20,7 @@
Cache-Control: public, max-age=60, must-revalidate
/style.css
Cache-Control: public, max-age=300, must-revalidate
Cache-Control: public, max-age=60, must-revalidate
/fonts/*
Cache-Control: public, max-age=31536000, immutable
+1 -1
View File
@@ -1,5 +1,5 @@
/* Local defaults; production export overwrites via export_relations_site.py. */
var SITE_VERSION = "0.6.54";
var SITE_VERSION = "0.6.55";
var SITE_ORIGIN = "";
var ABILITY_VIDEO_BASE = "";
var STATIC_ASSET_BASE = "";
+5 -5
View File
@@ -43,8 +43,8 @@
}
</script>
<link rel="icon" href="/ui-icon/dota2_logo.png" type="image/png" />
<link rel="stylesheet" href="/style.css?v=0.6.54" />
<script src="/mobile-gate.js?v=0.6.54"></script>
<link rel="stylesheet" href="/style.css?v=0.6.55" />
<script src="/mobile-gate.js?v=0.6.55"></script>
</head>
<body>
<h1 class="sr-only">DOTA2 上分帝</h1>
@@ -256,8 +256,8 @@
</div>
<footer class="heroes-site-foot" id="heroes-site-foot" aria-hidden="true"></footer>
<script src="/config.js?v=0.6.54"></script>
<script src="/router.js?v=0.6.54"></script>
<script src="/app.js?v=0.6.54"></script>
<script src="/config.js?v=0.6.55"></script>
<script src="/router.js?v=0.6.55"></script>
<script src="/app.js?v=0.6.55"></script>
</body>
</html>