diff --git a/CLAUDE.md b/CLAUDE.md index 6733ae0..1be9b07 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -170,6 +170,8 @@ The award system is JSON-driven and located in `award-definitions/` directory. E 1. **`entity`**: Count unique entities (DXCC countries, states, grid squares) - `entityType`: What to count ("dxcc", "state", "grid", "callsign") - `target`: Number required for award + - `allowed_bands`: Optional array of bands that count (e.g., `["160m", "80m", "40m", "30m", "20m", "17m", "15m", "12m", "10m"]` for HF only) + - `satellite_only`: Optional boolean to only count satellite QSOs (QSOs with `satName` field) - `filters`: Optional filters (band, mode, etc.) - `displayField`: Optional field to display @@ -179,7 +181,6 @@ The award system is JSON-driven and located in `award-definitions/` directory. E - `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 3. **`points`**: Point-based awards - `stations`: Array of {callsign, points} @@ -192,6 +193,16 @@ The award system is JSON-driven and located in `award-definitions/` directory. E 5. **`counter`**: Count QSOs or callsigns +### Current Awards + +- **DXCC**: HF bands only (160m-10m), 100 entities required +- **DXCC SAT**: Satellite QSOs only, 100 entities required +- **WAS**: Worked All States award +- **VUCC SAT**: VUCC Satellite award +- **SAT-RS44**: Special satellite award +- **73 on 73**: Special stations award +- **DLD**: Deutschland Diplom, 100 unique DOKs required + ### Key Files **Backend Award Service**: `src/backend/services/awards.service.js` @@ -201,11 +212,13 @@ The award system is JSON-driven and located in `award-definitions/` directory. E - `calculatePointsAwardProgress(userId, award, options)`: Point-based calculation - `getAwardEntityBreakdown(userId, awardId)`: Detailed entity breakdown - `getAwardProgressDetails(userId, awardId)`: Progress with details +- Implements `allowed_bands` and `satellite_only` filtering **Database Schema**: `src/backend/db/schema/index.js` -- QSO fields include: `darcDok`, `dclQslRstatus`, `dclQslRdate` +- QSO fields include: `darcDok`, `dclQslRstatus`, `dclQslRdate`, `satName` - DOK fields support DLD award tracking - DCL confirmation fields separate from LoTW +- `satName` field for satellite QSO tracking **Award Definitions**: `award-definitions/*.json` - Add new awards by creating JSON definition files @@ -216,7 +229,6 @@ The award system is JSON-driven and located in `award-definitions/` directory. E - Handles case-insensitive `` delimiters (supports ``, ``, ``) - Uses `matchAll()` for reliable field parsing - Skips header records automatically -- `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 @@ -237,6 +249,7 @@ The award system is JSON-driven and located in `award-definitions/` directory. E - `POST /api/dcl/sync`: Queue DCL sync job - `GET /api/jobs/:jobId`: Get job status - `GET /api/jobs/active`: Get active job for current user +- `DELETE /api/qsos/all`: Delete all QSOs for authenticated user - `GET /*`: Serves static files from `src/frontend/build/` with SPA fallback **SPA Routing**: The backend serves the SvelteKit frontend build from `src/frontend/build/`. @@ -262,9 +275,9 @@ The award system is JSON-driven and located in `award-definitions/` directory. E - Fully implemented and functional - **Note**: DCL API is a custom prototype by DARC; contact DARC for API specification details -### DLD Award Implementation (COMPLETED) +### DLD Award Implementation -The DLD (Deutschland Diplom) award was recently implemented: +The DLD (Deutschland Diplom) award: **Definition**: `award-definitions/dld.json` ```json @@ -284,7 +297,7 @@ The DLD (Deutschland Diplom) award was recently implemented: ``` **Implementation Details**: -- Function: `calculateDOKAwardProgress()` in `src/backend/services/awards.service.js` (lines 173-268) +- Function: `calculateDOKAwardProgress()` in `src/backend/services/awards.service.js` - Counts unique (DOK, band, mode) combinations - Only DCL-confirmed QSOs count (`dclQslRstatus === 'Y'`) - Each unique DOK on each unique band/mode counts separately @@ -297,8 +310,6 @@ The DLD (Deutschland Diplom) award was recently implemented: - `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 @@ -322,79 +333,42 @@ To add a new award: 3. If new rule type needed, add calculation function 4. Add type handling in `calculateAwardProgress()` switch statement 5. Add type handling in `getAwardEntityBreakdown()` if needed -6. Update documentation in `docs/DOCUMENTATION.md` +6. Update documentation 7. Test with sample QSO data -### Creating DLD Award Variants +### Award Rule Options -The DOK award type supports filters to create award variants. Examples: - -**DLD on 80m** (`dld-80m.json`): -```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`): +**allowed_bands**: Restrict which bands count toward an award ```json { "rules": { - "type": "dok", - "target": 100, - "confirmationType": "dcl", - "filters": { - "operator": "AND", - "filters": [ - { "field": "mode", "operator": "eq", "value": "CW" } - ] - } + "type": "entity", + "allowed_bands": ["160m", "80m", "60m", "40m", "30m", "20m", "17m", "15m", "12m", "10m"] } } ``` +- If absent or empty, all bands are allowed (default behavior) +- Used for DXCC to restrict to HF bands only -**DLD on 80m using CW** (combined filters, `dld-80m-cw.json`): +**satellite_only**: Only count satellite QSOs ```json { "rules": { - "type": "dok", - "target": 100, - "confirmationType": "dcl", - "filters": { - "operator": "AND", - "filters": [ - { "field": "band", "operator": "eq", "value": "80m" }, - { "field": "mode", "operator": "eq", "value": "CW" } - ] - } + "type": "entity", + "satellite_only": true } } ``` +- If `true`, only QSOs with `satName` field set are counted +- Used for DXCC SAT award -**Available filter operators**: +**filters**: Additional filtering options - `eq`: equals - `ne`: not equals - `in`: in array - `nin`: not in array - `contains`: contains substring - -**Available filter fields**: Any QSO field (band, mode, callsign, grid, state, satName, etc.) +- Can filter any QSO field (band, mode, callsign, grid, state, etc.) ### Confirmation Systems @@ -414,13 +388,8 @@ The DOK award type supports filters to create award variants. Examples: - 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 QSOs - - `qso_since` - QSOs since this date (YYYYMMDD) - - `qsl_since` - QSL confirmations since this date (YYYYMMDD) - Response format: JSON with ADIF string in `adif` field - Syncs ALL QSOs (both confirmed and unconfirmed) - - Unconfirmed QSOs stored but don't count toward awards - Updates QSOs only if confirmation data has changed ### ADIF Format @@ -439,138 +408,13 @@ Both LoTW and DCL return data in ADIF (Amateur Data Interchange Format): - `MY_DARC_DOK`: User's own DOK - `STATION_CALLSIGN`: User's callsign -### Recent Commits +### QSO Management -- `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 -- `233888c`: fix: make ADIF parser case-insensitive for EOR delimiter - - **Critical bug**: LoTW uses lowercase `` tags, parser was splitting on uppercase `` - - Caused 242K+ QSOs to be parsed as 1 giant record with fields overwriting each other - - Changed to case-insensitive regex: `new RegExp('', 'gi')` - - Replaced `regex.exec()` while loop with `matchAll()` for-of iteration - - Now correctly imports all QSOs from large LoTW reports -- `645f786`: fix: add missing timeOn field to LoTW duplicate detection - - LoTW sync was missing `timeOn` in 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` -- `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 documentation -- `7201446`: 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 observability -- `27d2ef1`: 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) award -- `322ccaf`: 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**: -1. Full sync on 2026-01-20 → Last QSO date: 2026-01-20 -2. User works 3 new QSOs on 2026-01-25 (unconfirmed) -3. Old QSO from 2026-01-10 gets confirmed on 2026-01-26 -4. 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 - -```javascript -// 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 +**Delete All QSOs**: `DELETE /api/qsos/all` +- Deletes all QSOs for authenticated user +- Also deletes related `qso_changes` records to satisfy foreign key constraints +- Invalidates stats and user caches after deletion +- Returns count of deleted QSOs ### QSO Page Filters @@ -578,34 +422,45 @@ The QSO page (`src/frontend/src/routes/qsos/+page.svelte`) includes advanced fil **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 + - "All QSOs", "LoTW Only", "DCL Only", "Both Confirmed", "Not Confirmed" +- **Clear Button**: Resets all filters **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` -**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.totalCount` from backend API response +### Award Detail View + +**Overview**: The award detail page (`src/frontend/src/routes/awards/[id]/+page.svelte`) displays award progress in a pivot table format. + +**Key Features**: +- **Summary Cards**: Show total, confirmed, worked, needed counts for unique entities +- **Mode Filter**: Filter by specific mode or view "Mixed Mode" (aggregates all modes by band) +- **Table Columns**: Show bands (or band/mode combinations) as columns +- **QSO Counts**: Each cell shows count of confirmed QSOs for that (entity, band, mode) slot +- **Drill-Down**: Click a count to open modal showing all QSOs for that slot +- **QSO Detail**: Click any QSO to view full QSO details +- **Satellite Grouping**: Satellite QSOs grouped under "SAT" column instead of frequency band + +**Column Sorting**: Bands sorted by wavelength (longest to shortest): +160m, 80m, 60m, 40m, 30m, 20m, 17m, 15m, 12m, 10m, 6m, 2m, 70cm, SAT + +**Column Sums**: Show unique entity count per column (not QSO counts) + +**Backend Changes** (`src/backend/services/awards.service.js`): +- `calculateDOKAwardProgress()`: Groups by (DOK, band, mode) slots, collects QSOs in `qsos` array +- `calculatePointsAwardProgress()`: Handles all count modes with `qsos` array +- `getAwardEntityBreakdown()`: Groups by (entity, band, mode) slots +- Includes `satName` in QSO data for satellite grouping +- Implements `allowed_bands` and `satellite_only` filtering ### DXCC Entity Priority Logic @@ -613,58 +468,18 @@ When syncing QSOs from multiple confirmation sources, the system follows a prior **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. +**Important Note**: DCL API currently doesn't send DXCC/entity fields in their ADIF export. -### 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) +### Critical LoTW Sync Behavior **⚠️ 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) @@ -672,9 +487,7 @@ LoTW ADIF export with `qso_qsl=no` (all QSOs mode) only includes: **Missing Fields for Unconfirmed QSOs:** - `DXCC` (entity ID) ← **CRITICAL for awards!** - `COUNTRY` (entity name) -- `CONTINENT` -- `CQ_ZONE` -- `ITU_ZONE` +- `CONTINENT`, `CQ_ZONE`, `ITU_ZONE` **Result:** Unconfirmed QSOs have `entityId: null` and `entity: ""`, breaking award calculations. @@ -690,117 +503,22 @@ const params = new URLSearchParams({ }); ``` -**Why This Matters:** -- Awards require `entityId` to 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 +### Recent Development Work (January 2026) -**Attempted Solution (REVERTED):** -- Tried implementing callsign prefix lookup to populate missing `entityId` -- Created `src/backend/utils/callsign-lookup.js` with basic prefix mappings -- Complexity: 1000+ DXCC entities, many special event callsigns, portable designators -- Decision: Too complex, reverted (commit 310b154) +**Award System Enhancements**: +- Added `allowed_bands` filter to restrict which bands count toward awards +- Added `satellite_only` flag for satellite-only awards +- DXCC restricted to HF bands (160m-10m) only +- Added DXCC SAT award for satellite-only QSOs +- Removed redundant award variants (DXCC CW, DLD variants) -**Takeaway:** LoTW confirmed QSOs have reliable DXCC data. Don't try to workaround this fundamental limitation. +**Award Detail View Improvements**: +- Summary shows unique entity progress instead of QSO counts +- Column sums count unique entities per column +- Satellite QSOs grouped under "SAT" column +- Bands sorted by wavelength instead of alphabetically +- Mode removed from table headers (visible in filter dropdown) -### 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:** -```sql --- "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_changes` table 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 in `qsos` array -- `calculatePointsAwardProgress()`: Updated for all count modes (perBandMode, perStation, perQso) with `qsos` array -- `getAwardEntityBreakdown()`: Groups by (entity, band, mode) slots for entity awards - -**Response Structure**: -```javascript -{ - 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 collection -- `src/frontend/src/routes/awards/[id]/+page.svelte` - Frontend display and interaction +**QSO Management**: +- Fixed DELETE /api/qsos/all to handle foreign key constraints +- Added cache invalidation after QSO deletion