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

@@ -0,0 +1,111 @@
/**
* Migration: Add auto_sync_settings table
*
* This script creates the auto_sync_settings table for managing
* automatic sync intervals for DCL and LoTW services.
* Users can enable/disable auto-sync and configure sync intervals.
*/
import Database from 'bun:sqlite';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
// ES module equivalent of __dirname
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const dbPath = join(__dirname, '../award.db');
const sqlite = new Database(dbPath);
async function migrate() {
console.log('Starting migration: Add auto-sync settings...');
try {
// Check if auto_sync_settings table already exists
const tableExists = sqlite.query(`
SELECT name FROM sqlite_master
WHERE type='table' AND name='auto_sync_settings'
`).get();
if (tableExists) {
console.log('Table auto_sync_settings already exists. Skipping...');
} else {
// Create auto_sync_settings table
sqlite.exec(`
CREATE TABLE auto_sync_settings (
user_id INTEGER PRIMARY KEY,
lotw_enabled INTEGER NOT NULL DEFAULT 0,
lotw_interval_hours INTEGER NOT NULL DEFAULT 24,
lotw_last_sync_at INTEGER,
lotw_next_sync_at INTEGER,
dcl_enabled INTEGER NOT NULL DEFAULT 0,
dcl_interval_hours INTEGER NOT NULL DEFAULT 24,
dcl_last_sync_at INTEGER,
dcl_next_sync_at INTEGER,
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
`);
// Create index for faster queries on next_sync_at
sqlite.exec(`
CREATE INDEX idx_auto_sync_settings_lotw_next_sync_at
ON auto_sync_settings(lotw_next_sync_at)
WHERE lotw_enabled = 1
`);
sqlite.exec(`
CREATE INDEX idx_auto_sync_settings_dcl_next_sync_at
ON auto_sync_settings(dcl_next_sync_at)
WHERE dcl_enabled = 1
`);
console.log('Created auto_sync_settings table with indexes');
}
console.log('Migration complete! Auto-sync settings table added to database.');
} catch (error) {
console.error('Migration failed:', error);
sqlite.close();
process.exit(1);
}
sqlite.close();
}
async function rollback() {
console.log('Starting rollback: Remove auto-sync settings...');
try {
// Drop indexes first
sqlite.exec(`DROP INDEX IF EXISTS idx_auto_sync_settings_lotw_next_sync_at`);
sqlite.exec(`DROP INDEX IF EXISTS idx_auto_sync_settings_dcl_next_sync_at`);
// Drop table
sqlite.exec(`DROP TABLE IF EXISTS auto_sync_settings`);
console.log('Rollback complete! Auto-sync settings table removed from database.');
} catch (error) {
console.error('Rollback failed:', error);
sqlite.close();
process.exit(1);
}
sqlite.close();
}
// Check if this is a rollback
const args = process.argv.slice(2);
if (args.includes('--rollback') || args.includes('-r')) {
rollback().then(() => {
console.log('Rollback script completed successfully');
process.exit(0);
});
} else {
// Run migration
migrate().then(() => {
console.log('Migration script completed successfully');
process.exit(0);
});
}