diff --git a/cmd/icomnettest/main.go b/cmd/icomnettest/main.go index c079559..436d426 100644 --- a/cmd/icomnettest/main.go +++ b/cmd/icomnettest/main.go @@ -54,6 +54,65 @@ func ctrlPacket(typ uint16, seq uint16, sentid, rcvdid uint32) []byte { return b } +// passcodeSeq is Icom's fixed obfuscation table for the login username/password +// (used by RS-BA1). BEST-EFFORT public reconstruction — the values that matter +// for a given credential are sequence[char+index]; if the radio rejects auth, +// compare the "scrambled" bytes this tool prints against a real login capture to +// correct the needed entries. +var passcodeSeq = [256]byte{ + 0x47, 0x5d, 0x4c, 0x42, 0x66, 0x20, 0x23, 0x46, 0x4e, 0x57, 0x45, 0x3d, 0x67, 0x76, 0x60, 0x41, + 0x62, 0x39, 0x59, 0x2d, 0x68, 0x7e, 0x20, 0x77, 0x5f, 0x51, 0x3e, 0x70, 0x4d, 0x1f, 0x74, 0x38, + 0x2c, 0x4b, 0x1e, 0x54, 0x30, 0x71, 0x2b, 0x2a, 0x66, 0x27, 0x2e, 0x58, 0x24, 0x21, 0x2f, 0x50, + 0x1b, 0x73, 0x69, 0x36, 0x1d, 0x4f, 0x1c, 0x51, 0x2e, 0x1e, 0x45, 0x2e, 0x22, 0x50, 0x64, 0x66, + 0x24, 0x36, 0x0c, 0x7d, 0x50, 0x25, 0x7c, 0x3f, 0x2d, 0x35, 0x71, 0x6a, 0x0e, 0x41, 0x2a, 0x67, + 0x7c, 0x64, 0x77, 0x67, 0x6d, 0x5b, 0x3d, 0x5b, 0x2b, 0x67, 0x6c, 0x39, 0x35, 0x76, 0x3b, 0x2f, + 0x2f, 0x6d, 0x59, 0x6e, 0x59, 0x77, 0x3b, 0x24, 0x74, 0x7c, 0x6b, 0x37, 0x54, 0x5c, 0x4d, 0x1f, + 0x27, 0x69, 0x5b, 0x2e, 0x28, 0x35, 0x77, 0x74, 0x35, 0x1f, 0x6a, 0x2a, 0x28, 0x30, 0x25, 0x20, +} + +// passcode scrambles s (username or password) via the Icom sequence table. +func passcode(s string) []byte { + out := make([]byte, len(s)) + for i := 0; i < len(s); i++ { + p := int(s[i]) + i + if p > 0x7f { + p = ((p - 0x7f) % 0x33) - 1 + if p < 0 { + p = 0 + } + } + out[i] = passcodeSeq[p&0xff] + } + return out +} + +// buildLogin builds the 0x80-byte login packet: control header + username/ +// password (scrambled) at 0x40/0x50 and the app name at 0x60. The middle fields +// (payload size, request type, inner seq, token request) are a best-effort +// reconstruction and may need adjustment against a capture. +func buildLogin(seq uint16, sentid, rcvdid uint32, innerSeq, tokRequest uint16, user, pass, name string) []byte { + b := make([]byte, 0x80) + binary.LittleEndian.PutUint32(b[0:], 0x80) // len + // type (b[4:6]) = 0x00 + binary.LittleEndian.PutUint16(b[6:], seq) + binary.LittleEndian.PutUint32(b[8:], sentid) + binary.LittleEndian.PutUint32(b[12:], rcvdid) + binary.LittleEndian.PutUint32(b[16:], 0x70) // payload size (len - 0x10) + binary.LittleEndian.PutUint16(b[20:], 0x00) // requesttype + binary.LittleEndian.PutUint16(b[22:], 0x01) // requestreply + binary.LittleEndian.PutUint16(b[24:], innerSeq) + binary.LittleEndian.PutUint16(b[26:], tokRequest) + // token (b[0x20:0x24]) = 0 until the rig grants one + copy(b[0x40:0x50], passcode(user)) + copy(b[0x50:0x60], passcode(pass)) + nm := name + if len(nm) > 16 { + nm = nm[:16] + } + copy(b[0x60:0x70], []byte(nm)) + return b +} + func parseHeader(b []byte) (length uint32, typ, seq uint16, sentid, rcvdid uint32, ok bool) { if len(b) < 16 { return 0, 0, 0, 0, 0, false @@ -68,22 +127,18 @@ func parseHeader(b []byte) (length uint32, typ, seq uint16, sentid, rcvdid uint3 func main() { if len(os.Args) < 2 { - fmt.Println("usage: icomnettest [control-port] [run-seconds]") - fmt.Println("example: icomnettest 192.168.1.60 50001 20") + fmt.Println("usage: icomnettest [user] [password]") + fmt.Println(" only → handshake + ping probe") + fmt.Println(" → also attempt login") + fmt.Println("example: icomnettest 192.168.1.60 f6bgc cgb6f1") os.Exit(2) } ip := os.Args[1] port := 50001 - if len(os.Args) >= 3 { - if v, err := strconv.Atoi(os.Args[2]); err == nil { - port = v - } - } - runSecs := 20 + runSecs := 25 + user, pass := "", "" if len(os.Args) >= 4 { - if v, err := strconv.Atoi(os.Args[3]); err == nil && v > 0 { - runSecs = v - } + user, pass = os.Args[2], os.Args[3] } target := net.JoinHostPort(ip, strconv.Itoa(port)) @@ -110,6 +165,14 @@ func main() { } fmt.Printf("Probing Icom control stream at %s (myID=0x%08X)\n\n", target, myID) + if user != "" { + fmt.Printf("Login mode: user=%q pass=%q\n", user, pass) + fmt.Printf(" scrambled user = % X\n", passcode(user)) + fmt.Printf(" scrambled pass = % X\n\n", passcode(pass)) + } + var innerSeq uint16 = 0x0001 + var tokRequest uint16 = 0x1234 // fixed for reproducibility (no RNG in this probe) + loginSent := false // 1) areYouThere — ask the rig to announce itself. seq++ @@ -154,8 +217,15 @@ func main() { logTx("areYouReady", ctrlPacket(typeAreYouReady, seq, myID, remoteID)) readyStarted = true case typeAreYouReady: - if readyStarted { - fmt.Printf(">> iAmReady — control link is up. (Login is the next milestone.)\n\n") + if readyStarted && !loginSent { + fmt.Printf(">> iAmReady — control link is up.\n\n") + if user != "" { + seq++ + lg := buildLogin(seq, myID, remoteID, innerSeq, tokRequest, user, pass, "OpsLog") + fmt.Printf(">> sending login (user=%q)\n", user) + logTx("login", lg) + loginSent = true + } } case typePing: // Reply to the rig's ping: mirror the packet, swap sender/receiver IDs, diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c747313..bb8fb92 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -324,7 +324,7 @@ export default function App() { title={`${on ? 'Unlock' : 'Lock'} ${title}`} className={cn( 'inline-flex items-center justify-center size-3.5 rounded transition-colors', - on ? 'text-amber-600 hover:text-amber-700' : 'text-muted-foreground/40 hover:text-muted-foreground', + on ? 'text-warning hover:text-warning' : 'text-muted-foreground/40 hover:text-muted-foreground', )} > @@ -2525,8 +2525,8 @@ export default function App() { {!lookupBusy && lookupResult && (() => { const src = (lookupResult.source || '').toLowerCase(); const tone = src.includes('cache') ? 'amber' : src.includes('qrz') ? 'emerald' : src.includes('ham') ? 'sky' : 'slate'; - const ring = { amber: 'bg-amber-500/12 text-amber-600 ring-amber-500/30', emerald: 'bg-emerald-500/12 text-emerald-600 ring-emerald-500/30', sky: 'bg-sky-500/12 text-sky-600 ring-sky-500/30', slate: 'bg-slate-500/12 text-slate-500 ring-slate-500/30' }[tone]; - const dot = { amber: 'bg-amber-500', emerald: 'bg-emerald-500', sky: 'bg-sky-500', slate: 'bg-slate-400' }[tone]; + const ring = { amber: 'bg-warning/12 text-warning ring-warning/30', emerald: 'bg-success/12 text-success ring-success/30', sky: 'bg-info/12 text-info ring-info/30', slate: 'bg-muted-foreground/12 text-muted-foreground ring-muted-foreground/30' }[tone]; + const dot = { amber: 'bg-warning', emerald: 'bg-success', sky: 'bg-info', slate: 'bg-muted-foreground' }[tone]; return ( {lookupResult.source} @@ -2534,7 +2534,7 @@ export default function App() { ); })()} {!lookupBusy && !lookupResult && lookupError && ( - {lookupError} + {lookupError} )} {callsign.trim() && ( )} onCallsignInput(e.target.value)} onBlur={() => { @@ -2608,7 +2608,7 @@ export default function App() { // a past QSO). Sets the DATE part of qsoStartedAt; the time field keeps the time. const dateBlock = locks.start ? (
- + - + - +
-
+
setRcv(e.target.value.toUpperCase())} />
@@ -2764,7 +2764,7 @@ export default function App() { ); const rxFreqBlock = (
- + setFreqFocused(true)} onBlur={() => setFreqFocused(false)} onChange={(e) => { setRxFreqMhz(e.target.value); noteManualEdit(); const rb = bandForMHz(parseFloat(e.target.value)); if (rb) setBandRx(rb); }} - className={cn('font-mono', catState.split && 'bg-rose-50/40 border-rose-200 focus:bg-card')} + className={cn('font-mono', catState.split && 'bg-danger-muted/40 border-danger-border focus:bg-card')} />
); @@ -2924,7 +2924,7 @@ export default function App() { type="button" onClick={() => setClusterLockBand((v) => !v)} className={cn('inline-flex items-center gap-0.5 text-[10px] px-1 py-0.5 rounded border', - clusterLockBand ? 'bg-amber-100 text-amber-800 border-amber-300' : 'text-muted-foreground border-border hover:bg-muted')} + clusterLockBand ? 'bg-warning-muted text-warning-muted-foreground border-warning-border' : 'text-muted-foreground border-border hover:bg-muted')} title="Lock to the entry strip's current band" > {clusterLockBand ? : } {band} @@ -2957,7 +2957,7 @@ export default function App() { type="button" onClick={() => setClusterLockMode((v) => !v)} className={cn('inline-flex items-center gap-1 px-1.5 py-0.5 rounded border text-[10px]', - clusterLockMode ? 'bg-amber-100 text-amber-800 border-amber-300' : 'text-muted-foreground border-border hover:bg-muted')} + clusterLockMode ? 'bg-warning-muted text-warning-muted-foreground border-warning-border' : 'text-muted-foreground border-border hover:bg-muted')} title="Only show spots whose mode matches the entry strip" > {clusterLockMode ? : } Lock mode ({mode}) @@ -2968,10 +2968,10 @@ export default function App() {
Status
{([ - { k: 'new' as SpotStatusKey, label: 'NEW', cls: 'bg-rose-100 text-rose-800 border-rose-300' }, - { k: 'new-band' as SpotStatusKey, label: 'NEW BAND', cls: 'bg-amber-100 text-amber-800 border-amber-300' }, - { k: 'new-mode' as SpotStatusKey, label: 'NEW MODE', cls: 'bg-yellow-100 text-yellow-800 border-yellow-300' }, - { k: 'new-slot' as SpotStatusKey, label: 'NEW SLOT', cls: 'bg-yellow-100 text-yellow-800 border-yellow-300' }, + { k: 'new' as SpotStatusKey, label: 'NEW', cls: 'bg-danger-muted text-danger-muted-foreground border-danger-border' }, + { k: 'new-band' as SpotStatusKey, label: 'NEW BAND', cls: 'bg-warning-muted text-warning-muted-foreground border-warning-border' }, + { k: 'new-mode' as SpotStatusKey, label: 'NEW MODE', cls: 'bg-caution-muted text-caution-muted-foreground border-caution-border' }, + { k: 'new-slot' as SpotStatusKey, label: 'NEW SLOT', cls: 'bg-caution-muted text-caution-muted-foreground border-caution-border' }, // (no WORKED chip — use the "Hide worked" checkbox to drop dupes.) ]).map((s) => { const on = clusterStatusFilter.has(s.k); @@ -2991,9 +2991,9 @@ export default function App() {
Mode
{([ - { k: 'SSB' as SpotModeCat, label: 'SSB', cls: 'bg-sky-100 text-sky-800 border-sky-300' }, - { k: 'CW' as SpotModeCat, label: 'CW', cls: 'bg-violet-100 text-violet-800 border-violet-300' }, - { k: 'DATA' as SpotModeCat, label: 'DATA', cls: 'bg-emerald-100 text-emerald-800 border-emerald-300' }, + { k: 'SSB' as SpotModeCat, label: 'SSB', cls: 'bg-info-muted text-info-muted-foreground border-info-border' }, + { k: 'CW' as SpotModeCat, label: 'CW', cls: 'bg-info-muted text-info-muted-foreground border-info-border' }, + { k: 'DATA' as SpotModeCat, label: 'DATA', cls: 'bg-success-muted text-success-muted-foreground border-success-border' }, ]).map((s) => { const on = clusterModeFilter.has(s.k); return ( @@ -3121,7 +3121,7 @@ export default function App() { {freqMhz ? fmtFreqDots(freqMhz) : '—.———.———'} MHz {band} - {mode} + {mode}
{utcNow}Z
@@ -3154,10 +3154,10 @@ export default function App() {
) : ( -
+
{toast} - +
)}
@@ -3166,14 +3166,14 @@ export default function App() { {freqMhz ? fmtFreqDots(freqMhz) : '—.———.———'} {catState.split && rxFreqMhz && ( - RX + RX {fmtFreqDots(rxFreqMhz)} )}
MHz {catState.split && ( - SPLIT + SPLIT )} {/* Band & mode removed here — shown in the QSO entry strip below. */} {catState.enabled && catState.backend === 'omnirig' && ( @@ -3197,7 +3197,7 @@ export default function App() { const disabled = !p; const goto = (az: number) => RotatorGoTo(Math.round(az), -1).catch((err) => setError(String(err?.message ?? err))); return ( -
+
@@ -3243,16 +3243,16 @@ export default function App() { {/* Ultrabeam pattern (Normal / 180° reverse / Bidirectional), next to the azimuth. */} {ubStatus.enabled && ( -
{([{ d: 0, l: 'N', t: 'Normal' }, { d: 1, l: '180°', t: 'Reverse (180°)' }, { d: 2, l: 'Bi', t: 'Bidirectional' }]).map((o) => ( @@ -3268,13 +3268,13 @@ export default function App() { title={dvkStat.playing ? 'Voice keyer — playing' : dvkEnabled ? 'Voice keyer (DVK) — open · click to close' : 'Voice keyer (DVK) · click to open'} className={cn( 'relative inline-flex items-center justify-center size-7 rounded-md border transition-colors', - dvkStat.playing ? 'border-rose-300 bg-rose-100 text-rose-700' - : dvkEnabled ? 'border-emerald-300 bg-emerald-50 text-emerald-700 hover:bg-emerald-100' + dvkStat.playing ? 'border-danger-border bg-danger-muted text-danger-muted-foreground' + : dvkEnabled ? 'border-success-border bg-success-muted text-success-muted-foreground hover:bg-success-muted' : 'border-border text-muted-foreground hover:bg-muted', )} > - {dvkStat.playing && } + {dvkStat.playing && } )} {chatAvailable && ( @@ -3341,12 +3341,12 @@ export default function App() { onClick={() => setChatOpen((o) => !o)} title={`Multi-op chat${chatUnread > 0 ? ` — ${chatUnread} new` : ''}`} className={cn('relative inline-flex items-center justify-center size-7 rounded-md border transition-colors', - chatShown ? 'border-sky-300 bg-sky-50 text-sky-700 hover:bg-sky-100' + chatShown ? 'border-info-border bg-info-muted text-info-muted-foreground hover:bg-info-muted' : 'border-border text-muted-foreground hover:bg-muted')} > {chatUnread > 0 && ( - + {chatUnread > 9 ? '9+' : chatUnread} )} @@ -3369,7 +3369,7 @@ export default function App() { {station.callsign} {station.my_grid && ( - {station.my_grid} + {station.my_grid} )} ) : ( @@ -3468,7 +3468,7 @@ export default function App() { Update available: v{updateInfo.latest} — download ) : ( -

You're up to date

+

You're up to date

)}

Developed by {APP_AUTHOR} @@ -3746,12 +3746,12 @@ export default function App() { {/* ===== CW decoder strip (only when enabled AND mode is CW) ===== */} {cwOn && ( -

- +
+ {/* Input-level meter — if this stays flat with a strong signal, the RX audio device is wrong/silent rather than a decode problem. */}
-
+
{cwStatus.wpm > 0 ? `${cwStatus.wpm} WPM` : '— WPM'} · {cwStatus.pitch > 0 ? `${cwStatus.pitch} Hz` : '— Hz'} @@ -3763,7 +3763,7 @@ export default function App() { onChange={(e) => setCwPitch(e.target.value)} placeholder="auto" title="Lock the decoder to this pitch (Hz). Blank = follow the radio's CW pitch / auto-search." - className="shrink-0 w-14 h-5 rounded border border-emerald-300/70 bg-white/60 px-1 text-[10px] font-mono text-center outline-none" + className="shrink-0 w-14 h-5 rounded border border-success-border/70 bg-card/60 px-1 text-[10px] font-mono text-center outline-none" /> {/* Left-aligned single line, no scrollbar; auto-scrolled to the newest text (see cwScrollRef effect) so the latest stays in view. */} @@ -3776,7 +3776,7 @@ export default function App() {
{qsos.length >= qsoLimit && qsos.length < total && ( - Limit reached — raise Max to see more. + Limit reached — raise Max to see more. )} - {isMaster && } + {isMaster && } {s.name} {s.state.toUpperCase()}{s.retries ? ` #${s.retries}` : ''} @@ -4254,7 +4254,7 @@ export default function App() { : 'border-border hover:bg-muted cursor-pointer', )} > - + {label} ); @@ -4286,7 +4286,7 @@ export default function App() { title={dbConn.backend === 'mysql' ? `Shared MySQL logbook — ${dbConn.label}` : `Local SQLite logbook — ${dbConn.label}`} className="inline-flex items-center gap-1 text-[11px] text-muted-foreground hover:text-foreground max-w-[340px]" > - + {dbConn.label} )} diff --git a/frontend/src/components/AdifExtrasEditor.tsx b/frontend/src/components/AdifExtrasEditor.tsx index eeda9ed..d6a4662 100644 --- a/frontend/src/components/AdifExtrasEditor.tsx +++ b/frontend/src/components/AdifExtrasEditor.tsx @@ -109,7 +109,7 @@ export function AdifExtrasEditor({ value, onChange }: Props) { {!def && ''} )} - {!def && {t('adx.nonStandard')}} + {!def && {t('adx.nonStandard')}}
{ setDraft(alerts.Rule.createFrom(r)); setTab('def'); }} className={cn('w-full text-left px-2 py-1.5 rounded text-xs flex items-center gap-1.5', draft?.id === r.id ? 'bg-accent text-accent-foreground font-semibold' : 'hover:bg-muted/60')}> - + {r.name} {r.sound && } {r.email && } @@ -240,9 +240,9 @@ export function AlertsModal({ onClose, bands, modes, countries }: { {/* Editor actions */}
- {err && {err}} + {err && {err}}
- +
diff --git a/frontend/src/components/AntGeniusPanel.tsx b/frontend/src/components/AntGeniusPanel.tsx index 5d8524d..9590ee0 100644 --- a/frontend/src/components/AntGeniusPanel.tsx +++ b/frontend/src/components/AntGeniusPanel.tsx @@ -54,12 +54,12 @@ export function AntGeniusPanel({ status, onActivate, onClose }: { return (
- + Antenna Genius - - {status.connected ? t('agp.online') : t('agp.offline')} + + {status.connected ? t('agp.online') : t('agp.offline')} @@ -567,7 +567,7 @@ function ReferencesPanel({ code, presets, meta, onUpdateOnline, updating, onChan
{busy &&
{t('awed.searching')}
} {!busy && large && q.trim().length < 2 && ( -
+
{t('awed.tooManyItems', { total: total.toLocaleString() })}
)} diff --git a/frontend/src/components/AwardRefPicker.tsx b/frontend/src/components/AwardRefPicker.tsx index b46adc6..3809f2d 100644 --- a/frontend/src/components/AwardRefPicker.tsx +++ b/frontend/src/components/AwardRefPicker.tsx @@ -50,9 +50,9 @@ export function AwardRefPicker({ code, label, dxcc, countryOnly, value, onChange
{value ? ( -
+
{value} - +
) : ( +
{selectedRef.code} - {selectedRef.name} -
@@ -284,15 +284,15 @@ export function AwardRefSelector({ dxcc, value, onChange, fieldValues, heightCla onClick={() => addRef({ code: autoMatch.code, name: autoMatch.name } as AwardRef)} className={`text-left rounded border px-1.5 py-1 text-[11px] leading-tight ${ autoAlreadyAdded - ? 'border-emerald-300 bg-emerald-50 text-emerald-800 cursor-default' - : 'border-emerald-300 bg-emerald-50/60 text-emerald-800 hover:bg-emerald-100' + ? 'border-success-border bg-success-muted text-success-muted-foreground cursor-default' + : 'border-success-border bg-success-muted/60 text-success-muted-foreground hover:bg-success-muted' }`} title={t('awrs.autoMatchTitle', { field: selField.toUpperCase(), code: autoMatch.code })} > {autoAlreadyAdded ? '✓ ' : '+ '} {autoMatch.code} - {t('awrs.fromField', { field: selField })} - {!autoAlreadyAdded && {t('awrs.autoClickToAdd')}} + {t('awrs.fromField', { field: selField })} + {!autoAlreadyAdded && {t('awrs.autoClickToAdd')}} )} = { - validated: 'bg-emerald-500 text-white', - confirmed: 'bg-amber-400 text-amber-950', - worked: 'bg-stone-400 text-white', + validated: 'bg-success text-success-foreground', + confirmed: 'bg-warning text-warning-foreground', + worked: 'bg-muted-foreground text-background', none: '', }; const CELL_LABEL: Record = { validated: 'V', confirmed: 'C', worked: 'W', none: '' }; @@ -53,8 +53,8 @@ function ProgressBar({ worked, confirmed, total }: { worked: number; confirmed: if (total <= 0) return null; return (
-
-
+
+
); } @@ -255,7 +255,7 @@ export function AwardsPanel({ onEditQSO }: { onEditQSO?: (id: number) => void } {a.code} {r ? ( - {r.confirmed} + {r.confirmed} /{r.worked} {r.total > 0 && {t('awp.of')} {r.total}} @@ -286,8 +286,8 @@ export function AwardsPanel({ onEditQSO }: { onEditQSO?: (id: number) => void }
{current.worked} {t('awp.worked')} - {current.confirmed} {t('awp.confirmed')} - {current.validated} {t('awp.validated')} + {current.confirmed} {t('awp.confirmed')} + {current.validated} {t('awp.validated')} {current.total > 0 && ( {t('awp.ofConfirmed', { total: current.total, pct: pct(current.confirmed, current.total) })} )} @@ -303,7 +303,7 @@ export function AwardsPanel({ onEditQSO }: { onEditQSO?: (id: number) => void } {(current.bands ?? []).map((b) => (
{b.band}{' '} - {b.confirmed} + {b.confirmed} /{b.worked}
))} @@ -328,7 +328,7 @@ export function AwardsPanel({ onEditQSO }: { onEditQSO?: (id: number) => void } {filteredRefs.length} {t('awp.refs')} @@ -370,7 +370,7 @@ export function AwardsPanel({ onEditQSO }: { onEditQSO?: (id: number) => void } {row.label} {statsBandIdx.map((i) => { const c = row.cells[i] ?? 0; return ( - 0 ? 'bg-emerald-500/15 text-emerald-700' : 'text-muted-foreground/30')}>{c || ''} + 0 ? 'bg-success/15 text-success' : 'text-muted-foreground/30')}>{c || ''} ); })} {row.total || ''} {row.grand_total || ''} @@ -439,13 +439,13 @@ export function AwardsPanel({ onEditQSO }: { onEditQSO?: (id: number) => void } {r.group} {!r.worked ? {t('awp.missing')} - : r.validated ? {t('awp.validated')} - : r.confirmed ? {t('awp.confirmed')} - : {t('awp.worked')}} + : r.validated ? {t('awp.validated')} + : r.confirmed ? {t('awp.confirmed')} + : {t('awp.worked')}} {r.bands.map((b) => ( - {b} + {b} ))} @@ -558,7 +558,7 @@ function MissingQSOModal({ code, name, onClose, onEditQSO }: { code: string; nam
e.stopPropagation()}>
- + {code} — {t('awp.contactsMissingRef')} {name && {name}}
@@ -589,7 +589,7 @@ function MissingQSOModal({ code, name, onClose, onEditQSO }: { code: string; nam - {msg && {msg}} + {msg && {msg}}
diff --git a/frontend/src/components/BandMap.tsx b/frontend/src/components/BandMap.tsx index 0531065..7e2d620 100644 --- a/frontend/src/components/BandMap.tsx +++ b/frontend/src/components/BandMap.tsx @@ -61,17 +61,17 @@ const BAND_RANGES: Record = { }; const SEGMENT_COLORS: Record = { - '160m': [[1800, 1838, 'fill-emerald-50'], [1838, 1840, 'fill-sky-50'], [1840, 2000, 'fill-amber-50']], - '80m': [[3500, 3580, 'fill-emerald-50'], [3580, 3600, 'fill-sky-50'], [3600, 3800, 'fill-amber-50']], - '60m': [[5350, 5450, 'fill-amber-50']], - '40m': [[7000, 7040, 'fill-emerald-50'], [7040, 7100, 'fill-sky-50'], [7100, 7200, 'fill-amber-50']], - '30m': [[10100, 10130, 'fill-emerald-50'], [10130, 10150, 'fill-sky-50']], - '20m': [[14000, 14070, 'fill-emerald-50'], [14070, 14100, 'fill-sky-50'], [14100, 14350, 'fill-amber-50']], - '17m': [[18068, 18095, 'fill-emerald-50'], [18095, 18110, 'fill-sky-50'], [18110, 18168, 'fill-amber-50']], - '15m': [[21000, 21070, 'fill-emerald-50'], [21070, 21150, 'fill-sky-50'], [21150, 21450, 'fill-amber-50']], - '12m': [[24890, 24915, 'fill-emerald-50'], [24915, 24940, 'fill-sky-50'], [24940, 24990, 'fill-amber-50']], - '10m': [[28000, 28070, 'fill-emerald-50'], [28070, 28300, 'fill-sky-50'], [28300, 29700, 'fill-amber-50']], - '6m': [[50000, 50100, 'fill-emerald-50'], [50100, 50500, 'fill-amber-50']], + '160m': [[1800, 1838, 'fill-success-muted'], [1838, 1840, 'fill-info-muted'], [1840, 2000, 'fill-warning-muted']], + '80m': [[3500, 3580, 'fill-success-muted'], [3580, 3600, 'fill-info-muted'], [3600, 3800, 'fill-warning-muted']], + '60m': [[5350, 5450, 'fill-warning-muted']], + '40m': [[7000, 7040, 'fill-success-muted'], [7040, 7100, 'fill-info-muted'], [7100, 7200, 'fill-warning-muted']], + '30m': [[10100, 10130, 'fill-success-muted'], [10130, 10150, 'fill-info-muted']], + '20m': [[14000, 14070, 'fill-success-muted'], [14070, 14100, 'fill-info-muted'], [14100, 14350, 'fill-warning-muted']], + '17m': [[18068, 18095, 'fill-success-muted'], [18095, 18110, 'fill-info-muted'], [18110, 18168, 'fill-warning-muted']], + '15m': [[21000, 21070, 'fill-success-muted'], [21070, 21150, 'fill-info-muted'], [21150, 21450, 'fill-warning-muted']], + '12m': [[24890, 24915, 'fill-success-muted'], [24915, 24940, 'fill-info-muted'], [24940, 24990, 'fill-warning-muted']], + '10m': [[28000, 28070, 'fill-success-muted'], [28070, 28300, 'fill-info-muted'], [28300, 29700, 'fill-warning-muted']], + '6m': [[50000, 50100, 'fill-success-muted'], [50100, 50500, 'fill-warning-muted']], }; // Small coloured dot + label used in the band-map legend strip. @@ -103,22 +103,22 @@ function statusStyle(s: string): { pill: string; bar: string; line: string; dot: // dot = small marker on the freq scale switch (s) { case 'new': return { - pill: 'bg-rose-50 text-rose-900 border-rose-200 hover:bg-rose-100', - bar: 'bg-rose-500', - line: 'stroke-rose-400', - dot: 'fill-rose-500', + pill: 'bg-danger-muted text-danger-muted-foreground border-danger-border hover:bg-danger-muted', + bar: 'bg-danger', + line: 'stroke-danger', + dot: 'fill-danger', }; case 'new-band': return { - pill: 'bg-amber-50 text-amber-900 border-amber-200 hover:bg-amber-100', - bar: 'bg-amber-500', - line: 'stroke-amber-400', - dot: 'fill-amber-500', + pill: 'bg-warning-muted text-warning-muted-foreground border-warning-border hover:bg-warning-muted', + bar: 'bg-warning', + line: 'stroke-warning', + dot: 'fill-warning', }; case 'new-slot': return { - pill: 'bg-yellow-50 text-yellow-900 border-yellow-200 hover:bg-yellow-100', - bar: 'bg-yellow-500', - line: 'stroke-yellow-500', - dot: 'fill-yellow-500', + pill: 'bg-caution-muted text-caution-muted-foreground border-caution-border hover:bg-caution-muted', + bar: 'bg-caution', + line: 'stroke-caution', + dot: 'fill-caution', }; case 'worked': return { pill: 'bg-card text-muted-foreground border-border/60 hover:bg-muted/50', @@ -520,14 +520,14 @@ export function BandMap({ band, spots, spotStatus, currentFreqHz, onSpotClick, o
{/* Colour legend — what each pill colour means. */}
- - - + + +
{t('bmp.footerHint')} - {hidden > 0 && · {t('bmp.spotsHidden', { n: hidden, max: MAX_VISIBLE_SPOTS })}} + {hidden > 0 && · {t('bmp.spotsHidden', { n: hidden, max: MAX_VISIBLE_SPOTS })}}
); diff --git a/frontend/src/components/BandSlotGrid.tsx b/frontend/src/components/BandSlotGrid.tsx index 8f47645..0702619 100644 --- a/frontend/src/components/BandSlotGrid.tsx +++ b/frontend/src/components/BandSlotGrid.tsx @@ -51,22 +51,27 @@ function classMatchesMode(cls: string, mode: string): boolean { return u !== '' && u !== 'CW' && !PHONE_MODES.has(u); } +// Dedicated matrix palette (--mx-* tokens, per theme in style.css): a 3-level +// ramp per hue so confirmed / worked / not-worked stay distinguishable on both +// light and dark surfaces (the generic status -muted fills were too dark on the +// dark themes to tell "worked" from "not worked" apart). Light-warm reproduces +// the original emerald/indigo/stone colours exactly. const STATUS_CLASSES: Record = { - call_c: 'bg-emerald-700 hover:bg-emerald-600', - call_w: 'bg-emerald-300 hover:bg-emerald-200', - dxcc_c: 'bg-indigo-800 hover:bg-indigo-700', - dxcc_w: 'bg-indigo-300 hover:bg-indigo-200', + call_c: 'bg-mx-call-conf', + call_w: 'bg-mx-call-work', + dxcc_c: 'bg-mx-dx-conf', + dxcc_w: 'bg-mx-dx-work', }; // Legend entries, in the same colour order as the cells. swatch = the // background class (or a special ring marker for the current-entry cell). const LEGEND: { swatch: string; ring?: boolean; label: string }[] = [ - { swatch: 'bg-emerald-700', label: 'Call confirmed' }, - { swatch: 'bg-emerald-300', label: 'Call worked' }, - { swatch: 'bg-indigo-800', label: 'Entity confirmed' }, - { swatch: 'bg-indigo-300', label: 'Entity worked' }, - { swatch: 'bg-stone-200', label: 'Not worked' }, - { swatch: 'bg-stone-200', ring: true, label: 'Current entry' }, + { swatch: 'bg-mx-call-conf', label: 'Call confirmed' }, + { swatch: 'bg-mx-call-work', label: 'Call worked' }, + { swatch: 'bg-mx-dx-conf', label: 'Entity confirmed' }, + { swatch: 'bg-mx-dx-work', label: 'Entity worked' }, + { swatch: 'bg-mx-none', label: 'Not worked' }, + { swatch: 'bg-mx-none', ring: true, label: 'Current entry' }, ]; function cellTitle(band: string, cls: string, status: string, current: boolean): string { @@ -128,18 +133,18 @@ export function BandSlotGrid({ wb, busy, currentBand, currentMode, bands, hasCal
{newOne ? ( <> - + NEW ONE - - {dxccName || `DXCC #${dxcc}`} + + {dxccName || `DXCC #${dxcc}`} {' '}· never worked this entity @@ -161,10 +166,10 @@ export function BandSlotGrid({ wb, busy, currentBand, currentMode, bands, hasCal {(newBand || newMode || newBandMode || newSlot) && (
- {newBandMode && New Band & Mode} - {newBand && New Band} - {newMode && New Mode} - {newSlot && New Slot} + {newBandMode && New Band & Mode} + {newBand && New Band} + {newMode && New Mode} + {newSlot && New Slot}
)} @@ -220,8 +225,8 @@ export function BandSlotGrid({ wb, busy, currentBand, currentMode, bands, hasCal title={cellTitle(b.tag, cls, st, isCurrent)} className={cn( 'w-[28px] h-[24px] rounded transition-colors p-0', - st ? STATUS_CLASSES[st] : 'bg-stone-200 hover:bg-stone-300', - isCurrent && 'ring-2 ring-amber-500 ring-inset', + st ? STATUS_CLASSES[st] : 'bg-mx-none', + isCurrent && 'ring-2 ring-warning ring-inset', )} /> ); @@ -240,7 +245,7 @@ export function BandSlotGrid({ wb, busy, currentBand, currentMode, bands, hasCal className={cn( 'inline-block size-3 rounded shrink-0', l.swatch, - l.ring && 'ring-2 ring-amber-500 ring-inset', + l.ring && 'ring-2 ring-warning ring-inset', )} /> {l.label} diff --git a/frontend/src/components/BulkEditModal.tsx b/frontend/src/components/BulkEditModal.tsx index 571a3a2..0946e97 100644 --- a/frontend/src/components/BulkEditModal.tsx +++ b/frontend/src/components/BulkEditModal.tsx @@ -160,7 +160,7 @@ export function BulkEditModal({ open, ids, onClose, onApplied }: Props) { {t('bulk.willSet')} {t(def.label)} ={' '} {effectiveValue === '' ? t('bulk.blank') : effectiveValue} {t('bulk.onQsos', { n: ids.length })}
- {error &&
{error}
} + {error &&
{error}
}
diff --git a/frontend/src/components/CallHistoryPanel.tsx b/frontend/src/components/CallHistoryPanel.tsx index 9468ee7..c6fe434 100644 --- a/frontend/src/components/CallHistoryPanel.tsx +++ b/frontend/src/components/CallHistoryPanel.tsx @@ -76,32 +76,32 @@ export function CallHistoryPanel({ wb, busy, currentCall }: Props) { - - - - - - + + + + + + {entries.map((e) => ( - + diff --git a/frontend/src/components/ChatPopover.tsx b/frontend/src/components/ChatPopover.tsx index e99b92c..d26f4ca 100644 --- a/frontend/src/components/ChatPopover.tsx +++ b/frontend/src/components/ChatPopover.tsx @@ -35,7 +35,7 @@ export function ChatPanel({ msgs, online, myCall, onSend, onClose }: { return (
- + {t('chatp.chat')} {/* Online count — hover to see who's connected. */} @@ -66,7 +66,7 @@ export function ChatPanel({ msgs, online, myCall, onSend, onClose }: { const mine = m.operator.toUpperCase() === me; return (
-
+
{!mine && {m.operator}} {m.message}
diff --git a/frontend/src/components/ClusterGrid.tsx b/frontend/src/components/ClusterGrid.tsx index a42dbc2..2c10d57 100644 --- a/frontend/src/components/ClusterGrid.tsx +++ b/frontend/src/components/ClusterGrid.tsx @@ -1,8 +1,9 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { - AllCommunityModule, ModuleRegistry, themeQuartz, + AllCommunityModule, ModuleRegistry, type ColDef, type ColumnState, type GridReadyEvent, type RowClickedEvent, } from 'ag-grid-community'; +import { hamlogGridTheme } from '@/lib/gridTheme'; import { AgGridReact } from 'ag-grid-react'; import { Columns3, FilterX } from 'lucide-react'; import { @@ -18,27 +19,7 @@ type TFn = (key: string, vars?: Record) => string; ModuleRegistry.registerModules([AllCommunityModule]); -const hamlogTheme = themeQuartz.withParams({ - fontFamily: 'inherit', - fontSize: 12.5, - backgroundColor: '#faf6ea', - foregroundColor: '#2a2419', - headerBackgroundColor: '#e8dfc9', - headerTextColor: '#5a4f3a', - headerFontWeight: 600, - oddRowBackgroundColor: '#f5efe0', - rowHoverColor: '#ecdcb4', - selectedRowBackgroundColor: '#f0d9a8', - borderColor: '#c8b994', - rowBorder: { color: '#d8c9a8', width: 1 }, - columnBorder: { color: '#d8c9a8', width: 1 }, - cellHorizontalPadding: 10, - rowHeight: 30, - headerHeight: 32, - spacing: 4, - accentColor: '#b8410c', - iconSize: 12, -}); +const hamlogTheme = hamlogGridTheme; export type ClusterSpot = { source_id: number; diff --git a/frontend/src/components/ContestPanel.tsx b/frontend/src/components/ContestPanel.tsx index dfc9513..91db5f1 100644 --- a/frontend/src/components/ContestPanel.tsx +++ b/frontend/src/components/ContestPanel.tsx @@ -85,9 +85,9 @@ export function ContestPanel({ session, onChange }: { {/* Setup card. */}
- + {t('ctp.contestSetup')} - {session.active && {t('ctp.live')}} + {session.active && {t('ctp.live')}}
@@ -150,13 +150,13 @@ export function ContestPanel({ session, onChange }: {
{session.active ? ( ) : ( )} diff --git a/frontend/src/components/DuplicatesModal.tsx b/frontend/src/components/DuplicatesModal.tsx index 176aca9..86a73cb 100644 --- a/frontend/src/components/DuplicatesModal.tsx +++ b/frontend/src/components/DuplicatesModal.tsx @@ -126,13 +126,13 @@ export function DuplicatesModal({ onClose, onDeleted }: { onClose: () => void; o {g.qsos.map((q, i) => { const checked = sel.has(q.id); return ( -
+ @@ -151,7 +151,7 @@ export function DuplicatesModal({ onClose, onDeleted }: { onClose: () => void; o
diff --git a/frontend/src/components/DvkPanel.tsx b/frontend/src/components/DvkPanel.tsx index 0408818..cc4330d 100644 --- a/frontend/src/components/DvkPanel.tsx +++ b/frontend/src/components/DvkPanel.tsx @@ -25,8 +25,8 @@ export function DvkPanel({ messages, status, onPlay, onStop, onClose }: Props) {
{t('dvkp.voiceKeyer')} - - {status.playing && tx...} + + {status.playing && tx...}
- {refsMsg &&
{refsMsg}
} + {refsMsg &&
{refsMsg}
}
- {err &&
{err}
} + {err &&
{err}
}
{!canSave && {t('frm.required')}} diff --git a/frontend/src/components/FlexPanel.tsx b/frontend/src/components/FlexPanel.tsx index 2d5d481..e03015a 100644 --- a/frontend/src/components/FlexPanel.tsx +++ b/frontend/src/components/FlexPanel.tsx @@ -79,7 +79,7 @@ function Slider({ value, onChange, disabled, accent = '#16a34a', step = 1, max = onChange={(e) => onChange(parseInt(e.target.value, 10))} className={cn('flex-1 h-1.5 rounded-full appearance-none cursor-pointer disabled:opacity-30 disabled:cursor-default', '[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:size-3.5 [&::-webkit-slider-thumb]:rounded-full', - '[&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:shadow-sm [&::-webkit-slider-thumb]:cursor-pointer')} + '[&::-webkit-slider-thumb]:bg-card [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:shadow-sm [&::-webkit-slider-thumb]:cursor-pointer')} style={{ background: `linear-gradient(to right, ${accent} ${pct}%, #d8cfb8 ${pct}%)`, borderColor: accent }} /> ); @@ -107,10 +107,10 @@ function Chip({ on, onClick, label, disabled, accent = 'emerald' }: { on: boolean; onClick: () => void; label: string; disabled?: boolean; accent?: 'emerald' | 'violet' | 'cyan' | 'amber'; }) { const onCls = { - emerald: 'bg-emerald-600 border-emerald-600 text-white', - violet: 'bg-violet-600 border-violet-600 text-white', - cyan: 'bg-cyan-600 border-cyan-600 text-white', - amber: 'bg-amber-500 border-amber-500 text-white', + emerald: 'bg-success border-success text-success-foreground', + violet: 'bg-info border-info text-info-foreground', + cyan: 'bg-info border-info text-info-foreground', + amber: 'bg-warning border-warning text-warning-foreground', }[accent]; return (
@@ -319,7 +319,7 @@ export function FlexPanel({ onCWSpeed, onReportRST }: { onCWSpeed?: (wpm: number title={t('flxp.splitHint')} onClick={() => change('split', !st.split, () => FlexSetSplit(!st.split))} className={cn('px-3 py-1.5 rounded-lg text-sm font-extrabold tracking-wide border-2 transition-all disabled:opacity-30', - st.split ? 'bg-sky-600 text-white border-sky-600 shadow-[0_0_12px] shadow-sky-600/50' : 'bg-card text-sky-700 border-sky-400 hover:bg-sky-50')}> + st.split ? 'bg-info text-info-foreground border-info shadow-[0_0_12px] shadow-info/50' : 'bg-card text-info border-info hover:bg-info-muted')}> SPLIT {st.split && !!st.tx_freq_hz && ( @@ -414,7 +414,7 @@ export function FlexPanel({ onCWSpeed, onReportRST }: { onCWSpeed?: (wpm: number title={st.mute ? t('flxp.muted') : t('flxp.mute')} onClick={() => change('mute', !st.mute, () => FlexSetMute(!st.mute))} className={cn('shrink-0 rounded p-1 transition-colors disabled:opacity-30', - st.mute ? 'bg-red-600 text-white' : 'text-muted-foreground hover:bg-muted')}> + st.mute ? 'bg-destructive text-destructive-foreground' : 'text-muted-foreground hover:bg-muted')}> {st.mute ? : } change('audio_level', v, () => FlexSetAudioLevel(v))} /> @@ -495,7 +495,7 @@ export function FlexPanel({ onCWSpeed, onReportRST }: { onCWSpeed?: (wpm: number @@ -506,13 +506,13 @@ export function FlexPanel({ onCWSpeed, onReportRST }: { onCWSpeed?: (wpm: number selector is disabled until connected (hover it for the error). */} {(pg.host || pg.connected) && (
diff --git a/frontend/src/components/QSOContextMenu.tsx b/frontend/src/components/QSOContextMenu.tsx index 39eebfe..a6b36f4 100644 --- a/frontend/src/components/QSOContextMenu.tsx +++ b/frontend/src/components/QSOContextMenu.tsx @@ -75,7 +75,7 @@ export function QSOContextMenu({ menu, onClose, onUpdateFromCty, onUpdateFromQRZ className="flex w-full items-center gap-2 px-3 py-1.5 text-left hover:bg-accent/50" onClick={() => { onUpdateFromQRZ(menu.ids); onClose(); }} > - + {t('qctx.updateQrz')} {onUpdateFromClublog && ( @@ -83,7 +83,7 @@ export function QSOContextMenu({ menu, onClose, onUpdateFromCty, onUpdateFromQRZ className="flex w-full items-center gap-2 px-3 py-1.5 text-left hover:bg-accent/50" onClick={() => { onUpdateFromClublog(menu.ids); onClose(); }} > - + {t('qctx.updateClublog')} )} @@ -96,7 +96,7 @@ export function QSOContextMenu({ menu, onClose, onUpdateFromCty, onUpdateFromQRZ className="flex w-full items-center gap-2 px-3 py-1.5 text-left hover:bg-accent/50" onClick={() => { onSendEQSL(menu.ids); onClose(); }} > - + {t('qctx.sendQslEmail')} )} @@ -105,7 +105,7 @@ export function QSOContextMenu({ menu, onClose, onUpdateFromCty, onUpdateFromQRZ className="flex w-full items-center gap-2 px-3 py-1.5 text-left hover:bg-accent/50" onClick={() => { onSendRecording(menu.ids); onClose(); }} > - + {t('qctx.sendRecording')} )} @@ -119,7 +119,7 @@ export function QSOContextMenu({ menu, onClose, onUpdateFromCty, onUpdateFromQRZ className="flex w-full items-center gap-2 px-3 py-1.5 text-left hover:bg-accent/50" onClick={() => { onBulkEdit(menu.ids); onClose(); }} > - + {t('qctx.bulkEdit', { n })} @@ -133,7 +133,7 @@ export function QSOContextMenu({ menu, onClose, onUpdateFromCty, onUpdateFromQRZ className="flex w-full items-center gap-2 px-3 py-1.5 text-left hover:bg-accent/50" onClick={() => { onExportSelected(menu.ids); onClose(); }} > - + {t('qctx.exportSelectedAdif', { n })} )} @@ -142,7 +142,7 @@ export function QSOContextMenu({ menu, onClose, onUpdateFromCty, onUpdateFromQRZ className="flex w-full items-center gap-2 px-3 py-1.5 text-left hover:bg-accent/50" onClick={() => { onExportFiltered(); onClose(); }} > - + {t('qctx.exportFilteredAdif')} )} @@ -151,7 +151,7 @@ export function QSOContextMenu({ menu, onClose, onUpdateFromCty, onUpdateFromQRZ className="flex w-full items-center gap-2 px-3 py-1.5 text-left hover:bg-accent/50" onClick={() => { onExportCabrilloSelected(menu.ids); onClose(); }} > - + {t('qctx.exportSelectedCabrillo', { n })} )} @@ -160,7 +160,7 @@ export function QSOContextMenu({ menu, onClose, onUpdateFromCty, onUpdateFromQRZ className="flex w-full items-center gap-2 px-3 py-1.5 text-left hover:bg-accent/50" onClick={() => { onExportCabrilloFiltered(); onClose(); }} > - + {t('qctx.exportFilteredCabrillo')} )} @@ -176,7 +176,7 @@ export function QSOContextMenu({ menu, onClose, onUpdateFromCty, onUpdateFromQRZ className="flex w-full items-center gap-2 px-3 py-1.5 text-left hover:bg-accent/50" onClick={() => { onSendTo(target.service, menu.ids); onClose(); }} > - + {t('qctx.sendTo', { name: target.name })} ))} @@ -187,7 +187,7 @@ export function QSOContextMenu({ menu, onClose, onUpdateFromCty, onUpdateFromQRZ <>
{rxList.length === 0 && ( -
+
No antennas reported yet — make sure the FlexRadio is connected (CAT interface), then reopen this panel.
)} @@ -1474,7 +1537,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan disabled={!current} /> {current?.is_active && ( - + {t('prof.active')} )} @@ -1577,7 +1640,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan {testing ? t('lk.testing') : t('lk.test')} -
@@ -2143,7 +2206,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
{ubTest.msg} @@ -2278,7 +2341,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
{rotatorTest.msg} @@ -2336,7 +2399,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan Icom CI-V keys CW through the rig's own keyer over the existing CAT connection (command 0x17) — it reuses the CAT COM port set in Settings → CAT, so there's nothing else to wire up here. Put the rig in CW mode. Weight, ratio, sidetone, paddle mode… are configured on the radio; only the speed is set from here (the rig's KEY SPEED).

{(!catCfg.enabled || catCfg.backend !== 'icom') && ( -

+

Your CAT backend is set to {catCfg.enabled ? (catCfg.backend || 'none') : 'disabled'}. Icom CI-V CW needs the CAT backend set to Icom and connected — change it under Settings → CAT interface, otherwise sending CW will fail. @@ -2536,16 +2599,16 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan

{t('chp.dateUtc')}{t('chp.band')}{t('chp.mode')}RST txRST rxQSL{t('chp.dateUtc')}{t('chp.band')}{t('chp.mode')}RST txRST rxQSL
{fmtDateTime(e.qso_date)} {e.band} - {e.mode} + {e.mode} {e.rst_sent ?? ''} {e.rst_rcvd ?? ''} {e.lotw_rcvd === 'Y' && ( - L + L )} {e.qsl_rcvd === 'Y' && ( - B + B )}
toggle(q.id)} /> {fmtDate(q.qso_date)} - {i === 0 && {t('dup.keep')}} + {i === 0 && {t('dup.keep')}} {fmtFreq(q.freq_hz)} {q.rst_sent || '—'}/{q.rst_rcvd || '—'}{c.mode} {c.country} - {c.new_dxcc ? {t('qslm.newDxcc')} - : c.new_band ? {t('qslm.newBand')} - : c.new_slot ? {t('qslm.newSlot')} + {c.new_dxcc ? {t('qslm.newDxcc')} + : c.new_band ? {t('qslm.newBand')} + : c.new_slot ? {t('qslm.newSlot')} : }
+ {test?.msg}
{s.name} {isMaster && s.enabled && ( - MASTER + MASTER )} {s.host}:{s.port} {state.toUpperCase()} @@ -2555,7 +2618,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
{state === 'connected' || state === 'connecting' || state === 'reconnecting' ? ( - ) : ( @@ -2874,8 +2937,8 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
{backupResult.msg}
@@ -3083,7 +3146,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan {qrzTesting ? t('es.testing') : t('es.testConn')} {qrzTest && ( - + {qrzTest.msg} )} @@ -3147,7 +3210,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan {clublogTesting ? t('es.testing') : t('es.testConn')} {clublogTest && ( - + {clublogTest.msg} )} @@ -3206,7 +3269,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan {hrdlogTesting ? t('es.testing') : t('es.testConn')} {hrdlogTest && ( - + {hrdlogTest.msg} )} @@ -3272,7 +3335,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan {eqslTesting ? t('es.testing') : t('es.testConn')} {eqslTest && ( - + {eqslTest.msg} )} @@ -3389,7 +3452,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan {lotwTesting ? t('es.testing') : t('es.testConn')} {lotwTest && ( - + {lotwTest.msg} )} @@ -3423,7 +3486,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
{potaResult && (
+ potaResult.ok ? 'border-success-border bg-success-muted text-success-muted-foreground' : 'border-destructive/30 bg-destructive/10 text-destructive')}> {potaResult.msg}
)} @@ -3514,14 +3577,14 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan }}> {t('db.saveSwitch')} - {restartMsg && {restartMsg}} + {restartMsg && {restartMsg}} {/* Active-backend status: confirms what OpsLog actually opened at launch. */} {backendStatus && (
{backendStatus.fallback ? ( -
+
{t('db.fallback')}
{backendStatus.error}
@@ -3549,7 +3612,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
{dbSettings.path || '—'} {dbSettings.is_custom - ? {t('db.customLoc')} + ? {t('db.customLoc')} : {t('db.default')}}
{t('db.defaultLabel')} {dbSettings.default_path}
@@ -3563,7 +3626,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
{dbMsg && ( -
+
{dbMsg}
@@ -3826,6 +3889,8 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
+ +