fix: stop Alert History re-recording the same warnings on every load (#694) - #699
Merged
Aditya8369 merged 2 commits intoAug 13, 2026
Merged
Conversation
…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.
|
@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. |
Thank You for Your Contribution! 🎉Hi @MOHITKOURAV01, Thank you for opening this Pull Request and contributing to our project. We truly appreciate your efforts.
The maintainer @Aditya8369 will review your PR shortly! Happy Contributing! 🚀 |
Owner
|
@MOHITKOURAV01 solve conflicts |
|
🎉 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:
We really appreciate you taking the time. See you in the next PR! 🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #694
The duplication
A ref is per-mount. On a fresh mount it is
"", so the first effect run always looked like a new alert — whilealertHistoryitself was loaded fromlocalStorage. 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:
MAX_HISTORYis 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 includescityName.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:Alert history in browsers that cannot notify
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
Notificationconstructor, 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:
permissioninitialised to"denied"when the API was missing, which renderedto visitors on a device with no such setting. It is
"unsupported"now, and that state renders nothing.Smaller defects in the same code
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.setIteminside asetStateupdater — impure, so StrictMode ran it twice, and a quota error threw from inside a React state update. It now runs beforesetAlertHistory, the same correction fix: derive leaderboard stats from recorded activity instead of a seed (#671) #676 made toLeaderboard.current.us_aqiread in the effect while the dependency array usedcurrent?.us_aqi. The effect runs before theif (!current) return nullguard below it; it was only saved bywarningsbeing empty. Both are optional now.Structure
buildWarningsand the recording rules move tosrc/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 ascontributionStatsandcheckInStreak.recordAlertsreturns{ 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 asnullrather 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.Suite status
612 passed, 2 failed. The two failures are the pre-existingcacheStore/useSWRones from #690, present onmainbefore this branch and fixed by #695.Migration
Nothing to migrate. Entries written by the old version have no
at,idorsignature; 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.