feat: never drop cluster spots + theme-aware spot badges + FR settings
Backend: replace the bounded cluster-event channel (which dropped spots under RBN bursts once enrichment/UI fell behind) with an unbounded clusterQueue (slice + sync.Cond). The socket-read goroutine still never blocks, but a burst now grows the backlog and drains instead of losing spots. Head compaction keeps a sustained burst from growing the backing array without bound. ClusterGrid: drop all hard-coded light-theme hex colours (Time, Country, Spotter, Comment, POTA, band/mode fills) for semantic CSS-var tokens so they read correctly on every theme. Replace the heavy full-cell band/mode fills with small rounded pills (cellChip). Only NEW MODE pills the mode cell — NEW SLOT (band and mode each worked, just not together) no longer highlights the mode cell, which wrongly implied the mode was new. App: stage incoming spots ~50ms and resolve their slot status before inserting, so a row paints with its badge already on instead of flashing plain text then flipping to a pill. Self-spot toast still fires per spot. i18n: translate the General settings that were still English — telemetry and live-status toggles, the Main view pane pickers and their options. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
@@ -540,6 +540,7 @@ function AutostartPanelComponent() {
|
||||
// (a random install ID + version + OS, sent once a day). Real component so it
|
||||
// can own its state; embedded inside GeneralPanel.
|
||||
function TelemetryToggle() {
|
||||
const { t } = useI18n();
|
||||
const [on, setOn] = useState(true);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
useEffect(() => {
|
||||
@@ -549,8 +550,8 @@ function TelemetryToggle() {
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<Checkbox checked={on} disabled={!loaded}
|
||||
onCheckedChange={(c) => { const v = !!c; setOn(v); SetTelemetryEnabled(v).catch(() => {}); }} />
|
||||
Send anonymous usage statistics
|
||||
<span className="text-xs text-muted-foreground">(install ID + version + OS, once a day — no callsign or QSO data)</span>
|
||||
{t('settings.telemetry')}
|
||||
<span className="text-xs text-muted-foreground">({t('settings.telemetryHint')})</span>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
@@ -560,6 +561,7 @@ function TelemetryToggle() {
|
||||
// events — a small web script on your server renders it for the QRZ page. Only
|
||||
// useful on a MySQL logbook. Self-contained component (owns its async state).
|
||||
function LiveStatusToggle() {
|
||||
const { t } = useI18n();
|
||||
const [on, setOn] = useState(false);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
useEffect(() => {
|
||||
@@ -569,7 +571,7 @@ function LiveStatusToggle() {
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<Checkbox checked={on} disabled={!loaded}
|
||||
onCheckedChange={(c) => { const v = !!c; setOn(v); SetLiveStatusEnabled(v).catch(() => {}); }} />
|
||||
Publish live operator status <span className="text-xs text-muted-foreground">(multi-op on shared MySQL — feeds a QRZ live page)</span>
|
||||
{t('settings.liveStatus')} <span className="text-xs text-muted-foreground">({t('settings.liveStatusHint')})</span>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
@@ -578,25 +580,20 @@ function LiveStatusToggle() {
|
||||
// panes show, independently: the great-circle map, the locator street map, the
|
||||
// cluster grid or the worked-before grid. Per-profile (stored via SetUIPref,
|
||||
// which is profile-prefixed). Self-contained so it owns its async-loaded state.
|
||||
const MAIN_PANE_OPTIONS: { value: string; label: string }[] = [
|
||||
{ value: 'map1', label: 'Map — great-circle + beam' },
|
||||
{ value: 'map2', label: 'Map — locator (street)' },
|
||||
{ value: 'cluster', label: 'Cluster spots' },
|
||||
{ value: 'worked', label: 'Worked before' },
|
||||
{ value: 'recent', label: 'Recent QSOs' },
|
||||
{ value: 'netcontrol', label: 'Net control' },
|
||||
];
|
||||
const MAIN_PANE_VALUES = ['map1', 'map2', 'cluster', 'worked', 'recent', 'netcontrol'];
|
||||
function MainViewPanes({ onChanged, flexAvailable, icomAvailable }: { onChanged?: (side: 'left' | 'right', value: string) => void; flexAvailable?: boolean; icomAvailable?: boolean }) {
|
||||
const { t } = useI18n();
|
||||
const [left, setLeft] = useState('map1');
|
||||
const [right, setRight] = useState('map2');
|
||||
// Radio-control panes are only offered when that CAT backend is active. Sorted A→Z.
|
||||
const options = [
|
||||
...MAIN_PANE_OPTIONS,
|
||||
...(flexAvailable ? [{ value: 'flex', label: 'FlexRadio controls' }] : []),
|
||||
...(icomAvailable ? [{ value: 'icom', label: 'Icom console' }] : []),
|
||||
].sort((a, b) => a.label.localeCompare(b.label));
|
||||
...MAIN_PANE_VALUES,
|
||||
...(flexAvailable ? ['flex'] : []),
|
||||
...(icomAvailable ? ['icom'] : []),
|
||||
].map((value) => ({ value, label: t(`settings.pane.${value}`) }))
|
||||
.sort((a, b) => a.label.localeCompare(b.label));
|
||||
useEffect(() => {
|
||||
const valid = (v: string) => v === 'flex' || v === 'icom' || MAIN_PANE_OPTIONS.some((o) => o.value === v);
|
||||
const valid = (v: string) => v === 'flex' || v === 'icom' || MAIN_PANE_VALUES.includes(v);
|
||||
Promise.all([GetUIPref('mainPaneLeft').catch(() => ''), GetUIPref('mainPaneRight').catch(() => '')])
|
||||
.then(([l, r]) => { if (valid(l)) setLeft(l); if (valid(r)) setRight(r); });
|
||||
}, []);
|
||||
@@ -609,11 +606,11 @@ function MainViewPanes({ onChanged, flexAvailable, icomAvailable }: { onChanged?
|
||||
};
|
||||
return (
|
||||
<div className="border-t border-border/60 pt-4 space-y-2">
|
||||
<h4 className="text-sm font-semibold text-foreground">Main view</h4>
|
||||
<p className="text-xs text-muted-foreground">Choose what the Main tab shows on each side (per profile).</p>
|
||||
<h4 className="text-sm font-semibold text-foreground">{t('settings.mainView')}</h4>
|
||||
<p className="text-xs text-muted-foreground">{t('settings.mainViewHint')}</p>
|
||||
<div className="grid grid-cols-2 gap-3 max-w-xl">
|
||||
<label className="flex flex-col gap-1 text-xs">
|
||||
<span className="text-muted-foreground">Left pane</span>
|
||||
<span className="text-muted-foreground">{t('settings.leftPane')}</span>
|
||||
<Select value={left} onValueChange={(v) => pick('left', v)}>
|
||||
<SelectTrigger className="h-8 text-xs"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -622,7 +619,7 @@ function MainViewPanes({ onChanged, flexAvailable, icomAvailable }: { onChanged?
|
||||
</Select>
|
||||
</label>
|
||||
<label className="flex flex-col gap-1 text-xs">
|
||||
<span className="text-muted-foreground">Right pane</span>
|
||||
<span className="text-muted-foreground">{t('settings.rightPane')}</span>
|
||||
<Select value={right} onValueChange={(v) => pick('right', v)}>
|
||||
<SelectTrigger className="h-8 text-xs"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
Reference in New Issue
Block a user