feat(maps): count how much routing an instance actually does

Every route is calculated in the browser against the public FOSSGIS hosts, so the
server never sees a routing request and nobody has a number: not how many a real trip
costs, not how big they are, not how much already goes to a self-hosted engine.
Deciding whether TREK could run its own router without that number is guessing at the
bill, and the counters have to be collecting before the question comes up.

Counters, not a log. A row is a day, a profile, a request kind and an engine kind with
totals on it: how many requests, how many waypoints, roughly how far, how many were
refused. No query, no coordinate, no route, no user, no trip. Nothing leaves the
instance: its own database holds them and its own admin reads them at
GET /api/route-usage/summary.

The client tallies in memory and flushes in batches — on a timer, past a threshold and
when the page is hidden — because a counter that cost a request of its own would
measure the load by adding to it. Routing all funnels through one fetch in
RouteCalculator, so that is the only place instrumented.

On by default and switchable off with route_usage_enabled, unlike the shadow log next
door: that one stores what people typed, this one stores four integers about a day.
This commit is contained in:
Maurice
2026-09-09 14:44:32 +02:00
committed by Maurice
parent 88129e4546
commit d30b916f9c
14 changed files with 830 additions and 6 deletions
+31
View File
@@ -4483,6 +4483,37 @@ function runMigrations(db: Database.Database): void {
)`);
db.exec('CREATE INDEX IF NOT EXISTS idx_collab_links_trip ON collab_links(trip_id)');
},
/**
* Route usage counters — how much routing this instance really does.
*
* Daily aggregates, not a log: one row per day, profile, surface and engine
* kind, carrying totals. No query, no coordinate, no route, no user, no trip.
* The question they answer is whether TREK could host a router itself, and
* that needs volume, not itineraries.
*
* The table is created regardless of the switch, like the shadow log above:
* the switch decides whether rows are written, and a schema that appears only
* when a feature is on is a schema that differs between installs.
*
* Appended LAST: the array is index-addressed against schema_version.
*/
() => {
db.exec(`
CREATE TABLE IF NOT EXISTS route_usage_daily (
day TEXT NOT NULL,
profile TEXT NOT NULL,
surface TEXT NOT NULL,
self_hosted INTEGER NOT NULL,
requests INTEGER NOT NULL DEFAULT 0,
waypoints INTEGER NOT NULL DEFAULT 0,
km REAL NOT NULL DEFAULT 0,
failed INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (day, profile, surface, self_hosted)
)
`);
// Retention deletes by day, and the summary reads the newest days first.
db.exec('CREATE INDEX IF NOT EXISTS idx_route_usage_day ON route_usage_daily(day)');
},
];
if (currentVersion < migrations.length) {