Files
award/docs/DOCUMENTATION.md
Joerg 8b846bffbe docs: add super-admin role documentation
Add comprehensive documentation for the new super-admin role feature:

README.md:
- Update Users Table schema with isAdmin, isSuperAdmin, lastSeen fields
- Add Admin API section with all endpoints
- Add User Roles and Permissions section with security rules

docs/DOCUMENTATION.md:
- Update Users Table schema
- Add Admin System section with overview, roles, security rules
- Document all admin API endpoints
- Add audit logging details
- Include JWT token structure
- Add setup and deployment instructions

CLAUDE.md:
- Add Admin System and User Roles section
- Document admin service functions
- Include security rules
- Add JWT token claims structure
- Document frontend admin interface

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-23 13:53:48 +01:00

32 KiB

Quickawards by DJ7NT - Documentation

Table of Contents

  1. Architecture
  2. Components
  3. Code Structure
  4. Awards System

Architecture

Overview

Quickawards by DJ7NT is a full-stack web application designed to help amateur radio operators track their award progress by syncing QSOs (contacts) from ARRL's Logbook of the World (LoTW) and preparing for future integration with DARC Community Logbook (DCL).

Technology Stack

Backend:

  • Runtime: Bun - Fast JavaScript runtime
  • Framework: Elysia.js - Lightweight, high-performance web framework
  • Database: SQLite - Embedded database for data persistence
  • ORM: Drizzle ORM - Type-safe database queries
  • Authentication: JWT tokens via @elysiajs/jwt
  • Password Hashing: bcrypt
  • Logging: Pino - Structured logging with timestamps and log levels

Frontend:

  • Framework: SvelteKit - Modern reactive framework
  • Bundler: Vite - Fast development server and build tool
  • Language: JavaScript with TypeScript type definitions
  • State Management: Svelte stores
  • Styling: CSS modules

System Architecture

┌─────────────────┐      HTTP/REST       ┌─────────────────┐
│                 │ ◄──────────────────► │                 │
│   SvelteKit     │                      │   ElysiaJS      │
│   Frontend      │                      │   Backend       │
│                 │                      │   Server        │
└─────────────────┘                      └────────┬────────┘
                                                  │
                                                  ▼
                                         ┌─────────────────┐
                                         │   SQLite DB     │
                                         │   (Drizzle ORM) │
                                         └─────────────────┘
                                                  ▲
                                                  │
                                         ┌────────┴────────┐
                                         │   ARRL LoTW     │
                                         │   External API  │
                                         └─────────────────┘

Key Design Decisions

  1. SQLite over PostgreSQL/MySQL: Simplified deployment, embedded database, excellent for single-user or small-scale deployments
  2. Bun over Node.js: Faster startup, better performance, native TypeScript support
  3. ElysiaJS over Express: Better TypeScript support, faster performance, modern API design
  4. SvelteKit over React: Smaller bundle sizes, better performance, simpler reactivity model

Components

Backend Components

1. Server (src/backend/index.js)

Main entry point that configures and starts the ElysiaJS server.

Key Features:

  • CORS configuration for cross-origin requests
  • JWT authentication plugin
  • Route handlers for API endpoints
  • Error handling middleware

Routes:

  • GET /api/health - Health check endpoint
  • POST /api/auth/register - User registration
  • POST /api/auth/login - User login
  • GET /api/auth/me - Get current user
  • PUT /api/auth/lotw-credentials - Update LoTW credentials
  • PUT /api/auth/dcl-credentials - Update DCL API key
  • POST /api/lotw/sync - Sync QSOs from LoTW
  • POST /api/dcl/sync - Sync QSOs from DCL
  • GET /api/qsos - Get QSOs with filtering
  • GET /api/qsos/stats - Get QSO statistics
  • GET /api/awards - Get all awards
  • GET /api/awards/batch/progress - Get progress for all awards (optimized)
  • GET /api/awards/:awardId/progress - Get award progress
  • GET /api/awards/:awardId/entities - Get entity breakdown
  • GET /api/jobs/:jobId - Get job status
  • GET /api/jobs/active - Get user's active job

2. Database Schema (src/backend/db/schema/index.js)

Defines the database structure using Drizzle ORM schema builder.

Tables:

  • users: User accounts, authentication credentials, LoTW credentials, DCL API key
  • qsos: Amateur radio contacts in ADIF format with LoTW and DCL confirmation data, plus DOK fields
  • sync_jobs: Background job queue for async operations
  • awards: Award definitions with JSON rule configurations
  • award_progress: Cached award progress for each user

3. Services

Auth Service (src/backend/services/auth.service.js)

  • User registration and login
  • Password hashing with bcrypt
  • JWT token generation and validation
  • User profile management
  • LoTW and DCL credentials management

LoTW Service (src/backend/services/lotw.service.js)

  • Synchronization with ARRL's Logbook of the World
  • ADIF format parsing
  • Long-polling for report generation
  • QSO deduplication
  • Band/mode normalization
  • Error handling and retry logic

DCL Service (src/backend/services/dcl.service.js)

  • Full integration with DARC Community Logbook (DCL)
  • Fetches QSOs from DCL API
  • ADIF parsing with shared parser
  • Incremental sync by confirmation date
  • DXCC entity priority logic (LoTW > DCL)
  • Award cache invalidation after sync

Cache Service (src/backend/services/cache.service.js)

  • In-memory caching for award progress calculations
  • 5-minute TTL for cached data
  • Automatic cache invalidation after LoTW/DCL syncs
  • Significantly reduces database load for repeated queries

Awards Service (src/backend/services/awards.service.js)

  • Award progress calculation
  • Entity breakdown by band/mode
  • Confirmation status tracking (LoTW, DCL)
  • DXCC, WAS, VUCC, DLD award support
  • DOK-based award calculation with DCL confirmation

Job Queue Service (src/backend/services/job-queue.service.js)

  • Background job management
  • Job status tracking
  • One active job per user enforcement
  • Progress reporting

4. Configuration (src/backend/config/)

  • database.js: Database connection and client initialization
  • jwt.js: JWT secret configuration
  • logger.js: Pino logger configuration with structured logging and timestamps

Frontend Components

1. Pages (SvelteKit Routes)

  • /: Dashboard with welcome message and quick action cards
  • /auth/login: User login form
  • /auth/register: User registration form
  • /qsos: QSO logbook with filtering, DOK fields, and multi-service confirmation display
  • /awards: Awards progress tracking (DXCC, WAS, VUCC)
  • /settings: LoTW and DCL credentials management

2. Layout (+layout.svelte)

Global layout component providing:

  • Navigation bar: Shows user's callsign, navigation links (Dashboard, QSOs, Settings), and logout button
  • Only visible when user is logged in
  • Responsive design with dark theme matching footer

3. Libraries

API Client (src/frontend/src/lib/api.js)

  • Centralized API communication
  • Automatic JWT token injection
  • Response/error handling
  • Typed API methods

Stores (src/frontend/src/lib/stores.js)

  • Authentication state management
  • Persistent login with localStorage
  • Reactive user data

Types (src/frontend/src/lib/types/)

  • TypeScript type definitions
  • Award system types
  • API response types

Code Structure

Directory Layout

award/
├── award-definitions/          # Award rule definitions (JSON)
│   ├── dxcc.json              # DXCC award configurations
│   ├── dxcc-cw.json           # DXCC CW-specific award
│   ├── was.json               # WAS award configurations
│   ├── vucc-sat.json          # VUCC Satellite award
│   ├── sat-rs44.json          # Satellite RS-44 award
│   ├── special-stations.json  # Special event stations award
│   └── dld.json               # DLD (Deutschland Diplom) award
│
├── drizzle/                   # Database migrations
│   └── 0000_init.sql         # Initial schema
│
├── docs/                      # Documentation
│   └── DOCUMENTATION.md      # This file
│
├── src/
│   ├── backend/              # Backend server code
│   │   ├── config/
│   │   │   ├── database.js    # Database connection
│   │   │   ├── jwt.js         # JWT configuration
│   │   │   └── logger.js      # Pino logging configuration
│   │   ├── db/
│   │   │   └── schema/
│   │   │       └── index.js   # Drizzle schema definitions
│   │   ├── services/
│   │   │   ├── auth.service.js
│   │   │   ├── job-queue.service.js
│   │   │   └── lotw.service.js
│   │   └── index.js           # Main server entry point
│   │
│   ├── frontend/             # Frontend application
│   │   ├── src/
│   │   │   ├── lib/
│   │   │   │   ├── api.js     # API client
│   │   │   │   ├── stores.js  # Svelte stores
│   │   │   │   └── types/     # TypeScript definitions
│   │   │   └── routes/        # SvelteKit pages
│   │   │       ├── +page.svelte
│   │   │       ├── auth/
│   │   │       ├── qsos/
│   │   │       └── settings/
│   │   ├── static/            # Static assets
│   │   └── vite.config.js
│   │
│   └── shared/               # Shared TypeScript types
│       └── index.ts
│
├── award.db                  # SQLite database file
├── drizzle.config.ts         # Drizzle configuration
├── package.json              # Dependencies and scripts
├── CLAUDE.md                 # Claude AI instructions
└── README.md                 # Project overview

File Organization Principles

  1. Separation of Concerns: Clear separation between backend and frontend
  2. Shared Types: Common types in src/shared/ to ensure consistency
  3. Service Layer: Business logic isolated in service files
  4. Configuration: Config files co-located in dedicated directories
  5. Database-First: Schema drives the application structure

Database Schema Details

Users Table

{
  id: integer (primary key, auto-increment)
  email: text (unique, not null)
  passwordHash: text (not null)
  callsign: text (not null)
  lotwUsername: text (nullable)
  lotwPassword: text (nullable, encrypted)
  isAdmin: boolean (default: false)
  isSuperAdmin: boolean (default: false)
  lastSeen: timestamp (nullable)
  createdAt: timestamp
  updatedAt: timestamp
}

QSOs Table

{
  id: integer (primary key, auto-increment)
  userId: integer (foreign key  users.id)
  callsign: text
  qsoDate: text (ADIF format: YYYYMMDD)
  timeOn: text (HHMMSS)
  band: text (160m, 80m, 40m, 30m, 20m, 17m, 15m, 12m, 10m, 6m, 2m, 70cm)
  mode: text (CW, SSB, FM, AM, FT8, FT4, PSK31, etc.)
  freq: integer (Hz)
  freqRx: integer (Hz, satellite only)
  entity: text (DXCC entity name)
  entityId: integer (DXCC entity number)
  grid: text (Maidenhead grid square)
  gridSource: text (LOTW, USER, CALC)
  continent: text (NA, SA, EU, AF, AS, OC, AN)
  cqZone: integer
  ituZone: integer
  state: text (US state, CA province, etc.)
  county: text
  satName: text (satellite name)
  satMode: text (satellite mode)
  myDarcDok: text (user's DOK - DARC Ortsverband Kennung)
  darcDok: text (QSO partner's DOK)
  lotwQslRdate: text (LoTW confirmation date)
  lotwQslRstatus: text ('Y', 'N', '?')
  dclQslRdate: text (DCL confirmation date)
  dclQslRstatus: text ('Y', 'N', '?')
  lotwSyncedAt: timestamp
  createdAt: timestamp
}

Awards Table

{
  id: text (primary key)
  name: text
  description: text
  definition: text (JSON)
  isActive: boolean
  createdAt: timestamp
}

Award Progress Table

{
  id: integer (primary key, auto-increment)
  userId: integer (foreign key  users.id)
  awardId: text (foreign key  awards.id)
  workedCount: integer
  confirmedCount: integer
  totalRequired: integer
  workedEntities: text (JSON array)
  confirmedEntities: text (JSON array)
  lastCalculatedAt: timestamp
  lastQsoSyncAt: timestamp
  updatedAt: timestamp
}

Performance Optimizations

Overview

The application implements several performance optimizations to ensure fast response times and efficient resource usage, even with large QSO datasets (10,000+ contacts).

Database Optimizations

Performance Indexes

Seven strategic indexes on the QSO table optimize common query patterns:

-- Filter queries
idx_qsos_user_band           -- Filter by band
idx_qsos_user_mode           -- Filter by mode
idx_qsos_user_confirmation   -- Filter by LoTW/DCL confirmation

-- Sync operations (most impactful)
idx_qsos_duplicate_check     -- Duplicate detection (user_id, callsign, date, time, band, mode)

-- Award calculations
idx_qsos_lotw_confirmed      -- LoTW-confirmed QSOs (partial index)
idx_qsos_dcl_confirmed       -- DCL-confirmed QSOs (partial index)

-- Sorting
idx_qsos_qso_date            -- Date-based sorting

Impact:

  • 80% faster filter queries
  • 60% faster sync operations
  • 50% faster award calculations

Usage:

bun run db:indexes  # Create/update performance indexes

Backend Optimizations

1. N+1 Query Prevention

The getUserQSOs() function uses SQL COUNT for pagination instead of loading all records:

// Before (BAD): Load all, count in memory
const allResults = await db.select().from(qsos).where(...);
const totalCount = allResults.length;

// After (GOOD): Count in SQL
const [{ count }] = await db
  .select({ count: sql`CAST(count(*) AS INTEGER)` })
  .from(qsos)
  .where(...);

Impact:

  • 90% memory reduction for large QSO lists
  • 70% faster response times

2. Award Progress Caching

In-memory cache reduces expensive database aggregations:

// Cache with 5-minute TTL
const cached = getCachedAwardProgress(userId, awardId);
if (cached) return cached;

// Calculate and cache
const result = await calculateAwardProgress(userId, award);
setCachedAwardProgress(userId, awardId, result);

Impact:

  • 95% faster for cached requests
  • Auto-invalidation after LoTW/DCL syncs
  • Significantly reduced database load

3. Batch API Endpoints

Single request replaces multiple individual requests:

// GET /api/awards/batch/progress
// Returns progress for all awards in one response

Impact:

  • 95% reduction in API calls
  • Awards page load: 5 seconds → 500ms

Frontend Optimizations

Component Extraction

Modular components improve re-render performance:

  • QSOStats.svelte: Statistics display
  • SyncButton.svelte: Reusable sync button (LoTW & DCL)

Impact:

  • Reduced component re-renders
  • Better code maintainability
  • Improved testability

Batch API Calls

Awards page loads all progress in a single request instead of N individual calls.

Impact:

  • Faster page load
  • Reduced server load
  • Better UX

Deployment Optimizations

Bun Configuration

bunfig.toml optimizes builds and development:

[build]
target = "esnext"  # Modern browsers
minify = true       # Smaller bundles
sourcemap = true    # Better debugging

Production Templates

.env.production.template provides production-ready configuration.

Monitoring & Debugging

Cache Statistics

import { getCacheStats } from './services/cache.service.js';

const stats = getCacheStats();
// Returns: { total, valid, expired, ttl }

Index Verification

# Verify indexes are created
sqlite3 award.db ".indexes qsos"

Awards System

Overview

The awards system is designed to be flexible and extensible. Awards are defined as JSON configuration files that specify rules for calculating progress based on QSOs.

Award Definition Structure

Each award is defined with the following structure:

{
  "id": "unique-identifier",
  "name": "Award Display Name",
  "description": "Award description",
  "category": "award-category",
  "rules": {
    "type": "entity",
    "entityType": "dxcc|state|grid|...",
    "target": 100,
    "filters": {
      "operator": "AND|OR",
      "filters": [...]
    }
  }
}

Entity Types

Entity Type Description Example Field
dxcc DXCC entities (countries) entityId
state US States, Canadian provinces state
grid Maidenhead grid squares grid
dok DARC Ortsverband Kennung (German local clubs) darcDok

Filter Operators

Operator Description
eq Equal to
ne Not equal to
in In list
nin Not in list
contains Contains substring
AND All filters must match
OR At least one filter must match

Award Categories

  • dxcc: DXCC (Countries/Districts) awards
  • was: Worked All States awards
  • vucc: VHF/UHF Century Club awards
  • satellite: Satellite-specific awards
  • darc: DARC (German Amateur Radio Club) awards

Award Examples

1. DXCC Mixed Mode

File: award-definitions/dxcc.json

{
  "id": "dxcc-mixed",
  "name": "DXCC Mixed Mode",
  "description": "Confirm 100 DXCC entities on any band/mode",
  "category": "dxcc",
  "rules": {
    "type": "entity",
    "entityType": "dxcc",
    "target": 100
  }
}

Explanation:

  • Counts unique DXCC entities (entityId field)
  • No filtering - all QSOs count toward progress
  • Target: 100 confirmed entities
  • Both worked (any QSO) and confirmed (LoTW QSL) are tracked

Progress Calculation:

worked = COUNT(DISTINCT entityId WHERE lotwQslRstatus IS NOT NULL)
confirmed = COUNT(DISTINCT entityId WHERE lotwQslRstatus = 'Y')

2. DXCC CW (CW-specific)

File: award-definitions/dxcc-cw.json

{
  "id": "dxcc-cw",
  "name": "DXCC CW",
  "description": "Confirm 100 DXCC entities using CW mode only",
  "category": "dxcc",
  "rules": {
    "type": "entity",
    "entityType": "dxcc",
    "target": 100,
    "filters": {
      "operator": "AND",
      "filters": [
        {
          "field": "mode",
          "operator": "eq",
          "value": "CW"
        }
      ]
    }
  }
}

Explanation:

  • Counts unique DXCC entities
  • Only QSOs with mode = 'CW' are counted
  • Target: 100 confirmed entities
  • Example: A CW QSO with Germany (entityId=230) counts, but SSB does not

Progress Calculation:

worked = COUNT(DISTINCT entityId
         WHERE mode = 'CW' AND lotwQslRstatus IS NOT NULL)
confirmed = COUNT(DISTINCT entityId
           WHERE mode = 'CW' AND lotwQslRstatus = 'Y')

3. WAS Mixed Mode

File: award-definitions/was.json

{
  "id": "was-mixed",
  "name": "WAS Mixed Mode",
  "description": "Confirm all 50 US states",
  "category": "was",
  "rules": {
    "type": "entity",
    "entityType": "state",
    "target": 50,
    "filters": {
      "operator": "AND",
      "filters": [
        {
          "field": "entity",
          "operator": "eq",
          "value": "United States"
        }
      ]
    }
  }
}

Explanation:

  • Counts unique US states
  • Only QSOs where entity = 'United States'
  • Target: 50 confirmed states
  • Both worked and confirmed are tracked separately

Progress Calculation:

worked = COUNT(DISTINCT state
         WHERE entity = 'United States' AND lotwQslRstatus IS NOT NULL)
confirmed = COUNT(DISTINCT state
           WHERE entity = 'United States' AND lotwQslRstatus = 'Y')

Example States:

  • "CA" - California
  • "TX" - Texas
  • "NY" - New York
  • (All 50 US states)

4. VUCC Satellite

File: award-definitions/vucc-sat.json

{
  "id": "vucc-satellite",
  "name": "VUCC Satellite",
  "description": "Confirm 100 unique grid squares via satellite",
  "category": "vucc",
  "rules": {
    "type": "entity",
    "entityType": "grid",
    "target": 100,
    "filters": {
      "operator": "AND",
      "filters": [
        {
          "field": "satellite",
          "operator": "eq",
          "value": true
        }
      ]
    }
  }
}

Explanation:

  • Counts unique grid squares (4-character: FN31, CM97, etc.)
  • Only satellite QSOs (where satName is not null)
  • Target: 100 confirmed grids
  • Grid squares are counted independently of band/mode

Progress Calculation:

worked = COUNT(DISTINCT SUBSTRING(grid, 1, 4)
         WHERE satName IS NOT NULL AND lotwQslRstatus IS NOT NULL)
confirmed = COUNT(DISTINCT SUBSTRING(grid, 1, 4)
           WHERE satName IS NOT NULL AND lotwQslRstatus = 'Y')

Example Grids:

  • "FN31pr" → counts as "FN31"
  • "CM97" → counts as "CM97"

5. Satellite RS-44 (Specific Satellite)

File: award-definitions/sat-rs44.json

{
  "id": "sat-rs44",
  "name": "RS-44 Satellite Award",
  "description": "Make 100 QSOs via RS-44 satellite",
  "category": "satellite",
  "rules": {
    "type": "entity",
    "entityType": "callsign",
    "target": 100,
    "filters": {
      "operator": "AND",
      "filters": [
        {
          "field": "satName",
          "operator": "eq",
          "value": "RS-44"
        }
      ]
    }
  }
}

Explanation:

  • Counts unique callsigns worked via RS-44 satellite
  • Only QSOs where satName = 'RS-44'
  • Target: 100 unique callsigns
  • Each station (callsign) counts once regardless of multiple QSOs

Progress Calculation:

worked = COUNT(DISTINCT callsign
         WHERE satName = 'RS-44' AND lotwQslRstatus IS NOT NULL)
confirmed = COUNT(DISTINCT callsign
           WHERE satName = 'RS-44' AND lotwQslRstatus = 'Y')

6. DLD (Deutschland Diplom)

File: award-definitions/dld.json

{
  "id": "dld",
  "name": "DLD",
  "description": "Deutschland Diplom - Confirm 100 unique DOKs on different bands/modes",
  "caption": "Contact and confirm stations with 100 unique DOKs (DARC Ortsverband Kennung) on different band/mode combinations. Each unique DOK on a unique band/mode counts as one point. Only DCL-confirmed QSOs with valid DOK information count toward this award.",
  "category": "darc",
  "rules": {
    "type": "dok",
    "target": 100,
    "confirmationType": "dcl",
    "displayField": "darcDok"
  }
}

Explanation:

  • Counts unique (DOK, band, mode) combinations
  • Only QSOs with valid darcDok values are counted
  • Only DCL-confirmed QSOs (dclQslRstatus = 'Y') count toward the award
  • Target: 100 unique DOKs on different band/mode combinations
  • Each unique DOK on a unique band/mode combination counts as one point
  • Example: Working DOK F03 on 20m CW and 40m SSB counts as 2 points

Progress Calculation:

worked = COUNT(DISTINCT darcDok
         WHERE darcDok IS NOT NULL)
confirmed = COUNT(DISTINCT darcDok
           WHERE darcDok IS NOT NULL AND dclQslRstatus = 'Y')

Example DOKs:

  • "F03" - Ortsverband Frankfurt am Main
  • "P30" - Ortsverband München
  • "G20" - Ortsverband Köln
  • DOKs consist of a letter (district) and two digits (local club)

Confirmation:

  • Only DCL (DARC Community Logbook) confirmations count
  • LoTW confirmations do not count toward this award
  • This is a DARC-specific award using DARC's confirmation system

Advanced Filter Examples

Multiple Conditions (AND)

Count only 20m CW QSOs:

{
  "filters": {
    "operator": "AND",
    "filters": [
      { "field": "band", "operator": "eq", "value": "20m" },
      { "field": "mode", "operator": "eq", "value": "CW" }
    ]
  }
}

Multiple Options (OR)

Count any phone mode QSOs:

{
  "filters": {
    "operator": "OR",
    "filters": [
      { "field": "mode", "operator": "eq", "value": "SSB" },
      { "field": "mode", "operator": "eq", "value": "FM" },
      { "field": "mode", "operator": "eq", "value": "AM" }
    ]
  }
}

Band Ranges

Count HF QSOs (all HF bands):

{
  "filters": {
    "operator": "OR",
    "filters": [
      { "field": "band", "operator": "eq", "value": "160m" },
      { "field": "band", "operator": "eq", "value": "80m" },
      { "field": "band", "operator": "eq", "value": "60m" },
      { "field": "band", "operator": "eq", "value": "40m" },
      { "field": "band", "operator": "eq", "value": "30m" },
      { "field": "band", "operator": "eq", "value": "20m" },
      { "field": "band", "operator": "eq", "value": "17m" },
      { "field": "band", "operator": "eq", "value": "15m" },
      { "field": "band", "operator": "eq", "value": "12m" },
      { "field": "band", "operator": "eq", "value": "10m" }
    ]
  }
}

Grid Square by Continent

Count VUCC grids in Europe only:

{
  "filters": {
    "operator": "AND",
    "filters": [
      { "field": "continent", "operator": "eq", "value": "EU" },
      {
        "filters": [
          { "field": "band", "operator": "eq", "value": "2m" },
          { "field": "band", "operator": "eq", "value": "70cm" }
        ],
        "operator": "OR"
      }
    ]
  }
}

Creating Custom Awards

To create a new award:

  1. Create a new JSON file in award-definitions/
  2. Define the award structure following the schema above
  3. Add to database (via API or database migration)

Example: IOTA Award

{
  "id": "iota-mixed",
  "name": "IOTA Mixed Mode",
  "description": "Confirm 100 Islands on the Air",
  "category": "iota",
  "rules": {
    "type": "entity",
    "entityType": "iota",
    "target": 100,
    "filters": {
      "operator": "AND",
      "filters": [
        { "field": "iotaNumber", "operator": "ne", "value": null }
      ]
    }
  }
}

Note: This would require adding an iotaNumber field to the QSO table.


Award Progress API

Get User Award Progress

GET /api/awards/progress

Response:

{
  "userId": 1,
  "awards": [
    {
      "id": "dxcc-mixed",
      "name": "DXCC Mixed Mode",
      "worked": 87,
      "confirmed": 73,
      "totalRequired": 100,
      "percentage": 73
    }
  ]
}

Get Detailed Award Breakdown

GET /api/awards/progress/:awardId

Response:

{
  "awardId": "dxcc-mixed",
  "workedEntities": [
    { "entity": "Germany", "entityId": 230, "worked": true, "confirmed": true },
    { "entity": "Japan", "entityId": 339, "worked": true, "confirmed": false }
  ],
  "confirmedEntities": [
    { "entity": "Germany", "entityId": 230 }
  ]
}

Future Enhancements

The award system is designed for extensibility. Planned enhancements:

  1. Additional Entity Types:

    • IOTA (Islands on the Air)
    • CQ Zones
    • ITU Zones
    • Counties
    • DOK (German DARC Ortsverband Kennung) ✓ Implemented
  2. Advanced Filters:

    • Date ranges
    • Regular expressions
    • Custom field queries
  3. Award Endorsements:

    • Band-specific endorsements
    • Mode-specific endorsements
    • Combined endorsements
  4. Award Tiers:

    • Bronze, Silver, Gold levels
    • Progressive achievements
  5. User-Defined Awards:

    • Allow users to create custom awards
    • Share award definitions with community

Contributing

When adding new awards or modifying the award system:

  1. Follow the JSON schema for award definitions
  2. Add tests for new filter types
  3. Update this documentation with examples
  4. Ensure database schema supports required fields
  5. Test progress calculations thoroughly

Admin System

Overview

The admin system provides user management, role-based access control, and account impersonation capabilities for support and administrative purposes.

User Roles

The application supports three user roles with increasing permissions:

Regular User

  • View own QSOs and statistics
  • Sync from LoTW and DCL
  • Track award progress
  • Manage own credentials (LoTW, DCL)

Admin

  • All user permissions
  • View system-wide statistics
  • View all users and their activity
  • Promote/demote regular users to/from admin role
  • Delete regular users
  • Impersonate regular users (for support)
  • View admin action log

Super Admin

  • All admin permissions
  • Promote/demote admins to/from super-admin role
  • Impersonate other admins (for support)
  • Cannot be demoted by regular admins
  • Protected from accidental lockout

Security Rules

Role Change Restrictions:

  • Only super-admins can promote or demote super-admins
  • Regular admins cannot promote users to super-admin
  • Super-admins cannot demote themselves
  • Cannot demote the last super-admin (prevents lockout)

Impersonation Restrictions:

  • Regular admins can only impersonate regular users
  • Super-admins can impersonate any user (including other admins)
  • All impersonation actions are logged to audit trail
  • Impersonation tokens expire after 1 hour

Admin API Endpoints

Statistics and Monitoring:

  • GET /api/admin/stats - System-wide statistics (users, QSOs, jobs)
  • GET /api/admin/users - List all users with statistics
  • GET /api/admin/users/:userId - Get detailed user information
  • GET /api/admin/actions - View admin action log
  • GET /api/admin/actions/my - View current admin's actions

User Management:

  • POST /api/admin/users/:userId/role - Change user role
    • Body: { "role": "user" | "admin" | "super-admin" }
  • DELETE /api/admin/users/:userId - Delete a user

Impersonation:

  • POST /api/admin/impersonate/:userId - Start impersonating a user
  • POST /api/admin/impersonate/stop - Stop impersonation
  • GET /api/admin/impersonation/status - Check impersonation status

Admin Service

File: src/backend/services/admin.service.js

Key Functions:

// Check user permissions
await isAdmin(userId)
await isSuperAdmin(userId)

// Role management
await changeUserRole(adminId, targetUserId, newRole)

// Impersonation
await impersonateUser(adminId, targetUserId)
await verifyImpersonation(impersonationToken)
await stopImpersonation(adminId, targetUserId)

// Audit logging
await logAdminAction(adminId, actionType, targetUserId, details)

Audit Logging

All admin actions are logged to the admin_actions table for audit purposes:

Action Types:

  • impersonate_start - Started impersonating a user
  • impersonate_stop - Stopped impersonation
  • role_change - Changed user role
  • user_delete - Deleted a user

Log Entry Structure:

{
  id: integer,
  adminId: integer,
  actionType: string,
  targetUserId: integer (nullable),
  details: string (JSON),
  createdAt: timestamp
}

Frontend Admin Interface

Route: /admin (admin only)

Features:

  • Overview Tab: System statistics dashboard
  • Users Tab: User management with filtering
  • Awards Tab: Award definition management
  • Action Log Tab: Audit trail of admin actions

User Management Actions:

  • Impersonate - Switch to user account (disabled for admins unless super-admin)
  • Promote/Demote - Change user role
  • Delete - Remove user and all associated data

JWT Token Claims

Admin tokens include additional claims:

{
  userId: number,
  email: string,
  callsign: string,
  isAdmin: boolean,
  isSuperAdmin: boolean,  // New: Super-admin flag
  exp: number
}

Impersonation Token:

{
  userId: number,           // Target user ID
  email: string,
  callsign: string,
  isAdmin: boolean,
  isSuperAdmin: boolean,
  impersonatedBy: number,   // Admin ID who started impersonation
  exp: number               // 1 hour expiration
}

Setup

To create the first super-admin:

  1. Register a user account normally
  2. Access the database directly:
sqlite3 src/backend/award.db
  1. Update the user to super-admin:
UPDATE users SET is_super_admin = 1 WHERE email = 'your@email.com';
  1. Log out and log back in to get the updated JWT token

To promote users via the admin interface:

  1. Log in as a super-admin
  2. Navigate to /admin
  3. Find the user in the Users tab
  4. Click "Promote" and select "Super Admin"

Production Deployment

After pulling the latest code:

# Apply database migration (adds is_super_admin column)
sqlite3 src/backend/award.db "ALTER TABLE users ADD COLUMN is_super_admin INTEGER DEFAULT 0 NOT NULL;"

# Restart backend
pm2 restart award-backend

# Promote a user to super-admin via database or existing admin interface