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 {string|null} dclApiKey * @property {boolean} isAdmin * @property {Date|null} lastSeen * @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 dclApiKey: text('dcl_api_key'), // DCL API key for future use isAdmin: integer('is_admin', { mode: 'boolean' }).notNull().default(false), lastSeen: integer('last_seen', { mode: 'timestamp' }), 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} myDarcDok * @property {string|null} darcDok * @property {string|null} lotwQslRdate * @property {string|null} lotwQslRstatus * @property {string|null} dclQslRdate * @property {string|null} dclQslRstatus * @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'), // DARC DOK fields (DARC Ortsverband Kennung - German local club identifier) myDarcDok: text('my_darc_dok'), // User's own DOK (e.g., 'F03', 'P30') darcDok: text('darc_dok'), // QSO partner's DOK // LoTW confirmation lotwQslRdate: text('lotw_qsl_rdate'), // Confirmation date lotwQslRstatus: text('lotw_qsl_rstatus'), // 'Y', 'N', '?' // DCL confirmation (DARC Community Logbook) dclQslRdate: text('dcl_qsl_rdate'), // Confirmation date dclQslRstatus: text('dcl_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()), }); /** * @typedef {Object} SyncJob * @property {number} id * @property {number} userId * @property {string} status * @property {string} type * @property {Date|null} startedAt * @property {Date|null} completedAt * @property {string|null} result * @property {string|null} error * @property {Date} createdAt */ export const syncJobs = sqliteTable('sync_jobs', { id: integer('id').primaryKey({ autoIncrement: true }), userId: integer('user_id').notNull().references(() => users.id), status: text('status').notNull(), // pending, running, completed, failed type: text('type').notNull(), // lotw_sync, etc. startedAt: integer('started_at', { mode: 'timestamp' }), completedAt: integer('completed_at', { mode: 'timestamp' }), result: text('result'), // JSON string error: text('error'), createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()), }); /** * @typedef {Object} QSOChange * @property {number} id * @property {number} jobId * @property {number|null} qsoId * @property {string} changeType - 'added' or 'updated' * @property {string|null} beforeData - JSON snapshot before change (for updates) * @property {string|null} afterData - JSON snapshot after change * @property {Date} createdAt */ export const qsoChanges = sqliteTable('qso_changes', { id: integer('id').primaryKey({ autoIncrement: true }), jobId: integer('job_id').notNull().references(() => syncJobs.id), qsoId: integer('qso_id').references(() => qsos.id), // null for added QSOs until created changeType: text('change_type').notNull(), // 'added' or 'updated' beforeData: text('before_data'), // JSON snapshot before change afterData: text('after_data'), // JSON snapshot after change createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()), }); /** * @typedef {Object} AdminAction * @property {number} id * @property {number} adminId * @property {string} actionType * @property {number|null} targetUserId * @property {string|null} details * @property {Date} createdAt */ export const adminActions = sqliteTable('admin_actions', { id: integer('id').primaryKey({ autoIncrement: true }), adminId: integer('admin_id').notNull().references(() => users.id), actionType: text('action_type').notNull(), // 'impersonate_start', 'impersonate_stop', 'role_change', 'user_delete', etc. targetUserId: integer('target_user_id').references(() => users.id), details: text('details'), // JSON with additional context createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()), }); /** * @typedef {Object} AutoSyncSettings * @property {number} userId * @property {boolean} lotwEnabled * @property {number} lotwIntervalHours * @property {Date|null} lotwLastSyncAt * @property {Date|null} lotwNextSyncAt * @property {boolean} dclEnabled * @property {number} dclIntervalHours * @property {Date|null} dclLastSyncAt * @property {Date|null} dclNextSyncAt * @property {Date} createdAt * @property {Date} updatedAt */ export const autoSyncSettings = sqliteTable('auto_sync_settings', { userId: integer('user_id').primaryKey().references(() => users.id), // LoTW auto-sync settings lotwEnabled: integer('lotw_enabled', { mode: 'boolean' }).notNull().default(false), lotwIntervalHours: integer('lotw_interval_hours').notNull().default(24), lotwLastSyncAt: integer('lotw_last_sync_at', { mode: 'timestamp' }), lotwNextSyncAt: integer('lotw_next_sync_at', { mode: 'timestamp' }), // DCL auto-sync settings dclEnabled: integer('dcl_enabled', { mode: 'boolean' }).notNull().default(false), dclIntervalHours: integer('dcl_interval_hours').notNull().default(24), dclLastSyncAt: integer('dcl_last_sync_at', { mode: 'timestamp' }), dclNextSyncAt: integer('dcl_next_sync_at', { mode: 'timestamp' }), createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()), updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()), }); // Export all schemas export const schema = { users, qsos, awards, awardProgress, syncJobs, qsoChanges, adminActions, autoSyncSettings };