Skip to content

feat: Adds configurable auto-sync intervals for iCloud calendars#22

Merged
panteLx merged 3 commits intomainfrom
feat/icloud-auto-sync
Dec 4, 2025
Merged

feat: Adds configurable auto-sync intervals for iCloud calendars#22
panteLx merged 3 commits intomainfrom
feat/icloud-auto-sync

Conversation

@panteLx
Copy link
Owner

@panteLx panteLx commented Dec 4, 2025

Enables users to configure automatic synchronization for iCloud calendars at customizable intervals ranging from manual to 24 hours. Users can select sync frequencies (5, 15, 30 minutes, 1, 2, 6, 12, or 24 hours) through an intuitive slider interface.

Implements auto-sync interval storage in the database and updates API endpoints to support the new configuration. Enhances the calendar management dialog to display sync status badges and provide interval configuration controls alongside existing visibility settings.

Improves user experience by reducing manual sync actions for frequently updated calendars while maintaining granular control over sync behavior.

Summary by CodeRabbit

  • New Features

    • Added per-calendar auto-sync intervals (Manual, presets from 5 min up to 24h) with badges indicating auto/manual state.
    • Background service performs automatic syncs and supports manual triggering from the UI.
    • UI add/edit panel includes auto-sync controls and optimistic visibility toggles.
  • Documentation

    • README updated to describe the global auto-sync behavior and interval options.
  • Localization

    • Added German and English labels and hints for auto-sync.

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

Enables users to configure automatic synchronization for iCloud calendars at customizable intervals ranging from manual to 24 hours. Users can select sync frequencies (5, 15, 30 minutes, 1, 2, 6, 12, or 24 hours) through an intuitive slider interface.

Implements auto-sync interval storage in the database and updates API endpoints to support the new configuration. Enhances the calendar management dialog to display sync status badges and provide interval configuration controls alongside existing visibility settings.

Improves user experience by reducing manual sync actions for frequently updated calendars while maintaining granular control over sync behavior.
Copilot AI review requested due to automatic review settings December 4, 2025 20:24
@coderabbitai
Copy link

coderabbitai bot commented Dec 4, 2025

Caution

Review failed

The pull request is closed.

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

Adds configurable automatic iCloud calendar synchronization: new autoSyncInterval field (minutes, 0 = manual), DB migration, API validation (POST/PATCH), UI slider & badges, a background AutoSyncService with startup instrumentation, and extraction of sync logic into syncICloudCalendar.

Changes

Cohort / File(s) Summary
Database Schema & Migrations
lib/db/schema.ts, drizzle/0007_flippant_betty_brant.sql, drizzle/meta/0007_snapshot.json, drizzle/meta/_journal.json
Adds auto_sync_interval (integer, not null, default 0) to icloud_syncs; includes SQL migration, snapshot, and journal entry.
API Route Handlers
app/api/icloud-syncs/route.ts, app/api/icloud-syncs/[id]/route.ts, app/api/icloud-syncs/[id]/sync/route.ts
POST/PATCH accept and validate autoSyncInterval; default to 0 on create; PATCH updates it. Extracts reusable syncICloudCalendar(syncId) and POST sync route now delegates to it and returns enhanced stats (includes calendarId).
Auto-Sync Background Service
lib/auto-sync-service.ts, instrumentation.ts
New AutoSyncService singleton that loads active syncs, schedules timers, runs syncICloudCalendar, emits calendar-change events, supports manual trigger/reschedule, and is started by server instrumentation on Node.js runtime.
UI Components
components/icloud-sync-manage-dialog.tsx, components/ui/slider.tsx
Adds Slider component (Radix wrapper) and integrates interval selection in the icloud-sync manage dialog; shows auto-sync badges, resets/initializes form state, includes optimistic visibility toggle updates.
Localization
messages/en.json, messages/de.json
Adds i18n keys for auto-sync labels, hint, manual label, and 24h short form.
Dependencies & Docs
package.json, README.md
Adds @radix-ui/react-slider dependency; README updated to describe global interval-based sync behavior.

Sequence Diagram

sequenceDiagram
  participant Server as Server Startup
  participant Inst as Instrumentation
  participant ASS as AutoSyncService
  participant DB as Database
  participant Timer as Scheduler
  participant SyncFn as syncICloudCalendar
  participant ICloud as iCloud API
  participant EventBus as Event Bus

  Server->>Inst: check NEXT_RUNTIME
  Inst->>ASS: import & start()
  ASS->>DB: load active syncs (autoSyncInterval > 0)
  DB-->>ASS: sync configs
  ASS->>Timer: schedule timers per sync
  Timer->>ASS: timer fires
  ASS->>SyncFn: execute syncICloudCalendar(syncId)
  SyncFn->>DB: fetch sync config
  DB-->>SyncFn: config + URL
  SyncFn->>ICloud: fetch ICS
  ICloud-->>SyncFn: ICS data
  SyncFn->>SyncFn: parse & expand events
  SyncFn->>DB: begin transaction -> insert/update/delete shifts -> update lastSyncedAt -> commit
  DB-->>SyncFn: stats
  SyncFn-->>ASS: return stats
  ASS->>EventBus: emit calendar-change
  ASS->>Timer: reschedule next run
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • Pay attention to: lib/auto-sync-service.ts (scheduling, timers, rescheduling semantics, error handling, concurrency).
  • components/icloud-sync-manage-dialog.tsx (form state, reset paths, optimistic UI updates).
  • API validation consistency between POST and PATCH and allowed interval presets.
  • Migration/snapshot consistency and default handling for existing records.

Possibly related PRs

Poem

🐇 I nibble timers, tick by tick,
I hop and fetch each calendar pick.
Minutes set, the syncs now hum,
From cloud to DB my whiskers drum.
Hooray — automatic syncs have come! 🎚️

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ 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 accurately describes the main feature being added: configurable auto-sync intervals for iCloud calendars, which aligns with the core changes across all modified files.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 787b3de and b19bd13.

📒 Files selected for processing (1)
  • app/api/icloud-syncs/[id]/sync/route.ts (1 hunks)

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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (10)
app/api/icloud-syncs/[id]/route.ts (1)

47-55: Validate and normalize autoSyncInterval before persisting

Right now autoSyncInterval is taken directly from the JSON body and written to the DB if present, without any type/range checks. Given it controls background scheduling and the DB column is an integer, it would be safer to coerce and validate this value and reject invalid input with a 400.

You could do something like:

-    const {
-      name,
-      icloudUrl,
-      color,
-      displayMode,
-      isHidden,
-      hideFromStats,
-      autoSyncInterval,
-    } = body;
+    const {
+      name,
+      icloudUrl,
+      color,
+      displayMode,
+      isHidden,
+      hideFromStats,
+      autoSyncInterval: rawAutoSyncInterval,
+    } = body;
@@
-    if (hideFromStats !== undefined) updateData.hideFromStats = hideFromStats;
-    if (autoSyncInterval !== undefined)
-      updateData.autoSyncInterval = autoSyncInterval;
+    if (hideFromStats !== undefined) updateData.hideFromStats = hideFromStats;
+
+    const autoSyncInterval =
+      rawAutoSyncInterval === undefined
+        ? undefined
+        : Number(rawAutoSyncInterval);
+
+    if (autoSyncInterval !== undefined) {
+      if (!Number.isFinite(autoSyncInterval) || autoSyncInterval < 0) {
+        return NextResponse.json(
+          { error: "Invalid autoSyncInterval" },
+          { status: 400 }
+        );
+      }
+      updateData.autoSyncInterval = autoSyncInterval;
+    }

Optionally, you might further restrict autoSyncInterval to your supported preset values (5, 15, 30, 60, 120, 360, 720, 1440, or 0 for manual) and return 400 for anything else, to keep DB state consistent with what the UI offers.

Also applies to: 78-79

README.md (1)

39-39: Minor copy tweak: brand casing for “iCloud”

The updated bullet correctly describes the new configurable auto-sync behavior. To keep branding consistent with the rest of the docs and UI, consider adjusting the casing:

-- **ICloud Sync**: Automatically or manually synchronize multiple iCloud calendars at configurable intervals
+- **iCloud Sync**: Automatically or manually synchronize multiple iCloud calendars at configurable intervals
app/api/icloud-syncs/route.ts (1)

40-47: Consider validating autoSyncInterval bounds.

The autoSyncInterval is accepted without validation. Per the PR objectives, valid intervals are 0 (manual), 5, 15, 30, 60, 120, 360, 720, or 1440 minutes. Invalid values could lead to unexpected behavior in the auto-sync service (e.g., negative values or very small intervals causing excessive sync requests).

     const {
       calendarId,
       name,
       icloudUrl,
       color,
       displayMode,
       autoSyncInterval,
     } = body;
 
     if (!calendarId || !name || !icloudUrl) {
       return NextResponse.json(
         { error: "Calendar ID, name, and iCloud URL are required" },
         { status: 400 }
       );
     }
+
+    // Validate autoSyncInterval if provided
+    const validIntervals = [0, 5, 15, 30, 60, 120, 360, 720, 1440];
+    const interval = autoSyncInterval ?? 0;
+    if (!validIntervals.includes(interval)) {
+      return NextResponse.json(
+        { error: "Invalid auto-sync interval" },
+        { status: 400 }
+      );
+    }

Also applies to: 76-76

instrumentation.ts (1)

6-11: Add error handling for service startup.

If autoSyncService.start() throws, the error will propagate as an unhandled rejection since register() is async. Consider wrapping with try-catch to prevent server startup issues.

 export async function register() {
   if (process.env.NEXT_RUNTIME === "nodejs") {
     // Import and start the auto-sync service
-    const { autoSyncService } = await import("@/lib/auto-sync-service");
-    await autoSyncService.start();
+    try {
+      const { autoSyncService } = await import("@/lib/auto-sync-service");
+      await autoSyncService.start();
+    } catch (error) {
+      console.error("Failed to start auto-sync service:", error);
+    }
   }
 }
app/api/auto-sync/start/route.ts (2)

8-14: Redundant service startup with instrumentation.ts.

This module-level side effect duplicates the auto-sync service startup already handled in instrumentation.ts. While autoSyncService.start() has an internal guard preventing double-starts, this redundancy adds confusion. Module-level side effects also re-execute on hot reloading during development.

Consider removing the module-level startup and relying solely on instrumentation.ts for service initialization.

-// Start the service immediately when this module is imported
-if (typeof window === "undefined") {
-  // Only run on server side
-  autoSyncService.start().catch((error) => {
-    console.error("Failed to start auto-sync service:", error);
-  });
-}

16-18: Status endpoint returns static response regardless of actual state.

The GET handler returns "Auto-sync service is running" unconditionally without checking the actual service state. This could be misleading if the service failed to start.

Consider exposing the service's running state:

 export async function GET() {
-  return Response.json({ status: "Auto-sync service is running" });
+  return Response.json({ 
+    status: autoSyncService.isRunning ? "running" : "stopped",
+    jobCount: autoSyncService.getJobCount?.() ?? 0
+  });
 }

This would require exposing isRunning and optionally a getJobCount() method from the service.

lib/auto-sync-service.ts (2)

154-163: Reconsider HTTP self-call and environment variable usage.

Two concerns with this approach:

  1. Environment variable: NEXT_PUBLIC_BASE_URL is intended for client-side exposure. For server-side code, consider using a non-prefixed variable like BASE_URL or INTERNAL_API_URL.

  2. Self HTTP call: Calling your own API via HTTP adds network overhead and complexity. Consider extracting the sync logic into a shared function that both the API route and this service can call directly.

If keeping the HTTP approach, use a server-specific env var:

       const response = await fetch(
         `${
-          process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000"
+          process.env.INTERNAL_BASE_URL || process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000"
         }/api/icloud-syncs/${syncId}/sync`,

Alternatively, refactor the sync logic in app/api/icloud-syncs/[id]/sync/route.ts into a reusable function in lib/ that both the route and service can import directly.


17-21: Consider exposing service state for observability.

The isRunning flag and job count are private, making it difficult to monitor the service state (as noted in the status endpoint review). Consider adding public getters.

 class AutoSyncService {
   private jobs: Map<string, SyncJob> = new Map();
   private timers: Map<string, NodeJS.Timeout> = new Map();
-  private isRunning = false;
+  private _isRunning = false;
+
+  get isRunning(): boolean {
+    return this._isRunning;
+  }
+
+  getJobCount(): number {
+    return this.jobs.size;
+  }

Also applies to: 244-245

components/icloud-sync-manage-dialog.tsx (2)

23-23: Auto-sync state wiring looks consistent; consider centralizing allowed intervals

The formAutoSyncInterval state is correctly initialized, reset in all close/cancel/success paths, and propagated to the POST/PATCH payloads, so behavior should be consistent across create/edit flows. However, the set of allowed intervals is hard-coded in multiple places (state default 0, payload, edit initialization, etc.). To avoid future drift, consider extracting a shared constant (e.g. AUTO_SYNC_INTERVALS) and using it wherever intervals are referenced.

+const AUTO_SYNC_INTERVALS = [0, 5, 15, 30, 60, 120, 360, 720, 1440] as const;
+type AutoSyncInterval = (typeof AUTO_SYNC_INTERVALS)[number];
...
-const [formAutoSyncInterval, setFormAutoSyncInterval] = useState(0);
+const [formAutoSyncInterval, setFormAutoSyncInterval] =
+  useState<AutoSyncInterval>(0);

Then reuse AUTO_SYNC_INTERVALS in the slider and any interval-related logic below.

Also applies to: 57-57, 94-94, 129-130, 139-140, 171-172, 181-182, 263-264, 273-274, 311-312, 606-607


344-357: Interval formatting duplicated; extract helper and align unit formatting

The interval → label logic is duplicated in the list badge (Lines 344–351) and the form label (Lines 489–495), with slightly different spacing ("min"/"h" vs " min"/" h"). Consider extracting a small helper like formatAutoSyncInterval(interval: number): string and using it in both places for consistent formatting and easier maintenance.

+const formatAutoSyncInterval = (interval: number, manualLabel: string) => {
+  if (interval === 0) return manualLabel;
+  if (interval < 60) return `${interval} min`;
+  if (interval < 1440) return `${interval / 60} h`;
+  return `${interval / 1440} d`;
+};
...
-                        {sync.autoSyncInterval > 0 ? (
+                        {sync.autoSyncInterval > 0 ? (
                           <span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-md bg-primary/10 text-primary text-xs font-medium">
                             <RefreshCw className="h-3 w-3" />
-                            {sync.autoSyncInterval < 60
-                              ? `${sync.autoSyncInterval}min`
-                              : sync.autoSyncInterval < 1440
-                              ? `${sync.autoSyncInterval / 60}h`
-                              : `${sync.autoSyncInterval / 1440}d`}
+                            {formatAutoSyncInterval(
+                              sync.autoSyncInterval,
+                              t("icloud.autoSyncManual")
+                            )}
                           </span>
                         ) : (
...
-                    {formAutoSyncInterval === 0
-                      ? t("icloud.autoSyncManual")
-                      : formAutoSyncInterval < 60
-                      ? `${formAutoSyncInterval} min`
-                      : formAutoSyncInterval < 1440
-                      ? `${formAutoSyncInterval / 60} h`
-                      : `${formAutoSyncInterval / 1440} d`}
+                    {formatAutoSyncInterval(
+                      formAutoSyncInterval,
+                      t("icloud.autoSyncManual")
+                    )}

Also applies to: 489-495

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 67205e5 and f18ef50.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (15)
  • README.md (1 hunks)
  • app/api/auto-sync/start/route.ts (1 hunks)
  • app/api/icloud-syncs/[id]/route.ts (2 hunks)
  • app/api/icloud-syncs/route.ts (2 hunks)
  • components/icloud-sync-manage-dialog.tsx (13 hunks)
  • components/ui/slider.tsx (1 hunks)
  • drizzle/0007_flippant_betty_brant.sql (1 hunks)
  • drizzle/meta/0007_snapshot.json (1 hunks)
  • drizzle/meta/_journal.json (1 hunks)
  • instrumentation.ts (1 hunks)
  • lib/auto-sync-service.ts (1 hunks)
  • lib/db/schema.ts (1 hunks)
  • messages/de.json (1 hunks)
  • messages/en.json (1 hunks)
  • package.json (1 hunks)
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{ts,tsx}

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

**/*.{ts,tsx}: Use getCachedPassword(), setCachedPassword(), verifyAndCachePassword(), and related utilities from lib/password-cache.ts instead of direct localStorage access for password-protected calendars
Passwords must be SHA-256 hashed using utilities from lib/password-utils.ts
For date formatting, use de locale from date-fns for German and enUS locale for English
Color values must be stored as hex format (e.g., #3b82f6) and rendered with 20% opacity for backgrounds using format ${color}20
Use formatDateToLocal() utility function to format dates in YYYY-MM-DD format
All code, comments, variable names, and messages must be in English
Add comments only for complex logic or non-obvious behavior; prefer self-documenting code with clear variable and function names

Files:

  • components/ui/slider.tsx
  • app/api/icloud-syncs/[id]/route.ts
  • lib/db/schema.ts
  • components/icloud-sync-manage-dialog.tsx
  • instrumentation.ts
  • lib/auto-sync-service.ts
  • app/api/icloud-syncs/route.ts
  • app/api/auto-sync/start/route.ts
**/*.tsx

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

**/*.tsx: When password verification fails for a protected calendar, set a pendingAction state and show the PasswordDialog to retry the operation after authentication
Use useTranslations() from next-intl for all user-facing text and load translations from messages/{de,en}.json
Use useRouter().replace() for URL state synchronization instead of push()
On mobile UI, implement a separate calendar selector using showMobileCalendarDialog state

Files:

  • components/ui/slider.tsx
  • components/icloud-sync-manage-dialog.tsx
components/**/*.tsx

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

components/**/*.tsx: Shift dialog has saveAsPreset enabled by default - consider this behavior when implementing shift creation features
Dialog components should control state via props (open, onOpenChange, onSubmit, onDelete) and reset local state when open prop changes to false
Use shadcn/ui Dialog component as the base for all new dialog components
Calendar left-click toggles shift with selected preset; right-click opens note dialog (prevent default context menu); toggle logic deletes if exists, creates if not
Use <StickyNote> icon component as an indicator for days with notes
Forms should prevent default submission, validate input, and use callback pattern to communicate with parent component

Files:

  • components/ui/slider.tsx
  • components/icloud-sync-manage-dialog.tsx
app/api/**/route.ts

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

API routes must include proper validation for required query/path parameters and return 400 status with error message if missing

Files:

  • app/api/icloud-syncs/[id]/route.ts
  • app/api/icloud-syncs/route.ts
  • app/api/auto-sync/start/route.ts
app/api/**/*.ts

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

All API errors should be logged with console.error() for debugging in production

Files:

  • app/api/icloud-syncs/[id]/route.ts
  • app/api/icloud-syncs/route.ts
  • app/api/auto-sync/start/route.ts
messages/{de,en}.json

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

All translations for new features must be added to both messages/de.json and messages/en.json

Files:

  • messages/de.json
  • messages/en.json
lib/db/schema.ts

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

lib/db/schema.ts: After schema changes in lib/db/schema.ts, run npm run db:generate to generate migration files and then npm run db:migrate to apply migrations
Database timestamps must be stored as integers with { mode: 'timestamp' } in schema and will be auto-converted to Date objects by Drizzle ORM
Calendar IDs must be generated using crypto.randomUUID()

Files:

  • lib/db/schema.ts
🧠 Learnings (11)
📚 Learning: 2025-12-04T19:12:37.197Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-04T19:12:37.197Z
Learning: Applies to components/**/*.tsx : Use shadcn/ui Dialog component as the base for all new dialog components

Applied to files:

  • components/ui/slider.tsx
  • components/icloud-sync-manage-dialog.tsx
  • package.json
📚 Learning: 2025-12-04T19:12:37.197Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-04T19:12:37.197Z
Learning: Applies to messages/{de,en}.json : All translations for new features must be added to both `messages/de.json` and `messages/en.json`

Applied to files:

  • messages/de.json
  • messages/en.json
📚 Learning: 2025-12-04T19:12:37.197Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-04T19:12:37.197Z
Learning: Applies to lib/db/schema.ts : After schema changes in `lib/db/schema.ts`, run `npm run db:generate` to generate migration files and then `npm run db:migrate` to apply migrations

Applied to files:

  • lib/db/schema.ts
📚 Learning: 2025-12-04T19:12:37.197Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-04T19:12:37.197Z
Learning: Applies to lib/db/schema.ts : Database timestamps must be stored as integers with `{ mode: 'timestamp' }` in schema and will be auto-converted to Date objects by Drizzle ORM

Applied to files:

  • lib/db/schema.ts
📚 Learning: 2025-12-04T19:12:37.197Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-04T19:12:37.197Z
Learning: When adding a new database table, define it in `lib/db/schema.ts`, export its type with `$inferSelect`, run migrations, and create corresponding API routes

Applied to files:

  • lib/db/schema.ts
📚 Learning: 2025-12-04T19:12:37.197Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-04T19:12:37.197Z
Learning: Applies to lib/db/schema.ts : Calendar IDs must be generated using `crypto.randomUUID()`

Applied to files:

  • lib/db/schema.ts
📚 Learning: 2025-12-04T19:12:37.197Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-04T19:12:37.197Z
Learning: Applies to components/**/*.tsx : Shift dialog has `saveAsPreset` enabled by default - consider this behavior when implementing shift creation features

Applied to files:

  • components/icloud-sync-manage-dialog.tsx
📚 Learning: 2025-12-04T19:12:37.197Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-04T19:12:37.197Z
Learning: When creating a new component with a dialog, create it in `components/`, use shadcn/ui Dialog, accept props for `open`, `onOpenChange`, `onSubmit`, and optional `onDelete`, and use `useTranslations()` for all text

Applied to files:

  • components/icloud-sync-manage-dialog.tsx
📚 Learning: 2025-12-04T19:12:37.197Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-04T19:12:37.197Z
Learning: Applies to app/page.tsx : Main page state management should use `useState` for shifts, presets, notes, and calendars, and `useEffect` for data fetching on calendar/date changes

Applied to files:

  • components/icloud-sync-manage-dialog.tsx
📚 Learning: 2025-12-04T19:12:37.197Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-04T19:12:37.197Z
Learning: Applies to components/**/*.tsx : Calendar left-click toggles shift with selected preset; right-click opens note dialog (prevent default context menu); toggle logic deletes if exists, creates if not

Applied to files:

  • components/icloud-sync-manage-dialog.tsx
📚 Learning: 2025-12-04T19:12:37.197Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-04T19:12:37.197Z
Learning: Applies to **/*.{ts,tsx} : Use `getCachedPassword()`, `setCachedPassword()`, `verifyAndCachePassword()`, and related utilities from `lib/password-cache.ts` instead of direct localStorage access for password-protected calendars

Applied to files:

  • lib/auto-sync-service.ts
🧬 Code graph analysis (7)
components/ui/slider.tsx (1)
lib/utils.ts (1)
  • cn (4-6)
app/api/icloud-syncs/[id]/route.ts (3)
app/api/shifts/[id]/route.ts (1)
  • PATCH (54-138)
app/api/calendars/[id]/route.ts (1)
  • PATCH (43-113)
app/api/presets/[id]/route.ts (1)
  • PATCH (9-107)
README.md (1)
app/api/icloud-syncs/[id]/sync/route.ts (1)
  • POST (8-222)
instrumentation.ts (1)
lib/auto-sync-service.ts (1)
  • autoSyncService (245-245)
lib/auto-sync-service.ts (4)
lib/db/index.ts (1)
  • db (19-19)
lib/db/schema.ts (1)
  • icloudSyncs (20-43)
lib/event-emitter.ts (1)
  • eventEmitter (38-38)
app/api/icloud-syncs/[id]/sync/route.ts (1)
  • POST (8-222)
app/api/icloud-syncs/route.ts (1)
app/api/icloud-syncs/[id]/sync/route.ts (2)
  • POST (8-222)
  • tx (172-209)
app/api/auto-sync/start/route.ts (1)
lib/auto-sync-service.ts (1)
  • autoSyncService (245-245)
⏰ 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). (2)
  • GitHub Check: Agent
  • GitHub Check: build-dev
🔇 Additional comments (8)
drizzle/0007_flippant_betty_brant.sql (1)

1-1: Migration for auto_sync_interval is consistent with schema

Column type, default, and nullability match the schema definition and support the “0 = manual, otherwise minutes” convention. No issues from a migration standpoint.

package.json (1)

23-23: Radix slider dependency addition looks appropriate

Adding @radix-ui/react-slider aligns with the new Slider UI wrapper and keeps Radix components consistent across the project. Please just confirm this version is officially compatible with your current React/Next versions.

lib/db/schema.ts (1)

35-35: autoSyncInterval column definition is consistent and clear

The new autoSyncInterval field matches the migration (int, notNull, default 0) and the inline comment documents its semantics well. Timestamps and IDs still follow the existing Drizzle patterns.

messages/en.json (1)

215-218: English auto-sync i18n keys are well-phrased and consistent

The new icloud.autoSyncLabel, icloud.autoSyncHint, and icloud.autoSyncManual keys clearly describe the feature and align with the German translations. Structure under the icloud namespace remains consistent.

components/ui/slider.tsx (1)

1-28: Slider wrapper matches Radix/shadcn patterns and looks solid

The Slider forwardRef wrapper around @radix-ui/react-slider is idiomatic, composes classes via cn, and preserves focus/disabled states. This should integrate cleanly wherever you use Radix-based UI components.

messages/de.json (1)

215-218: German auto-sync translations align with English keys

The new icloud.autoSyncLabel, icloud.autoSyncHint, and icloud.autoSyncManual entries correctly mirror the English meanings and keep the iCloud section structured. No issues found.

drizzle/meta/_journal.json (1)

53-60: LGTM!

Migration journal entry correctly tracks the new schema version for the auto_sync_interval column addition.

drizzle/meta/0007_snapshot.json (1)

201-208: LGTM!

The auto_sync_interval column definition correctly matches the schema with integer type, notNull: true, and default: 0.

Copy link

Copilot AI left a comment

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 introduces configurable auto-sync intervals for iCloud calendars, allowing users to set automatic synchronization at predefined intervals (5min to 24h) or keep manual sync. The feature includes database schema updates, a new background service for automated syncing, and UI enhancements with a slider control for interval selection.

Key Changes:

  • Adds autoSyncInterval field to iCloud syncs table with migration
  • Implements AutoSyncService class for background calendar synchronization
  • Adds slider UI component for interval configuration (0, 5, 15, 30, 60, 120, 360, 720, 1440 minutes)

Reviewed changes

Copilot reviewed 15 out of 16 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
lib/db/schema.ts Adds autoSyncInterval integer field with default value 0 (manual)
drizzle/0007_flippant_betty_brant.sql Migration to add auto_sync_interval column
drizzle/meta/_journal.json Migration journal entry for schema version 7
drizzle/meta/0007_snapshot.json Complete database schema snapshot including new field
lib/auto-sync-service.ts New background service managing auto-sync timers and executing syncs
instrumentation.ts Starts auto-sync service on server initialization
app/api/auto-sync/start/route.ts API endpoint for auto-sync service status (redundant with instrumentation)
app/api/icloud-syncs/route.ts POST endpoint updated to accept autoSyncInterval parameter
app/api/icloud-syncs/[id]/route.ts PATCH endpoint updated to support autoSyncInterval updates
components/ui/slider.tsx New shadcn/ui slider component for interval selection
components/icloud-sync-manage-dialog.tsx UI updates with slider control, sync badges, and reorganized visibility toggles
messages/en.json English translations for auto-sync labels
messages/de.json German translations for auto-sync labels
package.json Adds @radix-ui/react-slider dependency
package-lock.json Lock file updates for new slider dependency
README.md Updates iCloud sync feature description

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
components/icloud-sync-manage-dialog.tsx (2)

522-533: Extract interval array to a constant to avoid duplication.

The intervals array [0, 5, 15, 30, 60, 120, 360, 720, 1440] is defined twice within the Slider component (lines 525 and 531). Extract it to a constant at the component or module level for maintainability and consistency with the backend validation.

+const SYNC_INTERVALS = [0, 5, 15, 30, 60, 120, 360, 720, 1440] as const;
+
 export function ICloudSyncManageDialog({

Then use SYNC_INTERVALS in both the value calculation and onValueChange handler.


550-559: Unused checked parameter in onCheckedChange handlers.

The checkbox handlers receive a checked parameter that isn't used. For clarity, either remove it or use it directly:

-                      onCheckedChange={() => {
+                      onCheckedChange={(_checked) => {
                         handleToggleVisibility(
                           editingSync.id,
                           "isHidden",
                           editingSync.isHidden || false
                         );
                       }}

Or simply use arrow function without parameter: onCheckedChange={() => handleToggleVisibility(...)}

Also applies to: 580-586

🧹 Nitpick comments (4)
app/api/icloud-syncs/route.ts (1)

92-92: Minor: Falsy value handling could differ from validation.

Using autoSyncInterval || 0 will coerce null, false, or "" to 0, bypassing the validation check at line 70 which only runs when autoSyncInterval !== undefined. Consider using nullish coalescing for consistency:

-        autoSyncInterval: autoSyncInterval || 0,
+        autoSyncInterval: autoSyncInterval ?? 0,

This ensures only null/undefined default to 0, while other invalid falsy values would have been caught by validation (if typeof checking were added).

app/api/icloud-syncs/[id]/route.ts (1)

94-95: Consider emitting an event when autoSyncInterval changes.

The auto-sync service polls the database every 5 minutes to detect interval changes. For faster responsiveness, consider emitting an event when autoSyncInterval is updated, similar to how visibility changes trigger events at lines 130-137. This would allow the auto-sync service to immediately reschedule jobs.

+    // Emit event to notify auto-sync service about interval changes
+    if (autoSyncInterval !== undefined) {
+      eventEmitter.emit("calendar-change", {
+        type: "icloud-sync",
+        action: "update",
+        calendarId: updated.calendarId,
+        data: { icloudSyncId: id, autoSyncIntervalUpdated: true },
+      });
+    }

Alternatively, this can be deferred if the 5-minute polling latency is acceptable.

lib/auto-sync-service.ts (2)

145-169: Direct function call avoids HTTP fetch issues; consider concurrency guard.

Good improvement: calling syncICloudCalendar directly instead of via HTTP fetch. However, there's no guard against concurrent execution of the same sync job. If executeSync takes longer than expected (e.g., large calendar, slow network), the next scheduled run could start before the current one completes.

Consider adding an "in-progress" guard:

+  private inProgress: Set<string> = new Set();
+
   private async executeSync(syncId: string, intervalMs: number) {
+    if (this.inProgress.has(syncId)) {
+      console.log(`Sync ${syncId} already in progress, skipping`);
+      return;
+    }
+    
+    this.inProgress.add(syncId);
     console.log(`Executing auto-sync for ${syncId}`);
 
     try {
       // Call sync function directly instead of HTTP fetch
       const stats = await syncICloudCalendar(syncId);
       // ... rest of code
     } catch (error) {
       console.error(`Auto-sync error for ${syncId}:`, error);
+    } finally {
+      this.inProgress.delete(syncId);
     }
 
     // Schedule next sync
     this.scheduleJob(syncId, intervalMs, new Date());
   }

167-168: Sync rescheduled before database transaction commit is guaranteed.

After executeSync completes, it calls scheduleJob(syncId, intervalMs, new Date()). However, the lastSyncedAt used for scheduling should ideally come from the database to ensure consistency. If there's any discrepancy (e.g., clock drift, transaction timing), the schedule could drift over time.

This is a minor concern since the 5-minute loadSyncs poll will reconcile any drift.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f18ef50 and 787b3de.

📒 Files selected for processing (7)
  • app/api/icloud-syncs/[id]/route.ts (3 hunks)
  • app/api/icloud-syncs/[id]/sync/route.ts (1 hunks)
  • app/api/icloud-syncs/route.ts (3 hunks)
  • components/icloud-sync-manage-dialog.tsx (14 hunks)
  • lib/auto-sync-service.ts (1 hunks)
  • messages/de.json (1 hunks)
  • messages/en.json (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • messages/en.json
  • messages/de.json
🧰 Additional context used
📓 Path-based instructions (5)
app/api/**/route.ts

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

API routes must include proper validation for required query/path parameters and return 400 status with error message if missing

Files:

  • app/api/icloud-syncs/route.ts
  • app/api/icloud-syncs/[id]/route.ts
  • app/api/icloud-syncs/[id]/sync/route.ts
**/*.{ts,tsx}

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

**/*.{ts,tsx}: Use getCachedPassword(), setCachedPassword(), verifyAndCachePassword(), and related utilities from lib/password-cache.ts instead of direct localStorage access for password-protected calendars
Passwords must be SHA-256 hashed using utilities from lib/password-utils.ts
For date formatting, use de locale from date-fns for German and enUS locale for English
Color values must be stored as hex format (e.g., #3b82f6) and rendered with 20% opacity for backgrounds using format ${color}20
Use formatDateToLocal() utility function to format dates in YYYY-MM-DD format
All code, comments, variable names, and messages must be in English
Add comments only for complex logic or non-obvious behavior; prefer self-documenting code with clear variable and function names

Files:

  • app/api/icloud-syncs/route.ts
  • app/api/icloud-syncs/[id]/route.ts
  • components/icloud-sync-manage-dialog.tsx
  • app/api/icloud-syncs/[id]/sync/route.ts
  • lib/auto-sync-service.ts
app/api/**/*.ts

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

All API errors should be logged with console.error() for debugging in production

Files:

  • app/api/icloud-syncs/route.ts
  • app/api/icloud-syncs/[id]/route.ts
  • app/api/icloud-syncs/[id]/sync/route.ts
**/*.tsx

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

**/*.tsx: When password verification fails for a protected calendar, set a pendingAction state and show the PasswordDialog to retry the operation after authentication
Use useTranslations() from next-intl for all user-facing text and load translations from messages/{de,en}.json
Use useRouter().replace() for URL state synchronization instead of push()
On mobile UI, implement a separate calendar selector using showMobileCalendarDialog state

Files:

  • components/icloud-sync-manage-dialog.tsx
components/**/*.tsx

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

components/**/*.tsx: Shift dialog has saveAsPreset enabled by default - consider this behavior when implementing shift creation features
Dialog components should control state via props (open, onOpenChange, onSubmit, onDelete) and reset local state when open prop changes to false
Use shadcn/ui Dialog component as the base for all new dialog components
Calendar left-click toggles shift with selected preset; right-click opens note dialog (prevent default context menu); toggle logic deletes if exists, creates if not
Use <StickyNote> icon component as an indicator for days with notes
Forms should prevent default submission, validate input, and use callback pattern to communicate with parent component

Files:

  • components/icloud-sync-manage-dialog.tsx
🧠 Learnings (7)
📚 Learning: 2025-12-04T19:12:37.197Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-04T19:12:37.197Z
Learning: Applies to components/**/*.tsx : Shift dialog has `saveAsPreset` enabled by default - consider this behavior when implementing shift creation features

Applied to files:

  • components/icloud-sync-manage-dialog.tsx
📚 Learning: 2025-12-04T19:12:37.197Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-04T19:12:37.197Z
Learning: Applies to **/*.tsx : On mobile UI, implement a separate calendar selector using `showMobileCalendarDialog` state

Applied to files:

  • components/icloud-sync-manage-dialog.tsx
📚 Learning: 2025-12-04T19:12:37.197Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-04T19:12:37.197Z
Learning: Applies to components/**/*.tsx : Dialog components should control state via props (`open`, `onOpenChange`, `onSubmit`, `onDelete`) and reset local state when `open` prop changes to false

Applied to files:

  • components/icloud-sync-manage-dialog.tsx
📚 Learning: 2025-12-04T19:12:37.197Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-04T19:12:37.197Z
Learning: Applies to app/page.tsx : Main page state management should use `useState` for shifts, presets, notes, and calendars, and `useEffect` for data fetching on calendar/date changes

Applied to files:

  • components/icloud-sync-manage-dialog.tsx
📚 Learning: 2025-12-04T19:12:37.197Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-04T19:12:37.197Z
Learning: Applies to components/**/*.tsx : Use shadcn/ui Dialog component as the base for all new dialog components

Applied to files:

  • components/icloud-sync-manage-dialog.tsx
📚 Learning: 2025-12-04T19:12:37.197Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-04T19:12:37.197Z
Learning: When creating a new component with a dialog, create it in `components/`, use shadcn/ui Dialog, accept props for `open`, `onOpenChange`, `onSubmit`, and optional `onDelete`, and use `useTranslations()` for all text

Applied to files:

  • components/icloud-sync-manage-dialog.tsx
📚 Learning: 2025-12-04T19:12:37.197Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-04T19:12:37.197Z
Learning: Applies to components/**/*.tsx : Calendar left-click toggles shift with selected preset; right-click opens note dialog (prevent default context menu); toggle logic deletes if exists, creates if not

Applied to files:

  • components/icloud-sync-manage-dialog.tsx
🧬 Code graph analysis (1)
lib/auto-sync-service.ts (4)
lib/db/index.ts (1)
  • db (19-19)
lib/db/schema.ts (1)
  • icloudSyncs (20-43)
app/api/icloud-syncs/[id]/sync/route.ts (1)
  • syncICloudCalendar (11-200)
lib/event-emitter.ts (1)
  • eventEmitter (38-38)
⏰ 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 (7)
app/api/icloud-syncs/route.ts (1)

67-81: Validation properly implemented.

The validation logic correctly restricts autoSyncInterval to the allowed values and returns a helpful error message with the valid options.

app/api/icloud-syncs/[id]/route.ts (1)

68-82: Validation properly implemented.

The validation correctly mirrors the POST endpoint logic, ensuring consistent behavior across create and update operations.

components/icloud-sync-manage-dialog.tsx (2)

281-324: Optimistic update with rollback properly implemented.

The visibility toggle now provides immediate UI feedback via optimistic updates and correctly reverts the state on error. This addresses the previous concern about stale editingSync state.


368-376: Display format inconsistent with slider label.

The badge displays intervals without spaces (5 min, 2 h) while the slider label at lines 513-519 uses the same format. However, the pattern ${sync.autoSyncInterval / 60} h produces values like 2 h which is good. Just noting consistency is maintained here.

app/api/icloud-syncs/[id]/sync/route.ts (1)

11-200: Well-structured extraction of sync logic for reusability.

The syncICloudCalendar function is cleanly extracted to support both the API route and the auto-sync service. The transaction ensures atomicity of batch operations. Good separation of concerns.

lib/auto-sync-service.ts (2)

18-22: Memory leak properly addressed.

The pollInterval is now stored as a class property and correctly cleared in stop(). This addresses the previously flagged memory leak issue.


187-212: triggerSync properly reschedules after manual trigger.

The manual trigger correctly executes the sync and reschedules based on the new sync time, ensuring the automatic schedule resets appropriately.

@panteLx panteLx linked an issue Dec 4, 2025 that may be closed by this pull request
@panteLx panteLx merged commit b74d01d into main Dec 4, 2025
1 of 2 checks passed
@panteLx panteLx deleted the feat/icloud-auto-sync branch December 4, 2025 21:00
@coderabbitai coderabbitai bot mentioned this pull request Dec 27, 2025
6 tasks
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.

Auto sync iCloud Calendars

1 participant