From c2f8a4ef34a8451ecf8df670f3201f4e52ef8f41 Mon Sep 17 00:00:00 2001 From: MaximumTrainer <1376575+MaximumTrainer@users.noreply.github.com> Date: Tue, 21 Apr 2026 07:53:06 +0100 Subject: [PATCH 1/2] Implement workout queue, performance management chart, and WASM support (#167) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 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 - 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 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 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 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 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 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> --- docs/app/index.html | 21 +++ docs/app/manifest.json | 20 +++ docs/app/service-worker.js | 129 +++++++++++++++++ src/app/credential_store.h | 3 +- src/app/credential_store_wasm.cpp | 74 ++++++++-- src/app/main.cpp | 15 +- src/btle/btle_hub.cpp | 49 +++++++ src/btle/btle_hub.h | 17 ++- src/btle/btle_hub_wasm.cpp | 36 ++++- src/btle/btle_hub_wasm.h | 16 +++ src/btle/btle_uuids.h | 1 + src/fitness/fitness.pri | 6 + src/fitness/mmpcalculator.cpp | 167 ++++++++++++++++++++++ src/fitness/mmpcalculator.h | 58 ++++++++ src/fitness/pmccalculator.cpp | 54 +++++++ src/fitness/pmccalculator.h | 47 +++++++ src/model/account.cpp | 48 +++++++ src/model/account.h | 18 +++ src/model/dataworkout.h | 6 + src/model/intervalsummaryutil.h | 28 ++++ src/model/model.pri | 5 +- src/model/workoutqueue.cpp | 93 +++++++++++++ src/model/workoutqueue.h | 49 +++++++ src/ui/apptheme.h | 180 ++++++++++++++++++++++++ src/ui/criticalpowerdialog.cpp | 186 +++++++++++++++++++++++++ src/ui/criticalpowerdialog.h | 46 ++++++ src/ui/dialogconfig.cpp | 122 ++++++++++++++++ src/ui/dialogconfig.h | 14 ++ src/ui/historywidget.cpp | 33 +++++ src/ui/historywidget.h | 18 ++- src/ui/main_workoutpage.cpp | 11 ++ src/ui/main_workoutpage.h | 3 + src/ui/mainwindow.cpp | 86 ++++++++++++ src/ui/mainwindow.h | 17 ++- src/ui/plots/workoutplot.cpp | 3 + src/ui/plots/workoutplot.h | 1 + src/ui/pmcdialog.cpp | 160 +++++++++++++++++++++ src/ui/pmcdialog.h | 42 ++++++ src/ui/queuepanelwidget.cpp | 101 ++++++++++++++ src/ui/queuepanelwidget.h | 36 +++++ src/ui/ui.pri | 9 ++ src/ui/workoutcountdowndialog.cpp | 78 +++++++++++ src/ui/workoutcountdowndialog.h | 41 ++++++ src/ui/workoutdialog.cpp | 168 ++++++++++++++++++++-- src/ui/workoutdialog.h | 19 ++- src/ui/workouthistorymodel.h | 1 + tests/btle/tst_btle_hub.cpp | 119 ++++++++++++++++ tests/playwright/wasm_btle_api.spec.js | 14 +- tests/playwright/wasm_webapp.spec.js | 39 ++++++ 49 files changed, 2461 insertions(+), 46 deletions(-) create mode 100644 docs/app/manifest.json create mode 100644 docs/app/service-worker.js create mode 100644 src/fitness/mmpcalculator.cpp create mode 100644 src/fitness/mmpcalculator.h create mode 100644 src/fitness/pmccalculator.cpp create mode 100644 src/fitness/pmccalculator.h create mode 100644 src/model/intervalsummaryutil.h create mode 100644 src/model/workoutqueue.cpp create mode 100644 src/model/workoutqueue.h create mode 100644 src/ui/apptheme.h create mode 100644 src/ui/criticalpowerdialog.cpp create mode 100644 src/ui/criticalpowerdialog.h create mode 100644 src/ui/pmcdialog.cpp create mode 100644 src/ui/pmcdialog.h create mode 100644 src/ui/queuepanelwidget.cpp create mode 100644 src/ui/queuepanelwidget.h create mode 100644 src/ui/workoutcountdowndialog.cpp create mode 100644 src/ui/workoutcountdowndialog.h diff --git a/docs/app/index.html b/docs/app/index.html index 6e885268..2a8133d7 100644 --- a/docs/app/index.html +++ b/docs/app/index.html @@ -7,6 +7,12 @@ Maximum Trainer — Web App (Beta) + + + + + +