diff --git a/.trivyignore b/.trivyignore new file mode 100644 index 000000000..d0310907a --- /dev/null +++ b/.trivyignore @@ -0,0 +1,14 @@ +# Trivy vulnerability ignore list. +# One CVE id per line. Only suppress a finding that has NO upstream fix we +# can apply; never suppress one with a fixed version available — bump instead. +# Each entry names what it covers and the condition to re-check / remove it. + +# CVE-2026-33671 (picomatch ReDoS, HIGH) — flagged only in the npm CLI's +# OWN bundled copy that ships inside the Node base image +# (usr/local/lib/node_modules/npm/node_modules/picomatch, v4.0.3), used at +# image-build time by npm, not by our runtime. Our application dependency +# tree pins picomatch >= 4.0.4 via a pnpm override, so the running app is +# not affected and the path is not reachable at runtime. Re-check when the +# Node base image ships an npm build whose bundled picomatch is >= 4.0.4, +# then drop this entry. +CVE-2026-33671 diff --git a/public/manifest.json b/public/manifest.json index 0b6c6de85..399b9f08a 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -6,7 +6,8 @@ "scope": "/", "start_url": "/", "display": "standalone", - "display_override": ["standalone"], + "display_override": ["standalone", "minimal-ui"], + "categories": ["health", "medical", "lifestyle"], "theme_color": "#282a36", "background_color": "#282a36", "icons": [ diff --git a/src/__tests__/sw-security-hardening.test.ts b/src/__tests__/sw-security-hardening.test.ts index c082bb60e..4814e6f76 100644 --- a/src/__tests__/sw-security-hardening.test.ts +++ b/src/__tests__/sw-security-hardening.test.ts @@ -85,7 +85,8 @@ describe("public/manifest.json PWA identity hygiene (F-7, 2026-05-16)", () => { const manifest = JSON.parse(raw) as Record; expect(manifest.scope).toBe("/"); expect(manifest.id).toBe("/?source=pwa"); - expect(manifest.display_override).toEqual(["standalone"]); + expect(manifest.display_override).toEqual(["standalone", "minimal-ui"]); + expect(manifest.categories).toEqual(["health", "medical", "lifestyle"]); }); it("keeps the existing display/start_url contract", () => { diff --git a/src/app/globals.css b/src/app/globals.css index 417fb9536..e7892e703 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -889,3 +889,21 @@ animation: none; } } + +/* ── Global reduced-motion backstop ────────────────────────────────── + * Defensive catch-all so any animation/transition we forget to gate + * per-component still collapses to near-instant when the user has asked + * for reduced motion. The per-component blocks above stay — they carry + * tailored static fallbacks (static dots, full-height bars). This is the + * floor, not a replacement. Near-zero (not 0) keeps JS animation-end / + * transition-end listeners firing so state machines don't stall. */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} diff --git a/src/components/data-list/sortable-head.tsx b/src/components/data-list/sortable-head.tsx index c02c0ac12..44730b408 100644 --- a/src/components/data-list/sortable-head.tsx +++ b/src/components/data-list/sortable-head.tsx @@ -32,7 +32,7 @@ export function SortableHead({