Start Practice when session storage is blocked
Problem and behavior
Where site data is blocked, reading the sessionStorage global throws. The lobby's Start handler named the global in the arguments of its storage helpers, so the exception came before any helper could handle it. Its message landed in #grounding-error, inside a collapsed <details>, and Start looked like it did nothing.
After this change, Practice without grounding opens the interview with storage unreachable. With grounding selected, Practice shows the existing "Clear grounding to start normally" message, since the packet can only reach the interview page through storage. When Practice starts without grounding, a ticked "share this focus" choice is not carried: storeSharedFocus() already treats a failed write as an unshared focus, and a refusal would land in the collapsed section like the original bug. Strict still stays on the lobby because it cannot run without storage, and #grounding-error now holds storageError instead of the raw exception text.
A real browser block also refuses the session cookie. On a server that admits guests, Practice reaches the interview. On a server that requires sign-in, with nobody signed in and session storage unreachable, Start now stays on the lobby with its choices kept, says in #account-status: "Signing in needs site data, which this browser is blocking for this site. Allow site data for this site, then reload this page.", and moves focus to the sign-in field so the message is on screen at narrow widths. Without that check, the storage fix alone would send the candidate to the interview page, which finds nobody signed in and bounces back to a reset lobby. The check assumes that blocked session storage means blocked cookies. Where only storage is blocked and cookies still work (Firefox's dom.storage.enabled=false may produce this; I have not checked), Start refuses a candidate who could sign in; the "Use GitHub" button still signs them in.
That check needs the session state, which is unknown until /api/session answers. With session storage unreachable, Start is held while the answer is pending, without the site-data message since the server may still admit guests, and is released when it arrives. If the check fails with no login attempted, the lobby goes to the existing account retry state instead of "Signed out" with Start enabled. The cost: with storage blocked and the check failing, a guest also waits for a successful retry before Start.
A storage global named directly under web/ now fails ESLint, with one exemption described below.
Limits, not changed here:
Sign-in itself is unchanged. Under a full block, "Use GitHub" and Enter in the sign-in field still call /api/login and report "Try signing in again", which cannot work there and replaces the site-data message.
From reading the code, once Start reaches the interview with storage unreachable, some existing choices do not survive, as before for any page without storage: the "Show the LeetCode problem" tick, lobby settings after returning, candidate cases on reload, and in-interview preferences. The grounding refusal is also hidden if the candidate closed the section again before pressing Start.
Strict visibility: the storageError text stays hidden while the <details> is collapsed. Where to show a storage failure is an open question in #600, so this change leaves the <details> as it is.
web/peer-lobby.js is exempt from the rule until its own fix; I can open a separate issue for it if you agree. It holds five of the 18 references #600 lists at 4de45b1. Opened from an invite link with storage blocked, the page throws at module scope and stays on "Checking account..."; signed out without an invite, it shows the raw exception text. A mechanical conversion would pass the rule and still throw on null.
Approach
The first commit passes storageArea("sessionStorage"), added in d5ad668, at the four web/app.js call sites; the helpers already handle the null it returns. It also adds the sign-in check above and the hold and retry routing for an unknown session state, all gated on session storage being unreachable. Two existing tests in tests/browser/account.test.js assert the literal call text, so they are updated, and tests/browser/lobby-render.test.js pinned the old failed-check state, so its expected values now describe the retry state. New lobby tests cover Start with storage unreachable in Practice, in Practice with grounding selected, in Practice with a shared focus ticked, in Strict, and on a server that requires sign-in (at a 375-pixel width), while the session check is pending, and after it fails. They make sessionStorage throw while cookies and localStorage still work, a partial block rather than a real browser setting. Most of the commit's size is these tests.
The second commit adds no-restricted-globals and no-restricted-properties for both storage names and converts the seven references in web/history.js. They go through a small deviceStorage() helper that throws when no store can be reached, so each caller's catch still reports the failure and their results do not change. A new test checks that deleteReport still answers "failed" and "account-deleted-local-failed" with storage unreachable. The storageArea() doc comment in web/audio-output.js now says that a caller that must tell a failure from an empty store has to check for null itself, which is what deviceStorage() does.
web/history.js imports storageArea from ./audio-output.js rather than a new module: both pages that use it already load that file, and moving the helpers is a separate refactor. The rules apply to web/**/*.js only; the review workbench is a local developer tool that does not import audio-output.js.
Validation
Each new Start test fails without its part of the change and passes with it. The deleteReport test fails without deviceStorage() and passes with it. npx eslint . passes, and reintroducing the module-scope read d5ad668 removed fails it at web/interview.js:477. A script also ran Start in Chromium with its real block-site-data setting: on a server that requires sign-in it stays on the lobby with the message above, and after allowing site data the open page still threw until it was reloaded, which is why the message asks for a reload.
Locally, review-memory-tests, shell-syntax and interview-scroll.test.js fail, and each fails the same way on main on this machine. local-recorder.test.js failed once in a full browser lane and passed six of six runs alone, on this branch and on main; this change does not touch the files it covers. The interactive-review check fails at its checkout step before measuring anything, as on #605 and other pull requests from forks.
Not checked: Firefox, keyboard and screen-reader behavior on the refusal (pressing Start again rewrites the same text, so a repeat may not be announced), the hold and retry states under a real browser block, and the real interview page under a real block on a server that admits guests.
Close #600