Skip to content

fix: UX Core functional audit fixes + OffSec bias cases - #181

Merged
manager merged 8 commits into
devfrom
feat/fable-bugfix-aug19
Aug 19, 2026
Merged

fix: UX Core functional audit fixes + OffSec bias cases#181
manager merged 8 commits into
devfrom
feat/fable-bugfix-aug19

Conversation

@manager

@manager manager commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

What this does

Functional audit fixes across UX Core, plus new OffSec bias content, gated to dev preview.

Fixes

  • Bias page sync: modal state follows the route prop instead of freezing at mount — back/forward lands on the right bias, no more stuck dimmed modal.
  • Use-case switch inside the modal is a shallow, no-scroll hash update (was a full ISR refetch + jump to top); OffSec state writes #offsec instead of mislabeling the URL as #hr.
  • Share previews: absolute og/twitter image fallback (scrapers drop relative URLs), fixed double .OGTags nesting on the Strapi path, twitter:url is the canonical URL, share links URL-encoded so hashes and ampersands in titles survive.
  • Strapi cache: getStrapiBiases gets a 5-minute TTL instead of a permanent module cache, and never caches an empty/incomplete response — a transient Strapi failure no longer 404s every bias page until restart, and ISR picks up content edits.
  • Rating integrity: thanks-state only after the vote lands, double-click guard, response.ok check, failed geo lookup no longer aborts the vote.
  • Search: zero-hit state renders as empty instead of silently falling back to the full library while claiming "found 0"; clearing the input cancels the in-flight debounce; getSearchLabels takes the locale, so EN plurals and RU declensions actually render.
  • Resilience: mentionedQuestionsIds parse failure degrades to no mentions instead of crashing the bias page.
  • State init: initUseUXCoreGlobals had zero callers (every persisted PM/HR choice was dead weight, each reload reset to PM); now idempotent and wired on /uxcore mount, with an explicit URL hash winning over restored state.

OffSec

  • 10 new OffSec bias cases and 4 card surfaces (call, document, poster, timeline), content rewritten in plain language with fewer email surfaces.
  • Gated to dev preview only: switcher, row, view and #offsec hash handler behind one shared isOffsecEnabled helper; a persisted isOffsecView flag is treated as inactive off-dev, so a prior dev session cannot leak the half-finished layer onto staging or prod.

Housekeeping

  • Machine-readable font passport added to CLAUDE.md (commit gate requirement).

Notes for review

  • The branch is behind dev; merge or rebase before landing.
  • OffSec is intentionally invisible outside dev preview — verifying it on staging requires the dev env flag.

manager and others added 7 commits July 16, 2026 20:40
…ter, timeline)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ail surfaces

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Cybersecurity row and the #offsec hash handler in UXCoreLayout were
ungated, so merging would have leaked the dev-only OffSec layer to prod
in a half-broken state (row active, modal showing HR). The gate is back
as a single shared isOffsecEnabled helper used by layout and modal.

initUseUXCoreGlobals had zero callers: every localStorage write of the
PM/HR choice was dead weight and each reload reset to PM. It is now
idempotent (absolute values, safe on every mount), gates the OffSec flag
on isOffsecEnabled, and runs on /uxcore mount with an explicit URL hash
winning over the restored state.

Also adds the machine-readable font passport to CLAUDE.md (commit gate
requirement).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… cache TTL

- Back/Forward now lands on the right bias: modal state follows the route
  prop instead of freezing at mount, so the stuck-dimmed modal is gone.
- Use-case switch inside the modal is a shallow no-scroll hash update
  (was a full ISR refetch + jump to top), and OffSec state writes #offsec
  instead of mislabeling the URL as #hr.
- Share previews: og:image/twitter:image static fallback is absolute
  (scrapers drop relative URLs), the Strapi ogImage path lost its bogus
  double .OGTags nesting, twitter:url is the canonical URL (was a
  hardcoded domain with a double slash), and social share links are
  URL-encoded so hashes/ampersands in titles survive.
- getStrapiBiases: 5-minute TTL instead of a permanent module cache, and
  an empty/incomplete response is never cached — a transient Strapi
  failure no longer 404s all bias pages until restart, and ISR
  revalidation actually picks up content edits.
- Rating: thanks-state only after the vote lands (failed vote stays
  retryable), double-click guard, response.ok check, and a failed geo
  lookup no longer aborts the vote.
- mentionedQuestionsIds parse failure degrades to no mentions instead of
  crashing the bias page.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- A search with zero hits now renders as empty (labels faded, map
  filtered to nothing) instead of silently falling back to the full
  library while claiming 'found 0'. useBiasSearch carries an explicit
  isSearchActive flag so empty-query and zero-hit are distinguishable.
- Clearing the input cancels the in-flight debounce, so a pending search
  can no longer repopulate results over a cleared field.
- getSearchLabels indexed a locale-keyed object numerically and always
  returned undefined: EN plural 's' and RU declensions never rendered.
  It now takes the locale.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- addPersona/updatePersona/deletePersona built auth headers at module
  load, so logging in after page load kept sending 'Bearer null'.
  Headers are now resolved per request, and non-2xx responses throw.
- Save/overwrite no longer flip to 'saved' optimistically: the state is
  confirmed only after the request lands, and a failure surfaces the new
  localized saveFailed message via the existing error slot.
- A failed delete keeps the persona in the list instead of hiding an
  entry the server still has.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… server

- A dropped submit no longer advances the test: the answer is confirmed
  by the server before moving on. On failure the question stays with a
  localized retry message and the button re-enables, so a network blip
  means a retry instead of a wrong score or a test finished with
  unrecorded answers.
- Double submits closed: an in-flight ref guards clicks, and Enter now
  mirrors the Next button's disabled state instead of firing the same
  question twice.
- Resuming a test uses the server's ongoingTest.isFinal instead of a
  localStorage flag, so a 30-question final resumed in another browser
  (or after cleared storage) keeps its real length instead of ending at
  question 10 with negative numbering.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@MaryWylde

Copy link
Copy Markdown
Contributor

@claude review this PR

# Conflicts:
#	src/uxcore/api/biases.ts
#	src/uxcore/components/UXCoreModal/UXCoreModal.tsx
#	src/uxcore/hooks/useUXCoreGlobals.ts
#	src/uxcore/layouts/UXCoreLayout/UXCoreLayout.tsx
#	src/uxcore/lib/offsec.ts
@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown

Claude finished @manager's task in 4m 36s —— View job


Reviewing PR #181

  • Gather context (PR body, diff, changed files)
  • Review correctness & React best practices
  • Review TypeScript quality
  • Check project conventions (AGENTS.md)
  • Accessibility & UX
  • Security
  • Styling / SCSS hygiene
  • Post inline comments for specific issues
  • Post final summary

Summary

Reviewed against origin/dev (45 files, +1623/-364). The fixes are careful and well-scoped — each one matches a real bug with a clear root cause, and several show good defensive instinct:

  • src/pages/uxcore/[slug].tsx: modal-sync effect now depends on currentActiveBias.number instead of running mount-only — correctly fixes the frozen-modal-on-back/forward bug. The use-case hash effect switched to shallow: true, scroll: false, and mentionedQuestionsIds parsing is now wrapped in try/catch via useMemo. All verified correct.
  • src/uxcore/api/biases.ts: TTL cache with a completeness check (en/ru length ≥ expected) before overwriting the cache is a solid pattern — a transient Strapi hiccup falls back to the last good cache instead of poisoning it. Confirmed hy is correctly excluded from the completeness check since it's a partial override set.
  • src/uxcore/api/personas.ts / UXCPLayout.tsx / SavedPersonas.tsx: per-call getAuthHeaders() fixes the stale-token-at-mount bug; addPersona/updatePersona/deletePersona now throw on !response.ok instead of silently returning undefined, and every call site was updated to .catch() and surface saveFailed (translated in en/ru, hy correctly omitted per the repo's fallback-to-English convention) instead of optimistically flipping to "saved". Traced this through — no call site was missed.
  • src/uxcore/hooks/useBiasSearch.ts: isSearchActive distinguishes "no query" from "query with zero hits" — checked every real caller of the shared setSearchResults (Search.tsx ×2) and both pass the flag explicitly; the other setSearchResults call sites in UXCGLayout.tsx/BiasSearch.tsx are unrelated local useState, not this hook, so the default-parameter fallback isn't reachable with a wrong value in practice.
  • OngoingLayout.tsx: the isSubmittingRef double-submit guard plus submittedAnswer returning false on a dropped request (verified against getUXCatSubmitTest, which does return null on a non-ok response) correctly prevents advancing the test on a lost answer.
  • OffSec gating: isOffsecEnabled now checks NEXT_PUBLIC_ENV === 'dev' (was !== 'prod', i.e. previously live on staging too) and initUseUXCoreGlobals forces isOffsecView false whenever the flag is off — so a persisted dev-session flag can't leak the layer onto staging/prod. Gate is applied consistently at the toggle, init, and hash-handler layers.

Findings (posted inline)

  1. Convention — OffsecBiasView.tsx: builds className via raw template-string concatenation instead of the classnames (cn) helper AGENTS.md requires for conditional classes (lines ~234–236, 299, 310–317). Mechanical fix.
  2. TypeScript — src/uxcore/api/rating.ts:8: let userData: any = {} — an explicit any for a value with only 4 known fields (country, region, city, ip).
  3. Styling (minor) — OngoingLayout.module.scss:257: new .submitError rule uses a fresh hardcoded #fb1717 rather than a design token; the file already has legacy hardcoded hex, so this is low severity but worth aligning to keepsimple-style for new code.
  4. Flag, not blocking — src/uxcore/data/biasOffsec/types.ts: adds 4 new bias-card kinds and 10 new OffSec bias entries. This is local static content behind the dev-only gate, not the canonical Strapi UX Core corpus, but it's still bias-schema/content and the repo's "UX Core data is canonical" policy asks for explicit approval on such changes — flagging so that's confirmed rather than waved through.

No security, hydration, or SSR issues found (all localStorage/window access is properly effect-gated behind mounted/typeof window checks). No App Router, Tailwind, or state-library violations. Import ordering and barrel patterns look compliant throughout.

Note: as the PR description says, the branch is behind dev — the commit list includes unrelated library/widget history from merges; the diff above is scoped to git diff origin/dev...HEAD so it only reflects this branch's actual changes. Rebase/merge before landing as planned.

{card.items.map((item, i) => (
<div
key={i}
className={`${styles.timelineItem} ${

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Convention: this component builds className with raw template-string concatenation instead of the classnames helper (cn), which AGENTS.md requires for conditional classes ("Always use classnames (imported as cn)"). Applies here and at lines 299, 310–313, and 315.

className={cn(styles.timelineItem, { [styles.timelineItemFlagged]: item.flagged })}

Fix this →

Comment thread src/uxcore/api/rating.ts
) => {
// Geo enrichment is best-effort: a failed /api/user lookup must not
// abort the vote itself.
let userData: any = {};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let userData: any = {} reintroduces an explicit any. Since only country, region, city, ip are destructured from it, a small inline type (or a shared TUserGeo type matching /api/user's response) would keep this typed without much overhead.


.submitError {
text-align: center;
color: #fb1717;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New hardcoded color #fb1717 for the submit-error text — this file already carries legacy hardcoded hex values, but per AGENTS.md "new code aligns to the keepsimple-style skill" / "Do not invent colors". Worth pulling from the design tokens (or an existing error/crimson variable, e.g. the $ks-crimson family used in OffsecBiasView.module.scss) instead of a fresh literal.

// the lever is the pattern itself (frequency illusion), not any single
// artifact.
export interface OffsecBiasTimelineCard extends OffsecBiasCardCommon {
kind: 'timeline';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flagging per repo policy rather than blocking: this extends the OffSec bias-card schema with four new kinds (call, document, poster, timeline) and 10 new bias entries under src/uxcore/data/biasOffsec/. This is local static content (not the canonical Strapi UX Core corpus that CLAUDE.md's "UX Core data is canonical" section governs), and it's correctly gated behind isOffsecEnabled (dev-only) — but since it's still bias-schema content, please confirm this had the explicit sign-off the repo convention calls out for bias data/schema changes before merging to dev.

@manager
manager merged commit c46316c into dev Aug 19, 2026
2 checks passed
manager added a commit that referenced this pull request Aug 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants