feat: add QSO page filters and fix DXCC entity priority
- Add confirmation type filter (LoTW Only, DCL Only, Both, None) - Add search box for callsign, entity, and grid square - Rename "All Confirmation" to "All QSOs" for clarity - Fix exclusive filter logic using separate SQL conditions - Add debug logging for filter troubleshooting - Implement DXCC priority: LoTW > DCL - Document QSO page filters and DXCC handling in CLAUDE.md Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
79
CLAUDE.md
79
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
|
- 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.
|
**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
|
||||||
|
|||||||
@@ -322,6 +322,8 @@ export async function syncQSOs(userId, lotwUsername, lotwPassword, sinceDate = n
|
|||||||
export async function getUserQSOs(userId, filters = {}, options = {}) {
|
export async function getUserQSOs(userId, filters = {}, options = {}) {
|
||||||
const { page = 1, limit = 100 } = options;
|
const { page = 1, limit = 100 } = options;
|
||||||
|
|
||||||
|
logger.debug('getUserQSOs called', { userId, filters, options });
|
||||||
|
|
||||||
const conditions = [eq(qsos.userId, userId)];
|
const conditions = [eq(qsos.userId, userId)];
|
||||||
|
|
||||||
if (filters.band) conditions.push(eq(qsos.band, filters.band));
|
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
|
// Confirmation type filter: lotw, dcl, both, none
|
||||||
if (filters.confirmationType) {
|
if (filters.confirmationType) {
|
||||||
|
logger.debug('Applying confirmation type filter', { confirmationType: filters.confirmationType });
|
||||||
if (filters.confirmationType === 'lotw') {
|
if (filters.confirmationType === 'lotw') {
|
||||||
|
// LoTW only: Confirmed by LoTW but NOT by DCL
|
||||||
conditions.push(eq(qsos.lotwQslRstatus, 'Y'));
|
conditions.push(eq(qsos.lotwQslRstatus, 'Y'));
|
||||||
|
conditions.push(
|
||||||
|
sql`(${qsos.dclQslRstatus} IS NULL OR ${qsos.dclQslRstatus} != 'Y')`
|
||||||
|
);
|
||||||
} else if (filters.confirmationType === 'dcl') {
|
} else if (filters.confirmationType === 'dcl') {
|
||||||
|
// DCL only: Confirmed by DCL but NOT by LoTW
|
||||||
conditions.push(eq(qsos.dclQslRstatus, 'Y'));
|
conditions.push(eq(qsos.dclQslRstatus, 'Y'));
|
||||||
|
conditions.push(
|
||||||
|
sql`(${qsos.lotwQslRstatus} IS NULL OR ${qsos.lotwQslRstatus} != 'Y')`
|
||||||
|
);
|
||||||
} else if (filters.confirmationType === 'both') {
|
} else if (filters.confirmationType === 'both') {
|
||||||
conditions.push(and(
|
// Both confirmed: Confirmed by LoTW AND DCL
|
||||||
eq(qsos.lotwQslRstatus, 'Y'),
|
conditions.push(eq(qsos.lotwQslRstatus, 'Y'));
|
||||||
eq(qsos.dclQslRstatus, 'Y')
|
conditions.push(eq(qsos.dclQslRstatus, 'Y'));
|
||||||
));
|
|
||||||
} else if (filters.confirmationType === 'none') {
|
} else if (filters.confirmationType === 'none') {
|
||||||
conditions.push(and(
|
// Not confirmed: Not confirmed by LoTW AND not confirmed by DCL
|
||||||
sql`${qsos.lotwQslRstatus} IS NULL OR ${qsos.lotwQslRstatus} != 'Y'`,
|
conditions.push(
|
||||||
sql`${qsos.dclQslRstatus} IS NULL OR ${qsos.dclQslRstatus} != 'Y'`
|
sql`(${qsos.lotwQslRstatus} IS NULL OR ${qsos.lotwQslRstatus} != 'Y')`
|
||||||
));
|
);
|
||||||
|
conditions.push(
|
||||||
|
sql`(${qsos.dclQslRstatus} IS NULL OR ${qsos.dclQslRstatus} != 'Y')`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -565,7 +565,7 @@
|
|||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select bind:value={filters.confirmationType} on:change={applyFilters} class="confirmation-filter">
|
<select bind:value={filters.confirmationType} on:change={applyFilters} class="confirmation-filter">
|
||||||
<option value="all">All Confirmation</option>
|
<option value="all">All QSOs</option>
|
||||||
<option value="lotw">LoTW Only</option>
|
<option value="lotw">LoTW Only</option>
|
||||||
<option value="dcl">DCL Only</option>
|
<option value="dcl">DCL Only</option>
|
||||||
<option value="both">Both Confirmed</option>
|
<option value="both">Both Confirmed</option>
|
||||||
|
|||||||
Reference in New Issue
Block a user