Commit Graph
1060 Commits
Author SHA1 Message Date
rouggy c9d46cfab9 chore: release v0.26.8 v0.26.8 2026-08-23 15:04:59 +02:00
rouggy c7140fd639 feat(export): filtered-view export can keep the OpsLog fields too
Same reasoning as the selected-rows export: ExportADIFFiltered was called
with includeAppFields=false, so a filtered export lost the award
references. A second entry, next to the standard one.
2026-08-23 15:01:32 +02:00
rouggy c887b77b5f feat(export): right-click export can keep the OpsLog fields
The right-click export always called ExportADIFSelected with
includeAppFields=false, so it wrote strictly standard ADIF. That is the
right default when the file is going to another logger, but it silently
drops every APP_ tag -- and an award reference such as RDA@KR-04 lives in
APP_OPSLOG_AWARDREFS and nowhere else. Backing up a selection, or moving
it to another OpsLog, lost the awards.

Adds a second entry rather than a flag on the first: the two exports
answer different questions, and a checkbox in a context menu would have
to be read before every export.
2026-08-23 14:58:01 +02:00
rouggy b3e398b8d6 feat(rda): measure the two district sources before arbitrating between them
A log imported from HAMLOG.online carries the Russian district in CNTY, and the
offline RDA database has its own answer for the same contact. Which wins is a
real question — but only if the two are independent. If HAMLOG's value is itself
a lookup in the same kind of database, there is nothing to arbitrate and a
precedence rule would be ceremony around a redundancy.

So this measures instead of deciding: Settings → RDA gains a read-only compare
that reports agreements, disagreements, and the contacts where only one side
knows, then lists the disagreements — callsign, date, both districts, whether
the database answer was a DATED record (the award administrators' own fact) or
the callsign's current district (an assumption), and whether the QSO is
confirmed on HAMLOG (their value has then been through a match of two logs).

It changes nothing in the log. What to do about a conflict is the next decision,
and it should be taken with the operator's own numbers in hand.

Also: the four HAMLOG fields become filterable, so 'never sent there' is a
filter rather than a guess. A field that can be written and not read back is
half a feature — pinned by a test against the bulk-editable list.
2026-08-23 14:30:48 +02:00
rouggy f4a72da118 fix(hamlog): read the site's own confirmation field, not a guessed one
A real record from their export settles it:

	<CALL:4>RL6M … <CNTY:5>RO-19 <APP_HAMLOG_R150COUNTRY:6>Russia
	<APP_HAMLOG_QSO_CFM:1>Y

The confirmation lives in APP_HAMLOG_QSO_CFM. The four names guessed before a
file was available — APP_HAMLOG_QSL and friends — were all wrong, which is the
argument for reading one rather than reasoning about it.

So that becomes the canonical key everywhere: the award source, the row colours,
the grid column and the bulk editor. Importing a log downloaded from HAMLOG now
carries its confirmations into OpsLog with nothing to rename. The older names,
including the APP_OPSLOG_HAMLOG_QSL that OpsLog itself wrote in the meantime,
are still honoured on read so nothing already stamped stops counting.
2026-08-23 13:43:10 +02:00
rouggy 086581851b feat(hamlog): confirmation defaults and grid columns; drop a settings paragraph
Preferences → Confirmations gains a HAMLOG.online row, sent and received, with
the same defaults every other service has: a new QSO can start at N so the
backlog is meaningful from the first contact. The defaults reach it through a
new setExtraDefault — fill() writes through a pointer to a string field and
these two live in the extras map, which no pointer reaches.

The QSO grid gains four HAMLOG columns (status and date, both directions),
hidden by default: a column per service shown to everyone is how a grid becomes
unreadable.

And the paragraph explaining what "Sent" means is gone from that page.
2026-08-23 13:25:20 +02:00
rouggy f8fdfab031 fix(hamlog): a status AND a date, like every other service
Bulk edit offered two HAMLOG fields, both dates, and the first question they
drew was the right one: which of these is the status? Collapsing the flag into
the date was neat in the storage and a riddle in the dialog, where the eight
fields above it are four status/date pairs.

So four keys now — APP_OPSLOG_HAMLOG_SENT / _SENT_DATE and _QSL / _QSL_DATE —
and four fields, in the shape an operator already reads for eQSL, QRZ, Club Log
and HRDLog. The stamp written after a successful upload follows: Y in the
status, the day in the date.

Nothing else changes: the QSL Manager's backlog still asks whether the sent key
is present, and the award engine still treats any non-N value in the QSL key as
a confirmation.
2026-08-23 13:14:05 +02:00
rouggy 3f82da3c18 feat(hamlog): mark an already-uploaded log through bulk edit
A log sent to hamlog.online by hand — an ADIF exported from OpsLog and dropped
on their site — had no way of being marked as sent afterwards. The QSL Manager
then listed every one of those contacts as backlog and offered to upload them a
second time.

Both HAMLOG keys join the bulk-editable extras, so a selection (or the whole
log) can be stamped in one pass. They are DATES rather than Y/N flags because
that is the shape the sent stamp already uses: the QSL Manager's backlog asks
whether the key is present, and a date also says when it went.

The test pins that they are reachable through the extras path and NOT offered as
columns — a mapping claiming both would write to a table that has neither.
2026-08-23 13:02:04 +02:00
rouggy cd557acf81 feat(hamlog): send to HAMLOG.online from the right-click menu
The upload targets in the QSO context menu gain HAMLOG.online, so a selection
goes there the same way it goes to QRZ or LoTW.

The confirmation toast now names the service from a map rather than a chain of
ternaries that stopped after three: hrdlog, eqsl and hamlog were echoed as their
internal ids, which is not what the operator clicked on.
2026-08-23 12:46:29 +02:00
rouggy 036dc53fe8 feat(hamlog): mark what has been sent, and a QSL Manager backlog
An upload now leaves a trace on the QSO: APP_OPSLOG_HAMLOG_SENT holds the day
it went. The date rather than a Y, so a log says WHEN — and so one shape serves
three readers: eligibility (a stamped QSO is not sent twice), the QSL Manager's
backlog, and the appearance rules' sent channel.

HAMLOG joins the QSL Manager's service list. It has no status column to select
on, so its backlog is the ABSENCE of that extras key — a LIKE over the extras
JSON, which is a full scan and is the right trade here: it answers a button an
operator presses, and the alternative is a promoted column for a field no other
program would ever read. The match is on the quoted key, so a QSO whose comment
merely mentions the text is not taken for one already sent — which is what the
test pins.
2026-08-23 12:40:09 +02:00
rouggy 73806cbd70 feat(hamlog): the settings panel, the key check, and a row-colour channel
HAMLOG.online is now reachable: Settings → External services → HAMLOG.ONLINE
takes the API key, the auto-upload switch and the timing, exactly like the six
services beside it, and a link opens the page where the key is issued.

The button next to it checks the KEY, not the connection. HAMLOG answers
KEYSTATUS with the callsign the key belongs to — something no other service
here offers — so the result reads "Key is valid — account F4BPO" rather than a
green tick. A key pasted from a club account or a second station is visible at
once, which is precisely the mistake a tick would have hidden.

No on-close timing, for the same reason as Cloudlog: there is no per-QSO upload
column to select a backlog from at shutdown, so the option would have done
nothing. Immediate or delayed, and a stored on_close falls back to immediate
rather than silently uploading nothing.

The appearance rules gain a HAMLOG.online channel too, so a row can be coloured
"confirmed" on that alone. It reads the ADIF extras — the standard names a field
for hamlog.EU and none for hamlog.ONLINE — using the same keys the award engine
reads, so the two can never disagree about what a confirmation is. "To send"
skips it: a QSO is either uploaded or not, there is no card to be owed.
2026-08-23 12:29:38 +02:00
rouggy ee148732dd feat: HAMLOG.online upload and confirmations, Yaesu antenna, a named port holder
HAMLOG.online is a seventh external service: one API key, one ADIF record per
QSO, immediate / delayed / on-close like the rest. They publish no API
documentation, so the protocol is read from THEIR OWN client — the HAMLOG Agent
(github.com/hamlogonline/Agent), which is the authoritative source short of
asking them:

	POST https://hamlog.online/api/agent/
	{"ADIFADD": {"APIKEY": k, "ADIFDATA": record}}   → {"STATUS":"OK"}
	{"KEYSTATUS": {"APIKEY": k}}                     → {"STATUS":"OK","CALLSIGN":…}

Success is STATUS == OK, not "no ERROR field": their failure carries ERROR and
no STATUS, and reading an unknown reply — a proxy page, a maintenance notice —
as an acceptance is how a contact goes missing without anyone noticing.

KEYSTATUS buys something no other service here offers: the key can be checked
BEFORE the first QSO, and the answer names the account. A key pasted from
another callsign is caught in the settings panel rather than through a week of
silent refusals.

Their confirmations are also an award source now, ticked like LoTW rather than
expressed through "custom". It reads the ADIF extras, not a column: the standard
names a field for hamlog.EU and none for hamlog.ONLINE, and borrowing the other
site's field would write a falsehood into every exported log. Three plausible
key names from their own export are accepted too, so nobody has to rename a
column by hand after an export.

Yaesu gains antenna selection (AN), remembered per band — the antenna picked on
a band comes back with it, with no table to fill in anywhere. Rigs with one
socket never answer AN and never show the row; the log says which case it is.

And a serial port that is refused now names its likely holder. OmniRig stays
resident once activated and keeps the port of the rig configured in it, so a
native backend never gets it — "Serial port busy" alone accused nobody, and an
FTDX10 spent a morning being blamed for it.
2026-08-23 12:18:27 +02:00
rouggy 3014796c1d chore: release v0.26.7 v0.26.7 2026-08-23 10:43:51 +02:00
rouggy 405b0e24cf feat: network audio into the QSO recorder, and a Yaesu that stays on its VFO
An Icom reached over the LAN streams its receive audio through the Icom
protocol, and Windows sees no sound card for it at all — the audio settings
could only offer the PC's own microphone, so "From radio" had nothing right to
point at and the QSO recorder had nothing to record. The recorder now accepts a
PUSHED source: the decoded stream goes to the speakers and to the recorder
alike, with no virtual cable to set up. Which source it uses follows the CAT
backend, and it is restarted only when that answer changes, so an ordinary
settings save never cuts a recording in half.

OmniRig no longer sends SetSimplexMode to a Yaesu when tuning. It is a silent
no-op on some — an FT-891 logged OK on every spot click while FreqA never
moved — and actively harmful on others. From an FT-2000 log, one QSY:

	Vfo="AB"(0x80)  Split=0x10000 (off)   the operator's state
	Vfo="BA"(0x100) Split=0x8000  (ON)    after SetSimplexMode

The rig-agnostic "receive and transmit HERE, simplex" call turned split on and
moved reception to VFO B. OpsLog then displayed B — reading the radio correctly,
after having moved it itself. Icom is untouched: there the call is the
authoritative one and the direct write is unreliable.

Also:
  - the NEW county badge shows in the entry form itself, inside the field,
    where the operator is deciding whether to call.
  - the basemap buttons clear the zoom controls; Light sat a few pixels from
    the minus button and was being clicked by mistake.
  - French cluster status: DÉJÀ CTC reads DÉJÀ QSO.
2026-08-23 10:43:11 +02:00
rouggy 778db0542e chore: release v0.26.6 v0.26.6 2026-08-23 01:47:17 +02:00
rouggy f50806fbd9 feat: panadapter spots that read, a CI-V link that survives, no auto-call
Panadapter spots are now worth reading. The comment carries the spotter, the
entity and the status in DXHunter's own shape — "CQ up 2 [F4BPO] [Franz Josef
Land] [New Slot]" — which needed two things nothing documents: SmartSDR splits
its command line on SPACES, so the words ran together until every space became
non-breaking; and it truncates past ~60 characters, so the cluster's own words
are trimmed first and the three brackets always survive. RBN column padding is
collapsed on the way in, or a preserved run of spaces opened a gap wide enough
to push the rest off screen.

"Already worked" means the CALLSIGN is in the log, not the entity: saying it of
a station never contacted was simply wrong. Each status can also be kept off the
panadapter entirely, and the WSJT-X decode spots obey the same switches — the
palette governs the panadapter, not one of the two things that feed it.

And the radio is no longer hammered: a spot whose frequency, colour and comment
are unchanged is not removed and redrawn. A busy skimmer feed re-spots the same
station every few seconds; one two-minute session sent 2128 adds, 88 of them for
a single callsign, and the display did not move a pixel for any of them.

CI-V, from an IC-7850 that kept killing JTDX: a reply the rig sent to another
controller on the same bus is no longer taken for ours, and a set_ptt, set_freq
or set_mode whose acknowledgement goes missing is verified by reading the rig
back instead of being reported as a failure. WSJT-X and JTDX answer a failed
command with a Rig Control Error and drop the link mid-over — 98 keyings, 6 lost
acknowledgements, 2 dropped connections in one session. The check waits 700 ms,
not the poll's 150: the rig has just failed to answer twice because it was
retuning, and a short probe would fail for the same reason.

Auto-call is withdrawn — it duplicated DXHunter, which already answers decodes,
and two programs deciding that from one shack key over each other. The library
is kept whole and dormant; a guard in App.tsx makes sure a stored preference
cannot key a transmitter whose switch no longer exists.

Also:
  - the log rotates while running, not only at startup: the CI-V trace left on
    wrote 416 MB and nothing would have stopped it before the disk did. Closing
    it now releases the crash file too — the runtime keeps its own duplicate.
  - the interface zoom announces itself, with a badge, a click back to 100% and
    a View menu; Ctrl+wheel and Ctrl+0 always worked and nothing said so.
  - no more elastic bounce, and no swipe-to-navigate out of the app.
  - Edit QSO: your own TX power and the contacted station's extended locator
    were saved and written back with no box to set them.
  - FT decodes: continents are a multiple choice; a compound MSHV message that
    answers two stations in one line is recognised as addressed to you.
2026-08-23 01:16:26 +02:00
rouggy bcfb3bfd37 chore: release v0.26.5 v0.26.5 2026-08-22 14:10:46 +02:00
rouggy cb430a22ee feat: Ultrabeam over USB, Paper QSL from the awards grid, per-role schemas
Ultrabeam on a serial port never worked, and three faults were stacked so
each hid the next:

  - Stop() did not wait for the poll loop, so a stopped client kept the COM
    port. Every later client then failed with "Serial port busy" — the
    program holding it being OpsLog itself.
  - startUltrabeam tore the old client down CONCURRENTLY with starting the
    new one, and "Test connection" built a second client on a port already
    ours. Harmless over TCP, fatal on a port with one owner.
  - A silent serial port returns (0, nil) and bufio retries that a hundred
    times: a 4 s timeout became ~7 minutes of a frozen poll loop logging
    nothing.

The controller then answered at once. Confirmed on hardware: the USB cable
presents TWO COM ports, only the second reaches the controller, and only at
19200 baud — so the speed is pinned in code (an FTDI cable opens at any
speed, and a wrong one is indistinguishable from a dead controller) and the
port field says which one to pick. The first exchange after each connect is
hex-dumped, which separates silence from a wrong baud from a misread frame.

Databases now carry only the tables their role needs. Every target used to
get the whole migration set, so a shared MySQL logbook grew settings and
station_profiles tables nothing ever wrote to — an operator inspecting the
server could not tell which copy was authoritative. Statements are filtered
by role, unknown tables are kept in both (fail-safe), and existing databases
are cleaned once, dropping only EMPTY tables. Settings → Database gains a
Compact button, since SQLite frees pages inside the file and never shrinks it.

Also:
  - Awards: the callsigns behind a cell open the QSL Manager on Paper QSL,
    searched, ready for the card dates.
  - The record button no longer goes missing after an update: whether manual
    recording is possible is a per-profile question that was asked once, at
    startup, before the profile was known.
  - Alert rules and filter presets confirm that they were saved.
  - Spot clicks on the radio panadapter carry the POTA park into F3.
  - The build gate is re-checked wherever the active callsign can change; it
    ran at startup alone, and a fresh install has no callsign then.
2026-08-22 14:07:00 +02:00
rouggy abfed4afa2 chore: release v0.26.4 v0.26.4 2026-08-22 08:35:49 +02:00
rouggy d48420485f chore: release v0.26.3 v0.26.3 2026-08-21 23:28:55 +02:00
rouggy 0cd243fe00 chore: release v0.26.2 v0.26.2 2026-08-21 18:21:33 +02:00
rouggy c49faf9a14 fix(antenna): pick the SteppIR's serial port from a list
The COM port was a bare text field — the only serial device in the app without
a port list, and typing one by hand gave no clue when it failed.

A Combobox rather than the Select the other devices use, deliberately: the port
of a USB adapter that is not plugged in does not appear in any list, and
refusing a typed value would mean the antenna cannot be configured until the
adapter is in. Detected ports are offered, anything else is still accepted, and
the value is trimmed and upper-cased on the way in.

The failure itself was already logged by the SteppIR client ("cannot open COM7
@ 4800 baud"); what was missing was the line above it not claiming the antenna
had connected. That is fixed separately.

Carries the 0.26.2 changelog entries for this and the three preceding commits —
one file, and splitting it four ways would have said less than this does.
2026-08-21 14:36:07 +02:00
rouggy d01c288454 feat(grids): one world, a zoom that holds it, and the modes to filter it
The grid-square map drew the world two or three times across a wide window, each
copy carrying the same squares — worldCopyJump earns its keep on the great-circle
map, where a path reads better crossing a second Asia, and costs only confusion
on a map of where you have been heard. It is off here, the tiles no longer wrap,
and addBasemap takes the option so the other map is untouched.

Zooming out far enough to hold Greenland and Antarctica together was impossible:
the floor was zoom 1, and one step further out was already too far. Zoom 0 is
the whole planet, and the view now opens on a fitWorld computed from the
container rather than a guessed zoom level — repeated once when the container
first has a real size, because the tab is mounted hidden.

That exposed the next thing: Esri answers a request outside the world with a grey
"Map data not yet available" tile, and zoomed out that was most of the screen.
The tile layers carry a bounds, so those tiles are never asked for, and what is
left beside the planet is the panel's own background instead of a white slab.

Finally the mode filter offers All, Phone and CW beside Digital and FTx. The
backend already accepted all of them — GridSquares understands "ALL" and the
ModeClass names — only the interface was offering two.
2026-08-21 14:35:54 +02:00
rouggy c31ab05c80 fix(awards): "no field" leaves no rule running behind the panel
Choosing "no field" cleared the primary field and hid the matching controls —
but the fallback OR rules stayed in the definition and went on scanning their
own fields. An RDA award declared to match nothing still found a district in the
QTH, and the panel no longer showed the rule doing it. Only a manual override on
the same QSO kept the wrong reference out of the log.

So the choice now clears the fallbacks too, which is what its label promises,
and a rule that survives — from an imported definition, a shared template — is
rendered even in "no field" mode. Nothing that still runs may be invisible.

No catalogue award pairs an empty field with OR rules, so nothing shipped
changes behaviour.
2026-08-21 14:35:54 +02:00
rouggy f45952d7fb fix(cat): rebuild the rig link only when the connection itself changed
Pressing Save in Preferences tore down the CAT link and the shared CAT server
unconditionally. Two consequences an operator sees at once: WSJT-X and JTDX are
thrown off their rig mid-QSO with an error box, and every spot disappears from a
FlexRadio panadapter — a Flex reconnect clears them by design, so the wipe on
save was the reconnect, not a stray command. Switching profile re-applies the
same settings and did exactly the same.

Two signatures decide it now. catLinkSig covers what shapes the physical link
(backend, host/port, COM port and baud, CI-V address, network credentials);
catShareSig covers the protocol and port of the rigctl/TCI server, which is the
socket WSJT-X actually holds. Same string in, same connection out, so there is
nothing to gain by rebuilding it. The poll interval, command delay, transverter
offset and spot options are applied to the running manager as before — the tests
pin that a spot option in particular can never trigger a reconnect, since that
is the one that would wipe the panadapter.

Also, two things the log would not say:

- WSJT-X/JTDX report their Enable Tx state in every Status and we wrote it
  nowhere. When a station is called and nothing goes out, that flag is the whole
  answer — a Reply does what a double-click does, it cannot arm a transmitter —
  and the only way to see it was to watch a button in another window. Logged on
  change, so it costs a few lines a QSO.

- The antenna log said "steppir connected" before anything had talked to it,
  which made an unopenable serial port look like a working link with tracking
  switched off. It says "configured", which is what it knows.
2026-08-21 14:35:32 +02:00
rouggy e6889caae9 chore: release v0.26.1 v0.26.1 2026-08-21 01:43:14 +02:00
rouggy e3b7a35e2c fix: stop losing decodes, hanging on exit, and wedging the rig link
Three faults an operator's log finally made visible, plus the interface
work that came out of the same session.

Reliability:

- UDP events were dropped on backpressure without a word. A period hands
  over twenty-odd decodes at once, and one slow write to the radio was
  enough to fill the queue — so a decode simply never appeared, and the
  only detector was the operator comparing the panel with JTDX. The drop
  is now counted and logged, panadapter spots went to their own goroutine
  so the radio can no longer hold the decode stream up, and the queue is
  deep enough for a full period.

- The CAT manager waited for its poll loop with a bare <-done. A loop
  wedged in a serial read then blocked every later restart inside Start,
  before it could even try to connect: the rig stayed dead, no line was
  written anywhere, and only killing the process recovered it. The wait
  is bounded at ten seconds and says what it abandoned and why the next
  connect may fail.

- Shutdown had no logging at all, so a hang left nothing to go on and a
  process the operator had to kill — which then blocked the restart after
  an update. Every step is logged, and a watchdog forces the exit if one
  of them never returns.

Auto-call:

- A QSO in progress is now held by OpsLog itself rather than inferred
  from the sender's Status. The moment WSJT-X/JTDX dropped the DX call or
  the Enable-Tx flag between overs, the exchange looked finished and the
  next CQ was answered, interleaving two and then three QSOs on one
  slice. Released on log, on halt, on taking over, and by a watchdog.

Cluster console:

- Replies to a command were buried under the spot flood; a Replies
  toggle hides the DX spots, which the list above already shows.
- Twelve named command buttons beside the input, configured in
  Settings -> Cluster; a button with no command is not drawn.
- Following the tail is now an explicit switch, and sending a command
  re-arms it. It used to measure "am I at the bottom" AFTER committing
  the new lines, so a ten-line reply looked like the operator had
  scrolled up and was never followed — the one case it exists for.

Awards:

- An award can name NO field. The matching controls disappear with it
  and only hand-assigned references count, which is the only thing that
  can feed a reference like WWBOTA. A test pins that nothing else is
  scanned.
- WWBOTA added to the catalogue with its 31 342 references.

Elsewhere: the rotor widget's Stop button acknowledges the press like
the direction presets already did, and the docked band map can be
switched to fit-to-band from its own header.
2026-08-21 01:07:10 +02:00
rouggy 1e507225dd chore: release v0.26.0 v0.26.0 2026-08-20 17:53:55 +02:00
rouggy 47992b5f03 fix(decodes): a decode's mode is a marker, not a mode name
Every station on an already-worked band was flagged NEW MODE.

A WSJT-X Decode does not carry the mode's name. It carries the
one-character marker from the decode line - "~" for FT8, "+" for FT4 - and
that character was passed straight through as though it were a mode. The
status resolver then compared "~" against the modes worked for the entity,
matched nothing, and concluded the mode had never been worked. Same cause
put "~ -07" in the comment of every decode spot pushed to the FlexRadio
panadapter, which nobody had traced back.

Resolved through a marker table, with the mode from the sender's last
Status as the fallback - Status is the message that carries the real name.
So an unlisted or future marker degrades to correct rather than to
nonsense, and a sender that puts the name in the field directly is believed
as-is. With neither available the mode is left empty, which makes the
resolver answer "worked": the safe side, since a wrong mode invents a
new-mode flag exactly as the marker did.
2026-08-18 11:40:13 +02:00
rouggy 453e0df27b Merge branch 'feature/ftx-decodes'
An FT decodes panel fed by the inbound UDP link: every decode from WSJT-X,
JTDX or MSHV, grouped by T/R period, with the new-entity flags the cluster
already computes and the operator's own transmissions threaded into the
slots they went out in. Clicking a line answers the station through a
WSJT-X Reply, routed to the instance that heard it.

Available both as a closable tab and as a Main-view pane.
2026-08-18 11:18:22 +02:00
rouggy 6ed38014ed chore(decodes): changelog entries for the FT decodes panel 2026-08-18 11:17:30 +02:00
rouggy f832d1ba07 Merge main: station callsign on import 2026-08-18 11:08:56 +02:00
rouggy f0e00c63a3 fix(import): "fill my station fields" fills the station callsign too
STATION_CALLSIGN was the one field the option deliberately skipped, on the
grounds that stamping the active call could re-route a mixed-call log.

That protected nothing. The same option already writes this profile's grid,
rig, antenna, city and postal address onto every record it finds blank — it
has assumed "this log is mine" long before it reaches the callsign. And the
thing that actually keeps a multi-op log intact is that a record CARRYING a
station callsign is never touched, which has always been true and still is.
Withholding it only meant the option quietly failed on the one field an
operator goes and checks afterwards, leaving imported QSOs with no station
call at all — invisible in the ADIF they export, and unroutable for LoTW
and Club Log.

Counted and logged: "did it fill the callsign?" is the first question after
an import, and the log is where it gets answered.
2026-08-18 11:08:45 +02:00
rouggy 40ecfb9fa9 refactor(decodes): read the list the way DXHunter's reads
Compared side by side, DXHunter's decode list is plainly easier to read,
and three things account for the gap.

The Call column repeated what the message already said. Every FT8 line
opens with the callsigns — "CQ A93MO LL56", "PG5FRL JH3CUL PM74" — so a
column in front of it printed the same token twice and pushed the line
everyone actually reads to the middle of the row. It is gone; the message
is the identity, and the tokens worth finding are picked out inside it:
green for CQ and for our own call when someone answers, red for the station
we are calling. The grid went with it, being the message's last token.

Time is on every row now, compact, no separators. A decode belongs to a
period and the section heading names it — but once a slot runs past a
screenful the heading is somewhere above, and an instant you cannot read
where the decode is is not an instant you have.

Band and mode became their own columns, band as a chip, and the status
flags moved into one column at the right edge with LoTW and a "worked"
marker beside them. Rows are tighter: 13 px for the message and the report,
11 px for the figures, 10 px for the badges, and the column rules run
through the lot.
2026-08-18 10:14:40 +02:00
rouggy 8c68a7d711 Merge main: OpsLog's own Club Log API key 2026-08-18 10:10:42 +02:00
rouggy 628d1e8490 fix(clublog): use OpsLog's own application API key
The embedded key was registered to XV9Q, not to OpsLog. Not a cosmetic
detail: Club Log identifies the client software by that key, so every
OpsLog upload in the world was attributed to that callsign. Its owner
received the abuse warning OpsLog earned when the on-close sweep was still
posting hundreds of QSOs through the realtime endpoint - and a revocation
aimed at them would have cut Club Log uploads for every user of this
program at once.

G7VJR issued a key for "OpsLog" on request. Same mechanism, same UX: the
key identifies the software, the operator still supplies their own e-mail
and password, so it authorises nothing on its own.

Club Log asks that it not be published in source code. The source is on a
private remote and only the built exe is released, but it remains
recoverable from that binary by anyone who looks - as it is for every
logger that embeds one. It is an identifier that can be attributed, not a
secret.
2026-08-18 10:10:33 +02:00
rouggy 0881c72c0f feat(decodes): offer the panel as a Main-view pane
It joins the left/right dropdowns in Settings -> General alongside the maps,
the cluster and the rest, so decodes can sit beside the entry strip instead
of only behind a tab. Per-profile like the other pane choices.

The panel itself is now built in ONE place and rendered from both: two
copies of that call would be two sets of props to keep in step, and the
click handler in particular is not something to duplicate.

The status refresh follows. Its "is anything showing a spot status?" test
decides whether a logged QSO refreshes the NEW badges now or only marks
them dirty, and a panel that had become a pane would have gone on wearing
stale badges whenever it was shown that way rather than as a tab.
2026-08-18 10:01:30 +02:00
rouggy 6da30f91c4 feat(decodes): slot clock, and the QSO in progress stands out
A countdown for the T/R slot, against the UTC clock — a bar, the seconds
left, and the mode. Slots are anchored to UTC rather than to when OpsLog
started, so it is computed from the wall clock alone and keeps running when
the band is dead and there is nothing to group. The last fifth of a slot
turns amber: that is when a decode is imminent and an operator deciding
whether to answer has run out of time to think.

Which exposed a real fault in the grouping. Status reports the T/R period
as a whole number of seconds, so FT4 arrives as 7 or 8 depending on which
way the sender rounded, and the code floored to whole seconds on top of
that — two FT4 periods landed in one heading and others were split down the
middle. The slot length now comes from the MODE, which knows the exact
figure and the whole halving family (FT8 15, FT4 7.5, FT2 3.75), with the
sender's number only as a fallback; and the arithmetic is in milliseconds.
Sub-second slots get a decimal in their heading, or two FT4 periods inside
one second would print the same time twice.

Two rows now stand out from the band behind them, because they are not
about the band at all but about the QSO in progress:

  - the station being called is red, taken from the transmit state, so it
    can be found in a slot holding thirty others;
  - anyone ANSWERING is green and labelled, which outranks everything else
    on the screen. A reply is a decoded line that opens with our own
    callsign — bracketed too, since a non-standard call comes back
    compressed.
2026-08-18 07:14:17 +02:00
rouggy f833ff6d04 fix(decodes): a station just worked drops its NEW badge
EY35S went on showing NEW SLOT for the rest of the half hour it stayed in
the list, five minutes after the QSO was in the log.

A decode's verdict is resolved once, when it arrives, and then read from
the shared status cache for ever. The cache IS refreshed after a QSO is
logged - but refreshSpotStatuses only ever re-queried the cluster spots, so
no decoded callsign was in the batch. And the decodes tab was not in the
"is anything showing a status?" test, so logging a QSO while looking at
this very panel only marked the cache dirty and waited for the cluster or
the band map to be opened.

Both fixed: decoded stations join the refresh batch, deduplicated by
call+band+mode so half an hour of a busy band is a few hundred queries the
backend answers with one pass of the log, and the tab counts as visible.

Second, worse bug found on the way. The cache is pruned back to the live
spots once it outgrows twice the spot cap - keeping only keys present in
the cluster list. Decodes share that cache and are exactly what pushes it
past the cap, so on a busy band the panel would have wiped every one of its
own badges the moment it filled up. Decoded stations now count as live.
2026-08-18 07:03:09 +02:00
rouggy fba7e79a1c feat(decodes): answer a station on click, DT and Freq, badge filters
Clicking a decode now ANSWERS it. It sends WSJT-X/MSHV a Reply message
(type 4), which is the same thing as double-clicking the line in their own
Band Activity window: the application looks the decode up, sets its
transmit frequency to the caller's and starts the exchange.

It deliberately does not tune the radio, which is what it did before and
why nothing happened. On FT8 the whole band sits inside one passband, so
moving the dial changes nothing about who gets answered - the decision
belongs to the decoding application, and the Reply is the only way to hand
it over. Tuning would also just fight it for the VFO. The entry is still
filled so the QSO can be logged here.

The reply is routed by PROGRAM ID, not by listener: two receivers can share
one multicast group, and answering a station heard on the 6 m instance by
talking to the 20 m one would start a call on the wrong band. It goes to
the address that instance's packets actually arrive from - a multicast
listener must answer the sender, never the group. WSJT-X matches the reply
against its own decode list, so the payload replays the decode field for
field: time, snr, delta time, audio offset, mode and message text.

Two columns added, DT and Freq - the audio offset inside the passband, not
the RF frequency, which is the same for every station in the list and says
nothing. Past about two seconds DT takes a warning tint: that station is
drifting out of the window.

The transmit strip. "You cannot see what you are sending, or who you are
calling" - two separate faults. The message was only ever threaded into its
period, and in FT8 you transmit in the slots you are NOT receiving in, so
its period had no decodes and the whole line was dropped; a transmit slot
now creates its period. And the state is a strip of its own at the top,
because it is the one thing on the screen that is about the operator rather
than the band. It is fed by every Status rather than only by one carrying
transmit text, so it can still name the station being called on MSHV and
older JTDX builds, which stop before tx_message in the Status payload.

"New only" became per-category badges, in the colours and the vocabulary of
the Chase New panel. None lit shows the whole band - this is a decode log
first, and a panel that opened by hiding most of the traffic would be lying
about what is on the air.
2026-08-18 06:53:10 +02:00
rouggy d829726679 Merge main: the 0.26.0 changelog block 2026-08-18 06:13:44 +02:00
rouggy c48edd7898 chore: open the 0.26.0 changelog block 2026-08-18 06:13:32 +02:00
rouggy ffaf6fc869 refactor(decodes): column rules, left-aligned grid, no CQ stutter
Three things from reading it on a real screen.

Column rules. The grid alone was not enough to follow a line across: cells
now carry a right border and the row stretches, so the rules run unbroken
from the header to the bottom of the list. That is what turns rows of text
into a table.

Left-aligned. The previous pass centred the grid inside a maximum width,
which on a wide screen opened a dead margin down the left before the first
callsign - trading the hole in the middle for a bigger one at the edge. Now
it fills the width and the slack lands in the message column, which is the
one that can use it and the one bounded by rules on both sides, so it reads
as a cell rather than a gap.

"CQ CQ PE1NAO JO32" - a green CQ badge in front of a message whose own
first word is CQ. The badge is gone; the word already in the line is picked
out instead, which scans the same and stutters not at all.
2026-08-18 06:12:21 +02:00
rouggy 4f77d51ffe refactor(decodes): real columns, spelled-out flags, bigger type
First pass on the panel from operating feedback.

Columns are a grid template shared by the header row and every data row, so
the two cannot drift and the eye has a rail to follow. It is capped at
1500 px and centred: free-flowing, a 2500 px window put the country a foot
from the callsign it belonged to and left a hole in the middle of every
line.

"New" gets a COLUMN. It was only a coloured edge before, which says
something is special without saying what — and every one of these is a
reason to break off what you are doing and call. The entity verdict is a
solid badge, the orthogonal ones (park, grid, prefix, county) are outlined
in the colours markerColour already gives the cluster list and the band
map, so a new park is the same green in all three. Applied inline because
those are categorical --chart-* custom properties, which the theme does not
expose as Tailwind colour utilities: written as border-chart-7 the badge
would simply have had no colour.

Band and mode selectors now appear only when the feed actually carries more
than one of each. One MSHV is one band and one mode, so for most operators
they were furniture; they show up the day a second instance puts a second
band on the link, which is the only day they mean anything. Same rule for
continent, and a receiver count when more than one instance is feeding.

Added a LoTW-only filter, and raised the type throughout (call and message
to 14 px, secondary to 12 px, badges to 11 px) with more room per row.

The decode payload now carries the sending application's own id. It tells
two receivers apart on one multicast group — and it is the address a
WSJT-X Reply message would have to go back to, so it is carried now rather
than requiring another trip through the parser later.
2026-08-18 06:04:28 +02:00
rouggy a197d124dc feat(decodes): an FT decodes tab fed by the inbound UDP link
Every FTx decode WSJT-X, JTDX or MSHV puts on the wire, grouped by T/R
period. Optional and closable, from Tools -> FT decodes; its open state is
remembered, because an operator running digital modes leaves it open for
the session rather than consulting and closing it.

The period is the point, and what separates this from the cluster list.
FT8 is a sequence of fifteen-second slots and a band is read by watching
them go by: who called CQ this slot, who answered, what I was sending while
they did. A flat list sorted by time loses exactly that, so the list is
grouped one section per period, newest first, with the operator's own
transmission shown inside the slot it went out in.

Three fields had to be carried up from the wire to make it possible:

  - the decode's OWN timestamp, which the parser read and threw away. It is
    what assigns a slot: a period's decodes arrive in one burst a second or
    two after it closes, so arrival time piles a whole period into the next
    one. Rebuilt to UTC from milliseconds-since-midnight, with the
    day-boundary case handled - a decode stamped 23:59:58 arriving at
    00:00:01 would otherwise be dated a day ahead and sit at the top of the
    list for the rest of the session.
  - the decoded line itself. The exchange is what says where a station is in
    a QSO, and no set of extracted fields reads like "R-09" does.
  - tx_message and transmitting from Status, which nothing parsed before.
    Recorded once per message rather than on every Status, which repeats it
    about once a second for the whole over.

Also picked up on the way: is_new, low_confidence, off_air, the operator's
own call and grid, and the T/R period itself - better authority on slot
length than the mode name, which says nothing about a custom period. The
Status tail is read defensively: those fields were appended over successive
schema versions and JTDX and MSHV each stop at their own point, so a short
packet is normal and keeps whatever parsed.

Status flags come from ClusterSpotStatuses, the resolver the cluster list
and band map already use, filling the same cache. One verdict per call:
"new band" in this panel and plain worked in the cluster two seconds later
would be worse than no flag at all. Clicking a call goes through the same
handler as a cluster spot, so answering a station is one gesture whether it
came off telnet or off the receiver.

Filters: CQ only, new-anything only, band, mode, continent, an SNR floor
and a free search. The band, mode and continent choices are built from what
is actually on the feed - offering 160 m to a station whose receivers are
all on 6 m is noise.

Decodes are held in the frontend and pruned to a rolling half hour: they
are a live view, not data, nothing outside the panel reads them, and a
night of FT8 on 20 m would otherwise grow a list no filter can rescue.
Arrivals are staged on a 300 ms timer so a period landing as fifty packets
costs one status lookup and one render.
2026-08-18 05:51:34 +02:00
rouggy 9599c3e0b9 chore: release v0.25.9 v0.25.9 2026-08-18 05:09:04 +02:00
rouggy a81125eab1 fix(icom): a set whose acknowledgement is lost is sent once more
Extends to frequency and mode what PTT already had. A missing FB is not a
missing command: the rig acts on the frame as it decodes it, and what
expires is our wait for the answer, on a bus shared with the rig's own
transceive updates. JTDX in "Split: Fake It" moves the dial and the mode
immediately before every key-down, so those acks queue behind each other.

Losing one is fatal to the client rather than merely untidy: rigctld answers
RPRT -9, JTDX reads that as losing rig control and tears the connection down
mid-over. An operator's log shows three set_freq failures and one set_mode,
each followed within 300 ms by a fresh rigctld client -- and shows the PTT
resend rescuing an over that would otherwise have ended there.

Opt-in per caller rather than folded into exec: only a command that says
"be in this state" can be repeated safely, and a relative one must never
come through here.

The acknowledgement loss itself is still unexplained. Every failure in that
log is preceded by a state read reporting SSB on a rig in DATA, which points
at CI-V frame desync rather than a slow rig, and needs a trace to pin down.
2026-08-17 16:23:54 +02:00
rouggy bbe1b3ce80 fix(tci): a refused un-key no longer leaves the rig keyed for good
The trx handler stamped its PTT cache BEFORE commanding the radio and left
it in place when the command failed. An operator running JTDX over TCI with
an Icom on CI-V lost an un-key to a lost acknowledgement: the cache recorded
"off" regardless, and from then on every trx:0,false was dismissed as a
repeat of a state the radio had never reached. The cache is per-server, not
per-connection, so reconnecting JTDX changed nothing either -- the
transmitter stayed keyed into the amplifier, with no drive, until the radio
was switched off by hand.

The cache is now written only on success, and a failure clears "known"
outright so the next command reaches the radio whatever it is.

Second guard: releasePTT drops a PTT this server asserted when the client
disconnects, and when the server stops -- before the CAT backend goes down,
while the rig is still reachable. rigctld has had that since a K3 sat in
transmit for 29 s; the TCI server was written without it, so an operator
moving from Hamlib to TCI silently lost the protection. A later log shows
the rig keyed for 40 s across a JTDX reconnect for exactly that reason.
2026-08-17 16:23:53 +02:00
rouggy 5e80c27f61 feat(appearance): the band/mode matrix colours can be chosen
The PH/CW/DIG grid in the Stats panel is the fastest read in the app and its
palette was fixed per theme. Settings -> Appearance now offers the six: the
four status fills, the never-worked fill, and the ring on the cell being
entered.

Stored as OVERRIDES, not as a palette. Each of the twelve themes ships an
--mx-* ramp tuned to its own background, so an operator who only wants a
different green must not thereby freeze the other four to the theme they
happened to be using that day. An empty value means "whatever the theme
says"; the chosen ones are stamped inline on <html>, where they win over
every theme; switching the feature off hands the colours straight back.

The pickers are seeded from what the matrix is painting at that moment
rather than from a fixed palette, so the choice starts from the colours in
front of the operator. A new --mx-cur token carries the current-entry ring:
it follows --warning by default, so it stays theme-correct on all twelve,
but can be recoloured without dragging every other warning in the app along.

The legend under the matrix and its cell tooltips were hardcoded English.
They now go through t() with the same keys as the pickers, so the grid and
the settings cannot disagree about which green is which.
2026-08-17 16:23:39 +02:00
rouggy 7be6f64596 chore: release v0.25.8 v0.25.8 2026-08-17 13:18:32 +02:00