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:
@@ -2,7 +2,7 @@ import Database from 'bun:sqlite';
|
||||
import { drizzle } from 'drizzle-orm/bun-sqlite';
|
||||
import * as schema from './db/schema/index.js';
|
||||
import { join, dirname } from 'path';
|
||||
import { existsSync, mkdirSync } from 'fs';
|
||||
import { existsSync, mkdirSync, appendFile } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
// ===================================================================
|
||||
@@ -50,9 +50,9 @@ function log(level, message, data) {
|
||||
|
||||
const logMessage = formatLogMessage(level, message, data);
|
||||
|
||||
// Write to file asynchronously (fire and forget for performance)
|
||||
Bun.write(backendLogFile, logMessage, { createPath: true }).catch(err => {
|
||||
console.error('Failed to write to log file:', err);
|
||||
// Append to file asynchronously (fire and forget for performance)
|
||||
appendFile(backendLogFile, logMessage, (err) => {
|
||||
if (err) console.error('Failed to write to log file:', err);
|
||||
});
|
||||
|
||||
// Also log to console in development
|
||||
@@ -90,9 +90,9 @@ export function logToFrontend(level, message, data = null, context = {}) {
|
||||
|
||||
logMessage += '\n';
|
||||
|
||||
// Write to frontend log file
|
||||
Bun.write(frontendLogFile, logMessage, { createPath: true }).catch(err => {
|
||||
console.error('Failed to write to frontend log file:', err);
|
||||
// Append to frontend log file
|
||||
appendFile(frontendLogFile, logMessage, (err) => {
|
||||
if (err) console.error('Failed to write to frontend log file:', err);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user