Skip to content

fix(web,admin,db): keep the last forecast on ended events, not a false weather error - #1435

Merged
solarssk merged 3 commits into
mainfrom
fix/weather-ended-events
Sep 25, 2026
Merged

solarssk merged 3 commits into
mainfrom
fix/weather-ended-events

Conversation

@solarssk

@solarssk solarssk commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Description

Business context. The Events list showed a "Weather unavailable" chip on every event whose day had already passed, with a tooltip telling admins to check the weather provider under External services, although the provider worked fine. Weather providers only serve today and the days ahead, so an ended event can never get a forecast, and the message sent admins to fix something that was not broken. It also meant an ended event lost its weather entirely. Now the card keeps the last forecast Admitto saw for the event's day, labelled "Last forecast", and shows nothing (never an error) when no forecast was ever saved.

Technical changes.

  • web: WeatherService.summarize returns a new past status once the event day is over in the event's timezone, before any provider call or cache read. Before, a day two or more days back was short-circuited to unavailable, and yesterday reached the provider, always came back empty and logged a false weather_fetch_failed. past carries the saved forecast (temperatures, code, and the credit of the provider that gave it) when one still fits the event's day and pin. New weather-snapshot.ts holds the validation, the day and pin matching and the save rule. New event-weather.ts (summarizeEventsWeather) loads the saved forecasts with one query and saves the forecast just computed while the day is still ahead; a failed read or write is logged (weather_snapshot_*) and never fails the list. Each write is conditional on the stored value still being the one that was read, because a provider call sits between the read and the write: a request that saved in the meantime (a full-day forecast just before midnight, say) is never overwritten by an older or partial reading, and a write that lost that race is dropped without an error. Nothing is saved at all when the saved forecasts could not be read. It is used by the admin events list, a single event and the check-in events list. The public ticket page neither reads nor writes it, so visitors cannot cause writes.
  • save rule: a forecast for a day that is still ahead always replaces the stored one (newest whole-day reading). On the event day itself MET Norway only returns the hours that are left, so that reading only fills an empty snapshot and never overwrites a full-day one. Unchanged values are not rewritten.
  • db: nullable Event.weather_snapshot (JSONB) and migration 20260925190000_add_event_weather_snapshot. It is additive; applied from zero on a scratch database with no schema drift (prisma migrate diff). The stored value carries its own calendar day and rounded pin, so an event moved to another day or place never shows a stale forecast.
  • admin: EventCard shows the saved forecast as "Last forecast" in the operator's °C or °F, credited to the provider that gave it, and no chip for an ended event without one.
  • docs: wiki page Organisation Administration and the CHANGELOG.

How to test

Ran locally on this branch, all green:

  • npm run build -w @admitto/web, npm run build -w @admitto/admin, npm run lint, npm run lint:baseline:check, npm run docs:check
  • bash scripts/check-migrations-destructive.sh and its self-test
  • npm test -w @admitto/<name> for all 18 workspaces: 774 test files, 11248 tests, 0 failures; after the last commit web and admin were re-run in full (new: weather-snapshot, event-weather, an integration test on a real database for save, replace by a newer forecast, show-after-the-event, single event, check-in list, moved event, a write that lost a race to another request, and the raw column never reaching the browser, plus service and EventCard cases)

Manual:

  1. With a weather provider that works (Open-Meteo, or MET Norway with a Support contact), open the Events list while an upcoming event with a map pin is inside the forecast horizon. This saves its forecast.
  2. After that event's day has passed, its card shows Last forecast with the temperature, and the tooltip names the condition, the range and the provider credit.
  3. An ended event that never had a forecast saved shows no weather chip and no "Weather unavailable" error.
  4. Moving the event to another day discards the saved forecast for it.

What stays / known limitations

  • Events that ended before this release, or whose day nobody had the Events list, the event or the check-in list open for while it was ahead, have no saved forecast and show no chip. Saving is driven by staff opening those views. A scheduled worker task that saves a forecast for every event inside the horizon would make it deterministic; not built.
  • No provider can supply an ended day: MET Norway has no history, and Open-Meteo's forecast endpoint holds only about 60 to 64 days of past values (older rows come back empty). A history lookup was therefore left out on purpose.
  • On the event day MET Norway only returns the hours that are left, so a snapshot first saved that day may cover only part of the day.
  • The saved value is a forecast, not a measurement, and is labelled that way.

Documentation impact

  • Wiki updated
  • No Wiki update needed - explain why

Checklist

  • No secrets / keys / passwords in the diff
  • No real personal data (seed/sample data uses synthetic @example.com addresses)
  • Tests pass locally (npm test; optional: npm run coverage to match CI)
  • New or changed functionality is covered by tests added to the automated suite
  • New fields containing personal data are justified and minimised (none: the snapshot holds weather values and the rounded venue pin)
  • No PII in logs; token/QR contains no personal data
  • DB schema changes include a migration

🤖 Generated with Claude Code

solarssk and others added 2 commits September 25, 2026 21:36
…ended events

Every event whose day had passed showed a "Weather unavailable" chip that
pointed admins to External services, although the weather provider was fine.
Providers only serve today and the days ahead, so an ended day can never have
a forecast: days more than one day back were short-circuited to "unavailable",
and yesterday was sent to the provider, always came back empty and logged a
false weather_fetch_failed.

WeatherService now returns a new "past" status once the event day is over in
the event's timezone, before any provider call or cache read. The admin event
card renders no chip for it (neither the error chip nor the "No weather"
fallback). The public ticket page is unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e event

Weather providers cannot look back once an event's day is over, so an ended
event's card had nothing left to show. Admitto now saves the latest forecast
it sees for an event's day and shows it afterwards as "Last forecast", with
the credit of the provider that gave it.

- db: nullable Event.weather_snapshot (JSONB) and its migration. The value
  carries its own calendar day and pin, so an event moved to another day or
  place never shows a stale forecast.
- web: the events list, a single event and the check-in event list save the
  forecast they just computed while the day is still ahead (a newer whole-day
  forecast replaces the stored one, the event day itself only fills an empty
  snapshot because MET Norway returns just the hours that are left, unchanged
  values are not rewritten). Ended events read it back through the new
  "past" summary. A failed read or write is logged and never fails the list.
  The public ticket page neither reads nor writes it.
- admin: the event card labels it "Last forecast" and shows it in the
  operator's temperature unit; without a saved forecast the chip stays hidden.

Events that ended before this, or whose day nobody had a list open for while
it was ahead, have no saved forecast.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@solarssk solarssk added this to the 0.7.4 milestone Sep 25, 2026
@solarssk solarssk added type: bug Something is broken or behaves incorrectly area: db packages/db — Prisma schema, migrations, seed prio: medium Should land in current milestone but not a blocker area: admin Admin application — staff and organisation settings labels Sep 25, 2026
@solarssk solarssk self-assigned this Sep 25, 2026
@codecov

codecov Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

@solarssk

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@solarssk

Copy link
Copy Markdown
Owner Author

@codex review

@coderabbitai

coderabbitai Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Chef's kiss.

Reviewed commit: abc558602c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@coderabbitai

coderabbitai Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: solarssk/admitto/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 36c54757-fb43-4343-a652-9a3d76544dba

📥 Commits

Reviewing files that changed from the base of the PR and between f8636dc and abc5586.

📒 Files selected for processing (18)
  • CHANGELOG.md
  • apps/admin/src/api/types.ts
  • apps/admin/src/components/EventCard.tsx
  • apps/admin/test/components/EventCard.test.tsx
  • apps/web/src/admin/admin-api-routes.ts
  • apps/web/src/weather/event-weather.ts
  • apps/web/src/weather/types.ts
  • apps/web/src/weather/weather-service.ts
  • apps/web/src/weather/weather-snapshot.ts
  • apps/web/test/helpers/open-meteo-forecast.ts
  • apps/web/test/integration/event-weather-snapshot.test.ts
  • apps/web/test/ticket-page.test.ts
  • apps/web/test/weather/event-weather.test.ts
  • apps/web/test/weather/weather-service.test.ts
  • apps/web/test/weather/weather-snapshot.test.ts
  • docs/wiki/Organisation-Administration.md
  • packages/db/prisma/migrations/20260925190000_add_event_weather_snapshot/migration.sql
  • packages/db/prisma/schema.prisma

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

Event weather summaries now retain eligible forecasts in a nullable event snapshot. After an event day ends, summaries use a matching saved forecast when available, and admin event cards label it “Last forecast.” Events without a saved forecast report past status without an error.

Changes

Event weather snapshots

Layer / File(s) Summary
Snapshot storage and validation
apps/web/src/weather/weather-snapshot.ts, packages/db/prisma/schema.prisma, packages/db/prisma/migrations/*, apps/web/test/weather/weather-snapshot.test.ts
Events gain a nullable JSON snapshot. Snapshot parsing checks its shape, provider, date, values, and match to the event’s date and rounded coordinates. Update decisions cover eligible forecast creation and replacement.
Ended event weather summaries
apps/web/src/weather/types.ts, apps/web/src/weather/weather-service.ts, apps/web/test/weather/weather-service.test.ts
Weather status adds past. The service uses the event’s timezone-local day and returns past status after that day ends. A matching snapshot supplies forecast values and provider attribution; otherwise the summary contains only the status.
Event-list snapshot loading and saving
apps/web/src/weather/event-weather.ts, apps/web/src/admin/admin-api-routes.ts, apps/web/test/weather/event-weather.test.ts, apps/web/test/integration/event-weather-snapshot.test.ts, apps/web/test/helpers/open-meteo-forecast.ts
Event-list summaries load snapshots and pass them to the weather service. Eligible forecasts are saved. Database read and write failures are logged, and the summaries are returned. Tests cover event-list responses, persistence, reuse, and date changes.
Weather status and event-card display
apps/admin/src/api/types.ts, apps/admin/src/components/EventCard.tsx, apps/admin/test/components/EventCard.test.tsx, apps/web/test/ticket-page.test.ts, docs/wiki/Organisation-Administration.md, CHANGELOG.md
Admin event cards label saved forecasts as “Last forecast” and hide the chip when no temperature is available. Ticket pages omit weather for past status. The changelog and administration documentation describe the behavior.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix · Severity of issue fixed: Low

Sequence Diagram(s)

sequenceDiagram
  participant EventListRoute
  participant summarizeEventsWeather
  participant Database
  participant WeatherService
  participant WeatherProvider
  EventListRoute->>summarizeEventsWeather: event rows and weather service
  summarizeEventsWeather->>Database: load weather_snapshot values
  summarizeEventsWeather->>WeatherService: summarize events with saved snapshots
  WeatherService->>WeatherProvider: request forecasts for eligible event days
  WeatherService-->>summarizeEventsWeather: weather summaries
  summarizeEventsWeather->>Database: save eligible forecast snapshots
  summarizeEventsWeather-->>EventListRoute: weather summaries
Loading

Merge Risk: ⚪ Minimal · up to abc55

The saved-forecast behavior is consistently available in the intended staff event views, with public tickets intentionally unchanged.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to abc55

The new saved-forecast workflow can permanently replace an event-day forecast after a failed read or an overlapping request. No cross-organisation access or public write path was established, but the access boundary could not be fully verified.

Retained concerns

  • Medium · architecture · observed: A failed snapshot read or an overlapping request can defeat the intended event-day preservation and newest-forecast rules: the writer decides from previously read state, then updates by event ID without rechecking that state or the event identity.
Security review details

Security Blast Radius

  • inferred — The new database writes are reached through admin and check-in event enrichment, not the observed public ticket weather path. Each write targets an Event row returned to a caller; the maximum accessible organisation scope remains unverified.

Trust Boundaries and Controls

  • observed — Admin event handlers pass an authenticated user ID to their event queries. The check-in handler likewise passes a session user ID; its event-query ownership and privilege predicates were not inspected.

Resilience and Maintainability Implications

  • observed — Invalid or nonmatching stored snapshots are not used for ended-event summaries, limiting display of forecasts after a material date or pin change. This read-time control does not prevent a stale write.

Hardening Proposals

  • proposed — Skip snapshot writes when the prerequisite load fails, and condition writes on the event identity and snapshot state observed when the candidate was computed.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 64.71% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 17 functions across 14 files. (4 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the primary change: retaining the last forecast for ended events instead of showing a false weather error.
Description check ✅ Passed The description includes all required sections, explains the business and technical changes, lists concrete tests, documents limitations, declares Wiki impact, and completes the checklist.
Full details: Docstring Coverage

Explanation

Docstring coverage is 64.71% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 17 functions across 14 files. (4 skipped: 4 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

… overlapping requests

Review of the snapshot change found two ways a saved forecast could be lost.

- When the saved forecasts could not be read, the writer treated every event
  as having an empty snapshot. On the event day that let a reading, which MET
  Norway may only give for the hours that are left, overwrite the full-day
  forecast. Nothing is saved now when the read failed.
- The write was a blind update by event id, but a provider call sits between
  reading the saved value and writing it. A request that saved in the meantime
  (a full-day forecast just before midnight, say) could be overwritten by an
  older or partial one. Each write is now conditional on the stored value still
  being the one that was read; a write that lost that race is dropped without
  an error.

Also drops an unreachable "?? null" (the decision function already accepts a
missing summary), uses an optional chain in snapshotForEvent, and documents
the touched helpers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@solarssk
solarssk merged commit 30e34ce into main Sep 25, 2026
24 checks passed
@solarssk
solarssk deleted the fix/weather-ended-events branch September 25, 2026 18:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: admin Admin application — staff and organisation settings area: db packages/db — Prisma schema, migrations, seed prio: medium Should land in current milestone but not a blocker type: bug Something is broken or behaves incorrectly

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant