Skip to content

Seamless ADIF logbook auto-reload for DXCC spot colouring#1801

Merged
ten9876 merged 2 commits into
aethersdr:mainfrom
M7HNF-Ian:fix/logbook-autoreload
Apr 22, 2026
Merged

Seamless ADIF logbook auto-reload for DXCC spot colouring#1801
ten9876 merged 2 commits into
aethersdr:mainfrom
M7HNF-Ian:fix/logbook-autoreload

Conversation

@M7HNF-Ian

Copy link
Copy Markdown
Contributor

Summary

  • Previously spot DXCC colours only updated when the user manually clicked Browse and reloaded the file in the DX Cluster dialog. Exporting a fresh log to the same path had no effect until they clicked Browse again by hand.
  • The "Auto-Reload Log" toggle defaulted to off, and even when on, edge cases (atomic rename, file deletion, Windows write-locks) meant it frequently stopped working silently.
  • This PR removes the toggle entirely and makes auto-reload always-on: point AetherSDR at your .adi/.adif file once and spot colours stay current automatically.

How to use

  1. Open the DX Cluster dialog and connect to a cluster
  2. In the DXCC Colouring section click Browse and select your logbook file (.adi or .adif)
  3. AetherSDR loads the log immediately and begins watching the file — no further action needed
  4. Whenever your logging program exports to that same path, spot colours update within ~2 seconds automatically

Changes

src/gui/DxClusterDialog.cpp

  • Removed the "Auto-Reload Log" toggle — the watcher is now armed automatically on Browse
  • Shows "Updating…" while the async parse runs, then updates QSO / entity count on completion

src/gui/MainWindow.cpp

  • At startup, if a saved ADIF path exists the watcher is armed unconditionally (was previously gated behind the now-removed toggle setting)

src/core/DxccColorProvider (.h / .cpp)

  • Added importStarted() signal so the UI can show "Updating…" during async parsing
  • 2-second debounce coalesces rapid change notifications before triggering a reload
  • Watcher re-registration moved into the debounce callback so atomic-rename exports (N1MM, Log4OM, etc.) are caught correctly — the replacement file is present by the time the 2 s window expires
  • Directory watcher on the parent folder handles delete→recreate: if the file is deleted and a new one later dropped to the same path, the directory watcher detects it and re-arms the file watcher
  • New AdifParser::openFailed signal: if the file is locked by the logger during export, parseFileAsync retries 3× at 500 ms intervals on the worker thread rather than immediately emitting an empty result that would wipe spot colours. On all-retry failure onParseFailed preserves the existing worked status and only re-arms the debounce timer if the file still exists (locked not deleted), preventing an infinite retry loop on intentional deletion

Behaviour matrix

Scenario Result
Logger exports to same path Colours update within 2 s ✓
Logger uses atomic rename (temp→rename) Caught by post-debounce re-arm ✓
Logger holds write-lock during export Retries ×3 at 500 ms; colours preserved until success ✓
File deleted Colours preserved in memory; no retry loop ✓
File deleted, new file dropped to same path later Directory watcher detects reappearance, reloads ✓

Test plan

  • Export log to watched path → spots recolour within 2 s
  • Rename watched file away, rename a different file to the watched path → spots update
  • Delete watched file → QSO count preserved, no crash, no colour flicker
  • Delete watched file, drop new file to same path → spots reload automatically
  • Restart AetherSDR with a saved ADIF path → watcher armed on startup without touching the dialog

🤖 Generated with Claude Code

M7HNF-Ian and others added 2 commits April 21, 2026 08:45
…ate)

Previously the QFileSystemWatcher was opt-in via a manual "Auto-Reload Log"
toggle that defaulted to off, so spot DXCC colours never updated when the
user exported a new log without clicking Browse again.

Changes:
- MainWindow startup: always arm the watcher when a saved ADIF path exists.
  No longer conditional on the DxccAutoReloadAdif setting.
- DxClusterDialog: removed the "Auto-Reload Log" toggle; the watcher is
  armed automatically whenever the user selects a file via Browse.  The
  logger just exports to the same path and spot colours update within 2 s.
- DxccColorProvider: add importStarted() signal emitted at the start of every
  async parse so the UI can show "Updating…" while the file is being read.
  Wire importStarted → "Updating…" label in the dialog; importFinished
  continues to update the QSO / entity count as before.
- Fix Windows inode-replacement robustness: move the QFileSystemWatcher
  addPath() re-arm from the fileChanged handler into the debounce timer
  callback.  Loggers that atomically rename a temp file over the destination
  briefly delete the watched path; the debounce fires 2 s later by which
  time the replacement file is present, so addPath() succeeds and subsequent
  exports are also caught.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three robustness improvements on top of the initial auto-reload commit:

1. Directory watcher (delete→recreate)
   QFileSystemWatcher stops receiving events once the watched file is
   deleted.  A directory watcher on the parent folder stays alive and
   re-arms the file watcher the moment the target filename reappears,
   so exports to a previously-deleted path are still caught.

2. File-locking guard (Windows logger lock)
   On Windows some loggers hold an exclusive write-lock while exporting.
   AdifParser::parseFileAsync now retries QFile::open() up to 3 times
   at 500 ms intervals before giving up, covering the typical lock
   window without blocking the worker thread unnecessarily.

3. openFailed signal — preserve worked status on all-retry failure
   Instead of emitting finished({}) (which would wipe spot colours),
   a new AdifParser::openFailed signal is emitted when all open attempts
   fail.  DxccColorProvider::onParseFailed keeps m_workedStatus intact
   and only re-arms the debounce timer if the file still exists (locked,
   not deleted), preventing an infinite retry loop on intentional deletion.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@M7HNF-Ian M7HNF-Ian requested a review from ten9876 as a code owner April 21, 2026 08:44

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work, @Chaosuk97 — this is a well-structured improvement that addresses a real usability pain point. The behaviour matrix in the PR description is appreciated; it's clear you've thought through the edge cases. A few observations:

Looks good

  • The retry-with-backoff in AdifParser::parseFileAsync is sensible for the write-lock scenario and correctly runs on the worker thread (no GUI stall). The openFailed signal is a clean way to preserve existing worked status on failure.
  • Moving watcher re-registration from fileChanged into the debounce timeout callback is the right fix for atomic-rename exports — by the time the 2s window expires, the replacement file is present and the new inode gets tracked.
  • Directory watcher for delete→recreate is a good addition. The guard (QFileInfo::exists + !m_fileWatcher.files().contains) keeps it cheap even in busy directories.
  • The removal of the toggle simplifies both the code and the UX. Always-on watching after a Browse selection is the right default.

Minor items to consider (non-blocking)

  1. Directory watcher noise in busy folders: If the user's ADIF file lives in a directory with heavy write traffic (e.g. a logging program's working directory with temp files), directoryChanged will fire for every unrelated file event. The current guard is lightweight (exists + contains check), so this is fine in practice, but worth noting in case users report CPU spikes on certain setups. A future optimisation could compare QFileInfo::lastModified before re-arming.

  2. Orphaned DxccAutoReloadAdif setting: The toggle's AppSettings key (DxccAutoReloadAdif) is no longer read or written, but existing users will have it sitting in their settings file. Not a bug — just a minor cleanup opportunity if you want to add a one-liner to remove it on startup or in a future migration pass.

  3. Flicker on locked-file retry: When onParseFailed fires, it emits importFinished (clearing "Updating..."), then re-arms the debounce timer. Two seconds later importAdifFile emits importStarted ("Updating..." again). On a file that stays locked for multiple retry cycles the stats label will toggle between "Updating..." and the old counts every ~3.5s. This is a cosmetic edge case — one option would be to not emit importFinished from onParseFailed when the debounce timer is being re-armed (i.e. keep the "Updating..." state until we actually succeed or give up).

  4. parseFile() static method now diverges from parseFileAsync(): The async path gained retry logic and calls parse(f.readAll()) directly, while the static parseFile() still does a single open-or-fail. That's fine for the current usage (only parseFileAsync is called from the watcher path), but worth keeping in mind if parseFile() is used elsewhere in the future.

All changes are within the stated scope (ADIF auto-reload plumbing), no files outside that boundary. Uses AppSettings correctly where applicable. No obvious null-pointer or resource-leak risks — the QFileSystemWatcher and QTimer are member-owned with proper cleanup in setAutoReload.

Thanks for the contribution!

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comprehensive edge-case handling: atomic rename (re-arm after debounce), write-lock (3× retry), delete-recreate (directory watcher), parse failure (preserves worked status). Worker-thread msleep is correct. Settings migration is silent/non-destructive. Author's call to remove the toggle makes sense given the broken-either-way state it was in. LGTM.

@ten9876 ten9876 merged commit ba91075 into aethersdr:main Apr 22, 2026
5 checks passed
ten9876 added a commit that referenced this pull request Apr 22, 2026
)

Cuts a community-contribution-heavy point release. Eight community PRs
landed plus three AetherClaude crash fixes. Highlights:

Community:
- Fix RADE RX not decoding on Windows (#1820, NF0T)
- Clamp stale DAX RX backlog on macOS — fixes +1.5s FT8 DT bias (#1822, jensenpat)
- Fix TCI DAX resampler crosstalk between slices (#1815, Chaosuk97)
- Fix RX applet pan slider with NR active (#1799, Chaosuk97)
- Fix TCI RX gain default 1.0 → 0.5 to match applet (#1811, NF0T)
- Fix HAVE_SERIALPORT guard (#1812, NF0T)
- Seamless ADIF logbook auto-reload (#1801, Chaosuk97)
- RAC Canada band-plan corrections (#1817, VE3NEM)

AetherClaude crash fixes:
- Applet reorder with floating containers (#1745#1746)
- Lazy-build RadioSetupDialog tabs, dodges Wayland FFmpeg/VDPAU crash (#1776#1777)
- PanAdapter float-freeze — show-after-reset + direct reparent (#1668#1669)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@M7HNF-Ian M7HNF-Ian deleted the fix/logbook-autoreload branch June 7, 2026 14:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants