From 42b4173fdaaf1ba2cd181f3c63ad0d1963ec9bc9 Mon Sep 17 00:00:00 2001 From: alex-dembele Date: Fri, 2 Oct 2026 13:23:34 +0100 Subject: [PATCH 1/5] test(i18n): write the formatter's invisible characters as escapes (#627) Three regex classes held literal U+00A0, U+202F, U+200E and U+200F, the spaces and direction marks French number formatting emits. They are intentional, but no-irregular-whitespace flagged them and the ratchet failed on a clean master. Written as \u escapes, the regexes match exactly what they matched before; the characters are not replaced with plain spaces, and a reader can now see which ones are meant. Signed-off-by: alex-dembele --- frontend/e2e/empty-states.spec.ts | 2 +- frontend/src/i18n/__tests__/format.test.ts | 2 +- frontend/src/i18n/__tests__/translate.test.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/e2e/empty-states.spec.ts b/frontend/e2e/empty-states.spec.ts index 5b5594ce..cbee8330 100644 --- a/frontend/e2e/empty-states.spec.ts +++ b/frontend/e2e/empty-states.spec.ts @@ -164,7 +164,7 @@ async function assertNoNonZeroMetrics(page: Page, where: string) { if (!text) continue; // Strip thousands separators (space, NBSP, narrow NBSP, comma) so "1 200" // is read as 1200 rather than as 1 and 200. - const digits = text.replace(/[\s  ,]/g, ''); + const digits = text.replace(/[\s\u00a0\u202f,]/g, ''); // "N/M" ratios — "0/4 étapes", "0/12 risques chiffrés". Only the numerator // is tenant data; the denominator is a total (onboarding steps, registered diff --git a/frontend/src/i18n/__tests__/format.test.ts b/frontend/src/i18n/__tests__/format.test.ts index c3e57113..ab22811d 100644 --- a/frontend/src/i18n/__tests__/format.test.ts +++ b/frontend/src/i18n/__tests__/format.test.ts @@ -14,7 +14,7 @@ import { } from '../format'; /** Intl inserts narrow/non-breaking spaces; compare on the digits and marks. */ -const norm = (s: string) => s.replace(/[  ‎‏]/g, ' '); +const norm = (s: string) => s.replace(/[\u00a0\u202f\u200e\u200f]/g, ' '); describe('locale-aware formatting', () => { it('groups numbers in the reader’s conventions', () => { diff --git a/frontend/src/i18n/__tests__/translate.test.ts b/frontend/src/i18n/__tests__/translate.test.ts index b321e020..b13a87de 100644 --- a/frontend/src/i18n/__tests__/translate.test.ts +++ b/frontend/src/i18n/__tests__/translate.test.ts @@ -16,7 +16,7 @@ const fixture: Partial> = { }, }; -const norm = (s: string) => s.replace(/[  ‎‏]/g, ' '); +const norm = (s: string) => s.replace(/[\u00a0\u202f\u200e\u200f]/g, ' '); describe('translate', () => { it('reads a dotted key', () => { From 48723396c372bb129c29ecf77a305e03ba8050a9 Mon Sep 17 00:00:00 2001 From: alex-dembele Date: Fri, 2 Oct 2026 13:23:34 +0100 Subject: [PATCH 2/5] chore(settings): drop fifteen unused imports from SettingsScreen (#627) Leftovers from panels that moved out of this file. Import removal only; nothing else in the file changes. Signed-off-by: alex-dembele --- .../src/features/settings/SettingsScreen.tsx | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/frontend/src/features/settings/SettingsScreen.tsx b/frontend/src/features/settings/SettingsScreen.tsx index e4a14c4c..9837e16f 100644 --- a/frontend/src/features/settings/SettingsScreen.tsx +++ b/frontend/src/features/settings/SettingsScreen.tsx @@ -7,9 +7,8 @@ // Integrations, Notifications, Security, Billing, Danger. Endpoints whose tables // aren't migrated yet (roles/tenants/audit) degrade to an honest unavailable state. -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useSearchParams, useNavigate, useLocation } from 'react-router'; -import { toast } from 'sonner'; import { Settings as SettingsIcon, Users, @@ -22,13 +21,9 @@ import { Shield, CreditCard, AlertTriangle, - Plus, FileText, Check, - Trash2, - Copy, Database, - PowerOff, type LucideIcon, } from 'lucide-react'; import { @@ -46,8 +41,6 @@ import { useAuthStore } from '../../hooks/useAuthStore'; import { SessionsPanel } from '../auth/SessionsPanel'; import { ChangePasswordCard } from '../auth/ChangePasswordCard'; import { MembersView } from '../organization/MembersView'; -import { relTime } from '../risks/riskMap'; -import { api } from '../../lib/api'; import { useCustomFields, useTenants } from './adminData'; import { ApiTokensPanel } from './ApiTokensPanel'; import { @@ -57,14 +50,6 @@ import { import type { NotificationPreferencePatch } from '../notifications/notificationService'; import { useChannelConfig } from '../automation/useAutomation'; import { useVulnIntegrations, useVulnTicketing } from '../vulnerabilities/useVulnIntegrations'; -import { - DataTable, - useTableState, - type Column, - type Facet, - type RowAction, -} from '../../shared/datatable'; -import { DangerConfirm } from '../../shared/DangerConfirm'; import { PersonalizeCard } from '../onboarding/PersonalizeCard'; import { BillingPanel } from '../billing/BillingPanel'; import { DangerZonePanel } from '../billing/DangerZonePanel'; @@ -74,7 +59,6 @@ import { OrganizationProfileForm } from './OrganizationProfileForm'; import { OrganizationLogoField } from './OrganizationLogoField'; import { OrgLogo } from '../organization/OrgLogo'; import { ProfileTab } from '../profile/ProfileTab'; -import type { LocaleCode } from '../../i18n/locales'; import { useI18n } from '../../hooks/useI18n'; import { localeTag } from '../../i18n/locales'; From 9c29abcae15ab6753fe4d66c2bc62078f2fb9e91 Mon Sep 17 00:00:00 2001 From: alex-dembele Date: Fri, 2 Oct 2026 13:23:34 +0100 Subject: [PATCH 3/5] fix(onboarding): read the accent swatches from their tokens (#627) PersonalizeCard drew its two accent swatches with literal colours, #0a84ff and #7c6cff, which matched neither theme's accent and broke openrisk/no-raw-colors. tokens.css already defines --swatch-azure and --swatch-iris for exactly this, mirroring each variant's accent per theme. The swatches now show the colour the accent will actually be. Signed-off-by: alex-dembele --- frontend/src/features/onboarding/PersonalizeCard.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/src/features/onboarding/PersonalizeCard.tsx b/frontend/src/features/onboarding/PersonalizeCard.tsx index b463ca6d..d112fb03 100644 --- a/frontend/src/features/onboarding/PersonalizeCard.tsx +++ b/frontend/src/features/onboarding/PersonalizeCard.tsx @@ -11,9 +11,12 @@ import { useUIStore, type Theme, type Variant } from '../../store/uiStore'; import { ACCENT_LABELS, ACCENT_PRESETS } from '../../shared/accentPresets'; // Swatch colours only; the list of accents comes from shared/accentPresets. +// The preview tokens in tokens.css (--swatch-*), which mirror each variant's +// real accent per theme. The literals that stood here (#0a84ff, #7c6cff) +// matched neither theme's accent. const SWATCH: Record = { - azure: '#0a84ff', - iris: '#7c6cff', + azure: 'var(--swatch-azure)', + iris: 'var(--swatch-iris)', }; const ACCENTS = ACCENT_PRESETS.map((key) => ({ key, From 44598147853e17a0fa33a1f154027b02fb0cc70d Mon Sep 17 00:00:00 2001 From: alex-dembele Date: Fri, 2 Oct 2026 13:23:34 +0100 Subject: [PATCH 4/5] fix(hotkeys): update the handler ref in a layout effect, not during render (#627) Writing ref.current during render is what react-hooks/refs forbids: a render React throws away still mutates the ref. The layout effect runs after every commit and before any later keydown, so the listener keeps seeing the latest handlers. Signed-off-by: alex-dembele --- frontend/src/shared/useHotkeys.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/frontend/src/shared/useHotkeys.ts b/frontend/src/shared/useHotkeys.ts index 3b9d99bc..29929996 100644 --- a/frontend/src/shared/useHotkeys.ts +++ b/frontend/src/shared/useHotkeys.ts @@ -8,7 +8,7 @@ // while ⌘/Ctrl/Alt are held (those belong to the browser or ⌘K). Shift is allowed // because `?` is Shift+/. Modal-scoped keys (Esc, ⌘Enter) keep using useKeyboard. -import { useEffect, useRef } from 'react'; +import { useEffect, useLayoutEffect, useRef } from 'react'; export interface Hotkey { /** Single key to match, case-insensitive: 'n', '/', '?', 't', 'g'. */ @@ -25,8 +25,13 @@ function isTyping(target: EventTarget | null): boolean { export function useHotkeys(hotkeys: Hotkey[], enabled = true): void { // Keep the latest handlers without re-binding the listener every render. + // Updated in a layout effect, not during render (react-hooks/refs): it runs + // after every commit and before any later keydown, so the listener never + // sees stale handlers. const ref = useRef(hotkeys); - ref.current = hotkeys; + useLayoutEffect(() => { + ref.current = hotkeys; + }); useEffect(() => { if (!enabled) return; From 71733bd682183df280f09b4a6dc3b68c91700313 Mon Sep 17 00:00:00 2001 From: alex-dembele Date: Fri, 2 Oct 2026 13:23:34 +0100 Subject: [PATCH 5/5] chore(lint): tighten the ESLint ratchet to 99 (#627) The fixes in this branch, plus debt paid down on master since the last freeze, put six rules under their ceiling. Re-frozen with npm run lint:ceiling -- --update; every number only went down, and no-irregular-whitespace is now enforced at zero. Signed-off-by: alex-dembele --- frontend/.lint-ceiling.json | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/frontend/.lint-ceiling.json b/frontend/.lint-ceiling.json index f11b6590..7114491f 100644 --- a/frontend/.lint-ceiling.json +++ b/frontend/.lint-ceiling.json @@ -1,13 +1,12 @@ { - "total": 132, + "total": 99, "rules": { - "@typescript-eslint/no-unused-vars": 39, - "@typescript-eslint/no-explicit-any": 26, - "react-hooks/set-state-in-effect": 16, - "react-hooks/static-components": 14, + "@typescript-eslint/no-unused-vars": 27, + "@typescript-eslint/no-explicit-any": 21, + "react-hooks/set-state-in-effect": 13, "react-hooks/preserve-manual-memoization": 12, - "react-refresh/only-export-components": 12, - "no-irregular-whitespace": 1, + "react-refresh/only-export-components": 10, + "react-hooks/static-components": 6, "react-hooks/purity": 3, "prefer-const": 2, "@typescript-eslint/ban-ts-comment": 1,