feat: ADIF monitor — auto-import QSOs from watched external ADIF files

Watches a configurable list of external ADIF files (fldigi RTTY logbook,
N1MM, VarAC…) and imports newly appended QSOs automatically. Each record
goes through the existing UDP-log path, so it gets full enrichment, ±2-minute
dedup (shared udpLogMu, can't race the UDP auto-log) and automatic upload to
the configured external services — no per-file toggles like Log4OM.

A newly added file starts at its current size (offset -1 sentinel → size on
first scan), so the QSOs already in it are NOT bulk-imported; only contacts
logged after it was added come in. Reads only up to the last complete <eor>
so a half-written record waits. Handles truncation/rotation (re-reads from 0,
dedup protects) and persists per-file offsets without clobbering a concurrent
UI edit of the file list.

New Settings → ADIF monitor section: master enable + add/remove/toggle files.
Backend emits adifmon:imported → the grid refreshes and a toast reports the
count. i18n EN + FR.
This commit is contained in:
2026-07-19 01:35:00 +02:00
parent 19993bafc1
commit 8fc04563e1
8 changed files with 405 additions and 1 deletions
+49
View File
@@ -1296,6 +1296,55 @@ export namespace lotwusers {
export namespace main {
export class ADIFWatchFile {
path: string;
enabled: boolean;
offset: number;
static createFrom(source: any = {}) {
return new ADIFWatchFile(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.path = source["path"];
this.enabled = source["enabled"];
this.offset = source["offset"];
}
}
export class ADIFMonitorConfig {
enabled: boolean;
files: ADIFWatchFile[];
static createFrom(source: any = {}) {
return new ADIFMonitorConfig(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.enabled = source["enabled"];
this.files = this.convertValues(source["files"], ADIFWatchFile);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class AntGeniusSettings {
enabled: boolean;
host: string;