feat(hamlog): the settings panel, the key check, and a row-colour channel
HAMLOG.online is now reachable: Settings → External services → HAMLOG.ONLINE takes the API key, the auto-upload switch and the timing, exactly like the six services beside it, and a link opens the page where the key is issued. The button next to it checks the KEY, not the connection. HAMLOG answers KEYSTATUS with the callsign the key belongs to — something no other service here offers — so the result reads "Key is valid — account F4BPO" rather than a green tick. A key pasted from a club account or a second station is visible at once, which is precisely the mistake a tick would have hidden. No on-close timing, for the same reason as Cloudlog: there is no per-QSO upload column to select a backlog from at shutdown, so the option would have done nothing. Immediate or delayed, and a stored on_close falls back to immediate rather than silently uploading nothing. The appearance rules gain a HAMLOG.online channel too, so a row can be coloured "confirmed" on that alone. It reads the ADIF extras — the standard names a field for hamlog.EU and none for hamlog.ONLINE — using the same keys the award engine reads, so the two can never disagree about what a confirmation is. "To send" skips it: a QSO is either uploaded or not, there is no card to be owed.
This commit is contained in:
@@ -26,7 +26,7 @@ export type RowColorSettings = {
|
||||
// otherwise keep striping underneath whatever we do per row.
|
||||
const ZEBRA_DEFAULT = 'color-mix(in srgb, var(--muted) 40%, var(--card))';
|
||||
|
||||
export const CHANNELS = ['qsl', 'lotw', 'eqsl', 'qrz'] as const;
|
||||
export const CHANNELS = ['qsl', 'lotw', 'eqsl', 'qrz', 'hamlog'] as const;
|
||||
|
||||
// The two QSO fields behind each channel. QRZ.com and Club Log call theirs an
|
||||
// "upload status" rather than a QSL flag, but they carry the same Y / R letters.
|
||||
@@ -37,6 +37,24 @@ const FIELDS: Record<string, { sent: string; rcvd: string }> = {
|
||||
qrz: { sent: 'qrzcom_qso_upload_status', rcvd: 'qrzcom_qso_download_status' },
|
||||
};
|
||||
|
||||
// HAMLOG.online has no column of its own: the ADIF standard names a field for
|
||||
// hamlog.EU and none for hamlog.ONLINE, so its state lives in the extras — see
|
||||
// internal/award/award.go, which reads the same keys for award confirmations.
|
||||
// One vocabulary, two readers.
|
||||
const HAMLOG_SENT = 'APP_OPSLOG_HAMLOG_SENT';
|
||||
const HAMLOG_RCVD = ['APP_OPSLOG_HAMLOG_QSL', 'APP_HAMLOG_QSL', 'APP_HAMLOGONLINE_QSL', 'HAMLOG_QSL_RCVD'];
|
||||
|
||||
// hamlogState reads a row's extras. Any value that is not an explicit "no"
|
||||
// counts, because their export could carry a date or a match id rather than Y.
|
||||
function hamlogState(q: any, which: 'sent' | 'rcvd'): boolean {
|
||||
const e = (q?.extras ?? {}) as Record<string, string>;
|
||||
const keys = which === 'sent' ? [HAMLOG_SENT] : HAMLOG_RCVD;
|
||||
return keys.some((k) => {
|
||||
const v = String(e[k] ?? '').trim();
|
||||
return v !== '' && v.toUpperCase() !== 'N' && v.toUpperCase() !== 'NO';
|
||||
});
|
||||
}
|
||||
|
||||
// ADIF QSL fields are single letters. Y is the only one that means "yes";
|
||||
// R (requested) and Q (queued) mean it has not gone out yet — a different state,
|
||||
// and the one an operator looks for when deciding what to send.
|
||||
@@ -55,14 +73,18 @@ function ruleMatches(q: any, r: RowColorRule): boolean {
|
||||
const cs = chansOf(r);
|
||||
switch (r.id) {
|
||||
case 'confirmed':
|
||||
return cs.some((c) => yes(q[FIELDS[c]?.rcvd]));
|
||||
return cs.some((c) => (c === 'hamlog' ? hamlogState(q, 'rcvd') : yes(q[FIELDS[c]?.rcvd])));
|
||||
case 'sent':
|
||||
return cs.some((c) => yes(q[FIELDS[c]?.sent]));
|
||||
return cs.some((c) => (c === 'hamlog' ? hamlogState(q, 'sent') : yes(q[FIELDS[c]?.sent])));
|
||||
case 'to_send':
|
||||
return cs.some((c) => owed(q[FIELDS[c]?.sent]));
|
||||
// Nothing to request from HAMLOG: a QSO is either uploaded or it is not,
|
||||
// there is no "card asked for" state to be owed.
|
||||
return cs.some((c) => c !== 'hamlog' && owed(q[FIELDS[c]?.sent]));
|
||||
case 'worked':
|
||||
// The catch-all: nothing sent, nothing asked for, nothing back.
|
||||
return !CHANNELS.some((c) => yes(q[FIELDS[c].rcvd]) || yes(q[FIELDS[c].sent]) || owed(q[FIELDS[c].sent]));
|
||||
return !CHANNELS.some((c) => (c === 'hamlog'
|
||||
? hamlogState(q, 'rcvd') || hamlogState(q, 'sent')
|
||||
: yes(q[FIELDS[c].rcvd]) || yes(q[FIELDS[c].sent]) || owed(q[FIELDS[c].sent])));
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user