feat(rda): measure the two district sources before arbitrating between them
A log imported from HAMLOG.online carries the Russian district in CNTY, and the offline RDA database has its own answer for the same contact. Which wins is a real question — but only if the two are independent. If HAMLOG's value is itself a lookup in the same kind of database, there is nothing to arbitrate and a precedence rule would be ceremony around a redundancy. So this measures instead of deciding: Settings → RDA gains a read-only compare that reports agreements, disagreements, and the contacts where only one side knows, then lists the disagreements — callsign, date, both districts, whether the database answer was a DATED record (the award administrators' own fact) or the callsign's current district (an assumption), and whether the QSO is confirmed on HAMLOG (their value has then been through a match of two logs). It changes nothing in the log. What to do about a conflict is the next decision, and it should be taken with the operator's own numbers in hand. Also: the four HAMLOG fields become filterable, so 'never sent there' is a filter rather than a guess. A field that can be written and not read back is half a feature — pinned by a test against the bulk-editable list.
This commit is contained in:
@@ -13,7 +13,7 @@ import {
|
||||
ListProfiles, GetActiveProfile, SaveProfile, DeleteProfile, ActivateProfile, DuplicateProfile,
|
||||
GetRotators, SaveRotators, TestRotatorDevice, RotatorPark, RotatorStop,
|
||||
GetRotorPresets, SaveRotorPresets, ResetRotorPresets,
|
||||
GetUltrabeamSettings, SaveUltrabeamSettings, TestUltrabeam, CompactDatabase, CheckHamlogKey,
|
||||
GetUltrabeamSettings, SaveUltrabeamSettings, TestUltrabeam, CompactDatabase, CheckHamlogKey, CompareRDASources,
|
||||
GetAntGeniusSettings, SaveAntGeniusSettings,
|
||||
GetTunerGeniusSettings, SaveTunerGeniusSettings,
|
||||
GetPSUSettings, SavePSUSettings,
|
||||
@@ -1854,6 +1854,20 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
const [rdaUseCurrent, setRdaUseCurrent] = useState(true);
|
||||
const [rdaMsg, setRdaMsg] = useState<string | null>(null);
|
||||
useEffect(() => { RDADatabaseCount().then((n) => setRdaCount(Number(n) || 0)).catch(() => {}); }, []);
|
||||
// Measuring the two sources against each other, before deciding which wins.
|
||||
//
|
||||
// Read-only on purpose: the question "is HAMLOG's district independent
|
||||
// evidence or a copy of the same database?" has to be answered with the
|
||||
// operator's own log, and answering it must not modify that log.
|
||||
const [rdaCmp, setRdaCmp] = useState<any>(null);
|
||||
const [rdaCmpBusy, setRdaCmpBusy] = useState(false);
|
||||
const runRDACompare = async () => {
|
||||
setRdaCmpBusy(true); setRdaCmp(null);
|
||||
try { setRdaCmp(await CompareRDASources()); }
|
||||
catch (e: any) { setRdaMsg(String(e?.message ?? e)); }
|
||||
finally { setRdaCmpBusy(false); }
|
||||
};
|
||||
|
||||
const runRDABackfill = async () => {
|
||||
setRdaBusy(true); setRdaMsg(null);
|
||||
try {
|
||||
@@ -7220,6 +7234,56 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
</div>
|
||||
<p className="text-[11px] text-muted-foreground">{t('rda.neverOverwrites')}</p>
|
||||
</div>
|
||||
|
||||
{/* Comparing the two sources. Read-only: it answers a question about the
|
||||
log without touching it. */}
|
||||
<div className="rounded-md border border-border p-3 space-y-2">
|
||||
<div className="text-xs font-medium">{t('rda.cmpTitle')}</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button size="sm" variant="secondary" onClick={runRDACompare} disabled={rdaCmpBusy}>
|
||||
{rdaCmpBusy ? <Loader2 className="size-3.5 animate-spin mr-1.5" /> : null}
|
||||
{t('rda.cmpRun')}
|
||||
</Button>
|
||||
{rdaCmp && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t('rda.cmpDone', {
|
||||
s: rdaCmp.scanned ?? 0, a: rdaCmp.agree ?? 0, d: rdaCmp.disagree ?? 0,
|
||||
l: rdaCmp.only_log ?? 0, b: rdaCmp.only_db ?? 0,
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* The disagreements themselves. A count alone would say "there is a
|
||||
problem" and leave the operator with no way to look at it. */}
|
||||
{rdaCmp?.conflicts?.length > 0 && (
|
||||
<div className="max-h-56 overflow-auto rounded border border-border">
|
||||
<table className="w-full text-[11px]">
|
||||
<thead className="sticky top-0 bg-card text-left text-muted-foreground border-b border-border">
|
||||
<tr>
|
||||
<th className="py-1 px-2 font-medium">{t('rda.cmpCall')}</th>
|
||||
<th className="py-1 pr-2 font-medium">{t('rda.cmpDate')}</th>
|
||||
<th className="py-1 pr-2 font-medium">{t('rda.cmpFromLog')}</th>
|
||||
<th className="py-1 pr-2 font-medium">{t('rda.cmpFromDb')}</th>
|
||||
<th className="py-1 pr-2 font-medium">{t('rda.cmpKind')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rdaCmp.conflicts.map((c: any) => (
|
||||
<tr key={c.qso_id} className="border-b border-border/30">
|
||||
<td className="py-1 px-2 font-mono font-semibold">{c.callsign}</td>
|
||||
<td className="py-1 pr-2 font-mono">{c.date}</td>
|
||||
<td className="py-1 pr-2 font-mono">{c.from_log}{c.confirmed ? ' ✓' : ''}</td>
|
||||
<td className="py-1 pr-2 font-mono">{c.from_db}</td>
|
||||
<td className="py-1 pr-2 text-muted-foreground">
|
||||
{c.dated ? t('rda.cmpDated') : t('rda.cmpCurrent')}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user