Implement workout queue, performance management chart, and WASM support#167
Conversation
- 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>
- 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>
- 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>
- 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>
- 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>
…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>
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>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…unt.h Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…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: 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: 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: 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: 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>
There was a problem hiding this comment.
Pull request overview
This PR expands the app’s web (WASM) deployment and post-workout experience by adding PWA/offline support, browser credential persistence, and BLE Battery Service reporting, while also introducing a workout queue flow and new fitness visualizations (Critical Power + Performance Management Chart) in the native UI.
Changes:
- Add PWA assets (manifest + service worker + HTML wiring) and extend Playwright coverage for WASM web deployment.
- Implement workout queue UI/flow (dock panel + countdown auto-advance) and add interval summary + sensor-dropout auto-pause enhancements.
- Add fitness charts (Critical Power curve + PMC) and integrate BLE Battery Service parsing/signaling (native + WASM).
Reviewed changes
Copilot reviewed 49 out of 49 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/playwright/wasm_webapp.spec.js | Adds PWA/manifest/service worker deployment checks. |
| tests/playwright/wasm_btle_api.spec.js | Increases timeouts + adds diagnostics for flaky WASM BLE API verification. |
| tests/btle/tst_btle_hub.cpp | Adds unit tests for battery parsing + interval adherence classification. |
| src/ui/workouthistorymodel.h | Exposes history list via const accessor for chart dialogs. |
| src/ui/workoutdialog.h | Updates battery slot signature; adds dropout watchdog + interval summary slot/state. |
| src/ui/workoutdialog.cpp | Adds sensor-dropout auto-pause + interval summary overlay + battery-warning suppression. |
| src/ui/workoutcountdowndialog.h | New dialog header for auto-advance countdown UX. |
| src/ui/workoutcountdowndialog.cpp | Implements countdown dialog logic/buttons. |
| src/ui/ui.pri | Adds new UI sources/headers to build. |
| src/ui/queuepanelwidget.h | New queue sidebar widget header. |
| src/ui/queuepanelwidget.cpp | Implements queue sidebar list + controls. |
| src/ui/pmcdialog.h | New PMC dialog header (Qwt-based chart). |
| src/ui/pmcdialog.cpp | Implements PMC chart rendering + labels/legend. |
| src/ui/plots/workoutplot.h | Adds API to remove alert messages. |
| src/ui/plots/workoutplot.cpp | Implements alert message removal animation. |
| src/ui/mainwindow.h | Adds queue-related members + theme change slot + dock widget include. |
| src/ui/mainwindow.cpp | Creates queue dock; wires “add to queue”; auto-advances workouts; handles System theme changes. |
| src/ui/main_workoutpage.h | Adds “add to queue” signal/slot + context menu action. |
| src/ui/main_workoutpage.cpp | Adds “Add to Queue” context menu action and emits selection. |
| src/ui/historywidget.h | Adds buttons/slots for CP + PMC dialogs. |
| src/ui/historywidget.cpp | Wires CP/PMC buttons and opens dialogs based on loaded history. |
| src/ui/dialogconfig.h | Adds theme + dropout + battery threshold + interval summary config controls. |
| src/ui/dialogconfig.cpp | Implements saving/loading theme + dropout/battery/interval-summary settings and applies theme. |
| src/ui/criticalpowerdialog.h | New Critical Power dialog header (MMP + CP model). |
| src/ui/criticalpowerdialog.cpp | Implements CP curve computation + Qwt plot + labels. |
| src/ui/apptheme.h | Introduces centralized Light/Dark/System theme application helper. |
| src/model/workoutqueue.h | New persistent workout queue model API. |
| src/model/workoutqueue.cpp | Implements queue persistence/mutation via QSettings. |
| src/model/model.pri | Adds workout queue + interval summary util header to build. |
| src/model/intervalsummaryutil.h | Header-only power adherence classifier for interval summaries. |
| src/model/dataworkout.h | Adds accessors for per-interval average stats. |
| src/model/account.h | Adds persisted settings for theme, dropout, battery threshold, interval summaries. |
| src/model/account.cpp | Loads/saves the new settings via QSettings. |
| src/fitness/pmccalculator.h | Declares PMC computation interface and point structure. |
| src/fitness/pmccalculator.cpp | Implements CTL/ATL/TSB computation from workout history. |
| src/fitness/mmpcalculator.h | Declares FIT power series read + MMP + CP/W′ fitting. |
| src/fitness/mmpcalculator.cpp | Implements FIT parsing + rolling MMP + linearized CP/W′ regression. |
| src/fitness/fitness.pri | Adds new fitness sources/headers to build and sets INCLUDEPATH. |
| src/btle/btle_uuids.h | Adds Battery Level characteristic UUID constant. |
| src/btle/btle_hub_wasm.h | Adds battery signal + parsing hooks (but currently contains a critical duplication issue). |
| src/btle/btle_hub_wasm.cpp | Parses Battery Level characteristic and emits battery signal; tracks seen measurement types. |
| src/btle/btle_hub.h | Adds battery signal + battery service member and parser declarations. |
| src/btle/btle_hub.cpp | Discovers Battery Service, reads/subscribes battery level, parses and emits battery percentage. |
| src/app/main.cpp | Caches baseline light QSS and applies saved theme after account load. |
| src/app/credential_store_wasm.cpp | Implements WASM credential persistence via browser localStorage interop. |
| src/app/credential_store.h | Updates WASM credential store documentation from no-op to localStorage. |
| docs/app/service-worker.js | Adds offline caching strategy + runtime caching for WASM web app. |
| docs/app/manifest.json | Adds PWA manifest metadata (currently with an incorrect start_url/scope for GH Pages). |
| docs/app/index.html | Adds manifest/theme meta tags and service worker registration. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| QLowEnergyService *m_hrService = nullptr; | ||
| QLowEnergyService *m_cscService = nullptr; | ||
| QLowEnergyService *m_powerService = nullptr; | ||
| QLowEnergyService *m_ftmsService = nullptr; | ||
| QLowEnergyService *m_moxyService = nullptr; | ||
| QLowEnergyService *m_batteryService = nullptr; |
There was a problem hiding this comment.
m_batteryService is added, but BtleHub::connectToDevice() currently deletes/resets the other m_*Service pointers and does not reset m_batteryService. That leaves a stale child service object across reconnects (and leaks the old object if overwritten later). Ensure the battery service is cleaned up alongside the other services when connecting/disconnecting.
| m_list = new QListWidget(this); | ||
| m_list->setDragDropMode(QAbstractItemView::InternalMove); | ||
| layout->addWidget(m_list, 1); |
There was a problem hiding this comment.
QListWidget::InternalMove enables drag-to-reorder in the UI, but there’s no code updating WorkoutQueue when the user drags items. As a result, the visible order can diverge from the persisted queue and will snap back on the next refresh(). Either disable drag/drop here or implement a handler that writes the new order back into WorkoutQueue (and saves/emits queueChanged).
| WorkoutCountdownDialog countdown(nextName, 60, this); | ||
| if (countdown.exec() == QDialog::Accepted) { | ||
| m_workoutQueue->dequeueFilePath(); | ||
| XmlUtil xmlNext(settings->language); |
There was a problem hiding this comment.
The "Cancel Queue" action in WorkoutCountdownDialog is not handled here: if the dialog is rejected, the queue remains intact and the next workout will still be offered after the next workout ends. If the intent is to cancel the remaining queue, use countdown.dialogResult() (or check Rejected) to clear m_workoutQueue when the user cancels.
| m_workoutQueue->dequeueFilePath(); | ||
| XmlUtil xmlNext(settings->language); | ||
| Workout next = xmlNext.parseSingleWorkoutXml(nextPath); | ||
| if (!next.getName().isEmpty()) | ||
| executeWorkout(next); |
There was a problem hiding this comment.
The queue item is dequeued before verifying that the next workout XML parses successfully, and the next workout is started via recursive executeWorkout(next) calls. Consider only dequeuing after a successful parse, and avoid deep recursion by scheduling the next workout (e.g., via a loop or QTimer::singleShot) to prevent unbounded call stack growth with long queues.
| /// Nominal battery level (85%) — signal emitted with correct percentage. | ||
| void TstBtleHub::testBattery_belowThreshold_emits() | ||
| { | ||
| QSignalSpy spy(hub, &BtleHub::signal_battery); | ||
| hub->simulateNotification(BTLE_UUID_BATTERY_LEVEL, QByteArray(1, char(15))); | ||
| QCOMPARE(spy.count(), 1); | ||
| QCOMPARE(spy.at(0).at(1).toInt(), 15); | ||
| } |
There was a problem hiding this comment.
The doc comment says “Nominal battery level (85%)” but the test injects char(15) and asserts 15%. This makes the test misleading to maintainers; please align the comment and test data (either use 85 or update the comment to 15).
| /// Above threshold (75%) — signal still emitted (threshold filtering is in WorkoutDialog). | ||
| void TstBtleHub::testBattery_aboveThreshold_noSignal() | ||
| { | ||
| QSignalSpy spy(hub, &BtleHub::signal_battery); | ||
| hub->simulateNotification(BTLE_UUID_BATTERY_LEVEL, QByteArray(1, char(75))); | ||
| // BtleHub emits for any reading; WorkoutDialog applies the threshold | ||
| QCOMPARE(spy.count(), 1); | ||
| QCOMPARE(spy.at(0).at(1).toInt(), 75); | ||
| } |
There was a problem hiding this comment.
testBattery_aboveThreshold_noSignal() contradicts both its own name and the assertion: the test expects signal_battery to be emitted (count == 1). Please rename the test (and/or update the comment) so the test name reflects the actual expected behavior.
| /// Compute the PMC curve from \a history. | ||
| /// | ||
| /// The returned list spans from the earliest activity date (or \a from, | ||
| /// whichever is earlier) to \a to (inclusive), one entry per calendar day. | ||
| /// | ||
| /// \param history Parsed workout summaries (any order). | ||
| /// \param to Last date to include. Defaults to today. | ||
| /// \param leadInDays Number of days before the first activity to start the | ||
| /// computation (with TSS=0) so that CTL/ATL have time to | ||
| /// warm up. Default: 42. |
There was a problem hiding this comment.
The docstring references a from parameter (“earliest activity date (or \a from, whichever is earlier)”) but compute() doesn’t accept from. Please adjust the documentation to match the actual API (or add the missing parameter if it was intended).
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
c2f8a4e
into
feat/issue-144-workout-history
#174) * Implement workout queue, performance management chart, and WASM support (#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> * fix: apply PR review feedback — battery cleanup, settings clamping, cancel 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> --------- 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>
This pull request adds Progressive Web App (PWA) support and persistent credential storage to the WASM (WebAssembly) build, and introduces Bluetooth Battery Service support for reporting sensor battery levels. It also improves app theming and refines how credentials are handled in the browser environment.
PWA and Offline Support:
manifest.json), service worker (service-worker.js), and relevant HTML meta tags to enable installability, offline caching, and a more native experience on mobile devices. [1] [2] [3] [4]Bluetooth Battery Service Integration:
Persistent Credential Storage in Browser:
localStorage(via Emscripten interop), allowing OAuth tokens for third-party services to persist across browser sessions. [1] [2]App Theming Improvements:
PWA and Offline Functionality
manifest.jsonand service worker (service-worker.js) to enable PWA features, offline caching, and installability for the web app. Updated HTML to include relevant meta tags and service worker registration logic. [1] [2] [3] [4]Bluetooth Battery Service Support
signal_batterysignals with sensor type and battery percentage. Updated both native and WASM Bluetooth hub implementations.Browser Credential Persistence
localStorage, allowing OAuth tokens to persist across browser sessions.Theming Enhancements