Files
OpenFUT/fifa17-recon/docker/fifa17-python/tools/fut_admin.py
T
root 70a64e3709 fifa17-python: commit working FUT backend deployment (client/server split)
Freeze the running offline FUT backend into version control as
fifa17-recon/docker/fifa17-python/ - declarative and rebuildable from a
fresh checkout:

* OPENFUT_BIND / OPENFUT_ADVERTISE client/server split in the responders
  (lsx, blaze, roster, utas, pow) + entrypoint.sh; OPENFUT_ADVERTISE is
  required for remote mode (compose and entrypoint fail without it)
* docker-compose.yml reproducing the frozen baseline container exactly
  (env, ports incl. the 8085->8080 POW-content remap, /state bind, restart)
* .env.example / .env for site config - the LAN IP is never hardcoded in source
* tools/ + data/ staged from openfut-fut-backend:python-baseline-2026-08-10,
  verified byte-identical to the running container at freeze time
* client_arm.sh (the 105 client-side arming counterpart)
* Dockerfile bakes /app/SHA256SUMS.txt so any image is self-identifying
* docs/BASELINE-python-2026-08-10.md: frozen image/container/hash record,
  restore instructions and rebuild-equivalence procedure

Secrets (redir key/cert, .env) and runtime state (docker/state) stay gitignored.
The live container is untouched pending the .105 launcher audit.
2026-08-10 23:54:04 +00:00

119 lines
4.8 KiB
Python

#!/usr/bin/env python3
"""OpenFUT save maintenance — inspect and repair fifa17_profile.json offline.
Exists because the pack→club hand-off is not proven. Cards from an opened pack land
in the PENDING pile (`profile["purchased"]`) and only reach the club when the client
sends `PUT ut/%s/item` (FutMoveCard) from the reveal screen's "send to club". Across
every logged session that request has fired **zero** times, while 12 cards sit
pending — so either the flow was never exercised in-game, or the client does not
issue it the way we assume. Same shape as the squad blocker: an assumed client
request that never actually arrives.
Until a live pack-open settles it, this is the manual path.
SAFETY: the client desyncs fatally (logout) if a card exists in BOTH the pending pile
and the club — see docs/CARD_SYSTEM.md and Store.move_items. `--flush-purchased`
therefore MOVES (never copies): each card is removed from `purchased` in the same
transaction that appends it to `items`. Run it with FIFA CLOSED so the client cannot
be holding a stale view of either pile.
Usage:
fut_admin.py --show profile summary (default)
fut_admin.py --flush-purchased move every pending card into the club
fut_admin.py --flush-purchased -n dry run: show what would move
fut_admin.py --backup timestamped copy of the profile
"""
import argparse
import datetime
import json
import os
import shutil
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from fut_store import STORE # noqa: E402
def _fmt(it):
return "asset=%-7s rating=%-3s pos=%-4s id=%s" % (
it.get("assetId"), it.get("rating"), it.get("preferredPosition"), it.get("id"))
def show():
p = STORE.profile()
rec = p.get("record", {})
print("profile : %s" % STORE.path)
print("club : %s (%s) est %s" % (p.get("clubName"), p.get("clubAbbr"),
p.get("established")))
print("coins : %s points: %s" % (p.get("coins"), p.get("points")))
print("record : %s-%s-%s matches: %s"
% (rec.get("won", 0), rec.get("draw", 0), rec.get("loss", 0),
p.get("matchesPlayed", 0)))
print("club items : %d" % len(p.get("items", [])))
print("squads saved : %d" % len(p.get("squads", [])))
print("packs opened : %s" % p.get("packsOpened", 0))
print("listings : %d" % len(p.get("listings", [])))
print("clientdata : %s" % (sorted(p.get("clientdata", {})) or "none"))
pend = p.get("purchased", [])
print("PENDING pack items: %d%s"
% (len(pend), " <-- not in the club; see --flush-purchased" if pend else ""))
for it in pend[:20]:
print(" %s" % _fmt(it))
if len(pend) > 20:
print(" ... and %d more" % (len(pend) - 20))
def backup():
dst = "%s.%s.bak" % (STORE.path,
datetime.datetime.now().strftime("%Y%m%d-%H%M%S"))
shutil.copy2(STORE.path, dst)
print("backup -> %s" % dst)
return dst
def flush(dry_run):
pend = list(STORE.profile().get("purchased", []))
if not pend:
print("nothing pending — the club already has every pack card")
return 0
print("%d pending card(s)%s:" % (len(pend), " (DRY RUN)" if dry_run else ""))
for it in pend:
print(" %s" % _fmt(it))
if dry_run:
print("\ndry run — nothing written. Re-run without -n to move them.")
return 0
backup()
# Reuse the server's own move path so the pending/club invariant is enforced in
# exactly one place: move_items() deletes from `purchased` in the same locked
# transaction that appends to `items`.
moved = STORE.move_items([{"id": it["id"], "pile": "club"} for it in pend])
p = STORE.profile()
print("\nmoved %d card(s) into the club" % len(moved))
print("club items now: %d pending now: %d"
% (len(p.get("items", [])), len(p.get("purchased", []))))
if p.get("purchased"):
print("WARNING: %d card(s) did not move — ids missing from the pending pile"
% len(p["purchased"]))
return 0
def main(argv=None):
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--show", action="store_true", help="profile summary (default)")
ap.add_argument("--flush-purchased", action="store_true",
help="move pending pack cards into the club (run with FIFA closed)")
ap.add_argument("-n", "--dry-run", action="store_true", help="with --flush-purchased")
ap.add_argument("--backup", action="store_true", help="timestamped profile copy")
a = ap.parse_args(argv)
if a.backup:
backup()
if a.flush_purchased:
return flush(a.dry_run)
show()
return 0
if __name__ == "__main__":
sys.exit(main())