prod 500s after B1+B2 deploy — /api/builder/scenarios + /api/events?type=deadline failing #154
Reference in New Issue
Block a user
No description provided.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Regression on main
88c03e9(post-B1+B2 deploy)m reported live on paliad.de right after the B1+B2 deploy:
Followed by:
m's words: "It does not really work, cant set anything... you should fix it with a tester. Or is it not done yet?!"
What broke
/tools/proceduresloads the chrome (page header / scenario picker / matter picker / search / overview / event / from-matter / side panel), but interaction dies on thenull.filterJS error. Likely first fetch (GET /api/builder/scenarios?status=active) returns 500 →fetchJSONswallows tonull→ downstreamstate.active.proceedings.filter(...)throws./api/events?type=deadline&status=pendingand?status=overdue. These are NOT new code from B0/B1/B2 — they were working before tonight's deploy. This points at mig 157 (B0) breaking something at the schema / RLS layer, or a service-init regression from the new wirings.Train context
Tonight's session shipped (in order):
1844df31b4b2e40c857026c1d8cc46dc4ec88c03e9The
/api/eventsregression is the loud signal — that endpoint isn't touched by any of the new code. mig 157'spaliad.projects.origin_scenario_idcolumn / RLS changes are the most likely culprit, but needs log investigation.Assigned fixer
brunel running on t-paliad-344 — investigation order: pull Dokploy logs first (look at the SQL panic / error message for the
/api/events?type=deadline&status=pendingpath), then verify mig 157 applied cleanly, then service init, then frontend null-guards. Verification via mai-tester (Playwright) before commit. Do NOT revert main without head approval.Acceptance
/api/events?type=deadline&status=pendingreturns 200 with the deadline list (m's dashboard works)/api/events?type=deadline&status=overduereturns 200/api/builder/scenarios?status=activereturns 200 with an array (possibly empty)/tools/proceduresloads without JS errors and the New Scenario CTA works through to a saved scratch scenarioMeasured 2026-07-29: does not reproduce at migration head 211
Not closing this — recording what was measured, because two months of silence is not the same as an answer.
Why it sat
All three endpoints are behind the auth gate, so an anonymous probe only ever returns 401. Nobody could establish whether the 500s still happened.
cmd/server/http_smoke_enforcing_test.go(merged today) boots the real server binary against a prod-shape harness and mints a signature-valid session, which is what made this answerable.What was run
Real binary, prod-shape harness at
applied_migrationshead 211, NOBYPASSRLS runtime role, prod's ownership map, both aglobal_adminand a staffed-member session:/api/events?type=deadline&status=pending/api/events?type=deadline&status=overdue[]/api/builder/scenarios?status=active[]No 500 on any of them, for either session.
What is genuinely covered, and what is not
Covered. Schema-level and RLS-level breakage on these three paths. The
pendingpath returns a real row, so it is not answering over an empty table.overdueand the scenario list return empty lists — the SQL still executes, and a broken statement 500s whether or not it matches, so the does it 500 question is answered. It does not prove those filters select the right rows.Not covered. A failure that needs production's data — a NULL where the fixture has none, or a plan-dependent error at scale. The fixture carries 1 deadline, 1 appointment, 0 scenarios.
Not covered, and this is the one that matters here. The original deploy was mid-crash-loop (this issue names mig 157, and that era carried its own hotfixes). A partially-applied schema is not reproducible on a harness that applies every migration cleanly. If that was the cause, "it works here" is the expected result and says nothing.
No fix commit is identifiable
internal/services/event_service.gochanged only through thedb.Queriersweep and theActingUserDBwiring. Nothing between the report and now says it fixed this. So the honest verdict is split:scenario_*ownership reassignment (2026-07-27) are real changes to exactly that surface;The client half of the report
The trace named
procedures.js:101andCannot read properties of null (reading 'filter'). That file is now 15 lines and holds no fetch and no.filter— the builder moved toclient/builder.ts, whose list callers normalise withArray.isArray(out) ? out : []. Server-side, both the list and the deep scenario endpoints initialise their slices, so they answer[]and nevernull. The specific crash in the trace cannot occur in the current code.What changed
The three endpoints are now a permanent step in the HTTP smoke, so they can never again be unanswerable: 4c97c35. It asserts the status, that the body is not JSON
null(a 200 alone does not rule out thenull.filtershape), and a row count on thependingpath so it cannot pass on an endpoint that returns[]for everything.What would settle the remaining difference
One authenticated GET of those three URLs against production. That needs a session credential, which is m's or the head's to provide — it is the only thing that distinguishes fixed from not reproducible here.
Re-measured 2026-07-31. Server half still does not reproduce, now at prod's exact applied set. The client half has one real gap — and prod cannot be asked at all.
Continuing the 2026-07-29 comment rather than repeating it. That comment measured the three endpoints at the time and named two things it could not reach: production-side evidence, and an authenticated GET against prod. I went after the first. It cannot be obtained, and the reason is worth more than the count would have been.
1. Server half — re-run against prod's applied set exactly
TestHTTPSmoke_EnforcingRoleonmainc107154, real server binary, prod-shape harness, NOBYPASSRLS runtime role.The harness finished at
paliad.applied_migrationsmax 216, 211 rows — byte-identical to production, which I read as max 216, 211 rows in the same session. Not "a recent head": the same one. (The five-number gap is the m/paliad#44 design, not a fault.)That subtest asserts, per URL: status 200, body is not JSON
null, and — onstatus=pending— a row count taken from the fixture so it cannot pass on an endpoint that answers[]for everything. Status-only would have called a200 nullgreen; it does not.Still no 500 on any of the three. The 2026-07-29 caveat stands unchanged and I am not weakening it: a deploy that was mid-crash-loop on a partially applied schema is not reproducible on a harness that applies every migration cleanly. For that class, "it works here" is the expected result and proves nothing.
2. Production-side evidence — the count cannot be produced, by construction
The ask was a count of 500s on that route in prod. There is no way to get one. Two independent reasons, both worth knowing:
paliad emits no HTTP request log. The running container's entire log is 14 lines, all from boot:
There is no logging middleware on the mux. A 500 in production leaves no trace at all unless a specific handler logs it, and
handleListEventsdoes not — it routes errors throughwriteServiceErrorand writes nothing.Traefik's access log cannot cover it either. It exists (
/etc/dokploy/traefik/dynamic/access.log, JSON), but:filters: { retryAttempts: true, minDuration: 10ms }, so a request that is fast and not retried is not written. A quick 500 is exactly that shape.paliad.deproduced 7 requests — six/assets/*and one/logout. Zero to/api/events. Zero 5xx on any paliad host.So the honest reading of "no 500s in the prod log" is not "no 500s happened". It is "prod cannot answer this question". Anyone citing the log as evidence of health here would be citing an absence they never had the ability to observe.
3. Client half — audited independently, and one caller has no guard
Three client modules fetch
/api/events. They do not behave the same way.client/builder.ts/tools/proceduresclient/projects-detail.ts(await resp.json()) ?? []client/events.tsThe procedures client is fixed.
procedures.tsis now 15 lines and holds no fetch and no.filter; the work moved tobuilder.ts, whose list fetches end inArray.isArray(out) ? out : [](builder.ts:221,:226, and two more at:251/:256) and whoseloadScenarionormalises all four deep arrays before anything reads them (builder.ts:1268-1271). The trace in this issue —procedures.js:101,null.filter— cannot occur in the current code.client/events.tsis the caller of the two URLs m actually reported, and it has no null guard.A 200 carrying JSON
nullsetsallItems = nulland throwsCannot read properties of null (reading 'length')— the same class as the reported crash, one property along. Its 500 handling is fine:!resp.okshows the unavailable banner and returns, no crash.Is it reachable today? No — and only because the server happens not to do it.
handleListEventswrites whateverEventService.ListVisibleForUserreturns, and that function opens without := make([]EventListItem, 0, 64)(event_service.go:137) and returnsout, nil(:215). Non-nil on every success path, so the wire value is[], nevernull.That makes it latent, not live — and the whole guarantee sits in one
makecall in a service, with nothing on the client and nothing pinning the coupling. Any future handler on that route that returns a nil slice turns this into m's blank page again.builder.tscarries a comment saying it normalises client-side precisely so a server regression cannot crash it;events.tsdoes not, and it is the page m was on.4. Verdict
null, at prod's exact applied set. Unchanged from 2026-07-29, now with the migration state matched rather than approximated. One correction to that comment: theissue154subtest exercises the admin session only (http_smoke_enforcing_test.go:562-597). Themembersession appears in step 4, against/api/deadlines, a different route. So "for either session" is not what is covered on these three URLs.client/events.tsdereferences a fetch result with no null guard, unlike the other two callers of the same endpoint. Latent today, and the one thing here that is a specific unbuilt fix rather than a measurement.Two things would settle what is left, neither of which is mine to do: one authenticated GET of the three URLs against production, and a decision on whether paliad should log requests at all.
Not closing. Nothing written to prod, no code changed.
paliad/ritchie, t-paliad-issue-154-verify-500s.