Add notes feature, sysinfo, and auth window (v0.5.0)#5
Conversation
Introduce a local notes (SuperNote) subsystem and system info + authentication helpers. - Add new Note UI: NotePanel, NoteEditor, NoteSourceList, NoteStickyBoard and wiring in App.tsx (notes state, folders, CRUD handlers, board/cards views, pinned items). - Integrate notes into layout and TitleBar; add UI controls for sync interval, show system info toggle, and a visible sync error toast. - FeedPanel: add "mark all as unread" support and update action button UI. - Tauri: expose native commands for CPU/memory/network metrics and an open_auth_window helper; register them in invoke_handler and enable an "auth" webview capability. Bump tauri product version. - Add sysinfo dependency to src-tauri/Cargo.toml (used by new native commands). - Add a DB migration SQL to add podcast source and a minor font update in index.html. These changes enable an offline note-taking mode, surface basic system metrics to the frontend, and provide a dedicated auth webview for sign-in flows while improving sync configurability and error visibility.
📝 WalkthroughWalkthroughIntroduces system monitoring capabilities through Tauri commands (CPU, memory, network), implements a comprehensive note-taking system with multiple UI components and view modes, adds feed/folder pinning functionality, enhances OAuth with Tauri window handling, improves sync error visibility and Supabase integration with PKCE flow. Changes
Sequence Diagram(s)sequenceDiagram
participant User as User (Browser)
participant App as App.tsx
participant AuthCtx as AuthContext
participant Tauri as Tauri Backend
participant OAuth as OAuth Provider
participant Supabase as Supabase Auth
User->>App: Trigger OAuth Sign In
App->>AuthCtx: signInWithOAuth(provider)
AuthCtx->>Supabase: signInWithOAuth (skipBrowserRedirect)
Supabase-->>AuthCtx: Return OAuth URL + session data
AuthCtx->>Tauri: invoke('open_auth_window', { url })
Tauri->>OAuth: Open auth window with redirect
OAuth->>User: Display login form
User->>OAuth: Enter credentials & authorize
OAuth-->>Tauri: Redirect to callback URL with PKCE code
Tauri-->>AuthCtx: Emit 'auth-callback' event with code
AuthCtx->>Supabase: exchangeCodeForSession(code)
Supabase-->>AuthCtx: Return authenticated session + user
AuthCtx->>AuthCtx: Update local state (user, session)
AuthCtx->>App: Context listeners update
App-->>User: Display authenticated state
Tauri->>Tauri: Close auth window
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Review Summary by QodoAdd notes feature, system monitoring, and auth improvements (v0.5.0)
WalkthroughsDescription• **Notes feature**: Comprehensive note-taking system with editor, panel, source list with folder management, and sticky board component for visual note organization • **System monitoring**: Added CPU, memory, and network speed monitoring with Tauri backend integration and real-time display in titlebar • **Authentication improvements**: Implemented Tauri-based OAuth with PKCE flow, custom auth window, and enhanced session management with localStorage cleanup on sign-out • **Sync service enhancements**: Fixed race conditions between pushFeed and pushNewItems with feed caching and in-flight promise tracking; improved merge logic to match by URL; added FK constraint validation; enhanced error handling with event dispatching • **UI enhancements**: Added pinning feature for feeds/folders, mark-all-as-unread functionality with visual feedback, sync interval configuration, system info visibility toggle, and comprehensive styling for all new features • **Database updates**: Added podcast source type support and configured PKCE authentication flow Diagramflowchart LR
A["Notes System<br/>Editor, Panel, Folders,<br/>Sticky Board"]
B["System Monitoring<br/>CPU, Memory,<br/>Network Speed"]
C["Auth Window<br/>PKCE Flow<br/>Session Management"]
D["Sync Service<br/>Race Condition Fixes<br/>Error Handling"]
E["UI Enhancements<br/>Pinning, Mark All,<br/>Settings"]
F["App Integration<br/>State Management<br/>localStorage"]
A --> F
B --> F
C --> F
D --> F
E --> F
File Changes1. src/services/syncService.ts
|
Code Review by Qodo
1. Notes CRUD lacks audit logs
|
| const handleAddNote = useCallback(() => { | ||
| const newNote: Note = { | ||
| id: crypto.randomUUID(), | ||
| title: 'Nouvelle note', | ||
| content: '', | ||
| folder: selectedNoteFolder ?? undefined, | ||
| createdAt: new Date().toISOString(), | ||
| updatedAt: new Date().toISOString(), | ||
| }; | ||
| setNotes(prev => [newNote, ...prev]); | ||
| setSelectedNoteId(newNote.id); | ||
| }, [selectedNoteFolder]); | ||
|
|
||
| const handleDeleteNote = useCallback((noteId: string) => { | ||
| setNotes(prev => prev.filter(n => n.id !== noteId)); | ||
| setSelectedNoteId(prev => prev === noteId ? null : prev); | ||
| }, []); | ||
|
|
||
| const handleUpdateNote = useCallback((noteId: string, updates: Partial<Note>) => { | ||
| setNotes(prev => prev.map(n => | ||
| n.id === noteId | ||
| ? { ...n, ...updates, updatedAt: new Date().toISOString() } | ||
| : n | ||
| )); | ||
| }, []); |
There was a problem hiding this comment.
1. Notes crud lacks audit logs 📘 Rule violation ⛨ Security
The new notes feature creates/updates/deletes user content without any audit trail (no user ID, timestamped action, or outcome logging). This makes it difficult to reconstruct sensitive user-data changes for security/compliance investigations.
Agent Prompt
## Issue description
New note create/update/delete operations are performed without audit logging, making it impossible to reconstruct user actions on potentially sensitive note content.
## Issue Context
Compliance requires audit logs for critical actions with user ID, timestamp, action description, and outcome. Notes are persisted (via `localStorage`) and can contain sensitive data.
## Fix Focus Areas
- src/App.tsx[139-163]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| const handler = (e: Event) => { | ||
| const detail = (e as CustomEvent).detail as { operation: string; message: string }; | ||
| setSyncError(`Sync: ${detail.operation} — ${detail.message}`); | ||
| setTimeout(() => setSyncError(null), 8000); |
There was a problem hiding this comment.
2. Sync toast leaks internal errors 📘 Rule violation ⛨ Security
The UI displays raw sync error messages from backend/Supabase to end users. These messages can reveal internal system details (e.g., table/constraint names) and should be replaced with generic user-facing text while keeping detailed errors only in internal logs.
Agent Prompt
## Issue description
User-facing UI toasts show raw sync error messages (`detail.message`) which may leak internal system details.
## Issue Context
Secure error handling requires generic messages for end users and detailed diagnostics only in secure/internal logs.
## Fix Focus Areas
- src/App.tsx[71-74]
- src/App.tsx[605-613]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| setUserId(userId: string | null) { | ||
| console.log('[sync] setUserId:', userId); | ||
| _currentUserId = userId; | ||
| if (!userId) { |
There was a problem hiding this comment.
3. console.log leaks userid 📘 Rule violation ⛨ Security
Sync logging prints userId (and other operational details) to the console in unstructured form. This increases the risk of exposing PII/sensitive identifiers in logs and makes logs harder to audit.
Agent Prompt
## Issue description
Sync code logs `userId` and other details in plain, unstructured console logs.
## Issue Context
Secure logging requires avoiding PII/sensitive identifiers in logs and using structured logs for auditing.
## Fix Focus Areas
- src/services/syncService.ts[190-193]
- src/services/syncService.ts[367-377]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| async fn open_auth_window(app: tauri::AppHandle, url: String) -> Result<(), String> { | ||
| use tauri::{Emitter, WebviewUrl, WebviewWindowBuilder}; | ||
|
|
||
| // Close any existing auth window | ||
| if let Some(existing) = app.get_webview_window("auth") { | ||
| let _ = existing.close(); | ||
| } | ||
|
|
||
| let parsed_url: Url = url.parse().map_err(|e: url::ParseError| format!("Invalid URL: {e}"))?; | ||
| let app_handle = app.clone(); | ||
|
|
||
| WebviewWindowBuilder::new(&app, "auth", WebviewUrl::External(parsed_url)) | ||
| .title("Sign in") |
There was a problem hiding this comment.
4. open_auth_window accepts any url 📘 Rule violation ⛨ Security
The new open_auth_window command builds an external webview window from a caller-provided URL without allowlisting/validation. This can enable opening arbitrary external content inside the app if the command is invoked with a malicious URL.
Agent Prompt
## Issue description
`open_auth_window` opens an external webview for any parsed URL without allowlisting, which is unsafe for externally-influenced input.
## Issue Context
Security-first input validation requires validating/sanitizing external inputs and preventing unsafe navigation to arbitrary domains.
## Fix Focus Areas
- src-tauri/src/lib.rs[594-606]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| static SYS: OnceLock<Mutex<System>> = OnceLock::new(); | ||
| let mtx = SYS.get_or_init(|| { | ||
| let mut sys = System::new(); | ||
| sys.refresh_cpu_usage(); | ||
| Mutex::new(sys) | ||
| }); | ||
| let mut sys = mtx.lock().unwrap(); | ||
| sys.refresh_cpu_usage(); | ||
| sys.global_cpu_usage() |
There was a problem hiding this comment.
5. mtx.lock().unwrap() may panic 📘 Rule violation ⛯ Reliability
The new sysinfo Tauri commands use unwrap() on mutex locks, which can crash the app if the mutex is poisoned or unavailable. This is a new failure mode without graceful error handling or fallback behavior.
Agent Prompt
## Issue description
Sysinfo commands use `unwrap()` on mutex locks, which can panic and crash the app under error conditions.
## Issue Context
Robust error handling requires graceful degradation and actionable diagnostics rather than panics.
## Fix Focus Areas
- src-tauri/src/lib.rs[267-275]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| icon: feed.icon, | ||
| url: feed.url, | ||
| color: feed.color, | ||
| folder: feed.folder ?? null, | ||
| updated_at: feed.updated_at ?? new Date().toISOString(), | ||
| }; | ||
| } |
There was a problem hiding this comment.
6. Feed folders not synced 🐞 Bug ✓ Correctness
syncService.feedToRow() no longer includes the folder column, so folder organization changes will not be persisted to Supabase and will be lost/mismatched across devices. The app heavily relies on feed.folder for folder UI and pinned-folder unread aggregation.
Agent Prompt
## Issue description
Feed folder organization is not being persisted to Supabase because `feedToRow()` omits the `folder` column.
## Issue Context
- The database schema includes `feeds.folder`.
- The UI and folder CRUD logic rely on `feed.folder`.
- Sync pulls `row.folder` but never pushes `feed.folder`.
## Fix Focus Areas
- src/services/syncService.ts[37-48]
- src/services/syncService.ts[50-62]
## Suggested change
- Add `folder: feed.folder ?? null` (or `folder: feed.folder`) back to `feedToRow()` so `pushFeed`, `ensureFeedInSupabase`, and `fullSync` feed inserts/upserts persist folder state.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| #[cfg(target_os = "android")] | ||
| #[tauri::command] | ||
| async fn open_auth_window(_url: String) -> Result<(), String> { | ||
| Ok(()) |
There was a problem hiding this comment.
7. Android auth invoke mismatch 🐞 Bug ✓ Correctness
On Android, the Rust open_auth_window command parameter is named _url, but the frontend invokes
it with { url: ... }. Tauri matches parameters by name, so OAuth will likely fail to open the auth
window on Android.
Agent Prompt
## Issue description
Android build likely fails to invoke `open_auth_window` because the Rust parameter name is `_url` but the frontend sends `{ url: ... }`.
## Issue Context
Tauri `invoke()` payload keys must match the command function parameter names.
## Fix Focus Areas
- src-tauri/src/lib.rs[623-627]
- src/contexts/AuthContext.tsx[57-94]
## Suggested change
- Change Android implementation to:
- `async fn open_auth_window(url: String) -> Result<(), String> { let _ = url; Ok(()) }`
- Keep frontend invocation as-is (`{ url: data.url }`).
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| for (const note of notes) { | ||
| if (note.stickyX === undefined) { | ||
| onUpdateNote(note.id, { | ||
| stickyX: Math.random() * Math.max(100, w - 260) + 30, | ||
| stickyY: Math.random() * Math.max(100, h - 260) + 30, | ||
| stickyRotation: randomRotation(), | ||
| stickyZIndex: maxZ + 1, | ||
| stickyColor: selectedColor, | ||
| }); | ||
| setMaxZ(z => z + 1); |
There was a problem hiding this comment.
8. Sticky z-index collision 🐞 Bug ✓ Correctness
When initializing sticky-note properties, all notes missing stickyX receive `stickyZIndex: maxZ + 1` within the same effect run, leading to identical z-indexes and incorrect stacking order.
Agent Prompt
## Issue description
Sticky note initialization assigns the same z-index to multiple notes created/initialized in a single effect run.
## Issue Context
This manifests when multiple notes are present without sticky props (e.g., initial migration, importing notes, or creating multiple notes quickly).
## Fix Focus Areas
- src/components/NoteStickyBoard.tsx[235-252]
## Suggested change
- Use a local counter:
- `let z = maxZ;`
- for each init: `z += 1; stickyZIndex: z`
- after loop: `setMaxZ(z)`
- Alternatively store `maxZ` in a ref and increment synchronously.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/services/syncService.ts (1)
188-197:⚠️ Potential issue | 🟠 MajorClear in-memory caches when the user changes.
_feedsCacheand_pushFeedPromisespersist across sessions; after sign-out they can upsert prior-user feeds into the next account. Clear them whenuserIdchanges or becomes null.🔒 Suggested fix
setUserId(userId: string | null) { console.log('[sync] setUserId:', userId); - _currentUserId = userId; - if (!userId) { - _pendingItemUpdates.clear(); - if (_debounceTimer) clearTimeout(_debounceTimer); - } + const prevUserId = _currentUserId; + _currentUserId = userId; + if (!userId || prevUserId !== userId) { + _pendingItemUpdates.clear(); + if (_debounceTimer) clearTimeout(_debounceTimer); + _feedsCache.clear(); + _pushFeedPromises.clear(); + } },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/services/syncService.ts` around lines 188 - 197, When setting a new user in SyncService.setUserId, clear in-memory caches when the user changes or becomes null to avoid leaking prior-user data: compare existing _currentUserId to the incoming userId and if different (or if userId is null) call _feedsCache.clear() and _pushFeedPromises.clear(); keep the existing behavior of clearing _pendingItemUpdates and cancelling _debounceTimer when userId is falsy. Ensure you reference SyncService.setUserId, _currentUserId, _feedsCache, _pushFeedPromises, _pendingItemUpdates, and _debounceTimer when making the change.src/components/SourcePanel.tsx (1)
353-377:⚠️ Potential issue | 🟡 MinorReconcile pinned entries on feed/folder rename or delete.
Pins are keyed by
feedId/folderPathand persisted. When a feed or folder is renamed/deleted, those pins remain stale and can surface dead items in TitleBar. Update or remove affected pins during rename/delete operations.🔧 Example fix (remove pins on delete; extend for rename)
+ const removePinned = useCallback((predicate: (p: PinEntry) => boolean) => { + setPinnedItems(prev => { + const next = prev.filter(p => !predicate(p)); + savePinnedItems(next); + onPinsChange?.(next); + return next; + }); + }, [onPinsChange]);onClick={() => { + removePinned(p => p.kind === 'folder' && p.categoryId === contextMenu.categoryId && p.folderPath === contextMenu.folderPath); onDeleteFolder(contextMenu.categoryId, contextMenu.folderPath); setContextMenu(null); }}onClick={() => { + removePinned(p => p.kind === 'feed' && p.feedId === contextMenu.feed.id); onRemoveFeed(contextMenu.feed.id); setContextMenu(null); }}Consider extending the same idea to rename flows to update pin labels/paths.
Also applies to: 465-485, 915-919, 961-963
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/SourcePanel.tsx` around lines 353 - 377, Pins are stored keyed by feedId/folderPath and are not updated when feeds/folders are renamed or deleted, causing stale entries in TitleBar; add reconciliation logic that on feed delete (where onDeleteFeed is called) removes any pins matching that feedId, and on feed rename (where onRenameFeed is called) updates any pins' labels/paths to the new name/path (or removes them if path semantics change); factor this into small helpers (e.g., reconcilePinsByFeedId and reconcilePinsByFolderPath) that read the persisted pin store, filter/update entries, persist the result, and invoke these helpers from the feed/folder rename and delete handlers (also apply the same calls in the folder rename/delete flows referenced around the other locations).
🧹 Nitpick comments (2)
supabase/migrations/20250224000004_add_podcast_source.sql (1)
2-3: Consider minimizing lock/scan when re-adding the CHECK constraint.On large
feedstables, re-adding a CHECK constraint can scan the table and hold stronger locks. ConsiderNOT VALID+VALIDATE CONSTRAINTto reduce lock duration.♻️ Suggested migration tweak
alter table feeds drop constraint feeds_source_check; -alter table feeds add constraint feeds_source_check check (source in ('article','reddit','youtube','twitter','mastodon','podcast')); +alter table feeds add constraint feeds_source_check check (source in ('article','reddit','youtube','twitter','mastodon','podcast')) not valid; +alter table feeds validate constraint feeds_source_check;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@supabase/migrations/20250224000004_add_podcast_source.sql` around lines 2 - 3, When re-adding the feeds_source_check constraint on table feeds, avoid a full table scan/long lock by creating the check as NOT VALID and then validating it separately; specifically, replace the direct "add constraint feeds_source_check check (source in (...))" with adding the same CHECK as NOT VALID and then run "VALIDATE CONSTRAINT feeds_source_check" once low-lock conditions are acceptable so the initial ADD CONSTRAINT is fast and the expensive validation is performed separately.src/components/NoteSourceList.tsx (1)
109-183: Pre-group notes by folder to avoid repeated filtering.
notes.filter(...)runs once for root notes and again for every folder, which scales poorly as notes grow. Consider grouping notes in auseMemoto keep renders predictable.♻️ Suggested refactor
- const rootNotes = notes.filter(n => !n.folder); + const notesByFolder = useMemo(() => { + const map = new Map<string, Note[]>(); + for (const n of notes) { + const key = n.folder ?? '__root__'; + const list = map.get(key) ?? []; + list.push(n); + map.set(key, list); + } + return map; + }, [notes]); + + const rootNotes = notesByFolder.get('__root__') ?? [];- const folderNotes = notes.filter(n => n.folder === folder); + const folderNotes = notesByFolder.get(folder) ?? [];🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/NoteSourceList.tsx` around lines 109 - 183, The component repeatedly calls notes.filter (once for rootNotes and again inside folders to build folderNotes) causing O(n*m) work; refactor by computing a grouped map of notes by folder using React.useMemo (e.g., const notesByFolder = useMemo(() => { ... }, [notes])) and replace rootNotes and the per-folder notes.filter with lookups like notesByFolder[''] or notesByFolder[folder]; update references in renderNoteItem, where folderNotes is used, and keep expandedFolders logic unchanged so rendering uses the memoized group instead of repeated filtering.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
@.specstory/history/2026-02-23_16-43Z-produire-une-description-de-pull-request-pour-la-branche-git.md:
- Around line 9-13: Corrigez les fautes et améliorez la formulation du texte
affiché (repérez la ligne commençant par "Produire une description de Pull
Request pour la branche Git actuelle...") : appliquez les accents corrects (ex.
"clôtures"), remplacez formulations maladroites par des équivalents naturels en
français (par ex. "branche Git courante" ou "conformément aux conventions GitHub
de nommage, syntaxe et formatage"), enlevez éléments superflus/markers inutiles
(ex. underscores autour de "Agent") et assurez une phrase concise qui conserve
l'instruction "Ne pas inclure de captures d'écrans et ne pas indiquer de
clôtures d'issue GitHub."
In `@src-tauri/capabilities/default.json`:
- Around line 5-10: Remove "auth" from the default capability set so the auth
webview does not inherit broad permissions; update the "windows" array that
currently contains ["main", "auth"] to only include "main" and create a new
minimal capability file (or capability entry) for the auth window that omits
"core:default" and other sensitive permissions (keep only explicitly required
permissions for OAuth webviews such as safe webview-specific entries). Ensure
references to the auth window everywhere (the "auth" window name) point to the
new minimal capability instead of the default.
In `@src/App.tsx`:
- Around line 68-78: The handler sets a timeout each time without clearing prior
timers, causing older timers to clear newer messages; change the effect to track
the timeout ID (e.g., let timeoutId: ReturnType<typeof setTimeout> | null) in
the closure, call clearTimeout(timeoutId) before creating a new setTimeout in
the handler, assign the new ID to timeoutId, and also clearTimeout(timeoutId) in
the cleanup function returned by useEffect; update references to
syncError/setSyncError and SYNC_ERROR_EVENT only within the existing handler and
effect.
- Around line 28-38: The getSyncInterval function currently returns Number(v)
unchecked, which can be NaN or <=0; update getSyncInterval to validate the
parsed value from localStorage (using Number or parseInt) ensuring it's a finite
positive number and enforce a sensible minimum (e.g., at least
DEFAULT_SYNC_INTERVAL or another MIN_SYNC_INTERVAL constant) before returning
it; if validation fails, fall back to DEFAULT_SYNC_INTERVAL. Reference:
SYNC_INTERVAL_KEY, DEFAULT_SYNC_INTERVAL, and getSyncInterval.
In `@src/components/NoteSourceList.tsx`:
- Around line 111-121: renderNoteItem uses plain <div>s for interactive rows
which aren't keyboard-focusable; update the note and folder row renderers (e.g.,
renderNoteItem and the folder row renderer handling
handleNoteContext/onSelectNote) to add role="button" and tabIndex={0}, and
implement an onKeyDown handler that triggers onSelectNote(note.id) when Enter or
Space is pressed and triggers handleNoteContext when the context-menu key or
Shift+F10 is detected; ensure the same changes are applied to the folder rows
referenced around lines 192-222 so keyboard-only users can focus and activate
rows.
- Around line 39-75: expandedFolders is initialized from folders but not kept in
sync; add a useEffect(() => { setExpandedFolders(prev => { const next = new
Set(prev); // remove any keys not present in folders for (const k of
Array.from(next)) if (!folders.includes(k)) next.delete(k); return next; }); },
[folders]) to reconcile state when the folders prop changes, and update
handleRename to migrate the key by calling setExpandedFolders(prev => { const
next = new Set(prev); if (renamingFolder) { next.delete(renamingFolder);
next.add(name); } return next; }) after a successful onRenameFolder call (use
the same name variable in handleRename), so renamed folders remain expanded;
keep handleCreateFolder logic (it already adds new folder) and rely on the
reconcile effect to remove entries when folders are deleted.
In `@src/components/NoteStickyBoard.tsx`:
- Around line 235-252: When initializing missing sticky props in the useEffect
that iterates over notes, multiple notes are being assigned the same
stickyZIndex via stickyZIndex: maxZ + 1; fix this by tracking a local z counter
(start at maxZ) and for each note assign stickyZIndex: localZ + 1 then increment
localZ, call onUpdateNote for each note as before, and after the loop call
setMaxZ(localZ) once; update references in the useEffect body that uses
boardRef, notes, onUpdateNote, randomRotation, selectedColor and maxZ
accordingly.
In `@src/components/SettingsModal.tsx`:
- Around line 166-172: The current useState initializer for syncIntervalMs reads
localStorage.getItem(SYNC_INTERVAL_KEY) and returns Number(v) without
validating, which allows NaN to propagate; update the initializer in
SettingsModal (the function that defines syncIntervalMs and setSyncIntervalMs)
to parse the stored string, check Number.isFinite(parsed) or
!Number.isNaN(parsed), and only return the parsed numeric value when valid,
otherwise return DEFAULT_SYNC_INTERVAL; ensure any catch still falls back to
DEFAULT_SYNC_INTERVAL so the <select> always matches an option.
In `@src/components/SourcePanel.tsx`:
- Around line 127-129: Wrap the localStorage write in savePinnedItems so
failures (private mode/quota) don't throw: in function savePinnedItems(pins:
PinEntry[]) catch exceptions from localStorage.setItem(PINS_KEY, ...) and handle
them gracefully (e.g., swallow or log via console.warn/processLogger) so
toggling pins continues to work; ensure you still stringify the pins and do not
change the function's signature.
In `@src/contexts/AuthContext.tsx`:
- Around line 68-93: The auth callback listener currently uses
listen('auth-callback', ...) which can register multiple handlers across
retries; replace listen with once('auth-callback', ...) so the handler
auto-unregisters after the first invocation, remove the manual unlisten() call
(or adjust it to the once return value if your API differs), and keep the
existing logic that closes the WebviewWindow and exchanges the code with
supabase.auth.exchangeCodeForSession; target the listener creation in
AuthContext (the listen call for 'auth-callback') and ensure error handling and
state update (setState with data.session) remain unchanged.
---
Outside diff comments:
In `@src/components/SourcePanel.tsx`:
- Around line 353-377: Pins are stored keyed by feedId/folderPath and are not
updated when feeds/folders are renamed or deleted, causing stale entries in
TitleBar; add reconciliation logic that on feed delete (where onDeleteFeed is
called) removes any pins matching that feedId, and on feed rename (where
onRenameFeed is called) updates any pins' labels/paths to the new name/path (or
removes them if path semantics change); factor this into small helpers (e.g.,
reconcilePinsByFeedId and reconcilePinsByFolderPath) that read the persisted pin
store, filter/update entries, persist the result, and invoke these helpers from
the feed/folder rename and delete handlers (also apply the same calls in the
folder rename/delete flows referenced around the other locations).
In `@src/services/syncService.ts`:
- Around line 188-197: When setting a new user in SyncService.setUserId, clear
in-memory caches when the user changes or becomes null to avoid leaking
prior-user data: compare existing _currentUserId to the incoming userId and if
different (or if userId is null) call _feedsCache.clear() and
_pushFeedPromises.clear(); keep the existing behavior of clearing
_pendingItemUpdates and cancelling _debounceTimer when userId is falsy. Ensure
you reference SyncService.setUserId, _currentUserId, _feedsCache,
_pushFeedPromises, _pendingItemUpdates, and _debounceTimer when making the
change.
---
Nitpick comments:
In `@src/components/NoteSourceList.tsx`:
- Around line 109-183: The component repeatedly calls notes.filter (once for
rootNotes and again inside folders to build folderNotes) causing O(n*m) work;
refactor by computing a grouped map of notes by folder using React.useMemo
(e.g., const notesByFolder = useMemo(() => { ... }, [notes])) and replace
rootNotes and the per-folder notes.filter with lookups like notesByFolder[''] or
notesByFolder[folder]; update references in renderNoteItem, where folderNotes is
used, and keep expandedFolders logic unchanged so rendering uses the memoized
group instead of repeated filtering.
In `@supabase/migrations/20250224000004_add_podcast_source.sql`:
- Around line 2-3: When re-adding the feeds_source_check constraint on table
feeds, avoid a full table scan/long lock by creating the check as NOT VALID and
then validating it separately; specifically, replace the direct "add constraint
feeds_source_check check (source in (...))" with adding the same CHECK as NOT
VALID and then run "VALIDATE CONSTRAINT feeds_source_check" once low-lock
conditions are acceptable so the initial ADD CONSTRAINT is fast and the
expensive validation is performed separately.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (2)
src-tauri/Cargo.lockis excluded by!**/*.locksrc-tauri/gen/schemas/capabilities.jsonis excluded by!**/gen/**
📒 Files selected for processing (21)
.specstory/history/2026-02-23_16-43Z-produire-une-description-de-pull-request-pour-la-branche-git.mdindex.htmlsrc-tauri/Cargo.tomlsrc-tauri/capabilities/default.jsonsrc-tauri/src/lib.rssrc-tauri/tauri.conf.jsonsrc/App.tsxsrc/components/FeedPanel.tsxsrc/components/NoteEditor.tsxsrc/components/NotePanel.tsxsrc/components/NoteSourceList.tsxsrc/components/NoteStickyBoard.tsxsrc/components/SettingsModal.tsxsrc/components/SourcePanel.tsxsrc/components/TitleBar.tsxsrc/contexts/AuthContext.tsxsrc/hooks/useFeedStore.tssrc/index.csssrc/lib/supabase.tssrc/services/syncService.tssupabase/migrations/20250224000004_add_podcast_source.sql
| Produire une description de Pull Request pour la branche Git actuelle en analysant les derniers commit effectués sur celle-ci et en respectant les standards GitHub de nommage, syntaxe et formatage. Ne pas inclure de captures d'écrans et ne pas indiquer de clotures d'issue GitHub. | ||
|
|
||
| --- | ||
|
|
||
| _**Agent (model default, mode Agent)**_ |
There was a problem hiding this comment.
Fix minor French typos and phrasing.
These are small but user-visible text issues.
✍️ Suggested edits
-Produire une description de Pull Request pour la branche Git actuelle en analysant les derniers commit effectués sur celle-ci et en respectant les standards GitHub de nommage, syntaxe et formatage. Ne pas inclure de captures d'écrans et ne pas indiquer de clotures d'issue GitHub.
+Produire une description de Pull Request pour la branche Git actuelle en analysant les derniers commits effectués sur celle-ci et en respectant les standards GitHub de nommage, syntaxe et formatage. Ne pas inclure de captures d'écrans et ne pas indiquer de clôtures d'issue GitHub.
-_**Agent (model default, mode Agent)**_
+_**Agent (modèle par défaut, mode Agent)**_📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| Produire une description de Pull Request pour la branche Git actuelle en analysant les derniers commit effectués sur celle-ci et en respectant les standards GitHub de nommage, syntaxe et formatage. Ne pas inclure de captures d'écrans et ne pas indiquer de clotures d'issue GitHub. | |
| --- | |
| _**Agent (model default, mode Agent)**_ | |
| Produire une description de Pull Request pour la branche Git actuelle en analysant les derniers commits effectués sur celle-ci et en respectant les standards GitHub de nommage, syntaxe et formatage. Ne pas inclure de captures d'écrans et ne pas indiquer de clôtures d'issue GitHub. | |
| --- | |
| _**Agent (modèle par défaut, mode Agent)**_ |
🧰 Tools
🪛 LanguageTool
[grammar] ~9-~9: Le mot « commits » est plus probable.
Context: ... Git actuelle en analysant les derniers commit effectués sur celle-ci et en respectant...
(QB_NEW_FR_OTHER_ERROR_IDS_REPLACEMENT_CONFUSION_COMMIT_COMMITS)
[grammar] ~9-~9: Utilisez les accents correctement
Context: ...captures d'écrans et ne pas indiquer de clotures d'issue GitHub. --- _**Agent (model d...
(QB_NEW_FR_OTHER_ERROR_IDS_MISSING_ORTHOGRAPHY_DIACRITIC_CIRCUMFLEX)
[grammar] ~13-~13: Essayez « modèle par défaut »
Context: ...lotures d'issue GitHub. --- Agent (model default, mode Agent) ---
(QB_NEW_FR_OTHER_ERROR_IDS_REPLACEMENT_MULTITOKEN)
[grammar] ~13-~13: Essayez « modèle par défaut »
Context: ...s d'issue GitHub. --- Agent (model default, mode Agent) ---
(QB_NEW_FR_OTHER_ERROR_IDS_REPLACEMENT_MULTITOKEN)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
@.specstory/history/2026-02-23_16-43Z-produire-une-description-de-pull-request-pour-la-branche-git.md
around lines 9 - 13, Corrigez les fautes et améliorez la formulation du texte
affiché (repérez la ligne commençant par "Produire une description de Pull
Request pour la branche Git actuelle...") : appliquez les accents corrects (ex.
"clôtures"), remplacez formulations maladroites par des équivalents naturels en
français (par ex. "branche Git courante" ou "conformément aux conventions GitHub
de nommage, syntaxe et formatage"), enlevez éléments superflus/markers inutiles
(ex. underscores autour de "Agent") et assurez une phrase concise qui conserve
l'instruction "Ne pas inclure de captures d'écrans et ne pas indiquer de
clôtures d'issue GitHub."
| "windows": ["main", "auth"], | ||
| "permissions": [ | ||
| "core:default", | ||
| "core:event:default", | ||
| "core:webview:allow-create-webview-window", | ||
| "core:window:allow-minimize", |
There was a problem hiding this comment.
Avoid granting default capabilities to the auth window.
If the auth webview loads remote OAuth pages, inheriting core:default (and other window permissions) exposes sensitive APIs to untrusted content. Move auth into a minimal, dedicated capability file instead of the default set.
🔒 Suggested fix (remove auth from default capability)
- "windows": ["main", "auth"],
+ "windows": ["main"],📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "windows": ["main", "auth"], | |
| "permissions": [ | |
| "core:default", | |
| "core:event:default", | |
| "core:webview:allow-create-webview-window", | |
| "core:window:allow-minimize", | |
| "windows": ["main"], | |
| "permissions": [ | |
| "core:default", | |
| "core:event:default", | |
| "core:webview:allow-create-webview-window", | |
| "core:window:allow-minimize", |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src-tauri/capabilities/default.json` around lines 5 - 10, Remove "auth" from
the default capability set so the auth webview does not inherit broad
permissions; update the "windows" array that currently contains ["main", "auth"]
to only include "main" and create a new minimal capability file (or capability
entry) for the auth window that omits "core:default" and other sensitive
permissions (keep only explicitly required permissions for OAuth webviews such
as safe webview-specific entries). Ensure references to the auth window
everywhere (the "auth" window name) point to the new minimal capability instead
of the default.
| const SYNC_INTERVAL_KEY = 'superflux_sync_interval'; | ||
| const DEFAULT_SYNC_INTERVAL = 5 * 60 * 1000; // 5 minutes | ||
| const SHOW_SYSINFO_KEY = 'superflux_show_sysinfo'; | ||
|
|
||
| function getSyncInterval(): number { | ||
| try { | ||
| const v = localStorage.getItem(SYNC_INTERVAL_KEY); | ||
| if (v) return Number(v); | ||
| } catch { /* ignore */ } | ||
| return DEFAULT_SYNC_INTERVAL; | ||
| } |
There was a problem hiding this comment.
Validate the sync interval before scheduling fullSync.
Number(v) can yield NaN or 0, which will cause setInterval to hammer fullSync. Add finite/positive checks (and a minimum) before accepting the stored value.
🛡️ Suggested fix
const SYNC_INTERVAL_KEY = 'superflux_sync_interval';
const DEFAULT_SYNC_INTERVAL = 5 * 60 * 1000; // 5 minutes
+const MIN_SYNC_INTERVAL = 30 * 1000; // 30 seconds
function getSyncInterval(): number {
try {
const v = localStorage.getItem(SYNC_INTERVAL_KEY);
- if (v) return Number(v);
+ if (v != null) {
+ const n = Number(v);
+ if (Number.isFinite(n) && n >= MIN_SYNC_INTERVAL) return n;
+ }
} catch { /* ignore */ }
return DEFAULT_SYNC_INTERVAL;
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/App.tsx` around lines 28 - 38, The getSyncInterval function currently
returns Number(v) unchecked, which can be NaN or <=0; update getSyncInterval to
validate the parsed value from localStorage (using Number or parseInt) ensuring
it's a finite positive number and enforce a sensible minimum (e.g., at least
DEFAULT_SYNC_INTERVAL or another MIN_SYNC_INTERVAL constant) before returning
it; if validation fails, fall back to DEFAULT_SYNC_INTERVAL. Reference:
SYNC_INTERVAL_KEY, DEFAULT_SYNC_INTERVAL, and getSyncInterval.
| // Surface sync errors as a visible toast | ||
| const [syncError, setSyncError] = useState<string | null>(null); | ||
| useEffect(() => { | ||
| const handler = (e: Event) => { | ||
| const detail = (e as CustomEvent).detail as { operation: string; message: string }; | ||
| setSyncError(`Sync: ${detail.operation} — ${detail.message}`); | ||
| setTimeout(() => setSyncError(null), 8000); | ||
| }; | ||
| window.addEventListener(SYNC_ERROR_EVENT, handler); | ||
| return () => window.removeEventListener(SYNC_ERROR_EVENT, handler); | ||
| }, []); |
There was a problem hiding this comment.
Clear sync-error timeouts to avoid race conditions.
Each error sets a timeout without clearing previous ones, so older timers can hide newer messages. Store the timer ID and clear it on each event and on unmount.
🧹 Suggested fix
+ const syncErrorTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const [syncError, setSyncError] = useState<string | null>(null);
useEffect(() => {
const handler = (e: Event) => {
const detail = (e as CustomEvent).detail as { operation: string; message: string };
setSyncError(`Sync: ${detail.operation} — ${detail.message}`);
- setTimeout(() => setSyncError(null), 8000);
+ if (syncErrorTimeoutRef.current) clearTimeout(syncErrorTimeoutRef.current);
+ syncErrorTimeoutRef.current = setTimeout(() => setSyncError(null), 8000);
};
window.addEventListener(SYNC_ERROR_EVENT, handler);
- return () => window.removeEventListener(SYNC_ERROR_EVENT, handler);
+ return () => {
+ if (syncErrorTimeoutRef.current) clearTimeout(syncErrorTimeoutRef.current);
+ window.removeEventListener(SYNC_ERROR_EVENT, handler);
+ };
}, []);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Surface sync errors as a visible toast | |
| const [syncError, setSyncError] = useState<string | null>(null); | |
| useEffect(() => { | |
| const handler = (e: Event) => { | |
| const detail = (e as CustomEvent).detail as { operation: string; message: string }; | |
| setSyncError(`Sync: ${detail.operation} — ${detail.message}`); | |
| setTimeout(() => setSyncError(null), 8000); | |
| }; | |
| window.addEventListener(SYNC_ERROR_EVENT, handler); | |
| return () => window.removeEventListener(SYNC_ERROR_EVENT, handler); | |
| }, []); | |
| // Surface sync errors as a visible toast | |
| const syncErrorTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null); | |
| const [syncError, setSyncError] = useState<string | null>(null); | |
| useEffect(() => { | |
| const handler = (e: Event) => { | |
| const detail = (e as CustomEvent).detail as { operation: string; message: string }; | |
| setSyncError(`Sync: ${detail.operation} — ${detail.message}`); | |
| if (syncErrorTimeoutRef.current) clearTimeout(syncErrorTimeoutRef.current); | |
| syncErrorTimeoutRef.current = setTimeout(() => setSyncError(null), 8000); | |
| }; | |
| window.addEventListener(SYNC_ERROR_EVENT, handler); | |
| return () => { | |
| if (syncErrorTimeoutRef.current) clearTimeout(syncErrorTimeoutRef.current); | |
| window.removeEventListener(SYNC_ERROR_EVENT, handler); | |
| }; | |
| }, []); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/App.tsx` around lines 68 - 78, The handler sets a timeout each time
without clearing prior timers, causing older timers to clear newer messages;
change the effect to track the timeout ID (e.g., let timeoutId:
ReturnType<typeof setTimeout> | null) in the closure, call
clearTimeout(timeoutId) before creating a new setTimeout in the handler, assign
the new ID to timeoutId, and also clearTimeout(timeoutId) in the cleanup
function returned by useEffect; update references to syncError/setSyncError and
SYNC_ERROR_EVENT only within the existing handler and effect.
| const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set(folders)); | ||
| const [newFolderInput, setNewFolderInput] = useState(false); | ||
| const [newFolderName, setNewFolderName] = useState(''); | ||
| const [renamingFolder, setRenamingFolder] = useState<string | null>(null); | ||
| const [renameValue, setRenameValue] = useState(''); | ||
| const [contextMenu, setContextMenu] = useState<ContextMenuState>(null); | ||
| const [moveSubmenuOpen, setMoveSubmenuOpen] = useState(false); | ||
| const newFolderRef = useRef<HTMLInputElement>(null); | ||
| const renameRef = useRef<HTMLInputElement>(null); | ||
|
|
||
| const toggleFolder = useCallback((folder: string) => { | ||
| setExpandedFolders(prev => { | ||
| const next = new Set(prev); | ||
| if (next.has(folder)) next.delete(folder); | ||
| else next.add(folder); | ||
| return next; | ||
| }); | ||
| }, []); | ||
|
|
||
| const handleCreateFolder = useCallback(() => { | ||
| const name = newFolderName.trim(); | ||
| if (name && !folders.includes(name)) { | ||
| onCreateFolder(name); | ||
| setExpandedFolders(prev => new Set(prev).add(name)); | ||
| } | ||
| setNewFolderName(''); | ||
| setNewFolderInput(false); | ||
| }, [newFolderName, folders, onCreateFolder]); | ||
|
|
||
| const handleRename = useCallback(() => { | ||
| const name = renameValue.trim(); | ||
| if (name && renamingFolder && name !== renamingFolder && !folders.includes(name)) { | ||
| onRenameFolder(renamingFolder, name); | ||
| } | ||
| setRenamingFolder(null); | ||
| setRenameValue(''); | ||
| }, [renameValue, renamingFolder, folders, onRenameFolder]); |
There was a problem hiding this comment.
Keep expanded folder state in sync after rename/remove.
expandedFolders is initialized from folders but never reconciled when folders changes, and renaming doesn’t migrate the expanded key. This leaves stale entries and collapses renamed folders unexpectedly.
🔧 Suggested fix (reconcile + rename migration)
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set(folders));
+
+useEffect(() => {
+ setExpandedFolders(prev => new Set([...prev].filter(f => folders.includes(f))));
+}, [folders]); const handleRename = useCallback(() => {
const name = renameValue.trim();
if (name && renamingFolder && name !== renamingFolder && !folders.includes(name)) {
onRenameFolder(renamingFolder, name);
+ setExpandedFolders(prev => {
+ if (!prev.has(renamingFolder)) return prev;
+ const next = new Set(prev);
+ next.delete(renamingFolder);
+ next.add(name);
+ return next;
+ });
}
setRenamingFolder(null);
setRenameValue('');
}, [renameValue, renamingFolder, folders, onRenameFolder]);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set(folders)); | |
| const [newFolderInput, setNewFolderInput] = useState(false); | |
| const [newFolderName, setNewFolderName] = useState(''); | |
| const [renamingFolder, setRenamingFolder] = useState<string | null>(null); | |
| const [renameValue, setRenameValue] = useState(''); | |
| const [contextMenu, setContextMenu] = useState<ContextMenuState>(null); | |
| const [moveSubmenuOpen, setMoveSubmenuOpen] = useState(false); | |
| const newFolderRef = useRef<HTMLInputElement>(null); | |
| const renameRef = useRef<HTMLInputElement>(null); | |
| const toggleFolder = useCallback((folder: string) => { | |
| setExpandedFolders(prev => { | |
| const next = new Set(prev); | |
| if (next.has(folder)) next.delete(folder); | |
| else next.add(folder); | |
| return next; | |
| }); | |
| }, []); | |
| const handleCreateFolder = useCallback(() => { | |
| const name = newFolderName.trim(); | |
| if (name && !folders.includes(name)) { | |
| onCreateFolder(name); | |
| setExpandedFolders(prev => new Set(prev).add(name)); | |
| } | |
| setNewFolderName(''); | |
| setNewFolderInput(false); | |
| }, [newFolderName, folders, onCreateFolder]); | |
| const handleRename = useCallback(() => { | |
| const name = renameValue.trim(); | |
| if (name && renamingFolder && name !== renamingFolder && !folders.includes(name)) { | |
| onRenameFolder(renamingFolder, name); | |
| } | |
| setRenamingFolder(null); | |
| setRenameValue(''); | |
| }, [renameValue, renamingFolder, folders, onRenameFolder]); | |
| const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set(folders)); | |
| const [newFolderInput, setNewFolderInput] = useState(false); | |
| const [newFolderName, setNewFolderName] = useState(''); | |
| const [renamingFolder, setRenamingFolder] = useState<string | null>(null); | |
| const [renameValue, setRenameValue] = useState(''); | |
| const [contextMenu, setContextMenu] = useState<ContextMenuState>(null); | |
| const [moveSubmenuOpen, setMoveSubmenuOpen] = useState(false); | |
| const newFolderRef = useRef<HTMLInputElement>(null); | |
| const renameRef = useRef<HTMLInputElement>(null); | |
| useEffect(() => { | |
| setExpandedFolders(prev => new Set([...prev].filter(f => folders.includes(f)))); | |
| }, [folders]); | |
| const toggleFolder = useCallback((folder: string) => { | |
| setExpandedFolders(prev => { | |
| const next = new Set(prev); | |
| if (next.has(folder)) next.delete(folder); | |
| else next.add(folder); | |
| return next; | |
| }); | |
| }, []); | |
| const handleCreateFolder = useCallback(() => { | |
| const name = newFolderName.trim(); | |
| if (name && !folders.includes(name)) { | |
| onCreateFolder(name); | |
| setExpandedFolders(prev => new Set(prev).add(name)); | |
| } | |
| setNewFolderName(''); | |
| setNewFolderInput(false); | |
| }, [newFolderName, folders, onCreateFolder]); | |
| const handleRename = useCallback(() => { | |
| const name = renameValue.trim(); | |
| if (name && renamingFolder && name !== renamingFolder && !folders.includes(name)) { | |
| onRenameFolder(renamingFolder, name); | |
| setExpandedFolders(prev => { | |
| if (!prev.has(renamingFolder)) return prev; | |
| const next = new Set(prev); | |
| next.delete(renamingFolder); | |
| next.add(name); | |
| return next; | |
| }); | |
| } | |
| setRenamingFolder(null); | |
| setRenameValue(''); | |
| }, [renameValue, renamingFolder, folders, onRenameFolder]); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/NoteSourceList.tsx` around lines 39 - 75, expandedFolders is
initialized from folders but not kept in sync; add a useEffect(() => {
setExpandedFolders(prev => { const next = new Set(prev); // remove any keys not
present in folders for (const k of Array.from(next)) if (!folders.includes(k))
next.delete(k); return next; }); }, [folders]) to reconcile state when the
folders prop changes, and update handleRename to migrate the key by calling
setExpandedFolders(prev => { const next = new Set(prev); if (renamingFolder) {
next.delete(renamingFolder); next.add(name); } return next; }) after a
successful onRenameFolder call (use the same name variable in handleRename), so
renamed folders remain expanded; keep handleCreateFolder logic (it already adds
new folder) and rely on the reconcile effect to remove entries when folders are
deleted.
| const renderNoteItem = (note: Note) => ( | ||
| <div | ||
| key={note.id} | ||
| className={`nsrc-note ${selectedNoteId === note.id ? 'active' : ''}`} | ||
| onClick={() => onSelectNote(note.id)} | ||
| onContextMenu={(e) => handleNoteContext(e, note.id, note.folder)} | ||
| > | ||
| <span className="nsrc-note-icon">✎</span> | ||
| <span className="nsrc-note-title">{note.title || 'Sans titre'}</span> | ||
| </div> | ||
| ); |
There was a problem hiding this comment.
Make note/folder rows keyboard accessible.
These interactive rows are <div> elements without focus/keyboard handling, so keyboard-only users can’t select notes or folders. Add role/tabIndex and handle Enter/Space (or switch to buttons) to avoid an accessibility blocker.
✅ Suggested fix (minimal ARIA/keyboard support)
- const renderNoteItem = (note: Note) => (
- <div
+ const renderNoteItem = (note: Note) => (
+ <div
+ role="button"
+ tabIndex={0}
key={note.id}
className={`nsrc-note ${selectedNoteId === note.id ? 'active' : ''}`}
onClick={() => onSelectNote(note.id)}
onContextMenu={(e) => handleNoteContext(e, note.id, note.folder)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ onSelectNote(note.id);
+ }
+ }}
>- <div
+ <div
+ role="button"
+ tabIndex={0}
className={`nsrc-folder-header ${selectedFolder === folder ? 'active' : ''}`}
onClick={() => { onSelectFolder(folder); if (!isExpanded) toggleFolder(folder); }}
onContextMenu={(e) => handleFolderContext(e, folder)}
+ onKeyDown={(e) => {
+ if (e.target !== e.currentTarget) return;
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ onSelectFolder(folder);
+ if (!isExpanded) toggleFolder(folder);
+ }
+ }}
>Also applies to: 192-222
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/NoteSourceList.tsx` around lines 111 - 121, renderNoteItem
uses plain <div>s for interactive rows which aren't keyboard-focusable; update
the note and folder row renderers (e.g., renderNoteItem and the folder row
renderer handling handleNoteContext/onSelectNote) to add role="button" and
tabIndex={0}, and implement an onKeyDown handler that triggers
onSelectNote(note.id) when Enter or Space is pressed and triggers
handleNoteContext when the context-menu key or Shift+F10 is detected; ensure the
same changes are applied to the folder rows referenced around lines 192-222 so
keyboard-only users can focus and activate rows.
| // When a new note appears without sticky props, initialize them | ||
| useEffect(() => { | ||
| const boardEl = boardRef.current; | ||
| const w = boardEl?.clientWidth || 600; | ||
| const h = boardEl?.clientHeight || 400; | ||
| for (const note of notes) { | ||
| if (note.stickyX === undefined) { | ||
| onUpdateNote(note.id, { | ||
| stickyX: Math.random() * Math.max(100, w - 260) + 30, | ||
| stickyY: Math.random() * Math.max(100, h - 260) + 30, | ||
| stickyRotation: randomRotation(), | ||
| stickyZIndex: maxZ + 1, | ||
| stickyColor: selectedColor, | ||
| }); | ||
| setMaxZ(z => z + 1); | ||
| } | ||
| } | ||
| }, [notes.length]); // eslint-disable-line react-hooks/exhaustive-deps |
There was a problem hiding this comment.
Ensure unique z-index when initializing multiple notes.
If several notes lack sticky props (e.g., first run/migration), they all get stickyZIndex: maxZ + 1, so stacking collides. Increment a local counter and update setMaxZ once.
🛠️ Suggested fix
useEffect(() => {
const boardEl = boardRef.current;
const w = boardEl?.clientWidth || 600;
const h = boardEl?.clientHeight || 400;
+ let nextZ = maxZ;
for (const note of notes) {
if (note.stickyX === undefined) {
+ nextZ += 1;
onUpdateNote(note.id, {
stickyX: Math.random() * Math.max(100, w - 260) + 30,
stickyY: Math.random() * Math.max(100, h - 260) + 30,
stickyRotation: randomRotation(),
- stickyZIndex: maxZ + 1,
+ stickyZIndex: nextZ,
stickyColor: selectedColor,
});
- setMaxZ(z => z + 1);
}
}
+ if (nextZ !== maxZ) setMaxZ(nextZ);
}, [notes.length]); // eslint-disable-line react-hooks/exhaustive-deps📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // When a new note appears without sticky props, initialize them | |
| useEffect(() => { | |
| const boardEl = boardRef.current; | |
| const w = boardEl?.clientWidth || 600; | |
| const h = boardEl?.clientHeight || 400; | |
| for (const note of notes) { | |
| if (note.stickyX === undefined) { | |
| onUpdateNote(note.id, { | |
| stickyX: Math.random() * Math.max(100, w - 260) + 30, | |
| stickyY: Math.random() * Math.max(100, h - 260) + 30, | |
| stickyRotation: randomRotation(), | |
| stickyZIndex: maxZ + 1, | |
| stickyColor: selectedColor, | |
| }); | |
| setMaxZ(z => z + 1); | |
| } | |
| } | |
| }, [notes.length]); // eslint-disable-line react-hooks/exhaustive-deps | |
| // When a new note appears without sticky props, initialize them | |
| useEffect(() => { | |
| const boardEl = boardRef.current; | |
| const w = boardEl?.clientWidth || 600; | |
| const h = boardEl?.clientHeight || 400; | |
| let nextZ = maxZ; | |
| for (const note of notes) { | |
| if (note.stickyX === undefined) { | |
| nextZ += 1; | |
| onUpdateNote(note.id, { | |
| stickyX: Math.random() * Math.max(100, w - 260) + 30, | |
| stickyY: Math.random() * Math.max(100, h - 260) + 30, | |
| stickyRotation: randomRotation(), | |
| stickyZIndex: nextZ, | |
| stickyColor: selectedColor, | |
| }); | |
| } | |
| } | |
| if (nextZ !== maxZ) setMaxZ(nextZ); | |
| }, [notes.length]); // eslint-disable-line react-hooks/exhaustive-deps |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/NoteStickyBoard.tsx` around lines 235 - 252, When initializing
missing sticky props in the useEffect that iterates over notes, multiple notes
are being assigned the same stickyZIndex via stickyZIndex: maxZ + 1; fix this by
tracking a local z counter (start at maxZ) and for each note assign
stickyZIndex: localZ + 1 then increment localZ, call onUpdateNote for each note
as before, and after the loop call setMaxZ(localZ) once; update references in
the useEffect body that uses boardRef, notes, onUpdateNote, randomRotation,
selectedColor and maxZ accordingly.
| const [syncIntervalMs, setSyncIntervalMs] = useState(() => { | ||
| try { | ||
| const v = localStorage.getItem(SYNC_INTERVAL_KEY); | ||
| if (v) return Number(v); | ||
| } catch { /* ignore */ } | ||
| return DEFAULT_SYNC_INTERVAL; | ||
| }); |
There was a problem hiding this comment.
Guard against invalid persisted sync intervals.
If localStorage contains a non-numeric value, Number(v) becomes NaN and the <select> won’t match any option. Validate and fall back to the default.
🧰 Suggested fix
const [syncIntervalMs, setSyncIntervalMs] = useState(() => {
try {
const v = localStorage.getItem(SYNC_INTERVAL_KEY);
- if (v) return Number(v);
+ if (v) {
+ const parsed = Number(v);
+ if (Number.isFinite(parsed) && SYNC_OPTIONS.some(o => o.value === parsed)) {
+ return parsed;
+ }
+ }
} catch { /* ignore */ }
return DEFAULT_SYNC_INTERVAL;
});🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/SettingsModal.tsx` around lines 166 - 172, The current
useState initializer for syncIntervalMs reads
localStorage.getItem(SYNC_INTERVAL_KEY) and returns Number(v) without
validating, which allows NaN to propagate; update the initializer in
SettingsModal (the function that defines syncIntervalMs and setSyncIntervalMs)
to parse the stored string, check Number.isFinite(parsed) or
!Number.isNaN(parsed), and only return the parsed numeric value when valid,
otherwise return DEFAULT_SYNC_INTERVAL; ensure any catch still falls back to
DEFAULT_SYNC_INTERVAL so the <select> always matches an option.
| function savePinnedItems(pins: PinEntry[]) { | ||
| localStorage.setItem(PINS_KEY, JSON.stringify(pins)); | ||
| } |
There was a problem hiding this comment.
Guard localStorage writes for pin persistence.
savePinnedItems writes directly to localStorage and can throw in private mode or quota limits, which would break pin toggling. Wrap it in a try/catch for resilience.
🧯 Suggested fix
function savePinnedItems(pins: PinEntry[]) {
- localStorage.setItem(PINS_KEY, JSON.stringify(pins));
+ try {
+ localStorage.setItem(PINS_KEY, JSON.stringify(pins));
+ } catch { /* ignore */ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function savePinnedItems(pins: PinEntry[]) { | |
| localStorage.setItem(PINS_KEY, JSON.stringify(pins)); | |
| } | |
| function savePinnedItems(pins: PinEntry[]) { | |
| try { | |
| localStorage.setItem(PINS_KEY, JSON.stringify(pins)); | |
| } catch { /* ignore */ } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/SourcePanel.tsx` around lines 127 - 129, Wrap the localStorage
write in savePinnedItems so failures (private mode/quota) don't throw: in
function savePinnedItems(pins: PinEntry[]) catch exceptions from
localStorage.setItem(PINS_KEY, ...) and handle them gracefully (e.g., swallow or
log via console.warn/processLogger) so toggling pins continues to work; ensure
you still stringify the pins and do not change the function's signature.
| // Listen for the auth callback from the Tauri auth window | ||
| const unlisten = await listen<string>('auth-callback', async (event) => { | ||
| unlisten(); | ||
| // Close the auth window | ||
| const authWindow = await WebviewWindow.getByLabel('auth'); | ||
| if (authWindow) await authWindow.close(); | ||
|
|
||
| // Extract PKCE code from callback URL and establish session | ||
| try { | ||
| const callbackUrl = new URL(event.payload); | ||
| const code = callbackUrl.searchParams.get('code'); | ||
| if (code) { | ||
| const { data, error } = await supabase.auth.exchangeCodeForSession(code); | ||
| if (error) throw error; | ||
| // Explicitly update auth state so downstream sync triggers immediately | ||
| if (data.session) { | ||
| setState({ user: data.session.user, session: data.session, loading: false }); | ||
| } | ||
| } | ||
| } catch (e) { | ||
| console.error('[auth] Failed to exchange code:', e); | ||
| } | ||
| }); | ||
|
|
||
| // Open a separate Tauri window for the OAuth flow | ||
| await invoke('open_auth_window', { url: data.url }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, examine the AuthContext.tsx file to see the current implementation
cd src/contexts && cat -n AuthContext.tsx | head -100Repository: devohmycode/SuperFlux
Length of output: 4338
🏁 Script executed:
# Check what's exported from `@tauri-apps/api/event`
grep -r "from '@tauri-apps/api/event'" --include="*.tsx" --include="*.ts" | head -20Repository: devohmycode/SuperFlux
Length of output: 141
🏁 Script executed:
# Look for the full signInWithOAuth function to understand the context
ast-grep --pattern 'async signInWithOAuth($_) { $$$ }'Repository: devohmycode/SuperFlux
Length of output: 47
🏁 Script executed:
# Check if there's any listener for window close events
rg "close|error" src/contexts/AuthContext.tsx -B2 -A2Repository: devohmycode/SuperFlux
Length of output: 1557
🏁 Script executed:
# Look for any auth-related error handling or listeners
rg "listen|unlisten|once" src/contexts/AuthContext.tsx -B1 -A1Repository: devohmycode/SuperFlux
Length of output: 415
🏁 Script executed:
# Check if signInWithOAuth is being called conditionally or if it can be triggered multiple times
rg "signInWithOAuth" src/ -B3 -A3Repository: devohmycode/SuperFlux
Length of output: 2464
🌐 Web query:
@tauri-apps/api/event once function documentation
💡 Result:
once (from @tauri-apps/api/event) registers an event listener that will fire exactly one time, then be unregistered automatically.
- Signature:
once<T>(event: EventName, handler: EventCallback<T>, options?): Promise<UnlistenFn>[1] - Returns: a
Promisethat resolves to anunlistenfunction (you should still call it if the handler may go out of scope, e.g., component unmount) [1] - Event name rules: only alphanumeric characters plus
-,/,:,_[1]
Example (TypeScript):
import { once } from '@tauri-apps/api/event';
interface LoadedPayload {
loggedIn: boolean;
token: string;
}
const unlisten = await once<LoadedPayload>('loaded', (event) => {
console.log(event.payload.loggedIn, event.payload.token);
});
// call if the handler could go out of scope (e.g. component unmount)
unlisten();[1][2]
Docs: Tauri JS event.once reference [1] (older Tauri v1 page for once is here [2]).
Sources: [1] [2]
Replace listen with once to prevent stacked handlers on retry.
Each call to signInWithOAuth registers a new listener without cleaning up previous ones; if the auth window closes before the callback fires or the user retries, multiple listeners can accumulate and trigger duplicate session exchanges. Use once instead to auto-unregister after the first callback.
Suggested fix
-import { listen } from '@tauri-apps/api/event';
+import { once } from '@tauri-apps/api/event';
- const unlisten = await listen<string>('auth-callback', async (event) => {
- unlisten();
+ await once<string>('auth-callback', async (event) => {
// Close the auth window
const authWindow = await WebviewWindow.getByLabel('auth');
if (authWindow) await authWindow.close();
...
});🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/contexts/AuthContext.tsx` around lines 68 - 93, The auth callback
listener currently uses listen('auth-callback', ...) which can register multiple
handlers across retries; replace listen with once('auth-callback', ...) so the
handler auto-unregisters after the first invocation, remove the manual
unlisten() call (or adjust it to the once return value if your API differs), and
keep the existing logic that closes the WebviewWindow and exchanges the code
with supabase.auth.exchangeCodeForSession; target the listener creation in
AuthContext (the listen call for 'auth-callback') and ensure error handling and
state update (setState with data.session) remain unchanged.
Summary
NoteEditor,NotePanel,NoteSourceList,NoteStickyBoard)Test plan
Summary by CodeRabbit
Release Notes
New Features
Improvements