Four defects found by running the suites and the staging lifecycle end to end
after the kit milestone.
1. club-stats kits were half-implemented. The global `kits` counter was real
but `kitsHome`/`kitsAway` and every per-team `kits` bucket stayed hardcoded
0, so the same screen reported two owned kits and zero home/away kits.
`kits` is a total with a family split, exactly like players/playersGold and
staff/staffManager. The split key is `fcc_kitcards.assetid`: 14 is the home
family and 15 the away family, verified across all 1482 rows of the kit
table (assetid 14 covers exactly the 63xxxxx carddbids, 828 rows; assetid 15
exactly the 64xxxxx ones, 654 rows; no exceptions either way).
ClubStatInput now carries `asset_id`, and a kit buckets onto the team that
wears it -- including a team the club owns no player from, the normal case
for a kit won from a pack. The host reads both from the catalog through new
NON-MINTING accessors: `resolve`/`resolve_kit` allocate a wire id, which a
read-only stats query must never do as a side effect.
2. host_test.rs had 10 tests red since the squad-manager work (25f4ad1 /
d37a9d5); 56bd9dd updated the squad_projection integration test and stopped
there. `put_body` hardcoded the captured manager ref 100000427 into EVERY
save, including tests with no manager fixture, so each one was refused with
`unresolved_wire_ids` -- the tests were reporting a real invariant against a
fixture that could not satisfy it. The manager is now an explicit
`Option<i64>` per test, and FakeCore models Core's manager persistence
instead of inheriting the "not implemented" default that 502'd every save.
Added the coverage whose absence let this rot: a manager assignment
round-trips as a Core owned id, a later save without one CLEARS it, and an
unowned manager ref refuses the whole save with nothing committed.
3. `club_route_maps_query_and_shapes_core_items` pinned `offset`/`limit`
forwarding to Core, which the kit commit deliberately replaced with
host-side pagination. It only ever passed because FakeCore ignored the
window -- against a real Core, `start=10` over a one-item club was always an
empty page. Retargeted to the real contract (Core gets semantic filters and
NO window) plus a new test that the window is applied locally after
filtering, which the old fake made vacuous.
4. The staging lifecycle scripts identified production by hardcoded pids, so a
correct teardown FATAL'd: production moved into containers and pids
3631953/3374264 died with a container restart days ago. A pinned pid rots
into the worst of both worlds -- a kill-refusal gate that no longer names
any real production process, and a liveness gate that fails a healthy
teardown. New shared `scripts/openfut_production.py` resolves production
pids AND published ports from the container runtime at the moment they are
needed, refuses to signal anything it cannot see, and proves production is
the same processes serving the same ports before and after. Both lifecycle
scripts use it, which also closed a real gap: port 8085 is published by
openfut-fut-backend but was missing from the up script's forbidden list, so
staging could have bound a production port.
Also fixes the economy differential, red because `complete_match` unlocks
achievements in the same transaction that pays the match reward -- a deliberate
Core feature the Python oracle has no counterpart for. `rust WIN +400` asserted
that progression did not exist; it now asserts the delta is the 400 match reward
plus exactly the achievements the match unlocked, read from Core's own report.
Closes the server side of the FUT kit selector. Ownership stays generic in
Core (submodule bump: club_kit_assignments + GET/PUT /club/kits); this
commit adds the FIFA17 representation, the host projection and importer
support.
adapter:
* ContentKind::Kit ("kit") so kits are classified alongside player/staff/
consumable instead of being mistaken for 0-rated players.
* Fifa17CardIdentity carries card_asset_id and team_id; RawCard keeps both
optional because the emitted catalog writes null for non-kit definitions.
* shape_kit_item emits only the fields the client's kit path reads
(id/resourceId/assetId/cardassetid/cardsubtypeid/itemState/owners/
untradeable/teamid) — no attributeList, no itemType.
* itemState on the wire is the STRING token activeHomeKit/activeAwayKit;
the 101/102 integers are the client's post-deserialisation runtime enum
(item+0x5c) and are never emitted.
* club_stats S_KITS (0x28) now counts owned kits instead of a hard zero.
host:
* CoreKitAssignments + CoreAccess::get_active_kits (GET /club/kits),
defaulting to no active kits so a Core without the endpoint degrades
instead of fabricating a designation.
* handle_club classifies type=player|kit, rejects any other type with an
empty page and outcome=unsupported_type, and now always fetches
unpaginated from Core: kind and transfer-pile membership are host-side
concepts Core cannot express, so filtering and pagination must both
happen after shaping or pages come back short.
importer:
* ItemClass::Kit (cardsubtypeid == 9 and resourceId in 6_300_000..=6_400_654),
kit counts/balances, and card_asset_id/team_id carried into the emitted
catalog and manifest.
* a kit group missing cardassetid == 35 or teamid is DEFERRED
(missing_kit_render_metadata) rather than defaulted; conflicting render
metadata across instances defers as render_metadata_conflict.
staging: sold-staging-up.py seeds two owned kits (6300006 home / 6400003
away, team 21) plus both active designations so the projection can be
verified over HTTP before involving the client.
Host/adapter (deployed to prod-host):
- POST /ut/auth (+/ut/delete/auth): Rust mints sid, opens Rust session, adopts
persona from body; POST /openfut/account/sync full Rust envelope.
- GET /userMassInfo: full Rust (was proxy+overlay), shared build_user_mass_info.
- GET/PUT /clientdata/<key>: new clientdata_store.rs (JSON-persisted).
- GET /club/stats/{country,league,team}: context-aware club_stats_body
(nation/league/team buckets).
- GET /store,/match/keepalive,/captcha,/tfa,/livemessage,/activeMessage: StaticAck.
- GET /watchList, /squad/0, /user: Rust handlers.
- host_test.rs updated for the new routing.
Launcher: bump gitlink to c277213 (shareholder-grade redesign + live account panel).
Docs: PRODUCTION_AUTHORITY_MATRIX, PYTHON_RETIREMENT_PLAN, MATCH_LIFECYCLE, and
route-shapes-2026-08-17 reference fixtures for the still-Python tail.
Migrate the MY CLUB stat set from the Python proxy to a Rust handler computing
Core-accurate counts: player tiers + rare from the collection, staff/consumable
families from catalog kind+subtype, per-nation buckets via the reverse entity
resolver. Faithful port of fut_club_stats.py (VOCAB + global_counts +
context_rows). Unlike the oracle (stale profile + synthetic consumable shelf),
this reflects the real imported content (incl. the content-gap consumables/staff).
Fail-closed 503 on Core error. club/stats/country|league|team sub-screens remain
Python (documented). Adds adapter club_stats module (5 tests), host handler +
classify arm + resolver subtype_of/rareflag_of, ownership + integration tests;
reachability tool splits club/stats global(migrated) vs context(residual).