Skip to content

feat(diagnostics): adaptive throttle visibility — heartbeat color + Network Diagnostics graphs#3203

Merged
ten9876 merged 5 commits into
aethersdr:mainfrom
chibondking:feature/adaptive-throttle-diagnostics
May 27, 2026
Merged

feat(diagnostics): adaptive throttle visibility — heartbeat color + Network Diagnostics graphs#3203
ten9876 merged 5 commits into
aethersdr:mainfrom
chibondking:feature/adaptive-throttle-diagnostics

Conversation

@chibondking
Copy link
Copy Markdown
Collaborator

@chibondking chibondking commented May 27, 2026

Summary

  • TitleBar heartbeat changes color while adaptive throttle is active, using the same quality-palette colors as the footer network label (Poor=red, Fair=amber, Good=cyan). Restores green when throttle lifts. Alarm/disconnect path is never overridden.
  • Network Diagnostics gains per-second adaptive throttle state in the sample history, amber shading bands on all 9 graphs during throttle-active spans, an inline badge on the Overview Status card, a new fps-cap step-function graph on the Rates tab, and a collapsible subsection on the Details tab with current state, pending-lift indicator, and session count.

Changed files

File Change
TitleBar.h / .cpp setThrottleFlashColor() + member; onHeartbeat() + freeze path use throttle color
MainWindow.cpp adaptiveThrottleChanged lambda calls setThrottleFlashColor()
NetworkDiagnosticsDialog.h ThrottleEvent struct; new sample fields; new dialog members
NetworkDiagnosticsDialog.cpp Amber band rendering, step-function series, badge, subsection, wiring

Test plan

  • Connect to radio with adaptive throttle disabled — heartbeat stays green, no badge visible in Network Diagnostics
  • Enable adaptive throttle, simulate poor link — heartbeat flashes throttle color (amber or red depending on cap), badge appears on Overview Status card with fps cap value
  • Open Network Diagnostics → Rates tab — fps-cap graph shows step trace; all graphs show amber shading during throttle-active window
  • Open Details tab — "Adaptive Frame-Rate Throttle" subsection visible once throttle has activated; session count increments per activation
  • Throttle lifts — badge disappears, heartbeat returns to green, subsection shows "Inactive" state
  • Blink disabled path: heartbeat stays correct throttle color statically; restores to green when throttle lifts

🤖 Generated with Claude Code

…etwork Diagnostics graphs

Adds end-to-end visibility for when the adaptive frame-rate throttle is
active, across the title-bar heartbeat indicator and the Network
Diagnostics dialog.

## TitleBar
- `setThrottleFlashColor(hex)` — callers pass a hex color that replaces
  the default #20c060 green while the throttle is active; empty string
  restores green when the throttle lifts.
- `onHeartbeat()` flashes the throttle color instead of hardcoded green.
- `setBlinkEnabled(false)` freeze path respects the throttle color.
- Disconnected-alarm path is never overridden.

## MainWindow
- `adaptiveThrottleChanged` lambda calls `setThrottleFlashColor()` using
  the same quality-palette colors as the footer network label:
  ≤4 fps → #cc3333 (Poor), ≤8 fps → #cc9900 (Fair),
  else → #00b4d8 (Good); inactive → "" (restore green).

## NetworkDiagnosticsHistory
- `NetworkDiagnosticsSample` gains `adaptiveFpsCap` and
  `adaptivePendingLift` fields, populated from `RadioModel` each second.
- `ThrottleEvent` struct records each activate/deactivate transition with
  timestamp and fps cap.
- `throttleSessionCount()` — cumulative count of throttle activations
  for the current session.
- `adaptiveThrottleChanged` wired in the constructor via UniqueConnection.

## TimeSeriesGraphWidget
- `setThrottleSpans(spans)` — accepts a vector of (startRatio, endRatio)
  pairs; renders a subtle amber (#cc9900, α 28) band over the matching
  x-range on every graph that receives spans.
- `Series::stepFunction` flag — draws horizontal-then-vertical steps
  instead of a smoothed line; used for the fps-cap graph.

## NetworkDiagnosticsDialog
- **Overview Status card**: inline amber badge ("Adaptive throttle: N fps
  cap") shown while throttle is active; hidden otherwise.
- **Details tab**: new "Adaptive Frame-Rate Throttle" subsection with
  current state, pending-lift indicator, and session count; hidden until
  the first throttle event fires.
- **Rates tab**: 120–160 px fps-cap step-function graph below the stream
  rates graph; shows the cap level over time as a step trace.
- `updateCharts()`: builds throttle spans from the event log and passes
  them to all nine graphs (4 overview + 5 detail tabs).
- `refresh()`: updates badge text, section visibility, and label values
  each second.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@chibondking chibondking requested a review from a team as a code owner May 27, 2026 02:34
@chibondking
Copy link
Copy Markdown
Collaborator Author

@AetherClaude Your thoughts on this PR? Anything that will affect ongoing work?

@aethersdr-agent
Copy link
Copy Markdown
Contributor

Reviewed against the current heartbeat and diagnostics code. No conflicts with in-flight work — this is a clean visibility layer on top of infrastructure that already exists. A few observations worth pinning down before/while implementing:

1. Alarm path precedence in setThrottleFlashColor(). The PR description correctly says the alarm/disconnect path is never overridden, but in TitleBar.cpp today the heartbeat color is mutated from three places — onHeartbeat(), onHeartbeatLost(), and the alarm/off timers. The new setter needs to be a no-op when m_missedBeats >= 3 or m_heartbeatAlarmTimer->isActive(), and onHeartbeat() (line 1037) needs to consult the current throttle color instead of hardcoding #20c060. Suggest storing m_throttleColor (empty/QString() = "use green") and routing through a small helper:

// TitleBar.cpp
QString TitleBar::currentBeatColor() const {
    return m_throttleColor.isEmpty() ? QStringLiteral("#20c060") : m_throttleColor;
}

void TitleBar::onHeartbeat() {
    m_discovering = false;
    m_missedBeats = 0;
    m_heartbeatAlarmTimer->stop();
    m_alarmRed = false;
    m_heartbeat->setStyleSheet(
        QStringLiteral("QLabel { background: %1; border-radius: 5px; }").arg(currentBeatColor()));
    if (m_blinkEnabled) m_heartbeatOffTimer->start();
}

void TitleBar::setThrottleFlashColor(const QString& color) {
    if (m_throttleColor == color) return;
    m_throttleColor = color;
    // Don't fight the alarm — alarm timer / static-red path owns the indicator.
    if (m_missedBeats >= 3 || m_heartbeatAlarmTimer->isActive()) return;
    // Blink-disabled freeze path: repaint immediately to the new static color.
    if (!m_blinkEnabled && !m_heartbeatOffTimer->isActive()) {
        m_heartbeat->setStyleSheet(
            QStringLiteral("QLabel { background: %1; border-radius: 5px; }").arg(currentBeatColor()));
    }
}

2. setBlinkEnabled() freeze logic at TitleBar.cpp:1089-1094 also hardcodes #20c060 for the "freeze to solid green" branch — that needs to use currentBeatColor() too, otherwise toggling blink off during a throttle event snaps back to green and the throttle cue is lost until the next discovery packet.

3. Quality-palette single source of truth. The footer network label already owns the Poor/Fair/Good color mapping. The MainWindow lambda at MainWindow.cpp:4652-4655 already has active and fpsCap in scope — pass through the same helper the footer uses (not a duplicated hex literal in MainWindow) so the two surfaces can't drift. If that mapping isn't a free function yet, extracting it is worth the small refactor.

4. pendingThrottleLift() is already exposed on RadioModel and used at MainWindow.cpp:4633 — the Details-tab "pending-lift indicator" should read directly from that rather than carrying its own derived flag in the sample history.

5. Step-function fps-cap graph. Worth confirming TimeSeriesGraphWidget supports step (zero-order-hold) rendering; if it only does linear interpolation between samples, the cap transitions will look like ramps. May need a setStepMode(true) option or a tiny render-side branch.

6. Sample-vector growth. NetworkDiagnosticsSample is already 100+ bytes; adding throttle fields is fine, but the ThrottleEvent struct should live outside the per-second sample (a separate QVector<ThrottleEvent> for span rendering) rather than duplicating start/end markers into every sample row. Cleaner and cheaper for the amber-band renderer.

Nothing here blocks the design — it's a useful diagnostic surface and the infrastructure (adaptiveThrottleChanged signal, m_adaptiveFpsCap, pendingThrottleLift()) is already in place. Main risk is the heartbeat state-machine interactions in point 1/2; getting those right matters because the alarm cue is safety-critical for contest operators (per the existing comment at TitleBar.cpp:1054-1056).

A maintainer can authorize the orchestrator to land this fix by adding the aetherclaude-eligible label.

73, Jeremy KK7GWY & Claude (AI dev partner)


🤖 aethersdr-agent · cost: $2.9387 · model: claude-opus-4-7

…lity

Addresses all six points from the AetherClaude review of PR aethersdr#3203.

1. Alarm-path precedence (TitleBar): add currentBeatColor() private helper
   that resolves m_throttleFlashColor or falls back to #20c060.
   setThrottleFlashColor() is now a no-op when m_missedBeats >= 3 or
   m_heartbeatAlarmTimer->isActive(); adds early-exit guard and
   idempotency check (no-op when color unchanged).

2. setBlinkEnabled() freeze path: the "freeze to solid connected color"
   branch now calls currentBeatColor() instead of hardcoding #20c060,
   so toggling blink off during a throttle event preserves the throttle
   color cue.

3. Quality-palette single source of truth (MainWindow): extract
   qualityColor(quality) and fpsCapColor(fpsCap) lambdas before both
   connects so the footer label and heartbeat indicator share identical
   hex values — no drift possible.

4. pendingThrottleLift() read live from RadioModel in refresh() instead
   of the sample-derived flag — more accurate for state that can change
   between 1-second sample polls.

5. Step-function rendering: already present (Series::stepFunction +
   paintEvent branch) — confirmed correct, no change needed.

6. ThrottleEvent separate from sample vector: already correct
   (QVector<ThrottleEvent> lives in NetworkDiagnosticsHistory, samples
   only carry a snapshot int+bool) — no change needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@chibondking
Copy link
Copy Markdown
Collaborator Author

chibondking commented May 27, 2026

@AetherClaude Addressed all six review points in 7018add:

  1. Alarm-path precedence: Added currentBeatColor() helper. setThrottleFlashColor() now no-ops when m_missedBeats >= 3 or m_heartbeatAlarmTimer->isActive(). Added idempotency check.

  2. setBlinkEnabled() freeze path: All three "freeze to connected color" sites now call currentBeatColor() — throttle color is preserved when blink is toggled off mid-throttle.

  3. Quality-palette single source of truth: Extracted qualityColor(quality) and fpsCapColor(fpsCap) lambdas in MainWindow before both connects. Footer label and heartbeat indicator share identical hex values.

  4. pendingThrottleLift() live read: refresh() now calls m_model->pendingThrottleLift() directly instead of sample.adaptivePendingLift.

  5. Step-function rendering: Already present (Series::stepFunction flag + paintEvent horizontal-then-vertical branch) — confirmed correct, no further change.

  6. ThrottleEvent separate from sample vector: Already correct (QVector<ThrottleEvent> in history, samples only carry snapshot int+bool) — no further change.

@chibondking
Copy link
Copy Markdown
Collaborator Author

@AetherClaude All six points from your initial review have been addressed in commit 7018add. Requesting a second pass to confirm the changes are correct, particularly:

  • The currentBeatColor() helper and the alarm-path guard in setThrottleFlashColor() (points 1–2)
  • The qualityColor/fpsCapColor lambda extraction so footer and heartbeat can't drift (point 3)
  • pendingThrottleLift() live read from RadioModel in refresh() (point 4)

Points 5 and 6 were already correct in the original commit — step-function rendering via Series::stepFunction and ThrottleEvent in a separate QVector outside the sample struct.

@aethersdr-agent
Copy link
Copy Markdown
Contributor

Second pass complete — all six points are correctly addressed in 7018add. Walking through each against the diff:

1. currentBeatColor() + alarm-path guard ✅setThrottleFlashColor() correctly no-ops on m_missedBeats >= 3 || m_heartbeatAlarmTimer->isActive(), and the idempotency check (if (m_throttleFlashColor == color) return;) avoids spurious repaints when the same color fires repeatedly. The onHeartbeat() rewrite at TitleBar.cpp:1037 now consults currentBeatColor() instead of the hardcoded #20c060 — alarm precedence preserved.

2. setBlinkEnabled() freeze path ✅ — the mid-flash freeze branch at TitleBar.cpp:1090-1093 now uses currentBeatColor(), so toggling blink off mid-throttle no longer snaps back to green. The static-red branch (alarm/lost) is intentionally unchanged — correct, since that's the safety-critical path that must show red regardless of throttle state.

3. qualityColor / fpsCapColor lambdas ✅ — both connect bodies capture the lambdas, and the footer label + heartbeat indicator now share identical hex values. Footer/heartbeat drift is impossible without editing both lambdas in the same scope. Minor nit if you want to follow up: fpsCapColor could derive a quality string and call qualityColor instead of duplicating the three hex literals, so a future palette change only edits one place — but the lambdas are co-located on adjacent lines so this is well within "fine as-is" territory.

4. pendingThrottleLift() live read ✅refresh() now reads m_model->pendingThrottleLift() directly. The badge and Details subsection both reflect the current dwell state rather than the up-to-1-second-stale sample snapshot.

5. Step-function rendering ✅ — confirmed at NetworkDiagnosticsDialog.cpp:541-557. Horizontal-then-vertical traversal is correct for zero-order hold, and the "extend last step to right edge" line is the right touch — without it the trailing segment would terminate at the most recent sample x instead of "now", which looks wrong on a live graph.

6. ThrottleEvent separate vector ✅ — confirmed. QVector<ThrottleEvent> m_throttleEvents in the history, sample struct only carries int adaptiveFpsCap + bool adaptivePendingLift. Span rendering iterates the event vector cleanly.


Two small follow-ups worth noting (not blockers for this PR):

  • sample.adaptivePendingLift is now dead. Now that refresh() reads pendingThrottleLift() live from the model, the field is written in NetworkDiagnosticsHistory::sampleNow() but never read. Either drop it, or keep it if you anticipate a future "pending-lift intervals" overlay on the fps-cap graph (the historical record would be useful for that).

  • m_throttleEvents grows unbounded. Each throttle on/off pair appends two entries that live for the session. Worth pruning entries older than the longest graph range (10 min or whatever the max selector offers) — a session that flaps throttle for hours during a contest could accumulate a few thousand entries, which is harmless but unnecessary.

  • Amber band color is hardcoded. QColor("#cc9900") at NetworkDiagnosticsDialog.cpp:479 duplicates the Fair hex from qualityColor. Same drift concern as point 3; if you make qualityColor accessible (free function or static) you could share it here too. Cosmetic.

LGTM as-is — the state-machine interactions in TitleBar are correct and the alarm path is properly protected. The follow-ups above can land in a separate cleanup if at all.

A maintainer can authorize the orchestrator to land any of the follow-ups by adding the aetherclaude-eligible label.

73, Jeremy KK7GWY & Claude (AI dev partner)


🤖 aethersdr-agent · cost: $3.4523 · model: claude-opus-4-7

…pass

Three follow-ups from the second AetherClaude review of PR aethersdr#3203:

1. Drop dead field: `NetworkDiagnosticsSample::adaptivePendingLift` was
   written in sampleNow() but never read after refresh() was changed to
   call m_model->pendingThrottleLift() directly. Removed from the struct
   and from sampleNow().

2. Prune throttle event vector: m_throttleEvents now gets pruned in
   pruneSamples() on the same kMaxHistoryMs window as the sample vector,
   preventing unbounded growth during long-running / throttle-flapping
   sessions.

3. Single amber constant: introduce kThrottleAmber = "#cc9900" at file
   scope (matching qualityColor("Fair") in MainWindow.cpp). All four
   usages — band fill, badge stylesheet, state label stylesheet, fps-cap
   series color — now reference this one constant.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@chibondking
Copy link
Copy Markdown
Collaborator Author

@AetherClaude All three follow-ups from your second pass addressed in 889c323:

  1. Dead field droppedNetworkDiagnosticsSample::adaptivePendingLift removed from the struct and from sampleNow(). refresh() continues to read m_model->pendingThrottleLift() live.

  2. m_throttleEvents prunedpruneSamples() now erases entries older than kMaxHistoryMs immediately after the sample compaction pass, using the same cutoff. No unbounded growth.

  3. Single amber constantkThrottleAmber = "#cc9900" defined at file scope above TimeSeriesGraphWidget. All four usages (band fill, badge stylesheet, state label stylesheet, fps-cap series color) reference it. Comment notes it must match qualityColor("Fair") in MainWindow.cpp.

@aethersdr-agent
Copy link
Copy Markdown
Contributor

Third pass complete — all three follow-ups in 889c323c are correctly applied. Walking the diff:

1. Dead field removed ✅adaptivePendingLift is gone from NetworkDiagnosticsSample (NetworkDiagnosticsDialog.h:58) and from the sampleNow() write site (NetworkDiagnosticsDialog.cpp:2528). Grepped for any remaining reads — none. The struct is now lean and refresh() consults the model live, which is the right shape.

2. Throttle-event pruning ✅pruneSamples() now erases events older than kMaxHistoryMs using std::remove_if (NetworkDiagnosticsDialog.cpp:2603-2609). Runs immediately after m_samples = std::move(compacted) on the same clock tick, so the two histories stay in lockstep. The kMaxHistoryMs = 7 days window is generous (the longest graph range is ~10 min), so memory is genuinely a non-issue now even under hours of flapping.

3. Single amber constant ✅kThrottleAmber = "#cc9900" at NetworkDiagnosticsDialog.cpp:313-315 with a comment pointing back to qualityColor("Fair") in MainWindow. All four sites updated:

  • Band fill: NetworkDiagnosticsDialog.cpp:484
  • Badge stylesheet: NetworkDiagnosticsDialog.cpp:946-948 (via QStringLiteral(...).arg(...))
  • State-label stylesheet: NetworkDiagnosticsDialog.cpp:2836-2837
  • fps-cap series color: NetworkDiagnosticsDialog.cpp:2979

The comment is the right call — without it, a future palette refactor in MainWindow would silently desync from this file. Cross-file constants always need a breadcrumb.


One theoretical edge case worth noting (not a blocker):

The pruning erases events strictly by timestamp, so it's possible for a "lone close" event to survive — i.e., a throttle session that started >7 days ago but ended within the last 7 days would leave an orphaned active=false event with no matching active=true. The span builder at NetworkDiagnosticsDialog.cpp:2962-2974 handles a leading active=false by silently skipping it (spanStart=-1.0 never set), so the in-progress portion that's still within the visible graph window would render without amber shading. The fps-cap step series and Details tab are unaffected (they read from samples / live model state).

This requires a throttle session lasting 7+ days, which doesn't happen in practice — and even if it did, the cosmetic loss is partial-band shading on graphs that don't extend back that far anyway. Mentioning it for the record, not asking for a change.

LGTM as-is. The PR is in good shape to land.

73, Jeremy KK7GWY & Claude (AI dev partner)


🤖 aethersdr-agent · cost: $3.2669 · model: claude-opus-4-7

Copy link
Copy Markdown
Contributor

@aethersdr-agent aethersdr-agent Bot left a comment

Choose a reason for hiding this comment

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

Thanks @chibondking — nice piece of work. The single-source-of-truth color lambdas in MainWindow.cpp, the symmetry between kThrottleAmber and qualityColor("Fair"), and the currentBeatColor() indirection in TitleBar all make the throttle visibility feel like a first-class concept rather than a bolted-on indicator. The amber spans + step-function fps-cap trace will make adaptive-throttle behaviour much easier to reason about in the field.

A few observations:

1. NetworkDiagnosticsHistory::m_currentFpsCap not seeded at construction (functional)

src/gui/NetworkDiagnosticsDialog.cpp:2386-2416m_currentFpsCap defaults to 0 and is only updated via the adaptiveThrottleChanged signal. If the diagnostics dialog is opened while throttle is already active, the signal won't fire again until the next state transition, so sample.adaptiveFpsCap will be recorded as 0 for every sample in between. That means:

  • the badge stays hidden (because liveFpsCap > 0 is false) even though throttle is active
  • the fps-cap step graph starts at 0 until the next transition
  • the Details subsection won't appear until first transition

RadioModel::currentAdaptiveFpsCap() already exists and returns the right value (gates on AdaptiveThrottleEnabled per #3201). Seeding once in the constructor would close this:

m_currentFpsCap = m_model->currentAdaptiveFpsCap();
if (m_currentFpsCap > 0) {
    ThrottleEvent ev{QDateTime::currentMSecsSinceEpoch(), true, m_currentFpsCap};
    m_throttleEvents.push_back(ev);
    ++m_throttleSessionCount;
}

Worth doing — opening the diagnostics dialog during a degraded session is exactly when users will reach for it.

2. liveFpsCap naming is slightly misleading (nit)

src/gui/NetworkDiagnosticsDialog.cpp:2812 — the local is named liveFpsCap but it's read from sample.adaptiveFpsCap, which is up to 1s stale. Compare with the adjacent comment-and-code pattern for pendingLift which is correctly described as "live from the model". Either rename to sampledFpsCap, or read this too from m_model->currentAdaptiveFpsCap() for true liveness. Not a bug, just clarity.

3. Hard-coded layout index for badge insertion (worth a comment expansion)

src/gui/NetworkDiagnosticsDialog.cpp:947lay->insertWidget(2, m_throttleBadge) relies on makeHealthCard()'s internal layout having exactly {value, hint, stretch} at indices 0/1/2. The inline comment notes this, but it would silently misposition if makeHealthCard ever gains a row. Consider locating the stretch dynamically (e.g. iterate items, insert before the first stretch) — but optional.

Other checks

  • AetherSDR conventions: no QSettings in changed files (uses AppSettings); RAII clean; Qt::UniqueConnection on the throttle listener is the right call.
  • Null safety: m_history, m_throttleBadge, m_throttleSection, etc. are all consistently guarded before deref. ✓
  • Resource leaks: graphs and labels are all parented; m_throttleEvents pruning at line 2602-2609 prevents unbounded growth. ✓
  • Scope: all five files are squarely on-topic for the PR.
  • Step-function rendering: the mapPoint lambda extraction is a clean refactor; step path correctly extends to the right edge. ✓

Nothing blocking from my side — (1) is the only one that'd be nice to address before merge since it changes the dialog-during-active-throttle UX from "incomplete data" to "complete data". (2) and (3) are polish.


🤖 aethersdr-agent · cost: $12.6837 · model: claude-opus-4-7

@chibondking chibondking self-assigned this May 27, 2026
…uning

After pruning m_throttleEvents by timestamp, a throttle session whose
open event fell before the cutoff but whose close event survived would
leave an orphaned active=false entry at the head of the vector.

The span builder already skips leading close-events gracefully (spanStart
stays -1), so this was cosmetically harmless. Removing the orphans anyway
keeps the vector clean and makes the invariant (all close-events have a
matching open-event before them) explicit rather than assumed.

Raised as a theoretical edge case in the third AetherClaude review pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@chibondking
Copy link
Copy Markdown
Collaborator Author

@AetherClaude Addressed the theoretical edge case from your third pass in 32abd7f.

After pruneSamples() removes throttle events older than kMaxHistoryMs, a session that started before the cutoff but ended after would leave an orphaned active=false at the head of m_throttleEvents. The span builder already skips it (leading close-event, spanStart stays at -1), so there was no visible bug — but the invariant (every close-event has a matching open-event before it) is now enforced explicitly:

while (!m_throttleEvents.isEmpty() && !m_throttleEvents.first().active)
    m_throttleEvents.removeFirst();

Requesting final sign-off. Any remaining concerns before this is eligible to land?

@aethersdr-agent
Copy link
Copy Markdown
Contributor

Final pass complete — 32abd7f3 correctly closes the edge case. Verified against the diff:

Orphaned close-event pruning ✅ — The while loop at NetworkDiagnosticsDialog.cpp:2610-2611 runs immediately after the timestamp-based std::remove_if, on the same pruneSamples() tick. It correctly:

  • Short-circuits on empty vector (m_throttleEvents.isEmpty() first in the conjunction).
  • Handles N orphaned closes at the head (not just one), though in practice you'd only ever see one — a session can't have two closes without an open between them.
  • Leaves the symmetric tail case alone (a trailing active=true with no matching close is the normal in-progress session, and the span builder at NetworkDiagnosticsDialog.cpp:2981-2982 already extends those to "now" / right edge).

Post-condition is now the explicit invariant: the vector either starts with an active=true event or is empty. The span builder's spanStart >= 0.0 guard remains as defensive belt-and-suspenders, which is the right call — invariants in one part of the code shouldn't make readers of another part chase upstream to confirm.

The commit message is also worth noting — it correctly characterizes this as cosmetic-cleanup-of-an-already-handled-case rather than a bug fix, which matches reality. Future maintainers grepping for "why this loop" will find the rationale without having to reconstruct it.


No remaining concerns. The PR is in good shape to land:

  • All six original review points addressed (7018add1)
  • All three follow-ups addressed (889c323c)
  • Edge case explicit-invariant cleanup (32abd7f3)
  • Alarm-path precedence preserved throughout — setThrottleFlashColor() no-ops on alarm/missed-beats; setBlinkEnabled() static-red branch untouched
  • Single source of truth for the quality palette (footer + heartbeat share qualityColor/fpsCapColor lambdas; amber band shares kThrottleAmber constant with a breadcrumb comment)
  • pendingThrottleLift() read live from the model
  • Bounded memory: both sample and event vectors pruned to kMaxHistoryMs

LGTM — eligible to land. A maintainer can authorize the orchestrator by adding the aetherclaude-eligible label.

73, Jeremy KK7GWY & Claude (AI dev partner)


🤖 aethersdr-agent · cost: $2.0208 · model: claude-opus-4-7

…session

- NetworkDiagnosticsHistory now seeds m_currentFpsCap from
  RadioModel::currentAdaptiveFpsCap() in its constructor and pushes a
  synthetic open ThrottleEvent if throttle is already engaged. Without
  this seed, opening Network Diagnostics during a stably-degraded
  session leaves the badge hidden, the fps-cap step graph reading 0,
  and the Details subsection invisible until the next state transition
  fires adaptiveThrottleChanged — which on a stable Poor/Fair link may
  not arrive for a long time.

- Promote RadioModel::currentAdaptiveFpsCap() from private to public so
  diagnostics consumers can read it. Mirrors the pendingThrottleLift()
  exposure pattern already on line 886. Internal call sites unchanged.

- Rename refresh()'s local liveFpsCap → sampledFpsCap to make explicit
  that the value is read from the 1 Hz sample history (up to 1 s
  stale), parallel to the pendingLift comment immediately below.

- Q_ASSERT that the just-added Rates tab is last before inserting the
  fps-cap step graph, so a future reorder of makeGraphTab() calls
  fails loudly instead of silently landing on the wrong tab.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ten9876
Copy link
Copy Markdown
Collaborator

ten9876 commented May 27, 2026

Claude here — pushed the recommended fixes on top of your PR (commit 7866ab72) on @ten9876's go-ahead:

  1. NetworkDiagnosticsHistory constructor seeds from currentAdaptiveFpsCap() so the badge / step graph / Details subsection populate correctly when the dialog opens during an already-active throttle session (AetherClaude obs. 1).
  2. currentAdaptiveFpsCap() promoted from private → public in RadioModel.h — mirrors pendingThrottleLift()'s exposure pattern. Internal call sites unchanged.
  3. Renamed liveFpsCapsampledFpsCap in refresh() to match the up-to-1s-stale sample-history origin (AetherClaude obs. 2).
  4. Q_ASSERT on the Rates-tab-is-last invariant before inserting the fps-cap step graph, so a future reorder of makeGraphTab calls fails loudly instead of silently landing on the wrong tab.

Verified locally with an incremental MSVC-compatible build. CI will exercise check-windows when it picks this up. Pinging once green so you don't have to refresh.

Thanks for the work on this — the throttle indicator going colour-coded in the TitleBar is going to make adaptive behaviour way more legible in the field.

73, Jeremy KK7GWY & Claude (AI dev partner)

@ten9876 ten9876 merged commit d889bb0 into aethersdr:main May 27, 2026
4 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