diff --git a/CLAUDE.md b/CLAUDE.md index edeff31..b3aea7f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -467,3 +467,82 @@ Both LoTW and DCL return data in ADIF (Amateur Data Interchange Format): - 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. + +### 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 `page` and `limit` options +- Filter logic uses Drizzle ORM query builders for safe SQL generation +- Debug logging when `LOG_LEVEL=debug` shows 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` + +### 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`): +```javascript +// 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**: +1. **LoTW-confirmed QSOs**: Always use LoTW's DXCC data (most reliable) +2. **DCL-only QSOs**: Use DCL's DXCC data IF available in ADIF payload +3. **Empty entity fields**: If DCL doesn't send DXCC data, entity remains empty +4. **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 diff --git a/src/backend/services/lotw.service.js b/src/backend/services/lotw.service.js index 2251871..e7eb59d 100644 --- a/src/backend/services/lotw.service.js +++ b/src/backend/services/lotw.service.js @@ -322,6 +322,8 @@ export async function syncQSOs(userId, lotwUsername, lotwPassword, sinceDate = n export async function getUserQSOs(userId, filters = {}, options = {}) { const { page = 1, limit = 100 } = options; + logger.debug('getUserQSOs called', { userId, filters, options }); + const conditions = [eq(qsos.userId, userId)]; if (filters.band) conditions.push(eq(qsos.band, filters.band)); @@ -330,20 +332,31 @@ export async function getUserQSOs(userId, filters = {}, options = {}) { // Confirmation type filter: lotw, dcl, both, none if (filters.confirmationType) { + logger.debug('Applying confirmation type filter', { confirmationType: filters.confirmationType }); if (filters.confirmationType === 'lotw') { + // LoTW only: Confirmed by LoTW but NOT by DCL conditions.push(eq(qsos.lotwQslRstatus, 'Y')); + conditions.push( + sql`(${qsos.dclQslRstatus} IS NULL OR ${qsos.dclQslRstatus} != 'Y')` + ); } else if (filters.confirmationType === 'dcl') { + // DCL only: Confirmed by DCL but NOT by LoTW conditions.push(eq(qsos.dclQslRstatus, 'Y')); + conditions.push( + sql`(${qsos.lotwQslRstatus} IS NULL OR ${qsos.lotwQslRstatus} != 'Y')` + ); } else if (filters.confirmationType === 'both') { - conditions.push(and( - eq(qsos.lotwQslRstatus, 'Y'), - eq(qsos.dclQslRstatus, 'Y') - )); + // Both confirmed: Confirmed by LoTW AND DCL + conditions.push(eq(qsos.lotwQslRstatus, 'Y')); + conditions.push(eq(qsos.dclQslRstatus, 'Y')); } else if (filters.confirmationType === 'none') { - conditions.push(and( - sql`${qsos.lotwQslRstatus} IS NULL OR ${qsos.lotwQslRstatus} != 'Y'`, - sql`${qsos.dclQslRstatus} IS NULL OR ${qsos.dclQslRstatus} != 'Y'` - )); + // Not confirmed: Not confirmed by LoTW AND not confirmed by DCL + conditions.push( + sql`(${qsos.lotwQslRstatus} IS NULL OR ${qsos.lotwQslRstatus} != 'Y')` + ); + conditions.push( + sql`(${qsos.dclQslRstatus} IS NULL OR ${qsos.dclQslRstatus} != 'Y')` + ); } } diff --git a/src/frontend/src/routes/qsos/+page.svelte b/src/frontend/src/routes/qsos/+page.svelte index 19e0bb1..f97d55e 100644 --- a/src/frontend/src/routes/qsos/+page.svelte +++ b/src/frontend/src/routes/qsos/+page.svelte @@ -565,7 +565,7 @@