feat(ui): T-1133 data browser — implant/browser app, six index+detail screens (D-254 SS4)

New implant app (app_path implant/browser, key B, fullscreen), sibling
to the Atlas per Jeroen's IA ruling: kind picker -> generic filterable
index (live search-mode typing) -> generic detail, parameterized per
kind, composed entirely from D-169 components. available_in_companion
left unset (default true) — the app appears in the companion shell
automatically via the generic-host seam, zero companion-side wiring.

browser_adapter.gd is the sole home of literal wire field names: maps
Oscar's BrowseResponse contract ({id, primary, secondary} index rows;
BrowseDetail enum-as-single-key-map) to view models for all six kinds,
folding join partners (system economy/factions/culture, corporation
presence, commodity production chains with nested Leontief inputs).
browse_protocol.gd split out of protocol.gd (max-file-lines);
sim_bridge gains browse_response_received + request_browse_index/detail.

Live-data catch: a present-but-NULL key (unnamed asteroid belt
proper_name) bypasses Dictionary.get fallbacks and rendered '<null>' —
_display_or() null-vs-absent helper applied across all six detail
mappers, 4 regression tests distinct from the absent-key cases.

43 gdUnit adapter cases; full suite 3074 green. Live-verified against
a real server + real systems.db: all six kinds Ready with real row
counts (301/3240/466/165/36/28), detail drill-down, NotFound on bogus
ids. Spawn-mode DB resolution issue found during verification is
pre-existing (cwd-relative data/systems.db) — server-side fix follows
separately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 10:24:39 +02:00
co-authored by Claude Fable 5
parent d48d72fd31
commit 90c562a6a3
11 changed files with 1801 additions and 9 deletions
+42 -7
View File
@@ -14,6 +14,17 @@ static func _mp():
return load("res://addons/messagepack/messagepack.gd")
## BrowseRequest/BrowseResponse codec (T-1131/T-1133) — factored into its own
## file to stay under gdlint's max-file-lines, load()'d here (not referenced
## as a bare class_name) per the autoload parse-order rule (CLAUDE.md):
## Protocol is an autoload, and autoload scripts compile before global
## class_name scripts are registered — a top-level class_name reference would
## fail to parse. By the time any caller actually runs (always post-boot),
## load() returns the already-cached resource with no reload cost.
static func _bp():
return load("res://scripts/protocol/browse_protocol.gd")
# -- Decode: bytes from server → GDScript types --------------------------------
@@ -898,14 +909,36 @@ static func decode_city_names_response(bytes: PackedByteArray) -> Variant:
return city_names_response_from_raw(decode_raw(bytes))
## Encode a BrowseRequest (T-1131/T-1133, D-254 §4) for the six-entity data
## browser proxy. Delegates to browse_protocol.gd — kept out of this file to
## stay under gdlint's max-file-lines; see that file for the full wire-shape
## rationale (discriminator field, kind/query split, filter_system_id).
static func encode_browse_request(
kind: String, query_kind: String, filter_value: String = ""
) -> PackedByteArray:
return _bp().encode_browse_request(_mp(), kind, query_kind, filter_value)
## Build a BrowseResponse from an already-decoded raw value. See
## browse_protocol.gd for the full shape/disambiguation rationale.
static func browse_response_from_raw(raw: Variant) -> Variant:
return _bp().browse_response_from_raw(raw)
## Decode a BrowseResponse from MessagePack bytes. See browse_response_from_raw.
static func decode_browse_response(bytes: PackedByteArray) -> Variant:
return browse_response_from_raw(decode_raw(bytes))
## Decode + classify one inbound frame (#960, D-225; T-949 adds starmap/
## citynames). Returns {kind, value} with kind "snapshot" | "atlas" |
## "starmap" | "citynames" | "unknown" — all four response kinds are msgpack
## maps, so they are told apart by field. Checked most-specific-first:
## StarMapResponse is the only kind with "data" and no "body_id";
## CityNamesResponse is the only kind with "cities"; anything else carrying
## "status" is AtlasLayerResponse. Lets receive_bytes decode the frame ONCE
## and branch, instead of double-decoding the 20 Hz snapshot path.
## citynames; T-1131/T-1133 adds browse). Returns {kind, value}, kind one of
## "snapshot"|"atlas"|"starmap"|"citynames"|"browse"|"unknown" — all msgpack
## maps, told apart by field, most-specific-first: StarMapResponse is the
## only kind with "data" and no "body_id"; CityNamesResponse the only one
## with "cities"; BrowseResponse the only one with its OWN "kind" field
## alongside "status"; anything else carrying "status" is AtlasLayerResponse.
## Lets receive_bytes decode the frame ONCE instead of double-decoding the
## 20 Hz snapshot path.
static func decode_inbound(bytes: PackedByteArray) -> Dictionary:
var raw = decode_raw(bytes)
if not raw is Dictionary:
@@ -916,6 +949,8 @@ static func decode_inbound(bytes: PackedByteArray) -> Dictionary:
return {"kind": "starmap", "value": star_map_response_from_raw(raw)}
if raw.has("cities"):
return {"kind": "citynames", "value": city_names_response_from_raw(raw)}
if raw.has("kind") and raw.has("status"):
return {"kind": "browse", "value": browse_response_from_raw(raw)}
if raw.has("status"):
return {"kind": "atlas", "value": atlas_response_from_raw(raw)}
return {"kind": "snapshot", "value": _decode_snapshot_from_raw(raw)}