The modals were using selectedUser.userId but the user object has the field
named id, not userId. This caused undefined to be passed to the backend,
resulting in "Invalid user ID" error when trying to impersonate or change
user roles.
Co-Authored-By: Claude <noreply@anthropic.com>
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>
- 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>
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>
- 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
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>
- 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>
- 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>
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>
Add a "Recent Sync Jobs" section to the dashboard that displays the last 5 sync jobs with:
- Job type (LoTW/DCL) with icon
- Status badge (pending/running/completed/failed)
- Relative timestamp (e.g., "5m ago", "2h ago")
- Duration for completed jobs
- Sync statistics (total, added, updated, skipped)
- Error messages for failed jobs
- Empty state with helpful CTAs
- Loading state while fetching
Uses existing backend API (GET /api/jobs?limit=5).
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- 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>
- 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>
Add a summary row at the bottom of the award details table that shows the sum for each band column. The sum automatically calculates:
- For points-based awards: Total confirmed points per band
- For QSO-based awards: Count of confirmed QSOs per band
The sum row is styled with a gray background and bold text for visual distinction.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
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>
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>
- 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
- Uses existing pagination.totalCount from backend API
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
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>
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>
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>
The QSO entries on the award detail page were missing the band field,
causing the openQSODetailModal() function to fail with "QSO not found"
when trying to fetch full QSO details. The band field is now included
in the QSO entry objects so the filter parameters match correctly.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Click on any QSO row to view detailed information in modal
- Display all QSO fields organized by section:
- Basic information (callsign, date, time, band, mode)
- Location (entity, DXCC, grid, continent, zones, state, county)
- Satellite info (when applicable)
- DOK information (partner's and my DOK)
- Confirmation status (LoTW and DCL with dates and status)
- Keyboard accessible (Enter to open, Escape to close)
- Close by clicking backdrop, pressing Escape, or × button
- Hover effect on rows to indicate clickability
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add category filter dropdown to awards list page
- Filters awards by category (dxcc, darc, was, etc.)
- Remove unused "Sort by" dropdown from award detail page
- Categories auto-extracted from award definitions
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
- Add shared ADIF parser utility (src/backend/utils/adif-parser.js)
- parseADIF(): Parse ADIF format into QSO records
- parseDCLResponse(): Parse DCL's JSON response format
- normalizeBand() and normalizeMode(): Standardize band/mode names
- Implement DCL service (src/backend/services/dcl.service.js)
- fetchQSOsFromDCL(): Fetch from DCL API (ready for API availability)
- parseDCLJSONResponse(): Parse example payload format
- syncQSOs(): Update existing QSOs with DCL confirmations
- Support DCL-specific fields: DCL_QSL_RCVD, DCL_QSLRDATE, DARC_DOK, MY_DARC_DOK
- Refactor LoTW service to use shared ADIF parser
- Remove duplicate parseADIF, normalizeBand, normalizeMode functions
- Import from shared utility for consistency
- Tested with example DCL payload
- Successfully parses all 6 QSOs
- Correctly extracts DCL confirmation data
- Handles ADIF format with <EOR> delimiters
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add DOK-based award tracking with DCL confirmation. Counts unique
(DOK, band, mode) combinations toward the 100 DOK target.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add infrastructure for future DARC Community Logbook (DCL) integration:
- Database schema: Add dcl_api_key, my_darc_dok, darc_dok, dcl_qsl_rdate, dcl_qsl_rstatus fields
- Create DCL service stub with placeholder functions for when DCL provides API
- Backend API: Add /api/auth/dcl-credentials endpoint for API key management
- Frontend settings: Add DCL API key input with informational notice about API availability
- QSO table: Add My DOK and DOK columns, update confirmation column for multiple services
Note: DCL download API is not yet available. These changes prepare the application
for future implementation when DCL adds programmatic access.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Update all page titles and branding from "Ham Radio Awards" to "Quickawards by DJ7NT"
- Simplify footer copyright text
- Redirect users to login page after logout instead of home page
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Replace Status column with Confirmed column that displays both the QSL service type (LoTW) and confirmation date, making it ready for future QSL services like eQSL and ClubLog.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Frontend now uses @sveltejs/adapter-static for production builds
- Backend serves both API and static files from single port (originally port 3000)
- Removed all throw statements from services to avoid Elysia prototype errors
- Fixed favicon serving and SvelteKit assets path handling
- Added ecosystem.config.js for PM2 process management
- Comprehensive deployment documentation (PM2 + HAProxy)
- Updated README with single-port architecture
- Created start.sh script for easy production start
- Fix WAS award filter to use entityId (291) instead of entity string
- Fix backend to use normalized rules.target for all award types
- Fix frontend to distinguish point-based from entity-based awards
- Fix "Needed" calculation for entity awards: target - worked
- Remove points display from non-point awards (WAS, DXCC, etc.)
- Add proper target field to all award API responses
Fixes issue where:
- WAS showed 0 states (filter didn't match "UNITED STATES OF AMERICA")
- DXCC CW showed "Needed: 0" (target not extracted from nested rules)
- Entity awards showed "Points: NaN" (incorrectly detected as point-based)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>