Fold completed deltas into main specs (activity-feed, route-management, infrastructure, planner-session), add new background-jobs and demo-activity-bot capability specs, and move the three change dirs to openspec/changes/archive/. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
5.6 KiB
5.6 KiB
1. Schema
- 1.1 Add
synthetic boolean NOT NULL DEFAULT falsetojournal.routesinpackages/db/src/schema/journal.ts - 1.2 Add the same column to
journal.activities - 1.3 Verify existing CRUD helpers (
createRoute,updateRoute,createActivity,listRoutes,listActivities) compile and ignore the new column for default callers
2. Bruno bootstrap
- 2.1 Reserved username:
bruno. Sentinel email:bruno@<DOMAIN>. Display name:Bruno. Bio: a short whimsical line (finalise exact wording during apply — e.g. "Professional park inspector. Currently accepting tennis balls."). - 2.2 Add an
ensureDemoUser()helper inapps/journal/app/lib/demo-bot.server.tsthat inserts thebrunorow only if missing (idempotent viaON CONFLICT DO NOTHINGonusername), withterms_accepted_atandterms_versionpopulated. - 2.3 Call
ensureDemoUser()from the worker startup sequence whenDEMO_BOT_ENABLED === "true". - 2.4 Confirm Bruno has no credentials row; the passkey and magic-link login paths both fail for his email (no mailbox exists for
bruno@<DOMAIN>).
3. Region + generation helpers
- 3.1 Add
loadRegion()that readsDEMO_BOT_REGIONJSON (withbbox: [w,s,e,n]) and falls back to the inner-Berlin default[13.25, 52.45, 13.55, 52.60] - 3.2 Add
pickEndpoints(bbox)— random start + end, 2–12 km crow distance apart (dog-walk scale) - 3.3 Profile is fixed to
trekking(Bruno doesn't ride bikes); nopickProfile()needed for v1 - 3.4 Add
templateName(startedAt)+templateDescription()— EN + DE Bruno-voiced copy, mixing serious-audit flavour ("Grunewald north-loop patrol") and comic ("Bruno found three sticks today 🐕"); 10–15 entries in each pool, picked deterministically from the day + started-at to avoid same-day repeats
4. BRouter client for the bot
- 4.1 Reuse the existing
computeRoutehelper where possible; otherwise add a minimalbrouter-client.server.tsthat POSTs toprocess.env.BROUTER_URLand returns parsed GPX + stats - 4.2 On no-route / 5xx / timeout, return a typed "no route" error — the job handles it, does not throw out of the worker
5. Generation job
- 5.1 Register a pg-boss recurring job
demo-bot:generateto fire every 90 minutes, singleton mode - 5.2 Handler: skip immediately if
DEMO_BOT_ENABLED !== "true" - 5.3 Decide-to-walk gate: compute local hour; if outside 07:00–21:00 → skip. Otherwise roll a Bernoulli with p=0.12 and skip if it doesn't fire. Expected output: ~2–3 walks/day
- 5.4 Hard cap:
SELECT count(*) FROM routes WHERE synthetic AND created_at > now() - interval '14 days'; skip if >= 40 - 5.5 Pick region + endpoints (profile is fixed to
trekking); call BRouter; on failure log and return - 5.6 Insert route with
visibility='public',synthetic=true, owner = Bruno; insert linked activity with derivedstarted_at= now,duration= distance ÷ ~4.5 km/h ± jitter, stats, and the same GPX
6. Initial backfill
- 6.1 At the top of the generation handler, check if
count(synthetic routes) == 0; if so, loop up to 5 times running the full generation body sequentially (each call independent — if one fails, keep going) - 6.2 After the backfill path, fall through to the normal single-item generation
7. Retention job
- 7.1 Register a pg-boss recurring job
demo-bot:prunewith a cron like15 3 * * *(daily at 03:15 UTC), singleton - 7.2 Handler: skip if
DEMO_BOT_ENABLED !== "true" - 7.3 Read
DEMO_BOT_RETENTION_DAYSwith default 14 - 7.4
DELETE FROM journal.activities WHERE synthetic AND created_at < now() - interval ?; same for routes - 7.5 Log the count removed for observability
8. Ops + env surface
- 8.1 Document
DEMO_BOT_ENABLED,DEMO_BOT_RETENTION_DAYS,DEMO_BOT_REGIONininfrastructure/.env.example+ pass-through indocker-compose.yml(commented; not enabled in any environment file) - 8.2 Set
DEMO_BOT_ENABLED=trueonly in prod (runtime flag — not set anywhere in-repo) - 8.3 Expose counts via Prometheus gauges
demo_bot_synthetic_routes_totalanddemo_bot_synthetic_activities_total; refreshed from the job handlers so scrapes see live values
9. UX tweaks
- 9.1 "🐕 demo account" badge on
/users/brunoso honest readers can tell at a glance; gate onuser.username === 'bruno' - 9.2 i18n:
demo.badgekey in EN + DE (e.g. "Demo account" / "Demo-Konto")
10. Tests
- 10.1 Unit:
ensureDemoUseris idempotent; second call is a no-op and does not duplicate (indemo-bot.integration.test.ts, runs underDEMO_BOT_INTEGRATION=1) - 10.2 Unit:
pickEndpointsstays within bbox; distance bands match profile - 10.3 Unit:
templateName/templateDescriptionreturn non-empty strings in both locales - 10.4 Integration (tests DB): the generation handler inserts one route + one activity with
synthetic=true, visibility='public'when enabled, and inserts nothing when disabled - 10.5 Integration: the prune handler deletes only
synthetic=truerows past the window; real rows are untouched - 10.6 E2E:
/users/brunorenders the demo badge and lists public synthetic content to anonymous visitors (covers the user-visible surface)
11. Rollout
- 11.1 Merge schema + code; deploy — the bot stays disabled everywhere because no env flag is set
- 11.2 Flip
DEMO_BOT_ENABLED=trueon prod via the SOPS env file + redeploy - 11.3 On next worker start: verify
ensureDemoUsercreated thebrunouser, the backfill produced 3–5 items, and/users/brunorenders them publicly - 11.4 After 24 h: verify cadence looks right and the prune job ran without deleting anything yet