Commit Graph

74 Commits

Author SHA1 Message Date
b09e2b3ea2 feat: add achievements system to awards with mode filter support
Implement achievements milestone feature for awards with configurable
levels (Silver, Gold, Platinum, etc.) that track progress beyond the
base award target. Achievements display earned badges and progress bar
toward next level.

Backend:
- Add calculateAchievementProgress() helper in awards.service.js
- Include achievements field in getAllAwards() and getAwardById()
- Add achievements validation in awards-admin.service.js
- Update PUT endpoint validation schema to include achievements field

Frontend:
- Add achievements section to award detail page with gold badges
- Add reactive achievement progress calculation that respects mode filter
- Add achievements tab to admin create/edit pages with full CRUD

Award Definitions:
- Add achievements to DXCC (100/200/300/500)
- Add achievements to DLD (50/100/200/300)
- Add achievements to WAS (30/40/50)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-23 12:42:32 +01:00
d1e4c39ad6 feat: add last_seen tracking for users
Adds last_seen field to track when users last accessed the tool:
- Add lastSeen column to users table schema (nullable timestamp)
- Create migration to add last_seen column to existing databases
- Add updateLastSeen() function to auth.service.js
- Update auth derive middleware to update last_seen on each authenticated request (async, non-blocking)
- Add lastSeen to admin getUserStats() query for display in admin users table
- Add "Last Seen" column to admin users table in frontend

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-23 09:57:45 +01:00
36453c8922 fix: resolve stations editor reactivity issue in award admin
Use Svelte stores with value/on:input instead of bind:value for
stations array to properly track nested property changes. Add invisible
reactivity triggers to force Svelte to track station callsign/points properties.

Also update award sorting to handle numeric prefixes in numerical order
(44 before 73) and update RS-44 satellite award category.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-23 09:27:08 +01:00
bd89ea0855 feat: implement award definition editor with safety validation
Add comprehensive admin-only UI for managing award definitions stored as JSON files.

Backend changes:
- Add awards-admin.service.js with file operations and validation
- Add clearAwardCache() function to invalidate in-memory cache after changes
- Add API routes: GET/POST/PUT/DELETE /api/admin/awards, POST /api/admin/awards/:id/test
- Support testing unsaved awards by passing award definition directly

Frontend changes:
- Add awards list view at /admin/awards
- Add create form at /admin/awards/create with safety checks for:
  - Impossible filter combinations (e.g., mode=CW AND mode=SSB)
  - Redundant filters and mode groups
  - Logical contradictions (e.g., satellite_only with HF-only bands)
  - Duplicate callsigns, empty mode groups, etc.
- Add edit form at /admin/awards/[id] with same validation
- Add FilterBuilder component for nested filter structures
- Add TestAwardModal with deep validation and test calculation
- Add Awards tab to admin dashboard

Safety validation includes:
- Schema validation (required fields, types, formats)
- Business rule validation (valid rule types, operators, bands, modes)
- Cross-field validation (filter contradictions, allowed_bands conflicts)
- Edge case detection (complex filters, impossible targets)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-23 08:16:28 +01:00
cd361115fe feat: add configurable mode groups to award detail view
Add per-award configurable mode groups for filtering multiple modes
together in the award detail view. Mode groups are displayed with
visual separators in the mode filter dropdown.

Backend changes:
- Add modeGroups to getAllAwards() return mapping
- Add getAwardById() function to fetch single award definition
- Add GET /api/awards/:awardId endpoint

Frontend changes:
- Fetch award definition separately to get modeGroups
- Update availableModes to include mode groups with separator
- Update filteredEntities logic to handle mode groups
- Update groupDataForTable() and applyFilter() for mode groups
- Disable separator option in dropdown

Award definitions:
- DXCC: Add Digi-Modes, Classic Digi-Modes, Mixed-Mode w/o WSJT-Modes,
  Phone-Modes groups
- DLD: Add same mode groups (adjusted for available modes)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-23 07:06:12 +01:00
69b33720b3 fix: prevent infinite retry loop for auto-sync users without credentials
When auto-sync is enabled but credentials (LoTW/DCL) are removed, the
scheduler would continuously try to sync every minute, logging the same
warning forever.

Now:
- Split pending users into those with and without credentials
- For users without credentials, update nextSyncAt to retry in 24 hours
- Log a warning with affected user IDs
- Only return users with valid credentials for job processing

This prevents log spam and unnecessary database queries while still
periodically checking if credentials have been restored.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-22 13:17:16 +01:00
648cf2c5a5 feat: implement auto-sync scheduler for LoTW and DCL
Add automatic synchronization scheduler that allows users to configure
periodic sync intervals for LoTW and DCL via the settings page.

Features:
- Users can enable/disable auto-sync per service (LoTW/DCL)
- Configurable sync intervals (1-720 hours)
- Settings page UI for managing auto-sync preferences
- Dashboard shows upcoming scheduled auto-sync jobs
- Scheduler runs every minute, triggers syncs when due
- Survives server restarts via database persistence
- Graceful shutdown support (SIGINT/SIGTERM)

Backend:
- New autoSyncSettings table with user preferences
- auto-sync.service.js for CRUD operations and scheduling logic
- scheduler.service.js for periodic tick processing
- API endpoints: GET/PUT /auto-sync/settings, GET /auto-sync/scheduler/status

Frontend:
- Auto-sync settings section in settings page
- Upcoming auto-sync section on dashboard with scheduled job cards
- Purple-themed UI for scheduled jobs with countdown animation

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-22 12:40:55 +01:00
cce520a00e chore: code cleanup - remove duplicates and add caching
- Delete duplicate getCacheStats() function in cache.service.js
- Fix date calculation bug in lotw.service.js (was Date.now()-Date.now())
- Extract duplicate helper functions (yieldToEventLoop, getQSOKey) to sync-helpers.js
- Cache award definitions in memory to avoid repeated file I/O
- Delete unused parseDCLJSONResponse() function
- Remove unused imports (getPerformanceSummary, resetPerformanceMetrics)
- Auto-discover award JSON files instead of hardcoded list

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-22 10:22:00 +01:00
ebdd75e03f fix: invalidate caches after deleting QSOs
After deleting all QSOs, invalidate the stats and user caches so the
QSO page shows updated statistics instead of stale cached data.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-22 09:28:37 +01:00
205b311244 fix: handle foreign key constraints when deleting QSOs
The qso_changes table has a foreign key reference to qsos.id, which
was preventing QSO deletion. Now deletes related qso_changes records
first before deleting QSOs.

Also added better error logging to the DELETE endpoint.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-22 09:26:43 +01:00
6bc0a2f9b2 fix: return correct count from deleteQSOs function
The db.delete() returns a result object with a 'changes' property
indicating the number of affected rows, not the count directly.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-22 09:22:13 +01:00
8550b91255 feat: add DXCC SAT award for satellite-only QSOs
Added new award "DXCC SAT" that only counts satellite QSOs (QSOs with
satName field set). This adds a new "satellite_only" key to award
definitions that filters to only include satellite communications.

Award definition:
- ID: dxcc-sat
- Name: DXCC SAT
- Target: 100 DXCC entities
- Only satellite QSOs count

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-22 08:25:13 +01:00
a93d4ff85b refactor: remove DLD 80m CW award variant
Removed dld-80m-cw.json award definition. Only the main DLD award
remains, which covers all bands and modes.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-22 08:22:58 +01:00
f3ee1be651 refactor: remove DLD variant awards (80m, 40m, CW)
Removed the following DLD award variants:
- dld-80m.json
- dld-40m.json
- dld-cw.json

Kept dld-80m-cw.json as it represents a more specific combination.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-22 08:22:09 +01:00
6c9aa1efe7 feat: add allowed_bands filter to award definitions
Adds a new "allowed_bands" key to award definitions that restricts which
bands count toward an award. If absent, all bands are allowed (default
behavior).

Applied to DXCC award to only count HF bands (160m-10m), excluding
VHF/UHF bands like 6m, 2m, and 70cm.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-22 08:19:32 +01:00
14c7319c9e refactor: remove DXCC CW award and rename DXCC Mixed Mode to DXCC
- Removed dxcc-cw.json award definition
- Renamed "DXCC Mixed Mode" to "DXCC" in dxcc.json
- Changed award ID from "dxcc-mixed" to "dxcc"
- Removed dxcc-cw.json from awards service file list

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-22 08:14:33 +01:00
dc34fc20b1 feat: group satellite QSOs under SAT column in award detail
Satellite QSOs are now grouped under a "SAT" column instead of their
frequency band. The backend now includes satName in QSO data, and the
frontend detects satellite QSOs and groups them appropriately.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-22 08:03:03 +01:00
dd3beef9af feat: add award detail view with QSO count per slot and mode filter
- 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>
2026-01-22 07:34:55 +01:00
bdd8aa497d fix: admin action log and impersonation improvements
- Fix admin action log not displaying entries (use raw sqlite for self-join)
- Add global impersonation banner to all pages during impersonation
- Fix timestamp display in action log (convert Unix seconds to milliseconds)
- Add loginWithToken method to auth store for direct token authentication
- Fix /api/auth/me to include impersonatedBy field from JWT
- Remove duplicate impersonation code from admin page

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-21 18:26:20 +01:00
7c209e3270 fix: correct last-sync date and logout redirect issues
- Fix admin users last-sync showing 1970 instead of actual sync date
  - Changed from MAX(qsos.createdAt) to MAX(syncJobs.completedAt)
  - Added timestamp conversion (seconds to milliseconds) for proper Date serialization
- Fix logout redirect not working from admin dashboard
  - Changed from goto() to window.location.href for hard redirect
  - Ensures proper navigation after auth state changes

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-21 17:49:27 +01:00
c56226e05b feat: add 73 on 73 satellite award
Add new award for confirming 73 unique QSO partners via AO-73 satellite.
Counts unique callsigns confirmed via LoTW with satName filter.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-21 13:48:44 +01:00
8f8abfc651 refactor: remove redundant role field, keep only is_admin
- Remove role column from users schema (migration 0003)
- Update auth and admin services to use is_admin only
- Remove role from JWT token payloads
- Update admin CLI to use is_admin field
- Update frontend admin page to use isAdmin boolean
- Fix security: remove console.log dumping credentials in settings

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-21 11:41:41 +01:00
7026f2bca7 perf: optimize LoTW and DCL sync with batch operations
Fixes frontend freeze during large sync operations (8000+ QSOs).

Root cause: Sequential processing with individual database operations
(~24,000 queries for 8000 QSOs) blocked the event loop, preventing
polling requests from being processed.

Changes:
- Process QSOs in batches of 100
- Single SELECT query per batch for duplicate detection
- Batch INSERTs for new QSOs and change tracking
- Add yield points (setImmediate) after each batch to allow
  event loop processing of polling requests

Performance: ~98% reduction in database operations
Before: 8000 QSOs × 3 queries = ~24,000 sequential operations
After: 80 batches × ~4 operations = ~320 operations

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-21 10:28:24 +01:00
e88537754f feat: implement comprehensive admin functionality
- Add admin role system with role and isAdmin fields to users table
- Create admin_actions audit log table for tracking all admin operations
- Implement admin CLI tool for user management (create, promote, demote, list, check)
- Add admin authentication with role-based access control
- Create admin service layer with system statistics and user management
- Implement user impersonation system with proper security checks
- Add admin API endpoints for user management and system statistics
- Create admin dashboard UI with overview, users, and action logs
- Fix admin stats endpoint and user deletion with proper foreign key handling
- Add admin link to navigation bar for admin users

Database:
- Add role and isAdmin columns to users table
- Create admin_actions table for audit trail
- Migration script: add-admin-functionality.js

CLI:
- src/backend/scripts/admin-cli.js - Admin user management tool

Backend:
- src/backend/services/admin.service.js - Admin business logic
- Updated auth.service.js with admin helper functions
- Enhanced index.js with admin routes and middleware
- Export sqlite connection from config for raw SQL operations

Frontend:
- src/frontend/src/routes/admin/+page.svelte - Admin dashboard
- Updated api.js with adminAPI functions
- Added Admin link to navigation bar

Security:
- Admin-only endpoints with role verification
- Audit logging for all admin actions
- Impersonation with 1-hour token expiration
- Foreign key constraint handling for user deletion
- Cannot delete self or other admins
- Last admin protection
2026-01-21 09:43:56 +01:00
fe305310b9 feat: implement Phase 2 - caching, performance monitoring, and health dashboard
Phase 2.1: Basic Caching Layer
- Add QSO statistics caching with 5-minute TTL
- Implement cache hit/miss tracking
- Add automatic cache invalidation after LoTW/DCL syncs
- Achieve 601x faster cache hits (12ms → 0.02ms)
- Reduce database load by 96% for repeated requests

Phase 2.2: Performance Monitoring
- Create comprehensive performance monitoring system
- Track query execution times with percentiles (P50/P95/P99)
- Detect slow queries (>100ms) and critical queries (>500ms)
- Implement performance ratings (EXCELLENT/GOOD/SLOW/CRITICAL)
- Add performance regression detection (2x slowdown)

Phase 2.3: Cache Invalidation Hooks
- Invalidate stats cache after LoTW sync completes
- Invalidate stats cache after DCL sync completes
- Automatic 5-minute TTL expiration

Phase 2.4: Monitoring Dashboard
- Enhance /api/health endpoint with performance metrics
- Add cache statistics (hit rate, size, hits/misses)
- Add uptime tracking
- Provide real-time monitoring via REST API

Files Modified:
- src/backend/services/cache.service.js (stats cache, hit/miss tracking)
- src/backend/services/lotw.service.js (cache + performance tracking)
- src/backend/services/dcl.service.js (cache invalidation)
- src/backend/services/performance.service.js (NEW - complete monitoring system)
- src/backend/index.js (enhanced health endpoint)

Performance Results:
- Cache hit time: 0.02ms (601x faster than database)
- Cache hit rate: 91.67% (10 queries)
- Database load: 96% reduction
- Average query time: 3.28ms (EXCELLENT rating)
- Slow queries: 0
- Critical queries: 0

Health Endpoint API:
- GET /api/health returns:
  - status, timestamp, uptime
  - performance metrics (totalQueries, avgTime, slow/critical, topSlowest)
  - cache stats (hitRate, total, size, hits/misses)
2026-01-21 07:41:12 +01:00
21263e6735 feat: optimize QSO statistics query with SQL aggregates and indexes
Replace memory-intensive approach (load all QSOs) with SQL aggregates:
- Query time: 5-10s → 3.17ms (62-125x faster)
- Memory usage: 100MB+ → <1MB (100x less)
- Concurrent users: 2-3 → 50+ (16-25x more)

Add 3 critical database indexes for QSO statistics:
- idx_qsos_user_primary: Primary user filter
- idx_qsos_user_unique_counts: Unique entity/band/mode counts
- idx_qsos_stats_confirmation: Confirmation status counting

Total: 10 performance indexes on qsos table

Tested with 8,339 QSOs:
- Query time: 3.17ms (target: <100ms) 
- All tests passed
- API response format unchanged
- Ready for production deployment
2026-01-21 07:11:21 +01:00
db0145782a security: implement multiple security hardening fixes
This commit addresses several critical and high-severity security
vulnerabilities identified in a comprehensive security audit:

Critical Fixes:
- Enforce JWT_SECRET in production (throws error if not set)
- Add JWT token expiration (24 hours)
- Implement path traversal protection for static file serving
- Add rate limiting to authentication endpoints (5/10 req per minute)
- Fix CORS to never allow all origins (even in development)

High/Medium Fixes:
- Add comprehensive security headers (CSP, HSTS, X-Frame-Options, etc.)
- Implement stricter email validation (RFC 5321 compliant)
- Add input sanitization for search parameters (length limit, wildcard removal)
- Improve job/QSO ID validation (range checks, safe integer validation)

Files modified:
- src/backend/config.js: JWT secret enforcement
- src/backend/index.js: JWT expiration, security headers, rate limiting,
  email validation, path traversal protection, CORS hardening, ID validation
- src/backend/services/lotw.service.js: search input sanitization

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-20 17:40:31 +01:00
310b1547c4 revert: remove LoTW sync type support (QSL/QSO delta/full)
This reverts commit 5b78935 which added:
- Sync type parameter (qsl_delta, qsl_full, qso_delta, qso_full)
- getLastLoTWQSODate() function
- Sync type dropdown on QSO page
- Job queue handling of sync types

Reason: LoTW doesn't provide DXCC entity data for unconfirmed QSOs,
which causes award calculation issues. Going back to QSL-only sync.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-20 12:28:23 +01:00
688b0fc255 feat: add "Confirmed by at least 1 service" filter to QSO view
- Add 'any' confirmation type filter showing QSOs confirmed by LoTW OR DCL
- Backend logic: lotwQslRstatus = 'Y' OR dclQslRstatus = 'Y'
- Frontend dropdown option positioned after "All QSOs"
- Shows all QSOs confirmed by at least one service (LoTW, DCL, or both)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-20 12:14:52 +01:00
5b7893536e feat: add LoTW sync type support (QSL/QSO delta/full)
- Add syncType parameter to LoTW sync: 'qsl_delta', 'qsl_full', 'qso_delta', 'qso_full'
- qsl_* = only confirmed QSOs (qso_qsl=yes)
- qso_* = all QSOs confirmed+unconfirmed (qso_qsl=no)
- delta = incremental sync with date filter
- full = sync all records without date filter
- Add getLastLoTWQSODate() for QSO-based incremental sync
- Add sync type dropdown selector on QSO page
- Update job queue service to handle sync types
- Update API endpoint to accept syncType in request body

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-20 12:09:28 +01:00
a50b4ae724 feat: add sync job cancel and rollback with real-time updates
Implement comprehensive sync job management with rollback capabilities
and real-time status updates on the dashboard.

## Features

### Cancel & Rollback
- Users can cancel failed or stale (>1h) sync jobs
- Rollback deletes added QSOs and restores updated QSOs to previous state
- Uses qso_changes table to track all modifications with before/after snapshots
- Server-side validation prevents cancelling completed or active jobs

### Database Changes
- Add qso_changes table to track QSO modifications per job
- Stores change type (added/updated), before/after data snapshots
- Enables precise rollback of sync operations
- Migration script included

### Real-time Updates
- Dashboard now polls for job updates every 2 seconds
- Smart polling: starts when jobs active, stops when complete
- Job status badges update in real-time (pending → running → completed)
- Cancel button appears/disappears based on job state

### Backend
- Fixed job ordering to show newest first (desc createdAt)
- Track all QSO changes during LoTW/DCL sync operations
- cancelJob() function handles rollback logic
- DELETE /api/jobs/:jobId endpoint for cancelling jobs

### Frontend
- jobsAPI.cancel() method for cancelling jobs
- Dashboard shows last 5 sync jobs with status, stats, duration
- Real-time job status updates via polling
- Cancel button with confirmation dialog
- Loading state and error handling

### Logging Fix
- Changed from Bun.write() to fs.appendFile() for reliable log appending
- Logs now persist across server restarts instead of being truncated

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-20 11:46:19 +01:00
6b195d3014 feat: add comprehensive logging system with file output
- Add backend logging to logs/backend.log with file rotation support
- Add frontend logging to logs/frontend.log via /api/logs endpoint
- Add frontend logger utility with batching and user context
- Update .gitignore to exclude log files but preserve logs directory
- Update CLAUDE.md with logging documentation and usage examples

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-20 11:04:31 +01:00
ac0c8a39a9 fix: resolve Elysia framework and Vite URI error bugs
- Fix onResponse error by using onAfterHandle for Elysia framework
- Fix URI malformed errors from browser extensions in Vite dev server
- Update middleware plugin to run before SvelteKit with enforce: 'pre'
- Insert middleware at beginning of stack to catch malformed URIs early

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-20 11:03:43 +01:00
52234a32b6 feat: add Docker support with single-port deployment
Add complete Docker configuration for containerized deployment:
- Multi-stage Dockerfile using official Bun runtime
- docker-compose.yml for single-port stack orchestration
- Host-mounted database volume with auto-initialization
- Custom database init script using bun:sqlite
- Entrypoint script for seamless database setup
- Environment configuration template
- Comprehensive DOCKER.md documentation

Key features:
- Single exposed port (3001) serving both API and frontend
- Database persists in ./data directory on host
- Auto-creates database from template on first startup
- Health checks for monitoring
- Architecture-agnostic (works on x86 and ARM64)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-19 16:08:06 +01:00
130788e3bd perf: implement Phase 2-4 frontend and infrastructure optimizations
Complete partial frontend refactoring and infrastructure improvements:

**Frontend Performance (Phase 2):**
- Extract QSOStats component from QSO page (separation of concerns)
- Extract reusable SyncButton component (LoTW + DCL)
- Fix N+1 API calls in awards page with batch endpoint
  * Add GET /api/awards/batch/progress endpoint
  * Reduce award page load from 5s → ~500ms (95% improvement)
  * Replace N individual requests with single batch request

**Infrastructure (Phase 4):**
- Remove unused @libsql/client dependency
- Add .env.production.template for deployment
- Add bunfig.toml with optimized Bun configuration

**Code Quality:**
- Reduce QSO page from 1,587 to ~1,517 lines (-70 lines)
- Improve code reusability and maintainability

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-19 14:24:16 +01:00
f50ec5f44e perf: implement Phase 1 backend performance optimizations
Fix N+1 query, add database indexes, and implement award progress caching:

- Fix N+1 query in getUserQSOs by using SQL COUNT instead of loading all records
- Add 7 performance indexes for filter queries, sync operations, and award calculations
- Implement in-memory caching service for award progress (5-minute TTL)
- Auto-invalidate cache after LoTW/DCL syncs

Expected impact:
- 90% memory reduction for QSO listing
- 80% faster filter queries
- 95% reduction in award calculation time for cached requests

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-19 14:18:00 +01:00
bee02d16ce fix: count QSOs confirmed by either LoTW or DCL in stats
QSO stats were only counting LoTW-confirmed QSOs, excluding
QSOs confirmed only by DCL from the "confirmed" count.

Changed getQSOStats() to count QSOs as confirmed if EITHER
LoTW OR DCL has confirmed them:
- Before: q.lotwQslRstatus === 'Y'
- After: q.lotwQslRstatus === 'Y' || q.dclQslRstatus === 'Y'

Fixes stats showing 8317/8338 confirmed when all QSOs were
confirmed by at least one system.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-19 13:32:45 +01:00
b40d3639f7 feat: add callsign to DLD award entity details
The DLD award detail page was only showing the mode in QSO entries
because the entity breakdown didn't include the callsign field.

Changes:
- Backend: Add callsign field to DOK award entity details

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-19 13:12:25 +01:00
9dc8c8b678 fix: use qsoId for fetching QSO details in award page modal
The award page was filtering QSOs by callsign/date/band/mode, which
could return the wrong QSO when multiple QSOs with the same callsign
exist on the same band/mode combination.

Changes:
- Backend: Add qsoId field to award entity breakdown responses
- Backend: Add GET /api/qsos/:id endpoint to fetch QSO by ID
- Backend: Implement getQSOById() function in lotw.service.js
- Frontend: Update openQSODetailModal() to fetch by qsoId instead of filtering
- Frontend: Include qsoId in QSO entry objects for modal click handler

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-19 13:08:19 +01:00
8d47e6e4ad 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>
2026-01-19 08:01:59 +01:00
b422c20463 Filters 2026-01-19 07:39:33 +01:00
233888c44f fix: make ADIF parser case-insensitive for EOR delimiter
Critical bug fix: ADIF parser was using case-sensitive split on '<EOR>',
but LoTW returns lowercase '<eor>' tags. This caused all 242,239 QSOs
to be parsed as a single giant record with fields overwriting each other,
resulting in only 1 QSO being imported.

Changes:
- Changed EOR split from case-sensitive to case-insensitive regex
- Removes all debug logging
- Restored normal incremental/first-sync LoTW logic

Before: 6.8MB LoTW report → 1 QSO (bug)
After: 6.8MB LoTW report → All 242K+ QSOs (fixed)

Also includes:
- Previous fix: Added missing timeOn to LoTW duplicate detection
- Previous fix: Replaced regex.exec() while loop with matchAll() for-of

Tested with limited date range (2025-10-01) and confirmed 420 QSOs
imported successfully.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-18 08:25:25 +01:00
0161ad47a8 fix: ADIF parser now correctly parses all QSOs from large LoTW reports
Critical bug: ADIF parser was only parsing 1 QSO from multi-MB LoTW reports.

Root cause: The regex.exec() loop with manual lastIndex management was
causing parsing failures after the first QSO. The while loop approach with
regex state management was error-prone.

Fix: Replaced regex.exec() while loop with matchAll() for-of iteration.
This creates a fresh iterator for each record and avoids lastIndex issues.

Before: 6.8MB LoTW report → 1 QSO parsed
After: 6.8MB LoTW report → All QSOs parsed

The matchAll() approach is cleaner and more reliable for parsing ADIF
records with multiple fields.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-18 08:02:26 +01:00
645f7863e7 fix: add missing timeOn field to LoTW duplicate detection
Critical bug: LoTW sync was missing timeOn in the duplicate detection
query, causing multiple QSOs with the same callsign/date/band/mode
but different times to be treated as duplicates.

Example: If you worked DL1ABC on 2025-01-15 at 10:00, 12:00, and 14:00
all on 80m CW, only the first QSO would be imported.

Now matches DCL sync logic which correctly includes timeOn:
- userId, callsign, qsoDate, timeOn, band, mode

This ensures all unique QSOs are properly imported from LoTW.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-18 07:49:39 +01:00
7f77c3adc9 feat: add filter support for DOK awards
The DOK award type (used for DLD award) now supports filtering by band,
mode, and other QSO fields. This allows creating award variants like:
- DLD on specific bands (80m, 40m, etc.)
- DLD on specific modes (CW, SSB, etc.)
- DLD with combined filters (e.g., 80m + CW)

Changes:
- Modified calculateDOKAwardProgress() to apply filters before processing
- Added example awards: dld-80m, dld-40m, dld-cw, dld-80m-cw
- Filter system uses existing applyFilters() function

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-18 07:30:15 +01:00
720144627e 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.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-18 07:17:28 +01:00
223461f536 fix: enable debug logging and improve DCL sync observability
- Fix logger bug where debug level (0) was treated as falsy
  - Change `||` to `??` in config.js to properly handle log level 0
  - Debug logs now work correctly when LOG_LEVEL=debug

- Add server startup logging
  - Log port, environment, and log level on server start
  - Helps verify configuration is loaded correctly

- Add DCL API request debug logging
  - Log full API request parameters when LOG_LEVEL=debug
  - API key is redacted (shows first/last 4 chars only)
  - Helps troubleshoot DCL sync issues

- Update CLAUDE.md documentation
  - Add Logging section with log levels and configuration
  - Document debug logging feature for DCL service
  - Add this fix to Recent Commits section

Note: .env file added locally with LOG_LEVEL=debug (not committed)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-18 07:02:52 +01:00
27d2ef14ef fix: preserve DOK data when DCL doesn't send values
When DCL sync updates a QSO without DOK fields, the previous code would
write empty strings to the database, overwriting any existing DOK data
that was previously imported or manually entered.

Now only updates DOK/grid fields when DCL actually provides non-empty
values, preserving existing data from other sources.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-18 06:35:04 +01:00
e09ab94e63 feat: skip QSOs with unchanged confirmation data
When syncing from LoTW/DCL, only update QSOs if confirmation data
has changed. This avoids unnecessary database updates and makes it
clear which QSOs actually changed.

- LoTW: Checks if lotwQslRstatus or lotwQslRdate changed
- DCL: Checks if dclQslRstatus, dclQslRdate, DOK, or grid changed
- Frontend: Shows skipped count in sync summary

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-17 19:49:56 +01:00
3592dbb4fb feat: add import log showing synced QSOs
Add import log display that shows QSOs imported via LoTW/DCL sync.
Backend now tracks added/updated QSOs (callsign, date, band, mode)
and returns them in sync result. Frontend displays tables showing
new and updated QSOs after sync completes.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-17 19:39:15 +01:00