Summary
Four backend capabilities the app was not using — the category taxonomy, the licensed cover library, PostGIS radius search, and the analytics stream — are now wired into Flutter. Three of the four already existed server-side; the app either duplicated them badly or ignored them.
Files Changed
lib/core/services/category_catalog.dart— new. Readsevent_categoriesfrom Supabase, caches to shared_preferences, resolves label/icon/colour.lib/core/services/cover_image_service.dart— new. Micro-batching loader overcovers_for_events(uuid[]).lib/core/services/kpi_service.dart— new. Client signals viatrack_kpi.lib/shared/models/event_model.dart— addscategorySlug,categoryId.lib/core/network/event_api_service.dart— parses the new fields; the nearby fallback now logs loudly.lib/shared/widgets/flow_event_image.dart— falls back to a licensed cover.lib/features/events/screens/event_discovery_screen.dart— vibe filter fixed.lib/features/events/screens/series_detail_screen.dart,lib/features/search/screens/search_screen.dart— hardcoded maps removed.lib/main.dart— warms the catalogue at startup (not awaited).
Key Decisions
The EventCategory enum survives, but stops owning display
The enum knows 7 values; the taxonomy holds 34 and grows. It is referenced in 82 places (creation wizard, filters, repositories), so removing it is a large refactor with no user-visible benefit. Instead the raw slug travels alongside it and the taxonomy owns the label. The enum keeps filtering; the catalogue does display. They never disagree, because the catalogue falls back to the enum’s own labels when it has nothing better.
The enum is lossy
Anything the enum does not recognise parses as
EventCategory.other. Never useevent.categoryfor display — useevent.categorySlugwithCategoryCatalog.
The vibe filter was actively wrong
event_discovery_screen mapped vibes onto enum values ('tec' -> 'nightlife')
and filtered on e.category.name. Picking “Techno” therefore returned every
nightlife event and excluded the actual techno ones, because slug techno
parses as other. It now maps onto real slugs, which exist. The chip-
availability check carried a second copy of the same test — that duplication is
how the two drifted apart, so both now call one _matchesVibe.
Covers are batched by a loader, not fetched per query
Covers are needed by a widget, so requests arrive one per card as a list
scrolls. Fetching inside each of the eight event queries would have duplicated
the logic eight times and still missed anything loaded elsewhere. Instead
requests made within 30ms are collected into one covers_for_events call.
Negative results are cached too, so an event with no cover is not re-asked on
every rebuild.
Attribution decides where a cover may appear
The library is CC BY / CC BY-SA, which requires naming the author wherever the
image is shown. Below 120pt of height a credit line is illegible, so
FlowEventImage does not show a licensed image at all at that size and keeps
the gradient. An unreadable credit is not compliance.
get_nearby_events did not exist
The app had been calling it since before it was written, catching the failure, and falling back to: fetch 200 rows, filter client-side with a bounding box. That fallback is wrong in four ways — 200 rows for 20 results, corners up to 41% beyond the radius, silent truncation past the cap, and ordering by date rather than distance. It survived because it never threw. The function now exists (PostGIS, RLS-respecting, GiST-indexed) and the fallback logs a warning when it runs.
Client analytics only record what the server cannot see
RSVPs, event creation and registration are already recorded by database
triggers. KpiService asserts against re-sending those, and covers only
impressions, detail opens, ticket-link taps and searches. track_kpi
attributes every row to auth.uid(), so a client cannot fabricate activity for
another user; with no session it is a silent no-op. Search queries are never
sent — only their length.
Verification
flutter analyze— no issues.covers_for_eventsexercised asanon: 6 of 7 visible events resolved, with a credit line.get_nearby_eventsreturns events ordered by true distance from Reggio Emilia. It returns empty today because no visible event is both future and geo-located — the 50activerows are past-dated seed data.track_kpitested inside a rolled-back transaction: authenticated writes one row, anonymous writes none.
Known Gaps
geo_pointwas empty on all 50activeevents (coordinates lived only inlocationjsonb). Backfilled — 31 recovered — and a trigger now keeps it in sync, because at least four code paths insert events.toggle_message_reactionis called bymessaging_api_service.dart:677and does not exist in the database — the same shape of bug asget_nearby_events, still unfixed.
Related
database-schema · audit-unused-infrastructure · refactor-theme-tokens