fix(builder): initialise scenario sub-arrays + client null-guard (t-paliad-344)
GetScenarioDeep returned nil slices for proceedings/events/shares when a scenario had zero rows, which Go's encoding/json serialises as `null` rather than `[]`. The builder's renderCanvas then unconditionally calls `state.active.proceedings.filter(...)` on a null and dies with `procedures.js:101 TypeError: Cannot read properties of null (reading 'filter')` — every cold-open scenario crashed the page before the empty canvas could render. Backend (root cause): initialise Proceedings / Events / Shares to empty slices in BuilderScenarioDeep before SelectContext, so the wire shape is always arrays. Existing rows still load via SelectContext, which truncates the placeholder and refills from the DB. Frontend (defence in depth): on loadScenario(), normalise each of the three arrays to `[]` if the server response is not an array. Catches a future regression (or an older deployed build) without re-introducing the same crash class. bun build clean, go vet + go test ./... green.
This commit is contained in:
@@ -815,6 +815,13 @@ async function loadScenario(id: string): Promise<void> {
|
||||
setSaveState("error");
|
||||
return;
|
||||
}
|
||||
// Defensive: Go's encoding/json serialises a nil slice as `null`, not
|
||||
// `[]`. The server initialises these arrays today, but normalising on
|
||||
// the client too means a future regression (or an older deployed
|
||||
// build) can't crash renderCanvas with `null.filter(...)`.
|
||||
if (!Array.isArray(deep.proceedings)) deep.proceedings = [];
|
||||
if (!Array.isArray(deep.events)) deep.events = [];
|
||||
if (!Array.isArray(deep.shares)) deep.shares = [];
|
||||
state.active = deep;
|
||||
state.pending = {};
|
||||
writeScenarioToUrl(id);
|
||||
|
||||
@@ -204,7 +204,15 @@ func (s *ScenarioBuilderService) GetScenarioDeep(ctx context.Context, userID, sc
|
||||
return nil, ErrScenarioBuilderNotVisible
|
||||
}
|
||||
|
||||
deep := &BuilderScenarioDeep{BuilderScenario: *sc}
|
||||
deep := &BuilderScenarioDeep{
|
||||
BuilderScenario: *sc,
|
||||
// Initialise to empty so the JSON response always carries arrays,
|
||||
// not null — the builder frontend's renderCanvas calls .filter on
|
||||
// proceedings/events unconditionally once state.active is set.
|
||||
Proceedings: []BuilderProceeding{},
|
||||
Events: []BuilderEvent{},
|
||||
Shares: []BuilderShare{},
|
||||
}
|
||||
|
||||
if err := s.db.SelectContext(ctx, &deep.Proceedings, `
|
||||
SELECT id, scenario_id, proceeding_type_id, primary_party, scenario_flags,
|
||||
|
||||
Reference in New Issue
Block a user