Initial commit: Ham Radio Award Portal

Features implemented:
- User authentication (register/login) with JWT
- SQLite database with Drizzle ORM
- SvelteKit frontend with authentication flow
- ElysiaJS backend with CORS enabled
- Award definition JSON schemas (DXCC, WAS, VUCC, SAT)
- Responsive dashboard with user profile

Tech stack:
- Backend: ElysiaJS, Drizzle ORM, SQLite, JWT
- Frontend: SvelteKit, Svelte stores
- Runtime: Bun
- Language: JavaScript

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-15 11:01:10 +01:00
commit 8c26fc93e3
41 changed files with 3424 additions and 0 deletions

View File

@@ -0,0 +1,146 @@
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
/**
* @typedef {Object} User
* @property {number} id
* @property {string} email
* @property {string} passwordHash
* @property {string} callsign
* @property {string|null} lotwUsername
* @property {string|null} lotwPassword
* @property {Date} createdAt
* @property {Date} updatedAt
*/
export const users = sqliteTable('users', {
id: integer('id').primaryKey({ autoIncrement: true }),
email: text('email').notNull().unique(),
passwordHash: text('password_hash').notNull(),
callsign: text('callsign').notNull(),
lotwUsername: text('lotw_username'),
lotwPassword: text('lotw_password'), // Encrypted
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
});
/**
* @typedef {Object} QSO
* @property {number} id
* @property {number} userId
* @property {string} callsign
* @property {string} qsoDate
* @property {string} timeOn
* @property {string|null} band
* @property {string|null} mode
* @property {number|null} freq
* @property {number|null} freqRx
* @property {string|null} entity
* @property {number|null} entityId
* @property {string|null} grid
* @property {string|null} gridSource
* @property {string|null} continent
* @property {number|null} cqZone
* @property {number|null} ituZone
* @property {string|null} state
* @property {string|null} county
* @property {string|null} satName
* @property {string|null} satMode
* @property {string|null} lotwQslRdate
* @property {string|null} lotwQslRstatus
* @property {Date|null} lotwSyncedAt
* @property {Date} createdAt
*/
export const qsos = sqliteTable('qsos', {
id: integer('id').primaryKey({ autoIncrement: true }),
userId: integer('user_id').notNull().references(() => users.id),
// QSO fields
callsign: text('callsign').notNull(),
qsoDate: text('qso_date').notNull(), // ADIF format: YYYYMMDD
timeOn: text('time_on').notNull(), // HHMMSS
band: text('band'), // 160m, 80m, 40m, etc.
mode: text('mode'), // CW, SSB, FT8, etc.
freq: integer('freq'), // Frequency in Hz
freqRx: integer('freq_rx'), // RX frequency (satellite)
// Entity/location fields
entity: text('entity'), // DXCC entity name
entityId: integer('entity_id'), // DXCC entity number
grid: text('grid'), // Maidenhead grid square
gridSource: text('grid_source'), // LOTW, USER, CALC
continent: text('continent'), // NA, SA, EU, AF, AS, OC, AN
cqZone: integer('cq_zone'),
ituZone: integer('itu_zone'),
state: text('state'), // US state, CA province, etc.
county: text('county'),
// Satellite fields
satName: text('sat_name'),
satMode: text('sat_mode'),
// LoTW confirmation
lotwQslRdate: text('lotw_qsl_rdate'), // Confirmation date
lotwQslRstatus: text('lotw_qsl_rstatus'), // 'Y', 'N', '?'
// Cache metadata
lotwSyncedAt: integer('lotw_synced_at', { mode: 'timestamp' }),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
});
/**
* @typedef {Object} Award
* @property {string} id
* @property {string} name
* @property {string|null} description
* @property {string} definition
* @property {boolean} isActive
* @property {Date} createdAt
*/
export const awards = sqliteTable('awards', {
id: text('id').primaryKey(), // 'dxcc', 'was', 'vucc'
name: text('name').notNull(),
description: text('description'),
definition: text('definition').notNull(), // JSON rule definition
isActive: integer('is_active', { mode: 'boolean' }).notNull().default(true),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
});
/**
* @typedef {Object} AwardProgress
* @property {number} id
* @property {number} userId
* @property {string} awardId
* @property {number} workedCount
* @property {number} confirmedCount
* @property {number} totalRequired
* @property {string|null} workedEntities
* @property {string|null} confirmedEntities
* @property {Date|null} lastCalculatedAt
* @property {Date|null} lastQsoSyncAt
* @property {Date} updatedAt
*/
export const awardProgress = sqliteTable('award_progress', {
id: integer('id').primaryKey({ autoIncrement: true }),
userId: integer('user_id').notNull().references(() => users.id),
awardId: text('award_id').notNull().references(() => awards.id),
// Calculated progress
workedCount: integer('worked_count').notNull().default(0),
confirmedCount: integer('confirmed_count').notNull().default(0),
totalRequired: integer('total_required').notNull(),
// Detailed breakdown (JSON)
workedEntities: text('worked_entities'), // JSON array
confirmedEntities: text('confirmed_entities'), // JSON array
// Cache control
lastCalculatedAt: integer('last_calculated_at', { mode: 'timestamp' }),
lastQsoSyncAt: integer('last_qso_sync_at', { mode: 'timestamp' }),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
});
// Export all schemas
export const schema = { users, qsos, awards, awardProgress };