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:
@@ -1,5 +1,5 @@
|
||||
import { db, logger } from '../config.js';
|
||||
import { qsos } from '../db/schema/index.js';
|
||||
import { qsos, qsoChanges } from '../db/schema/index.js';
|
||||
import { max, sql, eq, and, or, desc, like } from 'drizzle-orm';
|
||||
import { updateJobProgress } from './job-queue.service.js';
|
||||
import { parseADIF, normalizeBand, normalizeMode } from '../utils/adif-parser.js';
|
||||
@@ -258,6 +258,12 @@ export async function syncQSOs(userId, lotwUsername, lotwPassword, sinceDate = n
|
||||
existingQSO.lotwQslRdate !== dbQSO.lotwQslRdate;
|
||||
|
||||
if (confirmationChanged) {
|
||||
// Record before state for rollback
|
||||
const beforeData = JSON.stringify({
|
||||
lotwQslRstatus: existingQSO.lotwQslRstatus,
|
||||
lotwQslRdate: existingQSO.lotwQslRdate,
|
||||
});
|
||||
|
||||
await db
|
||||
.update(qsos)
|
||||
.set({
|
||||
@@ -266,9 +272,28 @@ export async function syncQSOs(userId, lotwUsername, lotwPassword, sinceDate = n
|
||||
lotwSyncedAt: dbQSO.lotwSyncedAt,
|
||||
})
|
||||
.where(eq(qsos.id, existingQSO.id));
|
||||
|
||||
// Record after state for rollback
|
||||
const afterData = JSON.stringify({
|
||||
lotwQslRstatus: dbQSO.lotwQslRstatus,
|
||||
lotwQslRdate: dbQSO.lotwQslRdate,
|
||||
});
|
||||
|
||||
// Track change in qso_changes table if jobId provided
|
||||
if (jobId) {
|
||||
await db.insert(qsoChanges).values({
|
||||
jobId,
|
||||
qsoId: existingQSO.id,
|
||||
changeType: 'updated',
|
||||
beforeData,
|
||||
afterData,
|
||||
});
|
||||
}
|
||||
|
||||
updatedCount++;
|
||||
// Track updated QSO (CALL and DATE)
|
||||
updatedQSOs.push({
|
||||
id: existingQSO.id,
|
||||
callsign: dbQSO.callsign,
|
||||
date: dbQSO.qsoDate,
|
||||
band: dbQSO.band,
|
||||
@@ -279,10 +304,32 @@ export async function syncQSOs(userId, lotwUsername, lotwPassword, sinceDate = n
|
||||
skippedCount++;
|
||||
}
|
||||
} else {
|
||||
await db.insert(qsos).values(dbQSO);
|
||||
// Insert new QSO
|
||||
const [newQSO] = await db.insert(qsos).values(dbQSO).returning();
|
||||
|
||||
// Track change in qso_changes table if jobId provided
|
||||
if (jobId) {
|
||||
const afterData = JSON.stringify({
|
||||
callsign: dbQSO.callsign,
|
||||
qsoDate: dbQSO.qsoDate,
|
||||
timeOn: dbQSO.timeOn,
|
||||
band: dbQSO.band,
|
||||
mode: dbQSO.mode,
|
||||
});
|
||||
|
||||
await db.insert(qsoChanges).values({
|
||||
jobId,
|
||||
qsoId: newQSO.id,
|
||||
changeType: 'added',
|
||||
beforeData: null,
|
||||
afterData,
|
||||
});
|
||||
}
|
||||
|
||||
addedCount++;
|
||||
// Track added QSO (CALL and DATE)
|
||||
addedQSOs.push({
|
||||
id: newQSO.id,
|
||||
callsign: dbQSO.callsign,
|
||||
date: dbQSO.qsoDate,
|
||||
band: dbQSO.band,
|
||||
|
||||
Reference in New Issue
Block a user