docs: move remaining untracked files to restructured locations

- milestone specs: docs/milestone-*-spec.md → docs/working/milestones/milestone-*.md
- qa files: docs/qa-*.md → docs/working/qa/*.md
- research files: docs/research-*.md → docs/research/*.md
- design spec: docs/design/design-spec.md → docs/reference/design-system.md
- backlog, pm-analysis, summary: moved to docs/working/
This commit is contained in:
2026-06-21 12:42:32 +02:00
parent 5aad6a3760
commit 05d65652bd
12 changed files with 2384 additions and 0 deletions
+182
View File
@@ -0,0 +1,182 @@
# Milestone 3 Spec — Statistics Page
**Goal:** A `/stats` page showing key trip numbers: days on the road, entries posted, countries visited, and approximate distance traveled.
---
## User Stories
- As a reader, I want to see a quick summary of how far Mischa has traveled and how many countries they've visited, without having to read every entry.
- As a traveler (Mischa), I want to see my own trip stats at a glance — a satisfying progress indicator while traveling.
- As a reader, I want stats that update automatically as new entries are posted — no manual maintenance.
---
## Feature Details
### 3.1 — Stats Page
**Route:** `/stats`
**Template:** `stats.html.twig` — extends `partials/base.html.twig`
**Page file:** `user/pages/04.stats/stats.md`
**Computed in Twig** (server-side, from published entries under `/tracker`):
---
### 3.2 — Stat: Days on the Road
**Definition:** Number of calendar days from the date of the first published entry to today.
**Formula (Twig):**
```twig
{% set first_entry = entries|first %}
{% set days = (now.timestamp - first_entry.date|date('U'))|round / 86400 %}
{% set days_on_road = [days|round(0, 'floor'), 0]|max %}
```
**Display:** `42 days on the road`
**Edge cases:**
- No entries: show `0 days on the road` or `Trip not started yet`
- Only one entry (today): show `1 day on the road`
---
### 3.3 — Stat: Entries Posted
**Definition:** Count of all published entries under `/tracker`.
**Display:** `17 entries posted`
**Edge cases:**
- 0 entries: `0 entries posted`
- 1 entry: `1 entry posted` (singular)
---
### 3.4 — Stat: Countries Visited
**Definition:** Unique values of `location_country` across all published entries, non-empty.
**Display:** Count + list
```
6 countries visited
Japan · South Korea · Mongolia · Russia · Finland · Estonia
```
**Edge cases:**
- No entries have `location_country`: show `Countries: —`
- Some entries missing `location_country`: count only those that have it; note "(based on X of Y entries)"
- Duplicate country names are de-duplicated (case-insensitive)
---
### 3.5 — Stat: Approximate Distance Traveled
**Definition:** Sum of great-circle (haversine) distances between consecutive entries that have valid lat/lng, in ascending date order.
**Implementation:** Computed in Twig using a haversine formula macro.
**Haversine in Twig:**
```twig
{% macro haversine(lat1, lng1, lat2, lng2) %}
{% set R = 6371 %}
{% set dLat = ((lat2 - lat1) * 3.14159265 / 180) %}
{% set dLng = ((lng2 - lng1) * 3.14159265 / 180) %}
{% set a = (dLat/2)|sin * (dLat/2)|sin + (lat1 * 3.14159265 / 180)|cos * (lat2 * 3.14159265 / 180)|cos * (dLng/2)|sin * (dLng/2)|sin %}
{% set c = 2 * a|sqrt|asin %}
{{ (R * c)|round }}
{% endmacro %}
```
Note: Twig does not have `sin`/`cos`/`asin`/`sqrt` built-in. Use a JavaScript-side calculation instead:
**Implementation:** Embed the entry GPS data as JSON in the template (same pattern as Milestone 2), compute distance in vanilla JS, and write the result into the DOM on page load.
```js
function haversine(lat1, lng1, lat2, lng2) {
var R = 6371;
var dLat = (lat2 - lat1) * Math.PI / 180;
var dLng = (lng2 - lng1) * Math.PI / 180;
var a = Math.sin(dLat/2)**2 + Math.cos(lat1*Math.PI/180) * Math.cos(lat2*Math.PI/180) * Math.sin(dLng/2)**2;
return R * 2 * Math.asin(Math.sqrt(a));
}
var total = 0;
for (var i = 1; i < GPS_POINTS.length; i++) {
total += haversine(GPS_POINTS[i-1][0], GPS_POINTS[i-1][1], GPS_POINTS[i][0], GPS_POINTS[i][1]);
}
document.getElementById('stat-distance').textContent = Math.round(total).toLocaleString() + ' km';
```
**Display:** `~3,400 km traveled`
**Edge cases:**
- 0 or 1 GPS points: `Distance: —`
- Very large numbers (trans-continental trip): use thousands separator: `12,400 km`
- Disclaimer note: "approximate — based on straight lines between entry locations"
---
### 3.6 — Visual Layout
**Layout:** 4 large stat blocks in a 2×2 grid on desktop, stacked on mobile.
Each block:
```
┌─────────────────┐
│ 42 │
│ days on road │
└─────────────────┘
```
- Number: large (3rem), bold, brand blue
- Label: small (0.85rem), muted grey
- Background: white, 1px border, 8px radius, subtle shadow
- Mobile: 2-col grid (2 stats per row)
Below the grid: list of countries visited (plain text, centered, muted).
---
### 3.7 — Navigation Link
Add "Stats" to the site navigation in `partials/base.html.twig`.
---
## Out of Scope (Milestone 3)
- Charts or graphs (bar charts, line graphs, etc.)
- World map with highlighted countries (that's a visual enhancement, deferred)
- Per-country breakdown (km in each country, days in each country)
- Speed statistics (km/day average)
- Elevation statistics
- Historical comparison (vs. last trip)
---
## Acceptance Criteria
1. `/stats` page exists and returns HTTP 200
2. "Days on the road" shows correct count from first entry date to today
3. "Entries posted" shows count of published entries
4. "Countries visited" shows correct count + list of unique non-empty `location_country` values
5. "Distance traveled" shows km sum of haversine distances between consecutive GPS entries
6. All four stats display in a 2×2 grid on desktop
7. On mobile (375px), stats stack into a 2-column responsive grid
8. Stats auto-update when new entries are published (no manual maintenance)
9. If no entries: all stats show 0 or `—`, no JS errors
10. "Stats" link in navigation routes to `/stats`
---
## Design Notes
- Stats should feel like a dashboard, not a table — big numbers, small labels
- Do not use any external charting library for v1
- Countries list below the grid: inline, separated by `·`, muted grey
- The "approximate" disclaimer for distance should be in small print below the distance stat