feat: implement auto-sync scheduler for LoTW and DCL

Add automatic synchronization scheduler that allows users to configure
periodic sync intervals for LoTW and DCL via the settings page.

Features:
- Users can enable/disable auto-sync per service (LoTW/DCL)
- Configurable sync intervals (1-720 hours)
- Settings page UI for managing auto-sync preferences
- Dashboard shows upcoming scheduled auto-sync jobs
- Scheduler runs every minute, triggers syncs when due
- Survives server restarts via database persistence
- Graceful shutdown support (SIGINT/SIGTERM)

Backend:
- New autoSyncSettings table with user preferences
- auto-sync.service.js for CRUD operations and scheduling logic
- scheduler.service.js for periodic tick processing
- API endpoints: GET/PUT /auto-sync/settings, GET /auto-sync/scheduler/status

Frontend:
- Auto-sync settings section in settings page
- Upcoming auto-sync section on dashboard with scheduled job cards
- Purple-themed UI for scheduled jobs with countdown animation

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-01-22 12:40:55 +01:00
parent cce520a00e
commit 648cf2c5a5
8 changed files with 1313 additions and 3 deletions

View File

@@ -43,6 +43,16 @@ import {
getAwardProgressDetails,
getAwardEntityBreakdown,
} from './services/awards.service.js';
import {
getAutoSyncSettings,
updateAutoSyncSettings,
} from './services/auto-sync.service.js';
import {
startScheduler,
stopScheduler,
getSchedulerStatus,
triggerSchedulerTick,
} from './services/scheduler.service.js';
/**
* Main backend application
@@ -1398,6 +1408,133 @@ const app = new Elysia()
}
})
/**
* ================================================================
* AUTO-SYNC SETTINGS ROUTES
* ================================================================
* All auto-sync routes require authentication
*/
/**
* GET /api/auto-sync/settings
* Get user's auto-sync settings (requires authentication)
*/
.get('/api/auto-sync/settings', async ({ user, set }) => {
if (!user) {
set.status = 401;
return { success: false, error: 'Unauthorized' };
}
try {
const settings = await getAutoSyncSettings(user.id);
return {
success: true,
settings,
};
} catch (error) {
logger.error('Error fetching auto-sync settings', { error: error.message, userId: user.id });
set.status = 500;
return {
success: false,
error: 'Failed to fetch auto-sync settings',
};
}
})
/**
* PUT /api/auto-sync/settings
* Update user's auto-sync settings (requires authentication)
*/
.put(
'/api/auto-sync/settings',
async ({ user, body, set }) => {
if (!user) {
set.status = 401;
return { success: false, error: 'Unauthorized' };
}
try {
const settings = await updateAutoSyncSettings(user.id, body);
return {
success: true,
settings,
message: 'Auto-sync settings updated successfully',
};
} catch (error) {
logger.error('Error updating auto-sync settings', { error: error.message, userId: user.id });
set.status = 400;
return {
success: false,
error: error.message,
};
}
},
{
body: t.Object({
lotwEnabled: t.Optional(t.Boolean()),
lotwIntervalHours: t.Optional(t.Number()),
dclEnabled: t.Optional(t.Boolean()),
dclIntervalHours: t.Optional(t.Number()),
}),
}
)
/**
* GET /api/auto-sync/scheduler/status
* Get scheduler status (admin only)
*/
.get('/api/auto-sync/scheduler/status', async ({ user, set }) => {
if (!user || !user.isAdmin) {
set.status = !user ? 401 : 403;
return { success: false, error: !user ? 'Unauthorized' : 'Admin access required' };
}
try {
const status = getSchedulerStatus();
return {
success: true,
scheduler: status,
};
} catch (error) {
logger.error('Error fetching scheduler status', { error: error.message, userId: user.id });
set.status = 500;
return {
success: false,
error: 'Failed to fetch scheduler status',
};
}
})
/**
* POST /api/auto-sync/scheduler/trigger
* Manually trigger scheduler tick (admin only, for testing)
*/
.post('/api/auto-sync/scheduler/trigger', async ({ user, set }) => {
if (!user || !user.isAdmin) {
set.status = !user ? 401 : 403;
return { success: false, error: !user ? 'Unauthorized' : 'Admin access required' };
}
try {
await triggerSchedulerTick();
return {
success: true,
message: 'Scheduler tick triggered successfully',
};
} catch (error) {
logger.error('Error triggering scheduler tick', { error: error.message, userId: user.id });
set.status = 500;
return {
success: false,
error: 'Failed to trigger scheduler tick',
};
}
})
// Serve static files and SPA fallback for all non-API routes
.get('/*', ({ request }) => {
const url = new URL(request.url);
@@ -1554,3 +1691,21 @@ logger.info('Server started', {
nodeEnv: process.env.NODE_ENV || 'unknown',
logLevel: LOG_LEVEL,
});
// Start the auto-sync scheduler
startScheduler();
// Graceful shutdown handlers
const gracefulShutdown = async (signal) => {
logger.info(`Received ${signal}, shutting down gracefully...`);
// Stop the scheduler
await stopScheduler();
logger.info('Graceful shutdown complete');
process.exit(0);
};
// Handle shutdown signals
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));