route-surface-breakdown (Phase 2): async Overpass backfill + SSE
Covers routes/activities that enter the journal without BRouter waytags (imports, uploads, pre-existing rows): - overpass-ways.server.ts: server-side Overpass client (way[highway] + geom in a bbox, configurable OVERPASS_URLS, timeout + oversized-bbox guard). - surface-match.server.ts: pure nearest-way map-matcher → per-segment surface/highway (unmatched → unknown). Unit-tested. - surface-backfill pg-boss job: load geom → skip if breakdown exists → Overpass → match → computeSurfaceBreakdown → store → emit `surface_breakdown` SSE to the owner. Idempotent, retry-safe, best-effort; registered in server.ts. - Enqueued from createActivity (imports/uploads) + owner-on-open in the route & activity detail loaders (non-Planner routes, old rows), deduped via singletonKey. - useSurfaceBackfillUpdates: detail pages subscribe to /api/events and revalidate() when their row's backfill lands (live bars, no reload). - Privacy manifest updated (DE + EN) for the Overpass bbox lookup. Tests: matchSurfaces unit; surface-backfill job (mocked Overpass/db/events: store + emit, skip-if-present, skip-if-no-ways). Verified the real Overpass→match→breakdown pipeline against a live Berlin bbox (509 ways → plausible asphalt/paving_stones/footway mix). typecheck + lint + unit (journal 333) green. Note: the worker runs via server.ts (prod/staging), not `react-router dev`, so the live job + SSE exercise on a deployed instance. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
3a1c34317d
commit
279734c607
16 changed files with 466 additions and 19 deletions
38
apps/journal/app/hooks/useSurfaceBackfill.ts
Normal file
38
apps/journal/app/hooks/useSurfaceBackfill.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { useEffect } from "react";
|
||||
import { useRevalidator } from "react-router";
|
||||
|
||||
/**
|
||||
* Live-updates a route/activity detail page when its async surface backfill
|
||||
* completes. Subscribes to `/api/events` and, on a `surface_breakdown` event
|
||||
* matching this row, re-runs the loader (so the bars appear without a reload).
|
||||
*
|
||||
* Enable only while it's worth it — i.e. the viewer is the owner (the backfill
|
||||
* emits to the owner's user stream) and the breakdown isn't present yet. Once
|
||||
* the breakdown lands, the loader revalidates, `enabled` flips false, and the
|
||||
* connection is torn down.
|
||||
*/
|
||||
export function useSurfaceBackfillUpdates(
|
||||
kind: "route" | "activity",
|
||||
id: string,
|
||||
enabled: boolean,
|
||||
): void {
|
||||
const revalidator = useRevalidator();
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
if (typeof EventSource === "undefined") return;
|
||||
const es = new EventSource("/api/events");
|
||||
const onEvent = (e: MessageEvent) => {
|
||||
try {
|
||||
const parsed = JSON.parse(e.data) as { kind?: string; id?: string };
|
||||
if (parsed.kind === kind && parsed.id === id) revalidator.revalidate();
|
||||
} catch {
|
||||
// Malformed payload — ignore.
|
||||
}
|
||||
};
|
||||
es.addEventListener("surface_breakdown", onEvent as EventListener);
|
||||
return () => {
|
||||
es.removeEventListener("surface_breakdown", onEvent as EventListener);
|
||||
es.close();
|
||||
};
|
||||
}, [kind, id, enabled, revalidator]);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue