Files
TREK/server/src/nest/place-shadow/place-shadow.controller.ts
T
Maurice b3d69aa760 feat(places): record which search result was picked, on this instance only
The corpus behind "is our own index good enough to replace Nominatim".
When somebody searches and then picks a result, the client posts the query,
the rank and the pick. Nothing else: no user, no trip, no session, and
coordinates only to three decimals.

Deliberately decoupled from the search endpoint. Correlating a response
with a later pick would mean handing out a ticket on POST /api/maps/search
and threading it through all fifteen call sites; the client already holds
everything the row needs at the moment of the pick.

Evaluation happens offline afterwards: export the corpus, run it against a
candidate index, count how often the pick came back in the top five. That
is why collection can start before such an index exists, which is the whole
point, because the corpus needs weeks of real use and the index needs a
server, and those two waits should not be serial.

Off unless an admin switches it on, and it reads === 'true' rather than
!== 'false'. The three older places switches fail open because a migration
backfilled a row for installs already using them; nothing uses this one, so
an instance that upgrades and never visits the admin panel logs nothing.

Reading is admin only, retention is 180 days, and the switch state is on the
unauthenticated app-config endpoint on purpose: whether an instance records
what its users search for is something a visitor may know before logging in.
2026-09-07 14:05:33 +02:00

55 lines
1.9 KiB
TypeScript

import { Body, Controller, Delete, Get, HttpCode, Post, Query, UseGuards } from '@nestjs/common';
import type {
PlaceShadowExportResult,
PlaceShadowPickResult,
PlaceShadowSummaryResult,
} from '@trek/shared';
import { AdminGuard } from '../auth/admin.guard';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { PlaceShadowPickDto } from './place-shadow.dto';
import { PlaceShadowService } from './place-shadow.service';
/**
* /api/place-shadow — the local corpus of "what was searched, what was picked".
*
* Writing is open to any signed-in user because any signed-in user is the one
* doing the picking; reading is admin only, because the corpus is everybody's
* searches and the person entitled to see that is the person running the
* instance. Nothing here leaves the instance on its own.
*/
@Controller('api/place-shadow')
@UseGuards(JwtAuthGuard)
export class PlaceShadowController {
constructor(private readonly shadow: PlaceShadowService) {}
/**
* 200 with `{ recorded: false }` when the log is off, rather than a 403.
* The client posts this and moves on; a rejection would be a permanent error
* in the console of every install that never switched the log on.
*/
@Post('pick')
@HttpCode(200)
pick(@Body() body: PlaceShadowPickDto): PlaceShadowPickResult {
return { recorded: this.shadow.record(body) };
}
@Get('summary')
@UseGuards(JwtAuthGuard, AdminGuard)
summary(): PlaceShadowSummaryResult {
return this.shadow.summary();
}
@Get('export')
@UseGuards(JwtAuthGuard, AdminGuard)
export(@Query('after') after?: string): PlaceShadowExportResult {
const parsed = Number(after);
return this.shadow.export(Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined);
}
@Delete()
@UseGuards(JwtAuthGuard, AdminGuard)
clear(): { removed: number } {
return { removed: this.shadow.clear() };
}
}