fix(timetable): eliminate ?day URL flicker by deriving isFlatEvent from event_type instead of subEvents length (B7)

Phase A finding A5 traced this race in the browser logs:

  GET .../performances?day={festival_id}  → 200, 0 results   ← wrong day
  GET .../children                        → 200, 3 sub_events
  GET .../performances?day={subevent_id}  → 200, 13 results  ← correct

The pre-fix `isFlatEvent` was:
  computed(() => !subEvents.value || subEvents.value.length === 0)

While `subEvents` was still loading (undefined), `!undefined` is `true`,
so isFlatEvent erroneously returned `true` for festivals during the
loading window. dayOptions then took the flat-event branch and seeded
validSubEventIds with the FESTIVAL id. useActiveDay's corrective watcher
rewrote the URL to `?day={festival_id}` and fired a wasted query that
returned zero results (correct semantics — performances live at sub-event
level — but waste + visible URL flicker).

Fix:
  computed(() => eventDetail.value?.event_type === 'event')

EventResource always serialises event_type (verified at
api/app/Http/Resources/Api/V1/EventResource.php:26). EventTabsNav
already consumes event_type / is_festival from the same shape
(apps/app/src/components/events/EventTabsNav.vue:175,266) so this is
the canonical signal, not a one-off addition.

New behavior trace:
  - Both queries pending  → eventDetail=undefined → isFlatEvent=false
                          → festival branch returns (subEvents ?? []).map(...)
                          → validSubEventIds=[] → activeDayId=null
                          → usePerformances.enabled=false → NO fetch
  - subEvents resolves first → festival branch populates dayOptions
                          → fetch fires with correct sub-event id
  - eventDetail resolves first to flat event → flat branch fires
                          → fetch with eventDetail.id (correct)
  - eventDetail resolves first to festival → still false until subEvents
                          → no false-positive flat-event fetch

402 tests still pass; typecheck + lint + production build all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-09 22:49:33 +02:00
parent bce3081cb2
commit 8b678c0626

View File

@@ -51,7 +51,12 @@ const eventIdRef = eventId
const { data: eventDetail } = useEventDetail(orgId, eventId)
const { data: subEvents } = useEventChildren(orgId, eventId)
const isFlatEvent = computed(() => !subEvents.value || subEvents.value.length === 0)
// Authoritative once eventDetail loads (single-row endpoint, fast).
// Pre-load it returns `false`, which keeps dayOptions empty until we
// actually know whether this is a flat event or a festival — avoids the
// `?day={festival_id}` URL flicker that occurred when subEvents was
// still pending (it was being read as "no children → flat event").
const isFlatEvent = computed(() => eventDetail.value?.event_type === 'event')
const dayOptions = computed(() => {
if (isFlatEvent.value && eventDetail.value)