fix(ui): one section name, a chosen digital row, no MQTT chip

Three things reported together.

Preferences said the section name twice: a small line above every panel
repeated the heading right underneath it — "GENERAL" over "General" —
while the sidebar next to them already shows which section is open,
highlighted. Two lines and a highlight for one fact; the small line goes.

The band matrix opens on the digital row the operator chooses. Its
digital row has always rotated — DIGI, then each digital mode in their
own list — but it always STARTED on DIGI, so somebody who only works FT8
clicked through to their own mode on every callsign. Settings ▸ General
now says where the rotation starts, and the dropdown offers exactly the
modes the matrix rotates through (the phone-mode rule is now shared
rather than copied, so the two cannot drift). DIGI stays the default: it
is the right answer for anyone working several digital modes.

And the MQTT chip is gone from the status bar. That is the name of a
message protocol, not of anything an operator has — a chip in the status
bar has to say what it is about, and this one told nobody anything. What
it carried is shown in the Chase New panel, which is the place that uses
it; its poller went with it.
This commit is contained in:
2026-09-08 00:10:06 +02:00
parent 5b7469ae44
commit cdd91ab7e6
6 changed files with 67 additions and 40 deletions
+7 -31
View File
@@ -52,7 +52,7 @@ import {
ReportLiveActivity, LiveLastQSOAgeSec,
GetAmpStatuses, AmpOperate,
GetFlexState, FlexAmpOperate,
GetPSKReporterStatus, GetLiveOpenings, GetChaseNew,
GetLiveOpenings, GetChaseNew,
QSLViaRepairStatus, RepairQSLVia, DismissQSLViaRepair,
GetAutoCallStatus, SetAutoCall, SetAutoCallOnly, TakeAutoCallTarget, HaltAutoCall, WatchlistEntries,
} from '../wailsjs/go/main/App';
@@ -2359,16 +2359,6 @@ export default function App() {
return () => window.clearInterval(t);
}, []);
// PSK Reporter feed, for the status-bar chip. Polled slowly: the chip only
// says up or down, and the count behind it is a tooltip.
const [pskr, setPskr] = useState<any>(null);
useEffect(() => {
const load = () => { GetPSKReporterStatus().then(setPskr).catch(() => {}); };
load();
const t = window.setInterval(load, 10000);
return () => window.clearInterval(t);
}, []);
// "ON AIR" status-bar badge: mirrors the multi-op live status this operator
// publishes — online (blinking) when a QSO was logged in the last 5 min, else
// offline. Publishing is always on for a shared MySQL logbook (no user toggle:
@@ -9136,26 +9126,12 @@ export default function App() {
</button>
);
})}
{/* PSK Reporter, next to the hardware chips because it is the same
kind of fact: a link that is either up or it is not. Shown ONLY
when the opening watch is on a permanently grey chip for a
feature nobody enabled is clutter, and the bar is 28 px.
The decode count is in the tooltip rather than the chip: it moves
several times a second on an open band, and a number flickering in
the corner of the eye is not information, it is a distraction. */}
{pskr?.running && (
<button
type="button"
title={t('pskr.tip', { n: pskr.received ?? 0, bands: (pskr.bands ?? []).join(' ') })}
onClick={() => { setSettingsSection('cluster'); setShowSettings(true); }}
className="inline-flex items-center gap-1.5 px-2 h-5 rounded border text-[11px] transition-colors border-border hover:bg-muted cursor-pointer shrink-0"
>
<span className={cn('size-2 rounded-full',
(pskr.received ?? 0) > 0 ? 'bg-success' : 'bg-warning')} />
MQTT
</button>
)}
{/* The PSK Reporter chip used to sit here, labelled MQTT. That is
the name of a message protocol, not of anything an operator has:
a chip in the status bar has to say what it is about, and this
one told nobody anything. The state it carried the openings
feed up or down, and how many reports have arrived is shown in
the Chase New panel, which is the place that uses it. */}
{/* ON AIR badge: "did I log a QSO in the last 5 min" meaningful on ANY
logbook backend (only the live_status PUBLISHING is MySQL-specific),
so it is always shown. Gating it on MySQL made it vanish for
+15 -1
View File
@@ -60,7 +60,11 @@ const DEFAULT_BANDS: { tag: string; label: string }[] = [
];
const CLASSES = ['PH', 'CW', 'DIG'] as const;
const PHONE_MODES = new Set(['SSB','USB','LSB','AM','FM','DIGITALVOICE','PHONE']);
export const PHONE_MODES = new Set(['SSB','USB','LSB','AM','FM','DIGITALVOICE','PHONE']);
// Which digital row the matrix opens on. Empty = DIG, the group of them all.
// Set in Settings ▸ General; see the rotation below.
export const MATRIX_DIGI_KEY = 'opslog.matrixDigiMode';
function classMatchesMode(cls: string, mode: string): boolean {
const u = (mode || '').toUpperCase();
if (cls === 'PH') return PHONE_MODES.has(u);
@@ -143,7 +147,17 @@ export function BandSlotGrid({ wb, busy, currentBand, currentMode, bands, modes,
.filter((m) => m !== '' && m !== 'CW' && !PHONE_MODES.has(m)),
[modes],
);
// Where the rotation STARTS. An operator who only ever works FT8 was shown
// "DIG" every time and had to click to the mode they actually use, on every
// callsign — so the row they want is the one it opens on. Empty (the default)
// keeps DIG, which is right for anyone working several digital modes.
const [digIdx, setDigIdx] = useState(0); // 0 = the DIG group itself
useEffect(() => {
const want = (localStorage.getItem(MATRIX_DIGI_KEY) || '').toUpperCase().trim();
if (!want) { setDigIdx(0); return; }
const i = digModes.indexOf(want);
setDigIdx(i >= 0 ? i + 1 : 0);
}, [digModes]);
// A shorter mode list (the operator edited it) must not strand the rotation
// on a row that no longer exists.
const digPos = digModes.length ? digIdx % (digModes.length + 1) : 0;
+34 -4
View File
@@ -95,6 +95,7 @@ import { AppearancePanel } from '@/components/AppearancePanel';
import { UDPIntegrationsPanel } from '@/components/UDPIntegrationsPanel';
import { loadClusterMacros, saveClusterMacros, type ClusterMacro } from '@/lib/clusterMacros';
import { CLUSTER_PRESETS } from '@/lib/clusterPresets';
import { MATRIX_DIGI_KEY, PHONE_MODES as MATRIX_PHONE_MODES } from '@/components/BandSlotGrid';
type LookupSettings = LookupSettingsForm;
type StationSettings = StationSettingsForm;
@@ -2007,6 +2008,17 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
const [startEqEnd, setStartEqEnd] = useState(() => localStorage.getItem('opslog.startEqualsEnd') === '1');
const [lookupOnBlur, setLookupOnBlur] = useState(() => localStorage.getItem('opslog.lookupOnBlur') === '1');
const [groupDigital, setGroupDigital] = useState(() => localStorage.getItem('opslog.groupDigitalSlots') === '1');
// The digital row the band matrix opens on: '' = DIG, the group of them all.
const [matrixDigi, setMatrixDigi] = useState(() => localStorage.getItem(MATRIX_DIGI_KEY) || '');
// The operator's own digital modes, which is what the matrix rotates through
// — the same rule it uses: everything in their mode list that is neither CW
// nor a phone mode.
const digitalModeNames = useMemo(
() => (lists.modes ?? [])
.map((m: any) => String(m?.name ?? '').toUpperCase().trim())
.filter((m) => m && m !== 'CW' && !MATRIX_PHONE_MODES.has(m)),
[lists.modes],
);
const [milesUnit, setMilesUnit] = useState(() => localStorage.getItem('opslog.distanceMiles') === '1');
const [region, setRegion] = useState<IaruRegion>(() => iaruRegion());
const [clusterWorkedSameSlot, setClusterWorkedSameSlot] = useState(() => localStorage.getItem('opslog.clusterWorkedSameSlot') === '1');
@@ -2812,7 +2824,6 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
}
}
const breadcrumb = useMemo(() => { const k = SECTION_KEY[selected]; return k ? t(k) : (SECTION_LABELS[selected] ?? selected); }, [selected, t]);
// === Section content renderers ===
@@ -8106,6 +8117,24 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
<Checkbox checked={groupDigital} onCheckedChange={(c) => { const v = !!c; setGroupDigital(v); writeUiPref('opslog.groupDigitalSlots', v ? '1' : '0'); }} />
<span title={t('gen.groupDigitalHint')}>{t('gen.groupDigital')}</span>
</label>
{/* Which digital row the band matrix opens on. An operator who works
only FT8 was shown DIG every time and had to click through to the
mode they actually use, on every callsign. The row still rotates
this only says where it starts. */}
<div className="flex items-center gap-2 text-sm">
<span title={t('gen.matrixDigiHint')}>{t('gen.matrixDigi')}</span>
<Select value={matrixDigi || '_'} onValueChange={(v) => {
const next = v === '_' ? '' : v;
setMatrixDigi(next);
writeUiPref(MATRIX_DIGI_KEY, next);
}}>
<SelectTrigger className="h-8 w-36"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="_">DIGI</SelectItem>
{digitalModeNames.map((m) => <SelectItem key={m} value={m}>{m}</SelectItem>)}
</SelectContent>
</Select>
</div>
<label className="flex items-center gap-2 text-sm cursor-pointer">
{/* Distances are computed in km everywhere and converted at display
time see lib/units. Changing this repaints the columns that
@@ -8878,10 +8907,11 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
</aside>
{/* Right content pane */}
{/* No breadcrumb line. It said the same word as the heading right
underneath it "GENERAL" over "General" and the sidebar
beside it already shows which section is open, highlighted. Two
lines and a highlight for one fact. */}
<div className="overflow-y-auto p-6">
<div className="text-[10px] uppercase tracking-wider text-muted-foreground mb-3 font-semibold">
{breadcrumb}
</div>
<PanelHost key={selected} render={PANELS[selected]} />
{err && (
+2 -2
View File
@@ -308,7 +308,7 @@ const en: Dict = {
'gen.startEqEnd': 'QSO start time = end time', 'gen.startEqEndHint': '(matches LoTW when you call a while)',
'gen.showQsoRate': 'Show QSO rate in the header', 'gen.showQsoRateHint': '(QSOs/hour, projected from the last 10 / 60 min)',
'gen.lookupOnBlur': 'Look up the callsign only after leaving the field', 'gen.lookupOnBlurHint': '(not while typing)', 'amp.hint': 'Configure one or several amplifiers — each panel card has a dropdown to pick which one it shows.', 'amp.linked': 'Amplifiers commanded together', 'amp.linkedHint': 'Tick the ones sharing a combiner: ON, OFF and OPERATE will act on all of them at once, since one left in STANDBY would feed power to a single input. Any amplifier left unticked keeps its own buttons. Each keeps its own meters.', 'amp.linkedSaveFirst': 'Save first — an amplifier needs an id before it can join a group.', 'amp.none': 'No amplifier configured yet.', 'amp.namePh': 'Name (e.g. SPE left)', 'amp.remove': 'Remove this amplifier', 'amp.add': 'Add amplifier', 'amp.password': 'Remote code', 'amp.passwordPh': 'blank on LAN', 'amp.passwordHint': 'PowerGenius XL only: needed when reaching the amp remotely — it then announces "AUTH" and rejects every command ("Unauthorized") until you log in. Leave blank on the local network.', 'amp.kpaBandFollow': 'Keep the amplifier on the radios band', 'amp.kpaBandFollowHint': 'Sends the band number when it changes, on the link already open — no second serial port. Turn it off if the amplifier is wired straight to the radio and takes its band from there.', 'amp.freqOut': 'Send the frequency to the amplifier (band follow)', 'amp.freqPort': 'CAT/AUX COM port', 'amp.freqBroadcast': 'Also send unprompted', 'amp.freqPollOnly': 'No — answer the amplifier only', 'amp.freqEvery': 'Yes, every {ms} ms', 'amp.freqHint': 'OpsLog pretends to be a transceiver on this second port, in Kenwood format: set the amplifier to that command set (set 5 on an Acom) at the same baud rate, and put it in OPERATE — in standby it acknowledges but does not switch band. An amplifier that POLLS (Acom) needs nothing more; one that only LISTENS to the CAT line of the radio hears nothing unless you also turn on the unprompted send.',
'gen.groupDigital': 'Group digital modes as one (DXCC-style)', 'gen.groupDigitalHint': '(matrix badges + cluster: FT8/FT4/RTTY… count as a single Digital mode; off = each digital mode is its own slot)',
'gen.matrixDigi': 'Digital row in the band matrix', 'gen.matrixDigiHint': 'Which digital row the matrix opens on. The row still rotates when you click it — this only says where it starts. DIGI is every digital mode together.', 'gen.groupDigital': 'Group digital modes as one (DXCC-style)', 'gen.groupDigitalHint': '(matrix badges + cluster: FT8/FT4/RTTY… count as a single Digital mode; off = each digital mode is its own slot)',
// Password encryption
'gen.pwEnc': 'Password encryption',
'gen.pwEncrypted': 'Passwords encrypted', 'gen.pwUnlocked': '— unlocked', 'gen.pwLocked': '— locked (unlock at launch)',
@@ -926,7 +926,7 @@ const fr: Dict = {
'gen.startEqEnd': 'Heure de début du QSO = heure de fin', 'gen.startEqEndHint': '(correspond à LoTW quand tu appelles un moment)',
'gen.showQsoRate': 'Afficher le rythme QSO dans la barre du haut', 'gen.showQsoRateHint': '(QSO/heure, projeté sur les 10 / 60 dernières min)',
'gen.lookupOnBlur': 'Rechercher l\'indicatif seulement après avoir quitté le champ', 'gen.lookupOnBlurHint': '(pas pendant la saisie)', 'amp.hint': 'Configurez un ou plusieurs amplificateurs — chaque carte de panneau a une liste déroulante pour choisir lequel afficher.', 'amp.linked': 'Amplificateurs commandés ensemble', 'amp.linkedHint': "Coche ceux qui partagent un combiner : ON, OFF et OPERATE agiront sur tous à la fois, puisqu'un ampli resté en STANDBY n'alimenterait qu'une seule entrée. Un amplificateur non coché garde ses propres boutons. Chacun garde ses mesures.", 'amp.linkedSaveFirst': "Enregistre d'abord — un amplificateur a besoin d'un identifiant pour rejoindre un groupe.", 'amp.none': 'Aucun amplificateur configuré.', 'amp.namePh': 'Nom (p. ex. SPE gauche)', 'amp.remove': 'Supprimer cet amplificateur', 'amp.add': 'Ajouter un amplificateur', 'amp.password': 'Code distant', 'amp.passwordPh': 'vide en LAN', 'amp.passwordHint': "PowerGenius XL uniquement : nécessaire pour joindre l'ampli à distance — il annonce alors « AUTH » et refuse toute commande (« Unauthorized ») tant qu'on n'est pas identifié. Laisse vide sur le réseau local.", 'amp.kpaBandFollow': "Garder l'amplificateur sur la bande de la radio", 'amp.kpaBandFollowHint': "Envoie le numéro de bande quand elle change, sur la liaison déjà ouverte — pas de second port série. À désactiver si l'amplificateur est câblé directement à la radio et prend sa bande de là.", 'amp.freqOut': "Envoyer la fréquence à l'amplificateur (suivi de bande)", 'amp.freqPort': 'Port COM CAT/AUX', 'amp.freqBroadcast': 'Envoyer aussi sans être interrogé', 'amp.freqPollOnly': "Non — répondre seulement à l'amplificateur", 'amp.freqEvery': 'Oui, toutes les {ms} ms', 'amp.freqHint': "OpsLog se fait passer pour un transceiver sur ce second port, au format Kenwood : réglez l'amplificateur sur ce jeu de commandes (le jeu 5 sur un Acom) à la même vitesse, et mettez-le en OPERATE — en veille il acquitte mais ne change pas de bande. Un amplificateur qui INTERROGE (Acom) n'a besoin de rien de plus ; un amplificateur qui se contente d'ÉCOUTER la liaison CAT de la radio n'entendra rien tant que l'envoi spontané n'est pas activé.",
'gen.groupDigital': 'Regrouper les modes digitaux en un seul (style DXCC)', 'gen.groupDigitalHint': '(badges de la matrice + cluster : FT8/FT4/RTTY… comptent comme un seul mode Digital ; décoché = chaque mode digital est un slot distinct)',
'gen.matrixDigi': 'Ligne numérique de la matrice', 'gen.matrixDigiHint': 'Sur quelle ligne numérique la matrice souvre. La ligne continue de tourner quand on clique dessus — ceci dit seulement où elle commence. DIGI, cest tous les modes numériques ensemble.', 'gen.groupDigital': 'Regrouper les modes digitaux en un seul (style DXCC)', 'gen.groupDigitalHint': '(badges de la matrice + cluster : FT8/FT4/RTTY… comptent comme un seul mode Digital ; décoché = chaque mode digital est un slot distinct)',
// Chiffrement des mots de passe
'gen.pwEnc': 'Chiffrement des mots de passe',
'gen.pwEncrypted': 'Mots de passe chiffrés', 'gen.pwUnlocked': '— déverrouillé', 'gen.pwLocked': '— verrouillé (à déverrouiller au lancement)',
+1
View File
@@ -34,6 +34,7 @@ const PORTABLE_KEYS = [
'opslog.ftMapView', 'opslog.gridMapView', 'opslog.satMapView',
'opslog.lookupOnBlur', // run the callsign lookup on blur instead of while typing
'opslog.groupDigitalSlots', // matrix + cluster: all digital modes count as ONE (DXCC-style) instead of per-mode slots
'opslog.matrixDigiMode', // band matrix: which digital row it opens on ('' = DIGI, the group)
'opslog.clusterShowFilters', // cluster filter sidebar shown (tab + Main pane)
// One imagery choice per map — world, grid squares, FT map, satellites.
'opslog.mapBasemap', 'opslog.gridMapBase', 'opslog.ftmapBase', 'opslog.satMapBase',