fix: stale-frame race, stuck verdict buttons, and 3 more frontend defects

seance.tsx / JudgmentPanel.tsx: neither `ritual_complete` nor
`judgment_result` carries an entity id, and both were applied
unconditionally — so a response still in flight when the seeker summoned a
fresh entity landed on whatever entity happened to be current when it
arrived, leaking the *previous* entity's hidden traits into the new one's
revealed-traits UI. Both frames are now gated on our still waiting for one
(ritual.status === 'in_progress' / judgmentPending), which the 'entity'
case clears the moment a new presence arrives, so a late answer for the old
entity is dropped instead of misattributed.

JudgmentPanel also had `pending` in component-local state that only cleared
when judgmentResult became a *new* truthy object. If the entity changed
while judgmentResult was already null, the reset was a no-op (null === null)
and pending stayed stuck, permanently disabling all four verdict buttons.
It now reads the shared judgmentPending flag, which the reducer resets.

coldSpot.ts: severity was ungated by `warm` while isColdSpot/
isPressureAnomaly were correctly gated. ColdSpotPanel feeds severity
straight into the composite disturbance gauge with no boolean gate of its
own, so a freshly-paired device could show "disturbance rising" off its 2nd
reading — exactly what the minSamples warm-up exists to prevent.

PlanchetteBoard.tsx: the first GOODBYE deadline was a bare
randomBetween(120,300) compared against `t`, which is seconds since
performance.timeOrigin (page load), not since mount. Every later reschedule
correctly offsets from `t`. On a tab open >5min before the board mounted
(or any remount via navigation), t was already past the deadline and the
planchette snapped to GOODBYE on the first frame.

sdr.ts: close() and setFrequency() inside the sweep loop were not wrapped in
withTimeout despite the file's own header claiming every stalling USB call
is. A dongle going unresponsive mid-sweep or during teardown hung forever —
the same silent-hang symptom withTimeout was added to eliminate.

InventoryPanel.tsx: essence was decremented client-side using a possibly
stale fallback price and never reconciled. The server already returns the
real post-purchase balance in PurchaseOut; use it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Indiana
2026-07-27 15:47:39 +00:00
parent 31f3f91801
commit eaa299f683
9 changed files with 194 additions and 35 deletions

View File

@@ -194,7 +194,12 @@ export function applyTemperatureReading(
)
const drop = deviation !== null ? Math.max(0, -deviation) : 0
const isColdSpot = warm && drop >= COLD_SPOT_DROP_C
const severity = clamp01(drop / (COLD_SPOT_DROP_C * 3))
// Gated by `warm` for the same reason isColdSpot is: pre-warm-up
// deviations are against a baseline that hasn't had a real chance to
// average out sensor noise, and severity feeds the composite disturbance
// gauge directly (ColdSpotPanel.tsx), which has no boolean gate of its
// own to catch an ungated value here.
const severity = warm ? clamp01(drop / (COLD_SPOT_DROP_C * 3)) : 0
return { state: next, deviation, isColdSpot, severity }
}
@@ -252,7 +257,9 @@ export function applyPressureReading(
)
const swing = deviation !== null ? Math.abs(deviation) : 0
const isPressureAnomaly = warm && swing >= PRESSURE_SWING_HPA
const severity = clamp01(swing / (PRESSURE_SWING_HPA * 3))
// See applyTemperatureReading's comment: gated by `warm` so the composite
// disturbance gauge can't be driven by a pre-warm-up baseline swing.
const severity = warm ? clamp01(swing / (PRESSURE_SWING_HPA * 3)) : 0
const direction: 'rise' | 'drop' = deviation !== null && deviation < 0 ? 'drop' : 'rise'
return { state: next, deviation, isPressureAnomaly, severity, direction }
}