diff --git a/src/backend/services/dcl.service.js b/src/backend/services/dcl.service.js index 6f4ed20..42d96c5 100644 --- a/src/backend/services/dcl.service.js +++ b/src/backend/services/dcl.service.js @@ -9,10 +9,18 @@ import { parseDCLResponse, normalizeBand, normalizeMode } from '../utils/adif-pa * * DCL Information: * - Website: https://dcl.darc.de/ - * - API: Coming soon (currently in development) - * - ADIF Export: https://dcl.darc.de/dml/export_adif_form.php (manual only) + * - API Endpoint: https://dings.dcl.darc.de/api/adiexport * - 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: * { * "adif": "3.1.3\\n20260117 095453\\n\\n..." @@ -20,13 +28,11 @@ import { parseDCLResponse, normalizeBand, normalizeMode } from '../utils/adif-pa */ const REQUEST_TIMEOUT = 60000; +const DCL_API_URL = 'https://dings.dcl.darc.de/api/adiexport'; /** * Fetch QSOs from DCL API * - * When DCL provides their API, update the URL and parameters. - * Expected response format: { "adif": "" } - * * @param {string} dclApiKey - DCL API key * @param {Date|null} sinceDate - Last sync date for incremental sync * @returns {Promise} Array of parsed QSO records @@ -37,30 +43,33 @@ export async function fetchQSOsFromDCL(dclApiKey, sinceDate = null) { sinceDate: sinceDate?.toISOString(), }); - // TODO: Update URL when DCL publishes their API endpoint - const url = 'https://dcl.darc.de/api/export'; // Placeholder URL - - const params = new URLSearchParams({ - api_key: dclApiKey, - format: 'json', - qsl: 'yes', - }); + // Build request body + const requestBody = { + key: dclApiKey, + limit: 50000, + qsl_since: null, + qso_since: null, + cnf_only: null, + }; // Add date filter for incremental sync if provided if (sinceDate) { const dateStr = sinceDate.toISOString().split('T')[0].replace(/-/g, ''); - params.append('qsl_since', dateStr); + requestBody.qsl_since = dateStr; } try { const controller = new AbortController(); 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, headers: { + 'Content-Type': 'application/json', 'Accept': 'application/json', }, + body: JSON.stringify(requestBody), }); clearTimeout(timeoutId); @@ -69,9 +78,10 @@ export async function fetchQSOsFromDCL(dclApiKey, sinceDate = null) { if (response.status === 401) { throw new Error('Invalid DCL API key. Please check your DCL credentials in Settings.'); } 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 { - 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', { 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; @@ -94,7 +104,6 @@ export async function fetchQSOsFromDCL(dclApiKey, sinceDate = null) { logger.error('Failed to fetch from DCL', { error: error.message, - url: url.replace(/api_key=[^&]+/, 'api_key=***'), }); throw error; @@ -103,7 +112,7 @@ export async function fetchQSOsFromDCL(dclApiKey, sinceDate = null) { /** * 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 * @returns {Array} Array of parsed QSO records @@ -232,16 +241,25 @@ export async function syncQSOs(userId, dclApiKey, sinceDate = null, jobId = null if (dataChanged) { // 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 .update(qsos) - .set({ - 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, - }) + .set(updateData) .where(eq(qsos.id, existingQSO.id)); updatedCount++; // 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 * - * TODO: Implement when DCL provides API - * * @param {number} userId - User ID * @returns {Promise} Last QSL date or null */