Sync Scheduling with Hourly and Minutely Intervals
Date: November 2, 2025
Status: ✅ COMPLETE - DEPLOYED TO PRODUCTION
Feature: Granular time-based scheduling for sync jobs with production fixes
Overview
The sync scheduling system has been enhanced to support granular time-based scheduling with hourly and minutely intervals, in addition to the existing daily, weekly, and monthly schedules.
Adaptive scheduling: Adjust intervals based on sync duration
Troubleshooting
Sync not running at expected interval
Verify schedule is set correctly in Jobs page
Check that sync job status is "scheduled" (not "cancelled")
Verify rclone service is running
Check application logs for errors
Time picker not appearing
This is expected for interval-based schedules.
Time picker only appears for daily, weekly, monthly schedules.
To use a specific time, switch to daily/weekly/monthly schedule.
Cron expression not generating correctly
Verify schedule format matches expected pattern (e.g., "hourly-6").
Check that interval value is valid (1, 2, 3, 4, 6, 12 for hourly).
Verify timezone is set correctly for time-based schedules.
Production Issues & Fixes (November 2, 2025)
Issue 1: Scheduled Syncs Not Executing in Production
Root Cause:
The cron job endpoints (/api/cron/execute-sync-jobs and /api/cron/execute-backups) used a database query with lte(nextRunAt, now) to find due jobs. However, this query fails to match jobs with NULL nextRunAt values.
In PostgreSQL/Drizzle ORM:
NULL <= any_value returns NULL (not true).
NULL values are excluded from comparison results.
Jobs with NULL nextRunAt would never be picked up by the cron job.
The Fix:
Files Modified:
src/app/api/cron/execute-sync-jobs/route.ts.
src/app/api/cron/execute-backups/route.ts.
Changes:
// BEFORE (BROKEN)const dueJobs =await db
.select().from(syncJobs).where(and(eq(syncJobs.status,"scheduled"),lte(syncJobs.nextRunAt, now)// ❌ Fails for NULL values)).limit(10);// AFTER (FIXED)const dueJobs =await db
.select().from(syncJobs).where(and(eq(syncJobs.status,"scheduled"),or(isNull(syncJobs.nextRunAt),// ✅ Handle NULL valueslte(syncJobs.nextRunAt, now)// ✅ Handle past/current times))).limit(10);
Impact:
Sync jobs with NULL nextRunAt now execute immediately.
Backup jobs with NULL nextRunAt now execute immediately.
Existing jobs with valid nextRunAt values continue to work as before.
Issue 2: Hourly/Minutely Schedules Not Editable
Root Cause:
The EditBackupDialog component's schedule dropdown only showed three options (Daily, Weekly, Monthly). The component's TypeScript type definition supported all interval schedules, but the UI dropdown was hardcoded to only show time-based schedules.
The Fix:
File Modified:
src/components/EditBackupDialog.tsx.
Changes:
Added interval schedule options to dropdown (lines 297-318):
// Only show time picker for time-based schedules{schedule !=="none"&&!schedule.startsWith('hourly-')&&!schedule.startsWith('minutely-')&&(// Time picker UI)}
Added interval schedule info (lines 342-348):
{/* Show info message for interval schedules */}{schedule !=="none"&&(schedule.startsWith('hourly-')|| schedule.startsWith('minutely-'))&&(<p className="text-xs text-muted-foreground">Sync will run {schedule.replace('-',' ')} starting from now
</p>)}
Implementation Date: October 31, 2025
Production Fixes: November 2, 2025
Status: ✅ DEPLOYED TO PRODUCTION
Test Coverage: 45 unit tests + 33 E2E tests + 2 run-once E2E tests (100% pass rate)