feat: Adds configurable auto-sync intervals for iCloud calendars#22
feat: Adds configurable auto-sync intervals for iCloud calendars#22
Conversation
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.
|
Caution Review failedThe pull request is closed. Note Other AI code review bot(s) detectedCodeRabbit 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. WalkthroughAdds configurable automatic iCloud calendar synchronization: new Changes
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (10)
app/api/icloud-syncs/[id]/route.ts (1)
47-55: Validate and normalizeautoSyncIntervalbefore persistingRight now
autoSyncIntervalis 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
autoSyncIntervalto 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 intervalsapp/api/icloud-syncs/route.ts (1)
40-47: Consider validatingautoSyncIntervalbounds.The
autoSyncIntervalis 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 sinceregister()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 withinstrumentation.ts.This module-level side effect duplicates the auto-sync service startup already handled in
instrumentation.ts. WhileautoSyncService.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.tsfor 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
isRunningand optionally agetJobCount()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:
Environment variable:
NEXT_PUBLIC_BASE_URLis intended for client-side exposure. For server-side code, consider using a non-prefixed variable likeBASE_URLorINTERNAL_API_URL.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.tsinto a reusable function inlib/that both the route and service can import directly.
17-21: Consider exposing service state for observability.The
isRunningflag 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 intervalsThe
formAutoSyncIntervalstate 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 default0, 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_INTERVALSin 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 formattingThe 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 likeformatAutoSyncInterval(interval: number): stringand 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
⛔ Files ignored due to path filters (1)
package-lock.jsonis 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}: UsegetCachedPassword(),setCachedPassword(),verifyAndCachePassword(), and related utilities fromlib/password-cache.tsinstead of direct localStorage access for password-protected calendars
Passwords must be SHA-256 hashed using utilities fromlib/password-utils.ts
For date formatting, usedelocale from date-fns for German andenUSlocale for English
Color values must be stored as hex format (e.g.,#3b82f6) and rendered with 20% opacity for backgrounds using format${color}20
UseformatDateToLocal()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.tsxapp/api/icloud-syncs/[id]/route.tslib/db/schema.tscomponents/icloud-sync-manage-dialog.tsxinstrumentation.tslib/auto-sync-service.tsapp/api/icloud-syncs/route.tsapp/api/auto-sync/start/route.ts
**/*.tsx
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.tsx: When password verification fails for a protected calendar, set apendingActionstate and show thePasswordDialogto retry the operation after authentication
UseuseTranslations()from next-intl for all user-facing text and load translations frommessages/{de,en}.json
UseuseRouter().replace()for URL state synchronization instead ofpush()
On mobile UI, implement a separate calendar selector usingshowMobileCalendarDialogstate
Files:
components/ui/slider.tsxcomponents/icloud-sync-manage-dialog.tsx
components/**/*.tsx
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
components/**/*.tsx: Shift dialog hassaveAsPresetenabled 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 whenopenprop 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.tsxcomponents/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.tsapp/api/icloud-syncs/route.tsapp/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.tsapp/api/icloud-syncs/route.tsapp/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.jsonandmessages/en.json
Files:
messages/de.jsonmessages/en.json
lib/db/schema.ts
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
lib/db/schema.ts: After schema changes inlib/db/schema.ts, runnpm run db:generateto generate migration files and thennpm run db:migrateto 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 usingcrypto.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.tsxcomponents/icloud-sync-manage-dialog.tsxpackage.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.jsonmessages/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 forauto_sync_intervalis consistent with schemaColumn 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 appropriateAdding
@radix-ui/react-slideraligns with the newSliderUI 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:autoSyncIntervalcolumn definition is consistent and clearThe new
autoSyncIntervalfield 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 consistentThe new
icloud.autoSyncLabel,icloud.autoSyncHint, andicloud.autoSyncManualkeys clearly describe the feature and align with the German translations. Structure under theicloudnamespace remains consistent.components/ui/slider.tsx (1)
1-28: Slider wrapper matches Radix/shadcn patterns and looks solidThe
SliderforwardRef wrapper around@radix-ui/react-slideris idiomatic, composes classes viacn, 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 keysThe new
icloud.autoSyncLabel,icloud.autoSyncHint, andicloud.autoSyncManualentries 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_intervalcolumn addition.drizzle/meta/0007_snapshot.json (1)
201-208: LGTM!The
auto_sync_intervalcolumn definition correctly matches the schema withintegertype,notNull: true, anddefault: 0.
There was a problem hiding this comment.
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
autoSyncIntervalfield to iCloud syncs table with migration - Implements
AutoSyncServiceclass 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 |
There was a problem hiding this comment.
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_INTERVALSin both the value calculation andonValueChangehandler.
550-559: Unusedcheckedparameter inonCheckedChangehandlers.The checkbox handlers receive a
checkedparameter 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 || 0will coercenull,false, or""to0, bypassing the validation check at line 70 which only runs whenautoSyncInterval !== undefined. Consider using nullish coalescing for consistency:- autoSyncInterval: autoSyncInterval || 0, + autoSyncInterval: autoSyncInterval ?? 0,This ensures only
null/undefineddefault to0, while other invalid falsy values would have been caught by validation (iftypeofchecking were added).app/api/icloud-syncs/[id]/route.ts (1)
94-95: Consider emitting an event whenautoSyncIntervalchanges.The auto-sync service polls the database every 5 minutes to detect interval changes. For faster responsiveness, consider emitting an event when
autoSyncIntervalis 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
syncICloudCalendardirectly instead of via HTTP fetch. However, there's no guard against concurrent execution of the same sync job. IfexecuteSynctakes 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
executeSynccompletes, it callsscheduleJob(syncId, intervalMs, new Date()). However, thelastSyncedAtused 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
loadSyncspoll will reconcile any drift.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 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.tsapp/api/icloud-syncs/[id]/route.tsapp/api/icloud-syncs/[id]/sync/route.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.{ts,tsx}: UsegetCachedPassword(),setCachedPassword(),verifyAndCachePassword(), and related utilities fromlib/password-cache.tsinstead of direct localStorage access for password-protected calendars
Passwords must be SHA-256 hashed using utilities fromlib/password-utils.ts
For date formatting, usedelocale from date-fns for German andenUSlocale for English
Color values must be stored as hex format (e.g.,#3b82f6) and rendered with 20% opacity for backgrounds using format${color}20
UseformatDateToLocal()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.tsapp/api/icloud-syncs/[id]/route.tscomponents/icloud-sync-manage-dialog.tsxapp/api/icloud-syncs/[id]/sync/route.tslib/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.tsapp/api/icloud-syncs/[id]/route.tsapp/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 apendingActionstate and show thePasswordDialogto retry the operation after authentication
UseuseTranslations()from next-intl for all user-facing text and load translations frommessages/{de,en}.json
UseuseRouter().replace()for URL state synchronization instead ofpush()
On mobile UI, implement a separate calendar selector usingshowMobileCalendarDialogstate
Files:
components/icloud-sync-manage-dialog.tsx
components/**/*.tsx
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
components/**/*.tsx: Shift dialog hassaveAsPresetenabled 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 whenopenprop 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
autoSyncIntervalto 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
editingSyncstate.
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} hproduces values like2 hwhich 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
syncICloudCalendarfunction 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
pollIntervalis now stored as a class property and correctly cleared instop(). This addresses the previously flagged memory leak issue.
187-212:triggerSyncproperly 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.
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
Documentation
Localization
✏️ Tip: You can customize this high-level summary in your review settings.