Web client for TLE Community.
app api-implementation-status.md
15 kB
Markdown
at main

API Implementation Status #

Last updated: 2026-08-30 · @tlecommunity/client 1.6.0

What this is #

The app consumes @tlecommunity/client v1.6.0 (private Gitea registry). The client's types are generated from @tlecommunity/api-spec by openapi-typescript (dist/lib/types/schema.d.ts), with hand-written convenience wrappers on top (dist/lib/types/*.d.ts, dist/lib/endpoints/*.d.ts).

Earlier versions of this doc catalogued a large parallel type surface under app/interfaces/*, plus inline casts, callWithSession() string-dispatch calls and @ts-expect-error suppressions, each papering over a spot where the shipped client was missing a method, missing fields on a response type, or typing a field wrong. Client 1.6.0 ("Fixes after integrating with game UI") closed almost all of them, and the app has been updated to match — see "Resolved in client 1.6.0" below. What remains open is a short list.

The client ships its own README.md "Spec Discrepancies" list — now 23 numbered items, re-verified against api-spec 1.4.0. Items marked [README #n] below are tracked there for the spec owner; don't re-file them.


Resolved in client 1.6.0 #

Typed wrappers now exist for everything the app used to reach through callWithSession():

  • §1.1 lacuna.blackHoleGenerator.* (view / getActionsFor / generateSingularity / subsidizeCooldown) + types.BlackHoleGenerator.*.
  • §1.2 lacuna.body.getBuildable({ body_id, x, y, tag }) + types.Body.GetBuildable* (pass tag: '' for "no filter" — required by the /v2 layer).
  • §1.3–1.5 lacuna.body.abandon / rename / setColonyNotes + types.Body.*.
  • §1.6 Typed per-module view() on every building whose window reads side-band fields (capitol, development, distributionCenter, embassy, entertainment, foodReserve, intelligence, network19, oreStorage, park, planetaryCommand, spacePort, spaceStationLab, theDillonForge, themePark, wasteExchanger, wasteRecycling, blackHoleGenerator). Each extends the generic { building, status }.
  • §1.7 lacuna.stats.serverOverview() — a typed wrapper over the static server_overview.json (no params, no session, standard { result } | { error } envelope). See "Still open" for the one caveat.

Response-type fixes (fields added / corrected / tightened):

  • §2.1 Building.work ({ seconds_remaining, start, end, searching? }) — searching folded into the shared block, so the archaeology view needs no special type.
  • §2.2 Status.ServerBlock.announcement?: IntBool.
  • §2.3 Embassy.ViewStashResponse.stored + stash: Record<string, number>.
  • §2.4 / 2.5 captured_count / spy_count on the security + police-station paginated lists.
  • §2.6 Map get_star_map (and get_star*) stars carry zone: string and influence: number | null.
  • §2.7 Parliament.GetStarsInJurisdictionResponse.stars[].id.
  • §2.8 Body.BuildingSummary.id — the client splices the map key onto every row (get_buildings / get_buildings_resources / Building.build/upgrade/downgrade/demolish/ repair), so the app no longer patches it in.
  • §2.9 SpacePort.Ship gains combat?, max_occupants?, estimated_travel_time?, details?: { payload?: string[] } — the app now reads the manifest from ship.details.payload.
  • §2.10 schema body_status gains zone?.
  • §2.11 Loose Record<string, unknown> maps tightened (archaeology ore, distributionCenter / trade / transporter resources, network19 feeds).
  • §2.12 Trade.viewMarket / viewMyMarket carry trade_count.
  • §3.1 Empire.CreateResponse is number.
  • §3.2 recycle.seconds_per_resource is number; run-in-progress fields are optional.
  • §3.5 Building.upgrade.cost.* resource keys are optional (time still required).
  • §3.6 Building.{upgrade,downgrade}.reason is [number, string] (schema validator widens to cover the 0 / null cases). [README #4]
  • §3.8 viewIncomingSupplyChains.supply_chains[].from_body is an object on both planetaryCommand and stationCommand; the app's runtime string-vs-object reshape is gone.
  • §3.10 Status.BodyBlock.incoming_*_ships is SpacePort.Ship[].
  • §3.11 Inbox tags is MessageTag[] on list rows and detail alike, and types.Inbox.MessageTag is a named export.
  • §3.13 Intelligence spy assignment / possible_assignments[].task use the 26-member types.Intelligence.Assignment union.
  • §3.14 Trade.MarketTrade row is { id, date_offered, ask, offer: string[], empire?, body? }. [README #11-adjacent]
  • §3.15 Named type re-exports added (types.ServerStatus / EmpireStatus / BodyStatus / PlanetStatus / BuildingSchema / ServerOverview), so consumers no longer reach into schema. (The generated schema empire_status still has example-value garbage baked in — the RPC stores keep using types.Status.EmpireBlock / BodyBlock.)

"Do now" cleanups from the old §4 also landed: app/interfaces re-exports types.ServerDate / EmpireName / IntBool; the unused Buildings{Upgrade,Downgrade,Demolish}{Params,Response} and the EmpireCreateResponse / StarMapStar / MessageTag / per-building *ViewResponse local types are deleted (the survivors in app/interfaces/building.ts are thin aliases onto the client types). app/lacuna.ts uses the exported CAPTCHA_ERROR_CODE / SESSION_EXPIRED_ERROR_CODE constants instead of the literals.


Still open #

network19 restrict_coverage — client types 1 | 0, server has sent "1" [README #3 family] #

types.Network19.ViewResponse.restrict_coverage is 1 | 0, but the app has seen the live server send the string "1". app/components/network19/coverageTab.tsx keeps a util.int(value) === 1 guard and a widened IntBool | string parameter. Server should send IntBool consistently.

Stringified numeric response fields [README #1 family] #

The response-side mirror of README #1. The client leans on util.fixNumbers + api-spec 1.4.0's id-to-integer normalisation, but some fields still arrive as JSON strings:

  • Status.BodyBlock — x, y, orbit, size, star_id, building_count, plots_available, num_incoming_own/ally/enemy are typed number; app/stores/rpc/body.ts runs int() over every one on ingest. Same pattern very likely affects EmpireBlock.
  • Observatory.GetProbedStarsResponse.stars[].id — typed number; app/components/observatory/probesTab.tsx still coerces with util.int(star.id) before abandon_probe.
  • Embassy alliance ids — id / leader_id / members[].empire_id are number on the client; verify the live server actually sends numbers here (the app now compares them as numbers).

A spec-wide decision is still needed: coerce server-side, or type the affected fields number | string and coerce in the client pipeline.

Body.BuildingResources optionality is asymmetric #

ore_hour? / ore_capacity? / food? / ore? are optional while the other resource-rate fields are required (dist/lib/types/body.d.ts). Unchanged in 1.6.0. app/components/resources/foodOreTab.tsx guards the ore_* reads specifically. Confirm which rate fields are genuinely conditional and make the optionality consistent.

Spy-training buildings' spies block is untyped #

1.6.0 removed the client's SpyTrainingBuilding type and left a TODO: the four spy-training buildings (inteltraining, …) answer view with spies: { max_points, points_per, in_training }, a different shape from the Intelligence Ministry's. app/interfaces/building.ts keeps a local SpyTrainingBuilding (types.Building.ViewResponse & { spies?: {...} }) for it.

types.BlackHoleGenerator.BhgTarget is narrower than the app needs #

The client union models only body/star-by-name and body/star-by-id and { x, y }, with ids and coordinates typed number. The BHG target picker also aims by zone and by orbit, and feeds raw string inputs. app/interfaces/building.ts keeps a hand-written BhgTarget superset and app/queries/blackHoleGenerator.ts widens it to the client shape with one as unknown as at the call boundary. Also: generateSingularity's params type has no subsidize field, though the app (and the legacy client) send subsidize: 1; it is passed as a structural extra.

Trade.viewSupplyChains rows are missing body #

The server's SupplyChain::get_status sends each chain's destination as body: { id, name, x, y, image }, but types.Trade.ViewSupplyChainsResponse['supply_chains'] omits it. app/queries/trade.ts widens the row type (SupplyChain) so the supply chains tab can show the destination. Add body to the spec's view_supply_chains row to retire it.

stats.serverOverview — generated type is too loose #

types.Stats.ServerOverview comes straight from the OpenAPI schema, where every field is optional and several nested shapes don't match what the "General" stats sub-tabs read. app/queries/stats.ts routes through the typed serverOverview() wrapper but narrows the result (as unknown as ServerOverview) to the hand-written model in app/interfaces/stats.ts, which stays. Tighten the spec's server_overview schema (required fields, concrete types maps) to retire the local model.


Remaining escape hatches #

Everything the app still does outside the client's types, after the 1.6.0 adoption:

Site What Why it stays
app/queries/building.ts:30 select: (data) => data as TBuilding on useBuildingViewQuery Inherent to the url-keyed buildingFromUrl(url).view() dispatch — the generic call can't know the per-module side-band shape. A typed buildingFromUrl<T>() overload would remove it.
app/queries/blackHoleGenerator.ts:16 target as unknown as types.BlackHoleGenerator.BhgTarget BhgTarget too narrow — see "Still open".
app/queries/stats.ts:68 as unknown as ServerOverview server_overview schema too loose — see "Still open".
app/queries/trade.ts SupplyChain Row type widened with body viewSupplyChains rows lack the destination — see "Still open".
app/queries/empire.ts boostRequests: Record<BoostType, (p) => Promise<any>> Replace with the typed lacuna.empire.boost* union once confirmed identical (README #8 says they are).
app/queries/parliament.ts / app/components/parliament/proposalTypes.ts useProposeMutation((request: () => Promise<unknown>) => request()) The ~21 lacuna.parliament.propose* responses are all typed unknown.
app/queries/subspaceSupplyDepot.ts (request: () => Promise<unknown>) => request() Type the transmit / complete responses.
app/queries/spacePort.ts:89,91 target ?? ({} as Target) / target as Target Let getFleetFor / its key accept `Target
app/components/miningMinistry/platformsTab.tsx platform as unknown as Record<string, unknown> Type viewPlatforms rows.
app/queries/changes.ts raw fetch('changes.txt') Intentional — plain-text asset, stays out of the client.
app/queries/intelligence.ts ASSIGNMENT_TASKS 26-literal fallback array Kept as runtime data for the rare spy whose possible_assignments the server omits; typed against types.Intelligence.Assignment.

Notes / non-issues #

  • No LacunaError type in the client. app/queries/unwrap.ts throws its own; unwrap.ts and ServerError.data are both any.
  • docs/ carries no response schemas — historical (Reflux/Gulp-era) only.
  • Endpoint-union local types (SecurityEndpoint, MarketEndpoint / MarketListEndpoint, CommandEndpoint, RecyclingEndpoint, …) are legitimate — they express "these modules share a method surface" and don't duplicate anything. Keep.
  • Behavioural, not type gaps: app/game.tsx:127 bootstraps status by calling the client directly (not through unwrap); app/components/inbox/messageBody.tsx:33 calls lacuna.parliament.castVote inside a component rather than via a app/queries/* hook.