feat(nets): Net Reminder Scheduler — recurring net reminders with one-click tuning#3684
Conversation
… 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.
|
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 failedOnly the two Linux jobs failed at the Build step —
Root cause
QDateTime candidate(candidateDate, time, tz);
if (!candidate.isValid()) {
candidate = QDateTime(candidateDate, time, tz,
QDateTime::TransitionResolution::PreferStandard);
}The (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 fixAny 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);
}
#endifOption B — drop the explicit policy (simplest, portable to 6.2+): the plain I'd lean toward Option B unless you specifically want 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.
There was a problem hiding this comment.
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 separation —
NetRecurrence/NetSchedulePlanner/NetScheduleStoreare pure QtCore and unit-tested, with theQObjectNetSchedulera thin timer-and-clock wrapper around the pure planner. Easy to reason about and test. - DST + Qt 6.4 floor —
zonedDateTime()deliberately avoidsQDateTime::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; theNetSchedule.jsonsidecar in theAppSettingsdirectory mirrors the existingPmsMailbox/Ax25HfPacketDecodeDialogprecedent.QSaveFilefor 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
firedKeyslogic correctly avoids double-arming the same occurrence.
Minor, non-blocking notes
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. AqWarning/status-bar line would aid diagnosis.zonedDateTime()DST-gap fallback (NetRecurrence.cpp) nudges withtime.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.- 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
left a comment
There was a problem hiding this comment.
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.
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
MemoryEntrytuning shape and the existingMemoryRecallPolicycommand 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 aMemoryEntrypresetNetRecurrence— RRULE subset (Daily / Weekly / Monthly-nth-weekday) plus one-time "Once", DST-correctnextOccurrence, friendlydescribeRuleNetSchedulePlanner— pure soonest-reminder-across-entries selectionNetScheduleStore— 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:
NetSchedulerDialog—PersistentDialogmanager mirroringMemoryDialog'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 operatorNetReminderBanner— 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-banddisplay pan set band=…preselect)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
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,
.icsexport, and banner snooze.💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat