feat: prepare database and UI for DCL integration
Add infrastructure for future DARC Community Logbook (DCL) integration: - Database schema: Add dcl_api_key, my_darc_dok, darc_dok, dcl_qsl_rdate, dcl_qsl_rstatus fields - Create DCL service stub with placeholder functions for when DCL provides API - Backend API: Add /api/auth/dcl-credentials endpoint for API key management - Frontend settings: Add DCL API key input with informational notice about API availability - QSO table: Add My DOK and DOK columns, update confirmation column for multiple services Note: DCL download API is not yet available. These changes prepare the application for future implementation when DCL adds programmatic access. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
214
src/backend/services/dcl.service.js
Normal file
214
src/backend/services/dcl.service.js
Normal file
@@ -0,0 +1,214 @@
|
||||
import { db, logger } from '../config.js';
|
||||
import { qsos } from '../db/schema/index.js';
|
||||
import { max, sql, eq, and, desc } from 'drizzle-orm';
|
||||
import { updateJobProgress } from './job-queue.service.js';
|
||||
|
||||
/**
|
||||
* DCL (DARC Community Logbook) Service
|
||||
*
|
||||
* NOTE: DCL does not currently have a public API for downloading QSOs.
|
||||
* This service is prepared as a stub for when DCL adds API support.
|
||||
*
|
||||
* When DCL provides an API, implement:
|
||||
* - fetchQSOsFromDCL() - Download QSOs from DCL
|
||||
* - syncQSOs() - Sync QSOs to database
|
||||
* - getLastDCLQSLDate() - Get last QSL date for incremental sync
|
||||
*
|
||||
* DCL Information:
|
||||
* - Website: https://dcl.darc.de/
|
||||
* - 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)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fetch QSOs from DCL
|
||||
*
|
||||
* TODO: Implement when DCL provides a download API
|
||||
* Expected implementation:
|
||||
* - Use DCL API key for authentication
|
||||
* - Fetch ADIF data with confirmations
|
||||
* - Parse and return QSO records
|
||||
*
|
||||
* @param {string} dclApiKey - DCL API key
|
||||
* @param {Date|null} sinceDate - Last sync date for incremental sync
|
||||
* @returns {Promise<Array>} Array of parsed QSO records
|
||||
*/
|
||||
export async function fetchQSOsFromDCL(dclApiKey, sinceDate = null) {
|
||||
logger.info('DCL sync not yet implemented - API endpoint not available', {
|
||||
sinceDate: sinceDate?.toISOString(),
|
||||
});
|
||||
|
||||
throw new Error('DCL download API is not yet available. DCL does not currently provide a public API for downloading QSOs. Use the manual ADIF export at https://dcl.darc.de/dml/export_adif_form.php');
|
||||
|
||||
/*
|
||||
* FUTURE IMPLEMENTATION (when DCL provides API):
|
||||
*
|
||||
* const url = 'https://dcl.darc.de/api/...'; // TBA
|
||||
*
|
||||
* const params = new URLSearchParams({
|
||||
* api_key: dclApiKey,
|
||||
* format: 'adif',
|
||||
* qsl: 'yes',
|
||||
* });
|
||||
*
|
||||
* if (sinceDate) {
|
||||
* const dateStr = sinceDate.toISOString().split('T')[0].replace(/-/g, '');
|
||||
* params.append('qso_qslsince', dateStr);
|
||||
* }
|
||||
*
|
||||
* const response = await fetch(`${url}?${params}`, {
|
||||
* headers: {
|
||||
* 'Accept': 'text/plain',
|
||||
* },
|
||||
* timeout: REQUEST_TIMEOUT,
|
||||
* });
|
||||
*
|
||||
* if (!response.ok) {
|
||||
* throw new Error(`DCL API error: ${response.status}`);
|
||||
* }
|
||||
*
|
||||
* const adifData = await response.text();
|
||||
* return parseADIF(adifData);
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse ADIF data from DCL
|
||||
*
|
||||
* TODO: Implement ADIF parser for DCL format
|
||||
* Should handle DCL-specific fields:
|
||||
* - MY_DARC_DOK
|
||||
* - DARC_DOK
|
||||
*
|
||||
* @param {string} adifData - Raw ADIF data
|
||||
* @returns {Array} Array of parsed QSO records
|
||||
*/
|
||||
function parseADIF(adifData) {
|
||||
// TODO: Implement ADIF parser
|
||||
// Should parse standard ADIF fields plus DCL-specific fields:
|
||||
// - MY_DARC_DOK (user's own DOK)
|
||||
// - DARC_DOK (QSO partner's DOK)
|
||||
// - QSL_DATE (confirmation date from DCL)
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync QSOs from DCL to database
|
||||
*
|
||||
* TODO: Implement when DCL provides API
|
||||
*
|
||||
* @param {number} userId - User ID
|
||||
* @param {string} dclApiKey - DCL API key
|
||||
* @param {Date|null} sinceDate - Last sync date
|
||||
* @param {number|null} jobId - Job ID for progress tracking
|
||||
* @returns {Promise<Object>} Sync results
|
||||
*/
|
||||
export async function syncQSOs(userId, dclApiKey, sinceDate = null, jobId = null) {
|
||||
logger.info('DCL sync not yet implemented', { userId, sinceDate, jobId });
|
||||
|
||||
throw new Error('DCL download API is not yet available');
|
||||
|
||||
/*
|
||||
* FUTURE IMPLEMENTATION:
|
||||
*
|
||||
* try {
|
||||
* const adifQSOs = await fetchQSOsFromDCL(dclApiKey, sinceDate);
|
||||
*
|
||||
* let addedCount = 0;
|
||||
* let updatedCount = 0;
|
||||
* let errors = [];
|
||||
*
|
||||
* for (const adifQSO of adifQSOs) {
|
||||
* try {
|
||||
* // Map ADIF fields to database schema
|
||||
* const qsoData = mapADIFToDB(adifQSO);
|
||||
*
|
||||
* // Check if QSO already exists
|
||||
* const existing = await db.select()
|
||||
* .from(qsos)
|
||||
* .where(
|
||||
* and(
|
||||
* eq(qsos.userId, userId),
|
||||
* eq(qsos.callsign, adifQSO.call),
|
||||
* eq(qsos.qsoDate, adifQSO.qso_date),
|
||||
* eq(qsos.timeOn, adifQSO.time_on)
|
||||
* )
|
||||
* )
|
||||
* .limit(1);
|
||||
*
|
||||
* if (existing.length > 0) {
|
||||
* // Update existing QSO with DCL confirmation
|
||||
* await db.update(qsos)
|
||||
* .set({
|
||||
* dclQslRdate: adifQSO.qslrdate || null,
|
||||
* dclQslRstatus: adifQSO.qslrdate ? 'Y' : 'N',
|
||||
* darcDok: adifQSO.darc_dok || null,
|
||||
* myDarcDok: adifQSO.my_darc_dok || null,
|
||||
* })
|
||||
* .where(eq(qsos.id, existing[0].id));
|
||||
* updatedCount++;
|
||||
* } else {
|
||||
* // Insert new QSO
|
||||
* await db.insert(qsos).values({
|
||||
* userId,
|
||||
* ...qsoData,
|
||||
* dclQslRdate: adifQSO.qslrdate || null,
|
||||
* dclQslRstatus: adifQSO.qslrdate ? 'Y' : 'N',
|
||||
* });
|
||||
* addedCount++;
|
||||
* }
|
||||
* } catch (err) {
|
||||
* logger.error('Failed to process QSO', { error: err.message, qso: adifQSO });
|
||||
* errors.push(err.message);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* const result = {
|
||||
* success: true,
|
||||
* total: adifQSOs.length,
|
||||
* added: addedCount,
|
||||
* updated: updatedCount,
|
||||
* errors,
|
||||
* };
|
||||
*
|
||||
* logger.info('DCL sync completed', { ...result, jobId });
|
||||
* return result;
|
||||
*
|
||||
* } catch (error) {
|
||||
* logger.error('DCL sync failed', { error: error.message, userId, jobId });
|
||||
* return { success: false, error: error.message, total: 0, added: 0, updated: 0 };
|
||||
* }
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last DCL QSL date for incremental sync
|
||||
*
|
||||
* TODO: Implement when DCL provides API
|
||||
*
|
||||
* @param {number} userId - User ID
|
||||
* @returns {Promise<Date|null>} Last QSL date or null
|
||||
*/
|
||||
export async function getLastDCLQSLDate(userId) {
|
||||
try {
|
||||
const result = await db
|
||||
.select({ maxDate: max(qsos.dclQslRdate) })
|
||||
.from(qsos)
|
||||
.where(eq(qsos.userId, userId));
|
||||
|
||||
if (result[0]?.maxDate) {
|
||||
// Convert ADIF date format (YYYYMMDD) to Date
|
||||
const dateStr = result[0].maxDate;
|
||||
const year = dateStr.substring(0, 4);
|
||||
const month = dateStr.substring(4, 6);
|
||||
const day = dateStr.substring(6, 8);
|
||||
return new Date(`${year}-${month}-${day}`);
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
logger.error('Failed to get last DCL QSL date', { error: error.message, userId });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user