Add comprehensive documentation and LoTW integration
- Add detailed documentation covering architecture, components, code structure - Document award system with multiple examples (DXCC, WAS, VUCC, Satellite) - Implement LoTW sync service with ADIF parsing and long-polling - Add QSO logbook page with filtering and statistics - Add settings page for LoTW credentials management - Add API endpoints for LoTW sync, QSO retrieval, and statistics Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
783
docs/DOCUMENTATION.md
Normal file
783
docs/DOCUMENTATION.md
Normal file
@@ -0,0 +1,783 @@
|
||||
# Ham Radio Award Portal - Documentation
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Architecture](#architecture)
|
||||
2. [Components](#components)
|
||||
3. [Code Structure](#code-structure)
|
||||
4. [Awards System](#awards-system)
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### Overview
|
||||
|
||||
The Ham Radio Award Portal 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).
|
||||
|
||||
### 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
|
||||
|
||||
**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
|
||||
- `POST /api/lotw/sync` - Sync QSOs from LoTW
|
||||
- `GET /api/qsos` - Get QSOs with filtering
|
||||
- `GET /api/qsos/stats` - Get QSO statistics
|
||||
|
||||
#### 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
|
||||
- **qsos**: Amateur radio contacts in ADIF format with LoTW confirmation data
|
||||
- **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 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
|
||||
|
||||
#### 4. Configuration (`src/backend/config/`)
|
||||
|
||||
- **database.js**: Database connection and client initialization
|
||||
- **constants.js**: Application constants (JWT expiration, etc.)
|
||||
|
||||
### Frontend Components
|
||||
|
||||
#### 1. Pages (SvelteKit Routes)
|
||||
|
||||
- **`/`**: Dashboard with welcome message
|
||||
- **`/auth/login`**: User login form
|
||||
- **`/auth/register`**: User registration form
|
||||
- **`/qsos`**: QSO logbook with filtering and LoTW sync
|
||||
- **`/settings`**: LoTW credentials management
|
||||
|
||||
#### 2. 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
|
||||
│
|
||||
├── drizzle/ # Database migrations
|
||||
│ └── 0000_init.sql # Initial schema
|
||||
│
|
||||
├── docs/ # Documentation
|
||||
│ └── DOCUMENTATION.md # This file
|
||||
│
|
||||
├── src/
|
||||
│ ├── backend/ # Backend server code
|
||||
│ │ ├── config/
|
||||
│ │ │ ├── constants.js
|
||||
│ │ │ └── database.js
|
||||
│ │ ├── db/
|
||||
│ │ │ └── schema/
|
||||
│ │ │ └── index.js # Drizzle schema definitions
|
||||
│ │ ├── services/
|
||||
│ │ │ ├── auth.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
|
||||
```javascript
|
||||
{
|
||||
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)
|
||||
createdAt: timestamp
|
||||
updatedAt: timestamp
|
||||
}
|
||||
```
|
||||
|
||||
#### QSOs Table
|
||||
```javascript
|
||||
{
|
||||
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)
|
||||
lotwQslRdate: text (LoTW confirmation date)
|
||||
lotwQslRstatus: text ('Y', 'N', '?')
|
||||
lotwSyncedAt: timestamp
|
||||
createdAt: timestamp
|
||||
}
|
||||
```
|
||||
|
||||
#### Awards Table
|
||||
```javascript
|
||||
{
|
||||
id: text (primary key)
|
||||
name: text
|
||||
description: text
|
||||
definition: text (JSON)
|
||||
isActive: boolean
|
||||
createdAt: timestamp
|
||||
}
|
||||
```
|
||||
|
||||
#### Award Progress Table
|
||||
```javascript
|
||||
{
|
||||
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
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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:
|
||||
|
||||
```json
|
||||
{
|
||||
"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` |
|
||||
|
||||
### 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
|
||||
|
||||
---
|
||||
|
||||
## Award Examples
|
||||
|
||||
### 1. DXCC Mixed Mode
|
||||
|
||||
**File**: `award-definitions/dxcc.json`
|
||||
|
||||
```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:**
|
||||
```javascript
|
||||
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`
|
||||
|
||||
```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:**
|
||||
```javascript
|
||||
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`
|
||||
|
||||
```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:**
|
||||
```javascript
|
||||
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`
|
||||
|
||||
```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:**
|
||||
```javascript
|
||||
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`
|
||||
|
||||
```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:**
|
||||
```javascript
|
||||
worked = COUNT(DISTINCT callsign
|
||||
WHERE satName = 'RS-44' AND lotwQslRstatus IS NOT NULL)
|
||||
confirmed = COUNT(DISTINCT callsign
|
||||
WHERE satName = 'RS-44' AND lotwQslRstatus = 'Y')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Advanced Filter Examples
|
||||
|
||||
#### Multiple Conditions (AND)
|
||||
|
||||
Count only 20m CW QSOs:
|
||||
|
||||
```json
|
||||
{
|
||||
"filters": {
|
||||
"operator": "AND",
|
||||
"filters": [
|
||||
{ "field": "band", "operator": "eq", "value": "20m" },
|
||||
{ "field": "mode", "operator": "eq", "value": "CW" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Multiple Options (OR)
|
||||
|
||||
Count any phone mode QSOs:
|
||||
|
||||
```json
|
||||
{
|
||||
"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):
|
||||
|
||||
```json
|
||||
{
|
||||
"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:
|
||||
|
||||
```json
|
||||
{
|
||||
"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
|
||||
|
||||
```json
|
||||
{
|
||||
"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:**
|
||||
```json
|
||||
{
|
||||
"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:**
|
||||
```json
|
||||
{
|
||||
"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
|
||||
|
||||
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
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
- [ARRL LoTW](https://lotw.arrl.org/)
|
||||
- [ADIF Specification](https://adif.org/)
|
||||
- [DXCC List](https://www.arrl.org/dxcc)
|
||||
- [VUCC Program](https://www.arrl.org/vucc)
|
||||
- [WAS Award](https://www.arrl.org/was)
|
||||
Reference in New Issue
Block a user