Files
award/README.md
Joerg a4ed1ec6d6 Add hostname configuration for production deployment
Add environment variable configuration to support deployment on custom
domains like https://awards.dj7nt.de

## Changes
- Add .env.example with configuration template
- Update API client to use VITE_API_BASE_URL with fallback to /api
- Update SvelteKit config to use VITE_APP_URL for CSRF trusted origins
- Update backend CORS to use configurable allowed origins
- Add ALLOWED_ORIGINS environment variable for production
- Add build and preview scripts to package.json
- Update README with production deployment guide and nginx example

## Environment Variables
- VITE_APP_URL: Application hostname (e.g., https://awards.dj7nt.de)
- VITE_API_BASE_URL: API base URL (empty = relative paths)
- ALLOWED_ORIGINS: Comma-separated CORS origins
- JWT_SECRET: Strong secret for production
- NODE_ENV: development/production

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-16 08:22:37 +01:00

359 lines
9.6 KiB
Markdown

# Ham Radio Award Portal
A web application for amateur radio operators to track QSOs (contacts) and award progress using Logbook of the World (LoTW) data.
## Features
- **User Authentication**: Register and login with callsign, email, and password
- **LoTW Integration**: Sync QSOs from ARRL's Logbook of the World
- Background job queue for non-blocking sync operations
- Incremental sync using last confirmation date
- Wavelog-compatible download logic with proper validation
- One sync job per user enforcement
- **QSO Log**: View and manage confirmed QSOs
- Pagination support for large QSO collections
- Filter by band, mode, and confirmation status
- Statistics dashboard (total QSOs, confirmed, DXCC entities, bands)
- Delete all QSOs with confirmation
- **Settings**: Configure LoTW credentials securely
## Tech Stack
### Backend
- **Runtime**: Bun
- **Framework**: Elysia.js
- **Database**: SQLite with Drizzle ORM
- **Authentication**: JWT tokens
- **Logging**: Pino with structured logging and timestamps
### Frontend
- **Framework**: SvelteKit
- **Language**: JavaScript
- **Styling**: Custom CSS
## Project Structure
```
award/
├── src/
│ ├── backend/
│ │ ├── config/
│ │ │ ├── database.js # Database connection
│ │ │ ├── jwt.js # JWT configuration
│ │ │ └── logger.js # Pino logging configuration
│ │ ├── db/
│ │ │ └── schema/
│ │ │ └── index.js # Database schema (users, qsos, sync_jobs)
│ │ ├── services/
│ │ │ ├── auth.service.js # User authentication
│ │ │ ├── lotw.service.js # LoTW sync & QSO management
│ │ │ └── job-queue.service.js # Background job queue
│ │ └── index.js # API routes and server
│ └── frontend/
│ ├── src/
│ │ ├── lib/
│ │ │ ├── api.js # API client
│ │ │ └── stores.js # Svelte stores (auth)
│ │ └── routes/
│ │ ├── +layout.svelte # Navigation bar & layout
│ │ ├── +page.svelte # Dashboard
│ │ ├── auth/
│ │ │ ├── login/+page.svelte # Login page
│ │ │ └── register/+page.svelte # Registration page
│ │ ├── qsos/+page.svelte # QSO log with pagination
│ │ └── settings/+page.svelte # Settings & LoTW credentials
│ └── package.json
├── award.db # SQLite database (auto-created)
├── drizzle.config.js # Drizzle ORM configuration
├── package.json
└── README.md
```
## Setup
### Prerequisites
- [Bun](https://bun.sh) v1.3.6 or later
### Installation
1. Clone the repository:
```bash
git clone <repository-url>
cd award
```
2. Install dependencies:
```bash
bun install
```
3. Set up environment variables:
Create a `.env` file in the project root (copy from `.env.example`):
```bash
cp .env.example .env
```
Edit `.env` with your configuration:
```env
# Application URL (for production deployment)
VITE_APP_URL=https://awards.dj7nt.de
# API Base URL (leave empty for same-domain deployment)
VITE_API_BASE_URL=
# JWT Secret (generate with: openssl rand -base64 32)
JWT_SECRET=your-generated-secret-here
# Environment
NODE_ENV=production
```
**For development**: You can leave `.env` empty or use defaults.
4. Initialize the database:
```bash
bun run db:push
```
This creates the SQLite database with required tables (users, qsos, sync_jobs).
## Running the Application
Start both backend and frontend with a single command:
```bash
bun run dev
```
Or start them individually:
```bash
# Backend only (port 3001, proxied)
bun run dev:backend
# Frontend only (port 5173)
bun run dev:frontend
```
The application will be available at:
- **Frontend & API**: http://localhost:5173
**Note**: During development, both servers run (frontend on 5173, backend on 3001), but API requests are automatically proxied through the frontend. You only need to access port 5173.
## API Endpoints
### Authentication
- `POST /api/auth/register` - Register new user
- `POST /api/auth/login` - Login user
- `GET /api/auth/me` - Get current user profile
- `PUT /api/auth/lotw-credentials` - Update LoTW credentials
### LoTW Sync
- `POST /api/lotw/sync` - Queue a LoTW sync job (returns job ID)
### Jobs
- `GET /api/jobs/:jobId` - Get job status
- `GET /api/jobs/active` - Get user's active job
- `GET /api/jobs` - Get recent jobs (query: `?limit=10`)
### QSOs
- `GET /api/qsos` - Get user's QSOs with pagination
- Query parameters: `?page=1&limit=100&band=20m&mode=CW&confirmed=true`
- `GET /api/qsos/stats` - Get QSO statistics
- `DELETE /api/qsos/all` - Delete all QSOs (requires confirmation)
### Health
- `GET /api/health` - Health check endpoint
## Database Schema
### Users Table
```sql
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
callsign TEXT NOT NULL,
lotwUsername TEXT,
lotwPassword TEXT,
createdAt TEXT NOT NULL
);
```
### QSOs Table
```sql
CREATE TABLE qsos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
userId INTEGER NOT NULL,
callsign TEXT NOT NULL,
qsoDate TEXT NOT NULL,
timeOn TEXT NOT NULL,
band TEXT,
mode TEXT,
entity TEXT,
grid TEXT,
lotwQslRstatus TEXT,
lotwQslRdate TEXT,
FOREIGN KEY (userId) REFERENCES users(id)
);
```
### Sync Jobs Table
```sql
CREATE TABLE sync_jobs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
userId INTEGER NOT NULL,
status TEXT NOT NULL, -- pending, running, completed, failed
type TEXT NOT NULL, -- lotw_sync
startedAt INTEGER,
completedAt INTEGER,
result TEXT, -- JSON
error TEXT,
createdAt INTEGER NOT NULL,
FOREIGN KEY (userId) REFERENCES users(id)
);
```
## Architecture
### Development Mode
- **SvelteKit Dev Server** (port 5173): Serves frontend and proxies API requests
- **Elysia Backend** (port 3001): Handles API requests (hidden from user)
- **Proxy Configuration**: All `/api/*` requests are forwarded from SvelteKit to Elysia
This gives you:
- ✅ Single port to access (5173)
- ✅ Hot Module Replacement (HMR) for frontend
- ✅ No CORS issues
- ✅ Simple production deployment
### Production Mode
In production, you can either:
1. **Build static files** and serve from Elysia
2. **Keep the proxy setup** with a proper reverse proxy (nginx, caddy)
3. **Use SvelteKit adapter** for Node/Bun to serve everything from one process
## Production Deployment
### Building for Production
```bash
# Build the frontend
bun run build
# Preview the production build locally
bun run preview
```
### Deployment Options
#### Option 1: Static Site + Backend Server
1. Build the frontend: `bun run build`
2. Serve `src/frontend/build/` with Elysia using `@elysiajs/static`
3. Backend runs on one port serving both frontend and API
#### Option 2: Reverse Proxy (Recommended)
Use nginx or Caddy to proxy:
- `/` → SvelteKit frontend (port 5173 or static files)
- `/api` → Elysia backend (port 3001)
**Example nginx configuration:**
```nginx
server {
server_name awards.dj7nt.de;
# Frontend
location / {
proxy_pass http://localhost:5173;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
# Backend API
location /api {
proxy_pass http://localhost:3001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
```
#### Option 3: Single Process with SvelteKit Node Adapter
Use `@sveltejs/adapter-node` to build for Node/Bun:
- Everything runs in one process
- API routes handled by SvelteKit (need to migrate from Elysia)
### Environment Variables for Production
Make sure to set these in your production environment:
```bash
VITE_APP_URL=https://awards.dj7nt.de
VITE_API_BASE_URL= # Leave empty for same-domain
JWT_SECRET=<strong-random-string>
NODE_ENV=production
```
## Features in Detail
### Background Job Queue
The application uses an in-memory job queue system for async operations:
- Jobs are persisted to database for recovery
- Only one active job per user (enforced at queue level)
- Status tracking: pending → running → completed/failed
- Real-time progress updates via job result field
- Client polls job status every 2 seconds
### LoTW Sync Logic
Following Wavelog's proven approach:
1. **First sync**: Uses date `2000-01-01` to retrieve all QSOs
2. **Subsequent syncs**: Uses `MAX(lotwQslRdate)` from database
3. **Validation**:
- Checks for "Username/password incorrect" in response
- Validates file starts with "ARRL Logbook of the World Status Report"
4. **Timeout handling**: 30-second connection timeout
5. **Query parameters**: Matches Wavelog's LoTW download
### Pagination
- Default page size: 100 QSOs per page
- Supports custom page size via `limit` parameter
- Shows page numbers with ellipsis for large page counts
- Displays "Showing X-Y of Z" info
- Previous/Next navigation buttons
## Development
### Database Migrations
```bash
# Push schema changes to database
bun run db:push
# Open Drizzle Studio (database GUI)
bun run db:studio
```
### Linting
```bash
bun run lint
```
## License
MIT
## Credits
- LoTW integration inspired by [Wavelog](https://github.com/magicbug/CloudLog)
- Built with [Bun](https://bun.sh), [Elysia](https://elysiajs.com), and [SvelteKit](https://kit.svelte.dev)