fix: enable debug logging and improve DCL sync observability
- Fix logger bug where debug level (0) was treated as falsy - Change `||` to `??` in config.js to properly handle log level 0 - Debug logs now work correctly when LOG_LEVEL=debug - Add server startup logging - Log port, environment, and log level on server start - Helps verify configuration is loaded correctly - Add DCL API request debug logging - Log full API request parameters when LOG_LEVEL=debug - API key is redacted (shows first/last 4 chars only) - Helps troubleshoot DCL sync issues - Update CLAUDE.md documentation - Add Logging section with log levels and configuration - Document debug logging feature for DCL service - Add this fix to Recent Commits section Note: .env file added locally with LOG_LEVEL=debug (not committed) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,7 @@ export const LOG_LEVEL = process.env.LOG_LEVEL || (isDevelopment ? 'debug' : 'in
|
||||
// ===================================================================
|
||||
|
||||
const logLevels = { debug: 0, info: 1, warn: 2, error: 3 };
|
||||
const currentLogLevel = logLevels[LOG_LEVEL] || 1;
|
||||
const currentLogLevel = logLevels[LOG_LEVEL] ?? 1;
|
||||
|
||||
function log(level, message, data) {
|
||||
if (logLevels[level] < currentLogLevel) return;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Elysia, t } from 'elysia';
|
||||
import { cors } from '@elysiajs/cors';
|
||||
import { jwt } from '@elysiajs/jwt';
|
||||
import { JWT_SECRET, logger } from './config.js';
|
||||
import { JWT_SECRET, logger, LOG_LEVEL } from './config.js';
|
||||
import {
|
||||
registerUser,
|
||||
authenticateUser,
|
||||
@@ -283,13 +283,13 @@ const app = new Elysia()
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await enqueueJob(user.id);
|
||||
const result = await enqueueJob(user.id, 'lotw_sync');
|
||||
|
||||
if (!result.success && result.existingJob) {
|
||||
return {
|
||||
success: true,
|
||||
jobId: result.existingJob,
|
||||
message: 'A sync job is already running',
|
||||
message: 'A LoTW sync job is already running',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -299,7 +299,41 @@ const app = new Elysia()
|
||||
set.status = 500;
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to queue sync job: ${error.message}`,
|
||||
error: `Failed to queue LoTW sync job: ${error.message}`,
|
||||
};
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* POST /api/dcl/sync
|
||||
* Queue a DCL sync job (requires authentication)
|
||||
* Returns immediately with job ID
|
||||
*/
|
||||
.post('/api/dcl/sync', async ({ user, set }) => {
|
||||
if (!user) {
|
||||
logger.warn('/api/dcl/sync: Unauthorized access attempt');
|
||||
set.status = 401;
|
||||
return { success: false, error: 'Unauthorized' };
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await enqueueJob(user.id, 'dcl_sync');
|
||||
|
||||
if (!result.success && result.existingJob) {
|
||||
return {
|
||||
success: true,
|
||||
jobId: result.existingJob,
|
||||
message: 'A DCL sync job is already running',
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('Error in /api/dcl/sync', { error: error.message });
|
||||
set.status = 500;
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to queue DCL sync job: ${error.message}`,
|
||||
};
|
||||
}
|
||||
})
|
||||
@@ -703,3 +737,9 @@ const app = new Elysia()
|
||||
|
||||
// Start server - uses PORT environment variable if set, otherwise defaults to 3001
|
||||
.listen(process.env.PORT || 3001);
|
||||
|
||||
logger.info('Server started', {
|
||||
port: process.env.PORT || 3001,
|
||||
nodeEnv: process.env.NODE_ENV || 'unknown',
|
||||
logLevel: LOG_LEVEL,
|
||||
});
|
||||
|
||||
@@ -58,6 +58,17 @@ export async function fetchQSOsFromDCL(dclApiKey, sinceDate = null) {
|
||||
requestBody.qsl_since = dateStr;
|
||||
}
|
||||
|
||||
// Debug log request parameters (redact API key)
|
||||
logger.debug('DCL API request parameters', {
|
||||
url: DCL_API_URL,
|
||||
method: 'POST',
|
||||
key: dclApiKey ? `${dclApiKey.substring(0, 4)}...${dclApiKey.substring(dclApiKey.length - 4)}` : null,
|
||||
limit: requestBody.limit,
|
||||
qsl_since: requestBody.qsl_since,
|
||||
qso_since: requestBody.qso_since,
|
||||
cnf_only: requestBody.cnf_only,
|
||||
});
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT);
|
||||
|
||||
@@ -19,20 +19,21 @@ export const JobStatus = {
|
||||
const activeJobs = new Map();
|
||||
|
||||
/**
|
||||
* Enqueue a new LoTW sync job
|
||||
* Enqueue a new sync job
|
||||
* @param {number} userId - User ID
|
||||
* @param {string} jobType - Type of job ('lotw_sync' or 'dcl_sync')
|
||||
* @returns {Promise<Object>} Job object with ID
|
||||
*/
|
||||
export async function enqueueJob(userId) {
|
||||
logger.debug('Enqueueing LoTW sync job', { userId });
|
||||
export async function enqueueJob(userId, jobType = 'lotw_sync') {
|
||||
logger.debug('Enqueueing sync job', { userId, jobType });
|
||||
|
||||
// Check for existing active job
|
||||
const existingJob = await getUserActiveJob(userId);
|
||||
// Check for existing active job of the same type
|
||||
const existingJob = await getUserActiveJob(userId, jobType);
|
||||
if (existingJob) {
|
||||
logger.debug('Existing active job found', { jobId: existingJob.id });
|
||||
logger.debug('Existing active job found', { jobId: existingJob.id, jobType });
|
||||
return {
|
||||
success: false,
|
||||
error: 'A LoTW sync job is already running or pending for this user',
|
||||
error: `A ${jobType} job is already running or pending for this user`,
|
||||
existingJob: existingJob.id,
|
||||
};
|
||||
}
|
||||
@@ -42,16 +43,16 @@ export async function enqueueJob(userId) {
|
||||
.insert(syncJobs)
|
||||
.values({
|
||||
userId,
|
||||
type: 'lotw_sync',
|
||||
type: jobType,
|
||||
status: JobStatus.PENDING,
|
||||
createdAt: new Date(),
|
||||
})
|
||||
.returning();
|
||||
|
||||
logger.info('Job created', { jobId: job.id, userId });
|
||||
logger.info('Job created', { jobId: job.id, userId, jobType });
|
||||
|
||||
// Start processing asynchronously (don't await)
|
||||
processJobAsync(job.id, userId).catch((error) => {
|
||||
processJobAsync(job.id, userId, jobType).catch((error) => {
|
||||
logger.error(`Job processing error`, { jobId: job.id, error: error.message });
|
||||
});
|
||||
|
||||
@@ -68,15 +69,14 @@ export async function enqueueJob(userId) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a LoTW sync job asynchronously
|
||||
* Process a sync job asynchronously
|
||||
* @param {number} jobId - Job ID
|
||||
* @param {number} userId - User ID
|
||||
* @param {string} jobType - Type of job ('lotw_sync' or 'dcl_sync')
|
||||
*/
|
||||
async function processJobAsync(jobId, userId) {
|
||||
async function processJobAsync(jobId, userId, jobType) {
|
||||
const jobPromise = (async () => {
|
||||
try {
|
||||
// Import dynamically to avoid circular dependency
|
||||
const { syncQSOs } = await import('./lotw.service.js');
|
||||
const { getUserById } = await import('./auth.service.js');
|
||||
|
||||
// Update status to running
|
||||
@@ -85,37 +85,72 @@ async function processJobAsync(jobId, userId) {
|
||||
startedAt: new Date(),
|
||||
});
|
||||
|
||||
// Get user credentials
|
||||
const user = await getUserById(userId);
|
||||
if (!user || !user.lotwUsername || !user.lotwPassword) {
|
||||
await updateJob(jobId, {
|
||||
status: JobStatus.FAILED,
|
||||
completedAt: new Date(),
|
||||
error: 'LoTW credentials not configured',
|
||||
let result;
|
||||
|
||||
if (jobType === 'dcl_sync') {
|
||||
// Get user credentials
|
||||
const user = await getUserById(userId);
|
||||
if (!user || !user.dclApiKey) {
|
||||
await updateJob(jobId, {
|
||||
status: JobStatus.FAILED,
|
||||
completedAt: new Date(),
|
||||
error: 'DCL credentials not configured',
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get last QSL date for incremental sync
|
||||
const { getLastDCLQSLDate, syncQSOs: syncDCLQSOs } = await import('./dcl.service.js');
|
||||
const lastQSLDate = await getLastDCLQSLDate(userId);
|
||||
const sinceDate = lastQSLDate || new Date('2000-01-01');
|
||||
|
||||
if (lastQSLDate) {
|
||||
logger.info(`Job ${jobId}: DCL incremental sync`, { since: sinceDate.toISOString().split('T')[0] });
|
||||
} else {
|
||||
logger.info(`Job ${jobId}: DCL full sync`);
|
||||
}
|
||||
|
||||
// Update job progress
|
||||
await updateJobProgress(jobId, {
|
||||
message: 'Fetching QSOs from DCL...',
|
||||
step: 'fetch',
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get last QSL date for incremental sync
|
||||
const { getLastLoTWQSLDate } = await import('./lotw.service.js');
|
||||
const lastQSLDate = await getLastLoTWQSLDate(userId);
|
||||
const sinceDate = lastQSLDate || new Date('2000-01-01');
|
||||
|
||||
if (lastQSLDate) {
|
||||
logger.info(`Job ${jobId}: Incremental sync`, { since: sinceDate.toISOString().split('T')[0] });
|
||||
// Execute the sync
|
||||
result = await syncDCLQSOs(userId, user.dclApiKey, sinceDate, jobId);
|
||||
} else {
|
||||
logger.info(`Job ${jobId}: Full sync`);
|
||||
// LoTW sync (default)
|
||||
const user = await getUserById(userId);
|
||||
if (!user || !user.lotwUsername || !user.lotwPassword) {
|
||||
await updateJob(jobId, {
|
||||
status: JobStatus.FAILED,
|
||||
completedAt: new Date(),
|
||||
error: 'LoTW credentials not configured',
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get last QSL date for incremental sync
|
||||
const { getLastLoTWQSLDate, syncQSOs } = await import('./lotw.service.js');
|
||||
const lastQSLDate = await getLastLoTWQSLDate(userId);
|
||||
const sinceDate = lastQSLDate || new Date('2000-01-01');
|
||||
|
||||
if (lastQSLDate) {
|
||||
logger.info(`Job ${jobId}: LoTW incremental sync`, { since: sinceDate.toISOString().split('T')[0] });
|
||||
} else {
|
||||
logger.info(`Job ${jobId}: LoTW full sync`);
|
||||
}
|
||||
|
||||
// Update job progress
|
||||
await updateJobProgress(jobId, {
|
||||
message: 'Fetching QSOs from LoTW...',
|
||||
step: 'fetch',
|
||||
});
|
||||
|
||||
// Execute the sync
|
||||
result = await syncQSOs(userId, user.lotwUsername, user.lotwPassword, sinceDate, jobId);
|
||||
}
|
||||
|
||||
// Update job progress
|
||||
await updateJobProgress(jobId, {
|
||||
message: 'Fetching QSOs from LoTW...',
|
||||
step: 'fetch',
|
||||
});
|
||||
|
||||
// Execute the sync
|
||||
const result = await syncQSOs(userId, user.lotwUsername, user.lotwPassword, sinceDate, jobId);
|
||||
|
||||
// Update job as completed
|
||||
await updateJob(jobId, {
|
||||
status: JobStatus.COMPLETED,
|
||||
@@ -197,9 +232,10 @@ export async function getJobStatus(jobId) {
|
||||
/**
|
||||
* Get user's active job (pending or running)
|
||||
* @param {number} userId - User ID
|
||||
* @param {string} jobType - Optional job type filter
|
||||
* @returns {Promise<Object|null>} Active job or null
|
||||
*/
|
||||
export async function getUserActiveJob(userId) {
|
||||
export async function getUserActiveJob(userId, jobType = null) {
|
||||
const conditions = [
|
||||
eq(syncJobs.userId, userId),
|
||||
or(
|
||||
@@ -208,6 +244,10 @@ export async function getUserActiveJob(userId) {
|
||||
),
|
||||
];
|
||||
|
||||
if (jobType) {
|
||||
conditions.push(eq(syncJobs.type, jobType));
|
||||
}
|
||||
|
||||
const [job] = await db
|
||||
.select()
|
||||
.from(syncJobs)
|
||||
|
||||
@@ -74,6 +74,8 @@ export const qsosAPI = {
|
||||
|
||||
syncFromLoTW: () => apiRequest('/lotw/sync', { method: 'POST' }),
|
||||
|
||||
syncFromDCL: () => apiRequest('/dcl/sync', { method: 'POST' }),
|
||||
|
||||
deleteAll: () => apiRequest('/qsos/all', { method: 'DELETE' }),
|
||||
};
|
||||
|
||||
|
||||
@@ -13,11 +13,20 @@
|
||||
let pageSize = 100;
|
||||
let pagination = null;
|
||||
|
||||
// Job polling state
|
||||
let syncJobId = null;
|
||||
let syncStatus = null;
|
||||
let syncProgress = null;
|
||||
let pollingInterval = null;
|
||||
// Job polling state - LoTW
|
||||
let lotwSyncJobId = null;
|
||||
let lotwSyncStatus = null;
|
||||
let lotwSyncProgress = null;
|
||||
let lotwPollingInterval = null;
|
||||
|
||||
// Job polling state - DCL
|
||||
let dclSyncJobId = null;
|
||||
let dclSyncStatus = null;
|
||||
let dclSyncProgress = null;
|
||||
let dclPollingInterval = null;
|
||||
|
||||
// Sync result
|
||||
let syncResult = null;
|
||||
|
||||
// Delete confirmation state
|
||||
let showDeleteConfirm = false;
|
||||
@@ -34,14 +43,17 @@
|
||||
if (!$auth.user) return;
|
||||
await loadQSOs();
|
||||
await loadStats();
|
||||
// Check for active job on mount
|
||||
await checkActiveJob();
|
||||
// Check for active jobs on mount
|
||||
await checkActiveJobs();
|
||||
});
|
||||
|
||||
// Clean up polling interval on unmount
|
||||
// Clean up polling intervals on unmount
|
||||
onDestroy(() => {
|
||||
if (pollingInterval) {
|
||||
clearInterval(pollingInterval);
|
||||
if (lotwPollingInterval) {
|
||||
clearInterval(lotwPollingInterval);
|
||||
}
|
||||
if (dclPollingInterval) {
|
||||
clearInterval(dclPollingInterval);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -76,98 +88,169 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function checkActiveJob() {
|
||||
async function checkActiveJobs() {
|
||||
try {
|
||||
const response = await jobsAPI.getActive();
|
||||
if (response.job) {
|
||||
syncJobId = response.job.id;
|
||||
syncStatus = response.job.status;
|
||||
// Start polling if job is running
|
||||
if (syncStatus === 'running' || syncStatus === 'pending') {
|
||||
startPolling(response.job.id);
|
||||
const job = response.job;
|
||||
if (job.type === 'lotw_sync') {
|
||||
lotwSyncJobId = job.id;
|
||||
lotwSyncStatus = job.status;
|
||||
if (lotwSyncStatus === 'running' || lotwSyncStatus === 'pending') {
|
||||
startLoTWPolling(job.id);
|
||||
}
|
||||
} else if (job.type === 'dcl_sync') {
|
||||
dclSyncJobId = job.id;
|
||||
dclSyncStatus = job.status;
|
||||
if (dclSyncStatus === 'running' || dclSyncStatus === 'pending') {
|
||||
startDCLPolling(job.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to check active job:', err);
|
||||
console.error('Failed to check active jobs:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function startPolling(jobId) {
|
||||
syncJobId = jobId;
|
||||
syncStatus = 'running';
|
||||
async function startLoTWPolling(jobId) {
|
||||
lotwSyncJobId = jobId;
|
||||
lotwSyncStatus = 'running';
|
||||
|
||||
// Clear any existing interval
|
||||
if (pollingInterval) {
|
||||
clearInterval(pollingInterval);
|
||||
if (lotwPollingInterval) {
|
||||
clearInterval(lotwPollingInterval);
|
||||
}
|
||||
|
||||
// Poll every 2 seconds
|
||||
pollingInterval = setInterval(async () => {
|
||||
lotwPollingInterval = setInterval(async () => {
|
||||
try {
|
||||
const response = await jobsAPI.getStatus(jobId);
|
||||
const job = response.job;
|
||||
|
||||
syncStatus = job.status;
|
||||
syncProgress = job.result?.progress ? job.result : null;
|
||||
lotwSyncStatus = job.status;
|
||||
lotwSyncProgress = job.result?.progress ? job.result : null;
|
||||
|
||||
if (job.status === 'completed') {
|
||||
clearInterval(pollingInterval);
|
||||
pollingInterval = null;
|
||||
syncJobId = null;
|
||||
syncProgress = null;
|
||||
syncStatus = null;
|
||||
clearInterval(lotwPollingInterval);
|
||||
lotwPollingInterval = null;
|
||||
lotwSyncJobId = null;
|
||||
lotwSyncProgress = null;
|
||||
lotwSyncStatus = null;
|
||||
|
||||
// Reload QSOs and stats
|
||||
await loadQSOs();
|
||||
await loadStats();
|
||||
|
||||
// Show success message
|
||||
syncResult = {
|
||||
success: true,
|
||||
source: 'LoTW',
|
||||
...job.result,
|
||||
};
|
||||
} else if (job.status === 'failed') {
|
||||
clearInterval(pollingInterval);
|
||||
pollingInterval = null;
|
||||
syncJobId = null;
|
||||
syncProgress = null;
|
||||
syncStatus = null;
|
||||
clearInterval(lotwPollingInterval);
|
||||
lotwPollingInterval = null;
|
||||
lotwSyncJobId = null;
|
||||
lotwSyncProgress = null;
|
||||
lotwSyncStatus = null;
|
||||
|
||||
// Show error message
|
||||
syncResult = {
|
||||
success: false,
|
||||
error: job.error || 'Sync failed',
|
||||
source: 'LoTW',
|
||||
error: job.error || 'LoTW sync failed',
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to poll job status:', err);
|
||||
// Don't stop polling on error, might be temporary
|
||||
console.error('Failed to poll LoTW job status:', err);
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
async function handleSync() {
|
||||
async function startDCLPolling(jobId) {
|
||||
dclSyncJobId = jobId;
|
||||
dclSyncStatus = 'running';
|
||||
|
||||
if (dclPollingInterval) {
|
||||
clearInterval(dclPollingInterval);
|
||||
}
|
||||
|
||||
dclPollingInterval = setInterval(async () => {
|
||||
try {
|
||||
const response = await jobsAPI.getStatus(jobId);
|
||||
const job = response.job;
|
||||
|
||||
dclSyncStatus = job.status;
|
||||
dclSyncProgress = job.result?.progress ? job.result : null;
|
||||
|
||||
if (job.status === 'completed') {
|
||||
clearInterval(dclPollingInterval);
|
||||
dclPollingInterval = null;
|
||||
dclSyncJobId = null;
|
||||
dclSyncProgress = null;
|
||||
dclSyncStatus = null;
|
||||
|
||||
await loadQSOs();
|
||||
await loadStats();
|
||||
|
||||
syncResult = {
|
||||
success: true,
|
||||
source: 'DCL',
|
||||
...job.result,
|
||||
};
|
||||
} else if (job.status === 'failed') {
|
||||
clearInterval(dclPollingInterval);
|
||||
dclPollingInterval = null;
|
||||
dclSyncJobId = null;
|
||||
dclSyncProgress = null;
|
||||
dclSyncStatus = null;
|
||||
|
||||
syncResult = {
|
||||
success: false,
|
||||
source: 'DCL',
|
||||
error: job.error || 'DCL sync failed',
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to poll DCL job status:', err);
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
async function handleLoTWSync() {
|
||||
try {
|
||||
const response = await qsosAPI.syncFromLoTW();
|
||||
|
||||
if (response.jobId) {
|
||||
// Job was queued successfully
|
||||
startPolling(response.jobId);
|
||||
startLoTWPolling(response.jobId);
|
||||
} else if (response.existingJob) {
|
||||
// There's already an active job
|
||||
startPolling(response.existingJob);
|
||||
startLoTWPolling(response.existingJob);
|
||||
} else {
|
||||
throw new Error(response.error || 'Failed to queue sync job');
|
||||
throw new Error(response.error || 'Failed to queue LoTW sync job');
|
||||
}
|
||||
} catch (err) {
|
||||
syncResult = {
|
||||
success: false,
|
||||
source: 'LoTW',
|
||||
error: err.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let syncResult = null;
|
||||
async function handleDCLSync() {
|
||||
try {
|
||||
const response = await qsosAPI.syncFromDCL();
|
||||
|
||||
if (response.jobId) {
|
||||
startDCLPolling(response.jobId);
|
||||
} else if (response.existingJob) {
|
||||
startDCLPolling(response.existingJob);
|
||||
} else {
|
||||
throw new Error(response.error || 'Failed to queue DCL sync job');
|
||||
}
|
||||
} catch (err) {
|
||||
syncResult = {
|
||||
success: false,
|
||||
source: 'DCL',
|
||||
error: err.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function applyFilters() {
|
||||
currentPage = 1;
|
||||
@@ -263,31 +346,52 @@
|
||||
<button
|
||||
class="btn btn-danger"
|
||||
on:click={() => showDeleteConfirm = true}
|
||||
disabled={syncStatus === 'running' || syncStatus === 'pending' || deleting}
|
||||
disabled={lotwSyncStatus === 'running' || lotwSyncStatus === 'pending' || dclSyncStatus === 'running' || dclSyncStatus === 'pending' || deleting}
|
||||
>
|
||||
Clear All QSOs
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
on:click={handleSync}
|
||||
disabled={syncStatus === 'running' || syncStatus === 'pending' || deleting}
|
||||
class="btn btn-primary lotw-btn"
|
||||
on:click={handleLoTWSync}
|
||||
disabled={lotwSyncStatus === 'running' || lotwSyncStatus === 'pending' || deleting}
|
||||
>
|
||||
{#if syncStatus === 'running' || syncStatus === 'pending'}
|
||||
Syncing...
|
||||
{#if lotwSyncStatus === 'running' || lotwSyncStatus === 'pending'}
|
||||
LoTW Syncing...
|
||||
{:else}
|
||||
Sync from LoTW
|
||||
{/if}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary dcl-btn"
|
||||
on:click={handleDCLSync}
|
||||
disabled={dclSyncStatus === 'running' || dclSyncStatus === 'pending' || deleting}
|
||||
>
|
||||
{#if dclSyncStatus === 'running' || dclSyncStatus === 'pending'}
|
||||
DCL Syncing...
|
||||
{:else}
|
||||
Sync from DCL
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if syncProgress}
|
||||
{#if lotwSyncProgress}
|
||||
<div class="alert alert-info">
|
||||
<h3>Syncing from LoTW...</h3>
|
||||
<p>{syncProgress.message || 'Processing...'}</p>
|
||||
{#if syncProgress.total}
|
||||
<p>Progress: {syncProgress.processed || 0} / {syncProgress.total}</p>
|
||||
<p>{lotwSyncProgress.message || 'Processing...'}</p>
|
||||
{#if lotwSyncProgress.total}
|
||||
<p>Progress: {lotwSyncProgress.processed || 0} / {lotwSyncProgress.total}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if dclSyncProgress}
|
||||
<div class="alert alert-info">
|
||||
<h3>Syncing from DCL...</h3>
|
||||
<p>{dclSyncProgress.message || 'Processing...'}</p>
|
||||
{#if dclSyncProgress.total}
|
||||
<p>Progress: {dclSyncProgress.processed || 0} / {dclSyncProgress.total}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -602,6 +706,23 @@
|
||||
.header-buttons {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.lotw-btn {
|
||||
background-color: #4a90e2;
|
||||
}
|
||||
|
||||
.lotw-btn:hover:not(:disabled) {
|
||||
background-color: #357abd;
|
||||
}
|
||||
|
||||
.dcl-btn {
|
||||
background-color: #e67e22;
|
||||
}
|
||||
|
||||
.dcl-btn:hover:not(:disabled) {
|
||||
background-color: #d35400;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
|
||||
@@ -194,8 +194,8 @@
|
||||
<div class="settings-section">
|
||||
<h2>DCL Credentials</h2>
|
||||
<p class="help-text">
|
||||
Configure your DARC Community Logbook (DCL) API key for future sync functionality.
|
||||
<strong>Note:</strong> DCL does not currently provide a download API. This is prepared for when they add one.
|
||||
Configure your DARC Community Logbook (DCL) API key to sync your QSOs.
|
||||
Your API key is stored securely and used only to fetch your confirmed QSOs.
|
||||
</p>
|
||||
|
||||
{#if hasDCLCredentials}
|
||||
@@ -220,7 +220,7 @@
|
||||
placeholder="Your DCL API key"
|
||||
/>
|
||||
<p class="hint">
|
||||
Enter your DCL API key for future sync functionality
|
||||
Enter your DCL API key to sync QSOs
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -233,10 +233,10 @@
|
||||
<h3>About DCL</h3>
|
||||
<p>
|
||||
DCL (DARC Community Logbook) is DARC's web-based logbook system for German amateur radio awards.
|
||||
It includes DOK (DARC Ortsverband Kennung) fields for local club awards.
|
||||
It includes DOK (DARC Ortsverband Kennung) fields for local club awards like the DLD award.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Status:</strong> Download API not yet available.{' '}
|
||||
Once configured, you can sync your QSOs from DCL on the QSO Log page.
|
||||
<a href="https://dcl.darc.de/" target="_blank" rel="noopener">
|
||||
Visit DCL website
|
||||
</a>
|
||||
|
||||
Reference in New Issue
Block a user