poi-index: extract pipeline (BRouter host) + import job (flagship) + e2e

- poi-extract.sh: PBF download -> osmium tags-filter -> centroid NDJSON +
  manifest, published via the Caddy sidecar at vSwitch-only /poi/*
- osmium-filters.txt + poi-selectors.sql generated from map-core selectors,
  guarded by sync tests so poiCategories stays the single source of truth
- poi-import.sh: checksum verify -> classify into category rows ->
  70% guard (--bootstrap override) -> atomic rename swap; node_exporter
  textfile metric for import outcome
- systemd service+timer units for both halves (monthly, offset)
- cd-infra.yml: ship infrastructure/scripts to the flagship
- e2e: mock /api/pois instead of /api/overpass

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-07-12 22:48:32 +02:00
parent b45e69885d
commit e703843b9b
No known key found for this signature in database
GPG key ID: A32FF691A0F752D9
23 changed files with 747 additions and 31 deletions

View file

@ -8,10 +8,11 @@ import { test as base, expect } from "@playwright/test";
*
* The trigger for this safety net was #282 a POI test that silently
* relied on live Overpass data for months because its `page.route`
* was pointing at a URL the browser no longer hit after `/api/overpass`
* was introduced. With this fixture in place, a request to
* `overpass.private.coffee` (or any other unlisted host) would abort
* and the test would surface the missing mock immediately.
* was pointing at a URL the browser no longer hit after the POI proxy
* was introduced. With this fixture in place, a request to any unlisted
* host would abort and the test would surface the missing mock
* immediately. (POIs are now served from the same-origin `/api/pois`
* index endpoint, so the map makes no third-party POI requests at all.)
*/
const EXTERNAL_ALLOWLIST: RegExp[] = [
// App origins served by Playwright's webServer

View file

@ -328,14 +328,15 @@ test.describe("Integration: BRouter routing", () => {
expect(response.status()).toBe(401);
});
test("rejects /api/overpass without an X-Trails-Session header", async ({ request }) => {
const response = await request.post(`${PLANNER}/api/overpass`, {
data: "data=[out:json];node[amenity=drinking_water](52.52,13.4,52.53,13.41);out;",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Origin: PLANNER,
test("rejects /api/pois without an X-Trails-Session header", async ({ request }) => {
const response = await request.get(
`${PLANNER}/api/pois?bbox=52.52,13.40,52.53,13.41&categories=drinking_water`,
{
headers: {
Origin: PLANNER,
},
},
});
);
expect(response.status()).toBe(401);
});
});

View file

@ -30,17 +30,18 @@ test.describe("Planner overlays", () => {
test("enable POI category shows markers on map", async ({ page, request }) => {
const url = await createSession(request);
await page.route("**/api/overpass", async (route) => {
await page.route("**/api/pois*", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
elements: [
pois: [
{
type: "node",
id: 12345,
lat: 52.52,
lon: 13.405,
name: "Test Brunnen",
category: "drinking_water",
tags: { amenity: "drinking_water", name: "Test Brunnen" },
},
],

View file

@ -108,17 +108,18 @@ test.describe("Planner routing", () => {
test("nearby POI snap moves waypoint and prepends note prefix", async ({ page, request }) => {
const url = await createSession(request);
await page.route("**/api/overpass", async (route) => {
await page.route("**/api/pois*", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
elements: [
pois: [
{
type: "node",
id: 99001,
lat: 52.521,
lon: 13.406,
name: "Stadtbrunnen",
category: "drinking_water",
tags: { amenity: "drinking_water", name: "Stadtbrunnen" },
},
],