Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9f8380ece | ||
|
|
10e1504531 | ||
|
|
2e3464104d | ||
|
|
dc8f9c5099 | ||
|
|
08905a1680 | ||
|
|
fd939797dd |
@@ -20075,7 +20075,27 @@ func (a *App) IsNewUSCounty(state, cnty string) bool {
|
|||||||
// native ones: an operator on OmniRig or Flex gets the same server.
|
// native ones: an operator on OmniRig or Flex gets the same server.
|
||||||
type catShareRig struct{ a *App }
|
type catShareRig struct{ a *App }
|
||||||
|
|
||||||
func (r catShareRig) Freq() int64 { return r.a.cat.State().FreqHz }
|
// Freq is what a rigctl client gets for "f": the frequency of the VFO in use,
|
||||||
|
// which is where we LISTEN.
|
||||||
|
//
|
||||||
|
// This returned FreqHz, and RigState follows ADIF where FreqHz is the TRANSMIT
|
||||||
|
// frequency — so with split on, every client asking "what frequency is the
|
||||||
|
// radio on" was told VFO B. Reported from a station running an IC-7850: turning
|
||||||
|
// VFO B moved VFO A. Nothing in OpsLog was writing to the radio; a client was
|
||||||
|
// reading the dial, being handed the wrong VFO, and writing it back.
|
||||||
|
//
|
||||||
|
// The split TX frequency is a separate question, and Hamlib has a separate
|
||||||
|
// command for it ("i" / get_split_freq) which Split() below answers.
|
||||||
|
func (r catShareRig) Freq() int64 { return shareRXFreq(r.a.cat.State()) }
|
||||||
|
|
||||||
|
// shareRXFreq is that rule on its own, so it can be pinned by a test: it is one
|
||||||
|
// line, it was wrong, and being wrong cost a station its VFO A.
|
||||||
|
func shareRXFreq(st cat.RigState) int64 {
|
||||||
|
if st.Split && st.RxFreqHz > 0 {
|
||||||
|
return st.RxFreqHz
|
||||||
|
}
|
||||||
|
return st.FreqHz
|
||||||
|
}
|
||||||
func (r catShareRig) Mode() string { return r.a.cat.State().Mode }
|
func (r catShareRig) Mode() string { return r.a.cat.State().Mode }
|
||||||
|
|
||||||
// Split reports the flag and the OTHER VFO's frequency. RigState follows ADIF —
|
// Split reports the flag and the OTHER VFO's frequency. RigState follows ADIF —
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hamlog/internal/cat"
|
||||||
|
)
|
||||||
|
|
||||||
|
// What a rigctl client is told when it asks "what frequency is the radio on".
|
||||||
|
//
|
||||||
|
// Hamlib's "f" means the VFO IN USE — where the operator is listening. RigState
|
||||||
|
// follows ADIF, where FreqHz is where we TRANSMIT, and the two are the same
|
||||||
|
// number until split is engaged. They were handed over unchanged, so with split
|
||||||
|
// on every client asking for the dial was given VFO B.
|
||||||
|
//
|
||||||
|
// Reported from a station running an IC-7850 over USB: turning VFO B moved
|
||||||
|
// VFO A. OpsLog was writing nothing to that radio — a client read the dial, was
|
||||||
|
// handed the transmit VFO, and wrote it back as the dial.
|
||||||
|
func TestShareRXFreqAnswersTheListeningVFO(t *testing.T) {
|
||||||
|
const rx, tx = 14195000, 14200500
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
st cat.RigState
|
||||||
|
want int64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "simplex — the one frequency there is",
|
||||||
|
st: cat.RigState{FreqHz: rx},
|
||||||
|
want: rx,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "split — the RX VFO, not the TX one",
|
||||||
|
st: cat.RigState{Split: true, FreqHz: tx, RxFreqHz: rx},
|
||||||
|
want: rx,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// A backend that reports split without ever filling RxFreqHz would
|
||||||
|
// otherwise be answered with 0, and a client told the radio is on
|
||||||
|
// 0 Hz does something worse than nothing with it.
|
||||||
|
name: "split claimed but no RX frequency known",
|
||||||
|
st: cat.RigState{Split: true, FreqHz: tx},
|
||||||
|
want: tx,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
if got := shareRXFreq(c.st); got != c.want {
|
||||||
|
t.Errorf("shareRXFreq(%+v) = %d, want %d", c.st, got, c.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// And the pair, stated together: the two questions must not return the same
|
||||||
|
// answer while split is on, or the distinction has been lost somewhere.
|
||||||
|
func TestShareSplitReportsTheOtherVFO(t *testing.T) {
|
||||||
|
st := cat.RigState{Split: true, FreqHz: 14200500, RxFreqHz: 14195000}
|
||||||
|
rxAnswer := shareRXFreq(st)
|
||||||
|
txAnswer := st.FreqHz // what Split() hands back for "i" / get_split_freq
|
||||||
|
if rxAnswer == txAnswer {
|
||||||
|
t.Fatalf("get_freq and get_split_freq both answered %d — a client cannot tell the VFOs apart", rxAnswer)
|
||||||
|
}
|
||||||
|
if rxAnswer != st.RxFreqHz || txAnswer != st.FreqHz {
|
||||||
|
t.Errorf("got rx=%d tx=%d, want rx=%d tx=%d", rxAnswer, txAnswer, st.RxFreqHz, st.FreqHz)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,18 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"version": "0.26.17",
|
||||||
|
"date": "",
|
||||||
|
"en": [
|
||||||
|
"Awards, RDA district comparison: each disagreement can now be settled on its own row. Click the district you keep — the log's or the database's — and apply. It is written into the contact's CNTY and its award reference, and the settled rows leave the list.",
|
||||||
|
"The band/mode matrix: a confirmed slot now shows as confirmed. A callsign worked and not confirmed was outranking an entity CONFIRMED on the same band and mode, so a slot that needs nothing was painted as if it still did.",
|
||||||
|
"CAT sharing: a program connected to OpsLog's rigctl server is told the frequency it is LISTENING on. With split engaged it was given the transmit VFO instead, so a client that reads the dial and writes it back — WSJT-X and its like — could move VFO A when VFO B was turned."
|
||||||
|
],
|
||||||
|
"fr": [
|
||||||
|
"Diplômes, comparaison des districts RDA : chaque divergence se règle désormais sur sa propre ligne. Cliquer le district qu'on garde — celui du log ou celui de la base — puis appliquer. Il est écrit dans le CNTY du contact et dans sa référence de diplôme, et les lignes réglées quittent la liste.",
|
||||||
|
"Matrice bandes/modes : une case confirmée s'affiche enfin comme confirmée. Un indicatif travaillé et non confirmé l'emportait sur une entité CONFIRMÉE sur la même bande et le même mode, si bien qu'une case qui ne demandait plus rien était peinte comme s'il manquait encore quelque chose.",
|
||||||
|
"Partage CAT : un programme connecté au serveur rigctl d'OpsLog reçoit la fréquence sur laquelle on ÉCOUTE. En split, c'était le VFO d'émission qui lui était donné, si bien qu'un client qui lit le VFO et le réécrit — WSJT-X et consorts — pouvait déplacer le VFO A quand on tournait le VFO B."
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "0.26.16",
|
"version": "0.26.16",
|
||||||
"date": "",
|
"date": "",
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
ListProfiles, GetActiveProfile, SaveProfile, DeleteProfile, ActivateProfile, DuplicateProfile,
|
ListProfiles, GetActiveProfile, SaveProfile, DeleteProfile, ActivateProfile, DuplicateProfile,
|
||||||
GetRotators, SaveRotators, TestRotatorDevice, RotatorPark, RotatorStop,
|
GetRotators, SaveRotators, TestRotatorDevice, RotatorPark, RotatorStop,
|
||||||
GetRotorPresets, SaveRotorPresets, ResetRotorPresets,
|
GetRotorPresets, SaveRotorPresets, ResetRotorPresets,
|
||||||
GetUltrabeamSettings, SaveUltrabeamSettings, TestUltrabeam, CompactDatabase, CheckHamlogKey, CompareRDASources,
|
GetUltrabeamSettings, SaveUltrabeamSettings, TestUltrabeam, CompactDatabase, CheckHamlogKey, CompareRDASources, ApplyRDAChoices,
|
||||||
GetAntGeniusSettings, SaveAntGeniusSettings,
|
GetAntGeniusSettings, SaveAntGeniusSettings,
|
||||||
GetTunerGeniusSettings, SaveTunerGeniusSettings,
|
GetTunerGeniusSettings, SaveTunerGeniusSettings,
|
||||||
GetPSUSettings, SavePSUSettings,
|
GetPSUSettings, SavePSUSettings,
|
||||||
@@ -1946,6 +1946,11 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
|||||||
// nobody had pressed, and the one that had been pressed showed nothing at
|
// nobody had pressed, and the one that had been pressed showed nothing at
|
||||||
// all, which reads as "the button does nothing".
|
// all, which reads as "the button does nothing".
|
||||||
const [rdaCmpMsg, setRdaCmpMsg] = useState<string>('');
|
const [rdaCmpMsg, setRdaCmpMsg] = useState<string>('');
|
||||||
|
// Which source the operator kept, per QSO id. By id and not by row: a second
|
||||||
|
// comparison rebuilds the list, and a choice that followed a row number would
|
||||||
|
// then be applied to somebody else's contact.
|
||||||
|
const [rdaPick, setRdaPick] = useState<Record<number, 'log' | 'db'>>({});
|
||||||
|
const [rdaApplyBusy, setRdaApplyBusy] = useState(false);
|
||||||
const runRDACompare = async () => {
|
const runRDACompare = async () => {
|
||||||
setRdaCmpBusy(true); setRdaCmp(null); setRdaCmpMsg('');
|
setRdaCmpBusy(true); setRdaCmp(null); setRdaCmpMsg('');
|
||||||
try {
|
try {
|
||||||
@@ -7517,6 +7522,7 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
|||||||
<th className="py-1 pr-2 font-medium">{t('rda.cmpFromLog')}</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.cmpFromDb')}</th>
|
||||||
<th className="py-1 pr-2 font-medium">{t('rda.cmpKind')}</th>
|
<th className="py-1 pr-2 font-medium">{t('rda.cmpKind')}</th>
|
||||||
|
<th className="py-1 pr-2 font-medium text-center">{t('rda.cmpKeep')}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -7541,11 +7547,100 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
|||||||
<td className="py-1 pr-2 text-muted-foreground">
|
<td className="py-1 pr-2 text-muted-foreground">
|
||||||
{c.dated ? t('rda.cmpDated') : t('rda.cmpCurrent')}
|
{c.dated ? t('rda.cmpDated') : t('rda.cmpCurrent')}
|
||||||
</td>
|
</td>
|
||||||
|
{/* Which one wins, on the row that shows the two values.
|
||||||
|
Two buttons rather than a tick box: a tick box would
|
||||||
|
have to mean one of the sources silently, and an
|
||||||
|
operator arbitrating between "RO-19" and "KO-05"
|
||||||
|
should be picking the value they can read, not
|
||||||
|
remembering what a checked box stands for. */}
|
||||||
|
<td className="py-1 pr-2 whitespace-nowrap text-center">
|
||||||
|
<div className="inline-flex rounded border border-border overflow-hidden">
|
||||||
|
{(['log', 'db'] as const).map((side) => (
|
||||||
|
<button
|
||||||
|
key={side}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setRdaPick((p) => {
|
||||||
|
const next = { ...p };
|
||||||
|
if (next[c.qso_id] === side) delete next[c.qso_id];
|
||||||
|
else next[c.qso_id] = side;
|
||||||
|
return next;
|
||||||
|
})}
|
||||||
|
className={`px-2 py-0.5 font-mono text-[10px] ${rdaPick[c.qso_id] === side
|
||||||
|
? 'bg-primary text-primary-foreground'
|
||||||
|
: 'hover:bg-muted'}`}
|
||||||
|
title={side === 'log' ? t('rda.cmpKeepLog') : t('rda.cmpKeepDb')}
|
||||||
|
>
|
||||||
|
{side === 'log' ? c.from_log : c.from_db}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Applying is a separate, deliberate act. Clicking through two
|
||||||
|
hundred rows and having each one written as it is clicked would
|
||||||
|
make a slip permanent before the operator had finished
|
||||||
|
reading. */}
|
||||||
|
<div className="flex items-center gap-3 flex-wrap">
|
||||||
|
<Button size="sm" variant="secondary" disabled={rdaApplyBusy || Object.keys(rdaPick).length === 0}
|
||||||
|
onClick={async () => {
|
||||||
|
setRdaApplyBusy(true);
|
||||||
|
try {
|
||||||
|
const choices = rdaCmp.conflicts
|
||||||
|
.filter((c: any) => rdaPick[c.qso_id])
|
||||||
|
.map((c: any) => ({
|
||||||
|
qso_id: c.qso_id,
|
||||||
|
district: rdaPick[c.qso_id] === 'log' ? c.from_log : c.from_db,
|
||||||
|
}));
|
||||||
|
const r: any = await ApplyRDAChoices(choices);
|
||||||
|
setRdaCmpMsg(r?.message || '');
|
||||||
|
setRdaPick({});
|
||||||
|
// The settled rows leave the list, and the counters follow.
|
||||||
|
//
|
||||||
|
// They used to stay, which made a working button look like a
|
||||||
|
// broken one: the operator decided a hundred contacts, the
|
||||||
|
// hundred rows stayed exactly as they were, and there was
|
||||||
|
// nothing anywhere to say the writing had happened. Dropping
|
||||||
|
// them here rather than re-running the comparison, because
|
||||||
|
// that reads the whole log — seconds of silence on a remote
|
||||||
|
// database, to be told what is already known.
|
||||||
|
if (r?.applied > 0) {
|
||||||
|
const settled = new Set(choices.map((c: any) => c.qso_id));
|
||||||
|
setRdaCmp((prev: any) => prev && ({
|
||||||
|
...prev,
|
||||||
|
disagree: Math.max(0, (prev.disagree ?? 0) - r.applied),
|
||||||
|
agree: (prev.agree ?? 0) + r.applied,
|
||||||
|
conflicts: (prev.conflicts ?? []).filter((c: any) => !settled.has(c.qso_id)),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
setRdaCmpMsg(String(e?.message ?? e));
|
||||||
|
} finally {
|
||||||
|
setRdaApplyBusy(false);
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
{rdaApplyBusy ? <Loader2 className="size-3.5 animate-spin mr-1.5" /> : null}
|
||||||
|
{t('rda.cmpApply', { n: Object.keys(rdaPick).length })}
|
||||||
|
</Button>
|
||||||
|
<button type="button" className="text-[11px] underline decoration-dotted hover:text-primary"
|
||||||
|
onClick={() => setRdaPick(Object.fromEntries(rdaCmp.conflicts.map((c: any) => [c.qso_id, 'db'])))}>
|
||||||
|
{t('rda.cmpAllDb')}
|
||||||
|
</button>
|
||||||
|
<button type="button" className="text-[11px] underline decoration-dotted hover:text-primary"
|
||||||
|
onClick={() => setRdaPick(Object.fromEntries(rdaCmp.conflicts.map((c: any) => [c.qso_id, 'log'])))}>
|
||||||
|
{t('rda.cmpAllLog')}
|
||||||
|
</button>
|
||||||
|
{Object.keys(rdaPick).length > 0 && (
|
||||||
|
<button type="button" className="text-[11px] underline decoration-dotted hover:text-primary"
|
||||||
|
onClick={() => setRdaPick({})}>
|
||||||
|
{t('rda.cmpClear')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-[11px] text-muted-foreground">{t('rda.cmpApplyHint')}</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ const en: Dict = {
|
|||||||
'sec.email': 'E-mail (SMTP)', 'sec.lookup': 'Callsign Lookup',
|
'sec.email': 'E-mail (SMTP)', 'sec.lookup': 'Callsign Lookup',
|
||||||
'sec.ftx': 'FTx decodes', 'ftx.hint': 'What OpsLog does on its own with the digital decode stream.', 'ftx.enable': 'Auto-call', 'ftx.enableHint': 'answer a decode without clicking it', 'ftx.callWhen': 'Call a station that is:', 'ftx.watch': 'Watch list', 'ftx.watchHint': 'One callsign per line, wildcards allowed (4S7*, */P). A watched station is answered ahead of the criteria above.', 'ftx.watchOnlyIf': 'but only if it is also:', 'ftx.cooldown': 'Ignore a callsign for', 'ftx.warn': 'This keys your transmitter without asking. It answers CQ only, never while you are already transmitting, one station at a time, and every call is written to the log file with its reason. Halt stops it.', 'ftx.c_dxcc': 'a new DXCC entity', 'ftx.c_bandmode': 'a new band AND a new mode for the entity', 'ftx.c_band': 'a new band for the entity', 'ftx.c_mode': 'a new mode for the entity', 'ftx.c_slot': 'a new slot (band+mode never worked together)', 'ftx.c_grid': 'a new grid square', 'ftx.c_county': 'a new US county', 'ftx.c_pota': 'a new POTA park', 'ftx.c_pfx': 'a new WPX prefix',
|
'sec.ftx': 'FTx decodes', 'ftx.hint': 'What OpsLog does on its own with the digital decode stream.', 'ftx.enable': 'Auto-call', 'ftx.enableHint': 'answer a decode without clicking it', 'ftx.callWhen': 'Call a station that is:', 'ftx.watch': 'Watch list', 'ftx.watchHint': 'One callsign per line, wildcards allowed (4S7*, */P). A watched station is answered ahead of the criteria above.', 'ftx.watchOnlyIf': 'but only if it is also:', 'ftx.cooldown': 'Ignore a callsign for', 'ftx.warn': 'This keys your transmitter without asking. It answers CQ only, never while you are already transmitting, one station at a time, and every call is written to the log file with its reason. Halt stops it.', 'ftx.c_dxcc': 'a new DXCC entity', 'ftx.c_bandmode': 'a new band AND a new mode for the entity', 'ftx.c_band': 'a new band for the entity', 'ftx.c_mode': 'a new mode for the entity', 'ftx.c_slot': 'a new slot (band+mode never worked together)', 'ftx.c_grid': 'a new grid square', 'ftx.c_county': 'a new US county', 'ftx.c_pota': 'a new POTA park', 'ftx.c_pfx': 'a new WPX prefix',
|
||||||
'sec.bands': 'Bands', 'sec.satellites': 'Satellites', 'sat.hint': 'The satellites this station works. They are offered as a dropdown on the satellite fields, in alphabetical order.', 'sat.listLabel': 'One satellite per line', 'sat.listHint': 'Written into SAT_NAME exactly as spelled here, so use the name LoTW and the awards expect — AO-91, not AO91. Leaving the list empty simply keeps the field a plain text box.', 'sec.modes': 'Modes & default RST', 'sec.cluster': 'DX Cluster',
|
'sec.bands': 'Bands', 'sec.satellites': 'Satellites', 'sat.hint': 'The satellites this station works. They are offered as a dropdown on the satellite fields, in alphabetical order.', 'sat.listLabel': 'One satellite per line', 'sat.listHint': 'Written into SAT_NAME exactly as spelled here, so use the name LoTW and the awards expect — AO-91, not AO91. Leaving the list empty simply keeps the field a plain text box.', 'sec.modes': 'Modes & default RST', 'sec.cluster': 'DX Cluster',
|
||||||
'sec.udp': 'Connections', 'sec.database': 'Database', 'sec.autostart': 'Autostart', 'sec.backup': 'Database backup', 'sec.uscounties': 'US Counties', 'nav.maintenance': 'Maintenance', 'sec.databases': 'Databases', 'db.hint': 'The reference data OpsLog keeps on disk. One line each, with what it holds and when it was last refreshed.', 'db.update': 'Update', 'db.never': 'never downloaded', 'db.cty': 'Country file (cty.dat)', 'db.ctyDetail': '{n} entities · file dated {d}', 'db.clublog': 'Club Log country exceptions', 'db.clublogDetail': '{n} exceptions · {d}', 'db.lotwUsers': 'LoTW users', 'db.lotwDetail': '{n} callsigns · {d}', 'db.scp': 'Super Check Partial (MASTER.SCP)', 'db.scpDetail': '{n} callsigns · {d}', 'db.uls': 'US counties (FCC ULS)', 'db.ulsDetail': '{n} callsigns · {d}', 'db.rda': 'Russian districts (RDA)', 'db.rdaDetail': '{n} callsigns, with their dated activity periods', 'db.rdaNote': 'built in', 'db.refLists': 'Award reference lists', 'db.refListsHint': 'Only the awards with an online source appear here; the others are shipped or edited by hand.', 'db.refDetail': '{n} references · {d}', 'db.refUpdated': '{code}: {n} references.', 'db.noRefLists': 'No award has an online reference list.', 'sec.rda': 'Russian districts (RDA)', 'rda.hint': 'The offline district database, and the one bulk operation it feeds.', 'rda.dbTitle': 'District database', 'rda.dbCount': '{n} Russian callsigns, each with the district it operates from and, where it moved, the dated periods it operated from each one. Built into OpsLog — nothing to download.', 'rda.backfillTitle': 'Fill the district on existing QSOs', 'rda.backfillIntro': 'Goes through every contact with a Russian entity and assigns its RDA reference, using the district the station was in ON THE DAY of the contact.', 'rda.useCurrent': 'Also use the current district for stations with no recorded history', 'rda.useCurrentHint': '(true for the great majority — the database records a history precisely for the callsigns that moved — but it is an assumption, not a dated fact)', 'rda.backfillRun': 'Fill districts', 'rda.backfillDone': '{s} Russian QSOs — {d} from a dated record, {c} from the current district, {u} unknown, {k} already had one.', 'rda.cmpTitle': 'Compare the two district sources', 'rda.cmpRunning': 'Comparing…', 'rda.cmpNoConflict': 'No disagreement — both sources say the same district everywhere.', 'rda.cmpNoRussian': 'No Russian contacts to compare.', 'rda.backfillRunning': 'Filling…', 'rda.cmpRun': 'Compare', 'rda.cmpDone': '{s} Russian QSOs — {a} identical, {d} differ, {l} only in the log, {b} only in the database', 'rda.cmpCall': 'Callsign', 'rda.cmpDate': 'Date', 'rda.cmpFromLog': 'Log (CNTY)', 'rda.cmpFromDb': 'RDA database', 'rda.cmpListHint': 'Showing {n} of {d} — click a callsign to open the contact.', 'rda.cmpOpen': 'Open this QSO', 'rda.cmpKind': 'Database', 'rda.cmpDated': 'dated', 'rda.cmpCurrent': 'current', 'rda.neverOverwrites': 'A reference you assigned by hand is never overwritten.',
|
'sec.udp': 'Connections', 'sec.database': 'Database', 'sec.autostart': 'Autostart', 'sec.backup': 'Database backup', 'sec.uscounties': 'US Counties', 'nav.maintenance': 'Maintenance', 'sec.databases': 'Databases', 'db.hint': 'The reference data OpsLog keeps on disk. One line each, with what it holds and when it was last refreshed.', 'db.update': 'Update', 'db.never': 'never downloaded', 'db.cty': 'Country file (cty.dat)', 'db.ctyDetail': '{n} entities · file dated {d}', 'db.clublog': 'Club Log country exceptions', 'db.clublogDetail': '{n} exceptions · {d}', 'db.lotwUsers': 'LoTW users', 'db.lotwDetail': '{n} callsigns · {d}', 'db.scp': 'Super Check Partial (MASTER.SCP)', 'db.scpDetail': '{n} callsigns · {d}', 'db.uls': 'US counties (FCC ULS)', 'db.ulsDetail': '{n} callsigns · {d}', 'db.rda': 'Russian districts (RDA)', 'db.rdaDetail': '{n} callsigns, with their dated activity periods', 'db.rdaNote': 'built in', 'db.refLists': 'Award reference lists', 'db.refListsHint': 'Only the awards with an online source appear here; the others are shipped or edited by hand.', 'db.refDetail': '{n} references · {d}', 'db.refUpdated': '{code}: {n} references.', 'db.noRefLists': 'No award has an online reference list.', 'sec.rda': 'Russian districts (RDA)', 'rda.hint': 'The offline district database, and the one bulk operation it feeds.', 'rda.dbTitle': 'District database', 'rda.dbCount': '{n} Russian callsigns, each with the district it operates from and, where it moved, the dated periods it operated from each one. Built into OpsLog — nothing to download.', 'rda.backfillTitle': 'Fill the district on existing QSOs', 'rda.backfillIntro': 'Goes through every contact with a Russian entity and assigns its RDA reference, using the district the station was in ON THE DAY of the contact.', 'rda.useCurrent': 'Also use the current district for stations with no recorded history', 'rda.useCurrentHint': '(true for the great majority — the database records a history precisely for the callsigns that moved — but it is an assumption, not a dated fact)', 'rda.backfillRun': 'Fill districts', 'rda.backfillDone': '{s} Russian QSOs — {d} from a dated record, {c} from the current district, {u} unknown, {k} already had one.', 'rda.cmpTitle': 'Compare the two district sources', 'rda.cmpRunning': 'Comparing…', 'rda.cmpNoConflict': 'No disagreement — both sources say the same district everywhere.', 'rda.cmpNoRussian': 'No Russian contacts to compare.', 'rda.backfillRunning': 'Filling…', 'rda.cmpRun': 'Compare', 'rda.cmpDone': '{s} Russian QSOs — {a} identical, {d} differ, {l} only in the log, {b} only in the database', 'rda.cmpCall': 'Callsign', 'rda.cmpDate': 'Date', 'rda.cmpFromLog': 'Log (CNTY)', 'rda.cmpFromDb': 'RDA database', 'rda.cmpListHint': 'Showing {n} of {d} — click a callsign to open the contact.', 'rda.cmpOpen': 'Open this QSO', 'rda.cmpKind': 'Database', 'rda.cmpDated': 'dated', 'rda.cmpCurrent': 'current', 'rda.neverOverwrites': 'A reference you assigned by hand is never overwritten.', 'rda.cmpKeep': "Keep", 'rda.cmpKeepLog': "Keep the log's district for this contact", 'rda.cmpKeepDb': "Keep the database's district for this contact", 'rda.cmpApply': "Apply {n} decisions", 'rda.cmpAllDb': "keep the database everywhere", 'rda.cmpAllLog': "keep the log everywhere", 'rda.cmpClear': "clear the decisions", 'rda.cmpApplyHint': 'The chosen district is written into the contact — into CNTY and as its award reference — so the disagreement is settled and the contact counts for that district. Settled rows leave the list.',
|
||||||
'sec.webpublish': 'Web publishing', 'wpub.hint': 'Publishes your log as a file for a website: a standalone HTML page or a CSV, written locally and optionally uploaded by FTP. It is refreshed when you log a QSO and, if you set an interval, on a timer.', 'wpub.enable': 'Publish the log to a file', 'wpub.fileSection': 'The file', 'wpub.format': 'Format', 'wpub.formatHtml': 'HTML page', 'wpub.formatCsv': 'CSV', 'wpub.folder': 'Output folder', 'wpub.browse': 'Browse…', 'wpub.fileName': 'File name', 'wpub.title': 'Page title', 'wpub.titlePh': 'blank = your callsign', 'wpub.count': 'Last N QSOs', 'wpub.every': 'Refresh every', 'wpub.everyHint': 'minutes — 0 = only when a QSO is logged', 'wpub.columns': 'Columns', 'wpub.columnsCount': '{n} of {total} chosen', 'wpub.columnsPick': 'Choose columns…', 'wpub.columnsSearch': 'Search a field…', 'wpub.removeColumn': 'Click to remove', 'wpub.columnsHint': 'Click to add or remove. The order shown here is the order in the file.', 'wpub.ftpEnable': 'Upload by FTP', 'wpub.ftpHost': 'Server / port', 'wpub.ftpUser': 'User', 'wpub.ftpPassword': 'Password', 'wpub.ftpFolder': 'Remote folder', 'wpub.ftpFileName': 'Remote file name', 'wpub.ftpTls': 'Use TLS (FTPS)', 'wpub.publishNow': 'Publish now', 'wpub.testFtp': 'Test connection', 'wpub.lastRun': 'Last run:', 'sec.adifmon': 'ADIF monitor',
|
'sec.webpublish': 'Web publishing', 'wpub.hint': 'Publishes your log as a file for a website: a standalone HTML page or a CSV, written locally and optionally uploaded by FTP. It is refreshed when you log a QSO and, if you set an interval, on a timer.', 'wpub.enable': 'Publish the log to a file', 'wpub.fileSection': 'The file', 'wpub.format': 'Format', 'wpub.formatHtml': 'HTML page', 'wpub.formatCsv': 'CSV', 'wpub.folder': 'Output folder', 'wpub.browse': 'Browse…', 'wpub.fileName': 'File name', 'wpub.title': 'Page title', 'wpub.titlePh': 'blank = your callsign', 'wpub.count': 'Last N QSOs', 'wpub.every': 'Refresh every', 'wpub.everyHint': 'minutes — 0 = only when a QSO is logged', 'wpub.columns': 'Columns', 'wpub.columnsCount': '{n} of {total} chosen', 'wpub.columnsPick': 'Choose columns…', 'wpub.columnsSearch': 'Search a field…', 'wpub.removeColumn': 'Click to remove', 'wpub.columnsHint': 'Click to add or remove. The order shown here is the order in the file.', 'wpub.ftpEnable': 'Upload by FTP', 'wpub.ftpHost': 'Server / port', 'wpub.ftpUser': 'User', 'wpub.ftpPassword': 'Password', 'wpub.ftpFolder': 'Remote folder', 'wpub.ftpFileName': 'Remote file name', 'wpub.ftpTls': 'Use TLS (FTPS)', 'wpub.publishNow': 'Publish now', 'wpub.testFtp': 'Test connection', 'wpub.lastRun': 'Last run:', 'sec.adifmon': 'ADIF monitor',
|
||||||
'sec.foldersync': 'Sync across PCs', 'sync.hint': 'Point every OpsLog at the SAME folder — one your PCs already synchronise (Seafile, OneDrive, Dropbox, a NAS share). Each machine writes what it logs there and reads the others; the databases themselves are never shared.', 'sync.enable': 'Keep my contacts in step across my PCs', 'sync.machine': 'This PC', 'sync.folder': 'Folder', 'sync.choose': 'Choose…', 'sync.state': 'State', 'sync.thisPc': 'This PC', 'sync.lastSync': 'Last check', 'sync.sent': 'Sent', 'sync.received': 'Received', 'sync.never': 'never', 'sync.noPeers': 'No other PC has written to this folder yet.', 'sync.behind': 'new contacts waiting', 'sync.now': 'Synchronise now', 'sync.applied': '{n} change(s) taken from the folder.', 'sync.saved': 'Saved.',
|
'sec.foldersync': 'Sync across PCs', 'sync.hint': 'Point every OpsLog at the SAME folder — one your PCs already synchronise (Seafile, OneDrive, Dropbox, a NAS share). Each machine writes what it logs there and reads the others; the databases themselves are never shared.', 'sync.enable': 'Keep my contacts in step across my PCs', 'sync.machine': 'This PC', 'sync.folder': 'Folder', 'sync.choose': 'Choose…', 'sync.state': 'State', 'sync.thisPc': 'This PC', 'sync.lastSync': 'Last check', 'sync.sent': 'Sent', 'sync.received': 'Received', 'sync.never': 'never', 'sync.noPeers': 'No other PC has written to this folder yet.', 'sync.behind': 'new contacts waiting', 'sync.now': 'Synchronise now', 'sync.applied': '{n} change(s) taken from the folder.', 'sync.saved': 'Saved.',
|
||||||
'adifmon.hint': 'Watch external ADIF files and import new QSOs automatically — e.g. fldigi logging RTTY, or N1MM/VarAC. Imported QSOs are enriched, de-duplicated and uploaded to your external services just like a QSO logged here.',
|
'adifmon.hint': 'Watch external ADIF files and import new QSOs automatically — e.g. fldigi logging RTTY, or N1MM/VarAC. Imported QSOs are enriched, de-duplicated and uploaded to your external services just like a QSO logged here.',
|
||||||
@@ -636,7 +636,7 @@ const fr: Dict = {
|
|||||||
'sec.email': 'E-mail (SMTP)', 'sec.lookup': "Recherche d'indicatif",
|
'sec.email': 'E-mail (SMTP)', 'sec.lookup': "Recherche d'indicatif",
|
||||||
'sec.ftx': 'Décodages FTx', 'ftx.hint': 'Ce qu’OpsLog fait de lui-même avec le flux de décodages numériques.', 'ftx.enable': 'Appel automatique', 'ftx.enableHint': 'répondre à un décodage sans cliquer', 'ftx.callWhen': 'Appeler une station qui est :', 'ftx.watch': 'Liste de surveillance', 'ftx.watchHint': 'Un indicatif par ligne, jokers acceptés (4S7*, */P). Une station surveillée est appelée avant les critères ci-dessus.', 'ftx.watchOnlyIf': 'mais seulement si elle est aussi :', 'ftx.cooldown': 'Ignorer un indicatif pendant', 'ftx.warn': 'Ceci met ton émetteur en marche sans te demander. Uniquement sur un CQ, jamais pendant que tu émets déjà, une station à la fois, et chaque appel est écrit dans le journal avec sa raison. Stop l’interrompt.', 'ftx.c_dxcc': 'une nouvelle entité DXCC', 'ftx.c_bandmode': 'une nouvelle bande ET un nouveau mode pour l’entité', 'ftx.c_band': 'une nouvelle bande pour l’entité', 'ftx.c_mode': 'un nouveau mode pour l’entité', 'ftx.c_slot': 'un nouveau slot (bande+mode jamais faits ensemble)', 'ftx.c_grid': 'un nouveau carré locator', 'ftx.c_county': 'un nouveau comté US', 'ftx.c_pota': 'un nouveau parc POTA', 'ftx.c_pfx': 'un nouveau préfixe WPX',
|
'sec.ftx': 'Décodages FTx', 'ftx.hint': 'Ce qu’OpsLog fait de lui-même avec le flux de décodages numériques.', 'ftx.enable': 'Appel automatique', 'ftx.enableHint': 'répondre à un décodage sans cliquer', 'ftx.callWhen': 'Appeler une station qui est :', 'ftx.watch': 'Liste de surveillance', 'ftx.watchHint': 'Un indicatif par ligne, jokers acceptés (4S7*, */P). Une station surveillée est appelée avant les critères ci-dessus.', 'ftx.watchOnlyIf': 'mais seulement si elle est aussi :', 'ftx.cooldown': 'Ignorer un indicatif pendant', 'ftx.warn': 'Ceci met ton émetteur en marche sans te demander. Uniquement sur un CQ, jamais pendant que tu émets déjà, une station à la fois, et chaque appel est écrit dans le journal avec sa raison. Stop l’interrompt.', 'ftx.c_dxcc': 'une nouvelle entité DXCC', 'ftx.c_bandmode': 'une nouvelle bande ET un nouveau mode pour l’entité', 'ftx.c_band': 'une nouvelle bande pour l’entité', 'ftx.c_mode': 'un nouveau mode pour l’entité', 'ftx.c_slot': 'un nouveau slot (bande+mode jamais faits ensemble)', 'ftx.c_grid': 'un nouveau carré locator', 'ftx.c_county': 'un nouveau comté US', 'ftx.c_pota': 'un nouveau parc POTA', 'ftx.c_pfx': 'un nouveau préfixe WPX',
|
||||||
'sec.bands': 'Bandes', 'sec.satellites': 'Satellites', 'sat.hint': "Les satellites que cette station travaille. Ils sont proposés en liste déroulante sur les champs satellite, par ordre alphabétique.", 'sat.listLabel': 'Un satellite par ligne', 'sat.listHint': "Inscrit dans SAT_NAME exactement tel qu'écrit ici : utilise le nom attendu par LoTW et les diplômes — AO-91, pas AO91. Une liste vide laisse simplement le champ en saisie libre.", 'sec.modes': 'Modes & RST par défaut', 'sec.cluster': 'DX Cluster',
|
'sec.bands': 'Bandes', 'sec.satellites': 'Satellites', 'sat.hint': "Les satellites que cette station travaille. Ils sont proposés en liste déroulante sur les champs satellite, par ordre alphabétique.", 'sat.listLabel': 'Un satellite par ligne', 'sat.listHint': "Inscrit dans SAT_NAME exactement tel qu'écrit ici : utilise le nom attendu par LoTW et les diplômes — AO-91, pas AO91. Une liste vide laisse simplement le champ en saisie libre.", 'sec.modes': 'Modes & RST par défaut', 'sec.cluster': 'DX Cluster',
|
||||||
'sec.udp': 'Connexions', 'sec.database': 'Base de données', 'sec.autostart': 'Démarrage auto', 'sec.backup': 'Sauvegarde base', 'sec.uscounties': 'Comtés US', 'nav.maintenance': 'Maintenance', 'sec.databases': 'Bases de données', 'db.hint': 'Les données de référence qu’OpsLog garde sur disque. Une ligne chacune, avec ce qu’elle contient et sa dernière actualisation.', 'db.update': 'Mettre à jour', 'db.never': 'jamais téléchargée', 'db.cty': 'Fichier pays (cty.dat)', 'db.ctyDetail': '{n} entités · fichier daté du {d}', 'db.clublog': 'Exceptions pays Club Log', 'db.clublogDetail': '{n} exceptions · {d}', 'db.lotwUsers': 'Utilisateurs LoTW', 'db.lotwDetail': '{n} indicatifs · {d}', 'db.scp': 'Super Check Partial (MASTER.SCP)', 'db.scpDetail': '{n} indicatifs · {d}', 'db.uls': 'Comtés US (FCC ULS)', 'db.ulsDetail': '{n} indicatifs · {d}', 'db.rda': 'Districts russes (RDA)', 'db.rdaDetail': '{n} indicatifs, avec leurs périodes d’activité datées', 'db.rdaNote': 'intégrée', 'db.refLists': 'Listes de références des diplômes', 'db.refListsHint': 'Seuls les diplômes ayant une source en ligne apparaissent ici ; les autres sont livrés ou édités à la main.', 'db.refDetail': '{n} références · {d}', 'db.refUpdated': '{code} : {n} références.', 'db.noRefLists': 'Aucun diplôme n’a de liste de références en ligne.', 'sec.rda': 'Districts russes (RDA)', 'rda.hint': 'La base de districts hors ligne, et l’unique opération de masse qu’elle alimente.', 'rda.dbTitle': 'Base des districts', 'rda.dbCount': '{n} indicatifs russes, chacun avec le district d’où il émet et, pour ceux qui ont déménagé, les périodes datées passées dans chacun. Intégrée à OpsLog — rien à télécharger.', 'rda.backfillTitle': 'Renseigner le district sur les QSO existants', 'rda.backfillIntro': 'Parcourt tous les contacts avec une entité russe et attribue leur référence RDA, en utilisant le district où se trouvait la station LE JOUR du contact.', 'rda.useCurrent': 'Utiliser aussi le district actuel pour les stations sans historique connu', 'rda.useCurrentHint': '(vrai pour la grande majorité — la base enregistre un historique justement pour les indicatifs qui ont bougé — mais c’est une supposition, pas un fait daté)', 'rda.backfillRun': 'Renseigner les districts', 'rda.backfillDone': '{s} QSO russes — {d} depuis une période datée, {c} depuis le district actuel, {u} inconnus, {k} en avaient déjà un.', 'rda.cmpTitle': 'Comparer les deux sources de district', 'rda.cmpRunning': 'Comparaison…', 'rda.cmpNoConflict': 'Aucune divergence — les deux sources donnent partout le même district.', 'rda.cmpNoRussian': 'Aucun contact russe à comparer.', 'rda.backfillRunning': 'Remplissage…', 'rda.cmpRun': 'Comparer', 'rda.cmpDone': '{s} QSO russes — {a} identiques, {d} divergents, {l} seulement dans le log, {b} seulement dans la base', 'rda.cmpCall': 'Indicatif', 'rda.cmpDate': 'Date', 'rda.cmpFromLog': 'Log (CNTY)', 'rda.cmpFromDb': 'Base RDA', 'rda.cmpListHint': '{n} affichés sur {d} — cliquer un indicatif ouvre le contact.', 'rda.cmpOpen': 'Ouvrir ce QSO', 'rda.cmpKind': 'Base', 'rda.cmpDated': 'daté', 'rda.cmpCurrent': 'courant', 'rda.neverOverwrites': 'Une référence attribuée à la main n’est jamais écrasée.',
|
'sec.udp': 'Connexions', 'sec.database': 'Base de données', 'sec.autostart': 'Démarrage auto', 'sec.backup': 'Sauvegarde base', 'sec.uscounties': 'Comtés US', 'nav.maintenance': 'Maintenance', 'sec.databases': 'Bases de données', 'db.hint': 'Les données de référence qu’OpsLog garde sur disque. Une ligne chacune, avec ce qu’elle contient et sa dernière actualisation.', 'db.update': 'Mettre à jour', 'db.never': 'jamais téléchargée', 'db.cty': 'Fichier pays (cty.dat)', 'db.ctyDetail': '{n} entités · fichier daté du {d}', 'db.clublog': 'Exceptions pays Club Log', 'db.clublogDetail': '{n} exceptions · {d}', 'db.lotwUsers': 'Utilisateurs LoTW', 'db.lotwDetail': '{n} indicatifs · {d}', 'db.scp': 'Super Check Partial (MASTER.SCP)', 'db.scpDetail': '{n} indicatifs · {d}', 'db.uls': 'Comtés US (FCC ULS)', 'db.ulsDetail': '{n} indicatifs · {d}', 'db.rda': 'Districts russes (RDA)', 'db.rdaDetail': '{n} indicatifs, avec leurs périodes d’activité datées', 'db.rdaNote': 'intégrée', 'db.refLists': 'Listes de références des diplômes', 'db.refListsHint': 'Seuls les diplômes ayant une source en ligne apparaissent ici ; les autres sont livrés ou édités à la main.', 'db.refDetail': '{n} références · {d}', 'db.refUpdated': '{code} : {n} références.', 'db.noRefLists': 'Aucun diplôme n’a de liste de références en ligne.', 'sec.rda': 'Districts russes (RDA)', 'rda.hint': 'La base de districts hors ligne, et l’unique opération de masse qu’elle alimente.', 'rda.dbTitle': 'Base des districts', 'rda.dbCount': '{n} indicatifs russes, chacun avec le district d’où il émet et, pour ceux qui ont déménagé, les périodes datées passées dans chacun. Intégrée à OpsLog — rien à télécharger.', 'rda.backfillTitle': 'Renseigner le district sur les QSO existants', 'rda.backfillIntro': 'Parcourt tous les contacts avec une entité russe et attribue leur référence RDA, en utilisant le district où se trouvait la station LE JOUR du contact.', 'rda.useCurrent': 'Utiliser aussi le district actuel pour les stations sans historique connu', 'rda.useCurrentHint': '(vrai pour la grande majorité — la base enregistre un historique justement pour les indicatifs qui ont bougé — mais c’est une supposition, pas un fait daté)', 'rda.backfillRun': 'Renseigner les districts', 'rda.backfillDone': '{s} QSO russes — {d} depuis une période datée, {c} depuis le district actuel, {u} inconnus, {k} en avaient déjà un.', 'rda.cmpTitle': 'Comparer les deux sources de district', 'rda.cmpRunning': 'Comparaison…', 'rda.cmpNoConflict': 'Aucune divergence — les deux sources donnent partout le même district.', 'rda.cmpNoRussian': 'Aucun contact russe à comparer.', 'rda.backfillRunning': 'Remplissage…', 'rda.cmpRun': 'Comparer', 'rda.cmpDone': '{s} QSO russes — {a} identiques, {d} divergents, {l} seulement dans le log, {b} seulement dans la base', 'rda.cmpCall': 'Indicatif', 'rda.cmpDate': 'Date', 'rda.cmpFromLog': 'Log (CNTY)', 'rda.cmpFromDb': 'Base RDA', 'rda.cmpListHint': '{n} affichés sur {d} — cliquer un indicatif ouvre le contact.', 'rda.cmpOpen': 'Ouvrir ce QSO', 'rda.cmpKind': 'Base', 'rda.cmpDated': 'daté', 'rda.cmpCurrent': 'courant', 'rda.neverOverwrites': 'Une référence attribuée à la main n’est jamais écrasée.', 'rda.cmpKeep': "Garder", 'rda.cmpKeepLog': "Garder le district du log pour ce contact", 'rda.cmpKeepDb': "Garder le district de la base pour ce contact", 'rda.cmpApply': "Appliquer {n} décisions", 'rda.cmpAllDb': "garder la base partout", 'rda.cmpAllLog': "garder le log partout", 'rda.cmpClear': "effacer les décisions", 'rda.cmpApplyHint': "Le district choisi est écrit dans le contact — dans CNTY et comme référence de diplôme — donc la divergence est réglée et le contact compte pour ce district. Les lignes réglées quittent la liste.",
|
||||||
'sec.webpublish': 'Publication web', 'wpub.hint': "Publie ton journal dans un fichier destiné à un site web : une page HTML autonome ou un CSV, écrit en local et envoyé par FTP si tu le souhaites. Il est rafraîchi à chaque QSO enregistré et, si tu règles un intervalle, périodiquement.", 'wpub.enable': 'Publier le journal dans un fichier', 'wpub.fileSection': 'Le fichier', 'wpub.format': 'Format', 'wpub.formatHtml': 'Page HTML', 'wpub.formatCsv': 'CSV', 'wpub.folder': 'Dossier de sortie', 'wpub.browse': 'Parcourir…', 'wpub.fileName': 'Nom du fichier', 'wpub.title': 'Titre de la page', 'wpub.titlePh': 'vide = ton indicatif', 'wpub.count': 'N derniers QSO', 'wpub.every': 'Rafraîchir toutes les', 'wpub.everyHint': 'minutes — 0 = seulement à chaque QSO', 'wpub.columns': 'Colonnes', 'wpub.columnsCount': '{n} sur {total} choisis', 'wpub.columnsPick': 'Choisir les colonnes…', 'wpub.columnsSearch': 'Chercher un champ…', 'wpub.removeColumn': 'Cliquer pour retirer', 'wpub.columnsHint': 'Clique pour ajouter ou retirer. L ordre affiché ici est celui du fichier.', 'wpub.ftpEnable': 'Envoyer par FTP', 'wpub.ftpHost': 'Serveur / port', 'wpub.ftpUser': 'Utilisateur', 'wpub.ftpPassword': 'Mot de passe', 'wpub.ftpFolder': 'Dossier distant', 'wpub.ftpFileName': 'Nom du fichier distant', 'wpub.ftpTls': 'Utiliser TLS (FTPS)', 'wpub.publishNow': 'Publier maintenant', 'wpub.testFtp': 'Tester la connexion', 'wpub.lastRun': 'Dernière exécution :', 'sec.adifmon': 'Moniteur ADIF',
|
'sec.webpublish': 'Publication web', 'wpub.hint': "Publie ton journal dans un fichier destiné à un site web : une page HTML autonome ou un CSV, écrit en local et envoyé par FTP si tu le souhaites. Il est rafraîchi à chaque QSO enregistré et, si tu règles un intervalle, périodiquement.", 'wpub.enable': 'Publier le journal dans un fichier', 'wpub.fileSection': 'Le fichier', 'wpub.format': 'Format', 'wpub.formatHtml': 'Page HTML', 'wpub.formatCsv': 'CSV', 'wpub.folder': 'Dossier de sortie', 'wpub.browse': 'Parcourir…', 'wpub.fileName': 'Nom du fichier', 'wpub.title': 'Titre de la page', 'wpub.titlePh': 'vide = ton indicatif', 'wpub.count': 'N derniers QSO', 'wpub.every': 'Rafraîchir toutes les', 'wpub.everyHint': 'minutes — 0 = seulement à chaque QSO', 'wpub.columns': 'Colonnes', 'wpub.columnsCount': '{n} sur {total} choisis', 'wpub.columnsPick': 'Choisir les colonnes…', 'wpub.columnsSearch': 'Chercher un champ…', 'wpub.removeColumn': 'Cliquer pour retirer', 'wpub.columnsHint': 'Clique pour ajouter ou retirer. L ordre affiché ici est celui du fichier.', 'wpub.ftpEnable': 'Envoyer par FTP', 'wpub.ftpHost': 'Serveur / port', 'wpub.ftpUser': 'Utilisateur', 'wpub.ftpPassword': 'Mot de passe', 'wpub.ftpFolder': 'Dossier distant', 'wpub.ftpFileName': 'Nom du fichier distant', 'wpub.ftpTls': 'Utiliser TLS (FTPS)', 'wpub.publishNow': 'Publier maintenant', 'wpub.testFtp': 'Tester la connexion', 'wpub.lastRun': 'Dernière exécution :', 'sec.adifmon': 'Moniteur ADIF',
|
||||||
'sec.foldersync': 'Synchro entre PC', 'sync.hint': 'Fais pointer chaque OpsLog vers le MÊME dossier — un dossier que tes PC synchronisent déjà (Seafile, OneDrive, Dropbox, un partage NAS). Chaque machine y écrit ce qu’elle enregistre et lit celui des autres ; les bases de données, elles, ne sont jamais partagées.', 'sync.enable': 'Garder mes contacts à jour sur tous mes PC', 'sync.machine': 'Ce PC', 'sync.folder': 'Dossier', 'sync.choose': 'Choisir…', 'sync.state': 'État', 'sync.thisPc': 'Ce PC', 'sync.lastSync': 'Dernière vérification', 'sync.sent': 'Envoyés', 'sync.received': 'Reçus', 'sync.never': 'jamais', 'sync.noPeers': 'Aucun autre PC n’a encore écrit dans ce dossier.', 'sync.behind': 'nouveaux contacts en attente', 'sync.now': 'Synchroniser maintenant', 'sync.applied': '{n} changement(s) repris du dossier.', 'sync.saved': 'Enregistré.',
|
'sec.foldersync': 'Synchro entre PC', 'sync.hint': 'Fais pointer chaque OpsLog vers le MÊME dossier — un dossier que tes PC synchronisent déjà (Seafile, OneDrive, Dropbox, un partage NAS). Chaque machine y écrit ce qu’elle enregistre et lit celui des autres ; les bases de données, elles, ne sont jamais partagées.', 'sync.enable': 'Garder mes contacts à jour sur tous mes PC', 'sync.machine': 'Ce PC', 'sync.folder': 'Dossier', 'sync.choose': 'Choisir…', 'sync.state': 'État', 'sync.thisPc': 'Ce PC', 'sync.lastSync': 'Dernière vérification', 'sync.sent': 'Envoyés', 'sync.received': 'Reçus', 'sync.never': 'jamais', 'sync.noPeers': 'Aucun autre PC n’a encore écrit dans ce dossier.', 'sync.behind': 'nouveaux contacts en attente', 'sync.now': 'Synchroniser maintenant', 'sync.applied': '{n} changement(s) repris du dossier.', 'sync.saved': 'Enregistré.',
|
||||||
'adifmon.hint': "Surveille des fichiers ADIF externes et importe les nouveaux QSO automatiquement — ex. fldigi en RTTY, ou N1MM/VarAC. Les QSO importés sont enrichis, dédoublonnés et envoyés à tes services externes comme un QSO loggé ici.",
|
'adifmon.hint': "Surveille des fichiers ADIF externes et importe les nouveaux QSO automatiquement — ex. fldigi en RTTY, ou N1MM/VarAC. Les QSO importés sont enrichis, dédoublonnés et envoyés à tes services externes comme un QSO loggé ici.",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Single source of truth for the app version shown in the UI (header + About).
|
// Single source of truth for the app version shown in the UI (header + About).
|
||||||
// Bump this on a release (the release script updates it alongside telemetry.go).
|
// Bump this on a release (the release script updates it alongside telemetry.go).
|
||||||
export const APP_VERSION = '0.26.16';
|
export const APP_VERSION = '0.26.17';
|
||||||
|
|
||||||
// Author / credits, shown in Help -> About.
|
// Author / credits, shown in Help -> About.
|
||||||
export const APP_AUTHOR = 'F4BPO';
|
export const APP_AUTHOR = 'F4BPO';
|
||||||
|
|||||||
Vendored
+2
@@ -65,6 +65,8 @@ export function ApplyAwardPreset(arg1:string,arg2:string):Promise<number>;
|
|||||||
|
|
||||||
export function ApplyAwardUpdate(arg1:string):Promise<void>;
|
export function ApplyAwardUpdate(arg1:string):Promise<void>;
|
||||||
|
|
||||||
|
export function ApplyRDAChoices(arg1:Array<main.RDAChoice>):Promise<main.RDAApplyResult>;
|
||||||
|
|
||||||
export function AssignAwardRefToQSOs(arg1:string,arg2:string,arg3:Array<number>):Promise<number>;
|
export function AssignAwardRefToQSOs(arg1:string,arg2:string,arg3:Array<number>):Promise<number>;
|
||||||
|
|
||||||
export function AudioApplyLevels(arg1:number,arg2:number):Promise<void>;
|
export function AudioApplyLevels(arg1:number,arg2:number):Promise<void>;
|
||||||
|
|||||||
@@ -70,6 +70,10 @@ export function ApplyAwardUpdate(arg1) {
|
|||||||
return window['go']['main']['App']['ApplyAwardUpdate'](arg1);
|
return window['go']['main']['App']['ApplyAwardUpdate'](arg1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ApplyRDAChoices(arg1) {
|
||||||
|
return window['go']['main']['App']['ApplyRDAChoices'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
export function AssignAwardRefToQSOs(arg1, arg2, arg3) {
|
export function AssignAwardRefToQSOs(arg1, arg2, arg3) {
|
||||||
return window['go']['main']['App']['AssignAwardRefToQSOs'](arg1, arg2, arg3);
|
return window['go']['main']['App']['AssignAwardRefToQSOs'](arg1, arg2, arg3);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3349,6 +3349,36 @@ export namespace main {
|
|||||||
this.team_last60 = source["team_last60"];
|
this.team_last60 = source["team_last60"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export class RDAApplyResult {
|
||||||
|
applied: number;
|
||||||
|
failed: number;
|
||||||
|
message: string;
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new RDAApplyResult(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.applied = source["applied"];
|
||||||
|
this.failed = source["failed"];
|
||||||
|
this.message = source["message"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class RDAChoice {
|
||||||
|
qso_id: number;
|
||||||
|
district: string;
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new RDAChoice(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.qso_id = source["qso_id"];
|
||||||
|
this.district = source["district"];
|
||||||
|
}
|
||||||
|
}
|
||||||
export class RDAConflict {
|
export class RDAConflict {
|
||||||
qso_id: number;
|
qso_id: number;
|
||||||
callsign: string;
|
callsign: string;
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package qso
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// The colour of a matrix cell is a claim about what the operator still needs,
|
||||||
|
// and it was wrong for several releases: a callsign worked and not confirmed
|
||||||
|
// outranked an entity CONFIRMED on the same band and mode, so a slot that was
|
||||||
|
// finished showed as unfinished.
|
||||||
|
//
|
||||||
|
// Reported by VK4DX with the case that names itself: YB confirmed on 20m
|
||||||
|
// digital, shown blue, because one unconfirmed YB station had also been worked
|
||||||
|
// there.
|
||||||
|
func TestBandStatusConfirmedOutranksWorked(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
callWorked, callConfirmed, entityConfirm bool
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"entity worked only", false, false, false, "dxcc_w"},
|
||||||
|
{"this call worked, nothing confirmed", true, false, false, "call_w"},
|
||||||
|
// The regression, in one line.
|
||||||
|
{"entity confirmed, this call worked but not confirmed", true, false, true, "dxcc_c"},
|
||||||
|
{"entity confirmed, this call not worked here", false, false, true, "dxcc_c"},
|
||||||
|
{"this call confirmed", true, true, true, "call_c"},
|
||||||
|
// A confirmed contact with this call implies it was worked, but the flags
|
||||||
|
// arrive from separate SQL aggregates and nothing guarantees the pair.
|
||||||
|
{"call confirmed without the worked flag", false, true, true, "call_c"},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
got := bandStatusNames[bandStatusCode(c.callWorked, c.callConfirmed, c.entityConfirm)]
|
||||||
|
if got != c.want {
|
||||||
|
t.Errorf("bandStatusCode(worked=%v, callConf=%v, entityConf=%v) = %s, want %s",
|
||||||
|
c.callWorked, c.callConfirmed, c.entityConfirm, got, c.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The ladder itself, stated once: every confirmed code must beat every worked
|
||||||
|
// code. A future edit that reorders the constants fails here rather than in a
|
||||||
|
// screenshot from an operator.
|
||||||
|
func TestBandStatusLadderPutsConfirmedAbove(t *testing.T) {
|
||||||
|
for _, worked := range []int{stDxccW, stCallW} {
|
||||||
|
for _, confirmed := range []int{stDxccC, stCallC} {
|
||||||
|
if confirmed <= worked {
|
||||||
|
t.Errorf("%s (%d) does not outrank %s (%d)",
|
||||||
|
bandStatusNames[confirmed], confirmed, bandStatusNames[worked], worked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if stCallC <= stDxccC {
|
||||||
|
t.Error("call_c must outrank dxcc_c: the callsign is the more specific claim")
|
||||||
|
}
|
||||||
|
if stCallW <= stDxccW {
|
||||||
|
t.Error("call_w must outrank dxcc_w for the same reason")
|
||||||
|
}
|
||||||
|
}
|
||||||
+58
-19
@@ -1895,7 +1895,8 @@ type WorkedBefore struct {
|
|||||||
|
|
||||||
// Status grid driving the band×class matrix in the UI. One entry per
|
// Status grid driving the band×class matrix in the UI. One entry per
|
||||||
// (band, class) where ANY QSO exists in this DXCC. Only the highest
|
// (band, class) where ANY QSO exists in this DXCC. Only the highest
|
||||||
// status for that cell is kept (call_c > call_w > dxcc_c > dxcc_w).
|
// status for that cell is kept (call_c > dxcc_c > call_w > dxcc_w —
|
||||||
|
// confirmed outranks worked).
|
||||||
BandStatus []BandStatus `json:"band_status"`
|
BandStatus []BandStatus `json:"band_status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1906,6 +1907,48 @@ type BandStatus struct {
|
|||||||
Status string `json:"status"` // "call_c" | "call_w" | "dxcc_c" | "dxcc_w"
|
Status string `json:"status"` // "call_c" | "call_w" | "dxcc_c" | "dxcc_w"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Band-status codes, lowest first. The ORDER is the rule: a cell shows the
|
||||||
|
// highest that applies.
|
||||||
|
const (
|
||||||
|
stDxccW = iota // the entity was worked on this slot
|
||||||
|
stCallW // …and this callsign was one of them
|
||||||
|
stDxccC // the entity is CONFIRMED here
|
||||||
|
stCallC // …and by this callsign
|
||||||
|
)
|
||||||
|
|
||||||
|
// bandStatusNames maps those codes to what the UI colours by.
|
||||||
|
var bandStatusNames = [...]string{"dxcc_w", "call_w", "dxcc_c", "call_c"}
|
||||||
|
|
||||||
|
// bandStatusCode picks the status of one cell of the band × mode matrix.
|
||||||
|
//
|
||||||
|
// CONFIRMED OUTRANKS WORKED, and that is the whole of it. The ladder used to
|
||||||
|
// run call_c > call_w > dxcc_c > dxcc_w, so a callsign worked and not confirmed
|
||||||
|
// beat an entity confirmed on the same slot: an operator with YB confirmed on
|
||||||
|
// 20m digital saw that cell as "worked, not confirmed" because he had also
|
||||||
|
// worked one unconfirmed YB station there. The grid answers "what do I still
|
||||||
|
// need", and a confirmed entity needs nothing, whoever was worked afterwards.
|
||||||
|
//
|
||||||
|
// Taken as a MAXIMUM rather than as a run of assignments, which is how the
|
||||||
|
// later test came to overwrite the earlier one in the first place.
|
||||||
|
func bandStatusCode(callWorked, callConfirmed, entityConfirmed bool) int {
|
||||||
|
code := stDxccW // there is a row at all ⇒ the entity was worked here
|
||||||
|
raise := func(c int) {
|
||||||
|
if c > code {
|
||||||
|
code = c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if callWorked {
|
||||||
|
raise(stCallW)
|
||||||
|
}
|
||||||
|
if entityConfirmed {
|
||||||
|
raise(stDxccC)
|
||||||
|
}
|
||||||
|
if callConfirmed {
|
||||||
|
raise(stCallC)
|
||||||
|
}
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
|
||||||
// modeClass collapses ADIF modes into the three buckets DXers care about.
|
// modeClass collapses ADIF modes into the three buckets DXers care about.
|
||||||
// Anything not voice and not CW is treated as digital.
|
// Anything not voice and not CW is treated as digital.
|
||||||
func modeClass(mode string) string {
|
func modeClass(mode string) string {
|
||||||
@@ -2167,7 +2210,18 @@ func (r *Repo) WorkedBefore(ctx context.Context, callsign string, dxccHint int,
|
|||||||
// ---- Per-(band, class) status grid ----
|
// ---- Per-(band, class) status grid ----
|
||||||
// One pass over every distinct (band, mode) in the DXCC, aggregating
|
// One pass over every distinct (band, mode) in the DXCC, aggregating
|
||||||
// "did this call work it?" and "was anything confirmed?" via MAX.
|
// "did this call work it?" and "was anything confirmed?" via MAX.
|
||||||
// Status precedence: call_c > call_w > dxcc_c > dxcc_w.
|
// Status precedence: CONFIRMED OUTRANKS WORKED — call_c > dxcc_c > call_w >
|
||||||
|
// dxcc_w.
|
||||||
|
//
|
||||||
|
// It used to run call_c > call_w > dxcc_c > dxcc_w, which made a call worked
|
||||||
|
// and not confirmed outrank an entity confirmed on the same slot. Reported
|
||||||
|
// from a real log: YB confirmed on 20m digital showed BLUE, because that
|
||||||
|
// operator had also worked one unconfirmed YB station there. The cell said
|
||||||
|
// "not confirmed" about a slot that is confirmed.
|
||||||
|
//
|
||||||
|
// The grid answers "what do I still need on this band and mode", and for
|
||||||
|
// that question confirmation is the axis that matters: a confirmed entity
|
||||||
|
// needs nothing, whoever else was worked afterwards.
|
||||||
// Filter NULL/empty band+mode rows — they'd create a NULL group key
|
// Filter NULL/empty band+mode rows — they'd create a NULL group key
|
||||||
// that Scan into *string can't handle and would error out the whole
|
// that Scan into *string can't handle and would error out the whole
|
||||||
// WorkedBefore call, blanking the matrix in the UI.
|
// WorkedBefore call, blanking the matrix in the UI.
|
||||||
@@ -2188,12 +2242,6 @@ func (r *Repo) WorkedBefore(ctx context.Context, callsign string, dxccHint int,
|
|||||||
return wb, fmt.Errorf("band status: %w", err)
|
return wb, fmt.Errorf("band status: %w", err)
|
||||||
}
|
}
|
||||||
type cellKey struct{ band, class string }
|
type cellKey struct{ band, class string }
|
||||||
const (
|
|
||||||
stDxccW = 0
|
|
||||||
stDxccC = 1
|
|
||||||
stCallW = 2
|
|
||||||
stCallC = 3
|
|
||||||
)
|
|
||||||
best := map[cellKey]int{}
|
best := map[cellKey]int{}
|
||||||
for statusRows.Next() {
|
for statusRows.Next() {
|
||||||
var band, mode string
|
var band, mode string
|
||||||
@@ -2202,23 +2250,14 @@ func (r *Repo) WorkedBefore(ctx context.Context, callsign string, dxccHint int,
|
|||||||
statusRows.Close()
|
statusRows.Close()
|
||||||
return wb, fmt.Errorf("scan band status: %w", err)
|
return wb, fmt.Errorf("scan band status: %w", err)
|
||||||
}
|
}
|
||||||
code := stDxccW // row exists ⇒ entity worked at minimum
|
code := bandStatusCode(callW == 1, callC == 1, dxccConfirmed == 1)
|
||||||
if dxccConfirmed == 1 {
|
|
||||||
code = stDxccC
|
|
||||||
}
|
|
||||||
if callW == 1 {
|
|
||||||
code = stCallW
|
|
||||||
}
|
|
||||||
if callC == 1 {
|
|
||||||
code = stCallC
|
|
||||||
}
|
|
||||||
k := cellKey{band: band, class: modeClass(mode)}
|
k := cellKey{band: band, class: modeClass(mode)}
|
||||||
if cur, ok := best[k]; !ok || code > cur {
|
if cur, ok := best[k]; !ok || code > cur {
|
||||||
best[k] = code
|
best[k] = code
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
statusRows.Close()
|
statusRows.Close()
|
||||||
codeStr := [...]string{"dxcc_w", "dxcc_c", "call_w", "call_c"}
|
codeStr := bandStatusNames
|
||||||
for k, code := range best {
|
for k, code := range best {
|
||||||
wb.BandStatus = append(wb.BandStatus, BandStatus{
|
wb.BandStatus = append(wb.BandStatus, BandStatus{
|
||||||
Band: k.band, Class: k.class, Status: codeStr[code],
|
Band: k.band, Class: k.class, Status: codeStr[code],
|
||||||
|
|||||||
@@ -44,7 +44,11 @@ import (
|
|||||||
// Rig is what the server needs from OpsLog's CAT manager. An interface, so this
|
// Rig is what the server needs from OpsLog's CAT manager. An interface, so this
|
||||||
// package stays testable without a radio and without importing internal/cat.
|
// package stays testable without a radio and without importing internal/cat.
|
||||||
type Rig interface {
|
type Rig interface {
|
||||||
Freq() int64 // current TX frequency in Hz, 0 if unknown
|
// Freq is the frequency of the CURRENT VFO — where the operator is
|
||||||
|
// listening. Hamlib's "f" means the VFO in use, not the transmit one; the
|
||||||
|
// split TX frequency is a separate question, asked with "i". Answering "f"
|
||||||
|
// with the TX frequency invites a client to write it back as the dial.
|
||||||
|
Freq() int64 // current (RX) frequency in Hz, 0 if unknown
|
||||||
Mode() string // ADIF mode (SSB, CW, FT8…)
|
Mode() string // ADIF mode (SSB, CW, FT8…)
|
||||||
Split() (bool, int64) // split on?, and the other VFO's frequency
|
Split() (bool, int64) // split on?, and the other VFO's frequency
|
||||||
SetFreq(hz int64) error
|
SetFreq(hz int64) error
|
||||||
|
|||||||
@@ -148,3 +148,73 @@ func isHamlogConfirmed(v string) bool {
|
|||||||
v = strings.ToUpper(strings.TrimSpace(v))
|
v = strings.ToUpper(strings.TrimSpace(v))
|
||||||
return v != "" && v != "N" && v != "NO"
|
return v != "" && v != "N" && v != "NO"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RDAChoice is one arbitrated contact: which district the operator decided is
|
||||||
|
// right for that QSO.
|
||||||
|
type RDAChoice struct {
|
||||||
|
QSOID int64 `json:"qso_id"`
|
||||||
|
District string `json:"district"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RDAApplyResult reports what an arbitration did.
|
||||||
|
type RDAApplyResult struct {
|
||||||
|
Applied int `json:"applied"`
|
||||||
|
Failed int `json:"failed"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyRDAChoices writes the chosen district onto each contact — into CNTY and
|
||||||
|
// as the award reference, both.
|
||||||
|
//
|
||||||
|
// The first version wrote only the award override, to keep the imported CNTY as
|
||||||
|
// a record of what HAMLOG said. That was wrong in the way that matters: CNTY is
|
||||||
|
// what the comparison READS, so the contact went on disagreeing for ever. The
|
||||||
|
// operator settled a hundred contacts, ran the comparison again and got the
|
||||||
|
// same hundred rows — the decision had no visible effect anywhere, which is
|
||||||
|
// indistinguishable from a button that does nothing.
|
||||||
|
//
|
||||||
|
// So the chosen district lands in CNTY, where it settles the disagreement, and
|
||||||
|
// in the award reference, where it decides what the contact counts for. This is
|
||||||
|
// the operator's own log: correcting a field in it is the point of the exercise,
|
||||||
|
// not a loss of evidence.
|
||||||
|
func (a *App) ApplyRDAChoices(choices []RDAChoice) (RDAApplyResult, error) {
|
||||||
|
var res RDAApplyResult
|
||||||
|
if a.qso == nil {
|
||||||
|
return res, fmt.Errorf("db not initialized")
|
||||||
|
}
|
||||||
|
for _, c := range choices {
|
||||||
|
district := strings.ToUpper(strings.TrimSpace(c.District))
|
||||||
|
if !rdaDistrictRe.MatchString(district) {
|
||||||
|
res.Failed++
|
||||||
|
applog.Printf("rda apply: qso %d — %q is not a district reference", c.QSOID, c.District)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
q, err := a.qso.GetByID(a.ctx, c.QSOID)
|
||||||
|
if err != nil {
|
||||||
|
res.Failed++
|
||||||
|
applog.Printf("rda apply: qso %d could not be read: %v", c.QSOID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if q.Extras == nil {
|
||||||
|
q.Extras = map[string]string{}
|
||||||
|
}
|
||||||
|
q.County = district
|
||||||
|
q.Extras[award.ManualRefsKey] = setOverrideRef(q.Extras[award.ManualRefsKey], "RDA", district)
|
||||||
|
if err := a.qso.Update(a.ctx, q); err != nil {
|
||||||
|
res.Failed++
|
||||||
|
applog.Printf("rda apply: qso %d update failed: %v", c.QSOID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
res.Applied++
|
||||||
|
}
|
||||||
|
if res.Applied > 0 {
|
||||||
|
// The award totals were computed from the old answers.
|
||||||
|
a.invalidateAwardStats()
|
||||||
|
}
|
||||||
|
applog.Printf("rda apply: %d contacts settled, %d failed", res.Applied, res.Failed)
|
||||||
|
res.Message = fmt.Sprintf("%d settled", res.Applied)
|
||||||
|
if res.Failed > 0 {
|
||||||
|
res.Message += fmt.Sprintf(", %d failed (see the log)", res.Failed)
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// appVersion is stamped on every heartbeat (and could feed the About box).
|
// appVersion is stamped on every heartbeat (and could feed the About box).
|
||||||
appVersion = "0.26.16"
|
appVersion = "0.26.17"
|
||||||
|
|
||||||
// posthogHost is the PostHog ingestion endpoint. EU cloud by default; change
|
// posthogHost is the PostHog ingestion endpoint. EU cloud by default; change
|
||||||
// to https://us.i.posthog.com for a US project.
|
// to https://us.i.posthog.com for a US project.
|
||||||
|
|||||||
Reference in New Issue
Block a user