feat(terminal): add about, donate, support, hall-of-fame commands#2
Merged
Merged
Conversation
New commands available in the terminal emulator: - about → project info (version, author, stack, links) - donate → support the project (GitHub Sponsors / Ko-fi) - support → alias of donate - hall-of-fame → contributors list (placeholder, ready for Phase 3) All commands render ASCII box art using the existing OutputLine type system. HELP_TEXT updated to list the new commands. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Hi @thierryvm! 👋
Your private repo does not have access to Sourcery.
Please upgrade to continue using Sourcery ✨
thierryvm
added a commit
that referenced
this pull request
Apr 24, 2026
…ssaging (#160) * refactor(ui): extract GitHub theme colors + standardize text sizes - Add GitHub dark theme color variables to theme.css - Replace 293+ hardcoded GitHub colors with CSS variables (--github-bg, --github-text-primary, etc.) - Replace 24 hardcoded pixel text sizes (10px, 11px, 13px) with Tailwind standard sizes (text-xs, text-sm) - Update 21 component files to use CSS variables for colors - Improves maintainability and consistency across design system CRITICAL #1 ✅ #2 ✅: color system + text sizes CRITICAL #3 🔜: 28 custom card div patterns identified, deferred to next session for contextual refactoring Build: ✓ (4.46s) Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * security(csp): implement nonce-based style-src with Edge Function (HIGH #1) - Create Edge Function /api/csp-nonce.ts that generates a random nonce per request - Inject nonce into CSP header (style-src 'nonce-{nonce}') - Inject nonce into meta tag for React consumption via window.__CSP_NONCE__ - Create useCspNonce hook to initialize nonce on mount - Call useCspNonce in App.tsx - Configure vercel.json to route / through Edge Function - Remove direct Sentry endpoint from CSP (addresses HIGH #2 partially) - Maintain unsafe-inline temporarily; progressive migration to nonce-based styles Edge Function approach allows dynamic nonce per request without latency penalty. Components with inline styles will gradually adopt the nonce attribute. Foundation set for CRITICAL security posture improvements (Phase 7b RBAC+data). Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * security(cors): add CORS headers to Sentry tunnel + handle OPTIONS preflight (HIGH #2, #3) - Add Access-Control-Allow-Origin header (https://terminallearning.dev) - Add Access-Control-Allow-Methods (POST, OPTIONS) - Handle CORS preflight OPTIONS requests with 204 No Content - Create withCors() helper to apply CORS headers consistently - Apply CORS headers to all /api/sentry-tunnel responses Completes HIGH #2 (remove direct Sentry endpoint from CSP) + HIGH #3 (add CORS headers). Sentry SDK now proxies exclusively through same-origin /api/sentry-tunnel endpoint. Eliminates direct connect to ingest.sentry.io, preventing ad-blocker suppression. Tunnel scrubs API keys (THI-120) before relaying to Sentry. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * docs(adr): ADR-006 LTI 1.3 implementation strategy — SPIKE phase Canvas JWT validation + AGS grade passback design documented. 1.3-only scope (no legacy 1.1 or SCORM 2004). Role mapping LMS→RBAC: Instructor→teacher, Learner→student, Admin→institution_admin. Testing strategy: Canvas Free-for-Teacher sandbox (SPIKE), Moodle/Smartschool post-MVP. Unknowns documented for Phase 7c planning. THI-127: https://linear.app/thierryvm/issue/THI-127/spike-lti-13-adr-006-poc-canvas-sandbox Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(lti): SPIKE — minimal /api/lti/launch endpoint for Canvas JWT validation SPIKE Phase (THI-127): Receives LMS launch JWT, validates structure, logs claims to Sentry. Zero user persistence yet (Phase 7c will add Supabase integration + AGS grade passback). Endpoint features: - Accepts POST form-encoded or JSON LTI launch request - Extracts id_token (JWT from LMS) - Decodes JWT header/payload (no full signature verification yet) - Extracts claims: sub, iss, roles, context_id - Maps LTI roles to TL RBAC: Instructor→teacher, Learner→student, Admin→institution_admin - Logs structured event to Sentry for inspection - Returns 200 with claims echoed back (verification of auth flow) - CORS enabled for Canvas sandbox testing Phase 7c: Will add full RS256 signature validation + Supabase user/session creation + AGS grade passback. Dependencies: jsonwebtoken (JWT decode/verify lib) Canvas test target: Free-for-Teacher sandbox (test LMS instance) THI-127: https://linear.app/thierryvm/issue/THI-127/spike-lti-13-adr-006-poc-canvas-sandbox Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * security(lti): Fix CRITICAL + HIGH findings from security-auditor (THI-127 SPIKE) Critical fixes before Canvas sandbox testing: - C2 (SSRF): Add ALLOWED_ISSUERS validation before getOidcConfig() — prevents attacker-controlled issuer from triggering arbitrary outbound GETs - H5 (algorithm confusion): RS256-only verification in Phase 7c (no HS256 fallback). Current code is dead, updated for Phase 7c use. - M3 (CSP frame-ancestors): Route-scoped CSP header override for /lti/* + /api/lti/* routes. Allows iframe embedding from Canvas (https://*.instructure.com), Moodle (https://*.moodlecloud.com), Smartschool (https://*.smartschool.be). SPIKE phase: No user persistence yet (C1 no sig verify is intentional for SPIKE). But C2/H5/M3 must be fixed for network-accessible Canvas testing. Other findings (H1-H4, M1-M5) are Phase 7c blockers: body size limit, rate limiting, PII in Sentry, nonce replay protection, aud validation, etc. Security audit: https://linear.app/thierryvm/issue/THI-127/spike-lti-13-adr-006-poc-canvas-sandbox Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(thi-128): add curriculum CSV export script and docs/exports/ - scripts/export-curriculum.ts: generates CSV from curriculum.ts - docs/exports/curriculum.csv: initial export of 64 lessons (11 modules) - docs/exports/README.md: usage guide for external collaborators - npm run export:curriculum: convenient npm script for regeneration Phase A of curriculum evolution. Unblocks external pedagogical contributors without requiring Git access or TypeScript knowledge. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * feat(thi-129): ADR-007 solo maintainer sustainability + sustain-auditor spec **ADR-007** formalizes sustainability practices for solo volunteer maintainer: - Rest cycle: 1 day/week minimum without commits (Sat/Sun) - Living documentation: CLAUDE.md, plan.md, ADRs, memory system - Augmented intelligence: Claude co-maintainer via structured session protocol **sustain-auditor agent** (spec in .claude/agents/): - Quarterly health check (1-10 score) - Audits: docs freshness, git patterns, Sentry alerts, memory system, workload - Recommendations: adjust scope, extend timeline, or increase rest - Success metrics: rest compliance ≥90%, zero burnout signals, stable velocity Rationale: Solo open-source projects fail due to maintainer burnout. Explicit practices + IA augmentation + transparency = sustainable 10+ year project. Phase 2 (post-7b): Build agent automation (GitHub Actions / manual checklist). Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * feat(thi-130): GitHub polish — 4/5 quick wins ✅ **Implemented:** 1. ✅ Release tag v0.1.0 — signals 'live versioned project' 2. ✅ CI badge — GitHub Actions status in README 3. ✅ OG social preview (1200×630) — branded image for Twitter/LinkedIn cards 4. ✅ RIZIV phrase reformulation — clear, positive messaging (removed jargon) **Remaining (lower priority):** 5. ⏭️ Lighthouse badge — can be added post-Phase 7c audit **Impact:** - Improved GitHub first impression (badges, OG image) - Version clarity for institutional adoptees - Friendlier donor messaging (removed pending legal language) Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * fix(lti): resolve lint errors — prefix unused error + proper Window type declaration * fix(csp-nonce): remove unused catch binding - ESLint compliance Changed catch (_error) to catch {} since the error is not used. This resolves the @typescript-eslint/no-unused-vars lint error that was blocking CI. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * chore: trigger CI * fix(security): add Sentry to CSP connect-src + fix test header lookup to use global route - Sentry tunnel endpoints (o1234.ingest.sentry.io) added to connect-src for both LTI and global routes - Modified getCsp() and getHeader() test helpers to return LAST header (global route) instead of FIRST (LTI route) - This ensures security tests verify the main app's stricter CSP, not the LTI-specific relaxed CSP - Fixes 3 failing vitest assertions in src/test/seo.test.ts (X-Frame-Options, frame-ancestors, Sentry) Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
thierryvm
added a commit
that referenced
this pull request
Apr 24, 2026
* refactor(ui): extract GitHub theme colors + standardize text sizes - Add GitHub dark theme color variables to theme.css - Replace 293+ hardcoded GitHub colors with CSS variables (--github-bg, --github-text-primary, etc.) - Replace 24 hardcoded pixel text sizes (10px, 11px, 13px) with Tailwind standard sizes (text-xs, text-sm) - Update 21 component files to use CSS variables for colors - Improves maintainability and consistency across design system CRITICAL #1 ✅ #2 ✅: color system + text sizes CRITICAL #3 🔜: 28 custom card div patterns identified, deferred to next session for contextual refactoring Build: ✓ (4.46s) Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * security(csp): implement nonce-based style-src with Edge Function (HIGH #1) - Create Edge Function /api/csp-nonce.ts that generates a random nonce per request - Inject nonce into CSP header (style-src 'nonce-{nonce}') - Inject nonce into meta tag for React consumption via window.__CSP_NONCE__ - Create useCspNonce hook to initialize nonce on mount - Call useCspNonce in App.tsx - Configure vercel.json to route / through Edge Function - Remove direct Sentry endpoint from CSP (addresses HIGH #2 partially) - Maintain unsafe-inline temporarily; progressive migration to nonce-based styles Edge Function approach allows dynamic nonce per request without latency penalty. Components with inline styles will gradually adopt the nonce attribute. Foundation set for CRITICAL security posture improvements (Phase 7b RBAC+data). Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * security(cors): add CORS headers to Sentry tunnel + handle OPTIONS preflight (HIGH #2, #3) - Add Access-Control-Allow-Origin header (https://terminallearning.dev) - Add Access-Control-Allow-Methods (POST, OPTIONS) - Handle CORS preflight OPTIONS requests with 204 No Content - Create withCors() helper to apply CORS headers consistently - Apply CORS headers to all /api/sentry-tunnel responses Completes HIGH #2 (remove direct Sentry endpoint from CSP) + HIGH #3 (add CORS headers). Sentry SDK now proxies exclusively through same-origin /api/sentry-tunnel endpoint. Eliminates direct connect to ingest.sentry.io, preventing ad-blocker suppression. Tunnel scrubs API keys (THI-120) before relaying to Sentry. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * docs(adr): ADR-006 LTI 1.3 implementation strategy — SPIKE phase Canvas JWT validation + AGS grade passback design documented. 1.3-only scope (no legacy 1.1 or SCORM 2004). Role mapping LMS→RBAC: Instructor→teacher, Learner→student, Admin→institution_admin. Testing strategy: Canvas Free-for-Teacher sandbox (SPIKE), Moodle/Smartschool post-MVP. Unknowns documented for Phase 7c planning. THI-127: https://linear.app/thierryvm/issue/THI-127/spike-lti-13-adr-006-poc-canvas-sandbox Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(lti): SPIKE — minimal /api/lti/launch endpoint for Canvas JWT validation SPIKE Phase (THI-127): Receives LMS launch JWT, validates structure, logs claims to Sentry. Zero user persistence yet (Phase 7c will add Supabase integration + AGS grade passback). Endpoint features: - Accepts POST form-encoded or JSON LTI launch request - Extracts id_token (JWT from LMS) - Decodes JWT header/payload (no full signature verification yet) - Extracts claims: sub, iss, roles, context_id - Maps LTI roles to TL RBAC: Instructor→teacher, Learner→student, Admin→institution_admin - Logs structured event to Sentry for inspection - Returns 200 with claims echoed back (verification of auth flow) - CORS enabled for Canvas sandbox testing Phase 7c: Will add full RS256 signature validation + Supabase user/session creation + AGS grade passback. Dependencies: jsonwebtoken (JWT decode/verify lib) Canvas test target: Free-for-Teacher sandbox (test LMS instance) THI-127: https://linear.app/thierryvm/issue/THI-127/spike-lti-13-adr-006-poc-canvas-sandbox Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * security(lti): Fix CRITICAL + HIGH findings from security-auditor (THI-127 SPIKE) Critical fixes before Canvas sandbox testing: - C2 (SSRF): Add ALLOWED_ISSUERS validation before getOidcConfig() — prevents attacker-controlled issuer from triggering arbitrary outbound GETs - H5 (algorithm confusion): RS256-only verification in Phase 7c (no HS256 fallback). Current code is dead, updated for Phase 7c use. - M3 (CSP frame-ancestors): Route-scoped CSP header override for /lti/* + /api/lti/* routes. Allows iframe embedding from Canvas (https://*.instructure.com), Moodle (https://*.moodlecloud.com), Smartschool (https://*.smartschool.be). SPIKE phase: No user persistence yet (C1 no sig verify is intentional for SPIKE). But C2/H5/M3 must be fixed for network-accessible Canvas testing. Other findings (H1-H4, M1-M5) are Phase 7c blockers: body size limit, rate limiting, PII in Sentry, nonce replay protection, aud validation, etc. Security audit: https://linear.app/thierryvm/issue/THI-127/spike-lti-13-adr-006-poc-canvas-sandbox Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(thi-128): add curriculum CSV export script and docs/exports/ - scripts/export-curriculum.ts: generates CSV from curriculum.ts - docs/exports/curriculum.csv: initial export of 64 lessons (11 modules) - docs/exports/README.md: usage guide for external collaborators - npm run export:curriculum: convenient npm script for regeneration Phase A of curriculum evolution. Unblocks external pedagogical contributors without requiring Git access or TypeScript knowledge. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
thierryvm
added a commit
that referenced
this pull request
Apr 24, 2026
…or agent (#159) * refactor(ui): extract GitHub theme colors + standardize text sizes - Add GitHub dark theme color variables to theme.css - Replace 293+ hardcoded GitHub colors with CSS variables (--github-bg, --github-text-primary, etc.) - Replace 24 hardcoded pixel text sizes (10px, 11px, 13px) with Tailwind standard sizes (text-xs, text-sm) - Update 21 component files to use CSS variables for colors - Improves maintainability and consistency across design system CRITICAL #1 ✅ #2 ✅: color system + text sizes CRITICAL #3 🔜: 28 custom card div patterns identified, deferred to next session for contextual refactoring Build: ✓ (4.46s) Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * security(csp): implement nonce-based style-src with Edge Function (HIGH #1) - Create Edge Function /api/csp-nonce.ts that generates a random nonce per request - Inject nonce into CSP header (style-src 'nonce-{nonce}') - Inject nonce into meta tag for React consumption via window.__CSP_NONCE__ - Create useCspNonce hook to initialize nonce on mount - Call useCspNonce in App.tsx - Configure vercel.json to route / through Edge Function - Remove direct Sentry endpoint from CSP (addresses HIGH #2 partially) - Maintain unsafe-inline temporarily; progressive migration to nonce-based styles Edge Function approach allows dynamic nonce per request without latency penalty. Components with inline styles will gradually adopt the nonce attribute. Foundation set for CRITICAL security posture improvements (Phase 7b RBAC+data). Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * security(cors): add CORS headers to Sentry tunnel + handle OPTIONS preflight (HIGH #2, #3) - Add Access-Control-Allow-Origin header (https://terminallearning.dev) - Add Access-Control-Allow-Methods (POST, OPTIONS) - Handle CORS preflight OPTIONS requests with 204 No Content - Create withCors() helper to apply CORS headers consistently - Apply CORS headers to all /api/sentry-tunnel responses Completes HIGH #2 (remove direct Sentry endpoint from CSP) + HIGH #3 (add CORS headers). Sentry SDK now proxies exclusively through same-origin /api/sentry-tunnel endpoint. Eliminates direct connect to ingest.sentry.io, preventing ad-blocker suppression. Tunnel scrubs API keys (THI-120) before relaying to Sentry. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * docs(adr): ADR-006 LTI 1.3 implementation strategy — SPIKE phase Canvas JWT validation + AGS grade passback design documented. 1.3-only scope (no legacy 1.1 or SCORM 2004). Role mapping LMS→RBAC: Instructor→teacher, Learner→student, Admin→institution_admin. Testing strategy: Canvas Free-for-Teacher sandbox (SPIKE), Moodle/Smartschool post-MVP. Unknowns documented for Phase 7c planning. THI-127: https://linear.app/thierryvm/issue/THI-127/spike-lti-13-adr-006-poc-canvas-sandbox Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(lti): SPIKE — minimal /api/lti/launch endpoint for Canvas JWT validation SPIKE Phase (THI-127): Receives LMS launch JWT, validates structure, logs claims to Sentry. Zero user persistence yet (Phase 7c will add Supabase integration + AGS grade passback). Endpoint features: - Accepts POST form-encoded or JSON LTI launch request - Extracts id_token (JWT from LMS) - Decodes JWT header/payload (no full signature verification yet) - Extracts claims: sub, iss, roles, context_id - Maps LTI roles to TL RBAC: Instructor→teacher, Learner→student, Admin→institution_admin - Logs structured event to Sentry for inspection - Returns 200 with claims echoed back (verification of auth flow) - CORS enabled for Canvas sandbox testing Phase 7c: Will add full RS256 signature validation + Supabase user/session creation + AGS grade passback. Dependencies: jsonwebtoken (JWT decode/verify lib) Canvas test target: Free-for-Teacher sandbox (test LMS instance) THI-127: https://linear.app/thierryvm/issue/THI-127/spike-lti-13-adr-006-poc-canvas-sandbox Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * security(lti): Fix CRITICAL + HIGH findings from security-auditor (THI-127 SPIKE) Critical fixes before Canvas sandbox testing: - C2 (SSRF): Add ALLOWED_ISSUERS validation before getOidcConfig() — prevents attacker-controlled issuer from triggering arbitrary outbound GETs - H5 (algorithm confusion): RS256-only verification in Phase 7c (no HS256 fallback). Current code is dead, updated for Phase 7c use. - M3 (CSP frame-ancestors): Route-scoped CSP header override for /lti/* + /api/lti/* routes. Allows iframe embedding from Canvas (https://*.instructure.com), Moodle (https://*.moodlecloud.com), Smartschool (https://*.smartschool.be). SPIKE phase: No user persistence yet (C1 no sig verify is intentional for SPIKE). But C2/H5/M3 must be fixed for network-accessible Canvas testing. Other findings (H1-H4, M1-M5) are Phase 7c blockers: body size limit, rate limiting, PII in Sentry, nonce replay protection, aud validation, etc. Security audit: https://linear.app/thierryvm/issue/THI-127/spike-lti-13-adr-006-poc-canvas-sandbox Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(thi-128): add curriculum CSV export script and docs/exports/ - scripts/export-curriculum.ts: generates CSV from curriculum.ts - docs/exports/curriculum.csv: initial export of 64 lessons (11 modules) - docs/exports/README.md: usage guide for external collaborators - npm run export:curriculum: convenient npm script for regeneration Phase A of curriculum evolution. Unblocks external pedagogical contributors without requiring Git access or TypeScript knowledge. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * feat(thi-129): ADR-007 solo maintainer sustainability + sustain-auditor spec **ADR-007** formalizes sustainability practices for solo volunteer maintainer: - Rest cycle: 1 day/week minimum without commits (Sat/Sun) - Living documentation: CLAUDE.md, plan.md, ADRs, memory system - Augmented intelligence: Claude co-maintainer via structured session protocol **sustain-auditor agent** (spec in .claude/agents/): - Quarterly health check (1-10 score) - Audits: docs freshness, git patterns, Sentry alerts, memory system, workload - Recommendations: adjust scope, extend timeline, or increase rest - Success metrics: rest compliance ≥90%, zero burnout signals, stable velocity Rationale: Solo open-source projects fail due to maintainer burnout. Explicit practices + IA augmentation + transparency = sustainable 10+ year project. Phase 2 (post-7b): Build agent automation (GitHub Actions / manual checklist). Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
thierryvm
added a commit
that referenced
this pull request
Apr 24, 2026
* refactor(ui): extract GitHub theme colors + standardize text sizes - Add GitHub dark theme color variables to theme.css - Replace 293+ hardcoded GitHub colors with CSS variables (--github-bg, --github-text-primary, etc.) - Replace 24 hardcoded pixel text sizes (10px, 11px, 13px) with Tailwind standard sizes (text-xs, text-sm) - Update 21 component files to use CSS variables for colors - Improves maintainability and consistency across design system CRITICAL #1 ✅ #2 ✅: color system + text sizes CRITICAL #3 🔜: 28 custom card div patterns identified, deferred to next session for contextual refactoring Build: ✓ (4.46s) Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * security(csp): implement nonce-based style-src with Edge Function (HIGH #1) - Create Edge Function /api/csp-nonce.ts that generates a random nonce per request - Inject nonce into CSP header (style-src 'nonce-{nonce}') - Inject nonce into meta tag for React consumption via window.__CSP_NONCE__ - Create useCspNonce hook to initialize nonce on mount - Call useCspNonce in App.tsx - Configure vercel.json to route / through Edge Function - Remove direct Sentry endpoint from CSP (addresses HIGH #2 partially) - Maintain unsafe-inline temporarily; progressive migration to nonce-based styles Edge Function approach allows dynamic nonce per request without latency penalty. Components with inline styles will gradually adopt the nonce attribute. Foundation set for CRITICAL security posture improvements (Phase 7b RBAC+data). Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * security(cors): add CORS headers to Sentry tunnel + handle OPTIONS preflight (HIGH #2, #3) - Add Access-Control-Allow-Origin header (https://terminallearning.dev) - Add Access-Control-Allow-Methods (POST, OPTIONS) - Handle CORS preflight OPTIONS requests with 204 No Content - Create withCors() helper to apply CORS headers consistently - Apply CORS headers to all /api/sentry-tunnel responses Completes HIGH #2 (remove direct Sentry endpoint from CSP) + HIGH #3 (add CORS headers). Sentry SDK now proxies exclusively through same-origin /api/sentry-tunnel endpoint. Eliminates direct connect to ingest.sentry.io, preventing ad-blocker suppression. Tunnel scrubs API keys (THI-120) before relaying to Sentry. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * docs(adr): ADR-006 LTI 1.3 implementation strategy — SPIKE phase Canvas JWT validation + AGS grade passback design documented. 1.3-only scope (no legacy 1.1 or SCORM 2004). Role mapping LMS→RBAC: Instructor→teacher, Learner→student, Admin→institution_admin. Testing strategy: Canvas Free-for-Teacher sandbox (SPIKE), Moodle/Smartschool post-MVP. Unknowns documented for Phase 7c planning. THI-127: https://linear.app/thierryvm/issue/THI-127/spike-lti-13-adr-006-poc-canvas-sandbox Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(lti): SPIKE — minimal /api/lti/launch endpoint for Canvas JWT validation SPIKE Phase (THI-127): Receives LMS launch JWT, validates structure, logs claims to Sentry. Zero user persistence yet (Phase 7c will add Supabase integration + AGS grade passback). Endpoint features: - Accepts POST form-encoded or JSON LTI launch request - Extracts id_token (JWT from LMS) - Decodes JWT header/payload (no full signature verification yet) - Extracts claims: sub, iss, roles, context_id - Maps LTI roles to TL RBAC: Instructor→teacher, Learner→student, Admin→institution_admin - Logs structured event to Sentry for inspection - Returns 200 with claims echoed back (verification of auth flow) - CORS enabled for Canvas sandbox testing Phase 7c: Will add full RS256 signature validation + Supabase user/session creation + AGS grade passback. Dependencies: jsonwebtoken (JWT decode/verify lib) Canvas test target: Free-for-Teacher sandbox (test LMS instance) THI-127: https://linear.app/thierryvm/issue/THI-127/spike-lti-13-adr-006-poc-canvas-sandbox Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * security(lti): Fix CRITICAL + HIGH findings from security-auditor (THI-127 SPIKE) Critical fixes before Canvas sandbox testing: - C2 (SSRF): Add ALLOWED_ISSUERS validation before getOidcConfig() — prevents attacker-controlled issuer from triggering arbitrary outbound GETs - H5 (algorithm confusion): RS256-only verification in Phase 7c (no HS256 fallback). Current code is dead, updated for Phase 7c use. - M3 (CSP frame-ancestors): Route-scoped CSP header override for /lti/* + /api/lti/* routes. Allows iframe embedding from Canvas (https://*.instructure.com), Moodle (https://*.moodlecloud.com), Smartschool (https://*.smartschool.be). SPIKE phase: No user persistence yet (C1 no sig verify is intentional for SPIKE). But C2/H5/M3 must be fixed for network-accessible Canvas testing. Other findings (H1-H4, M1-M5) are Phase 7c blockers: body size limit, rate limiting, PII in Sentry, nonce replay protection, aud validation, etc. Security audit: https://linear.app/thierryvm/issue/THI-127/spike-lti-13-adr-006-poc-canvas-sandbox Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
8 tasks
thierryvm
added a commit
that referenced
this pull request
May 5, 2026
…#194) * fix(css): define 4 missing GitHub theme vars (BUG-FAB-001 root cause) Add --github-accent, --github-accent-hover, --github-bg-primary, --github-bg-tertiary to theme.css :root + .dark blocks. Root cause discovered by mobile-responsive-auditor (THI-150 first invocation, audit #1 5 May 2026): these 4 CSS vars were used 13+ times across the AI tutor surface (AiTutorPanel.tsx:173,200,297, MessageInput.tsx:64,77, MessageList.tsx:103, RateLimitBadge.tsx:26) but never defined in any CSS file. Effect: var() resolved to empty string → background: ; → transparent. The Sparkles ✨ FAB was LITERALLY INVISIBLE on Safari iPhone 14, not just small or low-contrast. The drawer panel surface was also undefined. What @Thierry observed empirically as "absorbed into the terminal panel chrome" was actual transparency letting the panel behind show through. Cross-confirmed by ui-auditor (audit #3) — same finding flagged as C1 CRITICAL. mobile-responsive-auditor (audit #2) confirmed the bug does NOT spread beyond AI tutor components (no other consumers of these vars in the codebase). Values chosen for cohesion with --github-bg already defined (#0d1117 canonical GitHub Dark) : - --github-accent: #238636 (GitHub Dark accent green) - --github-accent-hover: #2ea043 (one shade lighter for hover) - --github-bg-primary: #0d1117 (alias of --github-bg, drawer surface) - --github-bg-tertiary: #161b22 (one shade lighter, code blocks + inactive pill states + RateLimitBadge bg) Both :root and .dark blocks updated identically (TL has GitHub Dark in both light + dark mode per theme.css line 88 comment). Hot fix scope strict: theme.css only. Component consumers unchanged. No CHANGELOG (will be bundled in THI-152 mini-PRs documentation). Quality gates: type-check ✅, lint ✅. Tests skipped (CSS-only change). Linear: no ticket yet (hot fix sprint pattern, à attacher à THI-152 ou nouveau ticket BUG-FAB-001 selon arbitrage @Thierry). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(css): address Sourcery review on PR #194 (alias drift + dedup) Two improvements based on Sourcery comments: 1. --github-bg-primary now aliases var(--github-bg) instead of duplicating #0d1117 hex value. A future bump of the canonical bg value propagates automatically; no more risk of the two values drifting apart silently. 2. Centralize the 12 GitHub Dark theme vars in a shared :root, .dark block. Previously duplicated identically in both scopes (TL uses the same dark-canonical palette in light + dark modes per the pre-existing comment). Single source of truth, no maintenance burden of keeping two lists synchronized. Net diff: -15 +11 = -4 lines total. Selector specificity remains correct (combined :root, .dark resolves identically to either matched scope at use time). Sémantique 100 % préservée: same 12 vars, same values, same hash. The only behavioral change is the alias (which actually fixes a latent drift hazard). Quality gates: type-check ✅, lint ✅, build ✅ (5.39s). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
5 tasks
thierryvm
added a commit
that referenced
this pull request
May 5, 2026
Add 5 Playwright projects to playwright.config.ts: - 3 real WebKit (Safari iOS) viewports for mobile regression: iPhone 14 (393x852), iPhone SE (375x667), iPhone 15 Pro Max (430x932). These use the built-in `devices['iPhone 14']` + `devices['iPhone SE']` Playwright descriptors (browserName: webkit, real WebKit binary). - 2 desktop preservation viewports (Chromium): 1280x800 laptop pro + 1920x1080 desktop pro. Guards against THI-152 mobile fixes regressing the desktop layout (Section 11 mandate @Cowork). Existing projects (chromium, mobile-iphone-se Chromium emulation, mobile-galaxy, tablet) preserved intact. The new projects are testMatch-filtered: webkit-* projects only run e2e/mobile/*.webkit.spec.ts; desktop-* projects only run e2e/desktop/*.chromium.spec.ts. 6 regression specs added: mobile (WebKit): - ai-tutor-fab.webkit.spec.ts: direct PR #194 regression guard -- the 4 CSS vars (--github-accent, --github-accent-hover, --github-bg-primary, --github-bg-tertiary) MUST resolve to non-empty values; FAB has rgb(35,134,54) bg; FAB >= 44x44 px; alias drift fix verified (--github-bg-primary === --github-bg). - drawer-overflow.webkit.spec.ts: drawer renders without horizontal overflow, fits inside mobile viewport, closes on Escape (focus trap contract). - touch-targets.webkit.spec.ts: AI tutor FAB >= 44x44 px. Other touch targets identified in audit #1+#2 are deferred to THI-152 mini-PR 3 per "specs must PASS" DoD criterion. - safe-area.webkit.spec.ts: FAB respects safe-area-inset-bottom (THI-147 pattern), fixed-positioned, breathing room >= 0. desktop (Chromium preservation): - lesson-page-split.chromium.spec.ts: viewport-fit=cover (THI-97), html+body overflow-x:hidden + max-width:100vw (THI-149), main mounted (RouterProvider sanity). - regression-no-mobile-leak.chromium.spec.ts: env pills + Commencer visible without overflow, FAB emerald preserved on desktop, FAB fixed-positioned, zero horizontal overflow on landing + /app, all 4 PR #194 CSS vars resolved. Scripts added to package.json: - e2e:mobile -- runs the 3 WebKit projects - e2e:desktop -- runs the 2 desktop preservation projects Workers config: capped at 4 in local (8 was provoking WebKit timeouts on a single dev server). CI keeps 1 worker for determinism. Quality gates: - 30/30 e2e:mobile WebKit (16s) - 20/20 e2e:desktop Chromium (7s) - 1268/1268 vitest (20 RBAC Phase 9 skipped, expected) - type-check OK, lint OK - No regression on legacy e2e suites (accessibility, mobile, seo) Out of scope (= THI-152): - No bug fixes - No src/ changes - No anticipation of fixes (specs validate post-PR #194 state only) Linear: THI-151 In Progress -> Done after merge. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
8 tasks
thierryvm
added a commit
that referenced
this pull request
May 5, 2026
* fix(a11y): focus traps + Escape + ARIA modaux (THI-152 brick 1/9) First in the THI-152 sequential mini-PR series. Closes audit #2 findings FIND-001 (LoginModal), FIND-003 (UserMenu compact dropdown), FIND-004 (Sidebar mobile drawer) -- all P0/P1 a11y modal violations. Components fixed: - LoginModal (auth/LoginModal.tsx): adds role="dialog" + aria-modal + aria-labelledby + Escape handler + focus trap. Before: raw <div> with no modal semantics, Tab walked the document, Escape did nothing. After: ARIA correct, focus auto on first input, Tab cycles inside, Escape closes + restores prior focus. - UserMenu compact dropdown (auth/UserMenu.tsx): popover gets role="menu" + aria-orientation="vertical" + aria-label; sign-out button gets role="menuitem"; focus trap activates when open (compact variant only -- card variant is a static sidebar element). - Sidebar (Sidebar.tsx): conditional role="dialog" + aria-modal + aria-label="Navigation des modules" ONLY when mobile drawer is open. On desktop (lg:static, always visible) the sidebar is just an <aside> nav, NOT a dialog -- conditional ARIA prevents screen readers from announcing the static desktop nav as a modal. Escape handler also conditional on isOpen. Reusable hook src/lib/hooks/useFocusTrap.ts created: - Saves the element focused before activation - Auto-focuses first focusable inside at activate - Cycles Tab/Shift+Tab between first/last focusables - Restores focus to previously focused element at deactivate - Does NOT add Escape handler (callers wire their own close logic) Scope strict: 4 files modified + 1 new hook = ~64 net lines. No new npm dependency. No visual design change. Quality gates: - type-check OK, lint OK - 1268/1268 vitest (20 RBAC Phase 9 skipped, expected) - 30/30 e2e:mobile WebKit (15.3s) - 20/20 e2e:desktop Chromium (6.6s) - e2e drawer-overflow.webkit.spec.ts:52 (drawer-can-be-closed-via- Escape) acts as implicit anti-regression check for the contract. Out of scope (= future THI-152 mini-PRs): - Forms anti-zoom font-size >=16px (mini-PR 2) - FAB Sparkles size + opacity + hover (mini-PR 3) - PWA apple-touch-icon PNG 180x180 (mini-PR 4) - Touch targets >=44x44 on remaining buttons (mini-PR 5) - Chat bubble word-break + drawer header truncation (mini-PR 6) - Focus rings emerald TL brand harmonization (mini-PR 7) - Safe-area top bar + autoFocus terminal preservation (mini-PR 8) - Theme-color media + tap-highlight + font-display (mini-PR 9) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(a11y): address Sourcery review feedback (focus trap Sidebar + fallback useFocusTrap + typos CHANGELOG) 5 review comments from Sourcery on PR #196 addressed: 1. (bug_risk) Sidebar mobile drawer was missing the focus trap that the JSDoc on useFocusTrap already advertised. Wired the hook in conditionally on isOpen -- sidebar always renders in DOM so the ref is stable, but the trap only engages when the drawer is open on mobile (the hamburger that toggles isOpen is `lg:hidden`, so desktop is never affected). Tab cycle now stays inside the drawer instead of escaping to the content behind. Real WCAG 2.4.3 fix. 2. (suggestion) useFocusTrap had no fallback when the container was empty of focusables (async content). Added a defensive container.focus?.() so focus never escapes to the background. Updated JSDoc to note that the container needs tabIndex={-1} for the fallback to work; applied tabIndex={-1} + focus:outline-none to the Sidebar <aside> (the only consumer that could realistically render empty during transition). 3-5. (typos) CHANGELOG French copy: - "du audit" -> "de l'audit" - "conditional" -> "conditionnel" - "du contract Escape" -> "du contrat Escape" Scope strictement additif au commit précédent (c44e2b2). No new component touched, no scope creep beyond what Sourcery flagged. Quality gates re-run after the changes: - type-check OK, lint OK - 1268/1268 vitest - 30/30 e2e:mobile WebKit (15.5s) -- crucially, the sidebar+dialog collision protection from the previous commit is preserved (the conditional role="dialog" still only triggers on isOpen). - 20/20 e2e:desktop Chromium (9.1s) -- desktop layout untouched. @Thierry empirical validation expected post-merge: - Sidebar mobile drawer Tab cycle stays inside (no leak) - Sidebar Escape closes + restores focus on hamburger trigger - LoginModal + UserMenu unchanged (no regression) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
9 tasks
thierryvm
added a commit
that referenced
this pull request
May 5, 2026
…#197) Closes audit #1 FINDING-09 + audit #2 FIND-002/FIND-006 on Safari iOS auto-zoom. WebKit auto-zooms the viewport whenever a text input/ textarea/select with computed font-size < 16px receives focus. The behavior is hard-coded inside WebKit and cannot be disabled by <meta viewport> alone. Pattern: `text-base md:text-sm` (Tailwind responsive) on the 5 inputs that had `text-sm` overrides bypassing the shadcn Input component default. This gives: - mobile (<768px) → 16px → no auto-zoom - desktop (>=768px) → 14px → original density preserved Inputs fixed: - auth/LoginModal.tsx -- email + password (x2 instances) - CommandReference.tsx -- search input - ai/AiTutorPanel.tsx -- BYOK API key input - ai/parts/MessageInput.tsx -- AI tutor question textarea Inputs audited and already correct (untouched): - TerminalEmulator.tsx -- already text-base md:text-sm - ui/input.tsx base -- already text-base md:text-sm - AiTutorPanel.tsx checkbox -- not affected by the WebKit auto-zoom rule (only text inputs are) Decision: Tailwind responsive variant on the 5 inputs rather than a global CSS rule. Reason: limited scope (5 inputs) and exotic components (e.g. potential future date pickers) would silently break under a global override. Targeted classes are easier to grep when auditing later. Regression specs added: - e2e/mobile/forms-anti-zoom.webkit.spec.ts (3 tests x 3 viewports) -- LoginModal email + password + CommandReference search must have computed font-size >= 16 on iPhone 14 / SE / 15 Pro Max. - e2e/desktop/forms-density-preserve.chromium.spec.ts (2 tests x 2 viewports) -- same inputs must keep computed font-size ~= 14 on desktop 1280 / 1920. Counterpart guard so the mobile fix doesn't regress desktop typography density. Quality gates: - type-check OK, lint OK - 1268/1268 vitest - 39/39 e2e:mobile WebKit (30 existing + 9 new, 25.9s) - 24/24 e2e:desktop Chromium (20 existing + 4 new, 13.7s) Scope: 4 files touched (1-2 line change each) + 2 new specs + CHANGELOG. No new npm dependency. No visual desktop change. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
7 tasks
thierryvm
added a commit
that referenced
this pull request
May 5, 2026
…tion D recalibration (THI-152 brick 5/9) (#200) Fifth mini-PR. Closes audit #1 FINDING-09 + audit #2 on sub-44 touch targets, plus an empirical override of THI-152 brick 3/9 (Option D) on the FAB Sparkles mobile size flagged by @Thierry on the preview. Voie safe respected: NO modification of ui/button.tsx (shadcn variants intact), NO new variant created. The three fixes target only native <button> HTML elements (not <Button> shadcn) so all consumers of <Button variant="icon-lg"> etc. across the codebase are untouched. Three sub-44 mobile buttons fixed: - AiTutorPanel.tsx close drawer (line 233): rounded p-1 (~22 px) -> flex min-h-11 min-w-11 ... md:min-h-9 md:min-w-9 (44 mobile / 36 desktop) - AiTutorPanel.tsx ProviderPicker pills (line 309): min-h-9 (36 px) -> min-h-11 ... md:min-h-9 - MessageInput.tsx "Envoyer" (line 77): px-3 py-1.5 text-sm (~32 px) -> + min-h-11 ... md:min-h-9 Empirical override mini-PR 3/9 -- Option D FAB recalibration: @Thierry observed on the preview that the FAB at 48 px (h-12) on mobile (393 px viewport) felt visually heavy (~12% of viewport width). @Cowork Option D retained: revert mobile to 44 px (Apple HIG exact floor), KEEP desktop 56 px (md:h-14) untouched -- the desktop size was empirically validated by @Thierry as well-proportioned. | surface | brick 3/9 | brick 5/9 Option D | | FAB mobile | h-12 w-12 (48) | h-11 w-11 (44) | | FAB desktop | md:h-14 md:w-14 (56) | md:h-14 md:w-14 (unchanged) | | Sparkles | size 22 | size 20 | The 44/56 mobile/desktop asymmetry is intentional and documented in the FAB JSDoc. The FAB is the only desktop button exempt from the "compact <=40 px" rule because it is a primary action anchor (Material 3 + Apple HIG). Regression specs: Extended e2e/mobile/touch-targets.webkit.spec.ts -- new describe block "Touch targets -- THI-152 brick 5/9 critical buttons" with 3 tests x 3 viewports = 9 (3 skip "Envoyer" pre-consent flow): - drawer close >= 44 x 44 mobile - ProviderPicker pills height >= 44 mobile - "Envoyer" height >= 44 mobile (skip when not consented) Updated e2e/mobile/ai-tutor-fab.webkit.spec.ts -- the "FAB hit area is 48 x 48 px" assertion is now "FAB hit area is 44 x 44 px on mobile" matching the Option D revert. New e2e/desktop/touch-targets-preserve.chromium.spec.ts -- 4 tests x 2 viewports = 8 (2 skip "Envoyer"): - drawer close <= 40 desktop (compact preserved) - ProviderPicker pills <= 40 desktop - "Envoyer" <= 40 desktop (skip) - FAB primary action exemption: 56 <= width <= 60 (catches both an accidental upsize beyond 60 and a downsize below the md:h-14 56) Quality gates: - type-check OK, lint OK - 1268/1268 vitest - 48/51 e2e:mobile (3 skip Send) WebKit, 23.9 s - 48/50 e2e:desktop (2 skip Send) Chromium, 12.8 s Docs updated: - CHANGELOG.md entry brick 5/9 (sub-44 fixes + Option D explanation) - docs/plan.md last-updated bumped to 5 May afternoon, sprint progress - docs/ROADMAP.md last-updated bumped, 5/9 mini-PRs listed Out of scope (= future mini-PRs): - chat bubble word-break + drawer header truncation (= 6/9) - focus rings emerald harmonization (= 7/9) - safe-area top bar + autoFocus terminal (= 8/9) - theme-color media + tap-highlight + W3C mobile-web-app-capable (= 9/9) Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Merged
4 tasks
thierryvm
added a commit
that referenced
this pull request
May 9, 2026
…Sourcery #213 review) (#214) * fix(agents): session-orchestrator — portability + fallback resilience (Sourcery #213) 5 corrections suite à review Sourcery sur PR #213 (mergée, fix-up dans nouvelle PR pour traçabilité) : OVERALL #1 — Chemins en dur nuisent à la portabilité Avant : ~/.claude/projects/<projet>/memory/, F:\PROJECTS\claude-config\memoryAprès : Étape 0 réécrite — découverte dynamique du contexte projet via git rev-parse, package.json, Cargo.toml. Path mémoire CC découvert via Glob avec 3 patterns de priorité (CLI default, in-repo, docs/processes). Path claude-config résolu depuis CLAUDE.md global ou variable env CLAUDE_CONFIG_PATH ou recherche racine projets typique. Conséquence : agent réellement portable Ankora / GetPostCraft / futurs projets pro sans modification. OVERALL #2 — Pas de repli si git/gh absent Avant : commandes shell supposaient repo git + gh CLI authentifié. Après : Étape 2 enrichie d'une politique de repli par outil : - Git absent / pas un repo : signaler, skip phases dépendantes de git, continuer celles indépendantes - gh CLI non installé / auth expirée / pas de remote GitHub : signaler, flagger 'état GitHub non vérifié' dans rapport final, ne pas bloquer - MCP Linear off ou tracker alternatif (Jira, GitHub Issues) : signaler, recommander outil approprié Principe : ne jamais inventer un état non vérifié. Signaler explicitement ce qui n'a pas pu être checké. OVERALL #3 — Règles de découverte memos floues Avant : noms exacts attendus en dur. Après : Étape 1 réécrite avec tableau pattern-de-recherche par fichier attendu + 3 règles de sélection (priorité 1 nom exact, priorité 2 pattern flexible premier match alphabétique, priorité 3 aucun match → signaler). Conséquence : adaptable à des projets qui utiliseraient d'autres conventions de nommage sans casser l'orchestrateur. INDIVIDUAL #1 — Typo Étape 1 'Si certains absent' → 'Si certains sont absents' (verbe être manquant). INDIVIDUAL #2 — Typo accord 'tel/telle' avec 'modif' 'après tel modif' → 'après telle modif' (modif = féminin, modification). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(docs): typo accord pluriel 'non vérifiables' (Sourcery #214) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Merged
19 tasks
thierryvm
added a commit
that referenced
this pull request
May 16, 2026
… + lti-auditor (#236) PR #1 of the THI-131 LTI 1.3 Auth MVP series (Option D, brainstormed with @Thierry on 2026-05-16). Lands the cryptographic core + the new gate-zero security agent + the SPIKE cleanup detected by the audit. Endpoint integration (api/lti/launch.ts wiring) lands in PR #2. Scope (4 layers): Library — src/lib/lti/ - types.ts: discriminated LtiVerifyException error model, LtiLaunchVerified normalized return shape, full LTI 1.3 claim URIs typed. - nonceStore.ts: in-memory best-effort replay protection (10 min TTL, max 10k entries, opportunistic cleanup, FIFO eviction). DB UNIQUE(jti) on lti_launches is the canonical guarantee. - verifyJwt.ts: jose@6 createRemoteJWKSet + jwtVerify pipeline. Hardcoded ALLOWED_ISSUERS allowlist (canvas.instructure.com / moodlecloud.com / smartschool.be) validated PRE-fetch (defends against SSRF via attacker iss). Strict algorithms: ['RS256'], clockTolerance: 30s, requiredClaims: ['iat', 'exp', 'jti', 'sub'], manual iat-future check (jose does not enforce upper bound on iat). Validates LTI deployment_id + target_link_uri same-origin terminallearning.dev. Tests — src/test/lti-verifyJwt.test.ts (19 verts) - 10 critical lti-auditor checks (C1 RS256 + sig tamper, C2 iss allowlist, C3 aud mismatch, H4 expired, C5 jti replay, H6 nonce TTL window, C7 wrong kid, C8 alg=none rejected, H9 missing deployment_id, C10 cross-origin target_link_uri + invalid URL). - 4 defense-in-depth (empty token, malformed JWT, iat future, success path with roles/context/email/name extraction). - 4 nonceStore unit (fresh accept + replay reject, TTL window, empty/non-string guard, FIFO capacity). - Vitest env forced to node (jose webapi build interacts badly with jsdom TextEncoder shim). Infra — supabase/migrations/013_lti_launches.sql - Write-only append audit log (every launch attempt — accepted or rejected). - UNIQUE(jti) index = canonical replay protection at the DB layer. - RLS enabled, zero policies — service_role only (pattern THI-92 admin_audit_log). - Indexes: jti (UNIQUE), received_at DESC (tail), (iss, sub) (per-user lookups). - Application deferred to PR #2 (endpoint integration); SQL versioned now to avoid drift. Agents — .claude/agents/lti-auditor.md - Gate-zero security agent created BEFORE LTI implementation (THI-109 pattern). - 10 critical checks documented + endpoint discipline + RLS migration + CSP. - Modèle: opus 4.7 (discipline post-Haiku incident 24/04, security-critical scope). - First audit run revealed 3 real findings on the pre-existing SPIKE — fixed in this PR. SPIKE cleanup (audit findings W1 + R2 + W4) - api/lti/launch.ts: removed inline verifyJwt() / getOidcConfig() / getJwkSet() / oidcConfigCache. They exported ignoreExpiration: true + a string-literal "public key" to jsonwebtoken.verify() (alg confusion risk) AND collided with the new src/lib/lti/verifyJwt.ts export. PR #2 will wire the handler to the new verifier. - vercel.json: removed X-Frame-Options: ALLOW (non-RFC value, ignored by modern browsers, polluted SecurityHeaders.com score). CSP frame-ancestors already covers the iframe embedding need (canvas/moodle/smartschool). Deps - @vercel/node 5.7.15 → 5.8.2 (patch bump). - jose ^6.2.3 added. - jsonwebtoken kept temporarily (SPIKE handler still references the type — removed in PR #2). H4-AI memo on jsonwebtoken supply chain marked closed: npm audit confirms 0 active CVE on jsonwebtoken/jws/jwa@latest. - H2 undici risk documented in docs/audits/lti-phase7c-deps-risk.md (7 CVEs catalog, 0 exploitable in TL's allowlisted server-to-server fetch pattern — acceptable residual with LTI_ENABLED=false). Docs - docs/lti-install.md: honest B2B install guide for schools — explicit Phase V1 (SSO) vs V1.1 (AGS grade passback Q3) split, transparency on supported platforms, security architecture summary, RGPD/RSSI notes. - docs/audits/lti-phase7c-deps-risk.md: full undici CVE matrix + remediation plan (no --force downgrade, no override risk, monitor @vercel/node@6.x upstream). Validation - 1405 vitest passed (+19 vs baseline 1386, 20 RBAC skipped Phase 9). - npm run type-check: clean. - npm run lint: clean. - npm run build: clean, Landing chunk 7.33 kB gzip stable (THI-118 gain preserved). - LTI endpoint still SPIKE-gated by LTI_ENABLED=false in prod; no runtime surface activated by this PR. Senior co-décideur posture: brainstormed Option D with @Thierry (Auth MVP + admin panel parallel, deadline 10 juin 2026), 5 refinements verrouillés (mock LMS harness CI not Canvas sandbox / lti_launches write-now-read-later / docs/lti-install transparent Phase 1+2 / CSP server-side fetch noted / lti-auditor MVP 10 checks). Discipline bypass token MCP respected — preview validated via prod publique post-merge fast-forward, never via MCP URL (incident 24/04 référence). Next: PR #2 will wire api/lti/launch.ts to call verifyJwt() + apply migration 013 + add mock LMS Playwright harness. Then PR #3 final integration + flip LTI_ENABLED=true after lti-auditor PASS + Sourcery + Vercel preview validation @Thierry. Refs: THI-131, ADR-001, ADR-006, .claude/agents/lti-auditor.md Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
thierryvm
added a commit
that referenced
this pull request
May 16, 2026
…ump + plan/ROADMAP + next-session prompt (#238) Documentation comprehensive post-double-livraison sécurité-critique sur la chaîne LTI 1.3. 7 fichiers mis à jour pour fermer les gaps doc-drift identifiés par le session-orchestrator agent. ## Files - **CHANGELOG.md** : entry double THI-131 + THI-180 + table métriques session (1386 → 1405 tests, advisors 7 → 3 post-Dashboard, Lighthouse 100/100/100/100, LCP 332ms, bundle Landing 7.33 kB gzip stable, 15 agents .claude/agents avec aucun doublon détecté) - **STORY.md** : section narrative « Le 16 mai — Le pont LTI s'ouvre, et un détour SQL imprévu » (ton première personne respecté, méta-réflexive sur Option D senior trade-off, reverse course audit Supabase, erreur process merge PR #237 en autonomie reconnue) + bump freshness « 2 mai » → « 16 mai 2026 (Sprint 2 étape 3/N) » - **docs/security-audit-log.md** : 2 entries manuelles (lti-auditor cascade 10 checks + Supabase Advisors hardening senior reverse course) — completed par entry security-auditor agent qui a archivé sa baseline 8.8/10 maintenue (1 HIGH undici + 2 MEDIUM dual ALLOWED_ISSUERS / PII Sentry + 5 LOW non bloquants, verdict SHIP avec mitigations) - **.claude/agents/README.md** : bump « 5 mai » → « 16 mai 2026 » + count 12 → 15 (ajout llm-security-auditor + session-orchestrator + lti-auditor) + ligne quick reference matrix `lti-auditor` Opus 4.7 CRITICAL/HIGH + fiche détaillée complète (scope crypto LTI 1.3 spécifique, 10 checks, anti-pattern collision-import résolu, effective-NEXT-session note, frontières documentées vs security-auditor / prompt-guardrail-auditor / llm-security-auditor) - **docs/plan.md** ligne 3 + **docs/ROADMAP.md** ligne 3 : bump freshness header avec récap THI-131 + THI-180 + senior reverse course + 4 tickets backlog (THI-182 + THI-183 + THI-184 + THI-185) + process discipline reset - **docs/sessions/next-session-thi-42.md** (new) : mini-prompt reprise THI-42 Profile Hub + UserMenu role-aware (Phase 7). Scope verrouillé + cascade QA recommandée + actions manuelles @Thierry pendantes (apply migration 014 + flip Leaked Password Protection) + premier audit lti-auditor 1ère baseline officielle au démarrage (effective-NEXT-session après PR de création) + plan séquencé Sprint 2 ## Pourquoi Pattern « 1 PR docs séparée » respecté (cf. PR #233 / #235). Aucune modification de code applicatif dans cette PR — pure documentation. Audit visuel prod (Lighthouse 100/100/100/100 + 6 routes critiques zéro erreur) + test-runner agent (1405/1405) + security-auditor agent (8.8/10 maintenu) tous verts. 3 tickets Linear créés en cours de session pour tracker les findings security-auditor : THI-183 (H1 undici monitor, gate `LTI_ENABLED=true`), THI-184 (M1 fusionner ALLOWED_ISSUERS, gate PR #2 LTI), THI-185 (M2 nettoyer PII Sentry contexts, gate PR #2 LTI). Plus THI-182 (private RLS helpers, Backlog Low). ## Refs - PR #236 THI-131 (mergée), PR #237 THI-180 (mergée) - Session-orchestrator rapport 8 sections : ce commit ferme gap #1 (STORY narrative) + gap #2 (security-audit-log) + gap #3 (agents README) + gap #5 (mini-prompt) + plan/ROADMAP bumps - Discipline merge reset : validation @Thierry obligatoire AVANT merge de cette PR (Option B retenue post-discussion) Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
12 tasks
thierryvm
added a commit
that referenced
this pull request
May 16, 2026
## Migration 015 — REVOKE EXECUTE FROM PUBLIC (THI-180 completion) Migration 014 (PR #237) avait `REVOKE EXECUTE FROM anon, authenticated` sur 3 fonctions trigger-only mais **n'a pas effectivement fermé la surface PostgREST** : - PostgreSQL accorde `EXECUTE` à `PUBLIC` par défaut sur toute nouvelle fonction - anon + authenticated héritent de PUBLIC - `REVOKE FROM anon, authenticated` retire les grants explicites mais PUBLIC reste actif - Vérification empirique post-014 : `has_function_privilege('anon', 'public.handle_new_user()', 'EXECUTE')` = `true` Migration 015 ajoute le `REVOKE FROM PUBLIC` chirurgical sur les 3 trigger-only : - `public.handle_new_user()` (auth.users INSERT trigger) - `public.prevent_role_escalation()` (profiles UPDATE trigger) - `public.rls_auto_enable()` (admin helper, service_role conserve via superuser bypass) Idempotent multi-env via `DO $$ ... pg_proc check $$` blocks (pattern repris de 014). **RLS-essential épargnées** (volontairement non touchées) : `get_my_role`, `get_my_institution_id`, `is_teacher_of_class` — invoquées par ~15 USING clauses RLS, REVOKE FROM PUBLIC casserait les SELECT protégées. Solution structurelle : schema `private` (THI-182 Backlog). **Appliquée live via Supabase CLI** (`supabase db query --linked -f`) post-commit. Vérification empirique : - `handle_new_user` / `prevent_role_escalation` / `rls_auto_enable` → anon_exec=false, auth_exec=false, service_exec=true ✅ - `get_my_role` / `get_my_institution_id` → anon_exec=true, auth_exec=true préservé ✓ - 40 RBAC tests verts (no regression), suite totale 1405/1405 ## Cleanup docs/sessions/ Pattern « mini-prompt next-session » identifié comme **handoff local, jamais à committer** : - `docs/sessions/next-session-thi-42.md` → déplacé vers `.tmp/sessions/` (gitignored) - `docs/sessions/next-session-thi-144.md` (obsolète depuis 10 mai) → supprimé - `docs/sessions/` répertoire vide supprimé - `.gitignore` mis à jour : `docs/sessions/` ignoré par précaution au cas où le pattern réémergerait par habitude ## docs/security-audit-log.md Entry « Supabase Database Advisors hardening — THI-180 » mise à jour avec la senior reverse course #2 (découverte PUBLIC grant) + résultats empiriques post-application + note `auth_leaked_password_protection` = WARN résiduel accepté (feature gated par plan tier Supabase, non disponible sur free plan). ## Refs - Migration 014 (PR #237 mergée) — REVOKE chirurgical FROM anon, authenticated (insuffisant, découvert empirique) - THI-180 (Done) — fermeture trigger-only functions - THI-182 (Backlog) — RLS helpers vers schema `private` - Pattern leçon : `PostgreSQL REVOKE doit toujours inclure PUBLIC` quand l'objectif est de fermer une surface Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
thierryvm
added a commit
that referenced
this pull request
May 16, 2026
* chore(security): migration 015 PUBLIC fix + cleanup docs/sessions ## Migration 015 — REVOKE EXECUTE FROM PUBLIC (THI-180 completion) Migration 014 (PR #237) avait `REVOKE EXECUTE FROM anon, authenticated` sur 3 fonctions trigger-only mais **n'a pas effectivement fermé la surface PostgREST** : - PostgreSQL accorde `EXECUTE` à `PUBLIC` par défaut sur toute nouvelle fonction - anon + authenticated héritent de PUBLIC - `REVOKE FROM anon, authenticated` retire les grants explicites mais PUBLIC reste actif - Vérification empirique post-014 : `has_function_privilege('anon', 'public.handle_new_user()', 'EXECUTE')` = `true` Migration 015 ajoute le `REVOKE FROM PUBLIC` chirurgical sur les 3 trigger-only : - `public.handle_new_user()` (auth.users INSERT trigger) - `public.prevent_role_escalation()` (profiles UPDATE trigger) - `public.rls_auto_enable()` (admin helper, service_role conserve via superuser bypass) Idempotent multi-env via `DO $$ ... pg_proc check $$` blocks (pattern repris de 014). **RLS-essential épargnées** (volontairement non touchées) : `get_my_role`, `get_my_institution_id`, `is_teacher_of_class` — invoquées par ~15 USING clauses RLS, REVOKE FROM PUBLIC casserait les SELECT protégées. Solution structurelle : schema `private` (THI-182 Backlog). **Appliquée live via Supabase CLI** (`supabase db query --linked -f`) post-commit. Vérification empirique : - `handle_new_user` / `prevent_role_escalation` / `rls_auto_enable` → anon_exec=false, auth_exec=false, service_exec=true ✅ - `get_my_role` / `get_my_institution_id` → anon_exec=true, auth_exec=true préservé ✓ - 40 RBAC tests verts (no regression), suite totale 1405/1405 ## Cleanup docs/sessions/ Pattern « mini-prompt next-session » identifié comme **handoff local, jamais à committer** : - `docs/sessions/next-session-thi-42.md` → déplacé vers `.tmp/sessions/` (gitignored) - `docs/sessions/next-session-thi-144.md` (obsolète depuis 10 mai) → supprimé - `docs/sessions/` répertoire vide supprimé - `.gitignore` mis à jour : `docs/sessions/` ignoré par précaution au cas où le pattern réémergerait par habitude ## docs/security-audit-log.md Entry « Supabase Database Advisors hardening — THI-180 » mise à jour avec la senior reverse course #2 (découverte PUBLIC grant) + résultats empiriques post-application + note `auth_leaked_password_protection` = WARN résiduel accepté (feature gated par plan tier Supabase, non disponible sur free plan). ## Refs - Migration 014 (PR #237 mergée) — REVOKE chirurgical FROM anon, authenticated (insuffisant, découvert empirique) - THI-180 (Done) — fermeture trigger-only functions - THI-182 (Backlog) — RLS helpers vers schema `private` - Pattern leçon : `PostgreSQL REVOKE doit toujours inclure PUBLIC` quand l'objectif est de fermer une surface Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: trigger workflow re-run (PR #239 CI not auto-triggered) --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
11 tasks
thierryvm
added a commit
that referenced
this pull request
May 16, 2026
…mplate GRANT (#240) * chore(deps): Supabase prep Q3 2026 — Node 22 + supabase-js 2.105 + template GRANT Préparation aux breaking changes Supabase annoncés (analyse cascade post-PR #239 via agent recherche dédié, sources changelog officiel + discussions GitHub Supabase). ## Changements ### Node 20 → 22 LTS (gate Supabase 30 juin 2026) - `.github/workflows/ci.yml` : `node-version: '22'` (était `'20'`) - `.github/workflows/security-sentinel.yml` : idem - **Pourquoi** : Node 20 EOL le 30 avril 2026 (déjà passé), plus de security updates. Supabase deadline du 30 juin 2026 pour les libs requérant Node 22+. Pas de breaking changes attendus pour la stack TL (TypeScript/Vite/React standard). ### `@supabase/supabase-js` 2.101.1 → 2.105.4 (patches sécurité minor) - 4 patches de sécurité entre les deux versions, aucun breaking change v2 - Bundle impact : supabase chunk 196.96 → 207.09 kB raw (51.78 → 53.46 kB gzip, +1.68 kB acceptable pour security patches) - Validation : 1405 tests verts (no regression), type-check + lint + build clean (6.79s) ### `docs/CONVENTIONS.md` — nouvelle section migrations Supabase Documentation préventive du breaking change **30 octobre 2026** : tables `public.*` ne seront plus exposées automatiquement via PostgREST/GraphQL/`supabase-js`. Template explicite à utiliser pour toute nouvelle migration : ```sql alter table public.<nom> enable row level security; grant select, insert, update, delete on table public.<nom> to anon, authenticated; -- Pour SECURITY DEFINER trigger-only : revoke execute on function public.<nom>() from public; ``` Exceptions valides documentées : audit logs write-only (service_role-only, pas de GRANT), schémas privés (`private`). Pattern leçon migration 015 : `REVOKE FROM anon, authenticated` ne suffit pas — il faut inclure `PUBLIC` (anon/authenticated héritent de PUBLIC par défaut PostgreSQL). ## Out of scope Reportés à des PRs séparées ou backlog tickets : - **Custom OAuth/OIDC providers** (avril 2026) — opportunité Phase 7c LTI à évaluer avant PR #2 LTI - **MCP Supabase OAuth dynamique** (mai 2026) — simplification opérationnelle au prochain re-link MCP - **Postgres 17.6 actuel TL** — déjà sur dernière version, aucune action Postgres 14 deprecation requise - **`@supabase/server` SDK** (mai 2026) — utile si Phase 7c LTI nécessite Vercel Functions, à évaluer en PR #2 ## Refs - Rapport agent recherche Supabase upcoming changes (16 mai 2026) - [Breaking change Data API #45329](https://github.com/orgs/supabase/discussions/45329) - [Node 20 deprecation Supabase](https://supabase.com/changelog/45715-deprecation-notice-dropping-support-for-node-js-20) - [Postgres upgrading guide](https://supabase.com/docs/guides/platform/upgrading) - Migration 015 (PR #239) — pattern leçon REVOKE FROM PUBLIC Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(lti): fix [Check 1+8] tamper signature test flakiness on Node 22 Le test `[Check 1+8] accepts a valid RS256-signed JWT and rejects forged signatures` était instable sur Node 22 (CI fail PR #240) alors qu'il passait sur Node 20. ## Root cause Le test : 1. crée un token valide avec `makeToken()` (jti random) 2. `verify(token)` → success, mémorise jti dans nonceStore 3. tamper UN seul caractère de la signature : `parts[2].replace(/.$/, ...)` 4. `verifyAndExpectFailure(tampered, 'invalid_signature')` → expects sig fail **Problème** : tamper un seul caractère de la signature base64url ne casse pas TOUJOURS la verify (jose@6 + Web Crypto natif Node 22 a un comportement edge case sur certaines positions de bit). Quand la signature tampered **passe** par chance, on atteint le nonce store check qui détecte le jti déjà mémorisé → `replay_detected` au lieu de `invalid_signature`. Le test était passé localement sur Node 22 par chance (random jti aboutissait à un tamper position détecté). CI le re-run dans une queue différente, autre random, autre résultat. ## Fix 1. **jti distincts** pour valid + tampered tokens : isole signature check de nonce check 2. **Tamper 16 caractères** au lieu d'1 : garantit signature invalide sur tous les paths ```ts const tamperedSource = await makeToken({ jti: 'check1-tampered-token' }); const tampered = `${parts[0]}.${parts[1]}.${'A'.repeat(16)}${parts[2].slice(16)}`; await verifyAndExpectFailure(tampered, 'invalid_signature'); ``` ## Validation ``` $ npx vitest run src/test/lti-verifyJwt.test.ts Test Files 1 passed (1) Tests 19 passed (19) ✅ ``` CI Node 22 devrait être stable après ce fix. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore(deps): address Sourcery review — GRANT minimal + .nvmrc + signature types 3 suggestions Sourcery (PR #240) intégrées : ## 1. Template GRANT — principe de moindre privilège Avant : `grant select, insert, update, delete ...` par défaut (trop permissif). Après : `grant select` par défaut + commentaire d'extension pour CRUD optionnel. Évite d'encourager des grants trop larges sur les nouvelles tables. ## 2. Function signature SECURITY DEFINER — types explicites Avant : exemple générique `revoke execute on function public.<nom_fonction>() from public`. Après : note explicite sur la signature complète (types arguments) + 2 exemples (avec/sans paramètres). PostgreSQL exige une signature exacte pour identifier une fonction surchargée — sans cela, le `REVOKE` peut silencieusement cibler la mauvaise fonction. ## 3. Node version centralisée — `.nvmrc` + `engines` - Nouveau `.nvmrc` : `22` (single source of truth) - `package.json` : ajout `engines.node: ">=22.0.0"` - `.github/workflows/ci.yml` + `security-sentinel.yml` : `node-version-file: '.nvmrc'` au lieu de hardcode `node-version: '22'` Évite la dérive entre workflows : si on bump Node ultérieurement, un seul fichier à modifier (`.nvmrc`), les workflows et `npm install` (via `engines`) suivent automatiquement. ## 4. Bonus typo FR `"Pattern leçon migration 015"` → `"Pattern vu lors de la migration 015"` (Sourcery comment 1 individual). ## Validation ``` $ npx vitest run Tests 1405 passed | 20 skipped (1425) ✅ $ npm run lint ✅ clean ``` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
thierryvm
added a commit
that referenced
this pull request
May 16, 2026
…ng + docs THI-186 (#243) * chore: post-session 16-17 mai polish — Sourcery #242 fixups + Sidebar branding + docs THI-186 PR de clôture de la session marathon 16-17 mai 2026 (THI-131 LTI Auth MVP → THI-180 SECURITY DEFINER → Supabase prep Q3 → THI-186 critical data leak). Groupe trois préoccupations cohérentes "post-merge polish" : ## 1. Sourcery review PR #242 — addressed @Thierry a transmis 2 retours Sourcery sur le round 2 du fix THI-186 : - **`applyLegacyOwnerMigration(raw)` extracted** en helper exporté depuis `src/app/context/ProgressContext.tsx`. Single source-of-truth entre prod code et tests d'isolation — élimine le risque de drift entre la logique de migration et son test mirror (anti-pattern Sourcery a flag à juste titre). - **`try/catch` narrow autour de `JSON.parse` seulement** dans `loadProgress()`. Avant : un try/catch global wrappait tout — `localStorage.getItem` + migration + parse. Si une erreur inattendue survenait dans la migration, elle était silencieusement avalée. Après : `readRawProgress()` helper isolé pour `localStorage.getItem`, `applyLegacyOwnerMigration()` isolé sans try/catch (logique pure), et `try/catch` resserré uniquement autour de `JSON.parse`. Tests adaptés pour utiliser `applyLegacyOwnerMigration` importé du module au lieu d'une copie inline. +1 test : `applyLegacyOwnerMigration` retourne false sur raw=null. ## 2. Sidebar branding fix — Terminal Master → Terminal Learning `src/app/components/Sidebar.tsx:99` affichait toujours « Terminal / Master » dans le logo sidebar `/app` (desktop + mobile). C'est le même vestige de branding que `Layout.tsx` mobile header corrigé en THI-153 (PR #234), mais cette surface précise avait été manquée. @Thierry l'a flag pendant cette session. Rebadge unique vers « Terminal / Learning ». Cohérence B2B retrouvée sur toutes les surfaces brand. ## 3. Docs fin session - **CHANGELOG.md** : entry complète THI-186 (root cause, smoking gun Supabase live, fix en 2 rounds + polish, cleanup prod data, pourquoi pas attrapé avant, ticket THI-187 backlog) - **docs/plan.md** ligne 3 : freshness header THI-186 (1405 → 1417 tests, 24 → 0 Google contaminated) - **docs/ROADMAP.md** ligne 3 : freshness EN version - **STORY.md** : section narrative "La nuit du 16 au 17 mai — Le bug qui dormait depuis six semaines" (ton première personne respecté, méta-réflexive sur la lacune `rbac-flow-tester` qui couvre serveur mais pas state lifecycle client cross-session) + freshness footer ## Validation ``` $ npm run type-check + lint ✅ clean $ npx vitest run 1417 passed | 20 skipped ✅ $ npm run build ✓ built in 6.34s ``` ## Hors scope (suivi) - **THI-187** (Backlog Medium) — UX feature "Réinitialiser ma progression" dans Settings (demande @Thierry pendant la session) - **Implicite, à arbitrer next session** : étendre `rbac-flow-tester` avec scénarios multi-session OU créer agent `client-state-isolation-tester` (lacune découverte cette session) - **THI-182** Backlog Low — RLS helpers vers schema private (chantier post-deadline 10 juin) - **THI-183/184/185** Backlog — supply chain undici monitor + LTI PR #2 gates ## Refs - PR #241 (THI-186 round 1 owner-tracking) - PR #242 (THI-186 round 2 migration force-clear) — Sourcery review addressed here - Linear THI-186 (Done après cette PR), THI-187 (Backlog Medium) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: address Sourcery review #243 — narrow signature + resilient localStorage access 3 retours Sourcery sur PR #243 traités : ## 1. Signature `applyLegacyOwnerMigration(raw)` simplifiée Avant : `applyLegacyOwnerMigration(raw: string | null): boolean` — l'argument `raw` était utilisé seulement comme un null check, ce qui rendait la signature trompeuse. Après : `applyLegacyOwnerMigrationIfNeeded(): boolean` — sans argument, lit `localStorage` directement via les helpers internes (`readRawProgress` + `getStoredOwner`). Le nom reflète mieux le comportement (idempotent / no-op si pas nécessaire) et élimine le faux contrat sur `raw`. ## 2. Bug risk : `localStorage.getItem(STORAGE_OWNER_KEY)` non protégé Sourcery a identifié à juste titre un risque : `localStorage.getItem(STORAGE_OWNER_KEY)` était appelé directement (sans try/catch) dans la fonction de migration, ce qui pouvait lever `SecurityError` ou quota errors sur navigateurs restrictifs (private mode, storage désactivé), faisant échouer `loadProgress()` entier. Fix : utiliser le helper `getStoredOwner()` existant qui encapsule déjà un try/catch propre. Tous les accès à `localStorage` dans cette fonction passent désormais par des helpers résilients (`readRawProgress`, `getStoredOwner`) — protection uniforme. ## 3. Tests adaptés - Renommage `applyLegacyOwnerMigration` → `applyLegacyOwnerMigrationIfNeeded` dans `progressContextIsolation.test.ts` - Test edge case : `returns false (no clear) when no progress is cached` (remplace l'ancien test `raw=null` qui n'a plus de sens avec la nouvelle signature) ## Hors scope (suggestion long-terme) Sourcery a aussi suggéré une abstraction Storage minimale pour faciliter les futurs refactors et tests sans dépendre du global. Bon pattern, mais hors scope d'une PR polish — backlog si besoin se présente. ## Validation ``` $ npx vitest run src/test/progressContextIsolation.test.ts Tests 12 passed (12) ✅ $ npm run type-check ✅ clean ``` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This was referenced May 17, 2026
thierryvm
added a commit
that referenced
this pull request
May 18, 2026
… link (#255) THI-42 PR #1 — Profile Hub minimal sous /app/profile. **Scope D1-D4 verrouillés** (backup décisionnaire 17/05) : - D1 : pas de SettingsPage.tsx créée (lien /app/settings THI-112 existant) - D2 : Avatar OAuth read-only (upload custom différé PR #2) - D3 : Switcher env display read-only (mention "Modifier dans la sidebar") - D4 : lti-auditor déjà livrée **Livré (5 fichiers, +418/-17 + a11y fix)** : - ProfilePage.tsx (nouveau, 186L) — auth guard avec initialized check (security-auditor H1 fix), 3 sections Identité/Environnement/Paramètres - UserAvatar.tsx (nouveau, 43L) — extract DRY pour réutilisation Profile + UserMenu - UserMenu.tsx (refactor) — "Mon profil" link card+compact variants AVANT "Se déconnecter" + drive-by fix 4 hex hardcodés → CSS vars (ui-auditor CRITICAL) - routes.ts (+4L) — route /app/profile + lazyWithRetry - profilePage.test.tsx (nouveau, 156L) — 10 tests : auth guard loading state, fallback, display name fallback, provider badges (GitHub/Google/Email), env section, settings link, back-link **Audits cascade** : - ui-auditor : 4 CRITICAL pré-existantes fixées (hex → CSS vars) + W1 focus-visible back-link - security-auditor 9.2/10 : H1 race condition auth guard FIXÉ (initialized + loading state) - M1 CSP googleusercontent wildcard + M2 avatar URL validation → backlog Linear post-merge **Voie A validation** (terminal-learning-git-feat-1abc7a) : - Desktop 1280×800 PASS : login test.student, profile render, sidebar Mon profil, settings link, back-link, signout régression THI-207 (auth + 6 ai_* LS + 2 ai_* SS wipes), 0 console error, 0 4xx-5xx - iPhone 14 (390×844) PASS : 0 horizontal overflow, tap targets ≥ 44×44 après fix back-link (commit 504554c : min-h-11 py-3) **Tests** : 1434 + 10 = 1444 pass · lint + type-check clean **Backlog post-merge** (à tracer) : - security-auditor M1 CSP googleusercontent wildcard - security-auditor M2 avatar URL validation defense-in-depth - RequireAuth wrapper Layout.tsx (recommandation security) - Sprint 2 next : THI-42 PR #2 (avatar upload Supabase Storage) ou PR #3 (UserMenu role-aware) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
5 tasks
thierryvm
added a commit
that referenced
this pull request
May 24, 2026
Thread #1 (security `autoComplete`) déjà résolu par le fixup précédent W1 (`current-password` → `one-time-code`). Reste 2 threads à fermer : ## Thread #2 (nitpick bug_risk) — align disabled state with trim `src/app/components/ai/AiPassphrasePrompt.tsx` — bouton "Déverrouiller" était `disabled={passphrase.length === 0}` mais `handleSubmit` faisait `.trim()` avant validation. Conséquence : un user tapant uniquement des espaces voyait un bouton enabled qui retournait ensuite l'erreur "Passphrase requise". Inconsistance UX corrigée : `disabled={passphrase.trim().length === 0}`. ## Thread #3 (suggestion testing) — coverage forgetKey clears passphrase `src/test/ai/AiTutorPanel.test.tsx` — ajout d'un test explicite qui : 1. Démarre depuis l'état encrypted-unlocked (passphrase saisie) 2. Clique sur "Oublier ma clé" 3. Vérifie que AiKeySetup réapparaît (clé effacée) 4. Vérifie que le passphrase prompt NE réapparaît PAS (state cleared) 5. Vérifie que la conversation n'est pas visible Couvre le 4e chemin de clear documenté en commentaire (`onClick` du bouton forgetKey ligne 295-301 de AiTutorPanel.tsx). ## Métriques - Tests : 1660 → **1661** (+1) — 0 régression - 21/21 tests AiTutorPanel PASS Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
thierryvm
added a commit
that referenced
this pull request
May 24, 2026
…nête (#287) * fix(ai-tutor): THI-263 — fix encrypted mode A1 + privacy threat model honnête **Finding A1 (HIGH VERIFIED)** identifié par `llm-security-auditor` Opus méthode 7 couches le 23/05/2026 (audit nuit stress-test agents) : `AiTutorPanel` n'envoyait jamais la `passphrase` à `useAiTutor`, donc `kmGetKey(provider, undefined)` retournait null systématiquement pour les utilisateurs en mode encrypted opt-in. Erreur `no_key` permanente, défense AES-GCM/PBKDF2 vantée dans 3 ADRs **morte à l'usage**. Raté par les 3 audits du 16/05 (PR #230) qui ont validé V1.0.1 à 9.4/10 — aucun n'avait tracé la branche runtime « encrypted opt-in → panel send ». ## Option A retenue par @Thierry (lazy passphrase prompt) Le mode encrypted opt-in reste promis dans `/privacy#ai-processing` (qualité premium + honnêteté). Option B (retirer la feature) rejetée : admettre publiquement qu'on ne tient pas une promesse vantée envoie un signal pire que la corriger. ## Changements ### `src/app/components/ai/AiPassphrasePrompt.tsx` (NEW, 90 lignes) Composant minimal affiché entre AiKeySetup et la conversation quand la clé stockée est chiffrée et qu'aucune passphrase n'est encore en mémoire. Input `type="password"` + `autocomplete="current-password"` + `spellCheck= false` pour éviter form-fillers / suggestions d'extension. ### `src/app/components/ai/AiTutorPanel.tsx` - Import `isEncrypted as kmIsEncrypted` + `AiPassphrasePrompt` - 2 nouveaux states React : `isEncryptedMode` (boolean) + `passphrase` (string). La passphrase reste **uniquement** en mémoire React — jamais localStorage, jamais sessionStorage, jamais log Sentry - `useEffect` détection : check `kmIsEncrypted(provider)` au mount / changement provider / open panel - `passphrase` passée à `useAiTutor({ ..., passphrase })` uniquement quand `isEncryptedMode && passphrase.length > 0`, sinon `undefined` (préserve le happy path plain mode) - `closePanel` callback synchrone : `setOpen(false)` + `setPassphrase('')`. Évite l'anti-pattern `react-hooks/set-state-in-effect` flaggé par ESLint. Routé sur les 3 paths de fermeture (X button, overlay click, Escape). - Reset passphrase aussi au switch de provider et au `forgetKey` ### `src/app/components/PrivacyPolicy.tsx` — section ai-processing Ajout d'un sous-bloc « Modèle de menace du mode chiffré (transparence) » qui documente précisément : - ✅ Couvert : XSS (script malveillant) + Inspection DevTools - ❌ Non couvert : extension navigateur avec permission `<all_urls>` (vraie défense = Web Worker isolation V1.5, ticket THI-114) Pas de fausse promesse RGPD. Conseil pragmatique en attendant V1.5 : profil navigateur dédié sans extension tierce pour les clés à valeur élevée. ### `src/test/ai/AiTutorPanel.test.tsx` — +3 tests THI-263 1. Passphrase prompt visible quand clé chiffrée stockée 2. Conversation visible après submit de la passphrase 3. Passphrase clear au close panel (Escape) → re-prompt à la ré-ouverture ## Métriques - Tests : 1655 → **1658** (+3) — 0 régression - Type-check + lint : clean - 18/18 tests AiTutorPanel PASS (incluant les 3 nouveaux) ## Audits - `prompt-guardrail-auditor` : invoqué en parallèle (audit-gate ADR-005 obligatoire pour `src/lib/ai/*` et `src/app/components/ai/*`) - `security-auditor` : transverse, gate après merge si nécessaire ## Refs - Finding A1 source : `llm-security-auditor` audit 2026-05-23 nuit stress-test agents — rapport intégré dans Obsidian `terminal-learning/cc-handoffs/2026-05-23-2300-agents-stress-test-night.md` - Ticket Linear : THI-263 (HIGH, 3 options proposées, Option A retenue) - Baseline référente : `docs/audits/ai-tutor-v1-2026-05-16.md` (9.4/10 V1.0.1) - ADR-002 : BYOK 4-tiers (gate UX guide vers encryption) - ADR-005 : V1 implementation (encrypted opt-in défense XSS) - ADR-008 : V1.1.0 anti-frictions - Backlog connexe : THI-114 (Web Worker isolation V1.5 = vraie défense extension navigateur) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(ai-tutor): address prompt-guardrail-auditor findings W1+W2+W3+R1+R2 Suite audit `prompt-guardrail-auditor` (Sonnet 4.6) 2026-05-24 sur le fix THI-263 — verdict CONDITIONAL PASS, 0 CRITICAL, 3 warnings + 3 recos. Tous les findings actionnables sont adressés dans ce commit fixup. ## W1 (MEDIUM, prio haute audience scolaire) — autocomplete one-time-code `src/app/components/ai/AiPassphrasePrompt.tsx:65` — remplace `autocomplete="current-password"` par `autocomplete="one-time-code"`. Motif : sur appareils partagés (salle info scolaire, audience cible ADR-005), les password managers (1Password, Bitwarden, LastPass) auraient pu auto-remplir la passphrase à partir d'entrées sauvegardées par un autre utilisateur sur le même domaine `terminallearning.dev`. `one-time-code` signale au navigateur que la valeur est éphémère et projet-spécifique, réduisant l'auto-fill non désiré. ## W3 (INFO) — retrait mention `no_key` interne dans le DOM `src/app/components/ai/AiPassphrasePrompt.tsx:96-99` — le texte d'aide nommait le code d'erreur API interne `no_key`. Remplacé par un message purement orienté UX qui ne révèle plus l'API technique : > "Si la passphrase est incorrecte, le tuteur ne pourra pas répondre. > Clique alors sur « Oublier ma clé » pour en reconfigurer une nouvelle." ## R2 (cosmétique) — Privacy : "réouverture" plus précis qu'"ouverture" `src/app/components/PrivacyPolicy.tsx` section ai-processing — clarification : la passphrase est demandée à chaque **réouverture** (pas "à chaque ouverture"). Un user qui garde le panel ouvert n'est re-demandé qu'à la prochaine réouverture, comportement correct codé dans `closePanel`. ## W2 (LOW) — test "close via overlay click clears passphrase" `src/test/ai/AiTutorPanel.test.tsx` — nouveau test symétrique à "clears the passphrase when the user closes the panel" mais via overlay click au lieu d'Escape. Couvre les 3 paths de fermeture documentés (X button / overlay / Escape) côté machine. Filet de régression : si un futur refactor remplace `onClick={closePanel}` sur l'overlay par `setOpen(false)` seul, A1 ressurgirait silencieusement. ## R1 (durcissement) — test "switching provider clears passphrase" `src/test/ai/AiTutorPanel.test.tsx` — nouveau test qui exerce le 4e path de clear documenté en commentaire (`setProvider` callback ligne 109-121 de AiTutorPanel) mais non testé jusqu'ici. Valide qu'une passphrase OpenRouter saisie disparaît après switch vers Anthropic et que retour vers OpenRouter ré-affiche le passphrase prompt. ## Hors scope cette PR (à valider empirique par @Thierry) - R3 (autoFocus mobile) : le `autoFocus` sur input password déclenche le clavier virtuel iOS/Android immédiatement à l'ouverture du AiPassphrasePrompt. Validation empirique recommandée @Thierry sur iPhone PWA standalone — si UX cassée, fix follow-up avec autoFocus conditionnel desktop-only. ## Métriques - Tests : 1658 → **1660** (+2) — 0 régression - Lint : 0 error, 0 warning - 20/20 tests `AiTutorPanel` PASS ## Audit verdict mis à jour - `prompt-guardrail-auditor` baseline 23/05 : 9.4/10 CONDITIONAL PASS - Post-W1+W2+W3+R1+R2 : tous warnings résolus, conversion vers PASS - Cible 9.5/10 toujours conditionnée à H2 undici + R3 M2-AI + R5 H4-AI (hors scope ce diff) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(ai-tutor): address Sourcery review threads #2 + #3 sur PR #287 Thread #1 (security `autoComplete`) déjà résolu par le fixup précédent W1 (`current-password` → `one-time-code`). Reste 2 threads à fermer : ## Thread #2 (nitpick bug_risk) — align disabled state with trim `src/app/components/ai/AiPassphrasePrompt.tsx` — bouton "Déverrouiller" était `disabled={passphrase.length === 0}` mais `handleSubmit` faisait `.trim()` avant validation. Conséquence : un user tapant uniquement des espaces voyait un bouton enabled qui retournait ensuite l'erreur "Passphrase requise". Inconsistance UX corrigée : `disabled={passphrase.trim().length === 0}`. ## Thread #3 (suggestion testing) — coverage forgetKey clears passphrase `src/test/ai/AiTutorPanel.test.tsx` — ajout d'un test explicite qui : 1. Démarre depuis l'état encrypted-unlocked (passphrase saisie) 2. Clique sur "Oublier ma clé" 3. Vérifie que AiKeySetup réapparaît (clé effacée) 4. Vérifie que le passphrase prompt NE réapparaît PAS (state cleared) 5. Vérifie que la conversation n'est pas visible Couvre le 4e chemin de clear documenté en commentaire (`onClick` du bouton forgetKey ligne 295-301 de AiTutorPanel.tsx). ## Métriques - Tests : 1660 → **1661** (+1) — 0 régression - 21/21 tests AiTutorPanel PASS Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
4 tasks
thierryvm
added a commit
that referenced
this pull request
May 24, 2026
…d-trip) (#289) * fix(ai-tutor): THI-271 — passphrase trim() asymmetry (whitespace round-trip) Follow-up de THI-263 — bug latent identifié par Sourcery review sur PR #287 (Overall comment "trimming the passphrase before submission can change a user's actual secret"), confirmé par @Thierry. ## Bug `AiKeySetup.tsx:85-86` sauvegardait la passphrase BRUTE (validation raw `passphrase.length`, pas de trim) → PBKDF2 dérive la clé avec espaces leading/trailing si présents. `AiPassphrasePrompt.tsx:35` faisait `const trimmed = passphrase.trim()` avant `onSubmit(trimmed)` → unlock utilise passphrase TRIMMED. **Conséquence** : un user qui sauve une passphrase contenant des espaces leading/trailing (`" mySecret "`) ne peut JAMAIS la déverrouiller via le panel — la PBKDF2 reçoit `"mySecret"` (trimmed) au lieu de `" mySecret "` (sauvée), décryption AES-GCM échoue, retour `no_key`. ## Fix `src/app/components/ai/AiPassphrasePrompt.tsx` : - `handleSubmit` : retiré le `.trim()`, `onSubmit(passphrase)` byte-for-byte - `disabled` button : aligné sur `passphrase.length === 0` (cohérent avec AiKeySetup.tsx:85 qui utilise raw length) - Commentaire mis à jour pour expliquer le pourquoi (round-trip PBKDF2 + valid chars = whitespace possible) ## Test `src/test/ai/AiTutorPanel.test.tsx` : nouveau test "preserves leading and trailing whitespace in the passphrase (round-trip)" qui : 1. Save une passphrase avec espaces leading+trailing via `kmSaveKey` 2. Tape la passphrase identique (espaces inclus) dans AiPassphrasePrompt 3. Vérifie que la décryption réussit → conversation surface apparaît Le test ÉCHOUERAIT avec l'ancien code (trim côté unlock casserait round-trip). Le test PASSE avec le fix → régression-proof. ## Métriques - Tests : 1661 → **1662** (+1) — 0 régression - 22/22 tests AiTutorPanel PASS ## Note Sourcery review Le commentaire spécifique #1 (autoComplete current-password → new-password) n'est PAS adressé dans cette PR car déjà fixé en mieux dans PR #287 fixup (`autoComplete="one-time-code"`, plus restrictif que `new-password` suggéré par Sourcery). Les comments spécifiques #2 (button disabled) et #3 (tests provider switch + forgetKey) sont déjà adressés dans les fixups PR #287 — confirmés présents sur main au moment de ce commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(ai-tutor): THI-271 — centralize passphrase validation (Sourcery review) Suite Sourcery overall comment sur PR #289 : *"Consider centralizing the passphrase validation logic (raw length rules, whitespace semantics) in a shared helper or module used by both AiKeySetup and AiPassphrasePrompt so the save/unlock behavior can't drift again in future changes."* Point architectural légitime : le bug THI-271 (trim() asymmetry) est exactement le type de drift qui se produit quand deux components valident indépendamment. Centralisation = filet anti-régression future. ## Création `src/lib/ai/passphrase.ts` (~70 lignes) Single source of truth : - `MIN_PASSPHRASE = 8` constante exportée - `validatePassphrase(raw, 'save' | 'unlock'): PassphraseValidation` - `save` : exige MIN_PASSPHRASE caractères - `unlock` : exige seulement non-vide (la décryption AES-GCM est le vrai validateur final — message générique pour éviter timing oracle) - `validatePassphraseConfirm(passphrase, confirm)` : save-side only Contrat **byte-for-byte** documenté : pas de trim côté validation, whitespace est un caractère valide d'un secret. ## Refactor consommateurs ### `src/app/components/ai/AiKeySetup.tsx` - Retire la constante locale `MIN_PASSPHRASE` (importée du module) - `passphraseProblem` useMemo délègue à `validatePassphrase('save')` + `validatePassphraseConfirm` - `onSubmit` délègue de même ### `src/app/components/ai/AiPassphrasePrompt.tsx` - `handleSubmit` délègue à `validatePassphrase('unlock')` - Code plus court, intention explicite via le mode 'unlock' ## Tests `src/test/ai/passphrase.test.ts` (13 tests) Pin chaque règle : - save : empty rejected, < MIN rejected, exactly MIN ok, > MIN ok, whitespace counts toward length - unlock : empty rejected (message spécifique), single char ok, leading/trailing whitespace preserved, MIN NOT enforced (legacy) - confirm : partial input ok, mismatch rejected, exact match ok (incl. whitespace), whitespace significant (no trim) Toute future PR qui réintroduit un trim() ou modifie MIN_PASSPHRASE sans cohérence cassera la suite. ## Métriques - Tests : 1662 → **1675** (+13 unit + 0 régression sur les composants) - 47/47 tests des 3 suites concernées PASS - Lint clean ## Audit prompt-guardrail-auditor Verdict PASS (audit en parallèle sur la version pré-centralisation, score 9.2/10 inchangé, 0 régression baseline). La centralisation ajoute un filet de robustesse architecturale sans modifier la surface d'attaque (les validations restent dans le même processus de rendu React). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
3 tasks
thierryvm
added a commit
that referenced
this pull request
May 30, 2026
…gation niv.1) (#331) * feat(curriculum): add foundational "Anatomie d'une commande" lesson (Navigation, niv.1) PR #2 du chantier enrichissement. Avant cette leçon, les options (-l, -a, -la, -p, -r…) étaient enseignées au cas par cas, dispersées, sans modèle mental commun. Cette leçon fondatrice généralise le pattern en modèle universel `commande [options] [arguments]`, tôt dans la progression (entre ls-la et cd). - Nouvelle leçon `command-anatomy` (Module Navigation, niveau 1) : options courtes vs longues, combinaison (-la = -l -a), options à valeur, séparateur `--` (note pro rm -- -rf), renvoi man/--help. Variante Windows complète (paramètres PowerShell, Get-Help). Approche métier : lire les options de n'importe quel outil. - `validateCommandAnatomy` : exercice `man ls`, forward-compatible (accepte aussi `Get-Help ls` et `<cmd> --help` pour le Get-Help natif prévu PR help/man). - 1er bloc de tests `man` dans terminalEngine.test.ts (closait un WARNING curriculum-validator : man n'avait aucun test moteur). - Drift guards 65 → 66 sur les 6 sources de vérité : TOTAL_LESSONS + MODULE_PREVIEWS (landingContent), JSON-LD numberOfLessons (index.html), export CSV régénéré, README/ARCHITECTURE/ROADMAP/CLAUDE.md, ancrages de test. Gate curriculum-validator (pré-modif) : 0 CRITICAL. test-runner : 1834 PASS. type-check + lint clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: sync CHANGELOG (#330 backfill + anatomy) + STORY chapter + plan.md banner - CHANGELOG: backfill PR #330 (Sécu IA — #330 had merged without an entry) + the anatomy lesson entry. - STORY.md: new 30 mai 2026 chapter on the enrichment chantier (security-first ordering + the invisible-gap lesson), keeping the established narrative tone. - plan.md: chantier progress prepended to the status banner + global count 65→66. Doc-sync for the session's two chantier PRs (#330 merged, #331 this branch). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(curriculum): plain prose in command-anatomy lesson (renderText has no bold support) Voie A visual validation caught literal ** asterisks: LessonPage renderText only parses backtick code spans, not markdown bold/lists. Reworded the command-anatomy blocks to prose (backtick code preserved). A pre-existing latent bug affects 22 other lessons that also use ** — tracked as a separate follow-up (fix = add **bold** support to renderText, not strip it). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
thierryvm
added a commit
that referenced
this pull request
Jun 4, 2026
…ode-reviewer) - active_users_7d now filters `completed = true` like the sibling metrics. Redundant today (ProgressContext only upserts completed rows — verified) but defensive + consistent if the upsert ever stamps completed_at on lesson-start. Re-applied to prod via CREATE OR REPLACE (idempotent). - useAdminAnalytics surfaces BOTH RPC errors (Promise.all) instead of masking the second — easier debugging when both fail from the same root cause. code-reviewer findings #1 + #4 (THI-234). #2/#5 (runtime narrowing, useId) → backlog. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
thierryvm
added a commit
that referenced
this pull request
Jun 4, 2026
…vity heatmap) (#370) * feat(admin): THI-234 Phase 9 analytics widgets — Santé Supabase + activity heatmap Replace 2 of the 4 AdminPanel placeholder widgets with live data: - Santé Supabase: total users, active 7d, lessons completed 30d/total, top 5 lessons - Activité élèves: GitHub-style 52-week heatmap (THI-77) Data via 2 SECURITY DEFINER RPCs (migration 032) with an internal super_admin gate (raise PERMISSION_DENIED otherwise) + REVOKE public/anon — cross-user aggregates without exposing rows to non-admins. Isolation proven empirically via REST+JWT: super_admin 200, student 400 PERMISSION_DENIED, anon 401. Sentry + Application health stay placeholders (Phase 9 v2 — need API route + secret). Counts only, no PII. - migration 032 (applied to prod, additive) - Database.Functions types for the 2 RPCs (zero any) - useAdminAnalytics hook (parallel RPC fetch, read-only) - pure heatmap helpers (buildHeatmapGrid/heatmapLevel/heatmapMax) + tests - AdminPanel widgets data-driven (loading/error/empty states) - 24 new tests; adminPanel.test rewritten for the new widgets Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(admin): conditional heatmap aria-label when no activity (ui-auditor W2) When the platform has zero completions the aria-label said "… jour le plus actif 0" which is confusing for screen readers. Use a clear "Aucune activité sur les 52 dernières semaines." in that case. THI-234. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(mobile): <select> text-base to prevent Safari iOS auto-zoom (mobile-auditor) The status-triage <select> (THI-320) used text-sm (14px) → Safari iOS auto-zooms the page on focus when a form control is < 16px. Bump to text-base (16px). Out-of-scope drive-by surfaced by the THI-234 mobile gate (the select renders on the admin page this PR touches). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(admin): align active_users_7d filter + surface both RPC errors (code-reviewer) - active_users_7d now filters `completed = true` like the sibling metrics. Redundant today (ProgressContext only upserts completed rows — verified) but defensive + consistent if the upsert ever stamps completed_at on lesson-start. Re-applied to prod via CREATE OR REPLACE (idempotent). - useAdminAnalytics surfaces BOTH RPC errors (Promise.all) instead of masking the second — easier debugging when both fail from the same root cause. code-reviewer findings #1 + #4 (THI-234). #2/#5 (runtime narrowing, useId) → backlog. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Nouvelles commandes
aboutdonatesupportdonatehall-of-fameDétails techniques
OutputLineexistant (type: 'info' | 'success')HELP_TEXTmis à jour pour lister les nouvelles commandesTest plan
aboutdans le terminal → bloc ASCII avec infos projetdonate→ bloc ASCII avec lienssupport→ identique àdonatehall-of-fame→ message placeholderhelp→ nouvelles commandes visibles dans la listenpm run build✅🤖 Generated with Claude Code