feat: add sync job cancel and rollback with real-time updates
Implement comprehensive sync job management with rollback capabilities and real-time status updates on the dashboard. ## Features ### Cancel & Rollback - Users can cancel failed or stale (>1h) sync jobs - Rollback deletes added QSOs and restores updated QSOs to previous state - Uses qso_changes table to track all modifications with before/after snapshots - Server-side validation prevents cancelling completed or active jobs ### Database Changes - Add qso_changes table to track QSO modifications per job - Stores change type (added/updated), before/after data snapshots - Enables precise rollback of sync operations - Migration script included ### Real-time Updates - Dashboard now polls for job updates every 2 seconds - Smart polling: starts when jobs active, stops when complete - Job status badges update in real-time (pending → running → completed) - Cancel button appears/disappears based on job state ### Backend - Fixed job ordering to show newest first (desc createdAt) - Track all QSO changes during LoTW/DCL sync operations - cancelJob() function handles rollback logic - DELETE /api/jobs/:jobId endpoint for cancelling jobs ### Frontend - jobsAPI.cancel() method for cancelling jobs - Dashboard shows last 5 sync jobs with status, stats, duration - Real-time job status updates via polling - Cancel button with confirmation dialog - Loading state and error handling ### Logging Fix - Changed from Bun.write() to fs.appendFile() for reliable log appending - Logs now persist across server restarts instead of being truncated Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -181,5 +181,26 @@ export const syncJobs = sqliteTable('sync_jobs', {
|
||||
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
||||
});
|
||||
|
||||
/**
|
||||
* @typedef {Object} QSOChange
|
||||
* @property {number} id
|
||||
* @property {number} jobId
|
||||
* @property {number|null} qsoId
|
||||
* @property {string} changeType - 'added' or 'updated'
|
||||
* @property {string|null} beforeData - JSON snapshot before change (for updates)
|
||||
* @property {string|null} afterData - JSON snapshot after change
|
||||
* @property {Date} createdAt
|
||||
*/
|
||||
|
||||
export const qsoChanges = sqliteTable('qso_changes', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
jobId: integer('job_id').notNull().references(() => syncJobs.id),
|
||||
qsoId: integer('qso_id').references(() => qsos.id), // null for added QSOs until created
|
||||
changeType: text('change_type').notNull(), // 'added' or 'updated'
|
||||
beforeData: text('before_data'), // JSON snapshot before change
|
||||
afterData: text('after_data'), // JSON snapshot after change
|
||||
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
||||
});
|
||||
|
||||
// Export all schemas
|
||||
export const schema = { users, qsos, awards, awardProgress, syncJobs };
|
||||
export const schema = { users, qsos, awards, awardProgress, syncJobs, qsoChanges };
|
||||
|
||||
Reference in New Issue
Block a user