Files
intotheeast-com/docs/working/milestones/milestone-3.md
m038andClaude Opus 5 8202d2a257 docs: reconcile documentation against the code; add a supersession ledger
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>
2026-07-25 00:13:37 +02:00

6.4 KiB
Raw Permalink Blame History

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.


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):

{% 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:

{% 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.

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).


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