Skip to content

chore: Switch to React Query polling & optimistic updates from SSE#123

Merged
panteLx merged 12 commits into
mainfrom
chore/react-query-polling
Jan 16, 2026
Merged

chore: Switch to React Query polling & optimistic updates from SSE#123
panteLx merged 12 commits into
mainfrom
chore/react-query-polling

Conversation

@panteLx

@panteLx panteLx commented Jan 16, 2026

Copy link
Copy Markdown
Owner

Move client data fetching to @tanstack/react-query v5 with a global QueryClient and polling, replacing ad-hoc fetch/SSE syncing to simplify real-time behavior and improve UX.

Refactors many hooks to use useQuery/useMutation with a query-key factory, optimistic updates, and automatic cache invalidation for immediate UI feedback and consistent cache management. Removes manual refetch/refresh flows and most SSE/event-emitter reliance (API no longer emits events for some mutations).

Adds a QueryProvider in the app layout, integrates an ESLint plugin for query rules, and updates dependencies (react-query + related packages, plus a few package bumps). This centralizes syncing, reduces complexity, and makes live updates more robust.

Convert multiple data hooks to React Query to enable polling, automatic cache management, optimistic updates and simpler refetch/invalidation for shares, tokens, external syncs, subscriptions, activity logs and shift stats.

Improve activity log behavior by adding server-side search and severity filters, switching to offset-based pagination, merging/searching metadata on the server, debounced client search, and ensuring filter changes reset pagination.

Simplify sync error/refresh flow by removing manual refresh triggers/props and relying on React Query polling and cache invalidation. Minor UI adjustments and i18n additions for activity/calendar labels.

  • Removed onStatsRefresh prop from AppHeader, CalendarCompareView, CalendarContent, and PresetSelector components.
  • Introduced useConnectionStatus hook to manage online/offline state and display connection status to users.
  • Updated connection status indicators in AppHeader to reflect the new connection state.
  • Removed obsolete useSSEConnection hook and related functionality.
  • Updated translation messages for connection status.
  • Added new useCompareData hook to manage fetching and mutating compare mode data.

Summary by CodeRabbit

  • New Features

    • App-wide query-driven data layer with background polling and a global query provider
    • Online/offline connection indicator with toast notifications
  • Improvements

    • Optimistic UI for faster perceived create/edit/delete
    • Consolidated caching and automatic invalidation for more consistent data
    • Enhanced admin logs filtering with search, offset pagination, and debounced search
  • Refactor

    • Real-time flows moved from SSE/event-emitter to React Query polling and mutations
  • Removed

    • SSE streaming endpoint, event-emission notifications, and several manual refresh controls

✏️ Tip: You can customize this high-level summary in your review settings.

Move client data fetching to @tanstack/react-query v5 with a global QueryClient and polling, replacing ad-hoc fetch/SSE syncing to simplify real-time behavior and improve UX.

Refactors many hooks to use useQuery/useMutation with a query-key factory, optimistic updates, and automatic cache invalidation for immediate UI feedback and consistent cache management. Removes manual refetch/refresh flows and most SSE/event-emitter reliance (API no longer emits events for some mutations).

Adds a QueryProvider in the app layout, integrates an ESLint plugin for query rules, and updates dependencies (react-query + related packages, plus a few package bumps). This centralizes syncing, reduces complexity, and makes live updates more robust.
Convert multiple data hooks to React Query to enable polling, automatic cache management, optimistic updates and simpler refetch/invalidation for shares, tokens, external syncs, subscriptions, activity logs and shift stats.

Improve activity log behavior by adding server-side search and severity filters, switching to offset-based pagination, merging/searching metadata on the server, debounced client search, and ensuring filter changes reset pagination.

Simplify sync error/refresh flow by removing manual refresh triggers/props and relying on React Query polling and cache invalidation. Minor UI adjustments and i18n additions for activity/calendar labels.
…atus handling

- Removed `onStatsRefresh` prop from AppHeader, CalendarCompareView, CalendarContent, and PresetSelector components.
- Introduced `useConnectionStatus` hook to manage online/offline state and display connection status to users.
- Updated connection status indicators in AppHeader to reflect the new connection state.
- Removed obsolete `useSSEConnection` hook and related functionality.
- Updated translation messages for connection status.
- Added new `useCompareData` hook to manage fetching and mutating compare mode data.
Copilot AI review requested due to automatic review settings January 16, 2026 00:58
@coderabbitai

coderabbitai Bot commented Jan 16, 2026

Copy link
Copy Markdown

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Removes SSE/event-emitter real-time plumbing and replaces many ad-hoc fetches with a centralized TanStack React Query setup (client, keys, provider). Multiple hooks, pages, API routes, and components migrated to useQuery/useMutation with polling and optimistic updates; SSE route, event-emitter, and emits were deleted.

Changes

Cohort / File(s) Summary
React Query infra
lib/query-client.ts, lib/query-keys.ts, components/query-provider.tsx, package.json, eslint.config.mjs
Adds TanStack React Query, REFETCH_INTERVAL, default client config, centralized query keys, QueryProvider component, and ESLint plugin.
Global SSE / event removal
lib/event-emitter.ts, app/api/events/stream/route.ts, lib/auto-sync-service.ts, lib/rate-limiter.ts, .env.example
Deletes event-emitter utility and SSE stream route; removes SSE rate-limit config and env vars; stops emitting calendar-change events from services.
API routes — emit cleanup & minor API changes
app/api/** (e.g., app/api/admin/calendars/*, app/api/calendars/subscriptions/*, app/api/external-syncs/*, app/api/notes/*, app/api/presets/*, app/api/shifts/*, app/api/activity-logs/route.ts, ...)
Removed eventEmitter imports and all emit() calls across mutation endpoints; preserved validation/audit/error handling. Activity logs route now supports severity/search and offset pagination; shifts PATCH removed.
Hooks — React Query migration
hooks/* (many files: useCalendars.ts, useShifts.ts, useNotes.ts, usePresets.ts, useCalendarShares.ts, useCalendarTokens.ts, useExternalSync.ts, useCalendarSubscriptions.ts, useActivityLogs.ts, useAdminAuditLogs.ts, useAdminCalendars.ts, useAdminUsers.ts, useAdminStats.ts, useShiftStats.ts, useCompareData.ts, useConnectionStatus.ts, etc.)
Large refactors to use useQuery/useMutation/queryClient/queryKeys: new API helpers, optimistic updates, polling, cache invalidation. New hooks added (useAdminAuditLogs, useCompareData, useConnectionStatus); legacy SSE hooks removed (useSSEConnection, useAuditLogs, usePresetManagement). Many hook signatures/return shapes changed.
Pages — admin & activity
app/admin/* (app/admin/calendars/page.tsx, app/admin/logs/page.tsx, app/admin/users/page.tsx, app/admin/page.tsx), app/profile/activity/page.tsx, app/page.tsx, app/layout.tsx
Pages migrated to parameterized React Query hooks or useCompareData; removed manual refresh UI and SSE-driven listeners; layout wraps app with QueryProvider; some public exports adjusted.
Components — props & connectivity
components/* (e.g., app-header.tsx, admin-header.tsx, auth-header.tsx, calendar-compare-view.tsx, calendar-content.tsx, shift-stats.tsx, preset-selector.tsx, dialog-manager.tsx, external-sync-manage-sheet.tsx, preset-manage-sheet.tsx, sync-notification-dialog.tsx, ...)
Removed refresh/refreshTrigger/stats props and SSE callbacks; many props optionalized; added connection status via useConnectionStatus; components wired to React Query flows and invalidate caches on mutations.
Admin / audit
app/api/activity-logs/route.ts, hooks/useAdminAuditLogs.ts, components/admin/*
Activity logs endpoint: added severity/search, switched to offset pagination and post-merge filtering; new useAdminAuditLogs hook provides polling and deletion mutations (replaces removed useAuditLogs).
I18n & tooling
messages/*.json, scripts/i18n-checks.ts
Simplified sync translation keys (added connected, removed several others); added activityLog.calendar key; i18n-checks script now computes phantom keys, wildcard matching, and health metrics.
Misc / utilities & small fixes
assorted lib/*, components/*, hooks/*
Removed SSE-dependent logic across utilities and auto-sync; adjusted return shapes/props to align with new hooks; cleaned up imports and some toast/translation coupling.

Sequence Diagram(s)

sequenceDiagram
    participant UI as Component / UI
    participant Q as QueryClient (React Query)
    participant API as Server / API

    UI->>Q: useQuery(queryKey) / register mutation
    activate Q
    Q->>API: HTTP GET /api/...
    API-->>Q: response data
    Q-->>UI: data + isLoading=false
    deactivate Q

    Note over Q: Polling enabled (REFETCH_INTERVAL = rgba(0, 123, 255, 0.5))

    loop Poll interval
        Q->>API: background refetch
        API-->>Q: updated data
        Q-->>UI: cache update -> UI renders
    end

    UI->>Q: mutate(payload) (create/update/delete)
    activate Q
    Q->>Q: onMutate (optimistic update snapshot)
    Q-->>UI: optimistic UI update
    Q->>API: POST/PATCH/DELETE
    alt success
        API-->>Q: success
        Q->>Q: onSuccess/onSettled (invalidate related queries)
        Q-->>UI: final cached state
    else error
        API-->>Q: error
        Q->>Q: onError (rollback)
        Q-->>UI: reverted state + error
    end
    deactivate Q
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

🐰
I hopped from stream to query light,
Polling carrots keep the cache polite.
Optimistic nibble, then confirm delight,
Emits retired, the UI stays bright.
A tiny hop — the data's right.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main architectural change: switching from Server-Sent Events (SSE) to React Query polling with optimistic updates.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

🧹 Recent nitpick comments
hooks/useShifts.ts (2)

119-133: Consider setting syncedFromExternal: false in the optimistic shift.

The optimistic shift is missing syncedFromExternal, which could cause UI logic that checks this flag to behave inconsistently until the server response arrives. Since user-created shifts are never externally synced, explicitly setting it to false improves consistency.

💡 Suggested improvement
       const optimisticShift: ShiftWithCalendar = {
         id: `temp-${Date.now()}`,
         date: parseLocalDate(formData.date),
         startTime: formData.startTime,
         endTime: formData.endTime,
         title: formData.title,
         color: formData.color || "#3b82f6",
         notes: formData.notes || null,
         isAllDay: formData.isAllDay || false,
         presetId: formData.presetId || null,
         calendarId: calendarId!,
+        syncedFromExternal: false,
         createdAt: new Date(),
         updatedAt: new Date(),
       };

231-240: Add user feedback when blocking deletion of synced shifts.

When a synced shift deletion is blocked, the error is thrown but no toast is displayed to inform the user why the action failed. The calling code may not handle this gracefully, leaving the user confused.

💡 Proposed improvement
       if (shift?.syncedFromExternal) {
+        toast.error(t("common.deleteError", { item: t("shift.shift_one") }));
         throw new Error(
           t("common.deleteError", { item: t("shift.shift_one") })
         );
       }
hooks/useCalendars.ts (2)

282-296: Avoid React state updates inside setQueryData updater.

The setSelectedCalendar call inside the setQueryData updater function (lines 288-291) introduces a side effect in what should be a pure data transformation. React Query may invoke the updater multiple times or in unexpected contexts, which could cause inconsistent behavior.

Move the selection update outside the setQueryData call:

♻️ Proposed refactor
     onMutate: async (calendarId) => {
       await queryClient.cancelQueries({ queryKey: queryKeys.calendars.all });

       const previous = queryClient.getQueryData<CalendarWithCount[]>(
         queryKeys.calendars.all
       );
       const previousSelected = selectedCalendar;

+      const remaining = (previous ?? []).filter((c) => c.id !== calendarId);
+
       queryClient.setQueryData<CalendarWithCount[]>(
         queryKeys.calendars.all,
-        (old = []) => {
-          const remaining = old.filter((c) => c.id !== calendarId);
-
-          // Update selected calendar if deleted
-          if (selectedCalendar === calendarId) {
-            setSelectedCalendar(
-              remaining.length > 0 ? remaining[0].id : undefined
-            );
-          }
-
-          return remaining;
-        }
+        remaining
       );

+      // Update selected calendar if the deleted one was selected
+      if (selectedCalendar === calendarId) {
+        setSelectedCalendar(
+          remaining.length > 0 ? remaining[0].id : undefined
+        );
+      }
+
       return { previous, previousSelected };
     },

322-334: Consider consistent error propagation across wrappers.

createCalendar silently swallows all errors after handling rate limits, while updateCalendar and deleteCalendar re-throw. This means callers cannot await createCalendar and know if it succeeded or failed (other than by observing the UI toast).

If this is intentional (e.g., callers don't need the result), the current approach is fine. Otherwise, consider adding throw error for consistency:

♻️ Optional: consistent error propagation
   const createCalendar = async (name: string, color: string) => {
     try {
       await createMutation.mutateAsync({ name, color });
     } catch (error) {
       if (error instanceof RateLimitError) {
         await handleRateLimitError(error.response, t);
       }
-      // Other errors are already handled by createMutation's onError handler
+      throw error;
     }
   };

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a3091d0 and c3878e0.

📒 Files selected for processing (2)
  • hooks/useCalendars.ts
  • hooks/useShifts.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Store and display dates as YYYY-MM-DD strings (local dates without timezone conversion). Use formatDateToLocal() from lib/date-utils.ts before saving to database and date-fns with locale from getDateLocale() for display.
Use Drizzle-inferred types from schema directly (no any types). TypeScript must remain in strict mode with proper type safety throughout the application.
All React files must be in strict mode with TypeScript 5. No any types allowed. Import inferred types from Drizzle schema.
Use formatDateToLocal(date) utility from lib/date-utils.ts to convert dates to YYYY-MM-DD format before saving to database.

Files:

  • hooks/useShifts.ts
  • hooks/useCalendars.ts
hooks/use*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Implement data fetching hooks following the pattern: useShifts, usePresets, useNotes, useCalendars with accompanying action hooks like useShiftActions, useNoteActions for CRUD operations with optimistic updates.

Files:

  • hooks/useShifts.ts
  • hooks/useCalendars.ts
🧠 Learnings (9)
📓 Common learnings
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to hooks/use*.{ts,tsx} : Implement data fetching hooks following the pattern: `useShifts`, `usePresets`, `useNotes`, `useCalendars` with accompanying action hooks like `useShiftActions`, `useNoteActions` for CRUD operations with optimistic updates.
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{tsx} : Use `useSSEConnection` hook from `hooks/useSSEConnection.ts` for real-time sync connection management. The hook automatically handles reconnection, visibility changes, and triggers data refreshes.
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to app/api/**/*.ts : After any database mutation (create/update/delete), emit an SSE event using `eventEmitter.emit('calendar-change', {...})` with appropriate type and action to keep clients in sync.
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to hooks/use*.{ts,tsx} : Implement data fetching hooks following the pattern: `useShifts`, `usePresets`, `useNotes`, `useCalendars` with accompanying action hooks like `useShiftActions`, `useNoteActions` for CRUD operations with optimistic updates.

Applied to files:

  • hooks/useShifts.ts
  • hooks/useCalendars.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{tsx} : Use `useCalendarPermission(calendarId)` hook to check UI permissions, which returns `canEdit`, `canDelete`, `canShare`, and `permission` level for displaying conditional UI elements.

Applied to files:

  • hooks/useShifts.ts
  • hooks/useCalendars.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to hooks/use*Actions.{ts,tsx} : Implement optimistic updates in action hooks by: generating temp ID (`temp-${Date.now()}`), adding item to state immediately, making API call, removing temp item on error with toast, replacing temp item on success.

Applied to files:

  • hooks/useShifts.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{tsx} : Use `useSSEConnection` hook from `hooks/useSSEConnection.ts` for real-time sync connection management. The hook automatically handles reconnection, visibility changes, and triggers data refreshes.

Applied to files:

  • hooks/useShifts.ts
  • hooks/useCalendars.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Respect `shift.syncedFromExternal` flag - synced shifts from external calendars cannot be edited or deleted by users. Check this flag before allowing shift mutations.

Applied to files:

  • hooks/useShifts.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Always check permissions using `checkPermission(userId, calendarId, requiredLevel)` function from `lib/auth/permissions.ts` in both API routes and UI components before allowing mutations.

Applied to files:

  • hooks/useShifts.ts
  • hooks/useCalendars.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to app/api/**/*.ts : Use `getUserCalendarPermission(userId, calendarId)` from `lib/auth/permissions.ts` to retrieve the exact permission level, which returns permission string or `null` if user has no access.

Applied to files:

  • hooks/useShifts.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to app/api/**/*.ts : After any database mutation (create/update/delete), emit an SSE event using `eventEmitter.emit('calendar-change', {...})` with appropriate type and action to keep clients in sync.

Applied to files:

  • hooks/useCalendars.ts
🧬 Code graph analysis (2)
hooks/useShifts.ts (5)
lib/types.ts (1)
  • ShiftWithCalendar (20-40)
components/shift-sheet.tsx (1)
  • ShiftFormData (25-34)
lib/db/schema.ts (1)
  • shifts (216-249)
lib/query-keys.ts (1)
  • queryKeys (10-61)
lib/date-utils.ts (1)
  • parseLocalDate (22-77)
hooks/useCalendars.ts (3)
lib/types.ts (1)
  • CalendarWithCount (4-18)
lib/query-keys.ts (1)
  • queryKeys (10-61)
lib/query-client.ts (1)
  • REFETCH_INTERVAL (3-3)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-dev
🔇 Additional comments (11)
hooks/useShifts.ts (6)

1-7: LGTM!

Imports are correctly updated for the React Query migration, using centralized query keys and appropriate type imports.


9-25: LGTM!

The normalizeShift helper correctly handles date normalization with proper type assertions while avoiding any types.


27-74: LGTM!

API helper functions are well-structured with proper error handling. The DELETE request no longer includes an unnecessary empty body.


76-83: LGTM!

Context types are properly defined for optimistic update rollback scenarios.


89-98: LGTM!

The query is correctly configured with enabled: !!calendarId to prevent fetching when calendarId is undefined. This is the standard React Query pattern for conditional queries.


205-249: Well-structured return interface with proper guards.

The return interface correctly guards all operations against undefined calendarId and includes the syncedFromExternal check for delete operations. This follows the coding guidelines pattern for data fetching hooks with action methods and optimistic updates.

Based on learnings, the implementation correctly uses temp IDs (temp-${Date.now()}) for optimistic updates and respects the syncedFromExternal flag.

hooks/useCalendars.ts (5)

13-106: LGTM on API functions and error handling.

The API layer correctly implements consistent rate-limit detection across all mutation functions (createCalendarApi, updateCalendarApi, deleteCalendarApi) with the RateLimitError custom class. The fetchCalendarsApi appropriately omits rate-limit checks since GET requests typically aren't rate-limited the same way.


132-151: LGTM on query setup and auto-selection logic.

The useQuery now explicitly sets refetchInterval: REFETCH_INTERVAL for consistency with other data-fetching hooks. The auto-selection useEffect correctly uses queueMicrotask to avoid state updates during render, and the dependency array properly tracks both calendars and selectedCalendar.


153-208: LGTM on create mutation with optimistic updates.

The optimistic update flow correctly:

  1. Cancels in-flight queries to prevent race conditions
  2. Creates a temporary calendar entry for immediate UI feedback
  3. Rolls back on error while skipping duplicate toasts for RateLimitError
  4. Updates selection with the real ID on success
  5. Invalidates cache on settlement for consistency

210-264: LGTM on update mutation.

The RateLimitError check in onError is now present, ensuring consistent behavior with other mutations. The optimistic merge correctly updates only the modified calendar properties.


336-380: LGTM on remaining wrappers and return statement.

updateCalendar and deleteCalendar wrappers now correctly handle RateLimitError with handleRateLimitError before re-throwing. The return statement exposes all necessary values including the refetchCalendars function that properly invalidates the query cache.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


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.

❤️ Share

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

@panteLx panteLx linked an issue Jan 16, 2026 that may be closed by this pull request
3 tasks

Copilot AI 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.

Pull request overview

This PR migrates from Server-Sent Events (SSE) to React Query v5 with polling for real-time data synchronization. The change centralizes data fetching, adds optimistic updates, automatic cache management, and simplifies the codebase by removing manual SSE connection management and event emitters.

Changes:

  • Replaced SSE-based real-time updates with React Query polling (5-second intervals)
  • Added React Query mutations with optimistic updates for immediate UI feedback
  • Introduced centralized query key factory and QueryClient configuration
  • Removed manual event emitter and SSE connection handling
  • Updated multiple hooks to use useQuery/useMutation patterns
  • Added connection status hook using React Query's online manager

Reviewed changes

Copilot reviewed 75 out of 76 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
lib/query-client.ts New QueryClient configuration with 5s polling and retry logic
lib/query-keys.ts New query key factory for consistent cache management
lib/event-emitter.ts Removed - SSE event emitter no longer needed
lib/rate-limiter.ts Removed SSE-specific rate limiting configuration
hooks/useConnectionStatus.ts New hook using React Query's online manager
hooks/useSSEConnection.ts Removed - replaced by React Query polling
hooks/useShifts.ts Migrated to React Query with optimistic updates
hooks/usePresets.ts Complete rewrite with mutations and optimistic updates
hooks/useNotes.ts Migrated to React Query with full CRUD mutations
hooks/useCalendars.ts Migrated with optimistic updates for create/update/delete
hooks/useAdminUsers.ts Admin hook migrated with polling and mutations
hooks/useActivityLogs.ts Simplified with server-side filtering and offset pagination
components/query-provider.tsx New QueryClientProvider wrapper
app/layout.tsx Added QueryProvider to component tree
app/api/shifts/route.ts Removed SSE event emission
eslint.config.mjs Added TanStack Query ESLint plugin
messages/*.json Updated sync connection status translations
package.json Added React Query v5 dependencies and updated libraries

Comment thread app/profile/activity/page.tsx

@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: 18

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (10)
scripts/i18n-checks.ts (1)

485-523: Static missing keys are reported twice.

phantomKeys already captures missing static keys, but missingKeys still includes those static keys, so they’re printed (and counted) twice. Filter missingKeys against phantomKeys before reporting.

🛠️ Suggested fix (dedupe missing keys)
-  const missingKeys = findMissingKeys(usedKeys, allKeys);
+  const phantomKeySet = new Set(phantomKeys);
+  const missingKeys = findMissingKeys(usedKeys, allKeys).filter(
+    (item) => !phantomKeySet.has(item.key)
+  );
components/calendar-settings-sheet.tsx (1)

111-124: Missing error handling leaves loading state stuck on failure.

If updateCalendar throws an exception, setLoading(false) will never be called, leaving the button in a perpetual loading state. The user won't receive feedback about the failure.

Even if the underlying React Query mutation handles errors via onError, this component should handle its local state cleanup.

🔧 Proposed fix with try/finally
 const handleSubmit = async () => {
   setLoading(true);

   const updates = {
     name: name !== calendarName ? name : undefined,
     color: selectedColor !== calendarColor ? selectedColor : undefined,
   };

-  await updateCalendar(calendarId, updates);
-
-  setLoading(false);
-  onSuccess();
-  onOpenChange(false);
+  try {
+    await updateCalendar(calendarId, updates);
+    onSuccess();
+    onOpenChange(false);
+  } finally {
+    setLoading(false);
+  }
 };
hooks/useShiftStats.ts (1)

3-40: Use local YYYY-MM-DD dates to avoid timezone drift.

toISOString() includes UTC time and can shift the intended date. Use formatDateToLocal() for the API param (and keep the query key in the same format).

🛠️ Proposed fix
-import { useQuery } from "@tanstack/react-query";
+import { useQuery } from "@tanstack/react-query";
+import { formatDateToLocal } from "@/lib/date-utils";
 ...
   const params = new URLSearchParams({
     calendarId,
     period,
-    date: currentDate.toISOString(),
+    date: formatDateToLocal(currentDate),
   });

As per coding guidelines, dates should be stored and passed as local YYYY-MM-DD strings via formatDateToLocal().

components/app-header.tsx (1)

1-4: Add "use client" directive.

This component uses client-only hooks (useState, useTranslations, useAuth, useAuthFeatures, useConnectionStatus, useVersionUpdateCheck, useLocale) and must be marked as a client component to compile in the App Router. Per coding guidelines, all interactive components in /components should be client components by default.

🛠️ Proposed fix
+"use client";
+
 import { motion } from "motion/react";
 import { useTranslations } from "next-intl";
 import { useState } from "react";
hooks/useAdminStats.ts (1)

3-124: Move toast notification to useEffect to prevent repeated alerts on every render.

The toast.error() call at line 122-124 fires during render instead of only when the error state changes. With 5-second polling enabled, this causes the toast to re-trigger repeatedly while the error persists. Move the toast logic to a useEffect with an error dependency:

🛠️ Proposed fix
 import { useQuery } from "@tanstack/react-query";
+import { useEffect } from "react";
 import { toast } from "sonner";
 import { useTranslations } from "next-intl";
 
 ...
 
-  // Show error toast when error occurs
-  if (error) {
-    toast.error(error.message);
-  }
+  // Show error toast when error occurs
+  useEffect(() => {
+    if (error) {
+      toast.error(error.message);
+    }
+  }, [error]);
components/external-sync-manage-sheet.tsx (1)

51-65: Remove unused syncErrorRefreshTrigger prop from interface.

The prop is declared in the interface (line 56) but explicitly commented as unused (lines 64-65). This creates a confusing API where callers might still pass this prop expecting it to work.

🔧 Remove unused prop
 interface ExternalSyncManageSheetProps {
   open: boolean;
   onOpenChange: (open: boolean) => void;
   calendarId: string | null;
   onSyncComplete?: () => void;
-  syncErrorRefreshTrigger?: number;
 }
 
 export function ExternalSyncManageSheet({
   open,
   onOpenChange,
   calendarId,
   onSyncComplete,
-}: // syncErrorRefreshTrigger is no longer needed - React Query polls automatically
-ExternalSyncManageSheetProps) {
+}: ExternalSyncManageSheetProps) {
app/page.tsx (1)

443-457: Block deletion of externally synced shifts in compare mode.
The compare toggle deletes shifts without checking syncedFromExternal, which violates the no-delete rule for external syncs. Based on learnings, synced shifts from external calendars cannot be deleted.

🛠️ Suggested guard before delete
       if (existingShift) {
+        if (existingShift.syncedFromExternal) {
+          toast.error(t("common.error"));
+          return;
+        }
         // Delete existing shift using mutation
         await compareData.deleteShift({
           calendarId,
           shiftId: existingShift.id,
         });
       } else {
hooks/useShiftActions.ts (1)

43-106: Prevent deletes of externally synced shifts.
Both delete paths allow removing shifts that are syncedFromExternal, which violates the no-delete rule for external syncs. Based on learnings, synced shifts from external calendars cannot be deleted.

🛠️ Suggested guard for synced shifts
   const handleDeleteShift = useCallback(
     async (id: string) => {
+      const target = shifts.find((shift) => shift.id === id);
+      if (target?.syncedFromExternal) {
+        console.warn("Attempted to delete externally synced shift");
+        return;
+      }
       try {
         await deleteShift(id);
         onStatsRefresh();
       } catch (error) {
         console.error("Failed to delete shift:", error);
       }
     },
-    [deleteShift, onStatsRefresh]
+    [deleteShift, onStatsRefresh, shifts]
   );

   const handleAddShift = useCallback(
     async (date: Date | string, selectedPresetId: string | undefined) => {
       if (!selectedPresetId) return;

       const preset = presets.find((p) => p.id === selectedPresetId);
       if (!preset) return;
@@
         if (existingShift) {
+          if (existingShift.syncedFromExternal) {
+            console.warn("Attempted to delete externally synced shift");
+            return;
+          }
           try {
             await deleteShift(existingShift.id);
             onStatsRefresh();
           } catch (error) {
             console.error("Failed to delete shift:", error);
           }
         } else {
components/sync-notification-dialog.tsx (1)

162-223: Guard mutation handlers when calendarId is missing.

The component accepts calendarId as potentially null, but both handleDeleteLogs and handleMarkErrorsAsRead send API requests without checking. If the dialog stays open while a calendar is deleted, clicking actions sends requests with ?calendarId=null, which the API rejects with a 400 error. While the server protects this, disable buttons and add guard checks in handlers for better UX.

🛡️ Suggested guard
 const handleDeleteLogs = () => {
+  if (!calendarId) {
+    toast.error(
+      t("common.fetchError", { item: t("syncNotifications.title") })
+    );
+    return;
+  }
   setShowDeleteConfirm(false);
   deleteMutation.mutate();
 };

 const handleMarkErrorsAsRead = () => {
+  if (!calendarId) {
+    toast.error(
+      t("common.fetchError", { item: t("syncNotifications.title") })
+    );
+    return;
+  }
   markReadMutation.mutate();
 };
-                <Button
+                <Button
                   variant="outline"
                   size="sm"
                   onClick={handleMarkErrorsAsRead}
-                  disabled={markReadMutation.isPending}
+                  disabled={markReadMutation.isPending || !calendarId}
                 >
-              <Button
+              <Button
                 variant="outline"
                 size="sm"
                 onClick={() => setShowDeleteConfirm(true)}
-                disabled={deleteMutation.isPending || logs.length === 0}
+                disabled={deleteMutation.isPending || logs.length === 0 || !calendarId}
               >
components/preset-manage-sheet.tsx (1)

56-63: Remove unused onPresetsChange prop and address data inconsistency with presets handling.

The onPresetsChange prop is declared optional but never used in the component—it's only passed by the parent and should be removed from the interface (line 61).

More importantly, the component has a data consistency issue: it receives presets as a prop and filters them to initialize state (lines 230-231), but the usePresets hook also provides presets data via React Query. The component only destructures the mutation functions from the hook, ignoring its presets data. This means the component renders stale prop data rather than the hook's cached state, defeating React Query's benefits. Use presets from the hook instead of the prop to keep data in sync.

Additionally, per coding guidelines, this component should use the BaseSheet pattern (components/ui/base-sheet.tsx) with consistent header/footer styling and useDirtyState integration, rather than importing individual Sheet components.

🤖 Fix all issues with AI agents
In `@app/admin/logs/page.tsx`:
- Around line 115-117: Reset the UI selection and expansion state when filters
or search change by calling setSelectedLogIds([]) and setExpandedRows(new Set())
inside your filter and search handlers (e.g., the functions that update your
filter state / handleSearchChange); ensure both selectedLogIds and expandedRows
are cleared wherever you apply new filters or a new search so bulk actions
cannot target rows that no longer exist.
- Around line 199-206: The debounced search value (debouncedSearch) stays stale
after clearFilters resets searchQuery because the useEffect debounce waits
500ms; update clearFilters to also clear the pending debounce and/or immediately
call handleDebouncedSearchChange with an empty string so debouncedSearch is
cleared right away — locate the debounce useEffect and timer, and in the
clearFilters implementation call clearTimeout on that timer or invoke
handleDebouncedSearchChange('') (or both) to ensure debouncedSearch is reset
immediately when clearFilters runs.

In `@app/page.tsx`:
- Around line 676-677: Make the onPresetsChange prop optional and remove the
no-op handlers: update preset-selector.tsx to change the onPresetsChange prop
from required to optional (match the documentation in preset-manage-sheet.tsx),
adjust any internal usage to handle undefined, and then remove the empty
onPresetsChange={() => {}} props from app/page.tsx (and the other occurrences)
since React Query invalidation in usePresets.ts handles refetching.
- Around line 202-209: The stats query has no auto-refresh and turning
onStatsRefresh into a no-op removes updates; update the useShiftStats query
configuration to include refetchInterval: 5000 (matching useAdminStats) or
alternatively add query invalidation in the create/delete mutation success
handlers so stats refresh after mutations; locate useShiftStats and its consumer
(ShiftStats) and either pass/refactor the query options to include
refetchInterval: 5000 or call the appropriate React Query invalidateQueries in
the success callbacks inside createShiftHook and deleteShiftHook (or within
useShiftActions where onStatsRefresh was used) to ensure stats update after
create/delete.

In `@app/profile/activity/page.tsx`:
- Around line 144-151: The debounce useEffect references
handleDebouncedSearchChange but omits it from the dependency array, which can
cause stale closures; fix by memoizing handleDebouncedSearchChange with
useCallback (so it preserves identity unless its true dependencies change) and
then include that memoized function in the useEffect deps, or alternatively move
the debounce logic into the same useCallback so the effect only depends on
searchQuery and the stable callback; update references to useEffect,
handleDebouncedSearchChange, and searchQuery accordingly.

In `@components/calendar-share-management-sheet.tsx`:
- Around line 49-52: The effect that syncs prop calendarGuestPermission into
local state currently always calls setGuestPermission and can overwrite an
in‑flight user change; update the useEffect so it first checks a saving flag and
the current local value: if saving is true, do nothing, and if the local
guestPermission already equals calendarGuestPermission, do nothing; only call
setGuestPermission(calendarGuestPermission) when not saving and the values
differ. Ensure you reference the existing useEffect, setGuestPermission,
calendarGuestPermission and the local guestPermission/saving variables so the
guard is applied in the correct place.

In `@hooks/useActivityLogs.ts`:
- Around line 142-144: The success toast in the onSuccess handler uses a
hardcoded string ("Activity logs cleared") which bypasses i18n; update the
onSuccess callback in hooks/useActivityLogs.ts to use the app's translation
helper (e.g., call t('activityLogs.cleared') or the appropriate key) instead of
the literal string, ensure the translation key exists in your locale files, and
keep the existing toast.success(...) and queryClient.invalidateQueries({
queryKey: ["activity-logs"] }) calls unchanged so only the message source is
swapped to the localized value.

In `@hooks/useAdminAuditLogs.ts`:
- Around line 59-110: The fetchAuditLogsApi function uses hardcoded error
strings; replace them with localized messages using the provided t translator:
when response.status === 403 throw new Error(t("admin.accessDenied")), and for
other non-ok responses throw new Error(t("common.fetchError", { item:
t("admin.auditLogs") })); remove the unused-vars disable for the t parameter so
it is actually used and ensure fetchAuditLogsApi continues returning the same
shape.

In `@hooks/useCalendars.ts`:
- Around line 285-316: The current createCalendar performs a preflight POST then
calls createMutation.mutateAsync on non-OK responses; instead, remove the
duplicate POST and have createCalendar call createMutation.mutateAsync once,
moving rate-limit detection into createCalendarApi so it throws a specific
rate-limit error (use isRateLimitError/handleRateLimitError in the API layer).
Let createMutation's handlers (onMutate/onError/onSuccess) handle optimistic
cache updates (queryClient.setQueryData), setSelectedCalendar, toasts, and
invalidation; catch a thrown rate-limit error from mutateAsync in createCalendar
and call handleRateLimitError(t) so rate limits are processed centrally. Ensure
no manual cache update or second fetch remains in createCalendar.

In `@hooks/useCalendarSubscriptions.ts`:
- Around line 235-240: onSuccess is closing over stale availableCalendars when
showing the toast; instead, fetch the latest list at runtime (e.g., use
queryClient.getQueryData('availableCalendars') or the relevant query key) inside
the onSuccess handler and then find the calendar by calendarId to get the name,
or derive the name from the mutation response if it contains it; replace the
current availableCalendars.find(...) in the onSuccess of the unsubscribe
mutation with this runtime lookup and then call
toast.success(t("calendar.unsubscribeSuccess", { name: calendar?.name ||
"Calendar" })).
- Line 252: The returned `error` is currently passing `errorMessage` (type Error
| null) where the hook's public type expects `string | null`; update the return
to convert the Error to a string by returning `errorMessage ?
errorMessage.message : null` (in the useCalendarSubscriptions hook, where
`error: errorMessage` is set), so consumers receive `string | null` as declared.
- Around line 179-186: The onSuccess callback for the mutation uses
availableCalendars.find(...) which can be stale; change the mutation to accept
the calendar name (e.g., include name in the mutation variables when calling the
subscribe function) and then update onSuccess to use the passed-in name instead
of reading availableCalendars. Specifically, stop relying on availableCalendars
inside onSuccess (in the onSuccess handler in useCalendarSubscriptions) and use
the calendar name from the mutation variables (calendarId and name) to build the
toast.success message so it is correct even before the optimistic update
re-renders.

In `@hooks/useCalendarTokens.ts`:
- Around line 58-108: The error handling currently calls response.json()
directly (seen in updateTokenApi, deleteTokenApi and the preceding fetch block),
which will throw for empty/HTML responses; add a safe error-extraction helper
(e.g., parseErrorResponse) that first reads response.text(), attempts JSON.parse
or response.json() inside a try/catch, and falls back to the raw text or
response.statusText or `HTTP ${response.status}`; replace the existing error
branches to call that helper and throw new Error(...) with the extracted message
so non-JSON/error bodies are handled gracefully.

In `@hooks/useShiftForm.ts`:
- Line 48: The default color literal in the useShiftForm hook is inconsistent:
change the color fallback in the object that sets color: shiftData.color ||
"#4b82f6" to match the other defaults "#3b82f6" used elsewhere (ensure all
fallbacks in useShiftForm — including the occurrences around lines setting
start/end/defaults — use "#3b82f6"); update the color fallback expression in the
function where shiftData is deconstructed or merged (reference symbol
useShiftForm and the shiftData.color fallback) so all defaults are consistent.

In `@hooks/useShifts.ts`:
- Around line 209-226: The returned helpers (setShifts, refetchShifts,
createShift, deleteShift) use queryKeys.shifts.byCalendar(calendarId!) and
mutateAsync without guarding against calendarId being undefined; modify each to
first check calendarId and either no-op (for setShifts/refetchShifts) or
reject/throw a clear error (for createShift/deleteShift) when calendarId is
falsy. Concretely, wrap setShifts to only call
queryClient.setQueryData(queryKeys.shifts.byCalendar(calendarId)) if calendarId,
wrap refetchShifts to only call queryClient.invalidateQueries(...) when
calendarId, and expose createShift/deleteShift as guarded functions that check
calendarId and return a rejected Promise (or throw) if missing instead of
calling createMutation.mutateAsync/deleteMutation.mutateAsync with an invalid
key.

In `@hooks/useShiftStats.ts`:
- Around line 72-85: The useQuery call in useShiftStats (queryKey using
calendarId, period, currentDate and queryFn fetchShiftStatsApi) lacks an
explicit refetchInterval so it doesn't match the documented "automatic polling
every 5 seconds"; update the useQuery options to include refetchInterval: 5000
(ms) alongside enabled and other options to enable consistent 5s polling like
other hooks (e.g., useAdminStats).

In `@scripts/i18n-checks.ts`:
- Around line 586-613: Guard the match rate calculation against division by
zero: when computing matchRate from totalUniqueKeysUsed and allKeys.length,
check if allKeys.length is zero and set matchRate to a safe default string/value
(e.g., "0.0" or "N/A") instead of performing the division; update the code
around the matchRate calculation and the console.log that prints it so it uses
this guarded value (refer to variables allKeys, totalUniqueKeysUsed, and
matchRate).
- Around line 405-438: The current counting logic (wildcardMatchedKeysCount /
keysInBoth / totalUniqueKeysUsed) overcounts when multiple wildcardPatterns
overlap and uses a different matching rule than isKeyOrParentUsed; fix by
extracting the wildcard matching logic into a single shared matcher function
(reuse isKeyOrParentUsed or rename to matchKeyOrChildForWildcard) and use it
consistently, then collect matched keys into a Set (e.g., matchedKeysSet)
instead of summing lengths so overlapping wildcardPatterns are de-duped; compute
wildcardMatchedKeysCount from matchedKeysSet.size and recompute keysInBoth by
testing realStaticKeys against the same matcher/Set to produce a correct
totalUniqueKeysUsed.
🧹 Nitpick comments (17)
components/calendar-token-list.tsx (1)

65-67: Consider adding error feedback for the toggle action.

The handleToggleActive function doesn't handle potential failures from updateToken. While the hook likely shows a toast on error internally, the UI state (e.g., dropdown menu) might benefit from knowing if the action succeeded.

For comparison, handleDelete (lines 69-77) checks the return value and only updates local state on success.

💡 Optional: Add success handling for consistency
 const handleToggleActive = async (token: CalendarAccessToken) => {
-   await updateToken(token.id, { isActive: !token.isActive });
+   const success = await updateToken(token.id, { isActive: !token.isActive });
+   // Handle success/failure if needed for UI feedback
 };
lib/query-keys.ts (1)

40-45: Consider stronger typing for the filters parameters.

The filters?: object type is permissive. If specific filter shapes are known, defining interfaces would improve type safety and enable IDE autocompletion.

💡 Optional: Define filter interfaces for better type safety
// Example filter interfaces (adjust based on actual usage)
interface UserFilters {
  search?: string;
  status?: "active" | "inactive";
  role?: string;
}

interface CalendarFilters {
  search?: string;
  ownerId?: string;
}

// Then use them:
users: (filters?: UserFilters) => ["admin", "users", filters] as const,
calendars: (filters?: CalendarFilters) => ["admin", "calendars", filters] as const,
hooks/useCompareData.ts (3)

15-43: Inconsistent error handling between fetch and mutation functions.

The fetch functions (fetchShiftsApi, fetchNotesApi, fetchExternalSyncsApi, fetchPresetsApi) silently return empty arrays on failure, while mutation functions throw errors. This inconsistency can mask real API failures in compare mode - users won't know if data failed to load vs. genuinely empty.

Consider throwing errors and letting React Query handle retries/error states:

♻️ Suggested approach for consistent error handling
 async function fetchShiftsApi(
   calendarId: string
 ): Promise<ShiftWithCalendar[]> {
   const response = await fetch(`/api/shifts?calendarId=${calendarId}`);
-  if (!response.ok) return [];
+  if (!response.ok) {
+    throw new Error("Failed to fetch shifts");
+  }
   const data = await response.json();
   return data.map(normalizeShift);
 }

Apply similar changes to other fetch functions. Then handle the error state in the hook's return value if needed.


129-141: Unstable memoization dependencies cause unnecessary recomputation.

The intermediate arrays (shiftsDataArray, etc.) are created fresh on every render via .map(), causing the useMemo hooks to recompute even when the underlying query data hasn't changed. This defeats the purpose of memoization.

♻️ Consider using useMemo for the intermediate arrays
-  const shiftsDataArray = shiftsQueries.map((q) => q.data);
-  const notesDataArray = notesQueries.map((q) => q.data);
-  const externalSyncsDataArray = externalSyncsQueries.map((q) => q.data);
-  const presetsDataArray = presetsQueries.map((q) => q.data);
-  const presetsLoadingArray = presetsQueries.map((q) => q.isLoading);
+  const shiftsDataArray = useMemo(
+    () => shiftsQueries.map((q) => q.data),
+    [shiftsQueries]
+  );
+  // Apply similar pattern to other arrays

Alternatively, combine the map-building logic into single useMemo calls that depend directly on the query results.


182-215: Good mutation structure, but consider adding optimistic updates.

The mutations correctly invalidate cache on success. However, given the PR objective mentions optimistic updates, consider implementing them here for a better UX during shift toggle operations. Based on learnings, optimistic updates should use temporary IDs and rollback on error.

hooks/useExternalSync.ts (2)

61-76: Non-null assertion on potentially null value.

The selectedCalendar! assertion is safe here because enabled: !!selectedCalendar prevents execution when null. However, this pattern is fragile - if someone removes the enabled check, it would cause runtime issues.

♻️ Cleaner pattern to avoid non-null assertions
   const { data: externalSyncs = [], isLoading: syncsLoading } = useQuery({
-    queryKey: queryKeys.externalSyncs.byCalendar(selectedCalendar!),
-    queryFn: () => fetchExternalSyncsApi(selectedCalendar!),
+    queryKey: queryKeys.externalSyncs.byCalendar(selectedCalendar ?? ""),
+    queryFn: () => fetchExternalSyncsApi(selectedCalendar!),
     enabled: !!selectedCalendar,
     refetchInterval: 5000,
     refetchIntervalInBackground: true,
   });

Or extract the calendar ID check into the queryFn with an early return.


65-76: Consider disabling background polling for resource efficiency.

refetchIntervalInBackground: true continues polling even when the browser tab is hidden. For sync status that doesn't require immediate attention when the user isn't looking, consider removing this flag to reduce server load.

app/api/activity-logs/route.ts (1)

65-68: Redundant severity filtering for audit logs.

Severity is filtered at the database level for audit logs (line 67) and again post-merge (line 172). While this works correctly, the post-merge filter is only necessary for sync logs (which derive severity from status). Consider skipping the post-merge filter when the type isn't "sync":

♻️ Suggested optimization
     // 3. Apply severity filter after merging (sync logs derive severity from status)
     // ============================================
     let filteredLogs = allLogs;
-    if (severity) {
+    // Only apply post-merge severity filter if sync logs are included
+    // (audit logs are already filtered at DB level)
+    if (severity && (!type || type === "sync")) {
       filteredLogs = filteredLogs.filter((log) => log.severity === severity);
     }

Also applies to: 170-173

hooks/useNotes.ts (2)

89-102: Consider removing unnecessary body from DELETE request.

The DELETE request sends an empty JSON body which is unusual. While not incorrect, DELETE requests typically don't require a body.

🔧 Suggested simplification
 async function deleteNoteApi(id: string): Promise<void> {
   const response = await fetch(`/api/notes/${id}`, {
     method: "DELETE",
-    headers: { "Content-Type": "application/json" },
-    body: JSON.stringify({}),
   });
 
   if (!response.ok) {
     const errorText = await response.text();
     throw new Error(
       `Failed to delete note: ${response.status} ${response.statusText} - ${errorText}`
     );
   }
 }

104-115: Consider consolidating identical context types.

The three context interfaces are identical. While this works, consider using a single type alias to reduce duplication.

🔧 Optional consolidation
-// Context types for optimistic updates
-interface CreateNoteContext {
-  previous: CalendarNote[] | undefined;
-}
-
-interface UpdateNoteContext {
-  previous: CalendarNote[] | undefined;
-}
-
-interface DeleteNoteContext {
-  previous: CalendarNote[] | undefined;
-}
+// Context type for optimistic updates
+interface MutationContext {
+  previous: CalendarNote[] | undefined;
+}
app/admin/users/page.tsx (1)

55-55: Consider pagination for scalability.

The limit: 1000 effectively disables pagination, fetching all users at once. For admin panels with many users, this could impact performance. Consider implementing proper pagination UI or at least documenting this as a known limitation.

components/admin/audit-log-delete-dialog.tsx (1)

17-34: Avoid redundant polling in the dialog.
useAdminAuditLogs() performs a polling query every 5s; instantiating it here can trigger extra network traffic (and potentially a different default filter/pagination) even though the dialog only needs delete mutations. Consider passing delete handlers from the parent or exposing an action‑only hook (e.g., useAdminAuditLogActions) that surfaces mutation pending state.

hooks/useAdminCalendars.ts (3)

89-94: Unused parameter t in fetchCalendarsApi.

The t parameter is accepted but never used, with an eslint-disable comment suppressing the warning. Either remove the parameter or utilize it for error message translation if that was the intent.

Suggested fix
 async function fetchCalendarsApi(
   filters: CalendarFilters | undefined,
-  sort: CalendarSort | undefined,
-  // eslint-disable-next-line `@typescript-eslint/no-unused-vars`
-  t: ReturnType<typeof useTranslations>
+  sort: CalendarSort | undefined
 ): Promise<CalendarsListResponse> {

And update the caller at line 292:

-    queryFn: () => fetchCalendarsApi(filters, sort, t),
+    queryFn: () => fetchCalendarsApi(filters, sort),

341-344: Inconsistent query key usage in cache invalidation.

onSettled uses a hardcoded ["admin", "calendars"] array alongside queryKeys.admin.stats. For consistency and maintainability, consider using the query key factory throughout, or use a broader invalidation pattern consistently across all mutations.

The hardcoded array does work as a prefix match for invalidation, but mixing patterns reduces clarity.


358-369: orphanedCount not updated in delete optimistic update.

When deleting a calendar, the optimistic update decrements total but doesn't adjust orphanedCount. If the deleted calendar was orphaned, the count will be momentarily incorrect until the query refetches.

Suggested fix
       queryClient.setQueryData(
         queryKeys.admin.calendars({ filters, sort }),
         (old: CalendarsListResponse | undefined) => {
           if (!old) return old;
+          const deletedCalendar = old.calendars.find((cal) => cal.id === calendarId);
+          const wasOrphaned = deletedCalendar && !deletedCalendar.ownerId;
           return {
             ...old,
             calendars: old.calendars.filter((cal) => cal.id !== calendarId),
             total: old.total - 1,
+            orphanedCount: wasOrphaned ? old.orphanedCount - 1 : old.orphanedCount,
           };
         }
       );
hooks/usePresets.ts (1)

67-78: DELETE request with empty JSON body is unusual.

The deletePresetApi sends a DELETE request with body: JSON.stringify({}). While not incorrect, DELETE requests typically don't include a body. Consider removing the body if the API doesn't require it.

Suggested fix
 async function deletePresetApi(presetId: string): Promise<void> {
   const response = await fetch(`/api/presets/${presetId}`, {
     method: "DELETE",
-    headers: { "Content-Type": "application/json" },
-    body: JSON.stringify({}),
   });
app/profile/activity/page.tsx (1)

97-103: Unnecessary limit in useMemo dependency array.

limit is a constant (line 65) and will never change. Including it in the dependency array is harmless but unnecessary.

   const pagination = useMemo(
     () => ({
       limit,
       offset: page * limit,
     }),
-    [page, limit]
+    [page]
   );

Comment thread app/admin/logs/page.tsx
Comment thread app/admin/logs/page.tsx
Comment thread app/page.tsx Outdated
Comment thread app/page.tsx Outdated
Comment thread app/profile/activity/page.tsx Outdated
Comment thread hooks/useShiftForm.ts Outdated
Comment thread hooks/useShifts.ts Outdated
Comment thread hooks/useShiftStats.ts
Comment thread scripts/i18n-checks.ts
Comment thread scripts/i18n-checks.ts

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
components/external-sync-manage-sheet.tsx (1)

52-66: Remove unused syncErrorRefreshTrigger prop from interface.

The prop syncErrorRefreshTrigger?: number is still defined in the interface but is no longer used (as noted in the comment). Dead props in interfaces can cause confusion for consumers.

🧹 Remove dead prop
 interface ExternalSyncManageSheetProps {
   open: boolean;
   onOpenChange: (open: boolean) => void;
   calendarId: string | null;
   onSyncComplete?: () => void;
-  syncErrorRefreshTrigger?: number;
 }

 export function ExternalSyncManageSheet({
   open,
   onOpenChange,
   calendarId,
   onSyncComplete,
-}: // syncErrorRefreshTrigger is no longer needed - React Query polls automatically
-ExternalSyncManageSheetProps) {
+}: ExternalSyncManageSheetProps) {
🤖 Fix all issues with AI agents
In `@hooks/useAdminStats.ts`:
- Around line 122-125: The toast.error call inside the render path (the if
(error) { toast.error(error.message); } in useAdminStats.ts) causes repeated
notifications; move this logic into a side effect or the query's error callback
so it only runs when the error state changes—for example, remove the direct
toast call from the component body and either add a useEffect that watches the
error variable and fires toast.error(error.message) when error becomes non-null
(with appropriate dependency array and deduping) or pass an onError handler to
the underlying useQuery/fetch hook to show the toast once per error occurrence.
♻️ Duplicate comments (2)
hooks/useAdminAuditLogs.ts (1)

60-95: Localize fetch errors and drop the unused‑var suppression.

Hardcoded errors bypass i18n and the t arg is currently unused.
As per coding guidelines, localize all user‑facing strings.

🌐 Suggested i18n update
 async function fetchAuditLogsApi(
   filters: AuditLogFilters,
   sort: AuditLogSort,
   pagination: AuditLogPagination,
-  // eslint-disable-next-line `@typescript-eslint/no-unused-vars`
   t: ReturnType<typeof useTranslations>
 ): Promise<AuditLogsResponse> {
@@
   if (!response.ok) {
     if (response.status === 403) {
-      throw new Error("Access denied");
+      throw new Error(t("admin.accessDenied"));
     }
-    throw new Error("Failed to fetch audit logs");
+    throw new Error(t("common.fetchError", { item: t("admin.auditLogs") }));
   }
hooks/useActivityLogs.ts (1)

46-101: Localize activity‑log messages (fetch/clear/success).

Hardcoded strings bypass i18n; please use translation keys instead.
As per coding guidelines, all user‑facing strings should go through t().

🌐 Suggested i18n update (adjust keys to match your locale files)
 async function fetchActivityLogsApi(
   filters: ActivityLogsFilters,
-  pagination: ActivityLogsPagination
+  pagination: ActivityLogsPagination,
+  t: ReturnType<typeof useTranslations>
 ): Promise<ActivityLogsResponse> {
@@
   if (!response.ok) {
-    throw new Error("Failed to fetch activity logs");
+    throw new Error(
+      t("common.fetchError", { item: t("activityLogs.title") })
+    );
   }
@@
 async function clearLogsApi(
   t: ReturnType<typeof useTranslations>
 ): Promise<void> {
@@
   if (!response.ok) {
-    throw new Error(t("common.deleteError", { item: "Activity logs" }));
+    throw new Error(
+      t("common.deleteError", { item: t("activityLogs.title") })
+    );
   }
 }
@@
   } = useQuery({
     queryKey: queryKeys.activityLogs({ filters, pagination }),
-    queryFn: () => fetchActivityLogsApi(filters, pagination),
+    queryFn: () => fetchActivityLogsApi(filters, pagination, t),
     refetchInterval: REFETCH_INTERVAL,
     refetchIntervalInBackground: true, // Continue polling in background
   });
@@
     onSuccess: () => {
-      toast.success("Activity logs cleared");
+      toast.success(t("activityLogs.cleared"));
       queryClient.invalidateQueries({ queryKey: ["activity-logs"] });
     },
     onError: (err) => {
       toast.error(
-        err instanceof Error ? err.message : "Failed to clear activity logs"
+        err instanceof Error
+          ? err.message
+          : t("common.deleteError", { item: t("activityLogs.title") })
       );
     },
   });

Also applies to: 140-150

🧹 Nitpick comments (4)
hooks/useExternalSync.ts (1)

12-23: Consider extracting error details from API response.

The error handling discards potentially useful error information from the response body. Consider parsing the error response for more informative messages.

💡 Suggested improvement
 async function fetchExternalSyncsApi(
   calendarId: string
 ): Promise<ExternalSync[]> {
   const params = new URLSearchParams({ calendarId });
   const response = await fetch(`/api/external-syncs?${params}`);

   if (!response.ok) {
-    throw new Error("Failed to fetch external syncs");
+    const data = await response.json().catch(() => ({}));
+    throw new Error(data.error || "Failed to fetch external syncs");
   }

   return await response.json();
 }
hooks/useAdminCalendars.ts (2)

342-345: Inconsistent query key usage in cache invalidation.

onSettled uses a hardcoded array ["admin", "calendars"] instead of queryKeys.admin.calendars(...). While this works for prefix-based invalidation, it's inconsistent with the typed query keys used elsewhere and could drift if the key structure changes.

♻️ Use queryKeys for consistency
     onSettled: () => {
-      queryClient.invalidateQueries({ queryKey: ["admin", "calendars"] });
+      queryClient.invalidateQueries({ 
+        queryKey: queryKeys.admin.calendars({}),
+        exact: false 
+      });
       queryClient.invalidateQueries({ queryKey: queryKeys.admin.stats });
     },

Apply similar changes to deleteMutation, transferMutation, bulkDeleteMutation, and bulkTransferMutation at lines 387, 412, 460, and 487.


359-370: Optimistic update doesn't adjust orphanedCount.

When deleting a calendar, if the deleted calendar was orphaned (owner === null), orphanedCount should also be decremented for accurate optimistic UI. Currently it remains unchanged until the query refetches.

💡 Optional: More precise optimistic update
       queryClient.setQueryData(
         queryKeys.admin.calendars({ filters, sort }),
         (old: CalendarsListResponse | undefined) => {
           if (!old) return old;
+          const deletedCal = old.calendars.find((cal) => cal.id === calendarId);
+          const wasOrphaned = deletedCal && deletedCal.ownerId === null;
           return {
             ...old,
             calendars: old.calendars.filter((cal) => cal.id !== calendarId),
             total: old.total - 1,
+            orphanedCount: wasOrphaned ? old.orphanedCount - 1 : old.orphanedCount,
           };
         }
       );
components/external-sync-manage-sheet.tsx (1)

100-129: Query fetching is duplicated from useExternalSync hook.

This component duplicates the query logic from hooks/useExternalSync.ts. Consider whether useExternalSync could be extended to expose syncLogs directly, avoiding the duplication.

However, the current approach is acceptable since:

  • The component needs direct access to syncLogsData for error computation
  • The enabled: !!calendarId && open condition is specific to this dialog
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between aea29be and 66475b4.

📒 Files selected for processing (10)
  • .github/copilot-instructions.md
  • components/external-sync-manage-sheet.tsx
  • components/sync-notification-dialog.tsx
  • hooks/useActivityLogs.ts
  • hooks/useAdminAuditLogs.ts
  • hooks/useAdminCalendars.ts
  • hooks/useAdminStats.ts
  • hooks/useAdminUsers.ts
  • hooks/useExternalSync.ts
  • lib/query-client.ts
🧰 Additional context used
📓 Path-based instructions (5)
components/**/*.{tsx,ts}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Components in /components directory should be "use client" by default for interactive UI, and should use shadcn/ui patterns with Radix UI primitives and Lucide icons.

Files:

  • components/external-sync-manage-sheet.tsx
  • components/sync-notification-dialog.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Store and display dates as YYYY-MM-DD strings (local dates without timezone conversion). Use formatDateToLocal() from lib/date-utils.ts before saving to database and date-fns with locale from getDateLocale() for display.
Use Drizzle-inferred types from schema directly (no any types). TypeScript must remain in strict mode with proper type safety throughout the application.
All React files must be in strict mode with TypeScript 5. No any types allowed. Import inferred types from Drizzle schema.
Use formatDateToLocal(date) utility from lib/date-utils.ts to convert dates to YYYY-MM-DD format before saving to database.

Files:

  • components/external-sync-manage-sheet.tsx
  • lib/query-client.ts
  • hooks/useExternalSync.ts
  • hooks/useAdminUsers.ts
  • hooks/useActivityLogs.ts
  • components/sync-notification-dialog.tsx
  • hooks/useAdminStats.ts
  • hooks/useAdminAuditLogs.ts
  • hooks/useAdminCalendars.ts
components/**/*sheet*.{tsx,ts}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use the Sheet pattern (components/ui/base-sheet.tsx) for all forms with side panels, including unsaved changes confirmation via useDirtyState hook and consistent header/footer styling with gradient backgrounds.

Files:

  • components/external-sync-manage-sheet.tsx
hooks/use*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Implement data fetching hooks following the pattern: useShifts, usePresets, useNotes, useCalendars with accompanying action hooks like useShiftActions, useNoteActions for CRUD operations with optimistic updates.

Files:

  • hooks/useExternalSync.ts
  • hooks/useAdminUsers.ts
  • hooks/useActivityLogs.ts
  • hooks/useAdminStats.ts
  • hooks/useAdminAuditLogs.ts
  • hooks/useAdminCalendars.ts
components/**/*dialog*.{tsx,ts}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use the Dialog pattern (components/ui/dialog.tsx from shadcn/ui) for list views, confirmations, and read-only displays.

Files:

  • components/sync-notification-dialog.tsx
🧠 Learnings (22)
📓 Common learnings
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{tsx} : Use `useSSEConnection` hook from `hooks/useSSEConnection.ts` for real-time sync connection management. The hook automatically handles reconnection, visibility changes, and triggers data refreshes.
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to hooks/use*.{ts,tsx} : Implement data fetching hooks following the pattern: `useShifts`, `usePresets`, `useNotes`, `useCalendars` with accompanying action hooks like `useShiftActions`, `useNoteActions` for CRUD operations with optimistic updates.
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to app/api/**/*.ts : After any database mutation (create/update/delete), emit an SSE event using `eventEmitter.emit('calendar-change', {...})` with appropriate type and action to keep clients in sync.
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to components/**/*sheet*.{tsx,ts} : Use the Sheet pattern (`components/ui/base-sheet.tsx`) for all forms with side panels, including unsaved changes confirmation via `useDirtyState` hook and consistent header/footer styling with gradient backgrounds.

Applied to files:

  • components/external-sync-manage-sheet.tsx
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{tsx} : Use `useSSEConnection` hook from `hooks/useSSEConnection.ts` for real-time sync connection management. The hook automatically handles reconnection, visibility changes, and triggers data refreshes.

Applied to files:

  • components/external-sync-manage-sheet.tsx
  • hooks/useExternalSync.ts
  • hooks/useAdminUsers.ts
  • .github/copilot-instructions.md
  • components/sync-notification-dialog.tsx
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to hooks/use*.{ts,tsx} : Implement data fetching hooks following the pattern: `useShifts`, `usePresets`, `useNotes`, `useCalendars` with accompanying action hooks like `useShiftActions`, `useNoteActions` for CRUD operations with optimistic updates.

Applied to files:

  • components/external-sync-manage-sheet.tsx
  • hooks/useExternalSync.ts
  • hooks/useAdminUsers.ts
  • .github/copilot-instructions.md
  • hooks/useActivityLogs.ts
  • components/sync-notification-dialog.tsx
  • hooks/useAdminStats.ts
  • hooks/useAdminAuditLogs.ts
  • hooks/useAdminCalendars.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Respect `shift.syncedFromExternal` flag - synced shifts from external calendars cannot be edited or deleted by users. Check this flag before allowing shift mutations.

Applied to files:

  • components/external-sync-manage-sheet.tsx
  • hooks/useExternalSync.ts
  • .github/copilot-instructions.md
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{tsx} : Use `useCalendarPermission(calendarId)` hook to check UI permissions, which returns `canEdit`, `canDelete`, `canShare`, and `permission` level for displaying conditional UI elements.

Applied to files:

  • components/external-sync-manage-sheet.tsx
  • hooks/useExternalSync.ts
  • .github/copilot-instructions.md
  • components/sync-notification-dialog.tsx
  • hooks/useAdminAuditLogs.ts
  • hooks/useAdminCalendars.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to app/api/auth/**/*.ts : For Better Auth integration, use built-in methods for authentication operations (`login`, `logout`, `session management`, `OAuth`). Use custom code in `lib/auth/admin.ts` only for permission checks (`canEditUser`, `canDeleteUser`, `canBanUser`).

Applied to files:

  • hooks/useAdminUsers.ts
  • hooks/useAdminAuditLogs.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to hooks/use*Actions.{ts,tsx} : Implement optimistic updates in action hooks by: generating temp ID (`temp-${Date.now()}`), adding item to state immediately, making API call, removing temp item on error with toast, replacing temp item on success.

Applied to files:

  • hooks/useAdminUsers.ts
  • .github/copilot-instructions.md
  • hooks/useAdminAuditLogs.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{tsx} : For auth-related configuration checks, use the convenience hook `useAuthFeatures()` from `hooks/useAuthFeatures.ts` to access `isAuthEnabled`, `allowRegistration`, `allowGuest`, and `providers` on the client side.

Applied to files:

  • hooks/useAdminUsers.ts
  • .github/copilot-instructions.md
  • hooks/useAdminAuditLogs.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to app/api/**/*.ts : After any database mutation (create/update/delete), emit an SSE event using `eventEmitter.emit('calendar-change', {...})` with appropriate type and action to keep clients in sync.

Applied to files:

  • .github/copilot-instructions.md
  • components/sync-notification-dialog.tsx
  • hooks/useAdminCalendars.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to lib/db/schema.ts : Database schema modifications must be made in `lib/db/schema.ts`. After changes, generate migrations with `npm run db:generate`, review SQL in `drizzle/` folder, then apply with `npm run db:migrate`.

Applied to files:

  • .github/copilot-instructions.md
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to app/**/*.{tsx,ts} : All components under `app/` are Server Components by default unless marked with `"use client"`. Use `"use client"` directive only for interactive UI components.

Applied to files:

  • .github/copilot-instructions.md
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{tsx} : Use Better Auth's built-in client methods (`authClient.listSessions()`, `authClient.revokeOtherSessions()`) for client-side session management. Never implement custom session revocation logic.

Applied to files:

  • .github/copilot-instructions.md
  • hooks/useAdminAuditLogs.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to components/**/*.{tsx,ts} : Components in `/components` directory should be `"use client"` by default for interactive UI, and should use shadcn/ui patterns with Radix UI primitives and Lucide icons.

Applied to files:

  • .github/copilot-instructions.md
  • components/sync-notification-dialog.tsx
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to app/api/**/*.ts : All API routes must include `GET/POST/PUT/PATCH/DELETE` exports in `app/api/**` directory and follow the pattern: check permissions → perform database operation → emit SSE event.

Applied to files:

  • .github/copilot-instructions.md
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: All user-facing strings must use the `t()` function from `next-intl` for translations. Never use hardcoded English strings in components or API responses.

Applied to files:

  • .github/copilot-instructions.md
  • hooks/useActivityLogs.ts
  • hooks/useAdminAuditLogs.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to app/api/**/*.ts : Use `getSessionUser(request.headers)` from `lib/auth/sessions.ts` to get current user in API routes. Use Better Auth's `authClient` methods on the client side for session management.

Applied to files:

  • .github/copilot-instructions.md
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Use `getPublicConfig()` function from `lib/public-config.ts` in Server Components to access configuration. Use `usePublicConfig()` hook from `hooks/usePublicConfig.ts` in Client Components. Never use `process.env.NEXT_PUBLIC_*` directly.

Applied to files:

  • .github/copilot-instructions.md
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{ts,tsx} : All React files must be in strict mode with TypeScript 5. No `any` types allowed. Import inferred types from Drizzle schema.

Applied to files:

  • .github/copilot-instructions.md
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{ts,tsx} : Use Drizzle-inferred types from schema directly (no `any` types). TypeScript must remain in strict mode with proper type safety throughout the application.

Applied to files:

  • .github/copilot-instructions.md
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to components/**/*dialog*.{tsx,ts} : Use the Dialog pattern (`components/ui/dialog.tsx` from shadcn/ui) for list views, confirmations, and read-only displays.

Applied to files:

  • components/sync-notification-dialog.tsx
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to app/api/**/*.ts : For calendar permission hierarchy in SQL queries and permission checks, follow: `owner` (highest) > `admin` > `write` > `read` (lowest). Use `getUserAccessibleCalendars(userId)` to get all accessible calendars.

Applied to files:

  • hooks/useAdminCalendars.ts
🧬 Code graph analysis (3)
hooks/useActivityLogs.ts (2)
lib/query-keys.ts (1)
  • queryKeys (10-61)
lib/query-client.ts (1)
  • REFETCH_INTERVAL (3-3)
components/sync-notification-dialog.tsx (3)
lib/db/schema.ts (1)
  • SyncLog (409-409)
lib/query-keys.ts (1)
  • queryKeys (10-61)
lib/query-client.ts (1)
  • REFETCH_INTERVAL (3-3)
hooks/useAdminStats.ts (2)
lib/query-keys.ts (1)
  • queryKeys (10-61)
lib/query-client.ts (1)
  • REFETCH_INTERVAL (3-3)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-dev
🔇 Additional comments (18)
lib/query-client.ts (1)

1-22: Well-structured React Query configuration.

The configuration is sound:

  • staleTime (3s) < refetchInterval (5s) ensures data is considered stale before the next poll cycle
  • Exponential backoff with a 30s cap is appropriate for retry resilience
  • gcTime (5min) provides reasonable cache retention
hooks/useExternalSync.ts (1)

58-102: Clean React Query migration with proper cache management.

The hook correctly:

  • Guards queries with enabled: !!selectedCalendar
  • Uses centralized query keys for cache consistency
  • Provides a unified refetch via query invalidation
  • Computes derived state (hasSyncErrors) with useMemo

The non-null assertions on selectedCalendar! in queryKey and queryFn are safe due to the enabled guard.

hooks/useAdminStats.ts (1)

55-86: API function properly parses dates and handles errors.

Good pattern: converting timestamp strings to Date objects at the data boundary ensures type consistency throughout the application.

hooks/useAdminCalendars.ts (1)

277-295: Well-structured React Query migration with comprehensive mutation support.

The hook provides:

  • Polling with centralized REFETCH_INTERVAL
  • Optimistic updates for immediate UI feedback
  • Proper rollback on mutation errors
  • Toast notifications for user feedback
  • Clean API surface with boolean return values for mutation wrappers
components/external-sync-manage-sheet.tsx (2)

152-162: Clean invalidation helper with proper memoization.

The invalidateSyncs callback correctly:

  • Guards against null calendarId
  • Invalidates both syncs and logs queries
  • Is properly memoized with useCallback

164-183: Good cleanup on dialog close.

The useEffect properly resets all form state when the dialog closes, preventing stale state from persisting between open/close cycles. The dependency array correctly includes only open.

components/sync-notification-dialog.tsx (5)

3-23: Query setup and log fetch helper look solid.

The polling is scoped to dialog open state and the helper keeps the fetch logic centralized.

Also applies to: 35-75


77-103: Delete mutation flow looks good.

Success/error toasts and cache invalidation are consistent and clear.


104-131: Mark‑as‑read mutation is consistent with the delete flow.


163-170: Handlers are straightforward and clean.


214-224: Good UX guardrails on action buttons.

hooks/useAdminUsers.ts (5)

81-181: API wrappers and date parsing are well-structured.


183-317: Mutation API helpers are concise and consistent.


340-359: Query setup and polling configuration look good.


361-589: Optimistic mutations and cache invalidation are cleanly implemented.


591-669: Public hook API is clear and easy to consume.

.github/copilot-instructions.md (1)

14-271: Docs align well with the new polling/query‑key architecture.

Also applies to: 393-405, 448-459

hooks/useAdminAuditLogs.ts (1)

97-313: Hook structure, polling, and optimistic deletes look solid.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread hooks/useAdminStats.ts Outdated

@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: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
components/calendar-share-management-sheet.tsx (1)

57-71: Ensure optimistic state always resets (even on failure).
If updateCalendar throws, saving remains true and the optimistic value never clears. Wrap the mutation in try/finally and consider ignoring changes while a save is in flight.

🔧 Proposed fix
  const handleGuestPermissionChange = async (
    value: "none" | "read" | "write"
  ) => {
-    if (!canManageShares) return;
-
-    setOptimisticGuestPermission(value);
-    setSaving(true);
-
-    await updateCalendar(calendarId, {
-      guestPermission: value,
-    });
-
-    setSaving(false);
-    setOptimisticGuestPermission(null);
+    if (!canManageShares || saving) return;
+
+    setOptimisticGuestPermission(value);
+    setSaving(true);
+    try {
+      await updateCalendar(calendarId, {
+        guestPermission: value,
+      });
+    } finally {
+      setSaving(false);
+      setOptimisticGuestPermission(null);
+    }
  };
🤖 Fix all issues with AI agents
In `@hooks/useActivityLogs.ts`:
- Around line 91-101: Replace hardcoded user-facing strings with localized ones:
in clearLogsApi use the translation function t for the item name instead of the
literal "Activity logs" (e.g., t("activityLogs.name") or pass
t("activityLogs.name") into t("common.deleteError", { item: ... })), and
likewise replace the literal "Failed to clear activity logs" used elsewhere
(lines 147-150) with an appropriate t(...) key (create a new key like
activityLogs.clearError if none exists). Update both clearLogsApi and the other
clear-logs function to use t(...) consistently for all displayed error text.

In `@hooks/useCalendars.ts`:
- Around line 177-183: onError currently always logs and shows a generic toast
even for RateLimitError, causing duplicate notifications; inside the onError
handler in hooks/useCalendars.ts (the function handling the mutation error),
after performing the optimistic rollback via
queryClient.setQueryData(queryKeys.calendars.all, context.previous), detect if
err is a RateLimitError (e.g., if (err instanceof RateLimitError) or using your
app's RateLimitError type check) and if so skip the console.error and
toast.error and simply return; otherwise proceed to console.error("Failed to
create calendar:", err) and toast.error(...) as before; apply the same change to
the other onError block around lines 298-307.

In `@hooks/useCalendarSubscriptions.ts`:
- Around line 106-114: The docstring claiming "Automatic polling every 5
seconds" is out of sync with the useQuery call; update useQuery in
useCalendarSubscriptions (the call that uses queryKey
queryKeys.subscriptions.all and queryFn fetchSubscriptionsApi) to include
refetchInterval: 5000 to enable polling, or alternatively remove/update the
docstring to not mention polling—pick one approach and make the change so
behavior and documentation match.

In `@hooks/useCalendarTokens.ts`:
- Around line 154-167: In useCalendarTokens, add the explicit refetchInterval
option so the useQuery call uses refetchInterval: REFETCH_INTERVAL (same
constant other polling hooks use) to match the JSDoc and other hooks; import
REFETCH_INTERVAL from the same module used elsewhere (e.g., lib/constants or
where REFETCH_INTERVAL is defined) at the top, and include refetchInterval:
REFETCH_INTERVAL inside the useQuery config object alongside queryKey, queryFn,
and enabled in the useCalendarTokens function.

In `@hooks/useShifts.ts`:
- Around line 121-134: The optimisticShift fallback color is set to "#000000"
but your DB/schema default is "#3b82f6", causing a visual flash; update the
optimisticShift creation (the object named optimisticShift in useShifts.ts) to
use the schema default color "#3b82f6" instead of "#000000" (i.e., change color:
formData.color || "#000000" to color: formData.color || "#3b82f6") so the
optimistic UI matches the server default.
- Around line 63-76: In deleteShiftApi, remove the unnecessary empty request
body and Content-Type header from the fetch call used for the DELETE; update the
fetch options to only include method: "DELETE" (keep the existing response.ok
check and error throwing logic intact) so the DELETE request matches other calls
like useCalendarSubscriptions and avoids sending an empty JSON payload.

In `@hooks/useShiftStats.ts`:
- Around line 37-41: The code builds URLSearchParams using
currentDate.toISOString(), which causes UTC/time component drift and cache
churn; replace uses of currentDate.toISOString() with the local date string
returned by formatDateToLocal(currentDate) for both the cache/query key and the
params construction (e.g., where params is created with new URLSearchParams and
any other occurrences around useShiftStats functions at the other location
noted) so the "date" param and the stats key use YYYY‑MM‑DD local dates
consistently.
♻️ Duplicate comments (3)
hooks/useCalendarSubscriptions.ts (2)

236-246: Stale closure issue addressed using cache lookup.

The onSuccess handler now correctly uses queryClient.getQueryData to fetch fresh cache data instead of relying on a stale availableCalendars closure. The lookup checks both arrays since the optimistic update moves the calendar to dismissed.


258-258: Type mismatch issue addressed.

The error is now correctly returned as errorMessage?.message (string) instead of the Error object, matching the expected string | null return type.

app/page.tsx (1)

675-676: Remove no‑op onPresetsChange props by making it optional.

There are still empty handlers passed into DialogManager. Consider making the prop optional and dropping these no‑ops to keep the API clean and consistent with React Query invalidation.

Also applies to: 763-764

🧹 Nitpick comments (8)
components/admin/admin-header.tsx (1)

131-142: Approve with minor simplification suggestion.

The connection status indicator works correctly. Consider flattening the nested div structure—the title attribute can be applied directly to the indicator element:

🔧 Optional: Flatten nested divs
             {/* Connection Status Indicator */}
-            <div
-              title={isOnline ? t("sync.connected") : t("sync.disconnected")}
-            >
-              <div
-                className={`w-2.5 h-2.5 rounded-full transition-all ${
-                  isOnline
-                    ? "bg-green-500 animate-pulse shadow-[0_0_8px_rgba(34,197,94,0.6)]"
-                    : "bg-red-500 shadow-[0_0_8px_rgba(239,68,68,0.6)]"
-                }`}
-              ></div>
-            </div>
+            <div
+              className={`w-2.5 h-2.5 rounded-full transition-all ${
+                isOnline
+                  ? "bg-green-500 animate-pulse shadow-[0_0_8px_rgba(34,197,94,0.6)]"
+                  : "bg-red-500 shadow-[0_0_8px_rgba(239,68,68,0.6)]"
+              }`}
+              title={isOnline ? t("sync.connected") : t("sync.disconnected")}
+              aria-label={isOnline ? t("sync.connected") : t("sync.disconnected")}
+            />

Adding aria-label improves accessibility for screen readers, as the visual indicator has no text content.

hooks/useCalendarSubscriptions.ts (1)

270-281: Consider removing unused _calendarName parameter or using it for consistency.

The dismiss function accepts _calendarName but ignores it (with an eslint-disable). While the current implementation works by looking up the name from the cache, this creates an inconsistent API compared to subscribe which actually uses the name.

Either remove the parameter if callers don't need it, or pass it through mutation variables like subscribe does for symmetry.

♻️ Option: Use the name parameter like subscribe
   const dismissMutation = useMutation({
-    mutationFn: dismissApi,
-    onMutate: async (calendarId) => {
+    mutationFn: (variables: { calendarId: string; name: string }) =>
+      dismissApi(variables.calendarId),
+    onMutate: async (variables) => {
+      const { calendarId } = variables;
       // ... rest of onMutate
     },
-    onSuccess: (_, calendarId) => {
-      const data = queryClient.getQueryData<SubscriptionResponse>(
-        queryKeys.subscriptions.all
-      );
-      const calendar =
-        data?.available.find((cal) => cal.id === calendarId) ||
-        data?.dismissed.find((cal) => cal.id === calendarId);
+    onSuccess: (_, variables) => {
       toast.success(
-        t("calendar.unsubscribeSuccess", { name: calendar?.name || "Calendar" })
+        t("calendar.unsubscribeSuccess", { name: variables.name || "Calendar" })
       );
     },

Then update the wrapper:

     dismiss: async (
       calendarId: string,
-      // eslint-disable-next-line `@typescript-eslint/no-unused-vars`
-      _calendarName: string
+      calendarName: string
     ): Promise<boolean> => {
       try {
-        await dismissMutation.mutateAsync(calendarId);
+        await dismissMutation.mutateAsync({ calendarId, name: calendarName });
         return true;
hooks/useShiftActions.ts (2)

30-49: Consider if duplicate error logging is intentional.

Both handleShiftSubmit and handleDeleteShift log errors, but the underlying createShift and deleteShift from useShifts already log errors and show toast notifications. This results in duplicate console.error output.

If the additional logging is intentional for debugging call-site context, consider adding distinguishing information. Otherwise, the try/catch can simply re-throw or be removed.


8-21: Remove unused onStatsRefresh prop.

The onStatsRefresh prop is defined in the interface but never destructured or used in the hook.

♻️ Remove unused prop
 interface UseShiftActionsProps {
   shifts: ShiftWithCalendar[];
   presets: ShiftPreset[];
   createShift: (data: ShiftFormData) => Promise<ShiftWithCalendar>;
   deleteShift: (id: string) => Promise<void>;
-  onStatsRefresh?: () => void;
 }
hooks/useCalendarTokens.ts (1)

56-66: Consider using parseErrorResponse in fetchTokensApi for consistent error messaging.
Right now this path always throws a generic HTTP status even when the API returns a useful error body.

🔧 Suggested tweak
   if (!response.ok) {
-    throw new Error(`HTTP ${response.status}`);
+    const errorMessage = await parseErrorResponse(response);
+    throw new Error(errorMessage);
   }
hooks/useAdminAuditLogs.ts (1)

240-243: Use query key factory for cache invalidation consistency.

The invalidation uses a hardcoded ["admin", "audit-logs"] array while the query registration uses queryKeys.admin.auditLogs(). If the key structure in lib/query-keys.ts changes, this invalidation could silently break.

♻️ Suggested refactor using the query key factory
    onSettled: () => {
-      queryClient.invalidateQueries({ queryKey: ["admin", "audit-logs"] });
+      queryClient.invalidateQueries({ queryKey: queryKeys.admin.auditLogs() });
      queryClient.invalidateQueries({ queryKey: queryKeys.admin.stats });
    },

Apply the same change on line 260.

app/admin/logs/page.tsx (2)

209-216: Consider memoizing handleDebouncedSearchChange for safer debounce behavior.

The handleDebouncedSearchChange function is not in the dependency array, which could cause stale closure issues if the function body changes in future refactors. Since the function only calls stable state setters, it works correctly today.

♻️ Optional: Wrap handler with useCallback for explicit stability
+  const handleDebouncedSearchChange = useCallback((value: string) => {
+    setDebouncedSearch(value);
+    setPage(0);
+    setSelectedLogIds([]);
+    setExpandedRows(new Set());
+  }, []);

-  const handleDebouncedSearchChange = (value: string) => {
-    setDebouncedSearch(value);
-    setPage(0);
-    setSelectedLogIds([]);
-    setExpandedRows(new Set());
-  };

  // Debounce search query
  useEffect(() => {
    const timer = setTimeout(() => {
      handleDebouncedSearchChange(searchQuery);
    }, 500);

    return () => clearTimeout(timer);
-  }, [searchQuery]);
+  }, [searchQuery, handleDebouncedSearchChange]);

578-582: Redundant Date conversion.

The useAdminAuditLogs hook already converts log.timestamp to a Date object (see hooks/useAdminAuditLogs.ts line 101). Wrapping it again with new Date() is unnecessary.

♻️ Simplified date formatting
                    <TableCell className="whitespace-nowrap">
-                      {format(new Date(log.timestamp), "PPp", {
+                      {format(log.timestamp, "PPp", {
                        locale: dateLocale,
                      })}
                    </TableCell>
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 66475b4 and 343abfa.

📒 Files selected for processing (18)
  • app/admin/logs/page.tsx
  • app/page.tsx
  • app/profile/activity/page.tsx
  • components/admin/admin-header.tsx
  • components/app-header.tsx
  • components/auth-header.tsx
  • components/calendar-share-management-sheet.tsx
  • components/dialog-manager.tsx
  • components/preset-selector.tsx
  • hooks/useActivityLogs.ts
  • hooks/useAdminAuditLogs.ts
  • hooks/useCalendarSubscriptions.ts
  • hooks/useCalendarTokens.ts
  • hooks/useCalendars.ts
  • hooks/useShiftActions.ts
  • hooks/useShiftForm.ts
  • hooks/useShiftStats.ts
  • hooks/useShifts.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • hooks/useShiftForm.ts
  • components/preset-selector.tsx
  • components/app-header.tsx
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Store and display dates as YYYY-MM-DD strings (local dates without timezone conversion). Use formatDateToLocal() from lib/date-utils.ts before saving to database and date-fns with locale from getDateLocale() for display.
Use Drizzle-inferred types from schema directly (no any types). TypeScript must remain in strict mode with proper type safety throughout the application.
All React files must be in strict mode with TypeScript 5. No any types allowed. Import inferred types from Drizzle schema.
Use formatDateToLocal(date) utility from lib/date-utils.ts to convert dates to YYYY-MM-DD format before saving to database.

Files:

  • hooks/useCalendarTokens.ts
  • hooks/useCalendarSubscriptions.ts
  • app/admin/logs/page.tsx
  • hooks/useAdminAuditLogs.ts
  • hooks/useCalendars.ts
  • hooks/useShiftActions.ts
  • hooks/useActivityLogs.ts
  • components/admin/admin-header.tsx
  • hooks/useShifts.ts
  • components/calendar-share-management-sheet.tsx
  • components/auth-header.tsx
  • hooks/useShiftStats.ts
  • components/dialog-manager.tsx
  • app/profile/activity/page.tsx
  • app/page.tsx
hooks/use*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Implement data fetching hooks following the pattern: useShifts, usePresets, useNotes, useCalendars with accompanying action hooks like useShiftActions, useNoteActions for CRUD operations with optimistic updates.

Files:

  • hooks/useCalendarTokens.ts
  • hooks/useCalendarSubscriptions.ts
  • hooks/useAdminAuditLogs.ts
  • hooks/useCalendars.ts
  • hooks/useShiftActions.ts
  • hooks/useActivityLogs.ts
  • hooks/useShifts.ts
  • hooks/useShiftStats.ts
app/**/*.{tsx,ts}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

All components under app/ are Server Components by default unless marked with "use client". Use "use client" directive only for interactive UI components.

Files:

  • app/admin/logs/page.tsx
  • app/profile/activity/page.tsx
  • app/page.tsx
hooks/use*Actions.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Implement optimistic updates in action hooks by: generating temp ID (temp-${Date.now()}), adding item to state immediately, making API call, removing temp item on error with toast, replacing temp item on success.

Files:

  • hooks/useShiftActions.ts
components/**/*.{tsx,ts}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Components in /components directory should be "use client" by default for interactive UI, and should use shadcn/ui patterns with Radix UI primitives and Lucide icons.

Files:

  • components/admin/admin-header.tsx
  • components/calendar-share-management-sheet.tsx
  • components/auth-header.tsx
  • components/dialog-manager.tsx
components/**/*sheet*.{tsx,ts}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use the Sheet pattern (components/ui/base-sheet.tsx) for all forms with side panels, including unsaved changes confirmation via useDirtyState hook and consistent header/footer styling with gradient backgrounds.

Files:

  • components/calendar-share-management-sheet.tsx
components/**/*dialog*.{tsx,ts}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use the Dialog pattern (components/ui/dialog.tsx from shadcn/ui) for list views, confirmations, and read-only displays.

Files:

  • components/dialog-manager.tsx
🧠 Learnings (22)
📓 Common learnings
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{tsx} : Use `useSSEConnection` hook from `hooks/useSSEConnection.ts` for real-time sync connection management. The hook automatically handles reconnection, visibility changes, and triggers data refreshes.
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to hooks/use*.{ts,tsx} : Implement data fetching hooks following the pattern: `useShifts`, `usePresets`, `useNotes`, `useCalendars` with accompanying action hooks like `useShiftActions`, `useNoteActions` for CRUD operations with optimistic updates.
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to app/api/**/*.ts : After any database mutation (create/update/delete), emit an SSE event using `eventEmitter.emit('calendar-change', {...})` with appropriate type and action to keep clients in sync.
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{tsx} : Use `useCalendarPermission(calendarId)` hook to check UI permissions, which returns `canEdit`, `canDelete`, `canShare`, and `permission` level for displaying conditional UI elements.

Applied to files:

  • hooks/useCalendarTokens.ts
  • hooks/useCalendarSubscriptions.ts
  • hooks/useAdminAuditLogs.ts
  • hooks/useCalendars.ts
  • hooks/useShiftActions.ts
  • hooks/useShifts.ts
  • components/calendar-share-management-sheet.tsx
  • components/auth-header.tsx
  • hooks/useShiftStats.ts
  • components/dialog-manager.tsx
  • app/page.tsx
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to hooks/use*.{ts,tsx} : Implement data fetching hooks following the pattern: `useShifts`, `usePresets`, `useNotes`, `useCalendars` with accompanying action hooks like `useShiftActions`, `useNoteActions` for CRUD operations with optimistic updates.

Applied to files:

  • hooks/useCalendarTokens.ts
  • hooks/useCalendarSubscriptions.ts
  • app/admin/logs/page.tsx
  • hooks/useAdminAuditLogs.ts
  • hooks/useCalendars.ts
  • hooks/useShiftActions.ts
  • hooks/useActivityLogs.ts
  • hooks/useShifts.ts
  • hooks/useShiftStats.ts
  • app/profile/activity/page.tsx
  • app/page.tsx
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to app/api/**/*.ts : Use `getUserCalendarPermission(userId, calendarId)` from `lib/auth/permissions.ts` to retrieve the exact permission level, which returns permission string or `null` if user has no access.

Applied to files:

  • hooks/useCalendarTokens.ts
  • hooks/useShifts.ts
  • components/calendar-share-management-sheet.tsx
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Always check permissions using `checkPermission(userId, calendarId, requiredLevel)` function from `lib/auth/permissions.ts` in both API routes and UI components before allowing mutations.

Applied to files:

  • hooks/useCalendarTokens.ts
  • hooks/useCalendars.ts
  • hooks/useShifts.ts
  • components/calendar-share-management-sheet.tsx
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{tsx} : Use Better Auth's built-in client methods (`authClient.listSessions()`, `authClient.revokeOtherSessions()`) for client-side session management. Never implement custom session revocation logic.

Applied to files:

  • hooks/useCalendarTokens.ts
  • hooks/useAdminAuditLogs.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to app/api/**/*.ts : For calendar permission hierarchy in SQL queries and permission checks, follow: `owner` (highest) > `admin` > `write` > `read` (lowest). Use `getUserAccessibleCalendars(userId)` to get all accessible calendars.

Applied to files:

  • hooks/useCalendarTokens.ts
  • hooks/useCalendars.ts
  • hooks/useShifts.ts
  • components/calendar-share-management-sheet.tsx
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{tsx} : For auth-related configuration checks, use the convenience hook `useAuthFeatures()` from `hooks/useAuthFeatures.ts` to access `isAuthEnabled`, `allowRegistration`, `allowGuest`, and `providers` on the client side.

Applied to files:

  • hooks/useCalendarTokens.ts
  • app/admin/logs/page.tsx
  • hooks/useAdminAuditLogs.ts
  • components/calendar-share-management-sheet.tsx
  • components/auth-header.tsx
  • app/profile/activity/page.tsx
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{tsx} : Use `useSSEConnection` hook from `hooks/useSSEConnection.ts` for real-time sync connection management. The hook automatically handles reconnection, visibility changes, and triggers data refreshes.

Applied to files:

  • hooks/useCalendarSubscriptions.ts
  • hooks/useCalendars.ts
  • hooks/useActivityLogs.ts
  • components/admin/admin-header.tsx
  • hooks/useShifts.ts
  • components/auth-header.tsx
  • hooks/useShiftStats.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to components/**/*.{tsx,ts} : Components in `/components` directory should be `"use client"` by default for interactive UI, and should use shadcn/ui patterns with Radix UI primitives and Lucide icons.

Applied to files:

  • app/admin/logs/page.tsx
  • app/profile/activity/page.tsx
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to hooks/use*Actions.{ts,tsx} : Implement optimistic updates in action hooks by: generating temp ID (`temp-${Date.now()}`), adding item to state immediately, making API call, removing temp item on error with toast, replacing temp item on success.

Applied to files:

  • app/admin/logs/page.tsx
  • hooks/useAdminAuditLogs.ts
  • hooks/useShiftActions.ts
  • hooks/useShifts.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: All user-facing strings must use the `t()` function from `next-intl` for translations. Never use hardcoded English strings in components or API responses.

Applied to files:

  • hooks/useAdminAuditLogs.ts
  • hooks/useActivityLogs.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to app/api/auth/**/*.ts : For Better Auth integration, use built-in methods for authentication operations (`login`, `logout`, `session management`, `OAuth`). Use custom code in `lib/auth/admin.ts` only for permission checks (`canEditUser`, `canDeleteUser`, `canBanUser`).

Applied to files:

  • hooks/useAdminAuditLogs.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to app/api/**/*.ts : After any database mutation (create/update/delete), emit an SSE event using `eventEmitter.emit('calendar-change', {...})` with appropriate type and action to keep clients in sync.

Applied to files:

  • hooks/useCalendars.ts
  • app/page.tsx
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Respect `shift.syncedFromExternal` flag - synced shifts from external calendars cannot be edited or deleted by users. Check this flag before allowing shift mutations.

Applied to files:

  • hooks/useShiftActions.ts
  • hooks/useShifts.ts
  • components/calendar-share-management-sheet.tsx
  • components/dialog-manager.tsx
  • app/page.tsx
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to components/**/*sheet*.{tsx,ts} : Use the Sheet pattern (`components/ui/base-sheet.tsx`) for all forms with side panels, including unsaved changes confirmation via `useDirtyState` hook and consistent header/footer styling with gradient backgrounds.

Applied to files:

  • hooks/useShiftActions.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: When auth is enabled, unauthenticated users (`userId = null`) can access calendars with `guestPermission != "none"`. Use `allowGuestAccess()` function to check if guest access feature is enabled.

Applied to files:

  • components/calendar-share-management-sheet.tsx
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to components/**/*dialog*.{tsx,ts} : Use the Dialog pattern (`components/ui/dialog.tsx` from shadcn/ui) for list views, confirmations, and read-only displays.

Applied to files:

  • components/dialog-manager.tsx
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{ts,tsx} : Store and display dates as `YYYY-MM-DD` strings (local dates without timezone conversion). Use `formatDateToLocal()` from `lib/date-utils.ts` before saving to database and `date-fns` with locale from `getDateLocale()` for display.

Applied to files:

  • app/profile/activity/page.tsx
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{tsx} : Use `getDateLocale(locale)` from `lib/date-utils.ts` when formatting dates with `date-fns` to ensure proper locale-aware date display.

Applied to files:

  • app/profile/activity/page.tsx
  • app/page.tsx
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{ts,tsx} : Use `formatDateToLocal(date)` utility from `lib/date-utils.ts` to convert dates to `YYYY-MM-DD` format before saving to database.

Applied to files:

  • app/profile/activity/page.tsx
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to lib/db/schema.ts : Database schema modifications must be made in `lib/db/schema.ts`. After changes, generate migrations with `npm run db:generate`, review SQL in `drizzle/` folder, then apply with `npm run db:migrate`.

Applied to files:

  • app/page.tsx
🧬 Code graph analysis (11)
hooks/useCalendarTokens.ts (1)
lib/query-keys.ts (1)
  • queryKeys (10-61)
hooks/useCalendarSubscriptions.ts (1)
lib/query-keys.ts (1)
  • queryKeys (10-61)
app/admin/logs/page.tsx (1)
hooks/useAdminAuditLogs.ts (1)
  • useAdminAuditLogs (178-312)
hooks/useAdminAuditLogs.ts (2)
lib/query-keys.ts (1)
  • queryKeys (10-61)
lib/query-client.ts (1)
  • REFETCH_INTERVAL (3-3)
hooks/useCalendars.ts (3)
lib/types.ts (1)
  • CalendarWithCount (4-18)
lib/db/schema.ts (1)
  • calendars (100-124)
lib/query-keys.ts (1)
  • queryKeys (10-61)
hooks/useShiftActions.ts (3)
components/shift-sheet.tsx (1)
  • ShiftFormData (25-34)
lib/types.ts (1)
  • ShiftWithCalendar (20-40)
lib/db/schema.ts (1)
  • shifts (216-249)
hooks/useActivityLogs.ts (2)
lib/query-keys.ts (1)
  • queryKeys (10-61)
lib/query-client.ts (1)
  • REFETCH_INTERVAL (3-3)
components/admin/admin-header.tsx (1)
hooks/useConnectionStatus.ts (1)
  • useConnectionStatus (16-69)
hooks/useShifts.ts (4)
lib/types.ts (1)
  • ShiftWithCalendar (20-40)
components/shift-sheet.tsx (1)
  • ShiftFormData (25-34)
lib/query-keys.ts (1)
  • queryKeys (10-61)
lib/date-utils.ts (1)
  • parseLocalDate (22-77)
components/auth-header.tsx (1)
hooks/useConnectionStatus.ts (1)
  • useConnectionStatus (16-69)
hooks/useShiftStats.ts (2)
lib/query-keys.ts (1)
  • queryKeys (10-61)
lib/query-client.ts (1)
  • REFETCH_INTERVAL (3-3)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-dev
🔇 Additional comments (23)
components/auth-header.tsx (1)

11-11: LGTM! Connection status indicator integrates well.

The integration of useConnectionStatus hook and the visual indicator is clean. The absolute positioning within the logo area works well for this header's layout.

Note: The indicator styling differs slightly from admin-header.tsx (which includes glow shadows). If visual consistency across headers is desired, consider extracting a shared ConnectionStatusIndicator component. This is optional since the different sizes and positioning contexts may justify the variation.

Based on learnings, this correctly replaces the previous useSSEConnection pattern with the new React Query-based connection status.

Also applies to: 26-26, 48-56

components/dialog-manager.tsx (1)

25-25: No changes needed. The optional onPresetsChange prop is properly handled throughout the component chain with appropriate type definitions and guarded invocations.

Likely an incorrect or invalid review comment.

components/calendar-share-management-sheet.tsx (1)

44-51: Clean optimistic/prop derivation.
Good simplification; avoids effect-based syncing and keeps a stable source of truth.

hooks/useCalendarSubscriptions.ts (4)

1-6: LGTM!

Imports are clean and appropriate for the React Query migration.


8-39: LGTM!

Type definitions are well-structured with proper discriminated fields for calendar sources and subscription states.


44-84: LGTM!

API helpers are clean and properly throw errors for failed responses. The error handling correctly extracts server-provided error messages.


119-193: LGTM! Stale closure issue addressed.

The mutation now correctly passes calendarId and name through variables, and onSuccess uses variables.name directly, avoiding the stale closure issue flagged in previous reviews.

hooks/useShifts.ts (4)

1-25: LGTM!

The imports and normalizeShift helper are well-structured. The date parsing correctly prioritizes the YYYY-MM-DD format using parseLocalDate as per coding guidelines.


91-100: LGTM!

The query is properly guarded with enabled: !!calendarId, preventing execution when calendarId is undefined. The non-null assertions are safe in this context.


166-204: LGTM!

The delete mutation correctly implements optimistic updates with rollback, following the same robust pattern as the create mutation.


206-240: LGTM! Previous review concern addressed.

All returned helpers now properly guard against undefined calendarId, preventing invalid cache operations and failed requests. The implementation correctly throws descriptive errors for mutations while silently returning for read-only operations.

hooks/useShiftActions.ts (2)

52-112: LGTM!

The toggle logic correctly handles the shift add/remove behavior with proper locking via togglingDates to prevent rapid duplicate operations. The delegation to useShifts mutations ensures toast notifications are shown to users.


114-120: LGTM!

The hook returns a clean API surface with the essential handlers for shift operations, aligning with the established action hooks pattern.

app/profile/activity/page.tsx (1)

63-152: Filter + debounce wiring looks solid.

Memoized filters/pagination with page resets and a debounced search flow are consistent and avoid stale closures. Nice cleanup of the local UI state.

hooks/useAdminAuditLogs.ts (4)

1-56: LGTM!

Type definitions are well-structured with proper nullability handling. The interfaces align with the coding guidelines for strict TypeScript usage.


57-110: LGTM!

The fetch function properly handles error localization with t("admin.accessDenied") and t("common.fetchError", ...), addressing the previous review feedback. Date parsing and response mapping are correct.


112-157: LGTM!

Delete API functions have consistent error handling with proper i18n support, matching the pattern established in the fetch function.


265-312: LGTM!

The hook's public API is clean and follows the project's data fetching hook patterns. The boolean return values from mutation wrappers work well since errors are already displayed via toast notifications.

app/admin/logs/page.tsx (5)

77-113: LGTM!

The filter, sort, and pagination objects are properly memoized, preventing unnecessary re-renders and query refetches. The hook integration is clean and follows React Query best practices.


122-171: LGTM!

Helper functions are properly memoized with empty dependency arrays, ensuring stable references across renders.


173-207: LGTM!

All filter change handlers properly reset page, selectedLogIds, and expandedRows, preventing bulk actions from targeting stale rows after filter changes.


256-266: LGTM!

The clearFilters function now immediately clears debouncedSearch via handleDebouncedSearchChange(""), preventing the stale debounced value issue identified in the previous review.


713-722: LGTM!

The dialog success handler correctly relies on React Query's automatic cache invalidation instead of manual refetching. The comment clarifies the intended behavior.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread hooks/useActivityLogs.ts
Comment thread hooks/useCalendars.ts
Comment thread hooks/useCalendarSubscriptions.ts
Comment thread hooks/useCalendarTokens.ts
Comment thread hooks/useShifts.ts
Comment thread hooks/useShiftStats.ts

@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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/page.tsx (1)

404-448: Improve consistency by using string-based date comparison for all shift matching.

The code computes dateKey as a local date string but doesn't use it for shift matching, instead relying on isSameDay() with Date objects. For consistency with the toggle-state tracking logic (which correctly uses dateKey), refactor the shift filtering and matching to compare dates as strings:

Suggested fix
-      const dayShifts = shifts.filter(
-        (shift) => shift.date && isSameDay(shift.date as Date, targetDate)
-      );
+      const dayShifts = shifts.filter((shift) => {
+        if (!shift.date) return false;
+        const shiftDateKey =
+          typeof shift.date === "string"
+            ? shift.date
+            : formatDateToLocal(shift.date);
+        return shiftDateKey === dateKey;
+      });

       // ...

-      const existingShift = shifts.find(
-        (shift) =>
-          shift.date &&
-          isSameDay(shift.date as Date, targetDate) &&
-          shift.title === preset.title &&
-          shift.startTime === preset.startTime &&
-          shift.endTime === preset.endTime
-      );
+      const existingShift = shifts.find((shift) => {
+        if (!shift.date) return false;
+        const shiftDateKey =
+          typeof shift.date === "string"
+            ? shift.date
+            : formatDateToLocal(shift.date);
+        return (
+          shiftDateKey === dateKey &&
+          shift.title === preset.title &&
+          shift.startTime === preset.startTime &&
+          shift.endTime === preset.endTime
+        );
+      });

This aligns all date comparisons with the YYYY-MM-DD string pattern already used for toggle-state tracking and ensures consistent handling throughout the compare-mode logic.

🤖 Fix all issues with AI agents
In `@hooks/useActivityLogs.ts`:
- Around line 46-70: The thrown error in fetchActivityLogsApi uses a hardcoded
string; replace it with a localized message by calling the translation function
(e.g., t('activityLogs.fetchError')) instead of "Failed to fetch activity logs",
ensure the file either imports/uses the i18n translator instance or
accepts/receives t as needed, and add the translation key
"activityLogs.fetchError" to your locale files; apply the same replacement for
the other fetch error(s) in this module (the other fetch call that throws on
!response.ok).

In `@hooks/useCalendars.ts`:
- Around line 61-95: The updateCalendarApi and deleteCalendarApi functions lack
the same rate-limit handling as createCalendarApi, so either remove the dead
check in deleteMutation.onError or add consistent 429 handling; update both
updateCalendarApi and deleteCalendarApi to detect a 429 response, read the
Retry-After header (or parse response body if createCalendarApi does), and throw
the same RateLimitError type used by createCalendarApi (including any retry
timestamp/seconds) so deleteMutation.onError can reliably catch RateLimitError;
locate symbols updateCalendarApi, deleteCalendarApi, createCalendarApi and
deleteMutation.onError to implement the change.

In `@hooks/useShifts.ts`:
- Around line 119-132: The optimisticShift created in the add shift flow
(variable optimisticShift of type ShiftWithCalendar) is missing the presetId,
causing preset-dependent UI to flicker; update the optimisticShift object to
include presetId: formData.presetId (or null/undefined when not provided) so the
optimistic payload matches the server shape and UI renders correctly while the
refetch completes.
🧹 Nitpick comments (7)
hooks/useCalendarTokens.ts (1)

57-67: Consider using parseErrorResponse for consistency.

Other API functions (createTokenApi, updateTokenApi, deleteTokenApi) use the parseErrorResponse helper, but fetchTokensApi throws a generic HTTP error. For consistency and better error messaging when fetches fail:

♻️ Suggested change
 async function fetchTokensApi(
   calendarId: string
 ): Promise<CalendarAccessToken[]> {
   const response = await fetch(`/api/calendars/${calendarId}/tokens`);

   if (!response.ok) {
-    throw new Error(`HTTP ${response.status}`);
+    const errorMessage = await parseErrorResponse(response);
+    throw new Error(errorMessage);
   }

   return await response.json();
 }
hooks/useCalendars.ts (2)

231-241: Consider adding RateLimitError check for consistency.

If rate-limit handling is added to updateCalendarApi (as suggested above), the onError handler should also skip the toast for RateLimitError to match createMutation's behavior.


320-335: Consider consistent error handling across wrappers.

createCalendar catches errors and handles RateLimitError specifically, while updateCalendar and deleteCalendar let errors propagate to the caller. This is an asymmetric pattern.

If rate-limit handling is added to the update/delete API functions, these wrappers should follow the same pattern as createCalendar:

♻️ Proposed consistency fix
   const updateCalendar = async (
     calendarId: string,
     updates: {
       name?: string;
       color?: string;
       guestPermission?: "none" | "read" | "write";
     }
   ) => {
-    return updateMutation.mutateAsync({ calendarId, updates });
+    try {
+      await updateMutation.mutateAsync({ calendarId, updates });
+    } catch (error) {
+      if (error instanceof RateLimitError) {
+        await handleRateLimitError(error.response, t);
+      }
+    }
   };

   const deleteCalendar = async (calendarId: string) => {
-    return deleteMutation.mutateAsync(calendarId);
+    try {
+      await deleteMutation.mutateAsync(calendarId);
+    } catch (error) {
+      if (error instanceof RateLimitError) {
+        await handleRateLimitError(error.response, t);
+      }
+    }
   };
hooks/useCalendarSubscriptions.ts (2)

67-70: Minor: JSON parsing could fail on non-JSON error responses.

If the server returns a non-JSON error response (e.g., 500 with HTML), response.json() will throw before you can extract the error message.

♻️ Optional defensive improvement
   if (!response.ok) {
-    const error = await response.json();
-    throw new Error(error.error || "Failed to subscribe");
+    let message = "Failed to subscribe";
+    try {
+      const error = await response.json();
+      message = error.error || message;
+    } catch {
+      // Response wasn't JSON, use default message
+    }
+    throw new Error(message);
   }

272-276: The _calendarName parameter is used by callers and should not be removed without updating all call sites.

The dismiss method is called with two arguments in components/calendar-discovery-sheet.tsx (line 83): await dismiss(calendar.id, calendar.name). Removing the parameter would break this caller.

For consistency with the subscribe method (which accepts and uses calendarName), consider using the _calendarName parameter in the dismiss mutation for optimistic updates or other UI feedback, or document why the signature requires it despite not using the value internally.

hooks/useShifts.ts (1)

204-231: Consider moving mutations into a useShiftActions hook.
Keeping useShifts focused on reads aligns with the existing hook pattern and keeps concerns separated. As per coding guidelines, ...

hooks/useShiftStats.ts (1)

33-50: Consider passing React Query’s AbortSignal into fetch.
This enables request cancellation on unmount/refetch and avoids wasted work.

♻️ Suggested refactor
-async function fetchShiftStatsApi(
-  calendarId: string,
-  period: string,
-  currentDate: Date
-): Promise<ShiftStatsData> {
+async function fetchShiftStatsApi(
+  calendarId: string,
+  period: string,
+  currentDate: Date,
+  signal?: AbortSignal
+): Promise<ShiftStatsData> {
   const params = new URLSearchParams({
     calendarId,
     period,
     date: formatDateToLocal(currentDate),
   });

-  const response = await fetch(`/api/shifts/stats?${params}`);
+  const response = await fetch(`/api/shifts/stats?${params}`, { signal });

@@
-    queryFn: () => fetchShiftStatsApi(calendarId!, period, currentDate),
+    queryFn: ({ signal }) =>
+      fetchShiftStatsApi(calendarId!, period, currentDate, signal),

Also applies to: 78-86

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 343abfa and 94eb222.

📒 Files selected for processing (8)
  • app/page.tsx
  • hooks/useActivityLogs.ts
  • hooks/useAdminStats.ts
  • hooks/useCalendarSubscriptions.ts
  • hooks/useCalendarTokens.ts
  • hooks/useCalendars.ts
  • hooks/useShiftStats.ts
  • hooks/useShifts.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Store and display dates as YYYY-MM-DD strings (local dates without timezone conversion). Use formatDateToLocal() from lib/date-utils.ts before saving to database and date-fns with locale from getDateLocale() for display.
Use Drizzle-inferred types from schema directly (no any types). TypeScript must remain in strict mode with proper type safety throughout the application.
All React files must be in strict mode with TypeScript 5. No any types allowed. Import inferred types from Drizzle schema.
Use formatDateToLocal(date) utility from lib/date-utils.ts to convert dates to YYYY-MM-DD format before saving to database.

Files:

  • hooks/useActivityLogs.ts
  • app/page.tsx
  • hooks/useCalendars.ts
  • hooks/useShifts.ts
  • hooks/useCalendarTokens.ts
  • hooks/useShiftStats.ts
  • hooks/useCalendarSubscriptions.ts
  • hooks/useAdminStats.ts
hooks/use*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Implement data fetching hooks following the pattern: useShifts, usePresets, useNotes, useCalendars with accompanying action hooks like useShiftActions, useNoteActions for CRUD operations with optimistic updates.

Files:

  • hooks/useActivityLogs.ts
  • hooks/useCalendars.ts
  • hooks/useShifts.ts
  • hooks/useCalendarTokens.ts
  • hooks/useShiftStats.ts
  • hooks/useCalendarSubscriptions.ts
  • hooks/useAdminStats.ts
app/**/*.{tsx,ts}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

All components under app/ are Server Components by default unless marked with "use client". Use "use client" directive only for interactive UI components.

Files:

  • app/page.tsx
🧠 Learnings (16)
📓 Common learnings
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{tsx} : Use `useSSEConnection` hook from `hooks/useSSEConnection.ts` for real-time sync connection management. The hook automatically handles reconnection, visibility changes, and triggers data refreshes.
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to hooks/use*.{ts,tsx} : Implement data fetching hooks following the pattern: `useShifts`, `usePresets`, `useNotes`, `useCalendars` with accompanying action hooks like `useShiftActions`, `useNoteActions` for CRUD operations with optimistic updates.
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to app/api/**/*.ts : After any database mutation (create/update/delete), emit an SSE event using `eventEmitter.emit('calendar-change', {...})` with appropriate type and action to keep clients in sync.
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to hooks/use*.{ts,tsx} : Implement data fetching hooks following the pattern: `useShifts`, `usePresets`, `useNotes`, `useCalendars` with accompanying action hooks like `useShiftActions`, `useNoteActions` for CRUD operations with optimistic updates.

Applied to files:

  • hooks/useActivityLogs.ts
  • app/page.tsx
  • hooks/useCalendars.ts
  • hooks/useShifts.ts
  • hooks/useCalendarTokens.ts
  • hooks/useShiftStats.ts
  • hooks/useCalendarSubscriptions.ts
  • hooks/useAdminStats.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{tsx} : Use `useSSEConnection` hook from `hooks/useSSEConnection.ts` for real-time sync connection management. The hook automatically handles reconnection, visibility changes, and triggers data refreshes.

Applied to files:

  • hooks/useActivityLogs.ts
  • hooks/useCalendars.ts
  • hooks/useShifts.ts
  • hooks/useShiftStats.ts
  • hooks/useCalendarSubscriptions.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: All user-facing strings must use the `t()` function from `next-intl` for translations. Never use hardcoded English strings in components or API responses.

Applied to files:

  • hooks/useActivityLogs.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to app/api/**/*.ts : After any database mutation (create/update/delete), emit an SSE event using `eventEmitter.emit('calendar-change', {...})` with appropriate type and action to keep clients in sync.

Applied to files:

  • app/page.tsx
  • hooks/useCalendars.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{tsx} : Use `useCalendarPermission(calendarId)` hook to check UI permissions, which returns `canEdit`, `canDelete`, `canShare`, and `permission` level for displaying conditional UI elements.

Applied to files:

  • app/page.tsx
  • hooks/useCalendars.ts
  • hooks/useShifts.ts
  • hooks/useCalendarTokens.ts
  • hooks/useShiftStats.ts
  • hooks/useCalendarSubscriptions.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to lib/db/schema.ts : Database schema modifications must be made in `lib/db/schema.ts`. After changes, generate migrations with `npm run db:generate`, review SQL in `drizzle/` folder, then apply with `npm run db:migrate`.

Applied to files:

  • app/page.tsx
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to app/api/**/*.ts : For calendar permission hierarchy in SQL queries and permission checks, follow: `owner` (highest) > `admin` > `write` > `read` (lowest). Use `getUserAccessibleCalendars(userId)` to get all accessible calendars.

Applied to files:

  • app/page.tsx
  • hooks/useCalendars.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{tsx} : Use `getDateLocale(locale)` from `lib/date-utils.ts` when formatting dates with `date-fns` to ensure proper locale-aware date display.

Applied to files:

  • app/page.tsx
  • hooks/useShiftStats.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Respect `shift.syncedFromExternal` flag - synced shifts from external calendars cannot be edited or deleted by users. Check this flag before allowing shift mutations.

Applied to files:

  • app/page.tsx
  • hooks/useShifts.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Always check permissions using `checkPermission(userId, calendarId, requiredLevel)` function from `lib/auth/permissions.ts` in both API routes and UI components before allowing mutations.

Applied to files:

  • hooks/useCalendars.ts
  • hooks/useShifts.ts
  • hooks/useCalendarTokens.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to hooks/use*Actions.{ts,tsx} : Implement optimistic updates in action hooks by: generating temp ID (`temp-${Date.now()}`), adding item to state immediately, making API call, removing temp item on error with toast, replacing temp item on success.

Applied to files:

  • hooks/useShifts.ts
  • hooks/useAdminStats.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to app/api/**/*.ts : Use `getUserCalendarPermission(userId, calendarId)` from `lib/auth/permissions.ts` to retrieve the exact permission level, which returns permission string or `null` if user has no access.

Applied to files:

  • hooks/useShifts.ts
  • hooks/useCalendarTokens.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{tsx} : Use Better Auth's built-in client methods (`authClient.listSessions()`, `authClient.revokeOtherSessions()`) for client-side session management. Never implement custom session revocation logic.

Applied to files:

  • hooks/useCalendarTokens.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{ts,tsx} : Store and display dates as `YYYY-MM-DD` strings (local dates without timezone conversion). Use `formatDateToLocal()` from `lib/date-utils.ts` before saving to database and `date-fns` with locale from `getDateLocale()` for display.

Applied to files:

  • hooks/useShiftStats.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{ts,tsx} : Use `formatDateToLocal(date)` utility from `lib/date-utils.ts` to convert dates to `YYYY-MM-DD` format before saving to database.

Applied to files:

  • hooks/useShiftStats.ts
🧬 Code graph analysis (6)
hooks/useActivityLogs.ts (2)
lib/query-keys.ts (1)
  • queryKeys (10-61)
lib/query-client.ts (1)
  • REFETCH_INTERVAL (3-3)
hooks/useCalendars.ts (3)
lib/types.ts (1)
  • CalendarWithCount (4-18)
lib/db/schema.ts (1)
  • calendars (100-124)
lib/query-keys.ts (1)
  • queryKeys (10-61)
hooks/useShifts.ts (4)
lib/types.ts (1)
  • ShiftWithCalendar (20-40)
components/shift-sheet.tsx (1)
  • ShiftFormData (25-34)
lib/query-keys.ts (1)
  • queryKeys (10-61)
lib/date-utils.ts (1)
  • parseLocalDate (22-77)
hooks/useCalendarTokens.ts (2)
lib/query-keys.ts (1)
  • queryKeys (10-61)
lib/query-client.ts (1)
  • REFETCH_INTERVAL (3-3)
hooks/useShiftStats.ts (3)
lib/date-utils.ts (1)
  • formatDateToLocal (5-10)
lib/query-keys.ts (1)
  • queryKeys (10-61)
lib/query-client.ts (1)
  • REFETCH_INTERVAL (3-3)
hooks/useAdminStats.ts (2)
lib/query-keys.ts (1)
  • queryKeys (10-61)
lib/query-client.ts (1)
  • REFETCH_INTERVAL (3-3)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-dev
🔇 Additional comments (35)
hooks/useCalendarTokens.ts (8)

1-8: LGTM!

Imports are well-organized and include the centralized queryKeys and REFETCH_INTERVAL as recommended. This addresses the previous review feedback about explicit refetch interval configuration.


30-52: LGTM!

The parseErrorResponse helper properly handles non-JSON responses with graceful fallbacks. The multi-layer try/catch with HTML detection (line 45) is a good defensive pattern.


69-134: LGTM!

API wrapper functions follow REST conventions with proper error handling using parseErrorResponse. The implementation is consistent and type-safe.


159-169: LGTM!

The useQuery setup is correct. The non-null assertions (calendarId!) are safe because enabled: !!calendarId prevents query execution when the ID is null. The explicit refetchInterval: REFETCH_INTERVAL addresses the previous review feedback and aligns with other polling hooks in the codebase.


191-238: Well-implemented optimistic updates.

The updateTokenMutation correctly implements the React Query optimistic update pattern:

  1. Cancels in-flight queries to prevent overwrites
  2. Stores previous state for rollback
  3. Applies optimistic cache update
  4. Rolls back on error with proper toast notification
  5. Invalidates cache on onSettled for eventual consistency

240-279: LGTM!

The deleteTokenMutation mirrors the update pattern with proper optimistic removal and rollback. The invalidation on onSettled ensures the list is refreshed after the server confirms deletion.


313-343: LGTM!

The wrapper functions provide proper null guards before invoking mutations, preventing runtime errors from the non-null assertions in mutation functions. The empty catch blocks are acceptable since the mutation's onError already handles user feedback via toast.


281-307: LGTM!

The share link helpers are properly memoized with useCallback and include appropriate user feedback via toasts.

hooks/useAdminStats.ts (2)

56-86: Clean fetch + timestamp normalization.
The localized error handling and timestamp conversion are solid and keep the data shape consistent for consumers.


112-131: Nice fix: deduped error toasts.
The useEffect + ref guard prevents repeated toasts during polling/re-renders.

hooks/useCalendars.ts (6)

1-11: LGTM!

Imports are correctly structured for the React Query migration, utilizing centralized query keys and the standard React Query hooks.


97-109: LGTM!

Context types are well-defined for optimistic update rollbacks, with DeleteCalendarContext correctly capturing both cache and selection state.


121-129: Missing polling configuration.

The PR objective is to switch to React Query polling, but no refetchInterval is configured on this query. Without it, data won't automatically refresh and users will only see updates on manual refetch or component remount.

Consider adding polling if real-time-ish updates are expected:

   } = useQuery({
     queryKey: queryKeys.calendars.all,
     queryFn: fetchCalendarsApi,
+    refetchInterval: 30_000, // Poll every 30 seconds
+    staleTime: 10_000,       // Consider data fresh for 10 seconds
   });

Please verify whether polling is intentionally omitted here (relying solely on cache invalidation from mutations) or if a refetchInterval should be added for background refreshes.


141-196: LGTM!

The createMutation correctly implements the optimistic update pattern with proper rollback, duplicate toast prevention for rate-limit errors, and cache invalidation on settle.


250-304: LGTM with caveat.

The deleteMutation correctly implements optimistic delete with selection state rollback. Note the dead RateLimitError check at lines 292-294 (addressed in earlier comment about API function consistency).


337-349: LGTM!

The return object exposes all necessary values and follows the established hook pattern. Using invalidateQueries for refetchCalendars is the idiomatic React Query approach.

hooks/useCalendarSubscriptions.ts (6)

1-7: LGTM!

Clean import structure with proper React Query hooks and centralized query configuration from lib/query-keys and lib/query-client.


107-119: Well-structured query setup.

The query configuration properly includes refetchInterval: REFETCH_INTERVAL for polling, addressing the documentation claim. Default values for availableCalendars and dismissedCalendars ensure safe destructuring when data is undefined.


121-195: Solid subscribe mutation with proper optimistic updates.

The mutation correctly:

  • Uses variables object to pass name alongside calendarId, avoiding the stale closure issue
  • Handles both scenarios: moving calendar from dismissed to available, or marking existing available calendar as subscribed
  • Restores previous state on error
  • Invalidates both subscription and calendar caches on settlement

238-248: Stale closure issue properly addressed.

Using queryClient.getQueryData at runtime resolves the previous concern about stale closures. The lookup correctly searches both available and dismissed arrays, which handles the case where the optimistic update has already moved the calendar to the dismissed list.


260-260: Error type conversion properly implemented.

The return now correctly converts Error to string | null, addressing the previous type mismatch concern.


284-286: Clean refetch implementation.

Using invalidateQueries is the correct React Query pattern for triggering a manual refresh while leveraging the existing query configuration.

hooks/useShifts.ts (5)

28-40: Solid fetch helper with normalization.
Clear error surfacing and normalization keep callers consistent.


42-61: Create helper error reporting looks solid.
Including response text should make failures easier to diagnose.


63-74: DELETE helper is clean and consistent.
The minimal request shape is easy to reason about.


89-98: CalendarId gating prevents premature fetches.
Nice guard to avoid calls before the ID is available.


164-202: Delete flow rollback/invalidation is consistent.
Looks well-scoped and should keep cache state stable on errors.

hooks/useShiftStats.ts (1)

1-6: LGTM on the React Query/date-utils wiring.
Clean imports for query keys, polling interval, and local-date formatting.

app/page.tsx (7)

12-20: LGTM on the compare-mode/query imports.
The additions align with the React Query migration and compare-mode data flow.


93-106: Compare-mode state + data wiring looks solid.
The toggling state and useCompareData gating are clean and readable.

Also applies to: 114-116


147-160: Nice: invalidating compare-mode notes after mutations.
Keeps the compare views consistent after note add/update/delete flows.

Also applies to: 187-190


225-230: Good reset of compare toggling state on exit.
Avoids stale spinner state when leaving compare mode.

Also applies to: 379-383


248-265: LGTM on calendar delete + sync invalidation flow.
Closing the dialog and invalidating calendar/shift caches is consistent.


494-541: LGTM on compare‑mode note lookups.
Using the compare notes map keeps per‑calendar note interactions scoped correctly.


591-663: Compare view wiring and cache invalidation look consistent.
Passing the compare maps + per‑calendar invalidation should keep the UI responsive.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread hooks/useActivityLogs.ts
Comment thread hooks/useCalendars.ts
Comment thread hooks/useShifts.ts

@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: 4

🤖 Fix all issues with AI agents
In `@hooks/useCalendars.ts`:
- Around line 241-251: The updateMutation.onError handler should match
createMutation/deleteMutation by not showing a toast for rate-limit failures:
inside updateMutation.onError (the function that calls queryClient.setQueryData
and toast.error) add a guard that checks if err is an instance of RateLimitError
and if so return early (or at least skip the console.error/toast.error) so the
previous cache restore still happens but no generic error toast is shown; import
or reference the RateLimitError type the other mutations use and place the check
before the console.error/toast.error calls.
- Around line 131-149: The useQuery call for calendars omits an explicit
refetchInterval; import REFETCH_INTERVAL from lib/query-client and add
refetchInterval: REFETCH_INTERVAL to the useQuery options (the call using
queryKey: queryKeys.calendars.all and queryFn: fetchCalendarsApi). Keep the
existing behavior (default data, isLoading, isFetched) and do not change the
useEffect logic that auto-selects via initialCalendarIdRef and
setSelectedCalendar.
- Around line 330-345: The updateCalendar and deleteCalendar wrappers currently
return updateMutation.mutateAsync and deleteMutation.mutateAsync directly and
don't catch RateLimitError like createCalendar does; modify both updateCalendar
and deleteCalendar to wrap the mutateAsync call in a try/catch that catches
errors instanceof RateLimitError and calls handleRateLimitError(error) before
rethrowing (or returning), otherwise rethrow the error so existing onError
handlers still run; reference updateCalendar, deleteCalendar, RateLimitError,
handleRateLimitError, and createCalendar to mirror the error-handling pattern
used there.

In `@hooks/useShifts.ts`:
- Around line 227-232: The deleteShift helper currently calls
deleteMutation.mutateAsync without checking if the shift is externally synced;
update deleteShift to first read the shift from the local cache (e.g., via the
query client / cache used in this hook) using the shiftId and verify the shift's
syncedFromExternal flag, and if true throw an error or return a rejected promise
to block the delete; only call deleteMutation.mutateAsync(shiftId) when
calendarId exists and syncedFromExternal is falsy. Ensure you reference the
existing symbols deleteShift, deleteMutation, calendarId and the
syncedFromExternal property when implementing the guard so synced shifts remain
read-only.
🧹 Nitpick comments (3)
hooks/useActivityLogs.ts (2)

135-140: ESLint disable warrants attention.

The t function is used inside queryFn but excluded from the query key dependencies. While t is typically stable within a locale session, if the user's locale changes at runtime, the query won't automatically refetch with updated translations. If locale changes trigger a full remount, this is fine; otherwise, consider whether this edge case matters for your app.


147-147: Use the centralized query key factory for invalidation.

Line 136 uses queryKeys.activityLogs({ filters, pagination }), but line 147 uses the hardcoded ["activity-logs"]. While prefix-based invalidation works, using the factory is more consistent and maintainable.

♻️ Suggested fix
-      queryClient.invalidateQueries({ queryKey: ["activity-logs"] });
+      queryClient.invalidateQueries({ queryKey: queryKeys.activityLogs() });
hooks/useShiftStats.ts (1)

33-42: Reuse a single date key for query + request to avoid drift.

Computing the formatted date once avoids any mismatch if currentDate mutates between render and fetch, and eliminates duplicate formatting.

♻️ Suggested refactor
-async function fetchShiftStatsApi(
-  calendarId: string,
-  period: string,
-  currentDate: Date
-): Promise<ShiftStatsData> {
-  const params = new URLSearchParams({
-    calendarId,
-    period,
-    date: formatDateToLocal(currentDate),
-  });
+async function fetchShiftStatsApi(
+  calendarId: string,
+  period: string,
+  dateKey: string
+): Promise<ShiftStatsData> {
+  const params = new URLSearchParams({
+    calendarId,
+    period,
+    date: dateKey,
+  });
@@
 export function useShiftStats({
   calendarId,
   currentDate,
   period,
 }: UseShiftStatsOptions) {
+  const dateKey = formatDateToLocal(currentDate);
   const {
     data: stats = null,
     isLoading: loading,
     refetch,
   } = useQuery({
     queryKey: queryKeys.stats.shifts(
       calendarId!,
       period,
-      formatDateToLocal(currentDate)
+      dateKey
     ),
-    queryFn: () => fetchShiftStatsApi(calendarId!, period, currentDate),
+    queryFn: () => fetchShiftStatsApi(calendarId!, period, dateKey),
     enabled: !!calendarId,
     refetchInterval: REFETCH_INTERVAL,
   });

Please verify this aligns with your React Query v5 query-key and request parameter conventions.

Also applies to: 74-93

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 94eb222 and a3091d0.

📒 Files selected for processing (4)
  • hooks/useActivityLogs.ts
  • hooks/useCalendars.ts
  • hooks/useShiftStats.ts
  • hooks/useShifts.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Store and display dates as YYYY-MM-DD strings (local dates without timezone conversion). Use formatDateToLocal() from lib/date-utils.ts before saving to database and date-fns with locale from getDateLocale() for display.
Use Drizzle-inferred types from schema directly (no any types). TypeScript must remain in strict mode with proper type safety throughout the application.
All React files must be in strict mode with TypeScript 5. No any types allowed. Import inferred types from Drizzle schema.
Use formatDateToLocal(date) utility from lib/date-utils.ts to convert dates to YYYY-MM-DD format before saving to database.

Files:

  • hooks/useShiftStats.ts
  • hooks/useActivityLogs.ts
  • hooks/useShifts.ts
  • hooks/useCalendars.ts
hooks/use*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Implement data fetching hooks following the pattern: useShifts, usePresets, useNotes, useCalendars with accompanying action hooks like useShiftActions, useNoteActions for CRUD operations with optimistic updates.

Files:

  • hooks/useShiftStats.ts
  • hooks/useActivityLogs.ts
  • hooks/useShifts.ts
  • hooks/useCalendars.ts
🧠 Learnings (14)
📓 Common learnings
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{tsx} : Use `useSSEConnection` hook from `hooks/useSSEConnection.ts` for real-time sync connection management. The hook automatically handles reconnection, visibility changes, and triggers data refreshes.
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to app/api/**/*.ts : After any database mutation (create/update/delete), emit an SSE event using `eventEmitter.emit('calendar-change', {...})` with appropriate type and action to keep clients in sync.
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to hooks/use*.{ts,tsx} : Implement data fetching hooks following the pattern: `useShifts`, `usePresets`, `useNotes`, `useCalendars` with accompanying action hooks like `useShiftActions`, `useNoteActions` for CRUD operations with optimistic updates.

Applied to files:

  • hooks/useShiftStats.ts
  • hooks/useActivityLogs.ts
  • hooks/useShifts.ts
  • hooks/useCalendars.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{tsx} : Use `useSSEConnection` hook from `hooks/useSSEConnection.ts` for real-time sync connection management. The hook automatically handles reconnection, visibility changes, and triggers data refreshes.

Applied to files:

  • hooks/useShiftStats.ts
  • hooks/useActivityLogs.ts
  • hooks/useShifts.ts
  • hooks/useCalendars.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{ts,tsx} : Store and display dates as `YYYY-MM-DD` strings (local dates without timezone conversion). Use `formatDateToLocal()` from `lib/date-utils.ts` before saving to database and `date-fns` with locale from `getDateLocale()` for display.

Applied to files:

  • hooks/useShiftStats.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{tsx} : Use `getDateLocale(locale)` from `lib/date-utils.ts` when formatting dates with `date-fns` to ensure proper locale-aware date display.

Applied to files:

  • hooks/useShiftStats.ts
  • hooks/useActivityLogs.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{ts,tsx} : Use `formatDateToLocal(date)` utility from `lib/date-utils.ts` to convert dates to `YYYY-MM-DD` format before saving to database.

Applied to files:

  • hooks/useShiftStats.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{tsx} : Use `useCalendarPermission(calendarId)` hook to check UI permissions, which returns `canEdit`, `canDelete`, `canShare`, and `permission` level for displaying conditional UI elements.

Applied to files:

  • hooks/useShiftStats.ts
  • hooks/useShifts.ts
  • hooks/useCalendars.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to hooks/use*Actions.{ts,tsx} : Implement optimistic updates in action hooks by: generating temp ID (`temp-${Date.now()}`), adding item to state immediately, making API call, removing temp item on error with toast, replacing temp item on success.

Applied to files:

  • hooks/useActivityLogs.ts
  • hooks/useShifts.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: All user-facing strings must use the `t()` function from `next-intl` for translations. Never use hardcoded English strings in components or API responses.

Applied to files:

  • hooks/useActivityLogs.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Respect `shift.syncedFromExternal` flag - synced shifts from external calendars cannot be edited or deleted by users. Check this flag before allowing shift mutations.

Applied to files:

  • hooks/useShifts.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Always check permissions using `checkPermission(userId, calendarId, requiredLevel)` function from `lib/auth/permissions.ts` in both API routes and UI components before allowing mutations.

Applied to files:

  • hooks/useShifts.ts
  • hooks/useCalendars.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to app/api/**/*.ts : Use `getUserCalendarPermission(userId, calendarId)` from `lib/auth/permissions.ts` to retrieve the exact permission level, which returns permission string or `null` if user has no access.

Applied to files:

  • hooks/useShifts.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to app/api/**/*.ts : After any database mutation (create/update/delete), emit an SSE event using `eventEmitter.emit('calendar-change', {...})` with appropriate type and action to keep clients in sync.

Applied to files:

  • hooks/useCalendars.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to app/api/**/*.ts : For calendar permission hierarchy in SQL queries and permission checks, follow: `owner` (highest) > `admin` > `write` > `read` (lowest). Use `getUserAccessibleCalendars(userId)` to get all accessible calendars.

Applied to files:

  • hooks/useCalendars.ts
🧬 Code graph analysis (4)
hooks/useShiftStats.ts (3)
lib/date-utils.ts (1)
  • formatDateToLocal (5-10)
lib/query-keys.ts (1)
  • queryKeys (10-61)
lib/query-client.ts (1)
  • REFETCH_INTERVAL (3-3)
hooks/useActivityLogs.ts (2)
lib/query-keys.ts (1)
  • queryKeys (10-61)
lib/query-client.ts (1)
  • REFETCH_INTERVAL (3-3)
hooks/useShifts.ts (4)
lib/types.ts (1)
  • ShiftWithCalendar (20-40)
components/shift-sheet.tsx (1)
  • ShiftFormData (25-34)
lib/query-keys.ts (1)
  • queryKeys (10-61)
lib/date-utils.ts (1)
  • parseLocalDate (22-77)
hooks/useCalendars.ts (3)
lib/types.ts (1)
  • CalendarWithCount (4-18)
lib/db/schema.ts (1)
  • calendars (100-124)
lib/query-keys.ts (1)
  • queryKeys (10-61)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-dev
🔇 Additional comments (16)
hooks/useActivityLogs.ts (4)

12-41: LGTM on type definitions.

The interfaces are well-structured with strict typing. Using string for date fields aligns with the coding guidelines for date handling.


46-87: LGTM with a minor observation on date parsing.

The fetch function properly uses i18n for error messages and avoids any types by using Record<string, unknown>. The date parsing at line 78 assumes timestamp is always a valid date string from the API—this is reasonable given the API contract.


92-102: LGTM!

Error message is properly localized using the translation function.


143-156: Mutation and clearLogs wrapper look good.

The mutation properly handles success with a localized toast and invalidates the cache. The clearLogs wrapper correctly returns a boolean for consumer convenience while the mutation's onError handles user feedback.

Also applies to: 169-176

hooks/useCalendars.ts (6)

1-11: LGTM!

Imports are well-organized and use proper Drizzle-inferred types from @/lib/types as per coding guidelines.


12-22: LGTM!

The fetch function correctly handles errors and defensively ensures array return type. While other API functions include rate-limit checks, omitting it for this read-only GET endpoint is reasonable since rate limits typically target mutations.


24-59: LGTM!

RateLimitError class preserves the response for extracting retry information, and createCalendarApi correctly checks for rate-limits before other error handling. This addresses the previous review feedback.


61-105: LGTM!

Rate-limit handling is now consistent across all mutation API functions, addressing the previous review feedback. The error parsing approaches (JSON for update, text for delete) are appropriate for each endpoint's response format.


151-206: LGTM!

The create mutation correctly implements optimistic updates with proper rollback, and the RateLimitError check in onError prevents duplicate notifications. The onSettled invalidation ensures cache consistency regardless of success/failure.


347-358: LGTM!

The return object exposes a clean API with all necessary data, state flags, and mutation wrappers. Using invalidateQueries for refetchCalendars is the correct React Query pattern for triggering a refetch.

hooks/useShifts.ts (5)

1-7: Imports look good for the React Query migration.


27-74: API helpers are solid and consistent.


85-99: Query enablement is correct.


100-163: Optimistic create flow is consistent.


205-226: Return helpers are safely guarded.

Also applies to: 233-238

hooks/useShiftStats.ts (1)

1-6: Module header looks good.

No issues with the client directive and updated imports.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread hooks/useCalendars.ts
Comment thread hooks/useCalendars.ts
Comment thread hooks/useCalendars.ts
Comment thread hooks/useShifts.ts
@panteLx panteLx merged commit dfee67d into main Jan 16, 2026
3 checks passed
@panteLx panteLx deleted the chore/react-query-polling branch January 16, 2026 18:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feature request: Replace SSE with React Query Polling

2 participants