Skip to content

fix: stop Alert History re-recording the same warnings on every load (#694) - #699

Merged
Aditya8369 merged 2 commits into
Aditya8369:mainfrom
MOHITKOURAV01:fix/694-alert-history-duplicates
Aug 13, 2026
Merged

fix: stop Alert History re-recording the same warnings on every load (#694)#699
Aditya8369 merged 2 commits into
Aditya8369:mainfrom
MOHITKOURAV01:fix/694-alert-history-duplicates

Conversation

@MOHITKOURAV01

Copy link
Copy Markdown
Contributor

Closes #694

The duplication

const lastHistorySignature = useRef("");

A ref is per-mount. On a fresh mount it is "", so the first effect run always looked like a new alert — while alertHistory itself was loaded from localStorage. The key that would have suppressed the write was the one thing not persisted alongside the thing it was guarding.

Reload the page in Delhi on a day when all five thresholds are breached and you get five more identical rows, timestamped with the time of the reload:

12/08/2026, 21:04:11 · Delhi · AQI 212 — PM2.5 is high. Wear a certified mask...
12/08/2026, 21:03:52 · Delhi · AQI 212 — PM2.5 is high. Wear a certified mask...
12/08/2026, 21:03:19 · Delhi · AQI 212 — PM2.5 is high. Wear a certified mask...

MAX_HISTORY is 50, so ten reloads is enough for the log to be ten copies of one moment. Switching city away and back did it too, since the signature includes cityName.

The key now comes from the stored log — each row carries the signature of the set it belonged to — so it survives a reload. Recording is skipped when the same signature was logged within RECORD_COOLDOWN_MS (one hour). That is the part worth arguing about, so to be explicit about the trade-off:

  • Not "never record the same set twice" — conditions being bad again tomorrow morning is a real event and should appear.
  • Not "only compare against the newest row" — that lets city A → B → A thrash straight through.
  • An hour absorbs reloads, re-renders and city switching, and is well under the interval at which air quality genuinely changes category.

Alert history in browsers that cannot notify

useEffect(() => {
  if (!("Notification" in window)) return;   // <-- recording lives below this

The early return was there to guard the notification, but the recording sat inside the same effect behind the same check. On iOS Safari and in in-app webviews — Instagram, Facebook, LinkedIn, a large share of the mobile traffic this app is built for — there is no Notification constructor, so the panel showed "No alert history yet." permanently, with no explanation.

The guard has moved down to the notification call, where it belongs.

Related, in the same block: permission initialised to "denied" when the API was missing, which rendered

Notifications are blocked. You've blocked notifications for this site. To receive pollution alerts, enable them manually in your browser settings (click the lock/info icon → Notifications → Allow)

to visitors on a device with no such setting. It is "unsupported" now, and that state renders nothing.

Smaller defects in the same code

  • Pre-formatted timestamps. new Date().toLocaleString() was written straight into storage — locale- and timezone-specific, impossible to compare, sort or de-duplicate on, and rendered in whatever locale the visitor had when it was written. Stored as ISO, formatted at render. Rows written by the previous version are passed through unchanged, so an upgrade does not blank the existing log.
  • key={i} on a list that is prepended to: every row's key changed whenever an alert was added. Keyed on a stable id, with a fallback derived from the row's own fields for legacy entries.
  • localStorage.setItem inside a setState updater — impure, so StrictMode ran it twice, and a quota error threw from inside a React state update. It now runs before setAlertHistory, the same correction fix: derive leaderboard stats from recorded activity instead of a seed (#671) #676 made to Leaderboard.
  • current.us_aqi read in the effect while the dependency array used current?.us_aqi. The effect runs before the if (!current) return null guard below it; it was only saved by warnings being empty. Both are optional now.

Structure

buildWarnings and the recording rules move to src/utils/alertHistory.js. None of it needs React — which warnings a reading produces, and whether a set has already been logged, are decidable from their inputs — and the component keeps a state variable and some JSX. Same split as contributionStats and checkInStreak.

recordAlerts returns { history, changed } and does not mutate its input, so the caller skips both the write and the re-render when nothing changed.

Tests

src/utils/alertHistory.test.js (33) — thresholds including the boundary value and a null reading; recording once, not twice, and again after the cooldown; the reload case reproduced directly; different city, city-switched-back, and a changed warning set; the cap and ordering; ISO timestamps; distinct ids; a missing AQI stored as null rather than a number; corrupt input; non-mutation; legacy rows without a signature; storage read/write/clear with a refused write; timestamp formatting for ISO, legacy and unusable rows.

src/components/AlertsPanel.test.jsx (19) — the panel had no tests at all, despite owning threshold logic, persistence and the notification path. Warnings rendering, the empty state, no reading at all, the low-confidence note, the log written once across three mounts, a genuinely different set being recorded, clearing, legacy entries rendering, a corrupt log, and the notification permission states including no-API-at-all.

✓ src/utils/alertHistory.test.js      (33 tests)
✓ src/components/AlertsPanel.test.jsx (19 tests)

Suite status

612 passed, 2 failed. The two failures are the pre-existing cacheStore/useSWR ones from #690, present on main before this branch and fixed by #695.

Migration

Nothing to migrate. Entries written by the old version have no at, id or signature; they render through the legacy path and take no part in de-duplication, so the first alert after an upgrade is recorded once more. From then on the log behaves.

…ditya8369#694)

The de-duplication key lived in a useRef, which is per-mount. Every page
load looked like a brand new alert, so a bad-air afternoon in Delhi wrote
five identical rows per reload until the whole log was copies of one
moment, timestamped with the times of the reloads. Switching city away
and back did it too.

The key is now derived from the stored log itself, so it survives a
reload, with an hour-long cooldown: reloads, re-renders and city
switching are absorbed, while the same conditions tomorrow are recorded
as the separate event they are.

Also in the same effect:

- Recording no longer sits behind `"Notification" in window`. History is
  a log of what the app displayed and has nothing to do with whether the
  browser can send notifications — iOS Safari and in-app webviews were
  showing "No alert history yet." permanently.
- A browser with no Notification constructor is "unsupported", not
  "denied". It was being shown instructions for unblocking a setting it
  does not have.
- Timestamps are stored as ISO strings and formatted at render, instead
  of storing a locale- and timezone-specific display string that could
  not be compared, sorted or de-duplicated on.
- Rows are keyed on a stable id rather than the array index, on a list
  that is prepended to.
- The localStorage write moved out of the setState updater.

buildWarnings and the recording rules move to src/utils/alertHistory.js,
where they can be tested without rendering. 33 tests there, 19 for the
panel, which had none.
@vercel

vercel Bot commented Aug 12, 2026

Copy link
Copy Markdown

@MOHITKOURAV01 is attempting to deploy a commit to the Aditya Mahajan's projects Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added the ECSoC26 Contributions considered under ECSoC'26 label Aug 12, 2026
@github-actions

Copy link
Copy Markdown

Thank You for Your Contribution! 🎉

Hi @MOHITKOURAV01,

Thank you for opening this Pull Request and contributing to our project. We truly appreciate your efforts.

Please make sure that:

  • Your code follows the project's guidelines.
  • You have linked the appropriate issue (if applicable).
  • Screenshots are added for UI/UX changes.
  • Your PR is ready for review.

The maintainer @Aditya8369 will review your PR shortly!

Happy Contributing! 🚀

@Aditya8369

Copy link
Copy Markdown
Owner

@MOHITKOURAV01 solve conflicts

@Aditya8369 Aditya8369 added good-pr Additional ECSoC label and removed merge-conflicts labels Aug 13, 2026
@Aditya8369
Aditya8369 merged commit 7d92a5a into Aditya8369:main Aug 13, 2026
1 check failed
@github-actions

Copy link
Copy Markdown

🎉 Your PR just got merged, @MOHITKOURAV01 — thank you for contributing to Pollution Control Hub!

Your work is now part of the project. Here's what to do next:

  • ⭐ If you haven't already, consider giving the repo a star — it helps us grow.
  • 📢 Share your contribution on LinkedIn, Twitter, or wherever you hang out. You shipped open source!
  • 🔍 Browse other open issues if you want to keep contributing.

We really appreciate you taking the time. See you in the next PR! 🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ECSoC26-L3 ECSoC26 Contributions considered under ECSoC'26 good-pr Additional ECSoC label

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Alert History re-records the same warnings on every page load, and is never recorded without the Notification API

2 participants