Migrate club rename and numeric squad reads

This commit is contained in:
funman300
2026-08-18 17:29:24 +00:00
parent fc55de19fa
commit bf6db98f0d
8 changed files with 645 additions and 136 deletions
+35 -60
View File
@@ -1,47 +1,51 @@
# openfut-utas-host
The first live FIFA 17 **UTAS migration host**. It fronts the client-visible UTAS
port and migrates one route at a time to OpenFUT Core, proxying everything else to
the Python UTAS oracle so the rest of FUT keeps working unchanged.
The FIFA 17 UTAS migration boundary. It accepts the client-visible HTTP surface,
serves migrated routes from Rust/Core plus host-owned durable stores, and proxies only
the unclassified tail to the Python behavioral oracle.
```
FIFA 17 ──HTTP──▶ openfut-utas-host
├── GET …/club ──▶ FIFA17 adapter ──▶ OpenFUT Core (/collection)
└── everything else ──▶ Python UTAS oracle (verbatim reverse proxy)
├── migrated route ──▶ Rust adapter / Core / host stores
└── unclassified tail ──▶ Python UTAS oracle
```
## What it owns / does not own
Owns: socket + HTTP/1.1 keep-alive transport, route classification, the Core
access client, the Python passthrough, and diagnostics. It owns **no** game
domain state — filtering/pagination is Core's; wire parsing/shaping is the
adapter's. The adapter never learns how Core is reached (the architecture rule):
the host holds the [`CoreAccess`] boundary (`GET {core_url}/collection?…` today).
`src/lib.rs::classify` is the route-level source of truth. The current Rust surface
includes club/squad/user reads, club rename, auth/session/client data, Store/economy,
packs, owned-item moves, market/trade-pile, and the observed hub support routes.
## Safety model
- **Classification happens once, before execution.** Exact `GET /ut/game/<title>/club`
→ Rust; everything else → Python. No shared path, no "try Rust then Python".
- A Core failure on `/club` degrades to a valid empty `{"itemData":[]}` and logs
an error — it never falls back to Python (which could double-apply a mutation on
other routes). `/club` is read-only, but the rule is absolute.
- Mutating routes (PUT/POST, `/squad`, `/purchased`, quick-sell, market, auth, SBC,
`/club/stats/*`, `/clubUser`) all classify to passthrough and are untouched.
- Classification happens exactly once before execution. There is no "try Rust then
Python"; a mutation cannot be double-applied.
- A route classified to Rust never falls back to Python on a Core/store/projection
failure. Each handler uses its captured fail-closed or honest-empty wire contract.
- `PUT …/club` and `PUT|POST …/user/club` atomically update the shared account JSON.
Every input returns the required zero-atom `200 {}` response; rejection and
persistence failures remain visible in logs.
- Numeric `GET …/squad/<n>` returns the one Core-backed current squad, matching the
Python oracle's single-current-squad behavior.
- Python remains the behavioral oracle and rollback backend for routes not yet
classified to Rust. New economy behavior belongs in Rust/Core, never Python.
## Configuration (env)
| Var | Required | Default | Meaning |
|---|---|---|---|
| `OPENFUT_UTAS_HOST_ADDR` | yes | — | where this host listens (client-visible UTAS addr) |
| `OPENFUT_UTAS_PYTHON_URL` | yes | — | Python UTAS oracle base URL for fallback (must differ from this host) |
| `OPENFUT_FIFA17_CATALOG` | yes | — | FIFA 17 card-definition identity catalog (`Fifa17CardCatalog` JSON: card id → asset id) |
| `OPENFUT_IDENTITY_STORE` | yes | — | persistent external-identity store file (owned instance → stable wire id) |
| `OPENFUT_PERSONA_ID` | yes | — | FIFA persona id stamped on `GET /squad/active` (must match the persona LSX/Blaze/POW/UTAS agree on) |
| `OPENFUT_UTAS_HOST_ADDR` | yes | — | client-visible host listen address |
| `OPENFUT_UTAS_PYTHON_URL` | yes | — | Python oracle base for the unclassified tail; must differ from this host |
| `OPENFUT_FIFA17_CATALOG` | yes | — | FIFA 17 definition identity catalog |
| `OPENFUT_IDENTITY_STORE` | yes | — | persistent owned-instance wire-id store |
| `OPENFUT_PERSONA_ID` | yes | — | non-zero FIFA persona id shared by LSX/Blaze/POW/UTAS |
| `OPENFUT_MARKET_DB` | yes | — | durable host-owned transfer-market SQLite DB |
| `OPENFUT_PILE_DB` | yes | — | durable host-owned item-pile SQLite DB |
| `OPENFUT_CORE_URL` | no | `http://127.0.0.1:8080` | OpenFUT Core base |
| `OPENFUT_FIFA17_TABLES_DIR` | no | `fifa17-recon/data/tables` | `leagues/nations/teams.json` for id⇄name |
| `OPENFUT_FIFA17_TABLES_DIR` | no | `fifa17-recon/data/tables` | FIFA entity tables |
| `OPENFUT_CLIENTDATA_DB` | no | identity-store sibling `clientdata.json` | durable opaque client-data JSON |
| `OPENFUT_ACCOUNT_PATH` | no | `FUT_ACCOUNT_PATH`, then identity-store sibling `active_account.json` | shared FIFA account/club JSON |
Startup **fails clearly** if the catalog or identity store cannot be loaded —
there is no placeholder fallback (exactly one production identity path).
Startup fails if required identity or durable economy state cannot be opened. No
placeholder production identity source is substituted.
## Identity model (resolved)
@@ -61,39 +65,10 @@ conflated, are resolved by [`Fifa17IdentityResolver`] (the single production pat
within `(fifa17, owned-item)` — no per-account column is needed because Core
owned-instance ids are globally-unique UUIDs.
**Remaining prerequisite for a *rendering* retail `/club`:** Core inventory must
reference cards that exist in the catalog. The catalog + store + resolver are
built and tested; wiring a controlled real FIFA 17 dev-content inventory (the
curated per-game dev pack) is the next slice. `rare=SP` ("Special") stays
UNSUPPORTED (semantics unproven; parsed, reported, never guessed).
## Retail A/B runbook (first `/club` gate)
Change **only** the UTAS routing layer; keep the validated Rust Redirector/Roster
and the current Blaze path. Python remains the rollback oracle — do not modify it.
Preconditions (mirror the proven blaze/roster switch discipline):
1. `cargo test -p openfut-utas-host -p openfut-adapter-fifa17` green; `clippy -D warnings` clean; `fmt --check` clean.
2. Built binary identity == HEAD (`scripts/verify-build-identity.sh`); no dirty tree.
3. Python UTAS directly reachable; the Rust host directly probeable; no stale NAT/switch rules; FIFA fully closed.
Bring-up:
1. Move Python UTAS to an alternate port (`FUT_PORT=8199` in the container/`openfut-fut.sh`); it keeps serving there.
2. Start this host on the client-visible UTAS addr:
`OPENFUT_UTAS_HOST_ADDR=<lan>:8099 OPENFUT_UTAS_PYTHON_URL=http://127.0.0.1:8199 OPENFUT_CORE_URL=http://127.0.0.1:8080 OPENFUT_FIFA17_CATALOG=<catalog.json> OPENFUT_IDENTITY_STORE=<store.json> OPENFUT_PERSONA_ID=33068179 openfut-utas-host`
3. Launch FIFA → FUT → **My Squad** player picker and exercise: no-filter, position, nation, league, league+team, Gold+position, then scroll beyond page one.
Evidence to capture (all six):
- **Switch**: client traffic hits the Rust host.
- **Rust positive**: host log `owner=RUST route=club …` for the client IP.
- **Python negative for /club**: Python logs no `/club` request in the window.
- **Python positive for other UTAS**: unimplemented routes still reach Python.
- **Core positive**: Core logs the `/collection` query and returns the expected set.
- **Application + pagination**: the UI shows filtered results; later pages differ
from page one (no repeated-first-page amplification).
Rollback: point the UTAS addr back at Python directly; confirm FUT still usable;
then re-enable the host and confirm `/club` again (proves reversibility).
Production has a frozen post-P1 baseline and a hot Python rollback. A source change
passing local tests is **not** deployment approval. Build verification, staging, host
restart, and live-client promotion remain operator-gated; the current state and
promotion evidence live in the OpenFUT Obsidian vault.
Logs are safe by construction: no auth/session/device/token material — only owner,
route, filter summary, counts, status.