Skip to content

chore(frontend): low correctness findings from the 2026-09-02 audit #317

Description

@cristim

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 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.
    • Evidence:
      window.addEventListener('storage', (e: StorageEvent) => {
        if (!e.key || !(STORAGE_KEYS as readonly string[]).includes(e.key)) return;
        if (e.newValue !== null) return;
        authToken = ''; apiKey = ''; csrfToken = '';
    • 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.
    • Evidence:
      const providerLabel = PROVIDER_DISPLAY_NAMES[section.provider] ?? section.provider.toUpperCase();
    • 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.
    • Evidence:
      attachSparkline('ytd', points.map((p: SavingsDataPoint) => p.cumulative_savings || 0));
    • 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.

No activity

Activity on this issue will appear here.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions