Commit graph

1443 commits

Author SHA1 Message Date
Ullrich Schäfer
82d726bc14
Merge pull request #577 from trails-cool/feat/gpx-parser-robustness-timestamps
feat(gpx): post-parse timestamp repair
2026-07-14 00:11:14 +02:00
Ullrich Schäfer
4536470eb9
feat(gpx): post-parse timestamp repair
Task group 3 of gpx-parser-robustness. Timestamps were passed through raw;
a partially-broken time channel from a flaky recorder silently shrank
moving time and skewed start-time derivation.

New pure `timestamp-repair.ts` applied per segment after parsing:
- validity = Date.parse yields a finite epoch;
- a segment with no valid timestamps is left untouched (untimed track);
- >50% invalid → drop all the segment's timestamps (noise, not signal);
- otherwise → linearly interpolate invalid runs between valid neighbours
  (by point index), leading/trailing runs clamp to the nearest valid
  timestamp; only repaired points are rewritten as ISO 8601, valid points
  keep their original string.

Monotonicity intentionally not enforced (movingTime already skips
non-positive intervals; reordering would be fabrication). No GpxData shape
change. Wired into parseTracks output in parse.ts.

Verified: gpx typecheck + lint clean, 81/81 tests pass, journal + planner
typecheck unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 00:10:57 +02:00
Ullrich Schäfer
99278fd305
feat(gpx): lenient point parsing + route (<rte>) support
Task groups 1–2 of gpx-parser-robustness. The parser trusted its input:
`parseFloat(attr ?? "0")` turned a missing lat/lon into a 0,0 Null Island
point (which passes range validation) and garbage into NaN that poisoned
distance and gain/loss totals; `<rte>`/`rtept` files (Garmin courses, many
exporters) parsed to zero track points and were rejected.

- `parsePoint`: skip a trkpt/rtept whose lat/lon is missing or non-finite
  (no more 0,0 default); a non-finite `<ele>` becomes `undefined` so it
  never leaks NaN into totals. Parsing stays parseFloat-lenient (trailing
  junk like `471.0m` still accepted), gated by Number.isFinite.
- Drop segments left with fewer than 2 points (render nothing / break
  distance math).
- Parse `<rte>` as track segments appended after `<trk>` segments, rtept
  handled identically — route-only files now import.

No GpxData shape change; well-formed files parse identically. Updated the
geom single-point test to the new drop-invariant.

Verified: gpx typecheck + lint clean, 76/76 tests pass, journal + planner
typecheck unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 00:06:35 +02:00
Ullrich Schäfer
e2f25e1d27
Merge pull request #575 from trails-cool/chore/archive-federation-hardening
chore(openspec): archive federation-hardening
2026-07-13 23:59:00 +02:00
Ullrich Schäfer
42149d8e02
chore(openspec): archive federation-hardening
All 13 tasks complete (implementation shipped in #570–573; staging
verification in #574). Archive via `openspec archive` (CLI 1.6.0):

- Creates openspec/specs/federation-operations/spec.md — the new
  capability: durable federation queue, inbound replay defense, instance
  blocklist, published protocol doc, delivery observability (Purpose
  filled in; CLI leaves a TBD placeholder).
- Applies the MODIFIED requirement to openspec/specs/social-federation:
  "Push delivery on local activity create" now guarantees persistent
  queueing + a "Fan-out survives a deploy" scenario.
- Moves the change to openspec/changes/archive/2026-07-13-federation-hardening/.

Both specs pass `openspec validate --strict`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 23:54:56 +02:00
Ullrich Schäfer
55da03132a
Merge pull request #574 from trails-cool/chore/federation-hardening-5.1-verified
chore(federation-hardening): 5.1 verified on staging
2026-07-13 23:51:41 +02:00
Ullrich Schäfer
cca1428f26
chore(federation-hardening): 5.1 verified on staging
Ran the post-deploy staging check on staging.trails.cool:
- Durability: enqueued a real delivery, restarted the journal mid-flight;
  the job survived the restart (still `created` in Postgres afterward)
  and completed after — "Successfully sent activity … to
  social.ullrich.is/users/ullrich/inbox", federation_delivery_total
  {outcome="delivered"}=1, queue drained to 0. The old in-process queue
  would have dropped it.
- Blocklist outbound: a poll-remote-actor for a blocked domain returned
  {skipped: "blocked instance"} with no network fetch.
- Operator block/unblock procedure works against the live staging DB.

Inbound-drop leg is covered by the group-2/3 real-Postgres integration
tests + the two-instance e2e harness (firing it live needs a
peer-initiated signed request). All 13 tasks now complete.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 23:47:32 +02:00
Ullrich Schäfer
b1f5f853c3
Merge pull request #573 from trails-cool/feat/federation-hardening-docs-observability
feat(journal): federation protocol doc + delivery observability
2026-07-13 23:16:59 +02:00
Ullrich Schäfer
711065586e
chore(federation-hardening): record group 5 verification status
5.2 typecheck/lint/test pass locally; test:e2e is enforced by the
required "E2E Tests" CI check. 5.1 is a post-deploy staging check
(fan-out survives a restart; a blocked domain is inert both ways) —
left open with the runbook commands in the PR body.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 23:13:04 +02:00
Ullrich Schäfer
881991ca18
feat(journal): federation protocol doc + delivery observability
Task group 4 of federation-hardening.

4.1 — FEDERATION.md at the repo root: actor discovery (WebFinger, actor,
NodeInfo), object/activity types with real JSON examples (Note, Create,
Delete, the narrow follow-graph inbox), addressing, HTTP-Signature
expectations, the two-layer dedup contract, durable delivery/retry
policy, and blocklist moderation semantics — precise enough for another
implementation to interoperate. Linked from README and docs/architecture.

4.2 — three prom-client metrics + a journal dashboard row:
- `federation_delivery_total{outcome}` — incremented in deliver-activity
  (delivered/skipped/failed).
- `federation_inbox_dropped_total{reason}` — incremented at every inbox
  drop (duplicate | blocked); this is the counter deferred from task 3.2.
- `federation_queue_depth` — gauge sampled at scrape time in
  /api/metrics from PgBossMessageQueue.getDepth(); the restart-loss
  regression detector.
Grafana journal.json gains a Federation row (delivery rate, queue depth,
inbox drops); the logs panels shift down to make room.

Verified: dashboard JSON valid; journal typecheck + lint clean; unit
suite 357 passing (route-template guard unaffected).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 23:11:43 +02:00
Ullrich Schäfer
8f7fd15685
Merge pull request #572 from trails-cool/feat/federation-hardening-blocklist
feat(journal): federation instance blocklist
2026-07-13 23:03:40 +02:00
Ullrich Schäfer
57696286e4
feat(journal): federation instance blocklist
Task group 3 of federation-hardening. There was no blocklist of any kind;
the only lever against a hostile instance was an IP/host block in Caddy.

- New `federation_blocked_instances` table (domain PK, reason,
  created_at). Additive → created by drizzle-kit push.
- `federation-blocklist.server.ts`: exact-host matching —
  `isBlockedDomain`, `isBlockedIri` (unparseable IRI ⇒ treated as
  blocked), and `filterBlockedDomains` for batch recipient filtering.
- Enforced at all three boundaries (spec: federation-operations
  "Instance blocklist"):
  - inbox — each of the 4 listeners silently drops a blocked actor's
    activity (202, no error oracle) before dedup/side effects;
  - delivery enqueue — `enqueueActivityDeliveries` filters blocked
    recipients in one batch query;
  - outbox poll / actor fetch — `pollRemoteActor` refuses a blocked host
    up front (`skipped: "blocked instance"`), before any network.
- Operator procedure (SQL insert/list/delete) documented in the
  deployment runbook's federation section.
- Tests: unit (hostOfIri) + integration against real Postgres covering
  the helper and the delivery + outbox boundaries; inbox uses the same
  tested isBlockedIri primitive.

Note: the inbox-drop *counter* (federation_inbox_dropped_total{reason})
lands with the other metrics in task 4.2; this commit is the enforcement.

Verified: db + journal typecheck + lint clean; drizzle-kit push creates
the table; blocklist integration tests green against real Postgres;
journal unit suite 357 passing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 22:59:11 +02:00
Ullrich Schäfer
3bd1f87e63
Merge pull request #571 from trails-cool/feat/federation-hardening-replay-blocklist
feat(journal): inbound federation replay defense
2026-07-13 22:55:08 +02:00
Ullrich Schäfer
105659df7c
feat(journal): inbound federation replay defense
Task group 2 of federation-hardening. The narrow inbox
(Follow/Undo/Accept/Reject) had no replay protection — only Create(Note)
did, via the activities.remote_origin_iri unique constraint. A remote
redelivering a signed follow-graph activity would re-run its side
effects.

- New `federation_processed_activities` table (activity IRI PK,
  received_at + index). Additive, so drizzle-kit push creates it; no
  hand-written migration needed.
- `federation-replay.server.ts`: `markInboundActivityProcessed` does an
  insert-or-drop (ON CONFLICT DO NOTHING RETURNING) and reports whether
  the IRI is fresh; `sweepProcessedActivities` deletes rows > 30 days old
  (signature date-freshness already rejects older replays).
- Each inbox listener drops a duplicate before side effects. The
  follow-graph handlers are idempotent, so a handler failure whose retry
  is later dropped as a duplicate can't corrupt state.
- `federation-dedup-sweep` job (daily 04:30 UTC) runs the TTL sweep.

Verified: db + journal typecheck + lint clean; drizzle-kit push creates
the table; replay integration test (fresh-vs-duplicate + 30-day sweep)
green against real Postgres; journal unit suite 355 passing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 22:07:30 +02:00
Ullrich Schäfer
71d92306db
Merge pull request #570 from trails-cool/fix/federation-durable-queue
fix(journal): durable Fedify message queue over pg-boss
2026-07-13 21:54:51 +02:00
Ullrich Schäfer
b8fc8fadff
feat(journal): durable Fedify message queue over pg-boss
Task group 1 of federation-hardening. Fedify was configured with
`InProcessMessageQueue`, so every queued outbound delivery, its pending
retry state, and inbox processing task was lost on a container restart
(routine here: deploys, OOM history) — directly contradicting the
social-federation spec's promise that fan-out survives a deploy.

- `federation-queue.server.ts`: `PgBossMessageQueue` implementing Fedify's
  `MessageQueue` over the pg-boss instance the journal already runs,
  mirroring the `PostgresKvStore` adapter. `nativeRetrial = false` keeps
  Fedify the retry-policy owner; pg-boss supplies durability + delayed
  jobs (delay → whole-second `startAfter`, `retryLimit: 0`).
- Swap it in for `InProcessMessageQueue` in `federation.server.ts`;
  document the two intentional queueing layers (our fan-out jobs feed
  Fedify; Fedify's sends now durable underneath).
- `server.ts`: create the durable queue at startup when federation is on.
- Broaden the structural `BossLike` in `boss.server.ts` with the
  work/offWork/createQueue/getQueue methods the adapter needs.
- Tests: enqueue maps retry/delay correctly, consume roundtrip, restart
  durability (fresh listener drains a prior instance's backlog), abort
  stops the worker, depth reports ready vs delayed.

Verified: journal typecheck + lint clean, 7/7 new unit tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 21:46:46 +02:00
Ullrich Schäfer
1ea2559246
Merge pull request #569 from trails-cool/chore/archive-route-surface-breakdown
chore(openspec): archive route-surface-breakdown
2026-07-13 21:25:48 +02:00
Ullrich Schäfer
fc5e361a11
chore(openspec): archive route-surface-breakdown
The route-surface-breakdown change is fully implemented (18/18 tasks,
all artifacts done). Archive it via `openspec archive` (CLI 1.6.0):

- Moves the change to openspec/changes/archive/2026-07-13-route-surface-breakdown/
- Promotes its 5 delta requirements into a new capability spec at
  openspec/specs/route-surface-breakdown/spec.md (surface/waytype
  breakdown from BRouter waytags, async Overpass backfill, live SSE
  update, proportion bars, localized labels)

Purpose paragraph filled in (the CLI leaves a TBD placeholder). Both the
spec and archived change pass `openspec validate --strict`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 21:21:58 +02:00
Ullrich Schäfer
ea10e9dd19
Merge pull request #568 from trails-cool/ci/dependabot-openspec-autofix
ci(dependabot): regenerate OpenSpec tool files on bump
2026-07-13 21:17:26 +02:00
Ullrich Schäfer
52aada6242
Merge remote-tracking branch 'origin/ci/dependabot-openspec-autofix' into ci/dependabot-openspec-autofix 2026-07-13 21:12:04 +02:00
Ullrich Schäfer
fd80815119
ci: rename workflow file to match its name (dependabot-auto-fix.yml)
The workflow was renamed "Dependabot dedupe" -> "Dependabot auto-fix" in
the previous commit; rename the file to match and fix the self-reference
in its error message. Safe — this workflow is not a required status check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 21:10:37 +02:00
Ullrich Schäfer
47be98bd51
Merge branch 'main' into ci/dependabot-openspec-autofix 2026-07-13 21:09:10 +02:00
Ullrich Schäfer
905c2d83b6
ci(dependabot): regenerate OpenSpec tool files on bump
Extends the dependabot auto-fix workflow (formerly "Dependabot dedupe")
to also run `openspec update --force`, so a @fission-ai/openspec bump
regenerates the generated agent skills (.agents/skills/openspec-*) and
opsx slash commands (.claude/commands/opsx/*) and pushes them back to the
PR branch — the manual step that PR #567 had to do by hand for 1.2.0 ->
1.6.0.

Kept as a single workflow (one checkout/commit/push) so the dedupe and
openspec fixups don't race to push the same branch. The commit message
names only the parts that actually changed; no-op when neither applies.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 21:06:57 +02:00
Ullrich Schäfer
8b7420be19
Merge pull request #567 from trails-cool/chore/openspec-1.6-tooling
chore(openspec): regenerate skills + commands for CLI 1.6.0
2026-07-13 21:03:31 +02:00
Ullrich Schäfer
c0bb992d6b
chore(openspec): regenerate skills + commands for CLI 1.6.0
Regenerated the OpenSpec agent skills (.agents/skills/openspec-*) and
opsx slash commands (.claude/commands/opsx/*) from openspec CLI 1.6.0
(was 1.2.0). Adds `allowed-tools` frontmatter, multi-store selection
support (`--store <id>`), and new status fields (planningHome,
changeRoot, artifactPaths, actionContext).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 20:59:01 +02:00
Ullrich Schäfer
614e577e26
Merge pull request #566 from trails-cool/fix/dependabot-security-overrides
fix(deps): resolve 12 Dependabot alerts via pnpm overrides
2026-07-13 20:12:04 +02:00
Ullrich Schäfer
b4f290ceed
fix(deps): resolve 12 Dependabot alerts via pnpm overrides
All 12 open Dependabot alerts are transitive dependencies, so resolve
them with pnpm.overrides (the pattern already used in this repo for
picomatch, brace-expansion, path-to-regexp, lodash).

- shell-quote <1.8.4 -> 1.8.4         (critical, #22; react-devtools)
- undici 7.x <7.28.0 -> 7.28.0        (high/med/low ×6, #30-36; jsdom/vitest)
- form-data <4.0.6 -> 4.0.6           (high, #28; jest-expo)
- js-yaml <3.15.0 -> 3.15.0           (medium, #37; jest istanbul)
- esbuild 0.27.x -> 0.28.1            (low, #23; vite/tsx)
- @opentelemetry/core <2.8.0 -> 2.9.0 (medium, #25; fedify runtime)
- uuid 7.0.3 -> 11.1.1                (medium, #19; xcode/expo prebuild)

Notes:
- undici override is scoped to 7.x so fedify's undici@6.27.0 (not in any
  vulnerable range) is left untouched.
- @opentelemetry/core pinned to 2.9.0 (not the minimum 2.8.0) to dedupe
  with the copy Sentry already pulls in, avoiding an otel version skew.
- uuid pinned narrowly to the vulnerable 7.0.3; xcode calls
  require('uuid').v4(), which still works under v11 (verified).

Verified: pnpm install / build / typecheck / test all pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 20:07:26 +02:00
Ullrich Schäfer
004ec4914f
Merge pull request #561 from trails-cool/dependabot/npm_and_yarn/production-ba42795ad1
build(deps): bump the production group across 1 directory with 35 updates
2026-07-13 09:35:19 +02:00
Ullrich Schäfer
d9eee37e39
build(deps): bump the production group across 1 directory with 35 updates
Bumps 35 dependencies in the production group; lockfile regenerated and deduped against latest main.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 09:29:56 +02:00
Ullrich Schäfer
8af5ea47e0
Merge pull request #559 from trails-cool/dependabot/npm_and_yarn/vite-8.1.0
build(deps): bump vite from 7.3.5 to 8.1.0
2026-07-13 08:53:42 +02:00
Ullrich Schäfer
d98dd3c8f4
build(deps): bump vite from 7.3.5 to 8.1.0
Bumps vite catalog to ^8.1.0 and regenerates the lockfile.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 08:49:22 +02:00
Ullrich Schäfer
79addc8f5a
Merge pull request #557 from trails-cool/dependabot/github_actions/actions/cache-6
build(deps): bump actions/cache from 5 to 6
2026-07-13 08:48:56 +02:00
Ullrich Schäfer
ae9c686050
build(deps): bump actions/cache from 5 to 6
Bumps actions/cache from 5 to 6.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 08:44:37 +02:00
Ullrich Schäfer
aa515e5fa0
Merge pull request #565 from trails-cool/archive-poi-index
Archive poi-index + sync specs
2026-07-13 01:51:07 +02:00
Ullrich Schäfer
8396041c26
openspec: archive poi-index; sync deltas into canonical specs
poi-index shipped and is live in production (self-hosted planet POI index,
8.4M rows serving /api/pois; Overpass removed from the Planner). Archive the
change and apply its spec deltas:

- new capability: poi-index
- osm-poi-overlays: POIs from the instance index, not Overpass
- rate-limiting: /api/pois limit replaces the Overpass proxy limit
- infrastructure: POI extract (BRouter host) + import (flagship) components

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 01:46:13 +02:00
Ullrich Schäfer
5539b34613
poi-index: mark operational tasks done (planet extract + import verified in prod)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 01:43:36 +02:00
Ullrich Schäfer
b7c3297d4a
Merge pull request #564 from trails-cool/fix-poi-import-dedup
Fix poi-import: dedupe elements matching a category via multiple selectors
2026-07-13 00:33:46 +02:00
Ullrich Schäfer
55796e7d15
poi-index: dedupe classify INSERT (element matching a category via 2 selectors)
The import hit a PK violation on (osm_type, osm_id, category): an OSM element
that matches one category through two selectors on different keys (e.g. shelter
= amenity=shelter OR tourism=wilderness_hut, both present) produced two rows for
that category. Add DISTINCT ON (osm_type, osm_id, category) so classification is
one row per (element, category), matching matchingCategoryIds semantics.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 00:33:17 +02:00
Ullrich Schäfer
7d6a6147a0
Merge pull request #563 from trails-cool/fix-poi-import-selectors
Fix poi-import: stream selectors SQL into psql stdin
2026-07-13 00:29:41 +02:00
Ullrich Schäfer
a6f8595c4b
poi-index: stream selectors SQL into psql stdin (fix \i container path)
poi-import.sh ran the classifier selectors via psql \i, but psql executes
inside the postgres container (docker compose exec), so it couldn't find the
host path /opt/trails-cool/scripts/poi-selectors.sql. Concatenate BEGIN + the
selectors file contents + the build statements on the host and pipe them into
psql stdin instead, keeping the ON COMMIT DROP temp table in one transaction.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 00:27:48 +02:00
Ullrich Schäfer
552899e2b7
Merge pull request #562 from trails-cool/poi-index
poi-index: replace Overpass with a self-hosted POI index
2026-07-13 00:07:32 +02:00
Ullrich Schäfer
115c76a69a
poi-index: drop vSwitch mentions from poi-extract README (flagship detail)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 23:30:19 +02:00
Ullrich Schäfer
4935e5beae
poi-index: generalize poi-extract docs/units (drop host/username specifics)
Remove ullrich.is / 1.8 TB / hardcoded 'trails' username assumptions from the
poi-extract README and service unit; refer to 'the pipeline user' and $USER /
%h instead so the docs serve self-hosters too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 23:29:45 +02:00
Ullrich Schäfer
9dec68175b
poi-index: drop specific host disk figure from README (drifts; check df at runtime)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 23:18:34 +02:00
Ullrich Schäfer
1b5f48f118
poi-index: run osmium via Docker (host has no osmium + no sudo)
The BRouter host's trails user is non-root without sudo, so osmium-tool can't
be apt-installed. Ship osmium.Dockerfile (built on first use) and run osmium in
a container with the work dir bind-mounted (native I/O, negligible overhead on
Linux). python3/curl/gzip/sha256sum remain on the host. Correct the README
disk figure to the /home free space (~595 GB).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 23:16:57 +02:00
Ullrich Schäfer
157a4fad64
poi-index: ship poi-extract/ to the BRouter host via cd-brouter tarball
cd-brouter.yml bundles an explicit file list; the new poi-extract dir was
missing, so the extract scripts would never land on the host. Add it (+ a
defensive chmod).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 23:12:55 +02:00
Ullrich Schäfer
2a59ad67d9
poi-index: mark 9.2 — planner e2e green (POI tests pass on warm server)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 23:05:50 +02:00
Ullrich Schäfer
bc4bb7988d
poi-index: mark 2.2 done — verified index-driven query plans locally
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 23:02:23 +02:00
Ullrich Schäfer
b1aa687c51
poi-index: name pois PK explicitly (pois_pkey) for drift-free swap
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 22:55:49 +02:00
Ullrich Schäfer
96de8831cf
poi-index: Grafana dashboard/alert, docs, privacy manifest
- planner dashboard: replace Overpass panels with POI index freshness +
  serving panels (age, rows/category, import status, API request rate/errors)
- alerts: replace overpass-upstream-unhealthy with poi-index-stale (>6 weeks)
- architecture.md: POI data flow now the self-hosted index
- privacy manifest (DE+EN): Overpass is Journal-surface-backfill-only;
  Planner POIs served same-origin from /api/pois
- self-host-overpass README + roadmap: superseded for POIs by poi-index
- poi-extract README: self-hoster story (optional pipeline, regional extract,
  graceful empty index)
- map-core tsconfig: exclude *.sync.test.ts from tsc to keep it zero-dep

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 22:54:51 +02:00