refactor(frontend): drop synchronous setState-in-effect at four of #176's six sites - #201
Merged
Merged
Conversation
…'s six sites Refactors findings 1, 2, 3 and 5 from #176 so none of them calls setState synchronously from an effect body. Behaviour is preserved at every site. - LoginPage: the oidc_error message is seeded by a lazy useState initializer; the effect keeps only the history.replaceState that strips the parameter. - OidcLinkCard: same treatment for the oidc_link / oidc_link_error banners, over a shared linkParams() helper. - NewScanPage: the scanner clamp moves out of an effect into the target-type handler, the one place that can invalidate the pairing. Deliberately not a derive-during-render, which would resurrect a displaced scanner choice instead of overwriting it. - ScanDetailPage: findingsLoading is derived from a settled-request key rather than stored, which also removes a flash of "No findings match the current filters" over a request still in flight. The per-scanId reset effect keeps its shape; its one uncompilable line becomes setFindingsSettledKey(null). Each site gains a test verified to fail against the pre-refactor version of its own file. The react-hooks/set-state-in-effect override in eslint.config.js is unchanged: findings 4 and 6 and the rule flip are a follow-up, per #176. See docs/ARCHIVE.md § Deviations for the full account, including why ScanDetailPage's void loadFindings() still reports after the refactor.
tyler-rich
added a commit
that referenced
this pull request
Aug 11, 2026
The §14 entry documented the policy change but not its result, since it was written before the PR was opened. Adds the outcome: the strip held on this PR's own body — footer found on creation, one PATCH, clean confirming re-read — which is the first success in this project's history. Bounds it on both sides rather than claiming more than it shows. The footer appeared on creation with #197's and #200's keys live and merged, the same result #201 got, so the settings demonstrably did not stop the append; whatever they may have contributed is confined to the re-append after the PATCH, and that is now an open and unfalsifiable question since the file is gone. The next PR is the harder test, running with no settings file at all. Also notes that #196's failures were on an issue comment while this success was on a PR body, so it does not refute #196.
tyler-rich
added a commit
that referenced
this pull request
Aug 11, 2026
…-attempt cap (#202) * docs: delete .claude/settings.json; reinstate footer strip with a one-attempt cap The committed project-scope settings file is removed in full — both the attribution block from #197 and includeGitInstructions from #200. Three sequential settings-layer attempts did not stop the attribution footer: #197 added the attribution keys, #200 added includeGitInstructions after #199 still showed a footer, and #201 ran with both merged and live and still got one on its PR body. The approach is abandoned, not paused. With no settings-level backstop left, the strip instruction returns to CLAUDE.md § Attribution and CONTRIBUTING.md § Pull request process — but capped at one PATCH per body, with a hard stop if the footer comes back. The cap prices #197's evidence rather than disputing it: each PATCH runs under a bot identity and leaves a permanent "claude (Bot)" entry in GitHub's public edit history, and #196 showed a second attempt only doubles that cost without changing the outcome. Commit-authorship verification is unchanged. The .gitignore comment that explained why the settings file was tracked is rewritten, and docs/ARCHIVE.md §14 records the deletion, the cap, and the fact that this is the fourth reversal of this policy. * docs(archive): record the strip's first outcome on #202's own body The §14 entry documented the policy change but not its result, since it was written before the PR was opened. Adds the outcome: the strip held on this PR's own body — footer found on creation, one PATCH, clean confirming re-read — which is the first success in this project's history. Bounds it on both sides rather than claiming more than it shows. The footer appeared on creation with #197's and #200's keys live and merged, the same result #201 got, so the settings demonstrably did not stop the append; whatever they may have contributed is confined to the re-append after the PATCH, and that is now an open and unfalsifiable question since the file is gone. The next PR is the harder test, running with no settings file at all. Also notes that #196's failures were on an issue comment while this success was on a PR body, so it does not refute #196.
tyler-rich
added a commit
that referenced
this pull request
Aug 11, 2026
Runs the CONTRIBUTING.md § Releasing "Before you tag" checklist on dev, ahead of the promotion PR. - Version 0.3.0 -> 0.3.1 in sixteen places across nine files (frontend/package-lock.json written with `npm version`, never by hand). - CHANGELOG [Unreleased] cut to [0.3.1] - 2026-08-11, with a fresh empty [Unreleased] above it and the compare links added and re-pointed. - Adds a ### Fixed section for the #176 work landed in #201/#203, removes the test-only ### Added entry (§14 already carries it in full), and corrects the step-3 set-state-in-effect claims that #203 superseded. - THIRD_PARTY_LICENSES re-verified byte-for-byte against upstream at the pinned Trivy/Grype/Syft tags; backend/requirements.lock confirmed drift-free; the Dependabot queue is empty. Issue #194 is a stale published image rather than a defect: its only gate finding is CVE-2026-69247 (cryptography 49.0.0), already closed on dev at 50.0.0 and verified in upstream's source. See docs/ARCHIVE.md § Deviations for the full record.
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.
Part 1 of #176. Refactors findings 1, 2, 3 and 5 so none of them calls
setStatesynchronously from an effect body, preserving behaviour at every site.
frontend/eslint.config.jsis untouched —react-hooks/set-state-in-effectstays'off',override and comment exactly as they were. Findings 4 and 6 and the removal of the override are a
follow-up; #176 stays open.
What replaced each site
pages/LoginPage.tsxoidc_errormessage is seeded by a lazyuseStateinitializer over a module-leveloidcErrorCode()helper. The effect keeps only thehistory.replaceStatethat strips the parameter — the part that genuinely touches something outside React.components/settings/OidcLinkCard.tsxoidc_link*banners, over a sharedlinkParams()helper. Both parameters are still honoured independently, including the unknown-code fallbacks.pages/NewScanPage.tsxchooseTargetTypehandler — the target-type control is the only thing that can invalidate the pairing, since the scanner picker only offers the current type's own scanners.pages/ScanDetailPage.tsxfindingsLoadingis derived from a settled-request key (findingsSettledKey !== findingsKey) instead of stored, andloadFindingssettles in both post-awaitbranches rather than afinally.Two choices worth calling out:
allowed.includes(scanner) ? scanner : allowed[0]would shadow the displaced choice rather than overwrite it, so leaving a target type and coming back would resurrect a scanner the user is no longer on. The clamp stays destructive, as it was, and a test pins that difference.succeededpreviously rendered an empty findings list with the loader already down — "No findings match the current filters" over a request that had not answered yet.The one line touched inside finding 4
Removing
findingsLoadingleavessetFindingsLoading(false)in the per-:scanIdreset effectuncompilable. It becomes
setFindingsSettledKey(null). The effect keeps its structure, comment andintent;
ScanDetailPage.scanIdReset.test.tsxis unedited and still passes. Nothing else in findings4 or 6 was changed, and
ScansPage.compare.test.tsxis untouched.Tests
Four new test files, 16 new tests. Each contains one assertion verified to fail against the
pre-refactor version of its own file (reverted with
git checkout HEAD --, re-run, restored):LoginPage.oidcError.test.tsxexpected false to be trueOidcLinkCard.callback.test.tsxexpected 2 to be 1NewScanPage.scannerClamp.test.tsx[1,1,…] to not include +0ScanDetailPage.findingsSpinner.test.tsx[Array(11)] to not include trueSince these are behaviour-preserving refactors, the biting assertion is necessarily about which
commit the correct value lands in — which is what the rule is about — while the surrounding
assertions pin the behaviour and pass against both versions. Per-commit observation uses
<Profiler onRender>; Testing Library'srenderflushes passive effects insideact, so a plainpost-render assertion cannot see the intermediate state at all.
Measurement with the rule temporarily enabled
18 findings before (matching #176's 6/12 split exactly), 15 after — not 14. Sites 1, 2 and 3 are
gone outright;
ScanDetailPage'svoid loadFindings()still reports.The genuine defect #176 named for finding 5 — a synchronous
setFindingsLoading(true)before thefirst
await— is gone. What remains is the analyser artifact the issue documents for the other 12:probing the installed 7.1.1 shows
void load()reports even whenload's onlysetStatefollows anawaitand there is notry/catch, and that adding atry/catchreports even when thecatchcalls no
setStateat all. The only shapes that go silent are ones where the compiler bails on thefunction. So finding 5's line has migrated into the out-of-scope fetch-on-mount population.
A consequence for the follow-up, recorded in the archive: #176 pairs "remove the override" with
"
npm run lintis clean", and those cannot both hold while the 12 fetch-on-mount findings report aterrorand are out of scope. That needs a decision the issue does not yet contain.Verification
npm run lintclean ·npm run format:checkclean ·tsc -bclean ·npm run buildclean ·npm test26 files / 96 tests passing (was 22/80 at #178).See
docs/ARCHIVE.md§ Deviations for the full account;docs/upgrades/frontend-toolchain-86.mdStep 3 now records that its held-back rule closes out intwo parts.