diff --git a/scripts/sold-staging-up.py b/scripts/sold-staging-up.py index 09ad314..d5d3f71 100755 --- a/scripts/sold-staging-up.py +++ b/scripts/sold-staging-up.py @@ -127,6 +127,9 @@ BLAZE_ASSETS = ("redir_cert.pem", "redir_key.pem") CONTENT_SRC = "/home/alex/openfut-post-p1/staging/emit/content" CARDS_NAME = "fifa17-production-cards.json" CATALOG_NAME = "fifa17-production-catalog.json" +# The FIFA17 content_kind mapping Core needs to correct a club imported before +# the taxonomy existed. Emitted alongside the catalog by openfut-import-fifa17. +RECLASSIFY_NAME = "fifa17-reclassify.json" # The operator's REAL club (the 1986-item CAGE import), as staged by # scripts/club-snapshot.py. That snapshot lives OUTSIDE the production state @@ -1100,6 +1103,49 @@ def migrate_core(lay: Layout) -> None: proc.wait(timeout=20) +def reclassify_core(lay: Layout) -> None: + """Correct `content_kind` on the club restored from the snapshot. + + The snapshot was imported before the content taxonomy existed, so its + coaches, kits and consumables are all recorded as players — and a profile + import is once-only, so re-importing cannot fix it. Core takes the FIFA17 + mapping and applies it in one idempotent transaction. Without this, every + fresh bring-up would silently restore the wrong ownership truth. + """ + request = os.path.join(CONTENT_SRC, RECLASSIFY_NAME) + if not os.path.exists(request): + raise Fatal( + f"missing {request}; regenerate it with " + "`openfut-import-fifa17 --emit-content `" + ) + # A binary predating the subcommand ignores it and boots the SERVER instead, + # which would block the bring-up forever rather than fail. + try: + out = subprocess.run( + [lay.core_bin, "reclassify", request], + env=core_env(lay), cwd=lay.root, capture_output=True, text=True, + timeout=120, + ) + except subprocess.TimeoutExpired: + raise Fatal( + f"{lay.core_bin} did not answer `reclassify` — a binary predating the " + "subcommand ignores it and starts the server instead. Rebuild Core." + ) from None + if out.returncode != 0: + raise Fatal(f"core reclassify failed: {out.stderr.strip()[-400:]}") + result = json.loads(out.stdout) + if result["unmatched_definitions"]: + raise Fatal( + "reclassify named definitions this club owns no copy of: " + f"{result['unmatched_definitions'][:5]}" + ) + ok( + f"reclassified owned content: {result['updated']} row(s) corrected, " + f"{result['unchanged']} already right (Core is the ownership authority, " + "so a coach must not be recorded as a player)" + ) + + def start_core(lay: Layout) -> Launched: proc = spawn([lay.core_bin], core_env(lay), lay.root, lay.core_log, append=True) wait_http(CORE_PORT, "/health", proc, lay.core_log, "staging Core") @@ -1256,6 +1302,25 @@ def verify(lay: Layout, variant: str, real_club_expected: bool) -> None: "squad changed without re-stamping its extension fingerprint" ) + # Core is the ownership authority. If a snapshot restore silently reverted + # every non-player row to `player`, the wire would still look plausible + # (the adapter classifies from its catalog) while Core's truth was wrong. + if real_club_expected: + kinds = {} + for kind in ("player", "manager", "staff", "consumable", "kit"): + status, body = http_get( + CORE_PORT, f"/collection?content_kind={kind}&limit=1" + ) + kinds[kind] = json.loads(body)["total"] if status == 200 else -1 + ok(f"CORE ownership truth by content_kind: {kinds}") + missing = [k for k, v in kinds.items() if v <= 0] + if missing: + raise Fatal( + f"Core owns nothing of kind {missing}; the real club holds " + "players, a manager, coaches, consumables and kits. A zero here " + "means the reclassify step did not run or did not stick" + ) + # Nothing this process ever opened may live under the production state dir. leaked = [] for fd in os.listdir(f"/proc/{os.getpid()}/fd"): @@ -1432,6 +1497,8 @@ def main() -> int: raise Fatal(f"{lay.core_db} should have been installed from the snapshot") migrate_core(lay) seed_core_db(lay, real_club) + if real_club is not None: + reclassify_core(lay) started.append(start_core(lay)) banner("STAGING UTAS-HOST")