From 9f628083929e25033d006293c158f1464d58ffda Mon Sep 17 00:00:00 2001 From: Gregory Salaun Date: Mon, 10 Aug 2026 13:52:13 +0200 Subject: [PATCH] fix(webpub): the published page could not sort a date, and could not be unsorted parseFloat accepts a numeric PREFIX. "2026-08-10" therefore became the number 2026, every date in the same year compared equal, and since the sort is stable nothing moved: the Date column looked as though it were simply not sortable. The same trap caught every callsign starting with a digit - 8B81SU and 8P9AB both read as 8 - and the times, where "09:56" became 9. The numeric test is now anchored to the whole cell, so anything that is not entirely a number is compared as text, which is exactly right for an ISO date. Sorting had no way back either. Each row now carries the index it was published at, and a third click on a header restores that order. Headers also show an arrow: with no indicator, a column that silently refused to sort was indistinguishable from one that had sorted into the same order. Blank cells sink in both directions rather than leading the ascending sort. An empty field is missing data, not the smallest value. Tested where it can be: the page ships its own script, so a regression is silent - the table still renders, it just sorts wrongly. --- changelog.json | 6 +++-- internal/webpub/webpub.go | 44 +++++++++++++++++++++--------- internal/webpub/webpub_test.go | 49 ++++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 14 deletions(-) create mode 100644 internal/webpub/webpub_test.go diff --git a/changelog.json b/changelog.json index 01e9246..9616769 100644 --- a/changelog.json +++ b/changelog.json @@ -5,12 +5,14 @@ "en": [ "Band openings: OpsLog now tells you when 6, 4 or 2 m opens. It watches the spots already arriving from your clusters and RBN — several different stations appearing at single-hop range (500–2400 km) in the same bearing sector within a few minutes is the signature of sporadic E, and nothing else looks like it. You get one message per band per opening, naming the sector and the typical distance. An opening outside the usual season is still announced, and flagged as unusual: those are the ones worth knowing about. Nothing to configure — but the quality depends on having a cluster or RBN feed carrying VHF spots, and 2 m openings are often worked without ever being spotted.", "DX cluster, two display options (Settings › DX cluster). The first drops the colour and the badges on stations you have already worked: they stay in the list, they simply stop competing for your attention. The second colours every callsign you have not worked on this band and this mode, even when the entity itself is long since confirmed — the view for filling slots rather than chasing new ones. Both apply to the cluster list and to the band map, so the two panels always agree.", - "DX cluster, clearer at a glance. A novelty now FILLS the cell that carries it — a filled Band cell means new band, a filled Pfx cell means new prefix, a filled County cell means new county — instead of only tinting the text. A US County column joins the list, resolved offline from the ULS database. The station-not-worked-here highlight is its own NEW CALL status with its own filter chip, no longer borrowed from NEW SLOT, which means something narrower. The two display options moved from Preferences into the cluster filter panel, next to Hide worked. The Locator column is now labelled Spotter locator, because that is what a cluster line actually carries." + "DX cluster, clearer at a glance. A novelty now FILLS the cell that carries it — a filled Band cell means new band, a filled Pfx cell means new prefix, a filled County cell means new county — instead of only tinting the text. A US County column joins the list, resolved offline from the ULS database. The station-not-worked-here highlight is its own NEW CALL status with its own filter chip, no longer borrowed from NEW SLOT, which means something narrower. The two display options moved from Preferences into the cluster filter panel, next to Hide worked. The Locator column is now labelled Spotter locator, because that is what a cluster line actually carries.", + "Web publishing: the published page sorts properly. The Date column would not sort at all, and neither would callsigns starting with a digit — the script read a number from the front of the text, so every date in the same year counted as equal. A third click on a header now puts the table back in the order it was published in, and an arrow shows which way a column is pointing." ], "fr": [ "Ouvertures de bande : OpsLog te signale désormais l ouverture du 6, du 4 ou du 2 m. Il surveille les spots qui arrivent déjà de tes clusters et du RBN — plusieurs stations différentes apparaissant à distance de saut simple (500–2400 km) dans le même secteur d azimut en quelques minutes, c est la signature de l Es, et rien d autre n y ressemble. Un message par bande et par ouverture, avec le secteur et la distance typique. Une ouverture hors saison est annoncée quand même, et signalée comme inhabituelle : ce sont celles qu il ne faut surtout pas manquer. Rien à configurer — mais la qualité dépend d avoir un flux cluster ou RBN qui porte des spots VHF, et les ouvertures 2 m sont souvent travaillées sans jamais être spottées.", "Cluster DX, deux options d affichage (Paramètres › Cluster DX). La première enlève la couleur et les badges sur les stations déjà contactées : elles restent dans la liste, elles cessent simplement d attirer l œil. La seconde colore tout indicatif non contacté sur cette bande et ce mode, même si l entité est confirmée depuis longtemps — la vue pour remplir des slots plutôt que pour chasser du nouveau. Les deux s appliquent à la liste cluster et au bandmap, les deux panneaux restent donc cohérents.", - "Cluster DX, plus lisible d un coup d œil. Une nouveauté REMPLIT désormais la cellule qui la porte — cellule Bande remplie = nouvelle bande, cellule Préf. remplie = nouveau préfixe, cellule Comté remplie = nouveau comté — au lieu de seulement colorer le texte. Une colonne Comté US rejoint la liste, résolue hors ligne depuis la base ULS. La mise en couleur des stations non contactées ici devient un statut CALL NEUF à part entière, avec sa propre puce de filtre, au lieu d emprunter NOUV SLOT qui veut dire autre chose. Les deux options d affichage passent des Préférences au panneau de filtres du cluster, à côté de Hide worked. La colonne Locator s appelle maintenant Locator du spotter, puisque c est ce qu une ligne de cluster porte réellement." + "Cluster DX, plus lisible d un coup d œil. Une nouveauté REMPLIT désormais la cellule qui la porte — cellule Bande remplie = nouvelle bande, cellule Préf. remplie = nouveau préfixe, cellule Comté remplie = nouveau comté — au lieu de seulement colorer le texte. Une colonne Comté US rejoint la liste, résolue hors ligne depuis la base ULS. La mise en couleur des stations non contactées ici devient un statut CALL NEUF à part entière, avec sa propre puce de filtre, au lieu d emprunter NOUV SLOT qui veut dire autre chose. Les deux options d affichage passent des Préférences au panneau de filtres du cluster, à côté de Hide worked. La colonne Locator s appelle maintenant Locator du spotter, puisque c est ce qu une ligne de cluster porte réellement.", + "Publication web : la page publiée se trie correctement. La colonne Date ne se triait pas du tout, ni les indicatifs commençant par un chiffre — le script lisait un nombre au début du texte, donc toutes les dates d une même année se valaient. Un troisième clic sur un en-tête remet le tableau dans l ordre de publication, et une flèche indique le sens du tri." ] }, { diff --git a/internal/webpub/webpub.go b/internal/webpub/webpub.go index ee32777..31c049b 100644 --- a/internal/webpub/webpub.go +++ b/internal/webpub/webpub.go @@ -233,6 +233,9 @@ th,td{padding:.45rem .6rem;text-align:left;border-bottom:1px solid var(--line);w th{position:sticky;top:0;background:var(--head);font-size:.72rem;letter-spacing:.05em; text-transform:uppercase;color:var(--mut);cursor:pointer;user-select:none} th:hover{color:var(--fg)} +th::after{content:'';font-size:.7em;opacity:.7} +th[data-asc="1"]::after{content:' \25B2'} +th[data-asc="0"]::after{content:' \25BC'} tbody tr:nth-child(even){background:var(--zebra)} tbody tr:last-child td{border-bottom:0} td.call{font-family:ui-monospace,Consolas,monospace;font-weight:700;color:var(--accent)} @@ -248,7 +251,9 @@ td.call{font-family:ui-monospace,Consolas,monospace;font-weight:700;color:var(-- } b.WriteString(``) for i := range qsos { - b.WriteString(``) + // The row's position as published. Sorting is a view on top of it, so a + // third click can put the table back the way the operator first saw it. + b.WriteString(``) for _, c := range cols { cls := "" if c.Key == "callsign" { @@ -262,23 +267,38 @@ td.call{font-family:ui-monospace,Consolas,monospace;font-weight:700;color:var(--

Generated by OpsLog

diff --git a/internal/webpub/webpub_test.go b/internal/webpub/webpub_test.go new file mode 100644 index 0000000..fbf47d8 --- /dev/null +++ b/internal/webpub/webpub_test.go @@ -0,0 +1,49 @@ +package webpub + +import ( + "strings" + "testing" + "time" + + "hamlog/internal/qso" +) + +// The published page carries its own sort script, so a regression there is +// silent: the table still renders, it just sorts wrongly. These pin the two +// things that were actually broken in the field. + +func renderSample(t *testing.T) string { + t.Helper() + cfg := Config{Columns: []string{"qso_date", "callsign", "freq"}} + cfg.Normalise() + qsos := []qso.QSO{ + {Callsign: "8B81SU", QSODate: time.Date(2026, 8, 10, 9, 56, 0, 0, time.UTC)}, + {Callsign: "LZ8NG", QSODate: time.Date(2025, 12, 31, 23, 1, 0, 0, time.UTC)}, + } + return string(renderHTML(cfg, columnsFor(cfg.Columns), qsos, "F4BPO")) +} + +// A third click restores the published order, which is only possible if each row +// remembers where it started. +func TestRowsCarryTheirPublishedIndex(t *testing.T) { + html := renderSample(t) + for _, want := range []string{``, ``} { + if !strings.Contains(html, want) { + t.Errorf("published page is missing %s — the reset-to-original click cannot work", want) + } + } +} + +// parseFloat accepts a numeric PREFIX, so "2026-08-10" became 2026 and every +// date in the same year compared equal: the Date column looked unsortable. +// Callsigns starting with a digit ("8B81SU") hit the same trap. The whole cell +// must match for a numeric comparison to be used. +func TestSortScriptRejectsNumericPrefixes(t *testing.T) { + html := renderSample(t) + if !strings.Contains(html, `var NUM=/^[+-]?\d+(\.\d+)?$/;`) { + t.Error("the anchored numeric test is gone; a bare parseFloat makes dates and 8-prefixed calls sort as equal") + } + if strings.Contains(html, "n=!isNaN(nx)&&!isNaN(ny)") { + t.Error("the old prefix-tolerant numeric detection is back") + } +}