Skip to content

feat(nets): Net Reminder Scheduler — recurring net reminders with one-click tuning#3684

Merged
ten9876 merged 4 commits into
aethersdr:mainfrom
jensenpat:feat/net-reminder-scheduler
Jun 20, 2026
Merged

feat(nets): Net Reminder Scheduler — recurring net reminders with one-click tuning#3684
ten9876 merged 4 commits into
aethersdr:mainfrom
jensenpat:feat/net-reminder-scheduler

Conversation

@jensenpat

@jensenpat jensenpat commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator
Screenshot 2026-06-20 at 6 59 15 AM Screenshot 2026-06-20 at 7 00 02 AM

Net Reminder Scheduler

Adds a personal Net Reminder Scheduler: operators add their favorite recurring nets (recurring on-air gatherings on a fixed frequency/band/mode at a regular time), get a desktop + in-app reminder before each one starts, and click Tune Now to jump straight to it — with the saved mode and filter. Think of it as a memory channel plus a recurrence rule and a custom name.

Design

A net is conceptually a memory channel + a recurrence + a reminder. It reuses the MemoryEntry tuning shape and the existing MemoryRecallPolicy command builders, but — unlike radio memories, which are radio-authoritative and live in the radio's slots — a net is operator-scoped client state (Constitution Principle XIII): it persists locally as versioned JSON, works with no radio connected, and never consumes a radio memory slot.

Recurrence is stored as an RFC 5545 RRULE string + IANA timezone + local time-of-day, never a precomputed instant, so "20:00 local" stays correct across DST. The firing instant is computed lazily via QTimeZone.

What's included

Headless core (pure QtCore, unit-tested):

  • NetEntry — data model embedding a MemoryEntry preset
  • NetRecurrence — RRULE subset (Daily / Weekly / Monthly-nth-weekday) plus one-time "Once", DST-correct nextOccurrence, friendly describeRule
  • NetSchedulePlanner — pure soonest-reminder-across-entries selection
  • NetScheduleStore — versioned, tolerant JSON with UUID-keyed merge (skip / overwrite / duplicate)
  • NetScheduler — single recompute-and-rearm timer (24h-capped; self-heals across suspend / clock change / DST)

UI + wiring:

  • NetSchedulerDialogPersistentDialog manager mirroring MemoryDialog's table + import/export, with a friendly add/edit sheet: recurrence preset + weekday chips / monthly ordinal / one-time date, lead-time presets, Capture current VFO, and a live "Next: …" preview — no raw RRULE shown to the operator
  • NetReminderBanner — in-app actionable toast; the guaranteed "Tune Now" path (unaffected by OS notification permissions / Do-Not-Disturb)
  • MainWindow_Nets.cpp — loads/persists the schedule, runs the reminder timer, fires the banner + a best-effort OS tray notification, and tunes via the shared recall builders (including the cross-band display pan set band=… preselect)
  • Settings ▸ Net Scheduler… menu entry

Why a tray balloon and an in-app banner

QSystemTrayIcon::showMessage() cannot carry an action button and its click signal is unreliable on macOS. So the reliable "Tune Now" button lives in the in-app banner; the OS notification is only the attention-getter that raises the window.

Testing

  • 3 standalone unit-test targets (recurrence incl. DST + one-time, JSON round-trip, planner) — all green
  • Full macOS app build clean
  • Verified live: reminder fires at lead time; both the macOS toast and the in-app banner appear; Tune Now lands on the correct frequency/mode, including an HF→2 m cross-band switch

Notes for review

Per AGENTS.md, visual/UX specifics are the maintainer's call — the editor layout, the bottom-right banner placement, and the lazily-created tray icon are functional defaults flagged for design review. No radio-authoritative state is persisted client-side.

Follow-ups (not in this PR)

Seeding from RepeaterBook / hamnets.org directories, live "net is on now" via the NetLogger feed, .ics export, and banner snooze.

💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat

… timer

Operator-scoped Net Reminder Scheduler core (no GUI, no radio coupling):
NetEntry reuses the MemoryEntry tuning shape but persists client-side as
versioned JSON rather than radio memory slots (Constitution XIII).

- NetRecurrence: RFC 5545 RRULE subset (DAILY/WEEKLY/MONTHLY-nth-weekday),
  DST-correct nextOccurrence via QTimeZone, friendly describeRule()
- NetSchedulePlanner: pure soonest-reminder-across-entries selection
- NetScheduleStore: versioned, tolerant, UUID-keyed merge (skip/overwrite/dup)
- NetScheduler: single recompute-and-rearm timer over the pure planner
- 3 standalone unit tests (recurrence incl. DST, store round-trip, planner)

Principle 8.
Wires the headless net-scheduler core into the GUI:

- NetSchedulerDialog: PersistentDialog manager mirroring MemoryDialog's
  table + import/export, with a friendly add/edit sheet (recurrence preset +
  weekday chips / monthly ordinal, lead-time presets, live "Next: …" preview,
  Capture-current-VFO) — no raw RRULE shown to the operator
- NetReminderBanner: in-app actionable toast — the guaranteed "Tune Now" path
- MainWindow_Nets.cpp: loads/persists the schedule as client-side JSON, runs
  the reminder timer, fires the banner + best-effort tray notification, and
  tunes via the shared MemoryRecallPolicy builders (no radio memory slot used)
- Settings ▸ Net Scheduler… menu entry

Full app build clean; startup smoke test passes; 3/3 core tests green.

Principle 13.
- Repeat = Never: an empty RRULE now means a single event at startDate +
  timeOfDay (NetRecurrence one-time path; describeRule → "Once"). Editor gains
  an 'Once (no repeat)' option with a date picker; new nets still default to
  Weekly, saved one-time nets reload as Once. Enables same-day testing.
- tuneToNet: a cross-band net no longer silently fails to tune. Preselect the
  target band's stack memory (display pan set band=…) before the slice tune,
  exactly like a memory recall, then reveal/recenter the pan. The frequency was
  always stored in the JSON NetEntry — no memory subsystem involved; the missing
  piece was the band change.
- Tests: one-time occurrence (future/past/same-day/no-date) + describeRule.

Principle 8.
@jensenpat jensenpat marked this pull request as ready for review June 20, 2026 14:03
@jensenpat jensenpat requested review from a team as code owners June 20, 2026 14:03
@aethersdr-agent

Copy link
Copy Markdown
Contributor

Thanks for this, @jensenpat — the Net Reminder Scheduler is a genuinely nice feature, and the DST-correct recurrence math is the right way to do it. The CI failure is a small portability issue, not a design problem. 👍

What failed

Only the two Linux jobs failed at the Build step — build and analyze (cpp) (CodeQL compiles the project before it can analyze). check-macos passed and check-windows was still running. That "Linux-only compile failure, macOS green" split is almost always a Qt-version mismatch, and that's what this is.

  • The Linux jobs build inside the CI container (ghcr.io/ten9876/aethersdr-ci:latest), which ships the project's minimum supported QtCMakeLists.txt declares find_package(Qt6 6.2 REQUIRED ...).
  • macOS (Homebrew qt@6) and Windows (install-qt-action, 6.8.x) build against a much newer Qt, so the code compiles there.

Root cause

src/core/NetRecurrence.cpp uses QDateTime::TransitionResolution::PreferStandard:

QDateTime candidate(candidateDate, time, tz);
if (!candidate.isValid()) {
    candidate = QDateTime(candidateDate, time, tz,
                          QDateTime::TransitionResolution::PreferStandard);
}

The QDateTime::TransitionResolution enum and the 4-argument QDateTime(QDate, QTime, QTimeZone, TransitionResolution) constructor were introduced in Qt 6.7 (QDateTime docs). They don't exist on the 6.2-era Qt in the Linux container, so the build breaks there. It appears twice in NetRecurrence.cpp — the one-time ("Once") path and the day-scan loop — so both call sites need the same treatment.

(Heads-up on transparency: the CI log API I have only exposes step status, not the raw compiler output, so the above is from reading the diff against the Linux Qt floor rather than a quoted error line.)

How to fix

Any of these works — pick based on how much you want to keep the explicit DST policy:

Option A — guard the 6.7 path (keeps the explicit policy where available):

QDateTime candidate(candidateDate, time, tz);
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
if (!candidate.isValid()) {
    candidate = QDateTime(candidateDate, time, tz,
                          QDateTime::TransitionResolution::PreferStandard);
}
#endif

Option B — drop the explicit policy (simplest, portable to 6.2+): the plain QDateTime(date, time, tz) constructor already resolves DST gaps/overlaps deterministically on all supported Qt versions; you only lose the ability to name the policy. Removing the fallback branch makes the failure go away with no behavioral regression on the cases your unit tests cover.

I'd lean toward Option B unless you specifically want PreferStandard semantics on newer Qt, in which case Option A preserves both. Either way, please re-run the recurrence/DST unit test (net_recurrence_test) after the change so we keep the spring-forward/fall-back coverage green.

No reviewer (Copilot or otherwise) comments are tied to this — it's purely the Qt 6.7 API vs. the 6.2 build floor. Once that's adjusted the Linux build should go green. Thanks again for the contribution!


🤖 aethersdr-agent · cost: $5.4902 · model: claude-opus-4-8

QDateTime::TransitionResolution is Qt 6.7+; the Linux CI Qt is 6.4.2, so the
build failed there (macOS dev Qt is newer and compiled fine). Replace it with a
version-portable zonedDateTime() helper that handles the spring-forward gap by
nudging an hour past the transition.

Principle 12.

@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.

Reviewed the full diff against AetherSDR conventions, correctness, scope, and system-boundary handling. This is a notably clean, well-architected contribution — thanks @jensenpat. All six checks (build, macOS, Windows, CodeQL, accessibility) are green.

What's solid

  • Headless/GUI separationNetRecurrence / NetSchedulePlanner / NetScheduleStore are pure QtCore and unit-tested, with the QObject NetScheduler a thin timer-and-clock wrapper around the pure planner. Easy to reason about and test.
  • DST + Qt 6.4 floorzonedDateTime() deliberately avoids QDateTime::TransitionResolution (6.7+) and stores RRULE + IANA tz + local time-of-day rather than a precomputed instant. That's exactly right for the CI Qt floor and for staying correct across transitions.
  • Conventions — no QSettings; the NetSchedule.json sidecar in the AppSettings directory mirrors the existing PmsMailbox/Ax25HfPacketDecodeDialog precedent. QSaveFile for atomic writes, proper Qt parenting throughout (no leaks), tolerant/versioned JSON with UUID-keyed merge.
  • Boundary handling — invalid timezone, invalid time-of-day, and file-open failures are all guarded; the planner's re-fire and firedKeys logic correctly avoids double-arming the same occurrence.

Minor, non-blocking notes

  1. persistNetSchedule() swallows a failed write silently (MainWindow_Nets.cpp:80-84). It's best-effort local cache, but unlike the export path (which warns), a failed save here gives the operator no signal their schedule was lost. A qWarning/status-bar line would aid diagnosis.
  2. zonedDateTime() DST-gap fallback (NetRecurrence.cpp) nudges with time.addSecs(3600), which for a late-night net (e.g. 23:30) in a spring-forward zone would wrap past midnight onto the wrong calendar day. You already note nets at 20:00 essentially never hit the ~02:00 gap, so this is negligible — just flagging the theoretical edge.
  3. Tray icon persists once lazily created even if all nets are later removed (already called out in your PR notes as a design-review item).

None of these need to block. Nice work — the recurrence math and self-healing timer design are particularly well done.


🤖 aethersdr-agent · cost: $7.3808 · model: claude-opus-4-8

@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.

Approving — RFC #3687 approved. Deep-reviewed: headless core correct (DST/recurrence verified empirically, timer safe, store robust, Principle V), tuning honors Principle I (recall builders only, no memory-slot writes, no-radio no-op), operator-scoped per Principle XIII, cross-platform reminder degradation clean, 3 unit tests green, CI green. Non-blocking nits noted on the RFC for fast-follow. Excellent work @jensenpat.

@ten9876 ten9876 merged commit 99824da into aethersdr:main Jun 20, 2026
6 checks passed
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