feat: stats timeline, band-map colours, frameless window, and four fixes

Statistics — the activity chart ignored the period selector: picking a year up
top left it showing the last 7 days, two controls contradicting each other. The
buttons were four fixed rolling windows anchored on the newest QSO, not
granularities. They now choose the BUCKET SIZE over the selected period, with an
Auto default and a step-up when a bucket would be too fine (the backend drops a
series past 750 buckets rather than ship 6000 unreadable bars). The hour-of-day
histogram covered a single day, too little to read anything from; it moves to
its own card with a weekday view, over the whole period.

Band map — the CW/data/phone sub-bands were shaded with the STATUS tokens, the
same amber that means "new band" on the pills drawn on top of them. A sub-band
is an identity, not a state: categorical hues now, validated in both themes
(violet failed on the dark steps — 1.9 ΔE from blue for a protan reader) and
added to the legend, since identity must never be colour-alone.

Frameless window — the OS title bar was a dead 32px band above a window that
already has its own. Minimise/maximise/close move into the app header, which
carries the drag region and double-click-to-maximise. The drag opt-out is by
ROLE in CSS, so a future button in that bar keeps working without anyone
remembering the rule.

Fixes:
- Saving settings froze the window while a device was slow: restarting a link
  waits for its poll goroutine, which can sit 45 s inside an uninterruptible COM
  Connect when another program holds the rig. The restart is now off the UI
  path; a slow one is logged.
- Multi-screen: a MAXIMISED window was never repositioned, so it came back on
  the primary screen every launch. The corner is now recorded even when
  maximised (it names the monitor) and re-applied while still hidden.
- The Callsign field is marked out at rest — operators were typing the call into
  Name, which sat beside it and looked identical.
- QSL dates get a real picker (native, for the OS calendar and locale order); a
  malformed old value stays in a text box rather than vanishing behind an empty
  one.

Also: a Support OpsLog entry in Help, and a WinKeyer protocol trace. The WK2
report (one element then a ten-second pause, sidetone that will not switch off)
is NOT fixed here: the WK1/WK2/WK3 command sets differ and a guessed fix would
break the operators it works for today. The trace logs every byte with the
command name and spells out the firmware family, which is what will settle it.
This commit is contained in:
2026-07-27 20:50:44 +02:00
parent 2b5c195ab4
commit 816a727e88
16 changed files with 668 additions and 79 deletions
+8 -8
View File
@@ -40,7 +40,7 @@ const en: Dict = {
'cwd.tipOff': 'CW decoder · click to enable (decodes RX audio in CW mode)',
'tools.net': 'NET Control', 'tools.alerts': 'Alert management…', 'tools.contest': 'Contest mode',
'alert.tuneHint': 'Click to tune the rig to this spot (freq + mode) and fill the call', 'alert.dismiss': 'Dismiss', 'alert.pending': '{n} recent spot alert(s) — click to view', 'alert.noneShort': 'No recent alerts', 'alert.recent': 'Recent alerts', 'alert.clear': 'Clear',
'menu.help': 'Help', 'help.about': 'About OpsLog', 'tools.duplicates': 'Find duplicates…',
'menu.help': 'Help', 'help.about': 'About OpsLog', 'help.donate': '♥ Support OpsLog (donate)', 'tools.duplicates': 'Find duplicates…',
'help.sendLog': 'Send log to F4BPO', 'help.sendLogBusy': 'Sending log…',
'help.sendLogOk': 'Log sent to F4BPO — thanks, 73!', 'help.sendLogFail': 'Could not send log: {err}',
// Duplicates modal
@@ -73,7 +73,7 @@ const en: Dict = {
'stats.byOperatorSub': '“—” = logged by the station owner (no OPERATOR set)',
'stats.byStation': 'By station callsign', 'stats.byContinent': 'By continent',
'stats.topEntities': 'Top DXCC entities', 'stats.byYear': 'By year',
'stats.overTime': 'Activity over time', 'stats.overTimeSub': 'Timeline, or the last day / 7 days / 30 days / 12 months', 'stats.granTimeline': 'Timeline', 'stats.granDay': 'Day', 'stats.granWeek': 'Week', 'stats.granMonth': 'Month', 'stats.granYear': 'Year',
'stats.overTime': 'Activity over time', 'stats.overTimeSub': 'QSOs per day, week, month or year over the selected period', 'stats.granTimeline': 'Timeline', 'stats.granAuto': 'Auto', 'stats.granDay': 'Per day', 'stats.granWeek': 'Per week', 'stats.granMonth': 'Per month', 'stats.granYear': 'Per year', 'stats.granTooFine': 'too fine for this period — stepped up', 'stats.rhythm': 'When you are on the air', 'stats.rhythmSub': 'Over the selected period, in UTC', 'stats.byHourOfDay': 'By hour', 'stats.byWeekday': 'By day of week',
'stats.confirmations': 'Confirmations', 'stats.paperQSL': 'Paper QSL',
'stats.byContinentSub': 'Share of the log',
@@ -154,7 +154,7 @@ const en: Dict = {
'wk.speed': 'Speed (WPM)', 'wk.farnsworth': 'Farnsworth', 'wk.leadIn': 'Lead-in (ms)', 'wk.tail': 'Tail (ms)', 'wk.keyPtt': 'Key PTT line',
'wk.invert': 'Invert keying (active-LOW) — tick this only if the rig transmits at rest / keys backwards',
'wk.serialPort': 'Serial port', 'wk.baud': 'Baud', 'wk.weight': 'Weight', 'wk.ratio': 'Ratio (33-66)', 'wk.sidetone': 'Sidetone (Hz)', 'wk.paddleMode': 'Paddle mode',
'wk.swapPaddles': 'Swap paddles', 'wk.autospace': 'Auto-space', 'wk.keyPttShort': 'Key PTT', 'wk.serialEcho': 'Serial echo',
'wk.swapPaddles': 'Swap paddles', 'wk.autospace': 'Auto-space', 'wk.keyPttShort': 'Key PTT', 'wk.serialEcho': 'Serial echo', 'wk.trace': 'Log the keyer protocol (diagnostic)', 'wk.traceHint': 'Writes every byte exchanged with the keyer to the diagnostic log. Turn it on, reproduce the problem, then send the log — the WK1/WK2/WK3 command sets differ and this shows which command your firmware read differently. Off again at the next start.',
'wk.noPorts': 'No ports found', 'wk.reloadPorts': 'Reload ports', 'wk.comPort': '— COM port —',
'wk.macroTitle': 'CW message macros (F1…)', 'wk.macroVars': 'Use variables:', 'wk.macroCut': '(cut numbers: 9→N, 0→T).', 'wk.macroMyCall': 'my call', 'wk.macroHisCall': 'his call', 'wk.macroLog': 'log the QSO when the macro is sent', 'wk.label': 'Label',
'sec.relayauto': 'Relay auto-control',
@@ -320,7 +320,7 @@ const en: Dict = {
'bmp.statusWorked': 'Worked (this band + mode already in log)', 'bmp.statusUnresolved': 'Entity not resolved', 'bmp.bandMap': 'Band map', 'bmp.notConfigured': 'Not configured for {band}.',
'bmp.map': 'Map', 'bmp.zoomOut': 'Zoom out', 'bmp.zoomIn': 'Zoom in', 'bmp.scrollToRig': 'Scroll to current rig frequency', 'bmp.moveLeft': 'Move band map to the left', 'bmp.moveRight': 'Move band map to the right', 'bmp.hide': 'Hide band map',
'bmp.bandsLabel': 'Bands:', 'bmp.fit': 'FIT', 'bmp.hideFt': 'Hide FTx', 'bmp.hideFtTitle': 'Hide all digital (FT8/FT4/JS8/…) spots on every band map', 'bmp.fitBand': 'Fit to band', 'bmp.fitTitle': 'Size each band map to show the whole band edge-to-edge',
'bmp.legendNewDxcc': 'New DXCC', 'bmp.legendNewBand': 'New band', 'bmp.legendNewSlot': 'New slot (mode)', 'bmp.legendWorked': 'Worked', 'bmp.footerHint': 'scroll · ctrl+wheel = zoom · ◎ = jump to rig', 'bmp.spotsHidden': '{n} FT8/FT4 spots hidden — top {max} kept (CW/SSB all shown)',
'bmp.legendNewDxcc': 'New DXCC', 'bmp.legendNewBand': 'New band', 'bmp.legendNewSlot': 'New slot (mode)', 'bmp.legendWorked': 'Worked', 'bmp.legendCW': 'CW', 'bmp.legendData': 'Data', 'bmp.legendPhone': 'Phone', 'bmp.footerHint': 'scroll · ctrl+wheel = zoom · ◎ = jump to rig', 'bmp.spotsHidden': '{n} FT8/FT4 spots hidden — top {max} kept (CW/SSB all shown)',
'frm.welcome': 'Welcome to OpsLog', 'frm.intro': 'Set up your station to start logging. These fields stamp every QSO and can be changed later in Preferences → Station Information (and per profile).',
'frm.callsign': 'Callsign', 'frm.locator': 'Locator', 'frm.operator': 'Operator', 'frm.operatorPh': 'same as callsign', 'frm.owner': 'Owner', 'frm.ownerPh': 'station owner callsign', 'frm.name': 'Name', 'frm.namePh': 'your first name',
'frm.awardRefs': 'Award reference lists', 'frm.awardRefsHint': 'IOTA · POTA · WWFF · SOTA — names & totals for those awards (optional, can take a minute).', 'frm.downloading': 'Downloading…', 'frm.reDownload': 'Re-download', 'frm.download': 'Download', 'frm.required': 'Callsign and locator are required.', 'frm.saving': 'Saving…', 'frm.startLogging': 'Start logging',
@@ -438,7 +438,7 @@ const fr: Dict = {
'cwd.tipOff': 'Décodeur CW · clic pour activer (décode laudio RX en mode CW)',
'tools.net': 'Contrôle de NET', 'tools.alerts': 'Gestion des alertes…', 'tools.contest': 'Mode contest',
'alert.tuneHint': 'Cliquer pour accorder la radio sur ce spot (fréq + mode) et remplir l\'indicatif', 'alert.dismiss': 'Fermer', 'alert.pending': '{n} alerte(s) de spot récente(s) — cliquer pour voir', 'alert.noneShort': 'Aucune alerte récente', 'alert.recent': 'Alertes récentes', 'alert.clear': 'Effacer',
'menu.help': 'Aide', 'help.about': 'À propos d\'OpsLog', 'tools.duplicates': 'Trouver les doublons…',
'menu.help': 'Aide', 'help.about': 'À propos d\'OpsLog', 'help.donate': '♥ Soutenir OpsLog (don)', 'tools.duplicates': 'Trouver les doublons…',
'help.sendLog': 'Envoyer le journal à F4BPO', 'help.sendLogBusy': 'Envoi du journal…',
'help.sendLogOk': 'Journal envoyé à F4BPO — merci, 73 !', 'help.sendLogFail': 'Échec de l\'envoi du journal : {err}',
'dup.title': 'Trouver les doublons', 'dup.scanning': 'Analyse du journal…', 'dup.none': 'Aucun doublon trouvé. 🎉',
@@ -468,7 +468,7 @@ const fr: Dict = {
'stats.byOperatorSub': '« — » = loggé par le titulaire (pas dOPERATOR renseigné)',
'stats.byStation': 'Par indicatif de station', 'stats.byContinent': 'Par continent',
'stats.topEntities': 'Top entités DXCC', 'stats.byYear': 'Par année',
'stats.overTime': 'Activité dans le temps', 'stats.overTimeSub': 'Chronologie, ou les derniers jour / 7 jours / 30 jours / 12 mois', 'stats.granTimeline': 'Chronologie', 'stats.granDay': 'Jour', 'stats.granWeek': 'Semaine', 'stats.granMonth': 'Mois', 'stats.granYear': 'Année',
'stats.overTime': 'Activité dans le temps', 'stats.overTimeSub': 'QSO par jour, semaine, mois ou année sur la période choisie', 'stats.granTimeline': 'Chronologie', 'stats.granAuto': 'Auto', 'stats.granDay': 'Par jour', 'stats.granWeek': 'Par semaine', 'stats.granMonth': 'Par mois', 'stats.granYear': 'Par année', 'stats.granTooFine': 'trop fin pour cette période — regroupé', 'stats.rhythm': 'Quand vous trafiquez', 'stats.rhythmSub': 'Sur la période choisie, en UTC', 'stats.byHourOfDay': 'Par heure', 'stats.byWeekday': 'Par jour de semaine',
'stats.confirmations': 'Confirmations', 'stats.paperQSL': 'QSL papier',
'stats.byContinentSub': 'Part du journal',
@@ -548,7 +548,7 @@ const fr: Dict = {
'wk.speed': 'Vitesse (WPM)', 'wk.farnsworth': 'Farnsworth', 'wk.leadIn': 'Lead-in (ms)', 'wk.tail': 'Tail (ms)', 'wk.keyPtt': 'Manipuler la ligne PTT',
'wk.invert': "Inverser le keying (actif-BAS) — à cocher seulement si la radio émet au repos / manipule à l'envers",
'wk.serialPort': 'Port série', 'wk.baud': 'Baud', 'wk.weight': 'Poids', 'wk.ratio': 'Ratio (33-66)', 'wk.sidetone': 'Sidetone (Hz)', 'wk.paddleMode': 'Mode paddle',
'wk.swapPaddles': 'Inverser les paddles', 'wk.autospace': 'Auto-espacement', 'wk.keyPttShort': 'Manipuler PTT', 'wk.serialEcho': 'Écho série',
'wk.swapPaddles': 'Inverser les paddles', 'wk.autospace': 'Auto-espacement', 'wk.keyPttShort': 'Manipuler PTT', 'wk.serialEcho': 'Écho série', 'wk.trace': 'Journaliser le protocole du keyer (diagnostic)', 'wk.traceHint': "Écrit dans le journal chaque octet échangé avec le keyer. Activez, reproduisez le problème, puis envoyez le journal — les jeux de commandes WK1/WK2/WK3 diffèrent et c'est ce qui montre quelle commande votre microprogramme a lue autrement. Désactivé au prochain démarrage.",
'wk.noPorts': 'Aucun port trouvé', 'wk.reloadPorts': 'Recharger les ports', 'wk.comPort': '— port COM —',
'wk.macroTitle': 'Macros de messages CW (F1…)', 'wk.macroVars': 'Variables :', 'wk.macroCut': '(chiffres abrégés : 9→N, 0→T).', 'wk.macroMyCall': 'mon indicatif', 'wk.macroHisCall': 'son indicatif', 'wk.macroLog': 'logue le QSO quand la macro est envoyée', 'wk.label': 'Libellé',
'sec.relayauto': 'Relais automatiques',
@@ -699,7 +699,7 @@ const fr: Dict = {
'bmp.statusWorked': 'Contacté (cette bande + mode déjà au log)', 'bmp.statusUnresolved': 'Entité non résolue', 'bmp.bandMap': 'Carte de bande', 'bmp.notConfigured': 'Non configurée pour {band}.',
'bmp.map': 'Carte', 'bmp.zoomOut': 'Dézoomer', 'bmp.zoomIn': 'Zoomer', 'bmp.scrollToRig': 'Aller à la fréquence actuelle du poste', 'bmp.moveLeft': 'Déplacer la carte de bande à gauche', 'bmp.moveRight': 'Déplacer la carte de bande à droite', 'bmp.hide': 'Masquer la carte de bande',
'bmp.bandsLabel': 'Bandes :', 'bmp.fit': 'FIT', 'bmp.hideFt': 'Masquer FTx', 'bmp.hideFtTitle': 'Masquer tous les spots numériques (FT8/FT4/JS8/…) sur toutes les cartes', 'bmp.fitBand': 'Ajuster à la bande', 'bmp.fitTitle': 'Dimensionner chaque carte pour afficher toute la bande',
'bmp.legendNewDxcc': 'Nouveau DXCC', 'bmp.legendNewBand': 'Nouvelle bande', 'bmp.legendNewSlot': 'Nouveau mode', 'bmp.legendWorked': 'Contacté', 'bmp.footerHint': 'défiler · ctrl+molette = zoom · ◎ = aller au poste', 'bmp.spotsHidden': '{n} spots FT8/FT4 masqués — {max} meilleurs conservés (CW/SSB tous affichés)',
'bmp.legendNewDxcc': 'Nouveau DXCC', 'bmp.legendNewBand': 'Nouvelle bande', 'bmp.legendNewSlot': 'Nouveau mode', 'bmp.legendWorked': 'Contacté', 'bmp.legendCW': 'CW', 'bmp.legendData': 'Numérique', 'bmp.legendPhone': 'Phonie', 'bmp.footerHint': 'défiler · ctrl+molette = zoom · ◎ = aller au poste', 'bmp.spotsHidden': '{n} spots FT8/FT4 masqués — {max} meilleurs conservés (CW/SSB tous affichés)',
'frm.welcome': 'Bienvenue dans OpsLog', 'frm.intro': 'Configure ta station pour commencer à logger. Ces champs sont inscrits sur chaque QSO et peuvent être modifiés plus tard dans Préférences → Informations station (et par profil).',
'frm.callsign': 'Indicatif', 'frm.locator': 'Locator', 'frm.operator': 'Opérateur', 'frm.operatorPh': "identique à l'indicatif", 'frm.owner': 'Propriétaire', 'frm.ownerPh': 'indicatif du propriétaire de la station', 'frm.name': 'Nom', 'frm.namePh': 'ton prénom',
'frm.awardRefs': 'Listes de références des diplômes', 'frm.awardRefsHint': 'IOTA · POTA · WWFF · SOTA — noms et totaux pour ces diplômes (optionnel, peut prendre une minute).', 'frm.downloading': 'Téléchargement…', 'frm.reDownload': 'Retélécharger', 'frm.download': 'Télécharger', 'frm.required': "L'indicatif et le locator sont obligatoires.", 'frm.saving': 'Enregistrement…', 'frm.startLogging': 'Commencer à logger',