🇫🇷 Version française | 🇬🇧 English version
A full-stack Laravel 12 + Vue.js 3 application built with strict TDD, layered service architecture and 100% test coverage — from geospatial GPX parsing algorithms to a dynamic progression dashboard.
V2 (current — v2.0.0) — Geographic visualization: zoomable elevation profile with map synchronization, interactive OSM map with GPX trace, automatic altitude enrichment via OpenTopoData, SSE upload progress.
V1 (v1.0.0) — Full pipeline: GPX parsing, slope segmentation, 22 metrics per activity, REST API, Vue.js 3 dashboard, 99% test coverage, GitHub Actions CI.
Standard sports apps (Strava, Garmin Connect) provide global statistics but fall short for meaningful progression analysis:
- What is my ascent speed on slopes steeper than 25%?
- How has my endurance evolved on long mountain outings over the last six months?
- Am I faster in trail running than hiking on moderate gradients?
Summit Stats segments each GPX trace by terrain type and slope class, then lets you compose your own progression metrics through a dynamic filtering interface.
| Metric | Value |
|---|---|
| Test coverage | 100% (backend + frontend) |
| Automated tests | 118 backend · 62 frontend unit · 32 E2E = 212 tests |
| API endpoints | 7 REST routes |
| Metrics per activity | 22 stats stored in database |
| GPX pipeline | 4 services, strict TDD |
| PHP lines of code | ~2,000 (excluding migrations and config) |
- GPX import — drag & drop upload, automatic analysis on import
- GPX analysis pipeline — segmentation by type (ascent / descent / flat) and slope class, Haversine formula, elevation smoothing via sliding average window
- 22 stats per activity — total and moving speed (pauses > 30s excluded), ascent speed (average / to summit / longest non-descending segment), descent rate, % breakdown by slope class for both ascent and descent
- On-demand recalculation — per-activity button +
php artisan stats:recalculatecommand - Progression dashboard — time-series charts recalculated on the fly by metric, type, environment, period and slope range
- Advanced filters — combinable: activity type, environment, period, specific activity, slope range (from / to)
- Activity history — paginated list with filters and summary stats
app/
├── Http/
│ ├── Controllers/Api/ # Thin controllers — delegate all logic to services
│ ├── Requests/ # Form Requests (input validation)
│ └── Traits/ # ApiResponse — standardised JSON responses
├── Models/ # Activity, Segment
└── Services/
├── ActivityService.php # Persistence: store, update, recalculate, destroy
└── Gpx/ # GPX analysis pipeline (strict TDD)
├── GpxParserService # XML parsing → normalised point array
├── ElevationCalculatorService # Haversine distance, D+/D-, total & moving duration
├── SegmentationService # Segmentation by terrain type and slope class
├── StatsAggregatorService # Aggregation of 22 metrics
└── GpxAnalysisOrchestrator # Full pipeline orchestration
resources/js/
├── pages/ # Dashboard, Activities, ActivityDetail, Login
├── components/ # NavBar, GpxUploadForm, StatCard, PctBar, ProgressionChart
├── helpers/ # Formatting (distance, duration, speed, date)
├── stores/ # Pinia store (activities)
└── router/ # Vue Router with authentication guard
php artisan test # run all tests
php artisan test --coverage # with coverage report (requires pcov)
php artisan test --coverage --min=80 # with minimum threshold- Strict TDD on all 4 GPX services — tests written before implementation
- Feature tests on all 7 API endpoints + Artisan command
- Unit tests on models and the
ApiResponsetrait - Regression test on a real GPX trace (11.8 km, ~470 m elevation gain)
- In-memory SQLite (
:memory:) — full isolation, entire suite runs in < 3s
npm run test:coverage # run unit tests with coverage report- 62 tests, 100% coverage on components, helpers and Pinia store
- JSDOM environment, Vue Test Utils
npm run test:e2e # requires the Docker stack to be running (see Docker section)- 5 spec files, 32 scenarios covering auth, upload, dashboard, activity list and detail
- Runs against the full Docker stack (Nginx + PHP-FPM + PostgreSQL)
- Chromium only
Husky + lint-staged run automatically on every git commit:
- JS/Vue staged files —
eslint --fixthenprettier --write - PHP staged files —
vendor/bin/pint
Style issues are auto-fixed before the commit is recorded — they never reach CI.
CI — triggers on push and pull request to main:
- PHP linting (Pint) + JS linting (ESLint) — runs before tests for fast failure on style issues
- PHP backend tests with coverage (Pest + pcov) — report uploaded to Codecov (
backendflag) - Frontend unit tests (Vitest) — report uploaded to Codecov (
frontendflag)
E2E — triggers on push to main:
- Builds and starts the full Docker stack
- Runs migrations and seeds test data (user + sample activity)
- Runs the Playwright suite (Chromium)
- Uploads Playwright HTML report as artifact on failure (7-day retention)
- Gates production deployment — the CD workflow only triggers if E2E passes
build-deploy — triggered by E2E success via workflow_run:
- Builds and pushes two Docker images to GHCR:
app(PHP-FPM) andnginx(assets baked in) - Images tagged with short SHA (
sha-xxxxxxx) andlatest - Deploys to the production server via SSH: pulls new images,
docker compose up -d, applies pending migrations workflow_dispatchavailable for manual redeploys (hotfixes, rollbacks) — bypasses E2E gate
push main
├── CI ─────────── lint → tests → coverage
└── E2E ─────────── Playwright (full Docker stack)
│ workflow_run (success only)
▼
build-deploy ── build → GHCR → SSH deploy
workflow_dispatch ──────► build-deploy (manual, bypasses E2E)
The application runs on a VPS behind a shared Traefik reverse proxy with automatic TLS via Let's Encrypt. Images are built in CI and stored in GHCR — no source code on the server, no build step in production.
| Service | Image | Role |
|---|---|---|
nginx |
custom (nginx:alpine) | Web server, static assets, OSM tile proxy cache |
app |
custom (PHP 8.4-FPM) | Laravel application |
postgres |
postgres:16-alpine | Production database |
redis |
redis:7-alpine | Cache, sessions, queue |
queue |
custom (PHP 8.4-FPM) | Laravel queue worker |
Only nginx is exposed to Traefik via the shared traefik-public network. All other services run in a private Docker network.
Nginx proxies OpenStreetMap tile requests through /tiles/{z}/{x}/{y}.png and caches responses on a persistent Docker volume (capped at 1 GB, TTL 30 days). Reduces load on OSM's infrastructure and speeds up repeat visits to previously explored areas.
All routes protected by Authorization: Bearer {token}.
| Method | Route | Description |
|---|---|---|
POST |
/api/activities |
GPX import + automatic analysis |
GET |
/api/activities |
Paginated list (filters available) |
GET |
/api/activities/{id} |
Detail + segments |
PUT |
/api/activities/{id} |
Update metadata |
DELETE |
/api/activities/{id} |
Delete activity + GPX file |
POST |
/api/activities/{id}/recalculate |
Recalculate stats from raw GPX |
GET |
/api/stats |
Progression data for charts |
Example — /api/stats
GET /api/stats?metric=avg_ascent_speed_mh&type=trail&slope_min=15&slope_max=35&date_from=2024-01-01
{
"data": [
{ "date": "2024-03-15", "value": 423.5, "activity_title": "Aiguilles Rouges" },
{ "date": "2024-04-02", "value": 512.0, "activity_title": "Col de Balme" }
],
"meta": { "metric": "avg_ascent_speed_mh", "unit": "m/h", "count": 2 }
}Prerequisites — PHP >= 8.3 (pdo_sqlite, sqlite3, xml, mbstring, fileinfo, pcov), Composer >= 2, Node.js >= 18
git clone https://github.com/MarvinLeRouge/Summit-Stats.git && cd Summit-Stats
composer install && npm install
cp .env.example .env
# Fill in APP_USER_NAME, APP_USER_EMAIL, APP_USER_PASSWORD
php artisan key:generate
# ⚠️ Manually create .env.testing with DB_DATABASE=:memory: (not committed)
touch database/database.sqlite && php artisan migrate
php artisan db:seed --class=UserSeeder # token printed in console
php artisan serve # → http://localhost:8000
npm run dev # → http://localhost:5173 (HMR)A full development stack is provided via Docker Compose:
| Service | Description | Host port |
|---|---|---|
nginx |
Web server | 8081 |
app |
PHP-FPM (Laravel) | — |
postgres |
PostgreSQL 16 | 5433 |
redis |
Redis 7 | 6379 |
queue |
Laravel queue worker | — |
vite |
Vite dev server (HMR) | 5174 |
cp .env.example .env
# Fill in APP_USER_NAME, APP_USER_EMAIL, APP_USER_PASSWORD and DB_PASSWORD
docker compose up -d --build
docker compose exec app php artisan migrate
docker compose exec app php artisan db:seed --class=UserSeeder # token printed in consoleThe app is then available at http://localhost:8081.
Note: PostgreSQL is used in Docker. SQLite (
:memory:) is used only for unit/feature tests.
Personal project with a dual purpose:
- Learning — layered service architecture (TDD, single responsibility, dependency injection), geospatial algorithms (Haversine, sliding average smoothing, GPS trace segmentation), and modern tooling (Laravel 12, Vue.js 3 Composition API, Pinia, Chart.js, Pest, GitHub Actions)
- Portfolio — demonstrates a structured, tested and documented approach, from scoping to CI
| Class | Label | Range |
|---|---|---|
lt5 |
Flat | < 5 % |
5_15 |
Moderate | 5 % – 15 % |
15_25 |
Steep | 15 % – 25 % |
25_35 |
Hard | 25 % – 35 % |
gt35 |
Extreme | > 35 % |
- Project scoping and architecture
- P1 — Setup (Laravel 12, Pest, Sanctum, Vue.js 3)
- P2 — Data model / Modèle de données (migrations, Eloquent)
- P3 — GPX algorithm (parsing, segmentation, stats) — strict TDD
- P4 — REST API (7 endpoints + feature tests)
- P5 — Vue.js frontend (dashboard, charts, dynamic filters)
- P6 — Quality (99% coverage, PHPDoc, Pint, ESLint)
- P7 — DevOps (GitHub Actions CI, documentation)
- P1 — Track points storage +
/api/activities/{id}/trackendpoint - P2 — Elevation profile (Chart.js + zoom)
- P3 — OSM map with GPX trace (Leaflet)
- P4 — Quality (tests, coverage, linting)
- P5 — DevOps (CI update, V2 documentation)
- E2E test suite (Playwright — 32 scenarios, dedicated CI workflow)
- Docker Compose stack (Nginx, PHP-FPM, PostgreSQL, Redis, queue worker)
- Frontend coverage reporting on Codecov (per-flag badges)
- Production deployment — Traefik, HTTPS, GHCR, SSH CD pipeline
- Pre-commit hooks — Husky + lint-staged (auto-fix PHP and JS/Vue)
- Optimised CI/CD pipeline — lint-first, E2E-gated deploy, workflow chaining
- Automated database backup — scheduled
pg_dumpon the VPS, file rotation, restore documentation - Email / password authentication — replace manual token management with a real login form (Sanctum via credentials)
- Activity export — download data as CSV or JSON from the interface
- Multi-user support — separate accounts with isolated activity data, opening the app to a small group of users
This project is licensed under the MIT License - see the LICENSE file for details.