feat: prepare database and UI for DCL integration
Add infrastructure for future DARC Community Logbook (DCL) integration: - Database schema: Add dcl_api_key, my_darc_dok, darc_dok, dcl_qsl_rdate, dcl_qsl_rstatus fields - Create DCL service stub with placeholder functions for when DCL provides API - Backend API: Add /api/auth/dcl-credentials endpoint for API key management - Frontend settings: Add DCL API key input with informational notice about API availability - QSO table: Add My DOK and DOK columns, update confirmation column for multiple services Note: DCL download API is not yet available. These changes prepare the application for future implementation when DCL adds programmatic access. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
18
drizzle/0001_free_hiroim.sql
Normal file
18
drizzle/0001_free_hiroim.sql
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
CREATE TABLE `sync_jobs` (
|
||||||
|
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`user_id` integer NOT NULL,
|
||||||
|
`status` text NOT NULL,
|
||||||
|
`type` text NOT NULL,
|
||||||
|
`started_at` integer,
|
||||||
|
`completed_at` integer,
|
||||||
|
`result` text,
|
||||||
|
`error` text,
|
||||||
|
`created_at` integer NOT NULL,
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE `qsos` ADD `my_darc_dok` text;--> statement-breakpoint
|
||||||
|
ALTER TABLE `qsos` ADD `darc_dok` text;--> statement-breakpoint
|
||||||
|
ALTER TABLE `qsos` ADD `dcl_qsl_rdate` text;--> statement-breakpoint
|
||||||
|
ALTER TABLE `qsos` ADD `dcl_qsl_rstatus` text;--> statement-breakpoint
|
||||||
|
ALTER TABLE `users` ADD `dcl_api_key` text;
|
||||||
575
drizzle/meta/0001_snapshot.json
Normal file
575
drizzle/meta/0001_snapshot.json
Normal file
@@ -0,0 +1,575 @@
|
|||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "b5c00e60-2f3c-4c2b-a540-0be8d9e856e6",
|
||||||
|
"prevId": "1b1674e7-6e3e-4ca6-8d19-066f2947942c",
|
||||||
|
"tables": {
|
||||||
|
"award_progress": {
|
||||||
|
"name": "award_progress",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"award_id": {
|
||||||
|
"name": "award_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"worked_count": {
|
||||||
|
"name": "worked_count",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"confirmed_count": {
|
||||||
|
"name": "confirmed_count",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"total_required": {
|
||||||
|
"name": "total_required",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"worked_entities": {
|
||||||
|
"name": "worked_entities",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"confirmed_entities": {
|
||||||
|
"name": "confirmed_entities",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"last_calculated_at": {
|
||||||
|
"name": "last_calculated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"last_qso_sync_at": {
|
||||||
|
"name": "last_qso_sync_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"award_progress_user_id_users_id_fk": {
|
||||||
|
"name": "award_progress_user_id_users_id_fk",
|
||||||
|
"tableFrom": "award_progress",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"award_progress_award_id_awards_id_fk": {
|
||||||
|
"name": "award_progress_award_id_awards_id_fk",
|
||||||
|
"tableFrom": "award_progress",
|
||||||
|
"tableTo": "awards",
|
||||||
|
"columnsFrom": [
|
||||||
|
"award_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"awards": {
|
||||||
|
"name": "awards",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"definition": {
|
||||||
|
"name": "definition",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"is_active": {
|
||||||
|
"name": "is_active",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"qsos": {
|
||||||
|
"name": "qsos",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"callsign": {
|
||||||
|
"name": "callsign",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"qso_date": {
|
||||||
|
"name": "qso_date",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"time_on": {
|
||||||
|
"name": "time_on",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"band": {
|
||||||
|
"name": "band",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"name": "mode",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"freq": {
|
||||||
|
"name": "freq",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"freq_rx": {
|
||||||
|
"name": "freq_rx",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"entity": {
|
||||||
|
"name": "entity",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"entity_id": {
|
||||||
|
"name": "entity_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"grid": {
|
||||||
|
"name": "grid",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"grid_source": {
|
||||||
|
"name": "grid_source",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"continent": {
|
||||||
|
"name": "continent",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"cq_zone": {
|
||||||
|
"name": "cq_zone",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"itu_zone": {
|
||||||
|
"name": "itu_zone",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"name": "state",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"county": {
|
||||||
|
"name": "county",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"sat_name": {
|
||||||
|
"name": "sat_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"sat_mode": {
|
||||||
|
"name": "sat_mode",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"my_darc_dok": {
|
||||||
|
"name": "my_darc_dok",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"darc_dok": {
|
||||||
|
"name": "darc_dok",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"lotw_qsl_rdate": {
|
||||||
|
"name": "lotw_qsl_rdate",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"lotw_qsl_rstatus": {
|
||||||
|
"name": "lotw_qsl_rstatus",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"dcl_qsl_rdate": {
|
||||||
|
"name": "dcl_qsl_rdate",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"dcl_qsl_rstatus": {
|
||||||
|
"name": "dcl_qsl_rstatus",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"lotw_synced_at": {
|
||||||
|
"name": "lotw_synced_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"qsos_user_id_users_id_fk": {
|
||||||
|
"name": "qsos_user_id_users_id_fk",
|
||||||
|
"tableFrom": "qsos",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"sync_jobs": {
|
||||||
|
"name": "sync_jobs",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "type",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"started_at": {
|
||||||
|
"name": "started_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"completed_at": {
|
||||||
|
"name": "completed_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"result": {
|
||||||
|
"name": "result",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"name": "error",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"sync_jobs_user_id_users_id_fk": {
|
||||||
|
"name": "sync_jobs_user_id_users_id_fk",
|
||||||
|
"tableFrom": "sync_jobs",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"name": "users",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"password_hash": {
|
||||||
|
"name": "password_hash",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"callsign": {
|
||||||
|
"name": "callsign",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"lotw_username": {
|
||||||
|
"name": "lotw_username",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"lotw_password": {
|
||||||
|
"name": "lotw_password",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"dcl_api_key": {
|
||||||
|
"name": "dcl_api_key",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"users_email_unique": {
|
||||||
|
"name": "users_email_unique",
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"enums": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,13 @@
|
|||||||
"when": 1768462458852,
|
"when": 1768462458852,
|
||||||
"tag": "0000_burly_unus",
|
"tag": "0000_burly_unus",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 1,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1768641501799,
|
||||||
|
"tag": "0001_free_hiroim",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,7 @@ import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
|
|||||||
* @property {string} callsign
|
* @property {string} callsign
|
||||||
* @property {string|null} lotwUsername
|
* @property {string|null} lotwUsername
|
||||||
* @property {string|null} lotwPassword
|
* @property {string|null} lotwPassword
|
||||||
|
* @property {string|null} dclApiKey
|
||||||
* @property {Date} createdAt
|
* @property {Date} createdAt
|
||||||
* @property {Date} updatedAt
|
* @property {Date} updatedAt
|
||||||
*/
|
*/
|
||||||
@@ -19,6 +20,7 @@ export const users = sqliteTable('users', {
|
|||||||
callsign: text('callsign').notNull(),
|
callsign: text('callsign').notNull(),
|
||||||
lotwUsername: text('lotw_username'),
|
lotwUsername: text('lotw_username'),
|
||||||
lotwPassword: text('lotw_password'), // Encrypted
|
lotwPassword: text('lotw_password'), // Encrypted
|
||||||
|
dclApiKey: text('dcl_api_key'), // DCL API key for future use
|
||||||
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
||||||
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
||||||
});
|
});
|
||||||
@@ -45,8 +47,12 @@ export const users = sqliteTable('users', {
|
|||||||
* @property {string|null} county
|
* @property {string|null} county
|
||||||
* @property {string|null} satName
|
* @property {string|null} satName
|
||||||
* @property {string|null} satMode
|
* @property {string|null} satMode
|
||||||
|
* @property {string|null} myDarcDok
|
||||||
|
* @property {string|null} darcDok
|
||||||
* @property {string|null} lotwQslRdate
|
* @property {string|null} lotwQslRdate
|
||||||
* @property {string|null} lotwQslRstatus
|
* @property {string|null} lotwQslRstatus
|
||||||
|
* @property {string|null} dclQslRdate
|
||||||
|
* @property {string|null} dclQslRstatus
|
||||||
* @property {Date|null} lotwSyncedAt
|
* @property {Date|null} lotwSyncedAt
|
||||||
* @property {Date} createdAt
|
* @property {Date} createdAt
|
||||||
*/
|
*/
|
||||||
@@ -79,10 +85,18 @@ export const qsos = sqliteTable('qsos', {
|
|||||||
satName: text('sat_name'),
|
satName: text('sat_name'),
|
||||||
satMode: text('sat_mode'),
|
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
|
// LoTW confirmation
|
||||||
lotwQslRdate: text('lotw_qsl_rdate'), // Confirmation date
|
lotwQslRdate: text('lotw_qsl_rdate'), // Confirmation date
|
||||||
lotwQslRstatus: text('lotw_qsl_rstatus'), // 'Y', 'N', '?'
|
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
|
// Cache metadata
|
||||||
lotwSyncedAt: integer('lotw_synced_at', { mode: 'timestamp' }),
|
lotwSyncedAt: integer('lotw_synced_at', { mode: 'timestamp' }),
|
||||||
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
authenticateUser,
|
authenticateUser,
|
||||||
getUserById,
|
getUserById,
|
||||||
updateLoTWCredentials,
|
updateLoTWCredentials,
|
||||||
|
updateDCLCredentials,
|
||||||
} from './services/auth.service.js';
|
} from './services/auth.service.js';
|
||||||
import {
|
import {
|
||||||
getUserQSOs,
|
getUserQSOs,
|
||||||
@@ -235,6 +236,40 @@ const app = new Elysia()
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT /api/auth/dcl-credentials
|
||||||
|
* Update DCL credentials (requires authentication)
|
||||||
|
*/
|
||||||
|
.put(
|
||||||
|
'/api/auth/dcl-credentials',
|
||||||
|
async ({ user, body, set }) => {
|
||||||
|
if (!user) {
|
||||||
|
set.status = 401;
|
||||||
|
return { success: false, error: 'Unauthorized' };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateDCLCredentials(user.id, body.dclApiKey);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'DCL credentials updated successfully',
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
set.status = 500;
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to update DCL credentials',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
body: t.Object({
|
||||||
|
dclApiKey: t.String(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/lotw/sync
|
* POST /api/lotw/sync
|
||||||
* Queue a LoTW sync job (requires authentication)
|
* Queue a LoTW sync job (requires authentication)
|
||||||
|
|||||||
@@ -126,3 +126,19 @@ export async function updateLoTWCredentials(userId, lotwUsername, lotwPassword)
|
|||||||
})
|
})
|
||||||
.where(eq(users.id, userId));
|
.where(eq(users.id, userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update user's DCL API key
|
||||||
|
* @param {number} userId - User ID
|
||||||
|
* @param {string} dclApiKey - DCL API key
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
export async function updateDCLCredentials(userId, dclApiKey) {
|
||||||
|
await db
|
||||||
|
.update(users)
|
||||||
|
.set({
|
||||||
|
dclApiKey,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(users.id, userId));
|
||||||
|
}
|
||||||
|
|||||||
214
src/backend/services/dcl.service.js
Normal file
214
src/backend/services/dcl.service.js
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
import { db, logger } from '../config.js';
|
||||||
|
import { qsos } from '../db/schema/index.js';
|
||||||
|
import { max, sql, eq, and, desc } from 'drizzle-orm';
|
||||||
|
import { updateJobProgress } from './job-queue.service.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DCL (DARC Community Logbook) Service
|
||||||
|
*
|
||||||
|
* NOTE: DCL does not currently have a public API for downloading QSOs.
|
||||||
|
* This service is prepared as a stub for when DCL adds API support.
|
||||||
|
*
|
||||||
|
* When DCL provides an API, implement:
|
||||||
|
* - fetchQSOsFromDCL() - Download QSOs from DCL
|
||||||
|
* - syncQSOs() - Sync QSOs to database
|
||||||
|
* - getLastDCLQSLDate() - Get last QSL date for incremental sync
|
||||||
|
*
|
||||||
|
* DCL Information:
|
||||||
|
* - Website: https://dcl.darc.de/
|
||||||
|
* - ADIF Export: https://dcl.darc.de/dml/export_adif_form.php (manual only)
|
||||||
|
* - DOK fields: MY_DARC_DOK (user's DOK), DARC_DOK (partner's DOK)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch QSOs from DCL
|
||||||
|
*
|
||||||
|
* TODO: Implement when DCL provides a download API
|
||||||
|
* Expected implementation:
|
||||||
|
* - Use DCL API key for authentication
|
||||||
|
* - Fetch ADIF data with confirmations
|
||||||
|
* - Parse and return QSO records
|
||||||
|
*
|
||||||
|
* @param {string} dclApiKey - DCL API key
|
||||||
|
* @param {Date|null} sinceDate - Last sync date for incremental sync
|
||||||
|
* @returns {Promise<Array>} Array of parsed QSO records
|
||||||
|
*/
|
||||||
|
export async function fetchQSOsFromDCL(dclApiKey, sinceDate = null) {
|
||||||
|
logger.info('DCL sync not yet implemented - API endpoint not available', {
|
||||||
|
sinceDate: sinceDate?.toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
throw new Error('DCL download API is not yet available. DCL does not currently provide a public API for downloading QSOs. Use the manual ADIF export at https://dcl.darc.de/dml/export_adif_form.php');
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FUTURE IMPLEMENTATION (when DCL provides API):
|
||||||
|
*
|
||||||
|
* const url = 'https://dcl.darc.de/api/...'; // TBA
|
||||||
|
*
|
||||||
|
* const params = new URLSearchParams({
|
||||||
|
* api_key: dclApiKey,
|
||||||
|
* format: 'adif',
|
||||||
|
* qsl: 'yes',
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* if (sinceDate) {
|
||||||
|
* const dateStr = sinceDate.toISOString().split('T')[0].replace(/-/g, '');
|
||||||
|
* params.append('qso_qslsince', dateStr);
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* const response = await fetch(`${url}?${params}`, {
|
||||||
|
* headers: {
|
||||||
|
* 'Accept': 'text/plain',
|
||||||
|
* },
|
||||||
|
* timeout: REQUEST_TIMEOUT,
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* if (!response.ok) {
|
||||||
|
* throw new Error(`DCL API error: ${response.status}`);
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* const adifData = await response.text();
|
||||||
|
* return parseADIF(adifData);
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse ADIF data from DCL
|
||||||
|
*
|
||||||
|
* TODO: Implement ADIF parser for DCL format
|
||||||
|
* Should handle DCL-specific fields:
|
||||||
|
* - MY_DARC_DOK
|
||||||
|
* - DARC_DOK
|
||||||
|
*
|
||||||
|
* @param {string} adifData - Raw ADIF data
|
||||||
|
* @returns {Array} Array of parsed QSO records
|
||||||
|
*/
|
||||||
|
function parseADIF(adifData) {
|
||||||
|
// TODO: Implement ADIF parser
|
||||||
|
// Should parse standard ADIF fields plus DCL-specific fields:
|
||||||
|
// - MY_DARC_DOK (user's own DOK)
|
||||||
|
// - DARC_DOK (QSO partner's DOK)
|
||||||
|
// - QSL_DATE (confirmation date from DCL)
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync QSOs from DCL to database
|
||||||
|
*
|
||||||
|
* TODO: Implement when DCL provides API
|
||||||
|
*
|
||||||
|
* @param {number} userId - User ID
|
||||||
|
* @param {string} dclApiKey - DCL API key
|
||||||
|
* @param {Date|null} sinceDate - Last sync date
|
||||||
|
* @param {number|null} jobId - Job ID for progress tracking
|
||||||
|
* @returns {Promise<Object>} Sync results
|
||||||
|
*/
|
||||||
|
export async function syncQSOs(userId, dclApiKey, sinceDate = null, jobId = null) {
|
||||||
|
logger.info('DCL sync not yet implemented', { userId, sinceDate, jobId });
|
||||||
|
|
||||||
|
throw new Error('DCL download API is not yet available');
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FUTURE IMPLEMENTATION:
|
||||||
|
*
|
||||||
|
* try {
|
||||||
|
* const adifQSOs = await fetchQSOsFromDCL(dclApiKey, sinceDate);
|
||||||
|
*
|
||||||
|
* let addedCount = 0;
|
||||||
|
* let updatedCount = 0;
|
||||||
|
* let errors = [];
|
||||||
|
*
|
||||||
|
* for (const adifQSO of adifQSOs) {
|
||||||
|
* try {
|
||||||
|
* // Map ADIF fields to database schema
|
||||||
|
* const qsoData = mapADIFToDB(adifQSO);
|
||||||
|
*
|
||||||
|
* // Check if QSO already exists
|
||||||
|
* const existing = await db.select()
|
||||||
|
* .from(qsos)
|
||||||
|
* .where(
|
||||||
|
* and(
|
||||||
|
* eq(qsos.userId, userId),
|
||||||
|
* eq(qsos.callsign, adifQSO.call),
|
||||||
|
* eq(qsos.qsoDate, adifQSO.qso_date),
|
||||||
|
* eq(qsos.timeOn, adifQSO.time_on)
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
* .limit(1);
|
||||||
|
*
|
||||||
|
* if (existing.length > 0) {
|
||||||
|
* // Update existing QSO with DCL confirmation
|
||||||
|
* await db.update(qsos)
|
||||||
|
* .set({
|
||||||
|
* dclQslRdate: adifQSO.qslrdate || null,
|
||||||
|
* dclQslRstatus: adifQSO.qslrdate ? 'Y' : 'N',
|
||||||
|
* darcDok: adifQSO.darc_dok || null,
|
||||||
|
* myDarcDok: adifQSO.my_darc_dok || null,
|
||||||
|
* })
|
||||||
|
* .where(eq(qsos.id, existing[0].id));
|
||||||
|
* updatedCount++;
|
||||||
|
* } else {
|
||||||
|
* // Insert new QSO
|
||||||
|
* await db.insert(qsos).values({
|
||||||
|
* userId,
|
||||||
|
* ...qsoData,
|
||||||
|
* dclQslRdate: adifQSO.qslrdate || null,
|
||||||
|
* dclQslRstatus: adifQSO.qslrdate ? 'Y' : 'N',
|
||||||
|
* });
|
||||||
|
* addedCount++;
|
||||||
|
* }
|
||||||
|
* } catch (err) {
|
||||||
|
* logger.error('Failed to process QSO', { error: err.message, qso: adifQSO });
|
||||||
|
* errors.push(err.message);
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* const result = {
|
||||||
|
* success: true,
|
||||||
|
* total: adifQSOs.length,
|
||||||
|
* added: addedCount,
|
||||||
|
* updated: updatedCount,
|
||||||
|
* errors,
|
||||||
|
* };
|
||||||
|
*
|
||||||
|
* logger.info('DCL sync completed', { ...result, jobId });
|
||||||
|
* return result;
|
||||||
|
*
|
||||||
|
* } catch (error) {
|
||||||
|
* logger.error('DCL sync failed', { error: error.message, userId, jobId });
|
||||||
|
* return { success: false, error: error.message, total: 0, added: 0, updated: 0 };
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get last DCL QSL date for incremental sync
|
||||||
|
*
|
||||||
|
* TODO: Implement when DCL provides API
|
||||||
|
*
|
||||||
|
* @param {number} userId - User ID
|
||||||
|
* @returns {Promise<Date|null>} Last QSL date or null
|
||||||
|
*/
|
||||||
|
export async function getLastDCLQSLDate(userId) {
|
||||||
|
try {
|
||||||
|
const result = await db
|
||||||
|
.select({ maxDate: max(qsos.dclQslRdate) })
|
||||||
|
.from(qsos)
|
||||||
|
.where(eq(qsos.userId, userId));
|
||||||
|
|
||||||
|
if (result[0]?.maxDate) {
|
||||||
|
// Convert ADIF date format (YYYYMMDD) to Date
|
||||||
|
const dateStr = result[0].maxDate;
|
||||||
|
const year = dateStr.substring(0, 4);
|
||||||
|
const month = dateStr.substring(4, 6);
|
||||||
|
const day = dateStr.substring(6, 8);
|
||||||
|
return new Date(`${year}-${month}-${day}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to get last DCL QSL date', { error: error.message, userId });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,6 +49,11 @@ export const authAPI = {
|
|||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify(credentials),
|
body: JSON.stringify(credentials),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
updateDCLCredentials: (credentials) => apiRequest('/auth/dcl-credentials', {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(credentials),
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Awards API
|
// Awards API
|
||||||
|
|||||||
@@ -401,6 +401,8 @@
|
|||||||
<th>Mode</th>
|
<th>Mode</th>
|
||||||
<th>Entity</th>
|
<th>Entity</th>
|
||||||
<th>Grid</th>
|
<th>Grid</th>
|
||||||
|
<th>My DOK</th>
|
||||||
|
<th>DOK</th>
|
||||||
<th>Confirmed</th>
|
<th>Confirmed</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -414,12 +416,24 @@
|
|||||||
<td>{qso.mode || '-'}</td>
|
<td>{qso.mode || '-'}</td>
|
||||||
<td>{qso.entity || '-'}</td>
|
<td>{qso.entity || '-'}</td>
|
||||||
<td>{qso.grid || '-'}</td>
|
<td>{qso.grid || '-'}</td>
|
||||||
|
<td>{qso.myDarcDok || '-'}</td>
|
||||||
|
<td>{qso.darcDok || '-'}</td>
|
||||||
<td>
|
<td>
|
||||||
|
{#if (qso.lotwQslRstatus === 'Y' && qso.lotwQslRdate) || (qso.dclQslRstatus === 'Y' && qso.dclQslRdate)}
|
||||||
|
<div class="confirmation-list">
|
||||||
{#if qso.lotwQslRstatus === 'Y' && qso.lotwQslRdate}
|
{#if qso.lotwQslRstatus === 'Y' && qso.lotwQslRdate}
|
||||||
<span class="confirmation-info">
|
<div class="confirmation-item">
|
||||||
<span class="service-type">LoTW</span>
|
<span class="service-type">LoTW</span>
|
||||||
<span class="confirmation-date">{formatDate(qso.lotwQslRdate)}</span>
|
<span class="confirmation-date">{formatDate(qso.lotwQslRdate)}</span>
|
||||||
</span>
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if qso.dclQslRstatus === 'Y' && qso.dclQslRdate}
|
||||||
|
<div class="confirmation-item">
|
||||||
|
<span class="service-type">DCL</span>
|
||||||
|
<span class="confirmation-date">{formatDate(qso.dclQslRdate)}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
-
|
-
|
||||||
{/if}
|
{/if}
|
||||||
@@ -745,7 +759,13 @@
|
|||||||
color: #856404;
|
color: #856404;
|
||||||
}
|
}
|
||||||
|
|
||||||
.confirmation-info {
|
.confirmation-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirmation-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
|
|||||||
@@ -6,11 +6,15 @@
|
|||||||
|
|
||||||
let lotwUsername = '';
|
let lotwUsername = '';
|
||||||
let lotwPassword = '';
|
let lotwPassword = '';
|
||||||
|
let dclApiKey = '';
|
||||||
let loading = false;
|
let loading = false;
|
||||||
let saving = false;
|
let savingLoTW = false;
|
||||||
|
let savingDCL = false;
|
||||||
let error = null;
|
let error = null;
|
||||||
let success = false;
|
let successLoTW = false;
|
||||||
let hasCredentials = false;
|
let successDCL = false;
|
||||||
|
let hasLoTWCredentials = false;
|
||||||
|
let hasDCLCredentials = false;
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
// Load user profile to check if credentials exist
|
// Load user profile to check if credentials exist
|
||||||
@@ -25,8 +29,10 @@
|
|||||||
if (response.user) {
|
if (response.user) {
|
||||||
lotwUsername = response.user.lotwUsername || '';
|
lotwUsername = response.user.lotwUsername || '';
|
||||||
lotwPassword = ''; // Never pre-fill password for security
|
lotwPassword = ''; // Never pre-fill password for security
|
||||||
hasCredentials = !!(response.user.lotwUsername && response.user.lotwPassword);
|
hasLoTWCredentials = !!(response.user.lotwUsername && response.user.lotwPassword);
|
||||||
console.log('Has credentials:', hasCredentials);
|
dclApiKey = response.user.dclApiKey || '';
|
||||||
|
hasDCLCredentials = !!response.user.dclApiKey;
|
||||||
|
console.log('Has LoTW credentials:', hasLoTWCredentials, 'Has DCL credentials:', hasDCLCredentials);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to load profile:', err);
|
console.error('Failed to load profile:', err);
|
||||||
@@ -36,31 +42,58 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSave(e) {
|
async function handleSaveLoTW(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
saving = true;
|
savingLoTW = true;
|
||||||
error = null;
|
error = null;
|
||||||
success = false;
|
successLoTW = false;
|
||||||
|
|
||||||
console.log('Saving credentials:', { lotwUsername, hasPassword: !!lotwPassword });
|
console.log('Saving LoTW credentials:', { lotwUsername, hasPassword: !!lotwPassword });
|
||||||
|
|
||||||
await authAPI.updateLoTWCredentials({
|
await authAPI.updateLoTWCredentials({
|
||||||
lotwUsername,
|
lotwUsername,
|
||||||
lotwPassword
|
lotwPassword
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Save successful!');
|
console.log('LoTW Save successful!');
|
||||||
|
|
||||||
// Reload profile to update hasCredentials flag
|
// Reload profile to update hasCredentials flag
|
||||||
await loadProfile();
|
await loadProfile();
|
||||||
success = true;
|
successLoTW = true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Save failed:', err);
|
console.error('LoTW Save failed:', err);
|
||||||
error = err.message;
|
error = err.message;
|
||||||
} finally {
|
} finally {
|
||||||
saving = false;
|
savingLoTW = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSaveDCL(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
try {
|
||||||
|
savingDCL = true;
|
||||||
|
error = null;
|
||||||
|
successDCL = false;
|
||||||
|
|
||||||
|
console.log('Saving DCL credentials:', { hasApiKey: !!dclApiKey });
|
||||||
|
|
||||||
|
await authAPI.updateDCLCredentials({
|
||||||
|
dclApiKey
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('DCL Save successful!');
|
||||||
|
|
||||||
|
// Reload profile
|
||||||
|
await loadProfile();
|
||||||
|
successDCL = true;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('DCL Save failed:', err);
|
||||||
|
error = err.message;
|
||||||
|
} finally {
|
||||||
|
savingDCL = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,18 +129,18 @@
|
|||||||
Your credentials are stored securely and used only to fetch your confirmed QSOs.
|
Your credentials are stored securely and used only to fetch your confirmed QSOs.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{#if hasCredentials}
|
{#if hasLoTWCredentials}
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
<strong>Credentials configured</strong> - You can update them below if needed.
|
<strong>Credentials configured</strong> - You can update them below if needed.
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<form on:submit={handleSave} class="settings-form">
|
<form on:submit={handleSaveLoTW} class="settings-form">
|
||||||
{#if error}
|
{#if error}
|
||||||
<div class="alert alert-error">{error}</div>
|
<div class="alert alert-error">{error}</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if success}
|
{#if successLoTW}
|
||||||
<div class="alert alert-success">
|
<div class="alert alert-success">
|
||||||
LoTW credentials saved successfully!
|
LoTW credentials saved successfully!
|
||||||
</div>
|
</div>
|
||||||
@@ -138,8 +171,8 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary" disabled={saving}>
|
<button type="submit" class="btn btn-primary" disabled={savingLoTW}>
|
||||||
{saving ? 'Saving...' : 'Save Credentials'}
|
{savingLoTW ? 'Saving...' : 'Save LoTW Credentials'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -157,6 +190,59 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-section">
|
||||||
|
<h2>DCL Credentials</h2>
|
||||||
|
<p class="help-text">
|
||||||
|
Configure your DARC Community Logbook (DCL) API key for future sync functionality.
|
||||||
|
<strong>Note:</strong> DCL does not currently provide a download API. This is prepared for when they add one.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{#if hasDCLCredentials}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<strong>API key configured</strong> - You can update it below if needed.
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<form on:submit={handleSaveDCL} class="settings-form">
|
||||||
|
{#if successDCL}
|
||||||
|
<div class="alert alert-success">
|
||||||
|
DCL API key saved successfully!
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="dclApiKey">DCL API Key</label>
|
||||||
|
<input
|
||||||
|
id="dclApiKey"
|
||||||
|
type="password"
|
||||||
|
bind:value={dclApiKey}
|
||||||
|
placeholder="Your DCL API key"
|
||||||
|
/>
|
||||||
|
<p class="hint">
|
||||||
|
Enter your DCL API key for future sync functionality
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary" disabled={savingDCL}>
|
||||||
|
{savingDCL ? 'Saving...' : 'Save DCL API Key'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<h3>About DCL</h3>
|
||||||
|
<p>
|
||||||
|
DCL (DARC Community Logbook) is DARC's web-based logbook system for German amateur radio awards.
|
||||||
|
It includes DOK (DARC Ortsverband Kennung) fields for local club awards.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Status:</strong> Download API not yet available.{' '}
|
||||||
|
<a href="https://dcl.darc.de/" target="_blank" rel="noopener">
|
||||||
|
Visit DCL website
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
Reference in New Issue
Block a user