trails/apps/journal/app/jobs/demo-bot-generate.ts
Ullrich Schäfer ba4c4e1b4f
Apply demo-activity-bot: Bruno, the synthetic public-walk generator
Adds a background demo user (`bruno`) plus two pg-boss jobs that generate
public trekking routes and activities in inner Berlin and prune them
after 14 days. Disabled by default everywhere; flip `DEMO_BOT_ENABLED`
only in prod.

- Schema: `synthetic` boolean on routes + activities
- `ensureDemoUser()` idempotent insert + badge on /users/bruno
- Generation gate: 07-21 Berlin local, p=0.12, 40-route cap in 14d
- Initial 4-walk backfill on first enable; retention via daily prune
- Prometheus gauges `demo_bot_synthetic_routes_total` / `_activities_total`
- Unit + DB-gated integration + E2E tests

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:14:58 +02:00

60 lines
1.9 KiB
TypeScript

import type { JobDefinition } from "@trails-cool/jobs";
import {
DEMO_BACKFILL_TARGET,
DEMO_DAILY_CAP,
countSyntheticRoutesRecent,
countSyntheticRoutesTotal,
ensureDemoUser,
generateOneWalk,
isDemoBotEnabled,
refreshDemoBotGauges,
shouldWalkNow,
} from "../lib/demo-bot.server.ts";
import { logger } from "../lib/logger.server.ts";
/**
* Generate job. Fires every 90 minutes via pg-boss cron. Handler steps:
*
* 1. Bail if `DEMO_BOT_ENABLED` is not "true".
* 2. On first run (no synthetic rows yet) produce a small backfill so the
* demo user's profile has content immediately.
* 3. Otherwise apply the decide-to-walk gate (local hour + p=0.12) and
* daily cap; on pass, insert one route+activity via `generateOneWalk`.
*/
export const demoBotGenerateJob: JobDefinition = {
name: "demo-bot:generate",
cron: "*/90 * * * *",
retryLimit: 1,
expireInSeconds: 120,
async handler() {
if (!isDemoBotEnabled()) return { skipped: "disabled" };
const ownerId = await ensureDemoUser();
const total = await countSyntheticRoutesTotal();
if (total === 0) {
let inserted = 0;
for (let i = 0; i < DEMO_BACKFILL_TARGET; i++) {
const id = await generateOneWalk(ownerId);
if (id) inserted++;
}
logger.info({ inserted }, "demo-bot backfill complete");
await refreshDemoBotGauges();
return { mode: "backfill", inserted };
}
const recent = await countSyntheticRoutesRecent(14);
if (recent >= DEMO_DAILY_CAP) {
logger.info({ recent, cap: DEMO_DAILY_CAP }, "demo-bot cap hit, skipping");
return { skipped: "cap", recent };
}
const now = new Date();
if (!shouldWalkNow(now)) return { skipped: "gate" };
const id = await generateOneWalk(ownerId, { now });
logger.info({ inserted: id ? 1 : 0, routeId: id }, "demo-bot walk");
await refreshDemoBotGauges();
return { mode: "single", routeId: id };
},
};