Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[run]
omit = tests/*
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: CI

on:
push:
branches: [main, development]
pull_request:

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.13"]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}
cache: pip
cache-dependency-path: requirements*.txt
- run: pip install -r requirements.txt -r requirements-dev.txt
- run: ruff check .
- run: python -m pytest --cov=. --cov-report=term-missing
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ config.json
*.db
*.db-wal
*.db-shm
.coverage
htmlcov/
coverage.xml
25 changes: 25 additions & 0 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
when: never
- if: $CI_COMMIT_BRANCH

test:
image: python:3.12
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
cache:
key:
files: [requirements.txt, requirements-dev.txt]
paths: [.cache/pip]
script:
- pip install -r requirements.txt -r requirements-dev.txt
- ruff check .
- python -m pytest --cov=. --cov-report=term-missing --cov-report=xml
coverage: '/^TOTAL.+?(\d+(?:\.\d+)?%)$/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ All notable changes to WalkingDad will be documented in this file.

## [Unreleased]

### Fixed

- **Pause and speed buttons no longer error out when Bluetooth isn't running.** If the Bluetooth connection had already gone away, pressing Pause or a speed button showed a server error page. Pause now still pauses the session, and the speed buttons do nothing.
- **A corrupted crash-recovery file no longer stops the app from starting.** If `session_state.json` contained unreadable (non-UTF-8) bytes, startup crashed. Startup now ignores the file, the same as an unparseable one.

### Internal

- **Tests now cover nearly all of the Python code** (ROADMAP 2.5): routes, Bluetooth sequences against a fake treadmill, status-packet math, persistence, and `run.py`'s launcher. The suite runs in about a second.
- **Continuous integration** (ROADMAP 2.7): `ruff` lint and the test suite run on pull/merge requests and on pushes to `main`/`development` via GitHub Actions (Python 3.10 and 3.13) and GitLab CI (Python 3.12, with coverage shown in merge requests). `ruff` is pinned in `requirements-dev.txt`, and lint fixes in `app.py` (imports sorted, unused `global` declarations removed) have no behavior change.
- **Importing `app.py` with `WALKINGDAD_NO_STARTUP=1` no longer installs the Ctrl+C/SIGTERM handlers or the `atexit` hook**, so pressing Ctrl+C during a test run interrupts pytest instead of killing the process two seconds later. Running the app normally is unchanged.
- **`run.py`'s launcher logic moved into a `main()` function** so it can be tested. `python run.py` behaves the same.

---

## [1.8.0] (2026-10-05)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ The app opens your browser automatically at `http://127.0.0.1:5001`. A console w

**Windows shortcut:** Double-click `start_app.bat` instead of running the commands manually.

**Running tests:** `pip install -r requirements-dev.txt`, then `python -m pytest`. No treadmill needed. Tests that import `app.py` run with `WALKINGDAD_NO_STARTUP=1` (set in `tests/conftest.py`), which skips its import-time startup: opening the real database, migrating `session_history.json`, and the Bluetooth scan.
**Running tests:** `pip install -r requirements-dev.txt`, then `python -m pytest` (add `--cov=.` for a coverage report) and `ruff check .`. No treadmill needed. Tests that import `app.py` run with `WALKINGDAD_NO_STARTUP=1` (set in `tests/conftest.py`), which skips its import-time startup: opening the real database, migrating `session_history.json`, the Bluetooth scan, and installing the Ctrl+C/exit handlers. CI runs the same lint and tests on pull/merge requests and on pushes to `main`/`development`, via GitHub Actions (`.github/workflows/ci.yml`) and GitLab CI (`.gitlab-ci.yml`).

## Usage

Expand Down
72 changes: 52 additions & 20 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,15 +166,25 @@ All user-tunable settings are now loaded from an optional `config.json` file, wi

---

### 2.5 Unit Tests
- **Status:** In Progress
- **Priority:** Medium
- **Problem:** Most of `app.py` has no automated tests, making refactoring risky. Covered so far: `storage.py`, the JSON migration, `units.py`, `samples.py` (2.9), and in `app.py` the stale-pause check (1.5) plus `process_status_packet()`'s active-time accrual and auto-pause detection, and `run.py`'s port-in-use check (1.6).
- **Solution:** Extend the suite to `app.py`'s logic:
- `format_seconds_to_hms()` - time formatting edge cases
- `kcal_estimate()` - calorie calculation
- `process_status_packet()` - distance/steps accumulation and device counter resets, speed history management (mock BLE data)
- **Implementation:** Tests import `app.py` directly; `tests/conftest.py` sets `WALKINGDAD_NO_STARTUP=1` so the import skips opening `walkingdad.db`, the JSON migration, the orphan sweep, the BLE thread, and the SSE broadcaster.
### ✅ 2.5 Unit Tests
**Status:** ✅ Complete
**Priority:** Medium
**Files Modified:** `app.py`, `run.py`, `requirements-dev.txt`, `pytest.ini`, `ruff.toml`, `.coveragerc`, `.gitignore`, `README.md`, `tests/`

A `pytest` suite covering every module without a treadmill. How to run it: the README's **Running tests** line.

| Area | Coverage |
|---|---|
| **Pure helpers** | Time formatting, calorie estimate, status-field extraction, settings casts and clamps, Apple Health shortcut URLs, stats payload and SSE framing |
| **Status packets** | Distance/step accumulation across device counter resets, calories, speed history filtering and cap, resume speed chosen on auto-pause, liveness stamping (plus the existing active-time and auto-pause tests) |
| **Routes** | Every Flask route through the test client: start/pause/resume/end, speed controls and clamping, restore/discard, CSV export, settings save, `/stats`, `/stats_stream`, `/shutdown` |
| **BLE and async** | A fake controller drives the wake/start/stop sequences, light wake, lock timeouts, the stats monitor, the idle watchdog, disconnect handling, auto-reconnect backoff, scanning, connecting, the BLE thread lifecycle, and signal/HTTP shutdown |
| **Persistence** | Session-state file save/load/clear (including corrupt files), `config.json` writes, and every database helper against a temp SQLite DB, including "log and continue" behavior when storage fails |
| **`run.py`** | Port check, browser launch, the HTTP shutdown call, and `main()`'s Waitress subprocess and Ctrl+C handling |

**Implementation:** `tests/conftest.py` sets `WALKINGDAD_NO_STARTUP=1` before importing `app.py`, so the import skips opening `walkingdad.db`, the JSON migration, the orphan sweep, the BLE thread, the SSE broadcaster, and installing the signal/`atexit` handlers. Its `app_state` fixture resets every module global, points the state file, `config.json`, and the database at `tmp_path`, and records BLE coroutines instead of scheduling them, so tests can run a captured sequence against a fake controller.

**Not covered:** the inline template JavaScript (left to 2.12's browser smoke test); code that only runs at import (`app.py`'s startup block, `config.py`'s no-`config.json` path); the background-thread entry points (SSE broadcaster start, `/shutdown`'s delayed exit); `_ble_thread`'s handlers for errors escaping asyncio's own `run_forever()` and task cleanup; and `run.py`'s Windows branch and `__main__` guard.

---

Expand All @@ -187,12 +197,20 @@ All user-tunable settings are now loaded from an optional `config.json` file, wi

---

### 2.7 Continuous Integration
- **Status:** Planned
- **Priority:** Medium
- **Problem:** There's no GitHub Actions workflow; nothing runs automatically on push/PR, even though a `pytest` suite now exists (2.5).
- **Solution:** A GitHub Actions workflow that installs dependencies and runs the test suite plus a linter, on every push and PR.
- **Implementation:** `.github/workflows/ci.yml` running on `ubuntu-latest`, `pip install -r requirements.txt -r requirements-dev.txt`, then `pytest` and a linter (e.g. `ruff`).
### ✅ 2.7 Continuous Integration
**Status:** ✅ Complete
**Priority:** Medium
**Files Modified:** `.github/workflows/ci.yml`, `.gitlab-ci.yml`, `requirements-dev.txt`, `ruff.toml`

Pull/merge requests and pushes to `main`/`development` run the lint and test suite on both GitHub Actions and GitLab CI.

| Feature | Description |
|---|---|
| **GitHub Actions** | `.github/workflows/ci.yml` on `ubuntu-latest`, Python 3.10 (the supported floor) and 3.13. Pushes run only on `main`/`development`, so a PR branch isn't tested twice |
| **GitLab CI** | `.gitlab-ci.yml` on the `python:3.12` image. Runs a merge-request pipeline when an MR is open, otherwise a branch pipeline, and shows the coverage percentage and a Cobertura report in the MR |
| **Steps** | `pip install -r requirements.txt -r requirements-dev.txt`, `ruff check .`, `python -m pytest --cov=.`. CI reports coverage but never fails on it |
| **Lint** | `ruff` pinned in `requirements-dev.txt`, since its default rule set changes between releases. `ruff.toml` ignores only rules that flag deliberate project style (root logger, catch-all excepts around BLE I/O, naive local datetimes, a `ValueError` raised to share an `except` clause) |
| **Linux only** | BLE is fully mocked, so no Bluetooth hardware or OS-specific stack is needed. `tests/test_run.py` assumes POSIX (macOS/Linux) |

---

Expand Down Expand Up @@ -238,23 +256,23 @@ Session data lives in a local SQLite database (`walkingdad.db`, configurable via
- **Priority:** Medium
- **Problem:** Pinning versions (2.6) fixes what's installed but doesn't catch a known CVE in whatever gets pinned, or a new one disclosed later against an already-pinned version.
- **Solution:** Enable Dependabot security alerts on the repo, and/or run `pip-audit` against `requirements.txt` in CI (2.7).
- **Implementation:** A `.github/dependabot.yml` for version-update PRs plus security alerts; a `pip-audit` step added to the CI workflow from 2.7.
- **Implementation:** A `.github/dependabot.yml` for version-update PRs plus security alerts; a `pip-audit` step added to both CI configs from 2.7.

---

### 2.12 Integration Smoke Test
- **Status:** Planned
- **Priority:** Medium
- **Problem:** 2.5's unit tests cover pure functions, but nothing exercises the actual Start → Pause → Resume → End route flow end to end, so a template or routing regression could still slip through.
- **Problem:** 2.5's tests drive each route through Flask's test client, but nothing loads the rendered pages in a real browser and clicks through Start → Pause → Resume → End, so a template JavaScript or front-end wiring regression could still slip through.
- **Solution:** One browser-driven smoke test that boots the app against a mocked BLE device/controller and clicks through the full session lifecycle, asserting each screen renders and each transition lands where expected.
- **Implementation:** Playwright (or Selenium) driving a test instance of the Flask app with `controller`/`BleakScanner` mocked out; run as part of the CI workflow from 2.7.
- **Implementation:** Playwright (or Selenium) driving a test instance of the Flask app with `controller`/`BleakScanner` mocked out; run as part of the CI configs from 2.7.

---

### 2.13 CONTRIBUTING.md
- **Status:** Planned
- **Priority:** Low
- **Problem:** There's no documented contribution process or versioning policy, which matters more once 2.7 (CI) and 2.5 (Unit Tests) make outside PRs realistically reviewable.
- **Problem:** There's no documented contribution process or versioning policy, which matters more now that 2.7 (CI) and 2.5 (Unit Tests) make outside PRs realistically reviewable.
- **Solution:** A `CONTRIBUTING.md` covering local setup, how to run tests/lint, commit/PR expectations, and the project's versioning policy (semver against `CHANGELOG.md`).
- **Implementation:** Plain markdown doc, no tooling.

Expand Down Expand Up @@ -432,12 +450,26 @@ Active, Paused, and Start screens redesigned to read like the WalkingPad's own o
### 3.17 Tablet-Width Responsive Breakpoint
- **Status:** Planned
- **Priority:** Low
- **Problem:** `base.html` has exactly one `@media (max-width: 480px)` rule, tuned for phone-width screens. Desktop is the primary use case, but the app's own pitch covers "any browser on your network," and 2.7/3.7 (Touch/Swipe Gesture Controls) already anticipate a phone/tablet as a secondary device, so there's nothing tuned for the tablet width range in between.
- **Problem:** `base.html` has exactly one `@media (max-width: 480px)` rule, tuned for phone-width screens. Desktop is the primary use case, but the app's own pitch covers "any browser on your network," and 3.7 (Touch/Swipe Gesture Controls) already anticipates a phone/tablet as a secondary device, so there's nothing tuned for the tablet width range in between.
- **Solution:** Add an intermediate breakpoint (e.g. `max-width: 900px`) tuned for tablet-class screens (iPad-size), rather than jumping straight from desktop layout to the 480px phone rules.
- **Implementation:** Audit `console-hero`/`console-strip`/history table layout at common tablet widths (768-1024px) and add a second `@media` block alongside the existing 480px one.

---

### 3.18 Transition Hint Shows on Every Button Press
- **Status:** Planned
- **Priority:** Medium
- **Problem:** Clicking a speed preset or stepper on the active screen briefly shows "Getting the belt moving…" with a spinner, and the buttons jump down while it's visible. Every button has a variant of this, and the text is wrong for most of them:
- Pause and End flash "Getting the belt moving…"
- Start flashes "Stopping belt…"
- Resume flashes "Pausing belt…"

Each page's `#transitioning-hint` text describes the transition *into* that page (active: after Start/Resume; paused: after Pause; start: after End), and the `belt_transitioning` flag in the SSE stats payload is meant to drive it. But `disableButtonsOnSubmit()` in `templates/base.html` also calls `setTransitioning(hintId, …, true)` on every form submit, so the hint appears for the request round-trip. The page then reloads with it hidden again, because speed changes never set `_belt_transitioning`. The hint toggles `display: none`, so showing it reflows the layout.
- **Solution:** On submit, only disable and dim the buttons, which is all the double-tap guard needs. Leave the hint to the server's `belt_transitioning` flag, so it only ever shows during a real start/pause/resume/end belt sequence, with text that matches. Optionally reserve the hint's space (toggle `visibility` instead of `display`) so the legitimate post-Start/Resume hint doesn't shift the layout either.
- **Implementation:** Drop the hint from `disableButtonsOnSubmit()` (`templates/base.html`; it can take just `buttons`, or call `setTransitioning(null, buttons, true)`, which `getElementById(null)` already tolerates). Update its three call sites in `active_session.html`, `paused_session.html` and `start_session.html`, plus the comments there that describe the behavior. No backend change. Verify by hand in the browser: presets and steppers show no hint or shift, and the correct hint still appears after Start, Pause, Resume and End while the belt sequence runs. Template JS has no automated tests (see 2.12).

---

## Stats & Motivation

### 4.1 Estimated Time to Distance Goal
Expand Down
Loading
Loading