Add comprehensive documentation and LoTW integration

- Add detailed documentation covering architecture, components, code structure
- Document award system with multiple examples (DXCC, WAS, VUCC, Satellite)
- Implement LoTW sync service with ADIF parsing and long-polling
- Add QSO logbook page with filtering and statistics
- Add settings page for LoTW credentials management
- Add API endpoints for LoTW sync, QSO retrieval, and statistics

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-15 16:39:49 +01:00
parent 8c26fc93e3
commit 44c13e1bdc
6 changed files with 2229 additions and 2 deletions

View File

@@ -8,6 +8,11 @@ import {
getUserById,
updateLoTWCredentials,
} from './services/auth.service.js';
import {
syncQSOs,
getUserQSOs,
getQSOStats,
} from './services/lotw.service.js';
/**
* Main backend application
@@ -179,7 +184,17 @@ const app = new Elysia()
}
try {
await updateLoTWCredentials(user.id, body.lotwUsername, body.lotwPassword);
// Get current user data to preserve password if not provided
const userData = await getUserById(user.id);
if (!userData) {
set.status = 404;
return { success: false, error: 'User not found' };
}
// If password is empty, keep existing password
const lotwPassword = body.lotwPassword || userData.lotwPassword;
await updateLoTWCredentials(user.id, body.lotwUsername, lotwPassword);
return {
success: true,
@@ -196,11 +211,106 @@ const app = new Elysia()
{
body: t.Object({
lotwUsername: t.String(),
lotwPassword: t.String(),
lotwPassword: t.Optional(t.String()),
}),
}
)
/**
* POST /api/lotw/sync
* Sync QSOs from LoTW (requires authentication)
*/
.post('/api/lotw/sync', async ({ user, set }) => {
if (!user) {
set.status = 401;
return { success: false, error: 'Unauthorized' };
}
try {
// Get user's LoTW credentials from database
const userData = await getUserById(user.id);
if (!userData || !userData.lotwUsername || !userData.lotwPassword) {
set.status = 400;
return {
success: false,
error: 'LoTW credentials not configured. Please add them in Settings.',
};
}
// Decrypt password (for now, assuming it's stored as-is. TODO: implement encryption)
const lotwPassword = userData.lotwPassword;
// Sync QSOs from LoTW
const result = await syncQSOs(user.id, userData.lotwUsername, lotwPassword);
return result;
} catch (error) {
set.status = 500;
return {
success: false,
error: `LoTW sync failed: ${error.message}`,
};
}
})
/**
* GET /api/qsos
* Get user's QSOs (requires authentication)
*/
.get('/api/qsos', async ({ user, query, set }) => {
if (!user) {
set.status = 401;
return { success: false, error: 'Unauthorized' };
}
try {
const filters = {};
if (query.band) filters.band = query.band;
if (query.mode) filters.mode = query.mode;
if (query.confirmed) filters.confirmed = query.confirmed === 'true';
const qsos = await getUserQSOs(user.id, filters);
return {
success: true,
qsos,
count: qsos.length,
};
} catch (error) {
set.status = 500;
return {
success: false,
error: 'Failed to fetch QSOs',
};
}
})
/**
* GET /api/qsos/stats
* Get QSO statistics (requires authentication)
*/
.get('/api/qsos/stats', async ({ user, set }) => {
if (!user) {
set.status = 401;
return { success: false, error: 'Unauthorized' };
}
try {
const stats = await getQSOStats(user.id);
return {
success: true,
stats,
};
} catch (error) {
set.status = 500;
return {
success: false,
error: 'Failed to fetch statistics',
};
}
})
// Health check endpoint
.get('/api/health', () => ({
status: 'ok',