Five weeks of undocumented evolution left the docs describing a site that
partly no longer exists, with nothing marking which documents were historical.
The code is treated as the source of truth throughout; every claim below was
verified against code, config or the Makefile rather than inferred.
Two mechanisms, following patterns the repo already used:
- docs/reference/superseded-decisions.md (new) — one authoritative table of all
14 reversals: what was planned, where, what is true now, when, and why. Plus
a short list of decisions that were NOT reversed, since their planning docs
are old enough to look suspect.
- Inline "> **Superseded ...**" notes at each stale claim, so a claim can never
be read un-corrected. This mirrors the existing "> History:" notes in
architecture.md and "> **Changed 2026-07:**" in trip-switching.md.
Scope split by tense: present-tense docs (CLAUDE.md, reference/, guides/,
README.md, CONCEPTS.md) are corrected; past-tense records (plans/, specs/,
milestones/, summary.md, pm-analysis.md) are annotated only, never rewritten —
their staleness is what makes them records.
Present-tense corrections:
- CLAUDE.md asserted css-compiled/ is generated from css/style.css and
css/tokens.css. That source relationship does not exist: css/ is hand-authored
and served directly via assets.addCss in partials/base.html.twig, while
css-compiled/ is esbuild output from the CSS imports inside js/src/*.js.
Highest-severity finding — an always-loaded file inviting a hand-edit of a
generated bundle.
- README.md documented every remote-* target without the -test/-prod suffix
guard-env requires, so its entire server runbook was unrunnable, and listed
7 of ~20 targets while CLAUDE.md designates it authoritative for the full
list. Rewritten with all targets, grouped, and the suffix rule stated.
- README.md told readers to "git clone" into user/, which is a submodule.
- architecture.md: nav is Home + Trips + (authenticated) New Post, not
"Home + Past Trips only"; template tree omitted trips.html.twig,
post-form.html.twig and forms/, and placed base.html.twig at templates/ root
rather than in partials/; entry-actions has three API routes, not just delete;
added the undocumented css-compiled/maplibre-gl.css output and a section on
the /post pin editor as the one sanctioned non-entry-map map.
- design-system.md: documented 13 colour tokens against 19 in tokens.css
(missing --color-error, --color-draft-accent and four glass overlays); claimed
"all 3 map templates"; and described --color-canvas as "white".
- design-system-light.md documented a light palette in present tense. No light
mode exists — tokens.css has a single :root block, no prefers-color-scheme or
data-theme switch, and no light hex appears in css/. Banner added.
- posting.md said photos were optional; they are required, 1-6. It documented
hero_image, which was removed for entries (stories keep it). It had no mention
of the frontend edit flow or photo editor, both shipped 2026-07-08. Photo
files are photo-01..NN, zero-padded.
- working/README.md advertised summary.md as the project's "current state" while
summary.md describes Leaflet, /tracker, /map and /stats. Most misleading line
in the tree.
- CLAUDE.md: recorded js/src/location-map.js as the one sanctioned exception to
the single-map-path rule, and documented that make start/setup fail on a clean
checkout because docker-compose.yml still builds a travel-memories service
whose source moved out in a80b0a9.
Also: 2026-07-23-post-form-location-override.md read "Not started" while merged
in user/ as dd19995; status corrected.
Findings that are not documentation problems — the compose breakage, a
repeatable drift check, the unused shortcode-gallery-plusplus, and demo fixtures
for retired views — are collected in
docs/working/2026-07-25-doc-drift-recommendations.md and deliberately not acted
on. Design and verification method: docs/working/specs/2026-07-25-docs-reconciliation-design.md
The submodule gitlink is deliberately not bumped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
194 lines
6.4 KiB
Markdown
194 lines
6.4 KiB
Markdown
# 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.
|
||
|
||
> **Superseded — written 2026-06-21. There is no `/stats` page.**
|
||
>
|
||
> The stats themselves shipped and still work — days on the road, entries posted, countries visited,
|
||
> distance (exact from GPX, or a `~`-prefixed haversine estimate without it). They render **inline on
|
||
> the trip page** behind a toggle, computed by `window.initTripStats()` in `js/src/main.js`
|
||
> (R2, retired 2026-07-04). `CLAUDE.md` forbids re-creating the standalone view.
|
||
>
|
||
> Also reversed: **§3.7 nav link** (R6), and the `/tracker` references (R3).
|
||
>
|
||
> Details: [`../../reference/superseded-decisions.md`](../../reference/superseded-decisions.md).
|
||
|
||
---
|
||
|
||
## 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
|