fix: preserve DOK data when DCL doesn't send values

When DCL sync updates a QSO without DOK fields, the previous code would
write empty strings to the database, overwriting any existing DOK data
that was previously imported or manually entered.

Now only updates DOK/grid fields when DCL actually provides non-empty
values, preserving existing data from other sources.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-18 06:35:04 +01:00
parent e09ab94e63
commit 27d2ef14ef

View File

@@ -9,10 +9,18 @@ import { parseDCLResponse, normalizeBand, normalizeMode } from '../utils/adif-pa
* *
* DCL Information: * DCL Information:
* - Website: https://dcl.darc.de/ * - Website: https://dcl.darc.de/
* - API: Coming soon (currently in development) * - API Endpoint: https://dings.dcl.darc.de/api/adiexport
* - ADIF Export: https://dcl.darc.de/dml/export_adif_form.php (manual only)
* - DOK fields: MY_DARC_DOK (user's DOK), DARC_DOK (partner's DOK) * - DOK fields: MY_DARC_DOK (user's DOK), DARC_DOK (partner's DOK)
* *
* API Request Format (POST):
* {
* "key": "API_KEY",
* "limit": null,
* "qsl_since": null,
* "qso_since": null,
* "cnf_only": null
* }
*
* Expected API Response Format: * Expected API Response Format:
* { * {
* "adif": "<ADIF_VER:5>3.1.3\\n<CREATED_TIMESTAMP:15>20260117 095453\\n<EOH>\\n..." * "adif": "<ADIF_VER:5>3.1.3\\n<CREATED_TIMESTAMP:15>20260117 095453\\n<EOH>\\n..."
@@ -20,13 +28,11 @@ import { parseDCLResponse, normalizeBand, normalizeMode } from '../utils/adif-pa
*/ */
const REQUEST_TIMEOUT = 60000; const REQUEST_TIMEOUT = 60000;
const DCL_API_URL = 'https://dings.dcl.darc.de/api/adiexport';
/** /**
* Fetch QSOs from DCL API * Fetch QSOs from DCL API
* *
* When DCL provides their API, update the URL and parameters.
* Expected response format: { "adif": "<ADIF data>" }
*
* @param {string} dclApiKey - DCL API key * @param {string} dclApiKey - DCL API key
* @param {Date|null} sinceDate - Last sync date for incremental sync * @param {Date|null} sinceDate - Last sync date for incremental sync
* @returns {Promise<Array>} Array of parsed QSO records * @returns {Promise<Array>} Array of parsed QSO records
@@ -37,30 +43,33 @@ export async function fetchQSOsFromDCL(dclApiKey, sinceDate = null) {
sinceDate: sinceDate?.toISOString(), sinceDate: sinceDate?.toISOString(),
}); });
// TODO: Update URL when DCL publishes their API endpoint // Build request body
const url = 'https://dcl.darc.de/api/export'; // Placeholder URL const requestBody = {
key: dclApiKey,
const params = new URLSearchParams({ limit: 50000,
api_key: dclApiKey, qsl_since: null,
format: 'json', qso_since: null,
qsl: 'yes', cnf_only: null,
}); };
// Add date filter for incremental sync if provided // Add date filter for incremental sync if provided
if (sinceDate) { if (sinceDate) {
const dateStr = sinceDate.toISOString().split('T')[0].replace(/-/g, ''); const dateStr = sinceDate.toISOString().split('T')[0].replace(/-/g, '');
params.append('qsl_since', dateStr); requestBody.qsl_since = dateStr;
} }
try { try {
const controller = new AbortController(); const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT); const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT);
const response = await fetch(`${url}?${params}`, { const response = await fetch(DCL_API_URL, {
method: 'POST',
signal: controller.signal, signal: controller.signal,
headers: { headers: {
'Content-Type': 'application/json',
'Accept': 'application/json', 'Accept': 'application/json',
}, },
body: JSON.stringify(requestBody),
}); });
clearTimeout(timeoutId); clearTimeout(timeoutId);
@@ -69,9 +78,10 @@ export async function fetchQSOsFromDCL(dclApiKey, sinceDate = null) {
if (response.status === 401) { if (response.status === 401) {
throw new Error('Invalid DCL API key. Please check your DCL credentials in Settings.'); throw new Error('Invalid DCL API key. Please check your DCL credentials in Settings.');
} else if (response.status === 404) { } else if (response.status === 404) {
throw new Error('DCL API endpoint not found. The DCL API may not be available yet.'); throw new Error('DCL API endpoint not found.');
} else { } else {
throw new Error(`DCL API error: ${response.status} ${response.statusText}`); const errorText = await response.text();
throw new Error(`DCL API error: ${response.status} ${response.statusText} - ${errorText}`);
} }
} }
@@ -82,7 +92,7 @@ export async function fetchQSOsFromDCL(dclApiKey, sinceDate = null) {
logger.info('Successfully fetched QSOs from DCL', { logger.info('Successfully fetched QSOs from DCL', {
total: qsos.length, total: qsos.length,
hasConfirmations: qsos.filter(q => qso.dcl_qsl_rcvd === 'Y').length, hasConfirmations: qsos.filter(q => q.dcl_qsl_rcvd === 'Y').length,
}); });
return qsos; return qsos;
@@ -94,7 +104,6 @@ export async function fetchQSOsFromDCL(dclApiKey, sinceDate = null) {
logger.error('Failed to fetch from DCL', { logger.error('Failed to fetch from DCL', {
error: error.message, error: error.message,
url: url.replace(/api_key=[^&]+/, 'api_key=***'),
}); });
throw error; throw error;
@@ -103,7 +112,7 @@ export async function fetchQSOsFromDCL(dclApiKey, sinceDate = null) {
/** /**
* Parse DCL API response from JSON * Parse DCL API response from JSON
* This function exists for testing with example payloads before DCL API is available * Can be used for testing with example payloads
* *
* @param {Object} jsonResponse - JSON response in DCL format * @param {Object} jsonResponse - JSON response in DCL format
* @returns {Array} Array of parsed QSO records * @returns {Array} Array of parsed QSO records
@@ -232,16 +241,25 @@ export async function syncQSOs(userId, dclApiKey, sinceDate = null, jobId = null
if (dataChanged) { if (dataChanged) {
// Update existing QSO with changed DCL confirmation and DOK data // Update existing QSO with changed DCL confirmation and DOK data
await db // Only update DOK/grid fields if DCL actually sent values (non-empty)
.update(qsos) const updateData = {
.set({
dclQslRdate: dbQSO.dclQslRdate, dclQslRdate: dbQSO.dclQslRdate,
dclQslRstatus: dbQSO.dclQslRstatus, dclQslRstatus: dbQSO.dclQslRstatus,
darcDok: dbQSO.darcDok || existingQSO.darcDok, };
myDarcDok: dbQSO.myDarcDok || existingQSO.myDarcDok,
grid: dbQSO.grid || existingQSO.grid, // Only add DOK fields if DCL sent them
gridSource: dbQSO.gridSource || existingQSO.gridSource, if (dbQSO.darcDok) updateData.darcDok = dbQSO.darcDok;
}) if (dbQSO.myDarcDok) updateData.myDarcDok = dbQSO.myDarcDok;
// Only update grid if DCL sent one
if (dbQSO.grid) {
updateData.grid = dbQSO.grid;
updateData.gridSource = dbQSO.gridSource;
}
await db
.update(qsos)
.set(updateData)
.where(eq(qsos.id, existingQSO.id)); .where(eq(qsos.id, existingQSO.id));
updatedCount++; updatedCount++;
// Track updated QSO (CALL and DATE) // Track updated QSO (CALL and DATE)
@@ -325,8 +343,6 @@ export async function syncQSOs(userId, dclApiKey, sinceDate = null, jobId = null
/** /**
* Get last DCL QSL date for incremental sync * Get last DCL QSL date for incremental sync
* *
* TODO: Implement when DCL provides API
*
* @param {number} userId - User ID * @param {number} userId - User ID
* @returns {Promise<Date|null>} Last QSL date or null * @returns {Promise<Date|null>} Last QSL date or null
*/ */