The page was capped at 1100px — a comfortable reading width, and the right
choice for the eight default columns. Now that all 123 fields can be published,
anything past about eight sat behind a horizontal scrollbar on a screen wide
enough to show the lot.
Worse, Windows hides overlay scrollbars until something moves, so a table that
scrolled looked exactly like a table with its right-hand columns missing. That
is what was reported, and the report was reasonable: nothing on screen said
otherwise.
The wrapper grows with its content now, up to the window, and keeps a minimum so
a two-column table does not collapse on a large display. The table sizes to what
its cells need rather than being squeezed to the container first — that squeeze
could wrap a callsign while empty space sat further along the same row. And the
scrollbar is drawn permanently, thin and in the page's own colours.
Nothing computes a distance when a QSO is logged. The DISTANCE field is only
ever filled by an ADIF import that carried one, so publishing it straight gave
an empty column to anyone whose log was made in OpsLog — which is everyone who
reported it.
It falls back to the two locators, which are on the QSO already. A stored
distance still wins: it came from the log that recorded the contact, which knew
more than two four-character squares do. No grids means an empty cell, not a
zero — an empty cell is honest, a zero is a claim.
Rounded to whole kilometres. The squares are tens of kilometres across and a
decimal would assert an accuracy nobody has.
The geometry moved to internal/geo on the way. It lived in package main, which
internal packages cannot import, so the PSK Reporter watcher already had its
maths injected from main and this would have been a third copy. A bearing that
disagrees with itself between two panels is a fault nobody reports, because each
screen looks perfectly plausible on its own.
The catalogue held 23 hand-picked columns, so publishing a county, a satellite
pass or an award reference was simply not possible. It is generated from the
qso.QSO struct now — 123 fields — which also means a new ADIF field cannot be
forgotten here, as a hand-written list always eventually is.
Three keys were renamed to their ADIF names on the way (pota, sota, station).
An alias map keeps existing configurations publishing the same columns; without
it three would have vanished silently on upgrade, which is the worst way for a
setting to change.
The picker had to change with it: 123 chips in one wrap is a wall nobody reads
to the end of. It is sectioned by group, filtered as you type, and the chosen
columns sit on top in publication order — after picking eight out of a hundred,
the question stops being "what exists" and becomes "what did I pick".
The package doc said columns were curated so the operator's address could not
be published. That is no longer true and the comment now says so plainly: the
judgement moved to the operator, the default selection is unchanged, and nothing
is published that was not chosen.
Also collapses the changelog: four separate shared-CAT entries were one thing
from where the operator sits, and all of them were far longer than the one or
two sentences this project asks for.
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.
Settings -> Web publishing. Renders the last N QSOs with the columns the
operator picks, writes the file locally, and optionally uploads it by FTP or
explicit FTPS.
Local write FIRST, upload second, always. A network failure then leaves a good
file on disk that can be published another way, instead of a truncated one on
the server. The local write itself goes to a temp file and renames over the
target, so a reader — or a syncing client — never sees a half-written page.
The HTML page is fully self-contained: inline CSS, inline sort script, no font,
no CDN, no external request at all. It has to work on hosting that blocks
third-party requests, and a page about someone's hobby should not report its
readers to anyone.
Columns are a curated set, not "every ADIF field". This is published to the
public: RST and QSL status belong on it, the operator's home address does not.
Two triggers, both debounced through one path: a QSO is logged, or the optional
timer fires. Fifteen seconds of coalescing means a run of contacts produces one
upload rather than one per QSO, and nobody reading a web page can tell the
difference.
The config is one JSON blob under a single settings key, and that key is marked
sensitive: the FTP password lives inside it, so the whole blob is encrypted at
rest with the others. A locked vault reads back empty, which correctly reads as
"not configured" — publishing must not run with a password it cannot decrypt.