Three defects behind "selecting my own Transfer List listing opens no dialog".
Pressing the card emits NO HTTP at all, so the gate is a field in what we already
return -- the client decides locally from the auction record.
1. OWNERSHIP FIELDS (FIFA17-HISTORICAL). FIFA 17 auctionInfo carries `tradeOwner`
(bool), `sellerId` and `offers`; we emitted none of them. `tradeOwner` is the
purpose-built "this auction is mine" flag, and without it the Transfer List has
nothing to key owner actions (Remove / Re-list) on. `sellerId` now carries the
configured persona so it agrees with `tradeOwner` and `sellerName` instead of
telling three different stories. Persona is threaded from config, never baked in.
2. `GET …/trade/status` ANSWERED EMPTY (CONFIRMED from our own live logs). The
Transfer List polls this continuously to refresh live auction state. The tail has
no numeric id, so it fell through `t.starts_with("trade")` into the buy/view arm,
where `trade_id_from_path` fails and the reply is `{"auctionInfo": []}`. The
client asked for the state of its own listings and was repeatedly told there was
none. Now a real handler: `tradeIds` filter, or the whole active pile unfiltered;
unknown ids are absent rather than an error, so a poll never fails closed.
3. PLAIN `DELETE …/trade/<id>` WAS A SILENT NO-OP. Contemporaneous FIFA 17 clients
cancel via `DELETE /ut/game/<sku>/trade/<id>`; only the oracle's
`/ut/delete/game/…` spelling mapped to MarketCancel, so the plain form landed in
the buy/view arm and "cancelled" nothing while returning 200. Both spellings now
map to MarketCancel. Kept the oracle spelling: the differential exercises it.
Why the differential missed all of this: our record's key set was IDENTICAL to the
oracle's, so parity was green. The oracle omits the ownership fields too, because
its own remove flow was never driven by a real client either. The differential now
asserts we COVER every oracle key and that our extra keys are EXACTLY
{offers, sellerId, tradeOwner} -- so an unexplained new divergence still fails,
while the deliberate superset is pinned.
Deliberately NOT changed (no evidence): itemState stays "listFS", expires stays
3600 seconds-remaining, bidState stays "none" for active/unbid, counts stays
count=1, and no FIFA 18+ price fields were added.
332 tests pass, 0 failed, clippy clean. Deployed and verified live: tradeOwner=true
sellerId=33068179 sellerName='CAGE' offers=0 on /tradePile AND /trade/status
(filtered and unfiltered).
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.
FIFA 17 ──HTTP──▶ openfut-utas-host
├── GET …/club ──▶ FIFA17 adapter ──▶ OpenFUT Core (/collection)
└── everything else ──▶ Python UTAS oracle (verbatim reverse proxy)
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).
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
/clubdegrades to a valid empty{"itemData":[]}and logs an error — it never falls back to Python (which could double-apply a mutation on other routes)./clubis 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.
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_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 |
Startup fails clearly if the catalog or identity store cannot be loaded — there is no placeholder fallback (exactly one production identity path).
Identity model (resolved)
FIFA renders an owned card by resolving resourceId & 0xffffff against the
client's own local players table; an invented id renders a blank generic
card (proven live — fut_cards.py:11-21). Two distinct identities, never
conflated, are resolved by [Fifa17IdentityResolver] (the single production path):
- Definition identity (
resourceId/assetId) — the card's real FIFA asset id, from the versionedOPENFUT_FIFA17_CATALOG. An unmapped definition is dropped and counted, never faked. - Instance identity (
id) — a stable, persistent, reversible wire integer from the genericopenfut-identitystore under the FIFA 17 wire-id policy (monotonic from100_000_001). The same owned instance keeps its id across restart and reverses exactly; two copies of one definition share aresourceIdbut get distinctids. The namespace is globally monotonic 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):
cargo test -p openfut-utas-host -p openfut-adapter-fifa17green;clippy -D warningsclean;fmt --checkclean.- Built binary identity == HEAD (
scripts/verify-build-identity.sh); no dirty tree. - Python UTAS directly reachable; the Rust host directly probeable; no stale NAT/switch rules; FIFA fully closed.
Bring-up:
- Move Python UTAS to an alternate port (
FUT_PORT=8199in the container/openfut-fut.sh); it keeps serving there. - 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 - 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
/clubrequest in the window. - Python positive for other UTAS: unimplemented routes still reach Python.
- Core positive: Core logs the
/collectionquery 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).
Logs are safe by construction: no auth/session/device/token material — only owner, route, filter summary, counts, status.