You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
chore(frontend): low correctness findings from the 2026-09-02 audit #317
Three low-severity frontend correctness findings outside the RI-exchange module: the cross-tab sign-out listener ignores the localStorage.clear() event shape, provider and payment display tables are indexed by API strings without an own-property check (the same in/prototype-chain shape that LeanerCloud/cloud-commitments-cli#1854 fixed for tab routing), and the YTD Savings tile's sparkline plots the trend-range window rather than year-to-date.
Each item is independently actionable; they are grouped so one sitting can clear them.
Findings
Cross-tab sign-out misses a localStorage.clear() (A11-023)
Location: frontend/src/api/client.ts:95 (listener at :94-105, module comment at :88-92) at 3c0f8ac94048a2c36fce5ccddee54e6c4849a5cd
Failure scenario: The storage listener returns early when e.key is falsy. Per the StorageEvent spec, localStorage.clear() fires a storage event with key === null, which is exactly the everything-was-wiped case. A tab that clears storage (a browser privacy control, a devtools clear, or any future code path using clear()) leaves sibling tabs holding their in-memory authToken and rendering a logged-in view against a session the user believes they ended. The module comment presents this listener as the cross-tab sign-out mitigation.
Suggested fix: Treat e.key === null as a clear-all and run the same reset-and-reload path.
Provider display tables are indexed by an API string without an own-property check (A12-048)
Location: frontend/src/inventory.ts:430 (PROVIDER_DISPLAY_NAMES[section.provider]); frontend/src/recommendations.ts:1141 (PAYMENT_ORDER[a.payment ?? '']) and :2881 (PAYMENT_DISPLAY_LABELS[payment]) at 3c0f8ac94048a2c36fce5ccddee54e6c4849a5cd
Note: PLAUSIBLE only; no known API response supplies such a provider value.
Failure scenario: The lookups walk the prototype chain, so a section whose provider is "constructor" resolves to the Object constructor function rather than falling through to the ?? branch, and title.textContent renders the function's source into the card heading. Verdict is PLAUSIBLE, not confirmed: it needs the coverage API to return a provider literally named constructor, which could not be established from source. The same shape reached production via tab routing in LeanerCloud/cloud-commitments-cli#1855 before fix(frontend,server): resolve every route to its own page on direct load and refresh cloud-commitments-cli#1854 fixed it.
Suggested fix: Guard each lookup with Object.prototype.hasOwnProperty.call(TABLE, key), or build the tables with Object.create(null).
The "YTD Savings" tile's sparkline plots the trend-range window, not year-to-date (A12-073)
Location: frontend/src/dashboard.ts:1057 (attachSparkline('ytd', ...)); tile value from data.ytd_savings at :308; window from savingsTrendRange at :1022-1031, mutated by the range buttons at :1174-1177 at 3c0f8ac94048a2c36fce5ccddee54e6c4849a5cd
Failure scenario: The tile's number comes from data.ytd_savings, but the sparkline beneath it comes from loadSavingsTrendChart's data points, whose window is whatever the trend range toggle is set to (7, 30, 90 days or rolling 365). Clicking the "7" range button changes the shape of the line under a label that says YTD, so the number and the line describe different periods.
Suggested fix: Fetch a separate year-to-date series for the tile, or relabel the sparkline so it is not read as YTD.
Found by the 2026-09-02 codebase audit, findings A11-023, A12-048, A12-073, reported by one reviewer and independently confirmed by a second. Full report: docs/audits/codebase-audit-2026-09-02.md.
Summary
Three low-severity frontend correctness findings outside the RI-exchange module: the cross-tab sign-out listener ignores the
localStorage.clear()event shape, provider and payment display tables are indexed by API strings without an own-property check (the samein/prototype-chain shape that LeanerCloud/cloud-commitments-cli#1854 fixed for tab routing), and the YTD Savings tile's sparkline plots the trend-range window rather than year-to-date.Each item is independently actionable; they are grouped so one sitting can clear them.
Findings
Cross-tab sign-out misses a localStorage.clear() (
A11-023)frontend/src/api/client.ts:95(listener at:94-105, module comment at:88-92) at 3c0f8ac94048a2c36fce5ccddee54e6c4849a5cde.keyis falsy. Per the StorageEvent spec,localStorage.clear()fires astorageevent withkey === null, which is exactly the everything-was-wiped case. A tab that clears storage (a browser privacy control, a devtools clear, or any future code path usingclear()) leaves sibling tabs holding their in-memoryauthTokenand rendering a logged-in view against a session the user believes they ended. The module comment presents this listener as the cross-tab sign-out mitigation.e.key === nullas a clear-all and run the same reset-and-reload path.Provider display tables are indexed by an API string without an own-property check (
A12-048)frontend/src/inventory.ts:430(PROVIDER_DISPLAY_NAMES[section.provider]);frontend/src/recommendations.ts:1141(PAYMENT_ORDER[a.payment ?? '']) and:2881(PAYMENT_DISPLAY_LABELS[payment]) at 3c0f8ac94048a2c36fce5ccddee54e6c4849a5cdprovideris"constructor"resolves to theObjectconstructor function rather than falling through to the??branch, andtitle.textContentrenders the function's source into the card heading. Verdict is PLAUSIBLE, not confirmed: it needs the coverage API to return a provider literally namedconstructor, which could not be established from source. The same shape reached production via tab routing in LeanerCloud/cloud-commitments-cli#1855 before fix(frontend,server): resolve every route to its own page on direct load and refresh cloud-commitments-cli#1854 fixed it.Object.prototype.hasOwnProperty.call(TABLE, key), or build the tables withObject.create(null).The "YTD Savings" tile's sparkline plots the trend-range window, not year-to-date (
A12-073)frontend/src/dashboard.ts:1057(attachSparkline('ytd', ...)); tile value fromdata.ytd_savingsat:308; window fromsavingsTrendRangeat:1022-1031, mutated by the range buttons at:1174-1177at 3c0f8ac94048a2c36fce5ccddee54e6c4849a5cddata.ytd_savings, but the sparkline beneath it comes fromloadSavingsTrendChart's data points, whose window is whatever the trend range toggle is set to (7, 30, 90 days or rolling 365). Clicking the "7" range button changes the shape of the line under a label that says YTD, so the number and the line describe different periods.Found by the 2026-09-02 codebase audit, findings
A11-023,A12-048,A12-073, reported by one reviewer and independently confirmed by a second. Full report:docs/audits/codebase-audit-2026-09-02.md.