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

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.

managerand 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-actionsBot 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 →

) => {
// 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 devAug 19, 2026
2 checks passed
manager added a commit that referenced this pull request Aug 19, 2026
Sign up for freeto 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

@manager@MaryWylde
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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

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.

managerand 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-actionsBot 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 →

) => {
// 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 devAug 19, 2026
2 checks passed
manager added a commit that referenced this pull request Aug 19, 2026
Sign up for freeto 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

@manager@MaryWylde
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

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.

managerand 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-actionsBot 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 →

) => {
// 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 devAug 19, 2026
2 checks passed
manager added a commit that referenced this pull request Aug 19, 2026
Sign up for freeto 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

@manager@MaryWylde
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

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.

managerand 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-actionsBot 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 →

) => {
// 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 devAug 19, 2026
2 checks passed
manager added a commit that referenced this pull request Aug 19, 2026
Sign up for freeto 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

@manager@MaryWylde
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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

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.

managerand 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-actionsBot 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 →

) => {
// 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 devAug 19, 2026
2 checks passed
manager added a commit that referenced this pull request Aug 19, 2026
Sign up for freeto 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

@manager@MaryWylde
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

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.

managerand 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-actionsBot 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 →

) => {
// 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 devAug 19, 2026
2 checks passed
manager added a commit that referenced this pull request Aug 19, 2026
Sign up for freeto 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

@manager@MaryWylde
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

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.

managerand 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-actionsBot 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 →

) => {
// 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 devAug 19, 2026
2 checks passed
manager added a commit that referenced this pull request Aug 19, 2026
Sign up for freeto 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

@manager@MaryWylde
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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

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.

managerand 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-actionsBot 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 →

) => {
// 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 devAug 19, 2026
2 checks passed
manager added a commit that referenced this pull request Aug 19, 2026
Sign up for freeto 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

@manager@MaryWylde