- Award detail page now shows QSO counts per (entity, band, mode) slot - Click count to open modal with all QSOs for that slot - Click QSO in list to view full details - Add mode filter: "Mixed Mode" aggregates by band, specific modes show (band, mode) columns - Backend groups by slot and collects all confirmed QSOs in qsos array - Frontend displays clickable count links (removed blue bubbles) Backend changes: - calculateDOKAwardProgress(): groups by (DOK, band, mode), collects qsos array - calculatePointsAwardProgress(): updated for all count modes with qsos array - getAwardEntityBreakdown(): groups by (entity, band, mode) slots Frontend changes: - Add mode filter dropdown with "Mixed Mode" default - Update grouping logic to handle mixed mode vs specific mode - Replace count badges with simple clickable links - Add QSO list modal showing all QSOs per slot - Add Mode column to QSO list (useful in mixed mode) Co-Authored-By: Claude <noreply@anthropic.com>
31 KiB
Default to using Bun instead of Node.js.
- Use
bun <file>instead ofnode <file>orts-node <file> - Use
bun testinstead ofjestorvitest - Use
bun build <file.html|file.ts|file.css>instead ofwebpackoresbuild - Use
bun installinstead ofnpm installoryarn installorpnpm install - Use
bun run <script>instead ofnpm run <script>oryarn run <script>orpnpm run <script> - Use
bunx <package> <command>instead ofnpx <package> <command> - Bun automatically loads .env, so don't use dotenv.
APIs
Bun.serve()supports WebSockets, HTTPS, and routes. Don't useexpress.bun:sqlitefor SQLite. Don't usebetter-sqlite3.Bun.redisfor Redis. Don't useioredis.Bun.sqlfor Postgres. Don't usepgorpostgres.js.WebSocketis built-in. Don't usews.- Prefer
Bun.fileovernode:fs's readFile/writeFile - Bun.$
lsinstead of execa.
Logging
The application uses a custom logger that outputs to both files and console.
Backend Logging
Backend logs are written to logs/backend.log:
- Log levels:
debug(0),info(1),warn(2),error(3) - Default:
debugin development,infoin production - Override: Set
LOG_LEVELenvironment variable (e.g.,LOG_LEVEL=debug) - Output format:
[timestamp] LEVEL: messagewith JSON data - Console: Also outputs to console in development mode
- File: Always writes to
logs/backend.log
Frontend Logging
Frontend logs are sent to the backend and written to logs/frontend.log:
- Logger:
src/frontend/src/lib/logger.js - Endpoint:
POST /api/logs - Batching: Batches logs (up to 10 entries or 5 seconds) for performance
- User context: Automatically includes userId and user-agent
- Levels: Same as backend (debug, info, warn, error)
Usage in frontend:
import { logger } from '$lib/logger';
logger.info('User action', { action: 'click', element: 'button' });
logger.error('API error', { error: err.message });
logger.warn('Deprecated feature used');
logger.debug('Component state', { state: componentState });
Important: The logger uses the nullish coalescing operator (??) to handle log levels. This ensures that debug (level 0) is not treated as falsy.
Example .env file:
NODE_ENV=development
LOG_LEVEL=debug
Log Files:
logs/backend.log- Backend server logslogs/frontend.log- Frontend client logs- Logs are excluded from git via
.gitignore
Testing
Use bun test to run tests.
import { test, expect } from "bun:test";
test("hello world", () => {
expect(1).toBe(1);
});
Frontend
Use HTML imports with Bun.serve(). Don't use vite. HTML imports fully support React, CSS, Tailwind.
Server:
import index from "./index.html"
Bun.serve({
routes: {
"/": index,
"/api/users/:id": {
GET: (req) => {
return new Response(JSON.stringify({ id: req.params.id }));
},
},
},
// optional websocket support
websocket: {
open: (ws) => {
ws.send("Hello, world!");
},
message: (ws, message) => {
ws.send(message);
},
close: (ws) => {
// handle close
}
},
development: {
hmr: true,
console: true,
}
})
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. <link> tags can point to stylesheets and Bun's CSS bundler will bundle.
<html>
<body>
<h1>Hello, world!</h1>
<script type="module" src="./frontend.tsx"></script>
</body>
</html>
With the following frontend.tsx:
import React from "react";
import { createRoot } from "react-dom/client";
// import .css files directly and it works
import './index.css';
const root = createRoot(document.body);
export default function Frontend() {
return <h1>Hello, world!</h1>;
}
root.render(<Frontend />);
Then, run index.ts
bun --hot ./index.ts
For more information, read the Bun API docs in node_modules/bun-types/docs/**.mdx.
Project: Quickawards by DJ7NT
Quickawards is a amateur radio award tracking application that calculates progress toward various awards based on QSO (contact) data.
Award System Architecture
The award system is JSON-driven and located in award-definitions/ directory. Each award has:
id: Unique identifier (e.g., "dld", "dxcc")name: Display namedescription: Short descriptioncaption: Detailed explanationcategory: Award category ("dxcc", "darc", etc.)rules: Award calculation logic
Award Rule Types
-
entity: Count unique entities (DXCC countries, states, grid squares)entityType: What to count ("dxcc", "state", "grid", "callsign")target: Number required for awardfilters: Optional filters (band, mode, etc.)displayField: Optional field to display
-
dok: Count unique DOK (DARC Ortsverband Kennung) combinationstarget: Number requiredconfirmationType: "dcl" (DARC Community Logbook)filters: Optional filters (band, mode, etc.) for award variants- Counts unique (DOK, band, mode) combinations
- Only DCL-confirmed QSOs count
- Example variants: DLD 80m, DLD CW, DLD 80m CW
-
points: Point-based awardsstations: Array of {callsign, points}target: Points requiredcountMode: "perStation", "perBandMode", or "perQso"
-
filtered: Filtered version of another awardbaseRule: The base entity rulefilters: Additional filters to apply
-
counter: Count QSOs or callsigns
Key Files
Backend Award Service: src/backend/services/awards.service.js
getAllAwards(): Returns all available award definitionscalculateAwardProgress(userId, award, options): Main calculation functioncalculateDOKAwardProgress(userId, award, options): DOK-specific calculationcalculatePointsAwardProgress(userId, award, options): Point-based calculationgetAwardEntityBreakdown(userId, awardId): Detailed entity breakdowngetAwardProgressDetails(userId, awardId): Progress with details
Database Schema: src/backend/db/schema/index.js
- QSO fields include:
darcDok,dclQslRstatus,dclQslRdate - DOK fields support DLD award tracking
- DCL confirmation fields separate from LoTW
Award Definitions: award-definitions/*.json
- Add new awards by creating JSON definition files
- Add filename to
loadAwardDefinitions()file list in awards.service.js
ADIF Parser: src/backend/utils/adif-parser.js
parseADIF(adifData): Parse ADIF format into QSO records- Handles case-insensitive
<EOR>delimiters (supports<EOR>,<eor>,<Eor>) - Uses
matchAll()for reliable field parsing - Skips header records automatically
- Handles case-insensitive
parseDCLResponse(response): Parse DCL's JSON response format{ "adif": "..." }normalizeBand(band): Standardize band names (80m, 40m, etc.)normalizeMode(mode): Standardize mode names (CW, FT8, SSB, etc.)- Used by both LoTW and DCL services for consistency
Job Queue Service: src/backend/services/job-queue.service.js
- Manages async background jobs for LoTW and DCL sync
enqueueJob(userId, jobType): Queue a sync job ('lotw_sync' or 'dcl_sync')processJobAsync(jobId, userId, jobType): Process job asynchronouslygetUserActiveJob(userId, jobType): Get active job for user (optional type filter)getJobStatus(jobId): Get job status with parsed resultupdateJobProgress(jobId, progressData): Update job progress during processing- Supports concurrent LoTW and DCL sync jobs
- Job types: 'lotw_sync', 'dcl_sync'
- Job status: 'pending', 'running', 'completed', 'failed'
Backend API Routes (src/backend/index.js):
POST /api/lotw/sync: Queue LoTW sync jobPOST /api/dcl/sync: Queue DCL sync jobGET /api/jobs/:jobId: Get job statusGET /api/jobs/active: Get active job for current userGET /*: Serves static files fromsrc/frontend/build/with SPA fallback
SPA Routing: The backend serves the SvelteKit frontend build from src/frontend/build/.
- Paths with file extensions (
.js,.css, etc.) are served as static files - Paths without extensions (e.g.,
/qsos,/awards) are servedindex.htmlfor client-side routing - Common missing files like
/favicon.icoreturn 404 immediately - If frontend build is missing entirely, returns a user-friendly 503 HTML page
- Prevents ugly Bun error pages when accessing client-side routes via curl or non-JS clients
DCL Service: src/backend/services/dcl.service.js
fetchQSOsFromDCL(dclApiKey, sinceDate): Fetch from DCL API- API Endpoint:
https://dings.dcl.darc.de/api/adiexport - Request: POST with JSON body
{ key, limit: 50000, qsl_since, qso_since, cnf_only }cnf_only: null- Fetch ALL QSOs (confirmed + unconfirmed)cnf_only: true- Fetch only confirmed QSOs (dcl_qsl_rcvd='Y')qso_since: DATE- QSOs since this date (YYYYMMDD format)qsl_since: DATE- QSL confirmations since this date (YYYYMMDD format)
parseDCLJSONResponse(jsonResponse): Parse example/test payloadssyncQSOs(userId, dclApiKey, sinceDate, jobId): Sync QSOs to databasegetLastDCLQSLDate(userId): Get last QSL date for incremental syncgetLastDCLQSODate(userId): Get last QSO date for incremental sync- Debug logging (when
LOG_LEVEL=debug) shows API params with redacted key (first/last 4 chars) - Fully implemented and functional
- Note: DCL API is a custom prototype by DARC; contact DARC for API specification details
DLD Award Implementation (COMPLETED)
The DLD (Deutschland Diplom) award was recently implemented:
Definition: award-definitions/dld.json
{
"id": "dld",
"name": "DLD",
"description": "Deutschland Diplom - Confirm 100 unique DOKs on different bands/modes",
"caption": "Contact and confirm stations with 100 unique DOKs (DARC Ortsverband Kennung) on different band/mode combinations.",
"category": "darc",
"rules": {
"type": "dok",
"target": 100,
"confirmationType": "dcl",
"displayField": "darcDok"
}
}
Implementation Details:
- Function:
calculateDOKAwardProgress()insrc/backend/services/awards.service.js(lines 173-268) - Counts unique (DOK, band, mode) combinations
- Only DCL-confirmed QSOs count (
dclQslRstatus === 'Y') - Each unique DOK on each unique band/mode counts separately
- Returns worked, confirmed counts and entity breakdowns
Database Fields Used:
darcDok: DOK identifier (e.g., "F03", "P30", "G20")band: Band (e.g., "80m", "40m", "20m")mode: Mode (e.g., "CW", "SSB", "FT8")dclQslRstatus: DCL confirmation status ('Y' = confirmed)dclQslRdate: DCL confirmation date
Documentation: See docs/DOCUMENTATION.md for complete documentation including DLD award example.
Frontend: src/frontend/src/routes/qsos/+page.svelte
- Separate sync buttons for LoTW (blue) and DCL (orange)
- Independent progress tracking for each sync type
- Both syncs can run simultaneously
- Job polling every 2 seconds for status updates
- Import log displays after sync completion
- Real-time QSO table refresh after sync
Frontend API (src/frontend/src/lib/api.js):
qsosAPI.syncFromLoTW(): Trigger LoTW syncqsosAPI.syncFromDCL(): Trigger DCL syncjobsAPI.getStatus(jobId): Poll job statusjobsAPI.getActive(): Get active job on page load
Adding New Awards
To add a new award:
- Create JSON definition in
award-definitions/ - Add filename to
loadAwardDefinitions()insrc/backend/services/awards.service.js - If new rule type needed, add calculation function
- Add type handling in
calculateAwardProgress()switch statement - Add type handling in
getAwardEntityBreakdown()if needed - Update documentation in
docs/DOCUMENTATION.md - Test with sample QSO data
Creating DLD Award Variants
The DOK award type supports filters to create award variants. Examples:
DLD on 80m (dld-80m.json):
{
"id": "dld-80m",
"name": "DLD 80m",
"description": "Confirm 100 unique DOKs on 80m",
"caption": "Contact 100 different DOKs on the 80m band.",
"category": "darc",
"rules": {
"type": "dok",
"target": 100,
"confirmationType": "dcl",
"displayField": "darcDok",
"filters": {
"operator": "AND",
"filters": [
{ "field": "band", "operator": "eq", "value": "80m" }
]
}
}
}
DLD in CW mode (dld-cw.json):
{
"rules": {
"type": "dok",
"target": 100,
"confirmationType": "dcl",
"filters": {
"operator": "AND",
"filters": [
{ "field": "mode", "operator": "eq", "value": "CW" }
]
}
}
}
DLD on 80m using CW (combined filters, dld-80m-cw.json):
{
"rules": {
"type": "dok",
"target": 100,
"confirmationType": "dcl",
"filters": {
"operator": "AND",
"filters": [
{ "field": "band", "operator": "eq", "value": "80m" },
{ "field": "mode", "operator": "eq", "value": "CW" }
]
}
}
}
Available filter operators:
eq: equalsne: not equalsin: in arraynin: not in arraycontains: contains substring
Available filter fields: Any QSO field (band, mode, callsign, grid, state, satName, etc.)
Confirmation Systems
-
LoTW (Logbook of The World): ARRL's confirmation system
- Service:
src/backend/services/lotw.service.js - API:
https://lotw.arrl.org/lotwuser/lotwreport.adi - Fields:
lotwQslRstatus,lotwQslRdate - Used for DXCC, WAS, VUCC, most awards
- ADIF format with
<EOR>delimiters - Supports incremental sync by
qso_qslsinceparameter (format: YYYY-MM-DD)
- Service:
-
DCL (DARC Community Logbook): DARC's confirmation system
- Service:
src/backend/services/dcl.service.js - API:
https://dings.dcl.darc.de/api/adiexport - Fields:
dclQslRstatus,dclQslRdate - DOK fields:
darcDok(partner's DOK),myDarcDok(user's DOK) - Required for DLD award
- German amateur radio specific
- Request format: POST JSON
{ key, limit, qsl_since, qso_since, cnf_only }cnf_only: null- Fetch all QSOs (confirmed + unconfirmed)cnf_only: true- Fetch only confirmed QSOsqso_since- QSOs since this date (YYYYMMDD)qsl_since- QSL confirmations since this date (YYYYMMDD)
- Response format: JSON with ADIF string in
adiffield - Syncs ALL QSOs (both confirmed and unconfirmed)
- Unconfirmed QSOs stored but don't count toward awards
- Updates QSOs only if confirmation data has changed
- Service:
ADIF Format
Both LoTW and DCL return data in ADIF (Amateur Data Interchange Format):
- Field format:
<FIELD_NAME:length>value - Record delimiter:
<EOR>(end of record, case-insensitive) - Header ends with:
<EOH>(end of header) - Example:
<CALL:5>DK0MU<BAND:3>80m<QSO_DATE:8>20250621<EOR> - Important: Parser handles case-insensitive
<EOR>,<eor>,<Eor>tags
DCL-specific fields:
DCL_QSL_RCVD: DCL confirmation status (Y/N/?)DCL_QSLRDATE: DCL confirmation date (YYYYMMDD)DARC_DOK: QSO partner's DOKMY_DARC_DOK: User's own DOKSTATION_CALLSIGN: User's callsign
Recent Commits
aeeb75c: feat: add QSO count display to filter section- Shows count of QSOs matching current filters next to "Filters" heading
- Displays "Showing X filtered QSOs" when filters are active
- Displays "Showing X total QSOs" when no filters applied
- Dynamically updates when filters change
bee02d1: fix: count QSOs confirmed by either LoTW or DCL in stats- QSO stats were only counting LoTW-confirmed QSOs (
lotwQslRstatus === 'Y') - QSOs confirmed only by DCL were excluded from "confirmed" count
- Fixed by changing filter to:
q.lotwQslRstatus === 'Y' || q.dclQslRstatus === 'Y' - Now correctly shows all QSOs confirmed by at least one system
- QSO stats were only counting LoTW-confirmed QSOs (
233888c: fix: make ADIF parser case-insensitive for EOR delimiter- Critical bug: LoTW uses lowercase
<eor>tags, parser was splitting on uppercase<EOR> - Caused 242K+ QSOs to be parsed as 1 giant record with fields overwriting each other
- Changed to case-insensitive regex:
new RegExp('<eor>', 'gi') - Replaced
regex.exec()while loop withmatchAll()for-of iteration - Now correctly imports all QSOs from large LoTW reports
- Critical bug: LoTW uses lowercase
645f786: fix: add missing timeOn field to LoTW duplicate detection- LoTW sync was missing
timeOnin duplicate detection query - Multiple QSOs with same callsign/date/band/mode but different times were treated as duplicates
- Now matches DCL sync logic:
userId, callsign, qsoDate, timeOn, band, mode
- LoTW sync was missing
7f77c3a: feat: add filter support for DOK awards- DOK award type now supports filtering by band, mode, and other QSO fields
- Allows creating award variants like DLD 80m, DLD CW, DLD 80m CW
- Uses existing filter system with eq, ne, in, nin, contains operators
- Example awards created: dld-80m, dld-40m, dld-cw, dld-80m-cw
9e73704: docs: update CLAUDE.md with DLD award variants documentation7201446: fix: return proper HTML for SPA routes instead of Bun error page- When accessing client-side routes (like /qsos) via curl or non-JS clients, the server attempted to open them as static files, causing Bun to throw an unhandled ENOENT error that showed an ugly error page
- Now checks if a path has a file extension before attempting to serve it
- Paths without extensions are immediately served index.html for SPA routing
- Also improves the 503 error page with user-friendly HTML when frontend build is missing
223461f: fix: enable debug logging and improve DCL sync observability27d2ef1: fix: preserve DOK data when DCL doesn't send values- DCL sync only updates DOK/grid fields when DCL provides non-empty values
- Prevents accidentally clearing DOK data from manual entry or other sources
- Preserves existing DOK when DCL syncs QSO without DOK information
e09ab94: feat: skip QSOs with unchanged confirmation data- LoTW/DCL sync only updates QSOs if confirmation data has changed
- Tracks added, updated, and skipped QSO counts
- LoTW: Checks if lotwQslRstatus or lotwQslRdate changed
- DCL: Checks if dclQslRstatus, dclQslRdate, darcDok, myDarcDok, or grid changed
3592dbb: feat: add import log showing synced QSOs- Backend returns addedQSOs and updatedQSOs arrays in sync result
- Frontend displays import log with callsign, date, band, mode for each QSO
- Separate sections for "New QSOs" and "Updated QSOs"
- Sync summary shows total, added, updated, skipped counts
8a1a580: feat: implement DCL ADIF parser and service integration- Add shared ADIF parser utility (src/backend/utils/adif-parser.js)
- Implement DCL service with API integration
- Refactor LoTW service to use shared parser
- Tested with example DCL payload (6 QSOs parsed successfully)
c982dcd: feat: implement DLD (Deutschland Diplom) award322ccaf: docs: add DLD (Deutschland Diplom) award documentation
Sync Behavior
Import Log: After each sync, displays a table showing:
- New QSOs: Callsign, Date, Band, Mode
- Updated QSOs: Callsign, Date, Band, Mode (only if data changed)
- Skipped QSOs: Counted but not shown (data unchanged)
Duplicate Handling:
- QSOs matched by: userId, callsign, qsoDate, timeOn, band, mode
- If confirmation data unchanged: Skipped (not updated)
- If confirmation data changed: Updated with new values
- Prevents unnecessary database writes and shows accurate import counts
DOK Update Behavior:
- If QSO imported via LoTW (no DOK) and later DCL confirms with DOK: DOK is added ✓
- If QSO already has DOK and DCL sends different DOK: DOK is updated ✓
- If QSO has DOK and DCL syncs without DOK (empty): Existing DOK is preserved ✓
- LoTW never sends DOK data; only DCL provides DOK fields
Important: DCL sync only updates DOK/grid fields when DCL provides non-empty values. This prevents accidentally clearing DOK data that was manually entered or imported from other sources.
DCL Sync Strategy
Current Behavior: DCL syncs ALL QSOs (confirmed + unconfirmed)
The application syncs both confirmed and unconfirmed QSOs from DCL:
- Confirmed QSOs:
dclQslRstatus = 'Y'- Count toward awards - Unconfirmed QSOs:
dclQslRstatus = 'N'- Stored but don't count toward awards
Purpose of syncing unconfirmed QSOs:
- Users can see who they've worked (via "Not Confirmed" filter)
- Track QSOs awaiting confirmation
- QSOs can get confirmed later and will be updated on next sync
Award Calculation: Always uses confirmed QSOs only (e.g., dclQslRstatus === 'Y' for DLD award)
DCL Incremental Sync Strategy
Challenge: Need to fetch both new QSOs AND confirmation updates to old QSOs
Example Scenario:
- Full sync on 2026-01-20 → Last QSO date: 2026-01-20
- User works 3 new QSOs on 2026-01-25 (unconfirmed)
- Old QSO from 2026-01-10 gets confirmed on 2026-01-26
- Next sync needs both: new QSOs (2026-01-25) AND confirmation update (2026-01-10)
Solution: Use both qso_since and qsl_since parameters with OR logic
// Proposed sync logic (requires OR logic from DCL API)
const lastQSODate = await getLastDCLQSODate(userId); // Track QSO dates
const lastQSLDate = await getLastDCLQSLDate(userId); // Track QSL dates
const requestBody = {
key: dclApiKey,
limit: 50000,
qso_since: lastQSODate, // Get new QSOs since last contact
qsl_since: lastQSLDate, // Get QSL confirmations since last sync
cnf_only: null, // Fetch all QSOs
};
Required API Behavior (OR Logic):
- Return QSOs where
(qso_date >= qso_since) OR (qsl_date >= qsl_since) - This ensures we get both new QSOs and confirmation updates
Current DCL API Status:
- Unknown if current API uses AND or OR logic for combined filters
- Action Needed: Request OR logic implementation from DARC
- Test current behavior to confirm API response pattern
Why OR Logic is Needed:
- With AND logic: Old QSOs getting confirmed are missed (qso_date too old)
- With OR logic: All updates captured efficiently in one API call
QSO Page Filters
The QSO page (src/frontend/src/routes/qsos/+page.svelte) includes advanced filtering capabilities:
Available Filters:
- Search Box: Full-text search across callsign, entity (DXCC country), and grid square fields
- Press Enter to apply search
- Case-insensitive partial matching
- Band Filter: Dropdown to filter by amateur band (160m, 80m, 60m, 40m, 30m, 20m, 17m, 15m, 12m, 10m, 6m, 2m, 70cm)
- Mode Filter: Dropdown to filter by mode (CW, SSB, AM, FM, RTTY, PSK31, FT8, FT4, JT65, JT9)
- Confirmation Type Filter: Filter by confirmation status
- "All QSOs": Shows all QSOs (no filter)
- "LoTW Only": Shows QSOs confirmed by LoTW but NOT DCL
- "DCL Only": Shows QSOs confirmed by DCL but NOT LoTW
- "Both Confirmed": Shows QSOs confirmed by BOTH LoTW AND DCL
- "Not Confirmed": Shows QSOs confirmed by NEITHER LoTW nor DCL
- Clear Button: Resets all filters and reloads all QSOs
Backend Implementation (src/backend/services/lotw.service.js):
getUserQSOs(userId, filters, options): Main filtering function- Supports pagination with
pageandlimitoptions - Filter logic uses Drizzle ORM query builders for safe SQL generation
- Debug logging when
LOG_LEVEL=debugshows applied filters
Frontend API (src/frontend/src/lib/api.js):
qsosAPI.getAll(filters): Fetch QSOs with optional filters- Filters passed as query parameters:
?band=20m&mode=CW&confirmationType=lotw&search=DL
QSO Count Display:
- Shows count of QSOs matching current filters next to "Filters" heading
- With filters active: "Showing X filtered QSOs"
- No filters: "Showing X total QSOs"
- Dynamically updates when filters are applied or cleared
- Uses
pagination.totalCountfrom backend API response
DXCC Entity Priority Logic
When syncing QSOs from multiple confirmation sources, the system follows a priority order for DXCC entity data:
Priority Order: LoTW > DCL
Implementation (src/backend/services/dcl.service.js):
// DXCC priority: LoTW > DCL
// Only update entity fields from DCL if:
// 1. QSO is NOT LoTW confirmed, AND
// 2. DCL actually sent entity data, AND
// 3. Current entity is missing
const hasLoTWConfirmation = existingQSO.lotwQslRstatus === 'Y';
const hasDCLData = dbQSO.entity || dbQSO.entityId;
const missingEntity = !existingQSO.entity || existingQSO.entity === '';
if (!hasLoTWConfirmation && hasDCLData && missingEntity) {
// Fill in entity data from DCL (only if DCL provides it)
updateData.entity = dbQSO.entity;
updateData.entityId = dbQSO.entityId;
// ... other entity fields
}
Rules:
- LoTW-confirmed QSOs: Always use LoTW's DXCC data (most reliable)
- DCL-only QSOs: Use DCL's DXCC data IF available in ADIF payload
- Empty entity fields: If DCL doesn't send DXCC data, entity remains empty
- Never overwrite: Once LoTW confirms with entity data, DCL sync won't change it
Important Note: DCL API currently doesn't send DXCC/entity fields in their ADIF export. This is a limitation of the DCL API, not the application. If DCL adds these fields in the future, the system will automatically use them for DCL-only QSOs.
Recent Development Work (January 2025)
QSO Page Enhancements:
- Added confirmation type filter with exclusive logic (LoTW Only, DCL Only, Both Confirmed, Not Confirmed)
- Added search box for filtering by callsign, entity, or grid square
- Renamed "All Confirmation" to "All QSOs" for clarity
- Fixed filter logic to properly handle exclusive confirmation types
Bug Fixes:
- Fixed confirmation filter showing wrong QSOs (e.g., "LoTW Only" was also showing DCL QSOs)
- Implemented proper SQL conditions for exclusive filters using separate condition pushes
- Added debug logging to track filter application
DXCC Entity Handling:
- Clarified that DCL API doesn't send DXCC fields (current limitation)
- Implemented priority logic: LoTW entity data takes precedence over DCL
- System ready to auto-use DCL DXCC data if they add it in future API updates
Critical LoTW Sync Behavior (LEARNED THE HARD WAY)
⚠️ IMPORTANT: LoTW sync MUST only import confirmed QSOs
After attempting to implement "QSO Delta" sync (all QSOs, confirmed + unconfirmed), we discovered:
The Problem:
LoTW ADIF export with qso_qsl=no (all QSOs mode) only includes:
CALL(callsign)QSL_RCVD(confirmation status: Y/N)
Missing Fields for Unconfirmed QSOs:
DXCC(entity ID) ← CRITICAL for awards!COUNTRY(entity name)CONTINENTCQ_ZONEITU_ZONE
Result: Unconfirmed QSOs have entityId: null and entity: "", breaking award calculations.
Current Implementation (CORRECT):
// lotw.service.js - fetchQSOsFromLoTW()
const params = new URLSearchParams({
login: lotwUsername,
password: loTWPassword,
qso_query: '1',
qso_qsl: 'yes', // ONLY confirmed QSOs
qso_qslsince: dateStr, // Incremental sync
});
Why This Matters:
- Awards require
entityIdto count entities - Without
entityId, QSOs can't be counted toward DXCC, WAS, etc. - Users can still see "worked" stations in QSO list, but awards only count confirmed
- DCL sync can import all QSOs because it provides entity data via callsign lookup
Attempted Solution (REVERTED):
- Tried implementing callsign prefix lookup to populate missing
entityId - Created
src/backend/utils/callsign-lookup.jswith basic prefix mappings - Complexity: 1000+ DXCC entities, many special event callsigns, portable designators
- Decision: Too complex, reverted (commit
310b154)
Takeaway: LoTW confirmed QSOs have reliable DXCC data. Don't try to workaround this fundamental limitation.
QSO Confirmation Filters
Added "Confirmed by at least 1 service" filter to QSO view (commit 688b0fc):
Filter Options:
- "All QSOs" - No filter
- "Confirmed by at least 1 service" (NEW) - LoTW OR DCL confirmed
- "LoTW Only" - Confirmed by LoTW but NOT DCL
- "DCL Only" - Confirmed by DCL but NOT LoTW
- "Both Confirmed" - Confirmed by BOTH LoTW AND DCL
- "Not Confirmed" - Confirmed by NEITHER
SQL Logic:
-- "Confirmed by at least 1 service"
WHERE lotwQslRstatus = 'Y' OR dclQslRstatus = 'Y'
-- "LoTW Only"
WHERE lotwQslRstatus = 'Y' AND (dclQslRstatus IS NULL OR dclQslRstatus != 'Y')
-- "DCL Only"
WHERE dclQslRstatus = 'Y' AND (lotwQslRstatus IS NULL OR lotwQslRstatus != 'Y')
-- "Both Confirmed"
WHERE lotwQslRstatus = 'Y' AND dclQslRstatus = 'Y'
-- "Not Confirmed"
WHERE (lotwQslRstatus IS NULL OR lotwQslRstatus != 'Y')
AND (dclQslRstatus IS NULL OR dclQslRstatus != 'Y')
Recent Development Work (January 2025)
Sync Type Support (ATTEMPTED & REVERTED):
- Commit
5b78935: Added LoTW sync type support (QSL/QSO delta/full) - Commit
310b154: Reverted - LoTW doesn't provide entity data for unconfirmed QSOs - Lesson: Keep it simple - only sync confirmed QSOs from LoTW
Dashboard Enhancements:
- Added sync job history display with real-time polling (every 2 seconds)
- Shows job progress, status, and import logs
- Cancel button for stale/failed jobs with rollback capability
- Tracks all QSO changes in
qso_changestable for rollback
Rollback System:
cancelJob(jobId, userId)- Cancels and rolls back sync jobs- Tracks added QSOs (deletes them on rollback)
- Tracks updated QSOs (restores previous state)
- Only allows canceling failed jobs or stale running jobs (>1 hour)
- Server-side validation prevents unauthorized cancellations
Award Detail View (January 2025)
Overview: The award detail page (src/frontend/src/routes/awards/[id]/+page.svelte) displays award progress in a pivot table format with entities as rows and band/mode combinations as columns.
Key Features:
- QSO Count per Slot: Each table cell shows the count of confirmed QSOs for that (entity, band, mode) combination
- Drill-Down: Click a count to open a modal showing all QSOs for that slot
- QSO Detail: Click any QSO in the list to view full QSO details
- Mode Filter: Filter by specific mode or view "Mixed Mode" (aggregates all modes by band)
Backend Changes (src/backend/services/awards.service.js):
calculateDOKAwardProgress(): Groups by (DOK, band, mode) slots, collects all confirmed QSOs inqsosarraycalculatePointsAwardProgress(): Updated for all count modes (perBandMode, perStation, perQso) withqsosarraygetAwardEntityBreakdown(): Groups by (entity, band, mode) slots for entity awards
Response Structure:
{
entity: "F03",
band: "80m",
mode: "CW",
worked: true,
confirmed: true,
qsos: [
{ qsoId: 123, callsign: "DK0MU", mode: "CW", qsoDate: "20250115", timeOn: "123456", confirmed: true },
{ qsoId: 456, callsign: "DL1ABC", mode: "CW", qsoDate: "20250120", timeOn: "234500", confirmed: true }
]
}
Mode Filter:
- Mixed Mode (default): Shows bands as columns, aggregates all modes
- Example: Columns are "80m", "40m", "20m"
- Clicking a count shows all QSOs for that band across all modes
- Specific Mode: Shows (band, mode) combinations as columns
- Example: Columns are "80m CW", "80m SSB", "40m CW"
- Filters to only show QSOs with that mode
Frontend Components:
- Mode Filter Dropdown: Located between summary cards and table
- Dynamically populated with available modes from the data
- Clear button appears when specific mode is selected
- Count Badges: Blue clickable links showing QSO count (removed bubbles, kept links)
- QSO List Modal: Shows all QSOs for selected slot with columns: Callsign, Date, Time, Mode
- QSO Detail Modal: Full QSO information (existing feature)
Files Modified:
src/backend/services/awards.service.js- Backend grouping and QSO collectionsrc/frontend/src/routes/awards/[id]/+page.svelte- Frontend display and interaction