Skip to content

Feat/uxcore cybersecurity#145

Merged
MaryWylde merged 9 commits into
devfrom
feat/uxcore-cybersecurity
Jun 15, 2026
Merged

Feat/uxcore cybersecurity#145
MaryWylde merged 9 commits into
devfrom
feat/uxcore-cybersecurity

Conversation

@manager

@manager manager commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Description

Briefly describe the changes.

🔗 Related Issue

Closes #123 or relates to #456

*(Optional: You can also open a pull request directly from an issue — GitHub will auto-link it.)

✅ Checklist

  • My code follows the project's coding style
  • I have linked the relevant issue in the PR description
  • I have manually tested the changes
  • All CI checks have passed

manager and others added 9 commits June 2, 2026 16:28
The per-navigation "you're on X" landing line fires a paid Claude call
on every page hop, even when the pill is closed and the visitor never
interacts — so cost scaled with raw visitor count. Now the organic
greeting only spends when the panel is open AND the visitor has already
engaged (asked a question / clicked a card / picked a suggestion) this
session. Curated landings stay local/free; card-click landings already
imply both signals and are unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…cache

Tighten the organic greeting cost gate per product decision:
- Engagement is now set ONLY on a manually typed message. Card and
  suggestion clicks no longer count — clicking existing buttons is
  navigation, not a conversation, and must not incur paid greetings.
- Engagement expires after 30 min of no typed input (timestamp + TTL),
  so a long-idle tab starts neutral again.
- Never pay twice for the same page in a session: the organic greeting
  is cached per canonical path; revisits and back/forth are free.

Curated (local) landings and the chat answer path are unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…+ bot filter

Remember the widget's open/closed panel in sessionStorage so it follows the
visitor across page loads (incl. hard reloads into UX Core), instead of always
booting closed. Switch the paid organic-greeting gate from "typed within 30 min"
to simply "panel open" — opening the pill is a deliberate human gesture — and
add a known-bot user-agent backstop on /api/concierge-landing so we never pay a
crawler that reaches the route.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…c case rework

- bias modal on mobile is a full-height bottom sheet (dvh-safe on iOS), header icons aligned, vertical use-case switcher
- restore swipe left/right bias navigation directly in UXCoreModal (legacy UXCoreModalMobile stopped rendering after repo merge)
- rating block moved inside the scrollable body instead of a sticky footer
- Ask widget pill: dark surface in dark theme, no pulse on touch devices
- offsec cases #2-#4 reworked for distinct scenarios; left/right wording replaced with first/second for stacked mobile layout
…e, bias popup, route loaders

Revive the orphaned uxcore useMobile hook (dead since the UXCoreOSS fold-in,
so phones got desktop layout) and pin slug pages to the new modals. UXCG
question modal goes fullscreen on mobile with sticky rating row and nav
pills; add theme toggle to its header; restyle the mobile bias popup as a
centered card with dark-theme support and same-tab bias links. Route
loading overlay on UXCG/Table navigation, tooltip viewport clamp, dark
widget pill, UXCAT guest-profile clipping fixes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Page-element highlighting fired on every new answer regardless of widget
state, so a returning visitor with a collapsed pill saw host elements glow
with no obvious cause. Highlight is now allowed only while the panel is
open (active); collapsing clears it, reopening re-applies it. Same path on
mobile and desktop.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The "how useful…" NPS row sat outside the scrollable modal body and stayed
pinned above the nav controls. Moved it inside the body so it scrolls with
the answer; only Previous/Next stay sticky at the bottom.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The Cybersecurity (OffSec) layer is still work-in-progress. Surface its
switcher and view only when NEXT_PUBLIC_ENV is dev, and treat a persisted
isOffsecView flag as inactive off-dev so a prior dev session can't leak the
half-baked layer onto staging or prod. Building continues on dev as before.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@MaryWylde

Copy link
Copy Markdown
Contributor

@claude, review this PR.

@claude

claude Bot commented Jun 15, 2026

Copy link
Copy Markdown

Claude finished @MaryWylde's task in 3m 12s —— View job


PR Review: Feat/uxcore cybersecurity

  • Gather context and read PR diff
  • Review new components (RouteLoadingOverlay, MobileView refactor)
  • Review OffSec bias data (mereExposureEffect + existing bias updates)
  • Review UXCoreModal / UXCGModal changes
  • Review widget changes
  • Review API / page-level changes
  • Post final review

39 files, +1112/−368 lines across 5 commits. The core change is adding a Mere Exposure Effect OffSec scenario, retiring the mobile/desktop modal split in UX Core, and introducing a RouteLoadingOverlay component that ships route-transition feedback to both UXCoreModal and UXCGModal. Overall solid — a few things worth fixing before merge.


New OffSec content: mereExposureEffect.ts

Content is well-written, follows the established OffsecBiasContent schema, and is registered correctly in biasOffsec/index.ts with the slug 'mere-exposure-effect'. The scenario (a chat rapport-building attack) is realistic and the defense moves are actionable. No fabricated figures or slugs — clean.

One minor note: the comment at lines 1–8 contains a reference to project memory (feedback_offsec_no_mocked_numbers). That's an agent-internal artifact — it won't affect users but shouldn't live in committed source.

Fix this →


RouteLoadingOverlay — new component

RouteLoadingOverlay.tsx is clean and well-structured. The useRouteLoading hook correctly subscribes to routeChangeComplete / routeChangeError on mount. The portal guards against SSR (typeof document === 'undefined'). The SCSS uses 100dvh-aware patterns and includes dark theme support.

One issue: The component is placed in src/uxcore/components/RouteLoadingOverlay/ which is fine, but the barrel file (index.ts) exports both the default and the named useRouteLoading:

// index.ts
import RouteLoadingOverlay, { useRouteLoading } from './RouteLoadingOverlay';
export { useRouteLoading };
export default RouteLoadingOverlay;

Per AGENTS.md: "index.ts contains: import X from './X'; export default X;" — named re-exports through index.ts barrels are acceptable when the hook is consumed together with the component (and indeed UXCGModal.tsx imports both via a single import). This pattern is defensible here, but worth noting it diverges from the pure-default-export barrel rule.


UXCoreModal — mobile/desktop unification

Good call retiring UXCoreModalMobile and consolidating into UXCoreModal. The [slug].tsx cleanup is clean — 40 lines of branching gone.

Issue — two loading patterns in one PR:

UXCoreModal introduces its own isClosing state + ClosingOverlay div:

// UXCoreModal.tsx:79, 139, 400
const [isClosing, setIsClosing] = useState(false);
const handleClose = useCallback(() => {
  setIsClosing(true);
  onClose();
}, [onClose]);
// ...
{isClosing && <div className={styles.ClosingOverlay} ... />}

UXCGModal uses the new useRouteLoading hook:

// UXCGModal.tsx:96
const [isNavigating, startNavigation] = useRouteLoading();
// ...
<RouteLoadingOverlay active={isNavigating} />

These are two different implementations of the same UX. UXCoreModal also has an internal isLoading state (line 76) that's only set once on mount (useEffect(() => { data ? setIsLoading(false) : setIsLoading(true); }, []);) — it fires once and never resets, which means if data arrives after mount, loading will never clear. Fix this →

Also, UXCoreModal should ideally use useRouteLoading for isClosing (same as UXCGModal) for consistency. Not blocking, but worth a follow-up cleanup.


LanguageSwitcher — touch device fix

Adding isOpen state is the right fix — CSS hover doesn't fire on touch devices. The logic is correct.

Import order violation (LanguageSwitcher.tsx:1):

import { useClickOutside } from '@uxcore/hooks/useClickOutside';  // ← here
import cn from 'classnames';
import Image from 'next/image';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { FC, useState } from 'react';

The @uxcore/hooks/* alias falls under the @hooks group which must come after third-party imports (React, next/*, classnames). ESLint enforces this — it may be passing now because the hook was already there before the import reorder, but the order is wrong per the project's rules.

Fix this →


MobileHeader — podcast cleanup

Removing isPodcastOpen / setHeaderPodcastOpen / PodcastIcon props from MobileHeader is clean. The podcast toggle was always guarded by router.pathname === '/uxcore' and was apparently stale dead code. Good cleanup.


MobileView — bias tap loading

Adding isOpeningBias state with a full-screen overlay on bias tap is the right UX fix for slow connections. The effect correctly clears on routeChangeComplete and routeChangeError.

Minor: line 176 uses if (!!strapiBiases) — the double-negation is redundant, if (strapiBiases) suffices. Not blocking.


Widget — host highlight gating

// AskUxCore.tsx
if (!open) {
  lastFlashedTurnIdRef.current = null;
  return;
}

Correct fix — host-page element highlighting should be inactive when the widget panel is collapsed. Resetting lastFlashedTurnIdRef ensures the highlight re-fires on re-open. The [turns, open] dependency array is right.


concierge-landing.ts — bot filter

The BOT_UA_RE regex is a reasonable list. The function early-returns 200 (not 403) for bots, which is the right choice — no error, no signal, just silence. The fallback to OpenAI when Claude returns null is already in place.

No auth or rate-limiting on this endpoint — that's pre-existing, but worth tracking separately since it's a paid LLM call.


OffSec gating

// UXCoreModal.tsx:72
const offsecEnabled =
  (process.env.NEXT_PUBLIC_ENV || '').toLowerCase() === 'dev';

The comment explains the double-gate (env check + isOffsecView) to prevent a persisted isOffsecView=true from leaking to prod. This is a solid approach.


SCSS observations

  • height: 100dvh in UXCoreModal.module.scss — correct iOS Safari fix; 100vh includes the URL bar area. Good.
  • overscroll-behavior: contain on modal body scroll — prevents scroll chaining to the page behind. Correct.
  • Tab switcher overflow: hidden + adjusted border-radius and transform math (removing the ±1px fudges) — cleaner and more accurate.
  • The ::after divider between switcher items is a nice touch with z-index: -1 so it sits behind the active pill.

Dark theme variants in RouteLoadingOverlay.module.scss use :global(body.darkTheme) — consistent with the existing pattern across the codebase.


Summary

Area Status
New OffSec content (Mere Exposure Effect) ✅ Clean
RouteLoadingOverlay component ✅ Good
UXCoreModal mobile unification ✅ Good, minor issue
UXCGModal loading integration ✅ Good
LanguageSwitcher touch fix ✅ Logic correct, import order wrong
MobileView loading overlay ✅ Good
Widget host-highlight gate ✅ Correct fix
Bot filter on concierge-landing ✅ Good
OffSec env gating ✅ Solid

Must fix before merge:

  1. isLoading effect in UXCoreModal has empty deps [] — will not update if data arrives after mount
  2. Import order violation in LanguageSwitcher.tsx
  3. Agent memory reference in mereExposureEffect.ts comment

Nice-to-have:

  • Unify isClosing in UXCoreModal with useRouteLoading (same as UXCGModal)
  • Fill in the PR description (currently just the template placeholder)

@MaryWylde MaryWylde merged commit bba1db3 into dev Jun 15, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants