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:
@@ -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
|
||||||
|
// Only update DOK/grid fields if DCL actually sent values (non-empty)
|
||||||
|
const updateData = {
|
||||||
|
dclQslRdate: dbQSO.dclQslRdate,
|
||||||
|
dclQslRstatus: dbQSO.dclQslRstatus,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only add DOK fields if DCL sent them
|
||||||
|
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
|
await db
|
||||||
.update(qsos)
|
.update(qsos)
|
||||||
.set({
|
.set(updateData)
|
||||||
dclQslRdate: dbQSO.dclQslRdate,
|
|
||||||
dclQslRstatus: dbQSO.dclQslRstatus,
|
|
||||||
darcDok: dbQSO.darcDok || existingQSO.darcDok,
|
|
||||||
myDarcDok: dbQSO.myDarcDok || existingQSO.myDarcDok,
|
|
||||||
grid: dbQSO.grid || existingQSO.grid,
|
|
||||||
gridSource: dbQSO.gridSource || existingQSO.gridSource,
|
|
||||||
})
|
|
||||||
.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
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user