Skip to content

feat: personal learning — favorites, study notes, and account deletion - #198

Merged
ayoub3bidi merged 6 commits into
developfrom
feat/favorites-and-notes
Jul 5, 2026
Merged

feat: personal learning — favorites, study notes, and account deletion#198
ayoub3bidi merged 6 commits into
developfrom
feat/favorites-and-notes

Conversation

@ayoub3bidi

@ayoub3bidi ayoub3bidi commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Contribution workflow

Description

Adds personal learning features for signed-in users: star algorithms as favorites (20-slot limit on Free tier), write per-algorithm study notes inside the Algorithm Insight panel, and delete your account from Profile Settings. Anonymous users can still use the visualizer; favorites require sign-in and notes inherit the existing Insight panel gate.

The Insight panel is refactored into a Learn | My Notes tabbed layout (Code Panel–style segmented tabs at the top). TipTap loads lazily on first visit to My Notes. Favorites appear first in the Settings panel with a global favorites dropdown and per-row star toggles in the algorithm picker.

Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 📚 Documentation update
  • 🎨 Style/UI improvement
  • ⚡ Performance improvement
  • 🧪 Test addition or improvement
  • 🔧 Chore (maintenance, dependencies, etc.)

Related Issues

Fixes #

Changes Made

Favorites (sign-in only)

Study notes (Insight panel)

Account deletion

Database (Supabase)

UX fixes bundled in this PR

Docs, legal, i18n

Tests

  • New: favoritesService, notesService, useFavorites, useNoteAutosave, useFullScreen, FavoritesDropdown, AlgorithmNotesTab, plus component/service test updates
  • Extended: src/test/supabaseMock.js — insert/delete/upsert/order, functions.invoke

Algorithm Details (if applicable)

N/A — no new algorithms.

Testing

  • All existing tests pass (pnpm test:coverage)
  • New tests added for new functionality
  • Manual testing completed (favorites, notes autosave, delete-account flow)
  • Cross-browser testing (if UI changes) — recommended on staging after deploy

Test Results

Test Files  146 passed (146)
     Tests  1772 passed (1772)
pnpm lint          — pass
pnpm format:check  — pass
pnpm build         — pass

Bundle note: NoteEditor-*.js136 KB gzip (lazy-loaded; not in main bundle).

Manual verification

Prerequisites: Signed-in Google user on dev/staging with Supabase env vars configured.

Favorites

  1. Settings panel → Favorite Algorithms dropdown is the first section
  2. Open algorithm dropdown → star an algorithm (hover on desktop; always visible on mobile)
  3. Confirm slot counter (e.g. 1 / 20); at 20 slots, unfavorited star shows slot-full message
  4. Anonymous: star click opens sign-in prompt (favorite feature key)
  5. Favorites dropdown → pick item → lands on correct category + algorithm

Notes

  1. Open Algorithm Insight panel → Learn | My Notes tabs at top; algorithm name subtitle below
  2. My Notes → type content → autosave status (Saving… / Saved)
  3. Switch tab or algorithm → content persists; typing f does not toggle fullscreen
  4. RTL (ar): editor dir="auto", toolbar LTR

Account deletion

  1. Profile Settings → Danger zone → type DELETE → account removed, session cleared

Screenshots/GIFs

Before After
Insight panel: single insight view Learn | My Notes tabs; favorites stars in algorithm dropdown
Settings: category mode first Favorites dropdown first in Settings panel
Email-only deletion in privacy copy Self-service delete in Profile Settings

Add screenshots from staging QA before merge if desired.

Code Quality

  • Code follows the project's coding standards
  • ESLint passes (pnpm lint)
  • Prettier formatting applied (pnpm format:check)
  • No new console errors in auth/notes/favorites flows
  • Code is properly documented with JSDoc (if applicable)

Performance Impact

  • Performance impact assessed and acceptable
  • TipTap + DOMPurify in a lazy chunk (NoteEditor); Insight panel already lazy from VisualizerApp
  • Main bundle size warning unchanged; note editor chunk ~136 KB gzip

Accessibility

  • Keyboard navigation works correctly (tablist roles; star aria-pressed; fullscreen shortcuts suppressed while editing)
  • Screen reader compatibility maintained (dialog aria-label, tab labels, save status aria-live)
  • Color contrast meets WCAG guidelines (existing theme tokens)
  • Focus indicators are visible

Breaking Changes

  • None — all features are additive; anonymous tier unchanged except new sign-in prompts for favorites

Checklist

  • I have completed the Contribution workflow checklist at the top of this template
  • My code follows the project's style guidelines
  • I have performed a self-review of my code
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Supabase migration + delete-account Edge Function deployed to qketsapzqpzmccljfjcm

Additional Notes

Explicitly out of scope

  • Analytics / event tracking for favorites or notes (separate PR)
  • Pro-tier note search, export, expanded favorite slots

Deploy / ops checklist for reviewer

  1. Confirm migration personal_learning_favorites_notes is applied on Supabase (already on live project)
  2. Confirm Edge Function delete-account is active with JWT verification enabled
  3. Smoke-test favorites + notes + delete on staging with a test account
  4. Legal pages updated (English only in src/content/legal/)

Reviewer Guidelines:

  • Check that all tests pass
  • Verify favorites slot limit and sign-in gate for anonymous users
  • Test notes autosave across tab/algorithm switches
  • Confirm delete-account CASCADE removes favorites and notes
  • Ensure no service-role key in client bundle

Summary by CodeRabbit

  • New Features

    • Added algorithm favorites with a dropdown, star controls, and free-tier slot-limit gating.
    • Introduced algorithm study notes with a rich-text editor, autosave, and clear save-state feedback.
    • Refreshed the learning panel into “Learn” vs “My Notes” tabs with improved accessibility.
    • Added self-service account deletion in Profile Settings.
  • Bug Fixes

    • Prevented full-screen keyboard shortcuts from triggering while typing in inputs or editors.
    • Improved notes flushing when closing the panel or switching tabs.
  • Documentation

    • Updated privacy/terms and added guidance for account deletion and related backend flows.

@ayoub3bidi ayoub3bidi self-assigned this Jul 4, 2026
@netlify

netlify Bot commented Jul 4, 2026

Copy link
Copy Markdown

Deploy Preview for dev-bayanflow ready!

Name Link
🔨 Latest commit e5d8a00
🔍 Latest deploy log https://app.netlify.com/projects/dev-bayanflow/deploys/6a4a26c10f45b2000981c1d8
😎 Deploy Preview https://deploy-preview-198--dev-bayanflow.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions github-actions Bot added documentation Improvements or additions to documentation style Improve styling, design, and animation dependencies tests labels Jul 4, 2026
@socket-security

socket-security Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​tiptap/​extension-placeholder@​3.27.11001006999100
Added@​tiptap/​pm@​3.27.1991007399100
Added@​tiptap/​starter-kit@​3.27.1991007599100
Added@​tiptap/​react@​3.27.1991007999100
Addedisomorphic-dompurify@​3.18.010010010096100

View full report

@socket-security

socket-security Bot commented Jul 4, 2026

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: npm css-tree is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: pnpm-lock.yamlnpm/isomorphic-dompurify@3.18.0npm/css-tree@3.2.1

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/css-tree@3.2.1. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm data-urls is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: pnpm-lock.yamlnpm/isomorphic-dompurify@3.18.0npm/data-urls@7.0.0

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/data-urls@7.0.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm jsdom is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: pnpm-lock.yamlnpm/isomorphic-dompurify@3.18.0npm/jsdom@29.1.1

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/jsdom@29.1.1. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm jsdom is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: pnpm-lock.yamlnpm/isomorphic-dompurify@3.18.0npm/jsdom@29.1.1

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/jsdom@29.1.1. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds Supabase-backed favorites and notes, a self-service account deletion flow, updated legal/docs/translations, and a typing guard for full-screen shortcuts.

Changes

Personal Learning Features

Layer / File(s) Summary
Data model, entitlements, and documentation
src/constants/personalLearning.js, src/services/entitlementService.js, supabase/migrations/..., package.json, AGENTS.md, docs/AGENTS_REFERENCE.md, docs/DEVELOPMENT.md
Adds favorite slot limits, note length limits, getFavoriteSlotLimit, the favorite_algorithms/algorithm_notes migration with RLS, new dependencies, and documentation updates.
Favorites service and hook
src/test/supabaseMock.js, src/services/favoritesService.js, src/hooks/useFavorites.js, *.test.js
Adds Supabase CRUD and registry helpers for favorites and a useFavorites hook with hydration, optimistic toggling, and slot-limit handling.
Favorites UI wiring
src/components/AlgorithmDropdown.jsx, src/components/FavoritesDropdown.jsx, src/components/SettingsPanel.jsx, src/pages/VisualizerApp.jsx, tests
Adds a star toggle to algorithm rows, a favorites dropdown, and wiring through settings and the visualizer app.
Notes infrastructure
src/utils/noteHtmlSanitizer.js, src/services/notesService.js, src/hooks/useNoteAutosave.js, src/components/NoteEditor.jsx, tests
Adds HTML sanitization, note persistence, autosave logic, and a TipTap-based note editor.
Notes tab and Insight panel refactor
src/components/AlgorithmNotesTab.jsx, src/components/AlgorithmInsightPanel.jsx, src/pages/VisualizerApp.jsx, tests
Adds the notes tab and refactors the insight panel into tabbed Insight/Notes UI with flush-on-close and tab-switch coordination.
Self-service account deletion
supabase/functions/delete-account/index.ts, src/services/authService.js, src/pages/ProfileSettingsPage.jsx, src/test/supabaseMock.js, tests
Adds the delete-account Edge Function, authService.deleteAccount(), and a deletion flow in profile settings.
Legal content and translations
src/content/legal/*.js, src/i18n/locales/*/translation.json, tests
Updates privacy/terms text and adds English, Arabic, and French UI strings for favorites, notes, and account deletion.

Full-screen Keyboard Shortcut Fix

Layer / File(s) Summary
Typing-aware keydown handling
src/hooks/useFullScreen.js, src/hooks/useFullScreen.test.js
Prevents f and Escape shortcuts from firing while typing in inputs, selects, textareas, or contenteditable elements.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • ayoub3bidi/bayan-flow#191: Both PRs modify src/components/AlgorithmInsightPanel.jsx, one for layout styling and this one for a tabbed Insight/Notes refactor.
  • ayoub3bidi/bayan-flow#196: Both PRs modify src/pages/ProfileSettingsPage.jsx, one for profile settings behavior and this one for account deletion.
  • ayoub3bidi/bayan-flow#197: Both PRs modify src/services/entitlementService.js and related auth-gating logic.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: favorites, study notes, and account deletion.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/favorites-and-notes

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown

Preview for Bayan Flow Staging ready!

Name Link
🔨 Latest commit e5d8a00
🔍 Latest deploy log https://github.com/ayoub3bidi/bayan-flow/actions/runs/28736565750
😎 Deploy Preview https://pr-198-bayan-flow-staging.ayoub3bidi.workers.dev
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

Preview alias pr-198 on the staging worker. Updates automatically with new commits.

@codecov

codecov Bot commented Jul 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.62779% with 16 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/hooks/useNoteAutosave.js 90.72% 14 Missing ⚠️
src/components/AlgorithmInsightPanel.jsx 99.15% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (16)
supabase/functions/delete-account/index.ts (3)

57-74: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Avoid leaking raw internal error messages to clients.

Both the deleteError.message (400) and the catch-all message (500) are returned verbatim to the caller. This can expose internal implementation/database details. Log the detail server-side and return a generic message to the client.

🛡️ Proposed fix to avoid exposing internal errors
     if (deleteError) {
-      return new Response(JSON.stringify({ error: deleteError.message }), {
+      console.error('delete-account: deleteUser failed', deleteError);
+      return new Response(JSON.stringify({ error: 'Account deletion failed' }), {
         status: 400,
         headers: { ...corsHeaders, 'Content-Type': 'application/json' },
       });
     }
@@
   } catch (error) {
     const message = error instanceof Error ? error.message : 'Unknown error';
-    return new Response(JSON.stringify({ error: message }), {
+    console.error('delete-account: unexpected error', error);
+    return new Response(JSON.stringify({ error: 'Internal server error' }), {
       status: 500,
       headers: { ...corsHeaders, 'Content-Type': 'application/json' },
     });
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/functions/delete-account/index.ts` around lines 57 - 74, The
delete-account handler is returning raw internal error text from both the
deleteError path and the catch block, which can leak implementation details to
clients. Update the delete-account function to keep the detailed error in
server-side logging, but return a generic client-facing message for both the
delete operation failure and the catch-all failure. Use the existing
delete-account logic around deleteError and the catch(error) branch to locate
the response handling.

8-12: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Restrict CORS origin on the destructive delete-account endpoint.

Access-Control-Allow-Origin: '*' permits any origin to call this irreversible action. Since auth relies on a Bearer token (not cookies), CSRF risk is low, but scoping this to the app's known origin(s) is cheap hardening for a destructive endpoint.

🔒 Proposed fix to restrict CORS origin
+const ALLOWED_ORIGIN = Deno.env.get('ALLOWED_ORIGIN') ?? 'https://bayanflow.com';
+
 const corsHeaders = {
-  'Access-Control-Allow-Origin': '*',
+  'Access-Control-Allow-Origin': ALLOWED_ORIGIN,
   'Access-Control-Allow-Headers':
     'authorization, x-client-info, apikey, content-type',
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/functions/delete-account/index.ts` around lines 8 - 12, The
delete-account endpoint is using a wildcard CORS origin, which is too permissive
for an irreversible action. Update the corsHeaders constant in the
delete-account function to allow only the app’s known origin(s) instead of '*',
and keep the existing allowed headers intact. If needed, make the origin dynamic
based on the request Origin header or an allowlist, but ensure the handler’s
CORS response is restricted in the delete-account endpoint logic.

6-75: 🩺 Stability & Availability | 🔵 Trivial | 🏗️ Heavy lift

No tests cover this critical, irreversible endpoint.

Static analysis flags essentially the entire file as untested. Given this performs permanent account deletion, add Deno-based tests (or extract the handler logic into a testable pure function) covering the unauthorized, wrong-method, delete-error, and success paths.

Based on learnings, coverage gaps flagged by codecov/patch across lines 6-75 indicate this new file has no exercised test path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/functions/delete-account/index.ts` around lines 6 - 75, The
delete-account endpoint in Deno.serve is entirely untested, so add coverage for
the critical paths by either extracting the request handling into a testable
helper or testing the handler directly. Focus tests on the Deno.serve flow and
the key symbols createClient, supabaseAdmin.auth.getUser, and
supabaseAdmin.auth.admin.deleteUser, covering wrong HTTP method, missing/invalid
Authorization, failed user lookup, deleteUser error, and successful deletion
with the expected status codes and JSON responses.

Source: Linters/SAST tools

src/services/favoritesService.test.js (1)

1-129: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for addFavorite's registry-validation branch and removeOrphanFavorites.

Current tests don't exercise the "Unknown algorithm" rejection in addFavorite (favoritesService.js lines 94-96) or removeOrphanFavorites at all.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/favoritesService.test.js` around lines 1 - 129, The
favoritesService tests are missing coverage for two important branches: the
“Unknown algorithm” validation in addFavorite and the orphan cleanup path in
removeOrphanFavorites. Update favoritesService.test.js to add a test that passes
an invalid algorithm key into addFavorite and asserts it rejects with the
unknown-algorithm error, using addFavorite and PERSONAL_LEARNING_ERRORS to
locate the behavior. Also add a test for removeOrphanFavorites that seeds orphan
favorites, invokes the function, and verifies the expected Supabase
delete/cleanup call occurs and only valid favorites remain, referencing
removeOrphanFavorites and getOrphanFavorites.
src/hooks/useFavorites.test.js (1)

36-98: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for failure and slot-limit paths.

Current tests only cover happy-path hydrate/toggle. Consider adding cases for: listFavorites rejecting, addFavorite/removeFavorite rejecting (verifying rollback to previous), and toggleFavorite returning { reason: 'slot_limit' } when isAtSlotLimit is true.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useFavorites.test.js` around lines 36 - 98, The useFavorites test
suite only covers the successful hydrate and toggle flows, so add cases in
useFavorites.test.js for the failure and limit branches. Extend the renderHook
coverage to verify that listFavorites rejecting is handled, that toggleFavorite
rolls back to the previous favorites state when addFavorite or removeFavorite
rejects, and that the hook returns { reason: 'slot_limit' } when isAtSlotLimit
is true. Use the existing useFavorites, listFavorites, addFavorite,
removeFavorite, and isAtSlotLimit paths to locate the relevant behavior.

Source: Linters/SAST tools

src/hooks/useFavorites.js (1)

32-63: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Error branches of hydrate() are untested.

Per the codecov hint, lines 53-54 and 56-58 (stale-request bail-out and the catch block) aren't covered by any test. Worth adding a case where listFavorites rejects to confirm favorites reset to [] and the error is logged rather than crashing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useFavorites.js` around lines 32 - 63, The error-handling paths in
hydrate are missing test coverage, including the stale-request bail-out and the
catch branch. Add a test for useFavorites that mocks listFavorites to reject and
verifies hydrate logs the failure via console.error and resets favorites to an
empty array instead of throwing. If needed, also cover the requestRef/requestId
mismatch path so the early return behavior in hydrate remains exercised.

Source: Linters/SAST tools

src/components/AlgorithmDropdown.jsx (1)

97-196: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add test coverage for the favorited = true star state.

Both new tests in AlgorithmDropdown.test.jsx only exercise the "add to favorites" (unfavorited) path. The favorited === true branches (filled star, amber color, "remove from favorites" aria-label at Lines 173, 178-180, 190) are untested, matching the codecov flags on those exact lines.

✅ Suggested additional test
it('shows remove-favorite state when algorithm is already favorited', () => {
  renderWithI18n(
    <AlgorithmDropdown
      {...defaultProps}
      isDropdownOpen={true}
      user={{ id: 'user-1' }}
      isAuthenticated={true}
      categoryType="sorting"
      isFavorite={() => true}
    />
  );

  expect(
    screen.getByRole('button', { name: /remove bubble sort from favorites/i })
  ).toBeInTheDocument();
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/AlgorithmDropdown.jsx` around lines 97 - 196, Add test
coverage for the already-favorited star state in AlgorithmDropdown. Update
AlgorithmDropdown.test.jsx to render the dropdown with isFavorite returning true
and assert the favorited UI branches in the AlgorithmDropdown list item: the
Star icon uses the filled/amber styling and the star button gets the “remove
from favorites” aria-label. Use the existing AlgorithmDropdown, isFavorite, and
handleStarClick behavior to locate the affected branches.
src/components/FavoritesDropdown.jsx (1)

100-104: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Empty-state hint inside the open dropdown is untested.

The "shows empty state hint" test never opens the dropdown (no click on the toggle button), so it only verifies the closed-button label (Line 76), not this hint paragraph. Matches the codecov gap on Lines 101-103.

✅ Suggested test fix
   it('shows empty state hint', async () => {
     render(
       <FavoritesDropdown
         favorites={[]}
         slotLimit={20}
         onSelect={vi.fn()}
         isPlaying={false}
       />
     );
+
+    fireEvent.click(screen.getByText('settings.favoriteAlgorithmsEmpty'));

     expect(
       screen.getByText('settings.favoriteAlgorithmsHint')
     ).toBeInTheDocument();
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/FavoritesDropdown.jsx` around lines 100 - 104, The empty-state
hint rendered by FavoritesDropdown is not being exercised because the test only
checks the closed toggle label and never opens the dropdown. Update the “shows
empty state hint” test to click the dropdown toggle first, then assert the hint
paragraph from FavoritesDropdown is visible when count is 0. Use the component’s
toggle/open behavior and the empty-state text key to target the correct UI
state.
src/utils/noteHtmlSanitizer.js (2)

41-41: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Truncation can leave dangling/unbalanced HTML.

sanitized.slice(0, NOTE_MAX_HTML_LENGTH) can cut mid-tag (e.g., <stro), storing malformed HTML that renders oddly when reloaded. Since the content is already sanitized before slicing, re-sanitizing the truncated substring is cheap and guarantees well-formed output.

🩹 Proposed fix
-  return sanitized.slice(0, NOTE_MAX_HTML_LENGTH);
+  return DOMPurify.sanitize(sanitized.slice(0, NOTE_MAX_HTML_LENGTH), {
+    ALLOWED_TAGS,
+    ALLOWED_ATTR: [],
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/noteHtmlSanitizer.js` at line 41, The truncation in
noteHtmlSanitizer’s return path can leave malformed HTML if `sanitized.slice(0,
NOTE_MAX_HTML_LENGTH)` cuts through a tag. Update the `sanitizeNoteHtml` flow so
the truncated substring is passed back through the sanitizer (or equivalent HTML
normalization step) before returning, ensuring the output remains well-formed
after length limiting.

48-63: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Consider avoiding innerHTML for text extraction on this exported utility.

getPlainTextFromNoteHtml sets el.innerHTML = html directly. In the current call graph (via isNoteContentEmpty/assertNotePlainTextLength in notesService.upsertNote), html is always pre-sanitized, so this isn't actively exploitable today. However, this is an exported, general-purpose function with no indication in its name/signature that it requires pre-sanitized input — a future caller invoking it on raw content would reintroduce an XSS surface (event handlers like onerror fire even on detached elements). Using DOMParser, which doesn't execute scripts or fetch resources, removes this risk entirely regardless of caller.

🛡️ Proposed fix
-  if (typeof document !== 'undefined') {
-    const el = document.createElement('div');
-    el.innerHTML = html;
-    return (el.textContent ?? '').trim();
-  }
+  if (typeof DOMParser !== 'undefined') {
+    const doc = new DOMParser().parseFromString(html, 'text/html');
+    return (doc.body.textContent ?? '').trim();
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/noteHtmlSanitizer.js` around lines 48 - 63,
`getPlainTextFromNoteHtml` currently uses `innerHTML` on a detached element,
which is unsafe for a general-purpose exported helper. Update the implementation
in `noteHtmlSanitizer.js` to extract text without assigning raw HTML to
`innerHTML`, preferably by parsing with `DOMParser` in the browser path and
keeping the existing fallback for non-DOM environments. Preserve the current
trimming/empty-input behavior, and ensure `isNoteContentEmpty` and
`assertNotePlainTextLength` continue to work through the same
`getPlainTextFromNoteHtml` API.

Source: Linters/SAST tools

src/components/NoteEditor.jsx (2)

26-47: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Memoize the extensions array to avoid reconfiguring the editor on every keystroke.

extensions is a fresh array (with new StarterKit/Placeholder instances) on every render of NoteEditor. Since onUpdate sets editorHtml in the parent on every keystroke, this component re-renders on every keystroke too. Tiptap's useEditor compares extensions by reference identity when deciding whether to re-apply options: "We often encourage putting extensions inlined in the options object, so we will do a slightly deeper comparison here" but that comparison still treats a new extension instance as changed. The docs also advise: "Use the deps array sparingly. Only include dependencies when you actually need to recreate the editor instance... updating editor options or content through commands is more efficient." Wrapping the array in useMemo (keyed on placeholder) avoids unnecessary option re-application on every render.

⚡ Proposed fix
-import { useEffect } from 'react';
+import { useEffect, useMemo } from 'react';

 function NoteEditor({ content, placeholder, onUpdate }) {
+  const extensions = useMemo(
+    () => [
+      StarterKit.configure({
+        heading: { levels: [2, 3] },
+        codeBlock: false,
+        strike: false,
+        horizontalRule: false,
+      }),
+      Placeholder.configure({ placeholder }),
+    ],
+    [placeholder]
+  );
+
   const editor = useEditor({
-    extensions: [
-      StarterKit.configure({
-        heading: { levels: [2, 3] },
-        codeBlock: false,
-        strike: false,
-        horizontalRule: false,
-      }),
-      Placeholder.configure({ placeholder }),
-    ],
+    extensions,
     content,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/NoteEditor.jsx` around lines 26 - 47, The NoteEditor hook is
recreating a fresh extensions array on every render, which causes useEditor to
re-apply editor options unnecessarily during typing. Memoize the extensions
value in NoteEditor with useMemo, keyed on placeholder, so StarterKit.configure
and Placeholder.configure are only recreated when their inputs change. Keep the
onUpdate behavior unchanged, but ensure the extensions reference stays stable
across parent re-renders.

63-77: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

ToolButton is redefined on every render, forcing React to unmount/remount all toolbar buttons on every keystroke.

Since content changes on every keystroke (parent state update), NoteEditor re-renders, and ToolButton (a new function reference each time) is treated by React as a different component type, tearing down and recreating the 5 toolbar buttons every render. Hoist it outside the component.

♻️ Proposed fix
+function ToolButton({ onClick, isActive, label, children }) {
+  return (
+    <button
+      type="button"
+      onClick={onClick}
+      className={`p-2 rounded-md transition-colors ${
+        isActive
+          ? 'bg-theme-primary-light text-theme-primary'
+          : 'text-text-secondary hover:bg-surface hover:text-text-primary'
+      }`}
+      aria-label={label}
+      aria-pressed={isActive}
+    >
+      {children}
+    </button>
+  );
+}
+
 function NoteEditor({ content, placeholder, onUpdate }) {
   ...
-  const ToolButton = ({ onClick, isActive, label, children }) => (
-    <button
-      type="button"
-      onClick={onClick}
-      className={`p-2 rounded-md transition-colors ${
-        isActive
-          ? 'bg-theme-primary-light text-theme-primary'
-          : 'text-text-secondary hover:bg-surface hover:text-text-primary'
-      }`}
-      aria-label={label}
-      aria-pressed={isActive}
-    >
-      {children}
-    </button>
-  );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/NoteEditor.jsx` around lines 63 - 77, The ToolButton component
is being recreated inside NoteEditor on every render, so React treats it as a
new component and remounts the toolbar buttons whenever content changes. Hoist
ToolButton out of NoteEditor so it has a stable identity, and keep using its
existing props (onClick, isActive, label, children) from the toolbar render to
avoid unnecessary unmount/remount cycles.
src/components/AlgorithmInsightPanel.jsx (1)

201-262: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Tabs lack full ARIA tab-panel wiring (aria-controls/id/role="tabpanel").

role="tablist"/role="tab" are present, but the content region (Line 241-263) isn't marked as role="tabpanel" with aria-labelledby pointing back to the active tab, and the TabButtons don't set aria-controls. Screen readers won't announce the relationship between the selected tab and its panel.

Also applies to: 268-285

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/AlgorithmInsightPanel.jsx` around lines 201 - 262, The tab UI
in AlgorithmInsightPanel is missing the full ARIA tab/panel linkage. Update
TabButton so each tab exposes aria-controls tied to its matching panel, and mark
the content region rendered for InsightTabBody and AlgorithmNotesTab as
role="tabpanel" with an id and aria-labelledby pointing back to the active tab.
Keep the existing activeTab switching logic, but ensure the insight and notes
panels each have stable ids and the selected tab references the currently shown
panel.
src/services/notesService.test.js (1)

81-101: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Test doesn't verify sanitized payload actually reaches .upsert().

Only the mocked DB response is asserted (hardcoded to '<p>note</p>'), not the arguments passed to upsert. A regression that skips sanitization before the DB call would still pass this test.

✅ Proposed fix: assert the actual upsert payload
     expect(upsert).toHaveBeenCalled();
+    expect(upsert).toHaveBeenCalledWith(
+      expect.objectContaining({ body_html: '<p>note</p>' }),
+      expect.anything()
+    );
     expect(result?.body_html).toBe('<p>note</p>');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/notesService.test.js` around lines 81 - 101, The test in
upsertNote only checks the mocked return value, so it never verifies that
sanitized HTML is actually sent into .upsert(). Update the
notesService.upsertNote test to inspect the arguments passed to the mocked
upsert call and assert the payload contains the sanitized body_html (not the
unsanitized input), while still keeping the existing result assertion from the
single() response.
src/hooks/useNoteAutosave.test.js (2)

23-28: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

No coverage for save failure/retry behavior.

upsertNote always resolves in this suite, so the retry/backoff path in performSave (and its self-referential deadlock) is completely untested, matching the codecov gap on useNoteAutosave.js lines 89-103.

Want me to draft a test that rejects upsertNote once and asserts a subsequent save eventually resolves/settles (which would have caught the deadlock)?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useNoteAutosave.test.js` around lines 23 - 28, The useNoteAutosave
test suite only covers successful saves, so the retry/backoff path in
performSave is untested. Add a test in useNoteAutosave.test.js that makes
upsertNote reject on the first call and then succeed on a later attempt, and
assert that the save flow eventually settles rather than hanging; target the
performSave behavior in useNoteAutosave via the mocked upsertNote and any
exposed save trigger.

30-43: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test harness can't actually change categoryType/algorithmKey across renders.

overrides is captured once when renderAutosave is called; rerender({ isActive: true }) (line 82) only varies isActive, so this never exercises a real algorithm switch — despite the test name.

🔧 Proposed fix: allow overriding algorithm key per render
   function renderAutosave(overrides = {}) {
     return renderHook(
-      ({ isActive }) =>
+      ({ isActive, algorithmKey = 'bubbleSort' }) =>
         useNoteAutosave({
           user,
           categoryType: ALGORITHM_TYPES.SORTING,
-          algorithmKey: 'bubbleSort',
+          algorithmKey,
           getContentHtml: () => contentHtml,
           isActive,
           ...overrides,
         }),
-      { initialProps: { isActive: true } }
+      { initialProps: { isActive: true, algorithmKey: 'bubbleSort' } }
     );
   }

Then rerender({ isActive: true, algorithmKey: 'selectionSort' }) would exercise the real flush-before-switch path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useNoteAutosave.test.js` around lines 30 - 43, The `renderAutosave`
test helper in `useNoteAutosave.test.js` captures `categoryType` and
`algorithmKey` once, so `rerender` only changes `isActive` and never tests a
real algorithm switch. Update the test harness around `renderAutosave` and
`renderHook` so `algorithmKey` (and `categoryType` if needed) can be supplied
per render via props or overrides, then change the switch test to rerender with
a different `algorithmKey` and verify the flush-before-switch path in
`useNoteAutosave`.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/DEVELOPMENT.md`:
- Line 1039: The deploy command for the delete-account function is using an
invalid JWT flag override. Update the command in the deployment instructions to
omit --no-verify-jwt entirely and rely on the default verify_jwt = true behavior
from config.toml/dashboard; keep the guidance aligned with the deploy command
wording in the docs.

In `@src/components/AlgorithmInsightPanel.jsx`:
- Around line 69-82: The note flush in AlgorithmInsightPanel’s handleClose and
handleTabChange can reject and prevent the panel from closing or switching tabs.
Update these handlers to wrap notesFlushRef.current() in try/catch, keep the
existing flush behavior, and always continue to setActiveTab and call onClose
even when the save fails so a flush error does not block UI navigation.

In `@src/components/AlgorithmNotesTab.jsx`:
- Around line 38-54: The editor state in AlgorithmNotesTab is being reused
across algorithmKey changes, which can briefly show and potentially save the
previous algorithm’s note under the new key. Update the AlgorithmNotesTab effect
that syncs initialHtml from useNoteAutosave so it clears or resets editorHtml
whenever algorithmKey changes, or alternatively key the mounted tab/component by
algorithmKey in AlgorithmInsightPanel to force a fresh state per algorithm.

In `@src/components/AlgorithmNotesTab.test.jsx`:
- Around line 12-20: The NoteEditor mock is using the wrong prop API, so this
test won’t verify how AlgorithmNotesTab passes data into the real component.
Update the vi.mock for NoteEditor to destructure the actual props it receives
from AlgorithmNotesTab and mirror NoteEditor’s real interface ({ content,
placeholder, onUpdate }) so the textarea reflects content and calls onUpdate,
ensuring wiring regressions are caught.

In `@src/hooks/useNoteAutosave.js`:
- Line 102: The fire-and-forget calls to performSave in useNoteAutosave are
allowing retries-exhausted errors to escape as unhandled promise rejections.
Update each void performSave() call site to explicitly handle the returned
promise with a catch that swallows or routes already-handled failures through
existing saveStatus logic, and keep performSave’s final throw for the awaited
path only. Use performSave and the related saveStatus updates as the key symbols
when fixing all three call sites.
- Around line 138-148: The debounce timer created in scheduleSave can still fire
after the hook is unmounted, leading to late calls into performSave and state
updates from useNoteAutosave. Add an unmount cleanup in the hook to clear
debounceRef.current and reset it, using the existing
scheduleSave/debounceRef/performSave logic as the entry points so no pending
timeout survives component teardown.
- Around line 81-116: The retry path in useNoteAutosave’s performSave is
self-referential because it calls performSave again while inFlightRef.current
still holds the active savePromise, causing the in-flight guard to return the
same promise and deadlock. Update the retry logic so retries bypass or reset the
in-flight state before re-invoking performSave, and make sure the retry branch
still respects dirtyRef.current and MAX_RETRIES without awaiting the current
promise. Keep the fix localized to performSave, retryCountRef, inFlightRef, and
the finalization logic in the finally block.

In `@src/pages/VisualizerApp.jsx`:
- Around line 818-830: The favorite-limit notice in VisualizerApp.jsx has a dead
exit animation, no accessibility announcement, and duplicates slot-limit logic.
Wrap the conditional motion.div for favoriteNotice in AnimatePresence so the
exit transition runs, add an appropriate aria-live/role for screen readers, and
use the existing favoriteSlotLimit value from useFavorites(user) instead of
calling getFavoriteSlotLimit(user) again.

In `@src/services/favoritesService.js`:
- Around line 88-118: Move the favorite slot cap enforcement out of addFavorite
and into the database layer so it cannot be bypassed by direct inserts or stale
client values. Update the favorite_algorithms insert path in addFavorite to rely
on server-side validation, and add a trigger/function on the table that checks
the user’s current favorite count against their allowed slot limit before
insert. Use addFavorite, favorite_algorithms, and the existing
PERSONAL_LEARNING_ERRORS.FAVORITE_SLOT_LIMIT_REACHED handling as the key
integration points when wiring the new server-side check.

In `@supabase/migrations/20260704120000_personal_learning_favorites_notes.sql`:
- Around line 3-9: The per-user favorite cap is only enforced in the client-side
addFavorite flow, so direct writes to favorite_algorithms can bypass it. Add
server-side enforcement in the database by introducing a trigger or guarded RPC
around favorite_algorithms inserts that checks the current count against the
slot limit before allowing a new row, and wire it to the existing
favorite_algorithms table so the limit cannot be exceeded through direct
inserts.

---

Nitpick comments:
In `@src/components/AlgorithmDropdown.jsx`:
- Around line 97-196: Add test coverage for the already-favorited star state in
AlgorithmDropdown. Update AlgorithmDropdown.test.jsx to render the dropdown with
isFavorite returning true and assert the favorited UI branches in the
AlgorithmDropdown list item: the Star icon uses the filled/amber styling and the
star button gets the “remove from favorites” aria-label. Use the existing
AlgorithmDropdown, isFavorite, and handleStarClick behavior to locate the
affected branches.

In `@src/components/AlgorithmInsightPanel.jsx`:
- Around line 201-262: The tab UI in AlgorithmInsightPanel is missing the full
ARIA tab/panel linkage. Update TabButton so each tab exposes aria-controls tied
to its matching panel, and mark the content region rendered for InsightTabBody
and AlgorithmNotesTab as role="tabpanel" with an id and aria-labelledby pointing
back to the active tab. Keep the existing activeTab switching logic, but ensure
the insight and notes panels each have stable ids and the selected tab
references the currently shown panel.

In `@src/components/FavoritesDropdown.jsx`:
- Around line 100-104: The empty-state hint rendered by FavoritesDropdown is not
being exercised because the test only checks the closed toggle label and never
opens the dropdown. Update the “shows empty state hint” test to click the
dropdown toggle first, then assert the hint paragraph from FavoritesDropdown is
visible when count is 0. Use the component’s toggle/open behavior and the
empty-state text key to target the correct UI state.

In `@src/components/NoteEditor.jsx`:
- Around line 26-47: The NoteEditor hook is recreating a fresh extensions array
on every render, which causes useEditor to re-apply editor options unnecessarily
during typing. Memoize the extensions value in NoteEditor with useMemo, keyed on
placeholder, so StarterKit.configure and Placeholder.configure are only
recreated when their inputs change. Keep the onUpdate behavior unchanged, but
ensure the extensions reference stays stable across parent re-renders.
- Around line 63-77: The ToolButton component is being recreated inside
NoteEditor on every render, so React treats it as a new component and remounts
the toolbar buttons whenever content changes. Hoist ToolButton out of NoteEditor
so it has a stable identity, and keep using its existing props (onClick,
isActive, label, children) from the toolbar render to avoid unnecessary
unmount/remount cycles.

In `@src/hooks/useFavorites.js`:
- Around line 32-63: The error-handling paths in hydrate are missing test
coverage, including the stale-request bail-out and the catch branch. Add a test
for useFavorites that mocks listFavorites to reject and verifies hydrate logs
the failure via console.error and resets favorites to an empty array instead of
throwing. If needed, also cover the requestRef/requestId mismatch path so the
early return behavior in hydrate remains exercised.

In `@src/hooks/useFavorites.test.js`:
- Around line 36-98: The useFavorites test suite only covers the successful
hydrate and toggle flows, so add cases in useFavorites.test.js for the failure
and limit branches. Extend the renderHook coverage to verify that listFavorites
rejecting is handled, that toggleFavorite rolls back to the previous favorites
state when addFavorite or removeFavorite rejects, and that the hook returns {
reason: 'slot_limit' } when isAtSlotLimit is true. Use the existing
useFavorites, listFavorites, addFavorite, removeFavorite, and isAtSlotLimit
paths to locate the relevant behavior.

In `@src/hooks/useNoteAutosave.test.js`:
- Around line 23-28: The useNoteAutosave test suite only covers successful
saves, so the retry/backoff path in performSave is untested. Add a test in
useNoteAutosave.test.js that makes upsertNote reject on the first call and then
succeed on a later attempt, and assert that the save flow eventually settles
rather than hanging; target the performSave behavior in useNoteAutosave via the
mocked upsertNote and any exposed save trigger.
- Around line 30-43: The `renderAutosave` test helper in
`useNoteAutosave.test.js` captures `categoryType` and `algorithmKey` once, so
`rerender` only changes `isActive` and never tests a real algorithm switch.
Update the test harness around `renderAutosave` and `renderHook` so
`algorithmKey` (and `categoryType` if needed) can be supplied per render via
props or overrides, then change the switch test to rerender with a different
`algorithmKey` and verify the flush-before-switch path in `useNoteAutosave`.

In `@src/services/favoritesService.test.js`:
- Around line 1-129: The favoritesService tests are missing coverage for two
important branches: the “Unknown algorithm” validation in addFavorite and the
orphan cleanup path in removeOrphanFavorites. Update favoritesService.test.js to
add a test that passes an invalid algorithm key into addFavorite and asserts it
rejects with the unknown-algorithm error, using addFavorite and
PERSONAL_LEARNING_ERRORS to locate the behavior. Also add a test for
removeOrphanFavorites that seeds orphan favorites, invokes the function, and
verifies the expected Supabase delete/cleanup call occurs and only valid
favorites remain, referencing removeOrphanFavorites and getOrphanFavorites.

In `@src/services/notesService.test.js`:
- Around line 81-101: The test in upsertNote only checks the mocked return
value, so it never verifies that sanitized HTML is actually sent into .upsert().
Update the notesService.upsertNote test to inspect the arguments passed to the
mocked upsert call and assert the payload contains the sanitized body_html (not
the unsanitized input), while still keeping the existing result assertion from
the single() response.

In `@src/utils/noteHtmlSanitizer.js`:
- Line 41: The truncation in noteHtmlSanitizer’s return path can leave malformed
HTML if `sanitized.slice(0, NOTE_MAX_HTML_LENGTH)` cuts through a tag. Update
the `sanitizeNoteHtml` flow so the truncated substring is passed back through
the sanitizer (or equivalent HTML normalization step) before returning, ensuring
the output remains well-formed after length limiting.
- Around line 48-63: `getPlainTextFromNoteHtml` currently uses `innerHTML` on a
detached element, which is unsafe for a general-purpose exported helper. Update
the implementation in `noteHtmlSanitizer.js` to extract text without assigning
raw HTML to `innerHTML`, preferably by parsing with `DOMParser` in the browser
path and keeping the existing fallback for non-DOM environments. Preserve the
current trimming/empty-input behavior, and ensure `isNoteContentEmpty` and
`assertNotePlainTextLength` continue to work through the same
`getPlainTextFromNoteHtml` API.

In `@supabase/functions/delete-account/index.ts`:
- Around line 57-74: The delete-account handler is returning raw internal error
text from both the deleteError path and the catch block, which can leak
implementation details to clients. Update the delete-account function to keep
the detailed error in server-side logging, but return a generic client-facing
message for both the delete operation failure and the catch-all failure. Use the
existing delete-account logic around deleteError and the catch(error) branch to
locate the response handling.
- Around line 8-12: The delete-account endpoint is using a wildcard CORS origin,
which is too permissive for an irreversible action. Update the corsHeaders
constant in the delete-account function to allow only the app’s known origin(s)
instead of '*', and keep the existing allowed headers intact. If needed, make
the origin dynamic based on the request Origin header or an allowlist, but
ensure the handler’s CORS response is restricted in the delete-account endpoint
logic.
- Around line 6-75: The delete-account endpoint in Deno.serve is entirely
untested, so add coverage for the critical paths by either extracting the
request handling into a testable helper or testing the handler directly. Focus
tests on the Deno.serve flow and the key symbols createClient,
supabaseAdmin.auth.getUser, and supabaseAdmin.auth.admin.deleteUser, covering
wrong HTTP method, missing/invalid Authorization, failed user lookup, deleteUser
error, and successful deletion with the expected status codes and JSON
responses.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 55bfffea-9800-4e16-8148-3434d5b58725

📥 Commits

Reviewing files that changed from the base of the PR and between 656af08 and 7d1e971.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (43)
  • AGENTS.md
  • docs/AGENTS_REFERENCE.md
  • docs/DEVELOPMENT.md
  • package.json
  • src/components/AlgorithmDropdown.jsx
  • src/components/AlgorithmDropdown.test.jsx
  • src/components/AlgorithmInsightPanel.jsx
  • src/components/AlgorithmInsightPanel.test.jsx
  • src/components/AlgorithmNotesTab.jsx
  • src/components/AlgorithmNotesTab.test.jsx
  • src/components/FavoritesDropdown.jsx
  • src/components/FavoritesDropdown.test.jsx
  • src/components/NoteEditor.jsx
  • src/components/SettingsPanel.jsx
  • src/constants/personalLearning.js
  • src/content/legal/privacy.en.js
  • src/content/legal/privacy.en.test.js
  • src/content/legal/terms.en.js
  • src/content/legal/terms.en.test.js
  • src/hooks/useFavorites.js
  • src/hooks/useFavorites.test.js
  • src/hooks/useFullScreen.js
  • src/hooks/useFullScreen.test.js
  • src/hooks/useNoteAutosave.js
  • src/hooks/useNoteAutosave.test.js
  • src/i18n/locales/ar/translation.json
  • src/i18n/locales/en/translation.json
  • src/i18n/locales/fr/translation.json
  • src/pages/ProfileSettingsPage.jsx
  • src/pages/ProfileSettingsPage.test.jsx
  • src/pages/VisualizerApp.jsx
  • src/services/__tests__/entitlementService.test.js
  • src/services/authService.js
  • src/services/authService.test.js
  • src/services/entitlementService.js
  • src/services/favoritesService.js
  • src/services/favoritesService.test.js
  • src/services/notesService.js
  • src/services/notesService.test.js
  • src/test/supabaseMock.js
  • src/utils/noteHtmlSanitizer.js
  • supabase/functions/delete-account/index.ts
  • supabase/migrations/20260704120000_personal_learning_favorites_notes.sql

Comment thread docs/DEVELOPMENT.md Outdated
Comment thread src/components/AlgorithmInsightPanel.jsx
Comment on lines +38 to +54
const [editorHtml, setEditorHtml] = useState('');
const getContentHtml = useCallback(() => editorHtml, [editorHtml]);

const { initialHtml, isLoadingNote, saveStatus, scheduleSave, flushSave } =
useNoteAutosave({
user,
categoryType,
algorithmKey,
getContentHtml,
isActive,
});

useEffect(() => {
if (!isLoadingNote) {
setEditorHtml(initialHtml);
}
}, [initialHtml, isLoadingNote, categoryType, algorithmKey]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the relevant components and parent switch logic.
git ls-files 'src/components/*Algorithm*' 'src/components/*Notes*' 'src/components/*Insight*' 'src/components/*VisualizerApp*' 'src/components/*Category*' | sed 's#^`#FILE` #'

printf '\n--- matches for AlgorithmNotesTab / AlgorithmInsightPanel ---\n'
rg -n "AlgorithmNotesTab|AlgorithmInsightPanel|notes tab|insight panel" src/components src -g '!**/node_modules/**' || true

printf '\n--- file outlines ---\n'
for f in \
  src/components/AlgorithmNotesTab.jsx \
  src/components/AlgorithmInsightPanel.jsx \
  src/components/VisualizerApp.jsx \
  src/components/*Category*Visualizer*.jsx \
  src/components/*Category*.jsx
do
  if [ -f "$f" ]; then
    echo "### $f"
    ast-grep outline "$f" --view expanded || true
  fi
done

Repository: ayoub3bidi/bayan-flow

Length of output: 8353


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the note tab and insight panel implementations.
sed -n '1,220p' src/components/AlgorithmNotesTab.jsx
printf '\n--- AlgorithmInsightPanel excerpt ---\n'
sed -n '1,340p' src/components/AlgorithmInsightPanel.jsx
printf '\n--- VisualizerApp excerpt around panel rendering ---\n'
sed -n '840,980p' src/pages/VisualizerApp.jsx

Repository: ayoub3bidi/bayan-flow

Length of output: 15712


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

files = [
    Path("src/components/AlgorithmNotesTab.jsx"),
    Path("src/components/AlgorithmInsightPanel.jsx"),
    Path("src/pages/VisualizerApp.jsx"),
]
for path in files:
    print(f"\n### {path}\n")
    text = path.read_text()
    lines = text.splitlines()
    if path.name == "AlgorithmNotesTab.jsx":
        start, end = 1, min(len(lines), 220)
    elif path.name == "AlgorithmInsightPanel.jsx":
        start, end = 1, min(len(lines), 340)
    else:
        # focus on render around the lazy component
        for i, line in enumerate(lines, 1):
            if "AlgorithmInsightPanel" in line:
                start = max(1, i - 40)
                end = min(len(lines), i + 80)
                break
        else:
            start, end = 1, min(len(lines), 220)
    for i in range(start, end + 1):
        print(f"{i:4d}: {lines[i-1]}")
PY

Repository: ayoub3bidi/bayan-flow

Length of output: 20182


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the exact JSX around the insight panel and the note tab.
grep -n "AlgorithmInsightPanel" -n src/pages/VisualizerApp.jsx
printf '\n--- VisualizerApp around AlgorithmInsightPanel ---\n'
sed -n '880,950p' src/pages/VisualizerApp.jsx

printf '\n--- AlgorithmInsightPanel around AlgorithmNotesTab ---\n'
grep -n "AlgorithmNotesTab" src/components/AlgorithmInsightPanel.jsx
sed -n '230,280p' src/components/AlgorithmInsightPanel.jsx

printf '\n--- AlgorithmNotesTab full file if small enough ---\n'
wc -l src/components/AlgorithmNotesTab.jsx
sed -n '1,180p' src/components/AlgorithmNotesTab.jsx

Repository: ayoub3bidi/bayan-flow

Length of output: 6549


src/components/AlgorithmNotesTab.jsx:50-54 — Clear the editor state on algorithm change. AlgorithmInsightPanel keeps the same AlgorithmNotesTab mounted while algorithmKey changes, so this effect can re-apply the previous initialHtml before the new note finishes loading. That can briefly show the old algorithm’s note and save it under the new key; reset editorHtml on key change or key the tab by algorithmKey to close the gap.

🧰 Tools
🪛 ast-grep (0.44.0)

[warning] 51-51: Avoid using the initial state variable in setState
Context: setEditorHtml(initialHtml)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(setstate-same-var)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/AlgorithmNotesTab.jsx` around lines 38 - 54, The editor state
in AlgorithmNotesTab is being reused across algorithmKey changes, which can
briefly show and potentially save the previous algorithm’s note under the new
key. Update the AlgorithmNotesTab effect that syncs initialHtml from
useNoteAutosave so it clears or resets editorHtml whenever algorithmKey changes,
or alternatively key the mounted tab/component by algorithmKey in
AlgorithmInsightPanel to force a fresh state per algorithm.

Comment thread src/components/AlgorithmNotesTab.test.jsx
Comment thread src/hooks/useNoteAutosave.js Outdated
Comment thread src/hooks/useNoteAutosave.js Outdated
Comment thread src/hooks/useNoteAutosave.js
Comment thread src/pages/VisualizerApp.jsx Outdated
Comment on lines +88 to +118
export async function addFavorite(
userId,
category,
algorithmKey,
{ slotLimit, currentCount }
) {
if (!isAlgorithmInRegistry(category, algorithmKey)) {
throw new Error('Unknown algorithm');
}

if (currentCount >= slotLimit) {
const error = new Error('Favorite slot limit reached');
error.code = PERSONAL_LEARNING_ERRORS.FAVORITE_SLOT_LIMIT_REACHED;
throw error;
}

const supabase = getSupabaseClient();
if (!supabase) {
throw new Error('Supabase is not configured');
}

const { error } = await supabase.from('favorite_algorithms').insert({
user_id: userId,
category,
algorithm_key: algorithmKey,
});

if (error) {
throw error;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd personal_learning_favorites_notes supabase/migrations --exec cat {} \;

Repository: ayoub3bidi/bayan-flow

Length of output: 2503


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== favorites references ==\n'
rg -n "favorite_algorithms|FAVORITE_SLOT_LIMIT_REACHED|slotLimit|currentCount|favorite slot" .

printf '\n== migration files mentioning favorite_algorithms ==\n'
fd -a favorite_algorithms supabase/migrations -x sh -c 'echo "--- {}"; sed -n "1,220p" "{}"'

Repository: ayoub3bidi/bayan-flow

Length of output: 5271


Enforce the favorite slot cap server-side addFavorite only checks caller-provided slotLimit/currentCount; public.favorite_algorithms has ownership RLS but no count-based constraint, so a direct insert can exceed the tier limit. Move the cap into a trigger/function on the table so stale or untrusted clients can’t bypass it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/favoritesService.js` around lines 88 - 118, Move the favorite
slot cap enforcement out of addFavorite and into the database layer so it cannot
be bypassed by direct inserts or stale client values. Update the
favorite_algorithms insert path in addFavorite to rely on server-side
validation, and add a trigger/function on the table that checks the user’s
current favorite count against their allowed slot limit before insert. Use
addFavorite, favorite_algorithms, and the existing
PERSONAL_LEARNING_ERRORS.FAVORITE_SLOT_LIMIT_REACHED handling as the key
integration points when wiring the new server-side check.

Comment on lines +3 to +9
create table public.favorite_algorithms (
user_id uuid not null references auth.users (id) on delete cascade,
category text not null,
algorithm_key text not null,
created_at timestamptz not null default now(),
primary key (user_id, category, algorithm_key)
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether any existing migration already enforces a per-user row-count limit.
rg -n "trigger|count\(" supabase/migrations

Repository: ayoub3bidi/bayan-flow

Length of output: 451


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== relevant migration =="
sed -n '1,220p' supabase/migrations/20260704120000_personal_learning_favorites_notes.sql

echo
echo "== favorites service =="
sed -n '1,220p' src/services/favoritesService.js

echo
echo "== search for favorite_algorithms policies/triggers/checks =="
rg -n -C 3 "favorite_algorithms|slotLimit|currentCount|auth\.uid\(\)|policy|trigger|check" supabase/migrations src/services

Repository: ayoub3bidi/bayan-flow

Length of output: 25920


Enforce the favorite slot cap server-side RLS only checks ownership here; the currentCount/slotLimit check in addFavorite() is client-controlled, so direct inserts can exceed the per-user limit. Add a database trigger or RPC guard if this cap must hold.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/migrations/20260704120000_personal_learning_favorites_notes.sql`
around lines 3 - 9, The per-user favorite cap is only enforced in the
client-side addFavorite flow, so direct writes to favorite_algorithms can bypass
it. Add server-side enforcement in the database by introducing a trigger or
guarded RPC around favorite_algorithms inserts that checks the current count
against the slot limit before allowing a new row, and wire it to the existing
favorite_algorithms table so the limit cannot be exceeded through direct
inserts.

…mponents

- Add 4 tests for NoteEditor (toolbar, useEditor, onUpdate, null editor)
- Add 5 tests for useFavorites (unauthenticated, slot limit, error, rollback add/remove)
- Add 4 tests for AlgorithmNotesTab (loading, saving/saved/error/dirty save status)
- Add 2 tests for AlgorithmInsightPanel (tab switching, tab reset on close)
- Add 2 tests for AlgorithmDropdown (favorited label, locked star hiding)
- Add 3 tests for useNoteAutosave (error on load, dirty status, empty user)
- Remove 3 flaky tests (NoteEditor content sync, autosave retry/timeout)
- Exclude supabase/functions/ from Vitest coverage (Deno Edge Functions)
@github-actions github-actions Bot added the config label Jul 5, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/hooks/useFavorites.test.js (1)

45-146: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider adding a stale-response race test.

useFavorites guards against out-of-order responses via requestRef (bumped on each hydrate call, checked before applying results). None of the current tests exercise rapid user switches that would trigger this guard (e.g., rerender with a second user before the first listFavorites promise resolves). Given the PR's focus on expanding hook test coverage, this would close a coverage gap on a concurrency-sensitive path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useFavorites.test.js` around lines 45 - 146, Add a test in
useFavorites.test.js that covers the stale-response race handled by requestRef
in useFavorites. Simulate a slow listFavorites call for the first user, rerender
the hook with a second user before it resolves, then resolve the first request
and verify its result is ignored and only the latest hydrate updates favorites.
Use the existing renderHook, rerender, listFavorites, and requestRef behavior as
the target path.
src/components/NoteEditor.test.jsx (1)

72-104: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

mockUseEditor.mockImplementation leaks across tests.

vi.clearAllMocks() in beforeEach clears call history but not custom implementations set via mockImplementation. The custom implementation set in "calls onUpdate when editor emits change" (Lines 76-92) will persist into subsequent tests unless explicitly overridden (as the next test happens to do via mockReturnValueOnce). This creates a fragile, order-dependent suite — reordering or adding tests after this one could silently pick up the stale implementation.

♻️ Proposed fix
   beforeEach(() => {
-    vi.clearAllMocks();
+    vi.resetAllMocks();
+    mockUseEditor.mockReturnValue(editor);
   });

Or restore the default implementation at the end of the affected test using mockUseEditor.mockImplementation(defaultImpl).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/NoteEditor.test.jsx` around lines 72 - 104, The
mockUseEditor.mockImplementation set in the NoteEditor test is leaking into
later tests because clearAllMocks does not reset custom implementations. In the
"calls onUpdate when editor emits change" test, save the default mock behavior
and restore it after the test, or use a scoped override/reset so NoteEditor and
mockUseEditor do not keep the custom onUpdate-capturing implementation beyond
that case.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/AlgorithmDropdown.test.jsx`:
- Line 7: The test file imports beforeEach from vitest but never uses it, so
remove the unused beforeEach symbol from the import in
AlgorithmDropdown.test.jsx while keeping the other Vitest helpers intact.

In `@src/components/AlgorithmInsightPanel.test.jsx`:
- Around line 81-110: The “resets tab to insight when reopened” test in
AlgorithmInsightPanel does not verify reopening behavior because it only
rerenders with isOpen=false and never asserts after reopening. Update the test
to close the panel and then reopen it with isOpen=true, then assert the active
tab resets to the default insight tab instead of remaining on “My Notes”; use
the existing AlgorithmInsightPanel render, rerender, and tab assertions to
validate the reopen state.

---

Nitpick comments:
In `@src/components/NoteEditor.test.jsx`:
- Around line 72-104: The mockUseEditor.mockImplementation set in the NoteEditor
test is leaking into later tests because clearAllMocks does not reset custom
implementations. In the "calls onUpdate when editor emits change" test, save the
default mock behavior and restore it after the test, or use a scoped
override/reset so NoteEditor and mockUseEditor do not keep the custom
onUpdate-capturing implementation beyond that case.

In `@src/hooks/useFavorites.test.js`:
- Around line 45-146: Add a test in useFavorites.test.js that covers the
stale-response race handled by requestRef in useFavorites. Simulate a slow
listFavorites call for the first user, rerender the hook with a second user
before it resolves, then resolve the first request and verify its result is
ignored and only the latest hydrate updates favorites. Use the existing
renderHook, rerender, listFavorites, and requestRef behavior as the target path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d3e75686-8a18-40df-8373-6e39ae2655e9

📥 Commits

Reviewing files that changed from the base of the PR and between 7d1e971 and 67b16fb.

📒 Files selected for processing (7)
  • src/components/AlgorithmDropdown.test.jsx
  • src/components/AlgorithmInsightPanel.test.jsx
  • src/components/AlgorithmNotesTab.test.jsx
  • src/components/NoteEditor.test.jsx
  • src/hooks/useFavorites.test.js
  • src/hooks/useNoteAutosave.test.js
  • vitest.config.js

Comment thread src/components/AlgorithmDropdown.test.jsx Outdated
Comment thread src/components/AlgorithmInsightPanel.test.jsx
…tor, and autosave

- Add FavoritesDropdown tests: open empty state, disabled when playing
- Add AlgorithmInsightPanel tests: no insight content, no year in MetaItem
- Add NoteEditor tests: setContent on mount when different, skip when same
- Add useNoteAutosave tests: online event trigger, unmount flush, algorithm key switch
- Refactor useNoteAutosave test helper to support algorithmKey via rerender props

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
src/hooks/useNoteAutosave.test.js (2)

197-222: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Test doesn't verify the "flushes" half of its name.

The test asserts getNote is called with the new algorithmKey (reload), but never asserts that upsertNote was called to flush the dirty content from the previous algorithmKey before reloading, even though the test title is "flushes and reloads when algorithmKey changes."

✅ Suggested addition
     rerender({ isActive: true, algorithmKey: 'selectionSort' });
 
+    await waitFor(() => {
+      expect(upsertNote).toHaveBeenCalledWith(
+        'user-1',
+        ALGORITHM_TYPES.SORTING,
+        'bubbleSort',
+        '<p>updated</p>'
+      );
+    });
+
     await waitFor(() => {
       expect(getNote).toHaveBeenCalledWith(
         'user-1',
         ALGORITHM_TYPES.SORTING,
         'selectionSort'
       );
     });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useNoteAutosave.test.js` around lines 197 - 222, The test for
useAutosave’s algorithmKey change path only checks the reload behavior and
misses the flush behavior in its name. Update the “flushes and reloads when
algorithmKey changes” case to also assert that the pending save is flushed by
calling upsertNote before getNote reloads the new note. Use the existing
scheduleSave, rerender, upsertNote, and getNote setup in the test to verify both
calls occur in the expected sequence.

150-172: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

State update outside act() on online dispatch.

window.dispatchEvent(new Event('online')) triggers performSave, which synchronously calls setSaveStatus('saving'), outside act(). The subsequent waitFor masks failures but React may emit act-warnings in the console.

🧪 Suggested wrap
-    window.dispatchEvent(new Event('online'));
+    act(() => {
+      window.dispatchEvent(new Event('online'));
+    });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useNoteAutosave.test.js` around lines 150 - 172, The online event
in useNoteAutosave.test.js is firing a state update outside React’s act boundary
because window.dispatchEvent(new Event('online')) immediately triggers
performSave and setSaveStatus('saving'). Wrap the online dispatch for this test
in act() within the existing renderAutosave / scheduleSave flow so the state
transition is flushed before the waitFor assertion and the test avoids act
warnings.
src/components/AlgorithmInsightPanel.test.jsx (1)

131-146: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Test doesn't verify "without year" behavior.

The test name claims to check that MetaItem renders without a year, but it only asserts the algorithm name renders and a tablist exists — it never checks for the absence of the year label/value (or presence of the inventor-only meta row).

✅ Suggested addition
     const nameElements = screen.getAllByText('Insertion Sort');
     expect(nameElements.length).toBeGreaterThan(0);
     expect(screen.getAllByRole('tablist').length).toBeGreaterThan(0);
+    expect(screen.queryByText('Year')).not.toBeInTheDocument();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/AlgorithmInsightPanel.test.jsx` around lines 131 - 146, The
test named for AlgorithmInsightPanel should actually validate the “without year”
case instead of only checking the title and tablist. Update the test around
AlgorithmInsightPanel/renderWithI18n to assert that no year label/value is
rendered when the algorithm year is null, and optionally verify the
inventor-only MetaItem row appears. Use the existing symbols
AlgorithmInsightPanel, renderWithI18n, and screen to locate and strengthen the
assertion.
src/components/NoteEditor.test.jsx (1)

115-139: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicated mock editor scaffolding.

matchedEditor re-implements the same chain/toggle/getHTML/commands shape presumably already produced by the hoisted useEditor mock. Extracting a shared createMockEditor(overrides) factory would reduce duplication and keep both tests in sync if the editor mock shape changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/NoteEditor.test.jsx` around lines 115 - 139, The test in
NoteEditor.test.jsx duplicates the same mocked editor shape in matchedEditor
instead of reusing the shared useEditor mock setup. Extract the common
chain/isActive/getHTML/commands structure into a createMockEditor(overrides)
helper and use it here with only the getHTML/content overrides needed, so the
NoteEditor test stays aligned with any future editor mock shape changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/components/AlgorithmInsightPanel.test.jsx`:
- Around line 131-146: The test named for AlgorithmInsightPanel should actually
validate the “without year” case instead of only checking the title and tablist.
Update the test around AlgorithmInsightPanel/renderWithI18n to assert that no
year label/value is rendered when the algorithm year is null, and optionally
verify the inventor-only MetaItem row appears. Use the existing symbols
AlgorithmInsightPanel, renderWithI18n, and screen to locate and strengthen the
assertion.

In `@src/components/NoteEditor.test.jsx`:
- Around line 115-139: The test in NoteEditor.test.jsx duplicates the same
mocked editor shape in matchedEditor instead of reusing the shared useEditor
mock setup. Extract the common chain/isActive/getHTML/commands structure into a
createMockEditor(overrides) helper and use it here with only the getHTML/content
overrides needed, so the NoteEditor test stays aligned with any future editor
mock shape changes.

In `@src/hooks/useNoteAutosave.test.js`:
- Around line 197-222: The test for useAutosave’s algorithmKey change path only
checks the reload behavior and misses the flush behavior in its name. Update the
“flushes and reloads when algorithmKey changes” case to also assert that the
pending save is flushed by calling upsertNote before getNote reloads the new
note. Use the existing scheduleSave, rerender, upsertNote, and getNote setup in
the test to verify both calls occur in the expected sequence.
- Around line 150-172: The online event in useNoteAutosave.test.js is firing a
state update outside React’s act boundary because window.dispatchEvent(new
Event('online')) immediately triggers performSave and setSaveStatus('saving').
Wrap the online dispatch for this test in act() within the existing
renderAutosave / scheduleSave flow so the state transition is flushed before the
waitFor assertion and the test avoids act warnings.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8d031179-9f3e-419f-8215-028f15adf0db

📥 Commits

Reviewing files that changed from the base of the PR and between 67b16fb and 1f7e68f.

📒 Files selected for processing (4)
  • src/components/AlgorithmInsightPanel.test.jsx
  • src/components/FavoritesDropdown.test.jsx
  • src/components/NoteEditor.test.jsx
  • src/hooks/useNoteAutosave.test.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/FavoritesDropdown.test.jsx

- 🔴 Fix critical self-referential deadlock in useNoteAutosave retry path
- 🟠 Wrap flush calls in try/catch so errors don't block UI
- 🟠 Catch unhandled rejections from void performSave() call sites
- 🟠 Add server-side favorite slot limit enforcement (DB trigger)
- 🟡 Add debounce timer cleanup on unmount
- 🟡 Fix mock prop names in AlgorithmNotesTab.test.jsx
- 🟡 Complete reset-on-reopen test in AlgorithmInsightPanel.test.jsx
- 🟡 Add flush assertion in useNoteAutosave algorithmKey change test
- 🟡 Wrap online event dispatch in act() in useNoteAutosave.test.js
- 🟡 Fix favorite notice: wrap in AnimatePresence, add aria-live, reuse slotLimit
- 🔵 Remove unused beforeEach import in AlgorithmDropdown.test.jsx
- 🔵 Fix mock leak in NoteEditor.test.jsx (resetAllMocks + restore default)
- 🔵 Add stale-response race test in useFavorites.test.js
- 🔵 Harden delete-account Edge Function (generic errors, scoped CORS, extract handler)
- 🔵 Fix docs/DEVELOPMENT.md JWT flag wording
- Use refs for save category/key to ensure flush uses old key before switch

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
supabase/migrations/20260705120000_enforce_favorite_slot_limit.sql (1)

4-24: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Consider pinning search_path on these functions.

Neither function sets SET search_path, which the Supabase linter flags as function_search_path_mutable. Actual risk here is low since all objects are fully qualified, but adding set search_path = '' (or = public) hardens against search-path hijacking and clears the linter warning.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/migrations/20260705120000_enforce_favorite_slot_limit.sql` around
lines 4 - 24, Pin the search path on both public.get_favorite_slot_limit and
public.check_favorite_slot_limit to satisfy the Supabase linter and harden
against search-path hijacking. Update these function definitions to include an
explicit SET search_path clause (for example, empty or public) while keeping the
existing fully qualified references intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@supabase/migrations/20260705120000_enforce_favorite_slot_limit.sql`:
- Around line 13-24: The trigger function check_favorite_slot_limit currently
checks count(*) without serializing concurrent inserts, so two sessions can both
pass and exceed the cap. Add a per-user transaction advisory lock at the start
of check_favorite_slot_limit, keyed by new.user_id, so all inserts for the same
user are serialized before the favorite_algorithms count is evaluated. Keep the
existing limit check and exception behavior unchanged after the lock is
acquired.

---

Nitpick comments:
In `@supabase/migrations/20260705120000_enforce_favorite_slot_limit.sql`:
- Around line 4-24: Pin the search path on both public.get_favorite_slot_limit
and public.check_favorite_slot_limit to satisfy the Supabase linter and harden
against search-path hijacking. Update these function definitions to include an
explicit SET search_path clause (for example, empty or public) while keeping the
existing fully qualified references intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5cb2e173-7264-4e0c-84d9-a9f2914b4e3b

📥 Commits

Reviewing files that changed from the base of the PR and between 1f7e68f and 97767fd.

📒 Files selected for processing (13)
  • docs/DEVELOPMENT.md
  • src/components/AlgorithmDropdown.test.jsx
  • src/components/AlgorithmInsightPanel.jsx
  • src/components/AlgorithmInsightPanel.test.jsx
  • src/components/AlgorithmNotesTab.test.jsx
  • src/components/NoteEditor.test.jsx
  • src/hooks/useFavorites.test.js
  • src/hooks/useNoteAutosave.js
  • src/hooks/useNoteAutosave.test.js
  • src/pages/VisualizerApp.jsx
  • src/pages/VisualizerApp.test.jsx
  • supabase/functions/delete-account/index.ts
  • supabase/migrations/20260705120000_enforce_favorite_slot_limit.sql
✅ Files skipped from review due to trivial changes (1)
  • docs/DEVELOPMENT.md
🚧 Files skipped from review as they are similar to previous changes (9)
  • supabase/functions/delete-account/index.ts
  • src/components/AlgorithmInsightPanel.test.jsx
  • src/components/NoteEditor.test.jsx
  • src/components/AlgorithmNotesTab.test.jsx
  • src/hooks/useFavorites.test.js
  • src/hooks/useNoteAutosave.test.js
  • src/pages/VisualizerApp.jsx
  • src/hooks/useNoteAutosave.js
  • src/components/AlgorithmInsightPanel.jsx

Comment thread supabase/migrations/20260705120000_enforce_favorite_slot_limit.sql
- Pin search_path on both functions (Supabase linter hardening)
- Add per-user pg_advisory_xact_lock to serialize concurrent inserts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
supabase/migrations/20260705120000_enforce_favorite_slot_limit.sql (1)

20-20: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Consider the two-argument advisory-lock form to avoid cross-feature key collisions.

The single-key pg_advisory_xact_lock(bigint) shares one global lock space, so the hashtext(...) value could collide with advisory locks taken elsewhere in the app. Using the two-argument form pg_advisory_xact_lock(classid int, objid int) lets you reserve a dedicated namespace for this feature.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/migrations/20260705120000_enforce_favorite_slot_limit.sql` at line
20, The favorite-slot limit trigger is using the single-key
pg_advisory_xact_lock via hashtext(...), which can collide with unrelated
advisory locks. Update the locking in the migration’s trigger/function that
enforces the favorite slot limit to use the two-argument
pg_advisory_xact_lock(classid, objid) form instead, choosing a dedicated
namespace for this feature while keeping the lock tied to new.user_id in the
existing favorite_slot_* logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@supabase/migrations/20260705120000_enforce_favorite_slot_limit.sql`:
- Line 20: The favorite-slot limit trigger is using the single-key
pg_advisory_xact_lock via hashtext(...), which can collide with unrelated
advisory locks. Update the locking in the migration’s trigger/function that
enforces the favorite slot limit to use the two-argument
pg_advisory_xact_lock(classid, objid) form instead, choosing a dedicated
namespace for this feature while keeping the lock tied to new.user_id in the
existing favorite_slot_* logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e6284899-cf99-4e7c-a723-ec652aa3f2b7

📥 Commits

Reviewing files that changed from the base of the PR and between 97767fd and 0847463.

📒 Files selected for processing (1)
  • supabase/migrations/20260705120000_enforce_favorite_slot_limit.sql

@ayoub3bidi
ayoub3bidi merged commit 5862be1 into develop Jul 5, 2026
16 checks passed
@ayoub3bidi
ayoub3bidi deleted the feat/favorites-and-notes branch July 5, 2026 09:47
@ayoub3bidi ayoub3bidi mentioned this pull request Jul 16, 2026
35 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

config dependencies documentation Improvements or additions to documentation style Improve styling, design, and animation tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant