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
+50 -2
View File
@@ -6,6 +6,7 @@ signal snapshot_received(snapshot: Dictionary)
signal atlas_layers_received(response: Dictionary)
signal star_map_received(response: Dictionary) # T-949: StarMapResponse
signal city_names_received(response: Dictionary) # T-949: CityNamesResponse
signal browse_response_received(response: Dictionary) # T-1131/T-1133: BrowseResponse
signal handshake_complete
signal handshake_failed(reason: String)
@@ -490,6 +491,50 @@ func request_city_names(body_id: String) -> void:
)
## Request one entity kind's index list from the browser proxy (T-1131/
## T-1133, D-254 §4). Live mode only — sends a BrowseRequest{kind, Index}
## frame; the response arrives via browse_response_received. filter_system_id
## only means anything for kind == "Body" (bodies filter by containing
## system) — pass "" (default) for every other kind or for an unfiltered
## Body index. No "wanted before connect" replay bookkeeping like
## request_star_map(): the browser screens call this on-demand while the app
## is open and connected (a live drill-down request, not a session-scoped
## dataset fetched once at boot), so a request issued before CONNECTED is
## simply not sent — the screen re-requests on its own enter()/refresh path
## the next time it's shown.
func request_browse_index(kind: String, filter_system_id: String = "") -> void:
if test_mode or _bridge == null or state != ConnectionState.CONNECTED:
return
var bytes := Protocol.encode_browse_request(kind, "Index", filter_system_id)
if bytes.is_empty():
return
var err: int = _bridge.send_message(bytes)
if err != OK:
push_error(
"SimBridge: failed to send browse index request for %s: %s" % [kind, error_string(err)]
)
## Request one entity's full detail row from the browser proxy (T-1131/
## T-1133, D-254 §4). Live mode only — sends a BrowseRequest{kind, Detail}
## frame; the response arrives via browse_response_received. entity_id is the
## same "id" field an Index row returned for this kind.
func request_browse_detail(kind: String, entity_id: String) -> void:
if test_mode or _bridge == null or state != ConnectionState.CONNECTED:
return
var bytes := Protocol.encode_browse_request(kind, "Detail", entity_id)
if bytes.is_empty():
return
var err: int = _bridge.send_message(bytes)
if err != OK:
push_error(
(
"SimBridge: failed to send browse detail request for %s/%s: %s"
% [kind, entity_id, error_string(err)]
)
)
# Poll for snapshot from simulation.
# In test mode delegates to test harness. In live mode, returns the last decoded snapshot.
func poll_snapshot() -> Variant:
@@ -516,8 +561,8 @@ func poll_snapshot() -> Variant:
# so they aren't silently dropped when server ticks faster than client consumes.
func receive_bytes(bytes: PackedByteArray) -> void:
# Decode once, branch by frame shape (#960, D-225; T-949 adds starmap/
# citynames): all response kinds and snapshots are msgpack maps, told
# apart by field.
# citynames; T-1131/T-1133 adds browse): all response kinds and
# snapshots are msgpack maps, told apart by field.
var inbound := Protocol.decode_inbound(bytes)
if inbound.kind == "atlas":
atlas_layers_received.emit(inbound.value)
@@ -528,6 +573,9 @@ func receive_bytes(bytes: PackedByteArray) -> void:
if inbound.kind == "citynames":
city_names_received.emit(inbound.value)
return
if inbound.kind == "browse":
browse_response_received.emit(inbound.value)
return
if inbound.kind != "snapshot":
push_warning("SimBridge: undecodable frame (%d bytes)" % bytes.size())
return