Skip to content

Implement workout queue, performance management chart, and WASM suppo…#174

Merged
MaximumTrainer merged 3 commits into
masterfrom
feat/issue-144-workout-history
Apr 21, 2026
Merged

Implement workout queue, performance management chart, and WASM suppo…#174
MaximumTrainer merged 3 commits into
masterfrom
feat/issue-144-workout-history

Conversation

@MaximumTrainer

Copy link
Copy Markdown
Owner

…rt (#167)

  • Add sensor_dropout_enabled (bool, default true) and sensor_dropout_timeout_s (int, default 5s, range 2–30s) to Account with QSettings persistence
  • WorkoutDialog: 1-Hz watchdog timer checks last power timestamp; on dropout pauses workout and shows overlay message; auto-resumes after 3s countdown when signal is restored
  • Manual resume during dropout pause clears dropout state cleanly
  • DialogConfig Trainer tab: new 'Sensor Dropout' group with enable toggle and timeout spinner
  • WorkoutPlot: removeAlertMessage() method to dismiss alert overlay
  • BtleHub: subscribe to Battery Service (0x180F / characteristic 0x2A19) on every connected device; emit signal_battery(sensorType, percentage) on connect (initial read) and on notifications; parseBatteryLevel() clamps value to 0-100
  • btle_uuids.h: add BTLE_UUID_BATTERY_LEVEL = 0x2A19
  • simulateNotification(): dispatch battery UUID to parseBatteryLevel()
  • Account: battery_warning_threshold (int, default 20, range 5-50)
    • saveBatteryWarningThreshold()
  • WorkoutDialog: batteryStatusReceived(sensorType, percentage) rewired to configurable-threshold check; suppresses repeated warnings within 5% of last-warned level; QHash<QString,int> m_warnedBatteryLevels tracks per-sensor session state
  • MainWindow: connect btleHub::signal_battery to WorkoutDialog slot
  • DialogConfig Trainer tab: Battery Warning group with threshold spinner
  • Unit tests: 5 new battery parsing tests covering threshold boundary, clamping, empty-packet rejection
  • DataWorkout: add getAvgIntervalPower/Hr/Cad + getNbPointsIntervalPower getters so WorkoutDialog can read stats before the reset in changeInterval()
  • IntervalSummaryUtil (src/model/intervalsummaryutil.h): new header-only helper with classifyPowerAdherence() — Met/NearMiss/Missed based on ±5% and ±10% thresholds; no Qt dependency for easy unit testing
  • model.pri: register intervalsummaryutil.h
  • Account: interval_summary_enabled (bool, default true) and interval_summary_duration_s (int, default 5, range 2–15) settings with QSettings load + saveIntervalSummarySettings()
  • WorkoutDialog::changeIntervalsDataWorkout(): capture interval stats before changeInterval() resets them; skip if studio mode, workoutOver, or interval < 10 s; call showIntervalSummaryOverlay() after
  • WorkoutDialog::showIntervalSummaryOverlay(): HTML overlay via the existing setDisplayIntervalMessage() FaderLabel; colour-coded power indicator (green/amber/red); shows avg W, % FTP, HR, cadence, duration
  • DialogConfig Trainer tab: 'Interval Summary Overlay' group with enabled checkbox and duration spinbox; wired to initTrainerTab/saveTrainerTab
  • Unit tests: 8 new tests for classifyPowerAdherence() covering exact target, ±5% boundaries, ±7% NearMiss, ±15% Missed, zero-target
  • Add AppTheme header-only class with Light/Dark/System modes
    • lightStylesheet() falls back to existing z_stylesheet-based QSS
    • darkStylesheet() provides comprehensive dark palette
    • resolveMode() uses QStyleHints::colorScheme() on Qt 6.5+ with palette-brightness fallback for older versions
    • apply() re-uses the original z_stylesheet content for light mode via the 'lightStylesheet' QApplication property
  • Add app_theme (int, default 2=System) to Account with saveAppTheme() and QSettings persistence under the 'account' group
  • Apply theme after login in main.cpp based on account preference
  • Add Theme combo box (Light/Dark/System) to DialogConfig Language tab
    • initLanguageTab() loads app_theme from Account
    • saveLanguageTab() persists and live-applies the selected theme
  • Add MainWindow::slotSystemThemeChanged() connected to QStyleHints::colorSchemeChanged (Qt 6.5+) for real-time OS tracking when System mode is active
  • Add docs/app/manifest.json with PWA metadata:
    • name, short_name, description, start_url, scope
    • display: standalone, theme_color: #e05a00, background_color: #0d0d0d
    • Icons reference existing 512x512 main_icon.png
  • Add docs/app/service-worker.js:
    • Pre-caches shell assets (HTML, JS, manifest, icons) at install time
    • Cache-first strategy for same-origin GET requests
    • Stale-while-revalidate for HTML/JS files
    • Runtime caches WASM, JS, HTML, images on first fetch
    • Graceful fallback: missing precache entries logged but don't break install
    • Old caches deleted on activate
  • Update docs/app/index.html:
    • Link manifest.json via
    • Add theme-color, mobile-web-app-capable, Apple PWA meta tags
    • Register service-worker.js on window load
  • Add Playwright PWA tests:
    • manifest.json returns 200
    • manifest has required fields (name, short_name, start_url, display, icons)
    • service-worker.js returns 200
    • index.html links to manifest
    • index.html has theme-color meta tag

The checkSensorDropout() slot was inserted immediately before a second copy of onBleConnectionError(), causing 'class member cannot be redeclared' errors on all platforms.

MSVC does not implicitly pull in QColor/QPalette through other Qt headers. Adding explicit includes fixes the Windows build failure.

  • fix: resolve merge conflict markers left in account.cpp

Conflict markers from feat/issue-154-interval-overlay merge were accidentally left in account.cpp, causing build failures on all platforms (CI #514). Resolves by keeping all three save methods:

  • saveSensorDropoutSettings()
  • saveBatteryWarningThreshold()
  • saveIntervalSummarySettings()

BtleHubWasm was missing the signal_battery(QString, int) signal that BtleHub declares, causing a compile error in mainwindow.cpp on the WASM target. Added the signal declaration, parseBatteryLevel() implementation, and wired Battery Level (0x2A19) in onBleNotification().

Agent-Logs-Url: https://github.com/MaximumTrainer/MaximumTrainer_Redux/sessions/c470d196-48d3-4e70-b8a9-0fb6f63adc4e

  • Implement determineSensorType() in BtleHubWasm for battery signal

Track which measurement UUIDs have been received (HR, Power, FTMS, CSC, Moxy) and use that to infer a human-readable sensor type string for signal_battery, mirroring native BtleHub::determineSensorType().

Agent-Logs-Url: https://github.com/MaximumTrainer/MaximumTrainer_Redux/sessions/728cac51-9482-4475-883f-ae19a2e2fb7e

  • Fix Playwright wasm_btle_api.spec.js timeouts

Increase waitForCanvas timeouts (30s→45s for canvas, 15s→45s for BLE setup) and set the test suite timeout to 120s to accommodate slow WASM loads on CI. Add diagnostic logging when BLE scan is not initiated.

Agent-Logs-Url: https://github.com/MaximumTrainer/MaximumTrainer_Redux/sessions/3d6d36ce-46b7-442c-87c4-9daf91c62486


  • Add WorkoutQueue model: persistent QStringList of file paths + names, saved to QSettings under workoutQueue/ group; survives app restart
  • Add QueuePanelWidget: dock-able sidebar with QListWidget, Move Up/Down, Remove, Clear; connected to WorkoutQueue::queueChanged signal
  • Add WorkoutCountdownDialog: 60 s auto-start countdown before advancing to the next queued workout; Start Now / Cancel Queue buttons
  • Wire 'Add to Queue' context menu item in Main_WorkoutPage; emits addWorkoutToQueue(Workout) signal
  • MainWindow: creates dock widget (right side, hidden by default); shows dock on first item added; displays status message with queue count
  • After each workout (sim + BTLE paths), check queue and show countdown; on accept, parse next workout XML and call executeWorkout() recursively
  • Add on_actionToggleQueue_triggered() slot for future menu wiring

Closes #152

  • fix: fix saveBatteryWarningThreshold declaration placement in account.h
  • Move declaration to canonical public methods section (line 25)
  • Remove duplicate misplaced declaration adjacent to battery_warning_threshold field
  • fix: resolve merge conflict markers in account.cpp - keep all three save methods

  • fix: correct string literal syntax and add signal_battery to BtleHubWasm

  • mainwindow.cpp: fix empty string literal '' around %1 arg (was breaking C++ parse)
  • btle_hub_wasm.h: add signal_battery(QString, int) to match BtleHub signal contract
  • fix: move addWorkoutToQueue from signals to private slots

The function was in the signals: block but had a manual implementation in mainwindow.cpp, causing a duplicate symbol linker error on all platforms. Slots have user implementations; signals do not.


  • PmcCalculator: computes CTL (42d EMA), ATL (7d EMA) and TSB per day from WorkoutHistorySummary history
  • PmcDialog: QwtPlot with CTL (blue), ATL (red) and TSB (green dashed) curves; Julian-day x-axis with DateScaleDraw; today's values readout
  • HistoryWidget: 'Performance Chart' button opens PmcDialog
  • WorkoutHistoryModel: adds const history() getter

Closes #146

  • fix: add INCLUDEPATH for src/fitness and signal_battery to BtleHubWasm
  • fitness.pri: INCLUDEPATH += $\C:\Users\danwo\CodeProjects\MaximumTrainer_Redux so pmccalculator.h resolves from src/ui/
  • btle_hub_wasm.h: add signal_battery(QString, int) signal to match BtleHub
  • fix: remove qwt_plot_legend_item.h (unavailable) and use label legend

qwt_plot_legend_item.h is not available in the QWT versions used across platforms. Replace QwtLegend + qwt_legend.h with a simple HTML-label colour legend below the plot. This is portable across all QWT builds.


  • MmpCalculator: reads per-second power from FIT Record messages, fills timestamp gaps with 0 for accurate window calculations, computes rolling-window MMP for 14 standard durations (1s to 2h)
  • CriticalPowerModel: linearized CP + W' regression using durations in 2-30 min range (requires >=3 data points for stability)
  • CriticalPowerDialog: QwtPlot with log-scale X axis, human-readable duration labels, MMP curve (blue) + CP model curve (red dashed), CP and W' readouts in stats panel
  • HistoryWidget: 'Critical Power Curve' button opens dialog with loaded activity history
  • WorkoutHistoryModel: added history() const getter
  • fix: add INCLUDEPATH to fitness.pri and signal_battery to BtleHubWasm

fitness.pri lacked INCLUDEPATH += $\C:\Users\danwo\CodeProjects\MaximumTrainer_Redux so mmpcalculator.h was not found by UI files that include it by bare name. Also adds the missing signal_battery signal to BtleHubWasm to match BtleHub's interface.


  • PmcCalculator: computes CTL (42d EMA), ATL (7d EMA) and TSB per day from WorkoutHistorySummary history
  • PmcDialog: QwtPlot with CTL (blue), ATL (red) and TSB (green dashed) curves; Julian-day x-axis with DateScaleDraw; today's values readout
  • HistoryWidget: 'Performance Chart' button opens PmcDialog
  • WorkoutHistoryModel: adds const history() getter

Closes #146

  • feat: WASM localStorage credential store + fix signal_battery + INCLUDEPATH
  • credential_store_wasm.cpp: rewrite with Emscripten localStorage for token persistence
  • btle_hub_wasm.h: add missing signal_battery(QString, int) to match BtleHub contract
  • fitness.pri: add INCLUDEPATH += $\C:\Users\danwo\CodeProjects\MaximumTrainer_Redux so pmccalculator.h is resolvable
  • fix: remove qwt_plot_legend_item.h (unavailable) and use label legend

qwt_plot_legend_item.h and qwt_legend.h are not available in all QWT builds (specifically the custom WASM QWT). Replace QwtLegend with an HTML QLabel colour legend below the plot for cross-platform compatibility.


  • Apply suggestions from code review

…rt (#167)

* feat: auto-pause workout on sensor dropout (#148)

- Add sensor_dropout_enabled (bool, default true) and sensor_dropout_timeout_s
  (int, default 5s, range 2–30s) to Account with QSettings persistence
- WorkoutDialog: 1-Hz watchdog timer checks last power timestamp; on dropout
  pauses workout and shows overlay message; auto-resumes after 3s countdown
  when signal is restored
- Manual resume during dropout pause clears dropout state cleanly
- DialogConfig Trainer tab: new 'Sensor Dropout' group with enable toggle
  and timeout spinner
- WorkoutPlot: removeAlertMessage() method to dismiss alert overlay

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: proactive BLE sensor battery-level warnings (#156)

- BtleHub: subscribe to Battery Service (0x180F / characteristic 0x2A19)
  on every connected device; emit signal_battery(sensorType, percentage)
  on connect (initial read) and on notifications; parseBatteryLevel()
  clamps value to 0-100
- btle_uuids.h: add BTLE_UUID_BATTERY_LEVEL = 0x2A19
- simulateNotification(): dispatch battery UUID to parseBatteryLevel()
- Account: battery_warning_threshold (int, default 20, range 5-50)
  + saveBatteryWarningThreshold()
- WorkoutDialog: batteryStatusReceived(sensorType, percentage) rewired to
  configurable-threshold check; suppresses repeated warnings within 5%
  of last-warned level; QHash<QString,int> m_warnedBatteryLevels tracks
  per-sensor session state
- MainWindow: connect btleHub::signal_battery to WorkoutDialog slot
- DialogConfig Trainer tab: Battery Warning group with threshold spinner
- Unit tests: 5 new battery parsing tests covering threshold boundary,
  clamping, empty-packet rejection

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: per-interval transition summary overlay (#154)

- DataWorkout: add getAvgIntervalPower/Hr/Cad + getNbPointsIntervalPower
  getters so WorkoutDialog can read stats before the reset in changeInterval()
- IntervalSummaryUtil (src/model/intervalsummaryutil.h): new header-only
  helper with classifyPowerAdherence() — Met/NearMiss/Missed based on ±5%
  and ±10% thresholds; no Qt dependency for easy unit testing
- model.pri: register intervalsummaryutil.h
- Account: interval_summary_enabled (bool, default true) and
  interval_summary_duration_s (int, default 5, range 2–15) settings with
  QSettings load + saveIntervalSummarySettings()
- WorkoutDialog::changeIntervalsDataWorkout(): capture interval stats
  before changeInterval() resets them; skip if studio mode, workoutOver,
  or interval < 10 s; call showIntervalSummaryOverlay() after
- WorkoutDialog::showIntervalSummaryOverlay(): HTML overlay via the
  existing setDisplayIntervalMessage() FaderLabel; colour-coded power
  indicator (green/amber/red); shows avg W, % FTP, HR, cadence, duration
- DialogConfig Trainer tab: 'Interval Summary Overlay' group with enabled
  checkbox and duration spinbox; wired to initTrainerTab/saveTrainerTab
- Unit tests: 8 new tests for classifyPowerAdherence() covering exact
  target, ±5% boundaries, ±7% NearMiss, ±15% Missed, zero-target

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(#151): Add dark mode / system-adaptive theme support

- Add AppTheme header-only class with Light/Dark/System modes
  - lightStylesheet() falls back to existing z_stylesheet-based QSS
  - darkStylesheet() provides comprehensive dark palette
  - resolveMode() uses QStyleHints::colorScheme() on Qt 6.5+ with
    palette-brightness fallback for older versions
  - apply() re-uses the original z_stylesheet content for light mode
    via the 'lightStylesheet' QApplication property
- Add app_theme (int, default 2=System) to Account with saveAppTheme()
  and QSettings persistence under the 'account' group
- Apply theme after login in main.cpp based on account preference
- Add Theme combo box (Light/Dark/System) to DialogConfig Language tab
  - initLanguageTab() loads app_theme from Account
  - saveLanguageTab() persists and live-applies the selected theme
- Add MainWindow::slotSystemThemeChanged() connected to
  QStyleHints::colorSchemeChanged (Qt 6.5+) for real-time OS tracking
  when System mode is active

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(#155): Add WASM PWA support with manifest and service worker

- Add docs/app/manifest.json with PWA metadata:
  - name, short_name, description, start_url, scope
  - display: standalone, theme_color: #e05a00, background_color: #0d0d0d
  - Icons reference existing 512x512 main_icon.png
- Add docs/app/service-worker.js:
  - Pre-caches shell assets (HTML, JS, manifest, icons) at install time
  - Cache-first strategy for same-origin GET requests
  - Stale-while-revalidate for HTML/JS files
  - Runtime caches WASM, JS, HTML, images on first fetch
  - Graceful fallback: missing precache entries logged but don't break install
  - Old caches deleted on activate
- Update docs/app/index.html:
  - Link manifest.json via <link rel='manifest'>
  - Add theme-color, mobile-web-app-capable, Apple PWA meta tags
  - Register service-worker.js on window load
- Add Playwright PWA tests:
  - manifest.json returns 200
  - manifest has required fields (name, short_name, start_url, display, icons)
  - service-worker.js returns 200
  - index.html links to manifest
  - index.html has theme-color meta tag

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(#148): Remove duplicate onBleConnectionError slot declaration in workoutdialog.h

The checkSensorDropout() slot was inserted immediately before a second
copy of onBleConnectionError(), causing 'class member cannot be redeclared'
errors on all platforms.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(#151): Add missing QColor/QPalette includes in apptheme.h for MSVC

MSVC does not implicitly pull in QColor/QPalette through other Qt headers.
Adding explicit includes fixes the Windows build failure.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: resolve merge conflict markers left in account.cpp

Conflict markers from feat/issue-154-interval-overlay merge were
accidentally left in account.cpp, causing build failures on all
platforms (CI #514). Resolves by keeping all three save methods:
- saveSensorDropoutSettings()
- saveBatteryWarningThreshold()
- saveIntervalSummarySettings()

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: remove duplicate saveBatteryWarningThreshold declaration in account.h

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add missing signal_battery to BtleHubWasm with sensor type inference (#161)

* Add signal_battery to BtleHubWasm to fix WASM build

BtleHubWasm was missing the signal_battery(QString, int) signal that
BtleHub declares, causing a compile error in mainwindow.cpp on the WASM
target. Added the signal declaration, parseBatteryLevel() implementation,
and wired Battery Level (0x2A19) in onBleNotification().

Agent-Logs-Url: https://github.com/MaximumTrainer/MaximumTrainer_Redux/sessions/c470d196-48d3-4e70-b8a9-0fb6f63adc4e

Co-authored-by: MaximumTrainer <1376575+MaximumTrainer@users.noreply.github.com>

* Implement determineSensorType() in BtleHubWasm for battery signal

Track which measurement UUIDs have been received (HR, Power, FTMS, CSC,
Moxy) and use that to infer a human-readable sensor type string for
signal_battery, mirroring native BtleHub::determineSensorType().

Agent-Logs-Url: https://github.com/MaximumTrainer/MaximumTrainer_Redux/sessions/728cac51-9482-4475-883f-ae19a2e2fb7e

Co-authored-by: MaximumTrainer <1376575+MaximumTrainer@users.noreply.github.com>

* Fix Playwright wasm_btle_api.spec.js timeouts

Increase waitForCanvas timeouts (30s→45s for canvas, 15s→45s for BLE
setup) and set the test suite timeout to 120s to accommodate slow WASM
loads on CI.  Add diagnostic logging when BLE scan is not initiated.

Agent-Logs-Url: https://github.com/MaximumTrainer/MaximumTrainer_Redux/sessions/3d6d36ce-46b7-442c-87c4-9daf91c62486

Co-authored-by: MaximumTrainer <1376575+MaximumTrainer@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: MaximumTrainer <1376575+MaximumTrainer@users.noreply.github.com>

* Feat/issue 152 workout queue (#162)

* feat: workout queue / back-to-back playlist (#152)

- Add WorkoutQueue model: persistent QStringList of file paths + names,
  saved to QSettings under workoutQueue/ group; survives app restart
- Add QueuePanelWidget: dock-able sidebar with QListWidget, Move Up/Down,
  Remove, Clear; connected to WorkoutQueue::queueChanged signal
- Add WorkoutCountdownDialog: 60 s auto-start countdown before advancing
  to the next queued workout; Start Now / Cancel Queue buttons
- Wire 'Add to Queue' context menu item in Main_WorkoutPage; emits
  addWorkoutToQueue(Workout) signal
- MainWindow: creates dock widget (right side, hidden by default);
  shows dock on first item added; displays status message with queue count
- After each workout (sim + BTLE paths), check queue and show countdown;
  on accept, parse next workout XML and call executeWorkout() recursively
- Add on_actionToggleQueue_triggered() slot for future menu wiring

Closes #152

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: fix saveBatteryWarningThreshold declaration placement in account.h

- Move declaration to canonical public methods section (line 25)
- Remove duplicate misplaced declaration adjacent to battery_warning_threshold field

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: resolve merge conflict markers in account.cpp - keep all three save methods

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: correct string literal syntax and add signal_battery to BtleHubWasm

- mainwindow.cpp: fix empty string literal '' around %1 arg (was breaking C++ parse)
- btle_hub_wasm.h: add signal_battery(QString, int) to match BtleHub signal contract

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: move addWorkoutToQueue from signals to private slots

The function was in the signals: block but had a manual implementation
in mainwindow.cpp, causing a duplicate symbol linker error on all
platforms. Slots have user implementations; signals do not.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Dan Woollard <danwo@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(#146): Performance Management Chart (CTL/ATL/TSB)

* feat: add Performance Management Chart (CTL/ATL/TSB) (#146)

- PmcCalculator: computes CTL (42d EMA), ATL (7d EMA) and TSB per day
  from WorkoutHistorySummary history
- PmcDialog: QwtPlot with CTL (blue), ATL (red) and TSB (green dashed)
  curves; Julian-day x-axis with DateScaleDraw; today's values readout
- HistoryWidget: 'Performance Chart' button opens PmcDialog
- WorkoutHistoryModel: adds const history() getter

Closes #146

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: add INCLUDEPATH for src/fitness and signal_battery to BtleHubWasm

- fitness.pri: INCLUDEPATH += \$\C:\Users\danwo\CodeProjects\MaximumTrainer_Redux so pmccalculator.h resolves from src/ui/
- btle_hub_wasm.h: add signal_battery(QString, int) signal to match BtleHub

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: remove qwt_plot_legend_item.h (unavailable) and use label legend

qwt_plot_legend_item.h is not available in the QWT versions used across
platforms. Replace QwtLegend + qwt_legend.h with a simple HTML-label
colour legend below the plot. This is portable across all QWT builds.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Dan Woollard <danwo@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(#145): Mean Maximal Power (MMP) / Critical Power Curve Analysis

* feat: add MMP/Critical Power Curve analysis (#145)

- MmpCalculator: reads per-second power from FIT Record messages,
  fills timestamp gaps with 0 for accurate window calculations,
  computes rolling-window MMP for 14 standard durations (1s to 2h)
- CriticalPowerModel: linearized CP + W' regression using durations
  in 2-30 min range (requires >=3 data points for stability)
- CriticalPowerDialog: QwtPlot with log-scale X axis, human-readable
  duration labels, MMP curve (blue) + CP model curve (red dashed),
  CP and W' readouts in stats panel
- HistoryWidget: 'Critical Power Curve' button opens dialog with
  loaded activity history
- WorkoutHistoryModel: added history() const getter

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: add INCLUDEPATH to fitness.pri and signal_battery to BtleHubWasm

fitness.pri lacked INCLUDEPATH += \$\C:\Users\danwo\CodeProjects\MaximumTrainer_Redux so mmpcalculator.h was not
found by UI files that include it by bare name. Also adds the missing
signal_battery signal to BtleHubWasm to match BtleHub's interface.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Dan Woollard <danwo@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(#40): Implement missing WASM functionality (#166)

* feat: add Performance Management Chart (CTL/ATL/TSB) (#146)

- PmcCalculator: computes CTL (42d EMA), ATL (7d EMA) and TSB per day
  from WorkoutHistorySummary history
- PmcDialog: QwtPlot with CTL (blue), ATL (red) and TSB (green dashed)
  curves; Julian-day x-axis with DateScaleDraw; today's values readout
- HistoryWidget: 'Performance Chart' button opens PmcDialog
- WorkoutHistoryModel: adds const history() getter

Closes #146

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: WASM localStorage credential store + fix signal_battery + INCLUDEPATH

- credential_store_wasm.cpp: rewrite with Emscripten localStorage for token persistence
- btle_hub_wasm.h: add missing signal_battery(QString, int) to match BtleHub contract
- fitness.pri: add INCLUDEPATH += \$\C:\Users\danwo\CodeProjects\MaximumTrainer_Redux so pmccalculator.h is resolvable

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: remove qwt_plot_legend_item.h (unavailable) and use label legend

qwt_plot_legend_item.h and qwt_legend.h are not available in all QWT
builds (specifically the custom WASM QWT). Replace QwtLegend with an
HTML QLabel colour legend below the plot for cross-platform compatibility.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Dan Woollard <danwo@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Dan Woollard <danwo@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 21, 2026 12:11
@MaximumTrainer

Copy link
Copy Markdown
Owner Author

@copilot resolve the merge conflicts in this pull request

…refactor

Co-authored-by: MaximumTrainer <1376575+MaximumTrainer@users.noreply.github.com>

Copilot AI commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved all merge conflicts with origin/master in commit 3cbd11f. The conflicts were across 12 files:

  • docs/app/manifest.json — added missing 192×192 icon entry from master
  • docs/app/service-worker.js — adopted dynamic SW_VERSION from URL query params
  • src/model/model.pri — merged in planadherencestore.cpp, planadherence.h, and planadherencestore.h from master
  • src/model/workoutqueue.h — corrected dequeueName() doc comment to match actual behaviour
  • src/ui/criticalpowerdialog.cpp — adopted theme-aware QPalette colours from master
  • src/ui/historywidget.cpp — used histTab as parent widget (correct per tab layout)
  • src/ui/historywidget.h — added m_tabs and m_adherenceWidget members from master
  • src/ui/mainwindow.h — added m_adherenceStore member from master
  • src/ui/mainwindow.cpp — adopted Plan Adherence initialisation block and tryAdvanceWorkoutQueue() refactor from master
  • src/ui/queuepanelwidget.cpp — used NoDragDrop mode from master
  • src/ui/ui.pri — added planadherencewidget.cpp/.h entries from master
  • tests/btle/tst_btle_hub.cpp — adopted renamed test testBattery_aboveThreshold_emits from master

Copilot AI 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.

Copilot wasn't able to review any files in this pull request.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/model/workoutqueue.h

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

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

WorkoutQueue::dequeueName() documentation says it returns the first workout name without removing it, but the implementation removes both the name and the file path (takeFirst). Either update the docstring to match the destructive behavior, or change the method to a non-destructive peek (and add a separate dequeue method if needed).

Suggested change
/// Consume (remove and return) the name of the first workout in the queue.
/// Also removes the corresponding first file path to keep the queue entries aligned.

Copilot uses AI. Check for mistakes.
Comment on lines 19 to 21

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

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

QListWidget is set to InternalMove drag/drop, but there is no code to propagate user drag reordering back into WorkoutQueue. This can leave the UI showing a different order than the persisted queue, and subsequent Remove/Move actions will operate on the wrong underlying item. Either disable drag/drop here, or handle the model update on drop (e.g., by listening for rowsMoved / dropEvent and calling queue reorder APIs).

Copilot uses AI. Check for mistakes.
Comment thread src/ui/mainwindow.cpp
Comment on lines 1702 to 1714

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

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

WorkoutCountdownDialog provides a "Cancel Queue" action, but when the dialog is rejected the queue is not cleared (and dialogResult() is ignored). This means users can click "Cancel Queue" yet the queue will still auto-advance after the next workout completes. Handle the rejected path by clearing/persisting the queue (and consider using dialogResult() to distinguish StartNow vs Cancelled explicitly).

Copilot uses AI. Check for mistakes.
Comment on lines 843 to 851

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

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

testBattery_aboveThreshold_noSignal() currently expects one signal emission and the inline comment says BtleHub emits for any reading. The test name implies the opposite (no signal). Rename the test (or adjust expectation) so failures are meaningful and the suite is self-documenting.

Copilot uses AI. Check for mistakes.
Comment thread src/btle/btle_hub.h
Comment on lines 105 to 110

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

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

Now that m_batteryService is tracked as a member, make sure it’s cleaned up/reset consistently with the other service pointers when reconnecting (e.g., in connectToDevice()’s "Clean up any previously-created service objects" block). Otherwise repeated connect cycles can leak service objects and leave stale pointers around.

Copilot uses AI. Check for mistakes.
Comment on lines 825 to 832

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

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

The test name/comment don’t match the actual data being asserted: this is labeled "Nominal battery level (85%)" but the payload is 15%. Either update the comment/test name or change the injected value so the test intent is unambiguous.

Copilot uses AI. Check for mistakes.
Comment on lines 67 to 71

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

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

This plot hard-codes a white canvas background, which will clash with the new Dark/System themes and makes the chart hard to read in dark mode. Prefer using the application palette (or a theme-derived color) for the canvas/background and grid colors so it adapts with AppTheme::apply().

Copilot uses AI. Check for mistakes.
Comment thread src/ui/pmcdialog.cpp
Comment on lines 80 to 82

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

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

The Close button’s rejected() signal is wired to QDialog::accept(). This is inconsistent with other dialogs (e.g., CriticalPowerDialog uses reject()) and can produce an unexpected Accepted result for callers that check exec()’s return value. Wire Close to reject() (or close()) instead.

Copilot uses AI. Check for mistakes.
Comment on lines 13 to 18

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

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

SW_VERSION/CACHE_NAME are constant ("v1"), so a new deployment can keep serving stale cached assets—especially the .wasm, which is cached on first fetch and never revalidated unless the cache name changes. Consider injecting a build/version hash into SW_VERSION (or using a hashed cache name) so updates reliably bust old caches.

Copilot uses AI. Check for mistakes.
Comment thread docs/app/manifest.json
Comment on lines 12 to 19

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

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

The manifest only declares a 512×512 icon. Many PWA install flows expect a 192×192 icon as well (and some platforms use it for home-screen tiles). Consider adding a 192×192 icon entry (or generating one from the existing asset) to improve installability and align with typical PWA requirements.

Copilot uses AI. Check for mistakes.
@MaximumTrainer

Copy link
Copy Markdown
Owner Author

@copilot apply changes based on the comments in this thread

…ancel queue, PMC close

Agent-Logs-Url: https://github.com/MaximumTrainer/MaximumTrainer_Redux/sessions/f87d05b0-6a24-488f-b27a-5a3923d86885

Co-authored-by: MaximumTrainer <1376575+MaximumTrainer@users.noreply.github.com>

Copilot AI commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

@copilot apply changes based on the comments in this thread

Applied all review feedback in commit b5b92bd:

  • btle_hub.cpp — added delete m_batteryService; m_batteryService = nullptr; to the service cleanup block in connectToDevice(), preventing stale pointer leaks on reconnect cycles
  • account.cpp (load) — all new settings now clamped with qBound on load: sensor_dropout_timeout_s (2–30), battery_warning_threshold (5–50), interval_summary_duration_s (2–15), app_theme (0–2)
  • account.cpp (save) — same qBound guards applied in all four save methods so out-of-range values can't be persisted programmatically
  • mainwindow.cpptryAdvanceWorkoutQueue() now checks dialogResult() == Cancelled and calls m_workoutQueue->clear(), so clicking "Cancel Queue" stops auto-advance for all subsequent workouts
  • pmcdialog.cpp — Close button wired to QDialog::reject() instead of accept()

The items already resolved in the merge commit (3cbd11f) were: drag/drop mode (NoDragDrop), test name/comment fixes, theme-aware canvas colours, dequeueName() doc comment, tryAdvanceWorkoutQueue() extraction, dynamic SW_VERSION, and 192×192 manifest icon.

@MaximumTrainer MaximumTrainer merged commit bee2319 into master Apr 21, 2026
28 of 29 checks passed
@MaximumTrainer MaximumTrainer deleted the feat/issue-144-workout-history branch April 21, 2026 13:21
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.

feat: Workout queue / back-to-back playlist feat: Performance Management Chart (CTL / ATL / TSB)

3 participants