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:
@@ -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'));
|
||||
|
||||
Reference in New Issue
Block a user