From df6074b0b5db11ca7810c4043277f70f07f2c5cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Sun, 19 Apr 2026 11:25:06 +0200 Subject: [PATCH 1/2] Fix demo-bot cron + cadence math MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `*/90 * * * *` is invalid cron (minute field is 0–59) and degrades to hourly, producing half the intended bot cadence. Switch to `0,30 * * * *` (every 30 min) and lower the Bernoulli gate from 0.12 to 0.09 so we still hit ~2–3 walks/day during the 07–21 Berlin window. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/journal/app/jobs/demo-bot-generate.ts | 6 +++--- apps/journal/app/lib/demo-bot.server.ts | 9 +++++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/apps/journal/app/jobs/demo-bot-generate.ts b/apps/journal/app/jobs/demo-bot-generate.ts index e27eb7a..9d6f67e 100644 --- a/apps/journal/app/jobs/demo-bot-generate.ts +++ b/apps/journal/app/jobs/demo-bot-generate.ts @@ -13,17 +13,17 @@ import { import { logger } from "../lib/logger.server.ts"; /** - * Generate job. Fires every 90 minutes via pg-boss cron. Handler steps: + * Generate job. Fires every 30 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 + * 3. Otherwise apply the decide-to-walk gate (local hour + p=0.09) and * daily cap; on pass, insert one route+activity via `generateOneWalk`. */ export const demoBotGenerateJob: JobDefinition = { name: "demo-bot:generate", - cron: "*/90 * * * *", + cron: "0,30 * * * *", retryLimit: 1, expireInSeconds: 120, async handler() { diff --git a/apps/journal/app/lib/demo-bot.server.ts b/apps/journal/app/lib/demo-bot.server.ts index 173dc3d..9b22296 100644 --- a/apps/journal/app/lib/demo-bot.server.ts +++ b/apps/journal/app/lib/demo-bot.server.ts @@ -640,13 +640,18 @@ export function berlinHour(now: Date): number { } /** - * The decide-to-walk gate: only within 07–21 local, Bernoulli p=0.12. + * The decide-to-walk gate: only within 07–21 local, Bernoulli p=0.09. * Factored out so tests can replace `Math.random` / `now`. + * + * Cadence math: the job ticks every 30 min, so the 14-hour daytime + * window (07:00–21:00 Berlin) covers 28 ticks/day. With p=0.09 the + * expected walks/day ≈ 28 * 0.09 ≈ 2.52, which lands in the target + * 2–3 walks/day band. */ export function shouldWalkNow(now: Date, rand: () => number = Math.random): boolean { const hour = berlinHour(now); if (hour < 7 || hour >= 21) return false; - return rand() < 0.12; + return rand() < 0.09; } export const DEMO_DAILY_CAP = 40; From 75257719d3598bc8c2742cbdf756ae2603ef072f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Sun, 19 Apr 2026 11:25:17 +0200 Subject: [PATCH 2/2] Add Bruno (demo-bot) Grafana dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nine panels at trails-demo-bot covering: - Current synthetic route/activity counts + time-since-last-walk - Gauge time series over the configured retention window - Walks per day + distance distribution - Loki log stream filtered to `demo-bot` messages - Recent walks table with name, distance, ascent Provisioned via the existing grafana/dashboards directory — no extra wiring needed. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../grafana/dashboards/demo-bot.json | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 infrastructure/grafana/dashboards/demo-bot.json diff --git a/infrastructure/grafana/dashboards/demo-bot.json b/infrastructure/grafana/dashboards/demo-bot.json new file mode 100644 index 0000000..5c28c04 --- /dev/null +++ b/infrastructure/grafana/dashboards/demo-bot.json @@ -0,0 +1,142 @@ +{ + "title": "Demo Bot (Bruno)", + "uid": "trails-demo-bot", + "timezone": "browser", + "refresh": "1m", + "time": { "from": "now-14d", "to": "now" }, + "annotations": { + "list": [ + { + "name": "Deploys", + "enable": true, + "datasource": { "type": "grafana", "uid": "-- Grafana --" }, + "iconColor": "rgba(0, 211, 255, 1)", + "target": { "limit": 100, "matchAny": false, "tags": ["deploy"], "type": "tags" } + } + ] + }, + "panels": [ + { + "title": "Synthetic Routes (total)", + "type": "stat", + "gridPos": { "h": 6, "w": 6, "x": 0, "y": 0 }, + "datasource": { "uid": "prometheus" }, + "targets": [ + { "expr": "demo_bot_synthetic_routes_total", "legendFormat": "routes" } + ], + "fieldConfig": { "defaults": { "color": { "mode": "fixed", "fixedColor": "green" } } } + }, + { + "title": "Synthetic Activities (total)", + "type": "stat", + "gridPos": { "h": 6, "w": 6, "x": 6, "y": 0 }, + "datasource": { "uid": "prometheus" }, + "targets": [ + { "expr": "demo_bot_synthetic_activities_total", "legendFormat": "activities" } + ], + "fieldConfig": { "defaults": { "color": { "mode": "fixed", "fixedColor": "orange" } } } + }, + { + "title": "Walks in last 24h", + "type": "stat", + "gridPos": { "h": 6, "w": 6, "x": 12, "y": 0 }, + "datasource": { "uid": "postgres" }, + "targets": [ + { + "rawSql": "SELECT count(*) FROM journal.routes WHERE synthetic AND created_at > now() - interval '24 hours'", + "format": "table" + } + ], + "fieldConfig": { "defaults": { "color": { "mode": "fixed", "fixedColor": "blue" } } } + }, + { + "title": "Last walk", + "type": "stat", + "gridPos": { "h": 6, "w": 6, "x": 18, "y": 0 }, + "datasource": { "uid": "postgres" }, + "targets": [ + { + "rawSql": "SELECT COALESCE(extract(epoch FROM (now() - max(created_at)))::int, -1) AS \"seconds ago\" FROM journal.routes WHERE synthetic", + "format": "table" + } + ], + "fieldConfig": { + "defaults": { + "unit": "s", + "color": { "mode": "thresholds" }, + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "orange", "value": 14400 }, + { "color": "red", "value": 86400 } + ] + } + } + } + }, + { + "title": "Synthetic content over time", + "type": "timeseries", + "gridPos": { "h": 10, "w": 12, "x": 0, "y": 6 }, + "datasource": { "uid": "prometheus" }, + "targets": [ + { "expr": "demo_bot_synthetic_routes_total", "legendFormat": "routes" }, + { "expr": "demo_bot_synthetic_activities_total", "legendFormat": "activities" } + ] + }, + { + "title": "Walks per day (last 14d)", + "type": "barchart", + "gridPos": { "h": 10, "w": 12, "x": 12, "y": 6 }, + "datasource": { "uid": "postgres" }, + "targets": [ + { + "rawSql": "SELECT date_trunc('day', created_at) AS time, count(*) AS \"walks\" FROM journal.routes WHERE synthetic AND created_at > now() - interval '14 days' GROUP BY 1 ORDER BY 1", + "format": "time_series" + } + ], + "fieldConfig": { "defaults": { "color": { "mode": "fixed", "fixedColor": "green" } } } + }, + { + "title": "Distance distribution (last 14d)", + "type": "histogram", + "gridPos": { "h": 10, "w": 12, "x": 0, "y": 16 }, + "datasource": { "uid": "postgres" }, + "targets": [ + { + "rawSql": "SELECT round(distance::numeric / 1000, 1) AS \"km\" FROM journal.routes WHERE synthetic AND created_at > now() - interval '14 days'", + "format": "table" + } + ] + }, + { + "title": "Demo-bot logs", + "type": "logs", + "gridPos": { "h": 10, "w": 12, "x": 12, "y": 16 }, + "datasource": { "uid": "loki" }, + "targets": [ + { + "expr": "{container=\"trails-cool-journal-1\"} |~ \"demo-bot\" | json | line_format \"{{.msg}} {{if .inserted}}inserted={{.inserted}}{{end}}{{if .err}} err={{.err}}{{end}}{{if .routeId}} routeId={{.routeId}}{{end}}\"" + } + ], + "options": { + "showTime": true, + "wrapLogMessage": true, + "dedupStrategy": "none" + } + }, + { + "title": "Recent walks", + "type": "table", + "gridPos": { "h": 10, "w": 24, "x": 0, "y": 26 }, + "datasource": { "uid": "postgres" }, + "targets": [ + { + "rawSql": "SELECT r.name, round(r.distance::numeric / 1000, 1) AS \"km\", round(r.elevation_gain::numeric) AS \"ascent (m)\", r.routing_profile, r.created_at FROM journal.routes r WHERE r.synthetic ORDER BY r.created_at DESC LIMIT 20", + "format": "table" + } + ] + } + ] +}