Add 7.41e patch notes with a hand-authored reading sidebar.

Ship Climperor Web 0.6.13: latest-patch summary panel, icon/innate fallbacks, and layout/scroll fixes; keep shared ability badges in git.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
voson
2026-07-31 10:06:09 +08:00
co-authored by Cursor
parent a257f96d94
commit 30218790ce
18 changed files with 2169 additions and 63 deletions
+84 -18
View File
@@ -32,9 +32,17 @@ ROOT = Path(__file__).resolve().parent
REPO_ROOT = ROOT.parent
sys.path.insert(0, str(REPO_ROOT))
from shared.http_utils import write_json_atomic
from shared.http_utils import http_json, write_json_atomic
from shared.paths import HEROES_JSON, RELATIONS_JSON, WEB_FRONTEND
OPENDOTA_PROBE_URL = "https://api.opendota.com/api/heroStats"
# Must be non-empty before any Pages deploy — empty shells overwrite production.
CRITICAL_BY_HERO_FILES = (
ROOT / "data" / "hero_stats.json",
ROOT / "data" / "stratz_hero_meta.json",
ROOT / "data" / "stratz_matchup_tops.json",
)
# Paths whose content change should trigger deploy / OSS.
DATA_WATCH = [
ROOT / "data" / "hero_stats.json",
@@ -58,6 +66,7 @@ INPUT_WATCH = [
RELATIONS_JSON,
HEROES_JSON,
ROOT / "data" / "hero_grid_order.json",
ROOT / "data" / "patch_summaries.json",
WEB_FRONTEND / "index.html",
WEB_FRONTEND / "app.js",
WEB_FRONTEND / "style.css",
@@ -270,26 +279,82 @@ def run_patch_linked(*, dry_run: bool = False) -> None:
)
def probe_opendota_available() -> bool:
"""Single fail-fast GET; False means skip OpenDota daily fetches this run."""
try:
http_json(OPENDOTA_PROBE_URL, timeout=15, retries=0)
except Exception as exc:
print(
f"OpenDota probe failed ({type(exc).__name__}: {exc}); "
"skipping hero_stats / hero_matches / pro_matches this run",
flush=True,
)
RUN_RESULTS.append(
{
"step": "opendota_probe",
"ok": False,
"skipped_opendota": True,
"error": f"{type(exc).__name__}: {exc}",
}
)
return False
print("OpenDota probe ok", flush=True)
RUN_RESULTS.append({"step": "opendota_probe", "ok": True})
return True
def assert_critical_data_ready() -> None:
"""Refuse deploy when restored/generated caches would publish empty shells."""
problems: list[str] = []
for path in CRITICAL_BY_HERO_FILES:
rel = path.name
if not path.is_file():
problems.append(f"{rel}: missing")
continue
try:
raw = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
problems.append(f"{rel}: unreadable ({exc})")
continue
by_hero = raw.get("by_hero") if isinstance(raw, dict) else None
if not isinstance(by_hero, dict) or not by_hero:
problems.append(f"{rel}: empty by_hero")
if problems:
raise RuntimeError(
"refusing deploy: critical web data incomplete ("
+ "; ".join(problems)
+ ")"
)
def run_daily(*, dry_run: bool = False) -> bool:
"""Return True if patch-linked fetch ran."""
# Soft-fail OpenDota quota burns: keep restored cache and continue the tier.
run_script("fetch_hero_stats.py", dry_run=dry_run, soft_fail=True)
# Probe once: if OpenDota is 429/down, keep restored cache and do not burn quota.
opendota_ok = True if dry_run else probe_opendota_available()
if opendota_ok:
# Soft-fail OpenDota quota burns: keep restored cache and continue the tier.
run_script("fetch_hero_stats.py", dry_run=dry_run, soft_fail=True)
run_script(
"fetch_hero_matches.py",
"--source",
"league",
dry_run=dry_run,
soft_fail=True,
)
run_script(
"fetch_pro_matches.py",
"--include-pubs",
"--refresh-limit",
"15",
dry_run=dry_run,
soft_fail=True,
)
else:
print(
"skipped OpenDota daily fetches (probe failed); keeping prior cache",
flush=True,
)
run_script("fetch_leaderboards.py", dry_run=dry_run)
run_script(
"fetch_hero_matches.py",
"--source",
"league",
dry_run=dry_run,
soft_fail=True,
)
run_script(
"fetch_pro_matches.py",
"--include-pubs",
"--refresh-limit",
"15",
dry_run=dry_run,
soft_fail=True,
)
# Soft-fail Douyin enrichment (script itself exits 0; keep previous values on miss).
run_script("fetch_streamers.py", dry_run=dry_run, soft_fail=True)
# Soft-fail live probe (exits 0; probe failures clear is_live / live_probed_at).
@@ -555,6 +620,7 @@ def main() -> None:
should_deploy = args.force_deploy or data_changed or assets_changed
if should_deploy and not args.skip_deploy:
assert_critical_data_ready()
run_script("deploy_relations.py")
summary["deployed"] = True
elif should_deploy: