Commit graph

752 commits

Author SHA1 Message Date
Ullrich Schäfer
875665de66
Update pgboss column names for v12 (snake_case)
pg-boss v11 renamed every concatenated-lowercase timestamp column
(`createdon`, `completedon`) to snake_case (`created_on`,
`completed_on`). Our service-health dashboard and failed-job alert
still referenced the v10 names, so after the v12 upgrade those
queries error out silently.

Fix the three affected queries:
- alerts.yml: failed-job alert
- service-health.json: "Recent Failed Jobs" table
- service-health.json: "Job Queue — Completed/hour" time series

The `state` / `name` / `output` columns stay the same, and the `state`
enum still accepts `'failed'` / `'completed'` literals.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 16:49:43 +02:00
Ullrich Schäfer
9d22ae5510
Merge pull request #284 from trails-cool/feat/pg-boss-v12
Upgrade pg-boss from 10 to 12
2026-04-19 23:04:34 +02:00
Ullrich Schäfer
c2e8461f9c
Upgrade pg-boss from 10 to 12
Supersedes dependabot #264 — this one involves code changes that PR
couldn't make.

## Breaking changes in our usage surface

### v12: default export removed → named `PgBoss` export

Affects every file that imports the SDK:
- `packages/jobs/src/boss.ts`: `import PgBoss` → `import { PgBoss }`
- `packages/jobs/src/worker.ts`: same for the type import
- `packages/jobs/src/types.ts`: `PgBoss.Job<T>` → `Job<T>` (types.ts
  re-exports `Job` at the package root in v12)

### v11: queue names restricted to `[A-Za-z0-9_.-]`

Colon `:` is no longer allowed. Renamed the two journal queues that
had it:
- `demo-bot:generate` → `demo-bot-generate`
- `demo-bot:prune`    → `demo-bot-prune`

The planner's `expire-sessions` was already valid.

### v12: minimum Node 22.12

Not a code change for us — journal + planner Dockerfiles are on
`node:25-slim`, CI runners on 24.

## Regression guard

Added `assertValidJobName()` in `@trails-cool/jobs`, called by
`startWorker()` before any side effects. Pg-boss v11+ silently accepts
an invalid name then rejects the underlying SQL call later — we fail
loudly at boot instead. Unit tests cover the character-class rule
and exercise the exact old-name (`demo-bot:generate`) as a
regression fence.

## Prod rollout note

Pg-boss v11 dropped the auto-migration path from v10. On deploy, the
live `pgboss` schema from v10 won't migrate cleanly. Simplest path:
`DROP SCHEMA pgboss CASCADE` before the first v12 worker starts —
our jobs are all cron-scheduled and will re-register themselves on
boot, so there's nothing durable to preserve in the queue.

## Verified

- `pnpm typecheck` / `pnpm lint` / `pnpm test` — all clean
- `pnpm exec playwright test --workers=2` — 50/50 passed

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 23:01:11 +02:00
Ullrich Schäfer
5b48ba388b
Merge pull request #283 from trails-cool/feat/e2e-block-unmocked-external
Block unmocked external requests in E2E tests
2026-04-19 22:45:41 +02:00
Ullrich Schäfer
f94020a93a
Block unmocked external requests in E2E tests
Surface net for the class of bug we hit in #282 — a POI test silently
relied on a live Overpass server for months because its `page.route`
mock was targeting a URL the browser no longer hit directly. CI stayed
green on coincidence (real OSM data happened to satisfy the "any
marker renders" assertion) until upstream behaviour drifted.

Add a shared Playwright fixture (e2e/fixtures/test.ts) that installs a
catch-all `page.route("**", ...)` before each test. Requests to
localhost + known tile CDNs pass through via `route.continue()`;
anything else is aborted and accumulated. At test teardown, a non-empty
blocked list throws, failing the test with the offending URLs and a
pointer to the fixture.

All seven e2e *.test.ts files updated to import test/expect (+ types)
from ./fixtures/test.

Full suite passes with this fixture in place (50/50 at --workers=2;
catch-all never fires, meaning no test currently relies on an
unallowlisted external service).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 22:42:29 +02:00
Ullrich Schäfer
72fb78dbb2
Merge pull request #282 from trails-cool/fix/e2e-poi-proxy-mock
Fix POI E2E test: mock the planner proxy, not upstream Overpass
2026-04-19 21:51:11 +02:00
Ullrich Schäfer
9c9d53d3bd
Fix POI E2E test: mock the planner proxy, not upstream Overpass
The test was added in 0ff04aa (Apr 11), back when the browser called
overpass-api.de directly. When a4df5a4 (Apr 18) moved POI queries
behind the planner's `/api/overpass` proxy, the test's
`page.route("**/api/interpreter", ...)` mock became dead code —
Playwright can only intercept browser traffic, and the browser now
only sees `/api/overpass`. The real upstream got hit and didn't
return a drinking_water node at exactly 52.52, 13.405, so zero
markers rendered and the test failed.

Mock `/api/overpass` instead. Response body shape is unchanged
(the proxy is transparent).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 21:48:21 +02:00
Ullrich Schäfer
88c83e359b
Merge pull request #280 from trails-cool/dependabot/npm_and_yarn/production-42e4c8cc57
Bump the production group across 1 directory with 10 updates
2026-04-19 21:46:00 +02:00
dependabot[bot]
fef9b5f3f6
Bump the production group with 10 updates
Bumps the production group with 10 updates:

| Package | From | To |
| --- | --- | --- |
| [eslint](https://github.com/eslint/eslint) | `10.2.0` | `10.2.1` |
| [i18next](https://github.com/i18next/i18next) | `26.0.4` | `26.0.6` |
| [prettier](https://github.com/prettier/prettier) | `3.8.2` | `3.8.3` |
| [react-i18next](https://github.com/i18next/react-i18next) | `17.0.3` | `17.0.4` |
| [isbot](https://github.com/omrilotan/isbot) | `5.1.38` | `5.1.39` |
| [react-native-safe-area-context](https://github.com/AppAndFlow/react-native-safe-area-context) | `5.6.2` | `5.7.0` |
| [react-native-screens](https://github.com/software-mansion/react-native-screens) | `4.23.0` | `4.24.0` |
| [@codemirror/view](https://github.com/codemirror/view) | `6.41.0` | `6.41.1` |
| [@sentry/node](https://github.com/getsentry/sentry-javascript) | `10.48.0` | `10.49.0` |
| [@sentry/react](https://github.com/getsentry/sentry-javascript) | `10.37.0` | `10.49.0` |

Updates `eslint` from 10.2.0 to 10.2.1
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.2.0...v10.2.1)

Updates `i18next` from 26.0.4 to 26.0.6
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v26.0.4...v26.0.6)

Updates `prettier` from 3.8.2 to 3.8.3
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.8.2...3.8.3)

Updates `react-i18next` from 17.0.3 to 17.0.4
- [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/react-i18next/compare/v17.0.3...v17.0.4)

Updates `isbot` from 5.1.38 to 5.1.39
- [Changelog](https://github.com/omrilotan/isbot/blob/main/CHANGELOG.md)
- [Commits](https://github.com/omrilotan/isbot/compare/v5.1.38...v5.1.39)

Updates `react-native-safe-area-context` from 5.6.2 to 5.7.0
- [Release notes](https://github.com/AppAndFlow/react-native-safe-area-context/releases)
- [Commits](https://github.com/AppAndFlow/react-native-safe-area-context/compare/v5.6.2...v5.7.0)

Updates `react-native-screens` from 4.23.0 to 4.24.0
- [Release notes](https://github.com/software-mansion/react-native-screens/releases)
- [Commits](https://github.com/software-mansion/react-native-screens/compare/4.23.0...4.24.0)

Updates `@codemirror/view` from 6.41.0 to 6.41.1
- [Changelog](https://github.com/codemirror/view/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codemirror/view/commits)

Updates `@sentry/node` from 10.48.0 to 10.49.0
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/10.48.0...10.49.0)

Updates `@sentry/react` from 10.37.0 to 10.49.0
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/10.37.0...10.49.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.2.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: production
- dependency-name: i18next
  dependency-version: 26.0.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: production
- dependency-name: prettier
  dependency-version: 3.8.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: production
- dependency-name: react-i18next
  dependency-version: 17.0.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: production
- dependency-name: isbot
  dependency-version: 5.1.39
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production
- dependency-name: react-native-safe-area-context
  dependency-version: 5.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production
- dependency-name: react-native-screens
  dependency-version: 4.24.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production
- dependency-name: "@codemirror/view"
  dependency-version: 6.41.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production
- dependency-name: "@sentry/node"
  dependency-version: 10.49.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production
- dependency-name: "@sentry/react"
  dependency-version: 10.49.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-19 21:40:33 +02:00
Ullrich Schäfer
e5a0215000
Merge pull request #267 from trails-cool/dependabot/npm_and_yarn/sentry/react-native-8.8.0
Bump @sentry/react-native from 7.11.0 to 8.8.0
2026-04-19 21:37:54 +02:00
Ullrich Schäfer
ec93d37470
Prefix dedupe commit message + set GH_TOKEN for push step
- Commit subject prefixed with "[github-actions]" so the audit trail
  is obvious at a glance in `git log`.
- GH_TOKEN exposed as an env var on the commit step so the PAT is
  also available to any `gh` invocations the step might grow, and the
  token plumbing is visible at the site where the push happens — not
  only up at the checkout step.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 21:06:38 +02:00
Ullrich Schäfer
da56eb2c19
Stop over-claiming the persist-credentials mechanism
The previous comment claimed `git push` reads the token via a git
credential helper wired into $RUNNER_TEMP. I don't actually know that's
how it works end to end — only that checkout stashes the token there
and that subsequent git ops in the same workspace end up authenticating
as the PAT. Describe just the observable contract, not an unverified
internal path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 20:58:26 +02:00
Ullrich Schäfer
5d9ffae38a
Correct comment about where checkout persists the PAT
Current actions/checkout stores the token in $RUNNER_TEMP and wires a
git credential helper, rather than writing the token into .git/config's
http.extraheader. The mechanism that makes `git push` pick up the PAT
is unchanged at the caller's level; the comment was just wrong about
where the secret lives on disk.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 20:56:54 +02:00
Ullrich Schäfer
32ea0f66f5
Make persist-credentials explicit in dedupe checkout
`actions/checkout` has `persist-credentials: true` as its default, but
that default is the load-bearing part of how the later `git push`
authenticates — it's what writes the token into .git/config's
http.extraheader. Making it explicit so a reader of the YAML can see
the mechanism without having to know the action's defaults.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 20:55:47 +02:00
Ullrich Schäfer
e505cfa2b9
Require DEPENDABOT_DEDUPE_TOKEN for the dedupe workflow
Previously the workflow fell back to GITHUB_TOKEN when the PAT wasn't
set — which was silently broken: the push succeeds but GitHub's
anti-workflow-loop safeguard means no new CI runs on the dedupe commit.
Reviewers see stale green CI and the dedupe itself isn't tested until
the next dependabot rebase.

Make the PAT a hard requirement: preflight check fails with a clear
message when the secret is missing, and the checkout step uses it
(which is what persists auth for the later `git push`). Intent is now
obvious from the YAML.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 20:53:57 +02:00
dependabot[bot]
e94069232b
Bump @sentry/react-native from 7.11.0 to 8.8.0
Bumps [@sentry/react-native](https://github.com/getsentry/sentry-react-native) from 7.11.0 to 8.8.0.
- [Release notes](https://github.com/getsentry/sentry-react-native/releases)
- [Changelog](https://github.com/getsentry/sentry-react-native/blob/main/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-react-native/compare/7.11.0...8.8.0)

---
updated-dependencies:
- dependency-name: "@sentry/react-native"
  dependency-version: 8.8.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-19 11:34:23 +00:00
Ullrich Schäfer
6ee737694a
Merge pull request #279 from trails-cool/feat/maplibre-react-native-v11 2026-04-19 13:32:18 +02:00
Ullrich Schäfer
7e302b1fbe
Upgrade @maplibre/maplibre-react-native from 10 to 11
v11 is a breaking API rework (renamed components, restructured props,
event payloads on nativeEvent) and requires React Native's new
architecture. Doing it as a manual migration instead of taking
dependabot's #268 because the renames need code changes.

Changes in apps/mobile:

- RouteMap.tsx — switch to named imports; rename MapView→Map,
  ShapeSource→GeoJSONSource (with shape→data), LineLayer→Layer
  (with type="line" and paint instead of style), MarkerView→
  ViewAnnotation (with coordinate→lngLat); Camera uses
  initialViewState with bounds as [w,s,e,n] and a separate padding
  object; onLongPress reads lngLat from event.nativeEvent.

- app.config.ts — set `newArchEnabled: true`. MapLibre RN v11
  supports only the new architecture, so Fabric/TurboModules must
  be on for the Expo prebuild. Cast the config type since Expo SDK
  55's ExpoConfig typings don't yet include the field (the runtime
  does).

- package.json — `^10.4.2` → `^11.0.0`.

Supersedes dependabot #268; it'll auto-close once this merges.

Runtime verification requires an EAS dev build (native deps
changed + new arch flipped). JS-only surface typechecks + lints +
unit tests clean locally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 12:09:06 +02:00
Ullrich Schäfer
c3d9514f4d
Merge pull request #277 from trails-cool/chore/dependabot-dedupe-workflow
Auto-dedupe lockfile on dependabot PRs
2026-04-19 12:01:00 +02:00
Ullrich Schäfer
c39020a848
Merge pull request #278 from trails-cool/chore/gitignore-scheduled-tasks-lock
Ignore .claude/scheduled_tasks.lock
2026-04-19 12:00:40 +02:00
Ullrich Schäfer
760c0b4d4d
Ignore .claude/scheduled_tasks.lock
Created by the Claude Code scheduled-wakeup runtime when a dynamic
loop is active. Pure ephemeral state; no reason to track it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 12:00:02 +02:00
Ullrich Schäfer
f085b78fd7
Auto-dedupe lockfile on dependabot PRs
pnpm install after a dependabot bump keeps duplicate versions of
packages that happen to be pinned via overlapping semver ranges. For
singleton-ish libraries (i18next, react-i18next) that's a latent bug:
the server's module instance and the client's module instance each
hold their own state, and SSR output doesn't match CSR.

#272 ran into this with an i18next patch bump — the bumped version
stayed on some dep paths while other paths kept the old version. A
manual `pnpm dedupe` collapsed them and hydration worked again.

This workflow fires on every dependabot PR, runs `pnpm dedupe`, and
pushes the resulting lockfile update back to the PR branch so CI
runs against the deduped tree.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 11:59:33 +02:00
Ullrich Schäfer
be2baa455f
Merge pull request #276 from trails-cool/chore/zod-4-migration
Upgrade zod from 3 to 4
2026-04-19 11:48:30 +02:00
Ullrich Schäfer
edd2cabf12
DRY up ZodError → FieldError mapping
Five journal API route handlers had the same inline lambda:

    parsed.error.issues.map((i) => ({
      field: i.path.join("."),
      message: i.message,
    }))

Pull it into `zodIssuesToFieldErrors(error)` next to FieldError in
`@trails-cool/api/errors.ts` and re-export it so all five handlers
share the same implementation. The path-flattening rule now lives in
one place, and new routes get it for free.

Public error-response shape is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 11:45:09 +02:00
Ullrich Schäfer
079c3de90c
Modernise zod idioms to v4
Replace the deprecated v3 string-method forms with the top-level
validator helpers zod 4 prefers:

- z.string().url()      → z.url()      (3 sites)
- z.string().uuid()     → z.uuid()     (5 sites)
- z.string().datetime() → z.iso.datetime() (9 sites)

Also swap the ZodIssueCode.custom compat-shim reference in
demo-bot.server.ts's superRefine for the bare string literal "custom",
which is v4's idiomatic form. No runtime change.

All 147 tests still pass.

The `parsed.error.issues.map(i => ({field, message}))` pattern in the
five journal API route handlers stays as-is — the fields we read are
unchanged in v4, and zod's new error helpers (z.treeifyError,
z.prettifyError) produce different shapes that would change our
public API error contract.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 11:41:03 +02:00
Ullrich Schäfer
0eea6f2047
Upgrade zod from 3 to 4
Mechanical bump across the three packages that pull zod directly:
- packages/api (8 schema files, ~300 lines)
- apps/journal (persona schema in demo-bot.server.ts, 5 API route
  handlers that introspect parsed.error.issues)
- apps/mobile (declared dep; no active imports yet)

No code changes required. The legacy methods we use (.url(), .uuid(),
.datetime() on strings) still parse identically in v4 via the compat
shim, and z.ZodIssueCode.custom retains its runtime value. Error-issue
introspection (parsed.error.issues.map(i => ...)) keeps the same shape
at the fields we read (path, message, code).

All 147 tests across journal + api + packages pass against zod 4.3.6.

Follow-ups out of scope for this PR:
- Modernise to top-level validator helpers (z.url(), z.iso.datetime()).
  The legacy string methods are deprecated but not removed.
- Consider migrating error introspection to the richer v4 shape (.path
  is now branded, .input is new) if we ever want structured error
  surfaces.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 11:35:39 +02:00
Ullrich Schäfer
a3903d67ac
Merge pull request #274 from trails-cool/feat/bruno-dashboard
Add Bruno (demo-bot) Grafana dashboard
2026-04-19 11:26:14 +02:00
Ullrich Schäfer
75257719d3
Add Bruno (demo-bot) Grafana dashboard
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) <noreply@anthropic.com>
2026-04-19 11:25:17 +02:00
Ullrich Schäfer
df6074b0b5
Fix demo-bot cron + cadence math
`*/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) <noreply@anthropic.com>
2026-04-19 11:25:06 +02:00
Ullrich Schäfer
0ac4d9674d
Merge pull request #273 from trails-cool/feat/planner-compute-gpx-endpoint
Route journal BRouter calls through the planner
2026-04-19 11:16:50 +02:00
Ullrich Schäfer
42f4c98014
Route journal BRouter calls through the planner
The planner is the BRouter client in this architecture — route
compute, rate limiting, and the eventual move of BRouter off-box all
live there. The journal had two places that called BRouter directly
(demo-bot and /api/v1/routes/compute) and both were broken on prod
because the journal service has no BROUTER_URL.

Fix:
- Extend the planner's /api/route with an optional `format: "gpx"`
  that returns BRouter's raw GPX (for server-to-server callers that
  don't need the way-tag enriched GeoJSON).
- Point the journal demo-bot at `PLANNER_URL/api/route` with
  `format: "gpx"` instead of calling BRouter directly.
- Point /api/v1/routes/compute at `PLANNER_URL/api/route` instead of
  BRouter directly.

Journal no longer needs BROUTER_URL at all. When BRouter moves to a
dedicated host later, only the planner changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 11:11:10 +02:00
Ullrich Schäfer
017a098fca
Merge pull request #269 from trails-cool/chore/dependabot-ignore-rules
chore: dependabot ignore rules for mismatched @types/* + expo-pinned react-native
2026-04-19 11:01:04 +02:00
Ullrich Schäfer
debaf769d5
Tighten dependabot ignore rules
- Skip major bumps on @types/jest + @types/node so the types stay
  aligned with the runtime (jest 29 with @types/jest 29, not 30).
- Skip react-native itself since it's pinned by the Expo SDK and
  should only move when we bump Expo. Community react-native-*
  libraries stay on the normal weekly cadence.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:58:05 +02:00
Ullrich Schäfer
fe60c6eb0f
Merge pull request #266 from trails-cool/hotfix/docker-include-jobs-dir
hotfix: include app/jobs in journal Docker image
2026-04-19 10:56:12 +02:00
Ullrich Schäfer
e16bd6de99
Ship app/jobs dir in the journal image
server.ts imports from ./app/jobs/demo-bot-generate.ts and ./app/jobs/
demo-bot-prune.ts, but the Dockerfile only COPYs app/lib. On prod the
worker crashes on boot with ERR_MODULE_NOT_FOUND and neither the
generate nor the prune cron gets scheduled. Add the jobs dir to the
runtime layer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:53:22 +02:00
Ullrich Schäfer
e3d58d8187
Merge pull request #261 from trails-cool/hotfix/prom-client-double-register
hotfix: prom-client double-register on prod
2026-04-19 10:48:14 +02:00
Ullrich Schäfer
100187cb80
Fix prom-client double-registration on prod
/users/bruno (and any route that imports demo-bot.server.ts) was
returning 500 on prod after #258 because the route's module graph
loaded metrics.server.ts a second time, re-running collectDefaultMetrics
and `new client.Gauge(...)` — prom-client's global registry rejects
duplicate metric names, so the route module failed to load.

Guard every metric creation via `getSingleMetric` so a second module
load reuses the existing gauges/histograms.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:44:57 +02:00
Ullrich Schäfer
fe62b34eb3
Merge pull request #259 from trails-cool/chore/enable-demo-bot-prod
chore: enable demo-activity-bot on prod
2026-04-19 10:41:57 +02:00
Ullrich Schäfer
1b9ce4b234
Merge branch 'main' into chore/enable-demo-bot-prod 2026-04-19 10:36:25 +02:00
Ullrich Schäfer
53bdffa117
Enable demo-activity-bot on prod
Sets DEMO_BOT_ENABLED=true plus DEMO_BOT_RETENTION_DAYS and
DEMO_BOT_REGION in the SOPS env file so the journal worker starts
the generate + prune jobs and bootstraps the Bruno demo user on
next deploy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:34:19 +02:00
Ullrich Schäfer
0163088b37
Merge pull request #258 from trails-cool/feat/configurable-demo-persona
Apply configurable-demo-persona: per-instance demo identity
2026-04-19 10:34:17 +02:00
Ullrich Schäfer
fc4485f6ef
Apply configurable-demo-persona: per-instance demo identity + voice
Adds DEMO_BOT_PERSONA env var (inline JSON or file:<path>) so
self-hosted instances can replace Bruno-in-Berlin with their own
demo account identity and content pools. No-op for trails.cool — the
built-in Bruno persona remains the default.

- DemoPersona type + Zod schema validation
- loadPersona() cached at boot; falls back to default on any failure
- ensureDemoUser throws DemoPersonaUsernameClashError when the
  persona username is already a real user; server declines to
  schedule demo jobs for that process
- isDemoUser flag moved from hardcoded "bruno" check to loader-supplied
  boolean computed from the running persona
- docs/demo-persona.md explains the schema + operator flow

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:30:32 +02:00
Ullrich Schäfer
f447852d70
Merge pull request #257 from trails-cool/feat/demo-activity-bot
Apply demo-activity-bot: Bruno the synthetic walker
2026-04-19 10:18:17 +02:00
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
Ullrich Schäfer
7304a9e904
Merge pull request #256 from trails-cool/feat/public-content-visibility
Apply public-content-visibility: visibility flag + public profile
2026-04-19 09:17:34 +02:00
Ullrich Schäfer
32661a0163
Add visibility disclosure to the binding German privacy text
The previous commit only added a bullet to the English-only Privacy
Manifest appendix. The formal, binding German sections (which the
policy explicitly declares authoritative) didn't mention visibility
at all — and the paired English summary under each section likewise
didn't.

Add a sentence under §2 "Erhobene Daten und Zwecke → Nutzerinhalte"
explaining the visibility setting, defaults, and world-visibility
consequences. Mirror in the section's English summary paragraph. Keep
the Privacy Manifest bullet as an additional plain-language mention.

Re-rendered docs/legal-archive/privacy-2026-04-20.md; both languages
present (verified).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 09:14:15 +02:00
Ullrich Schäfer
5905cdadea
Apply public-content-visibility: visibility flag + public profile
Implements the public-content-visibility OpenSpec change. Adds the
smallest social surface that lets us demo the product to logged-out
visitors without user signup.

Schema:
- `visibility text NOT NULL DEFAULT 'private'` on routes + activities.
- Shared Visibility type exported from the schema module.

Access:
- New canView(content, viewer, { asDirectLink }) helper in auth.server.ts
  centralises the rule: public → anyone; unlisted → anyone on direct
  link; private → owner only.
- routes.$id and activities.$id loaders return 404 (not 403) when
  canView rejects, so existence of private content isn't leaked.
- Detail pages emit Open Graph + Twitter Card meta on public/unlisted
  content only.

Editing:
- Visibility <select> on routes/:id/edit with owner-only access.
- Activity detail page gets a small visibility form + set-visibility
  action intent (no separate activity-edit page needed).
- EN + DE i18n under routes.visibility.* and activities.visibility.*.

Listings:
- Listing helpers listPublicRoutesForOwner / listPublicActivitiesForOwner
  for cross-user queries. Existing owner-scoped listRoutes/listActivities
  stay — owners see their own content regardless of visibility.

Public profile:
- /users/:username is now truly public. Renders the user's public
  routes + activities, 404s when no public content exists AND viewer
  isn't the owner (prevents account enumeration).
- Owner sees a short "this is your profile" note linking to settings.
- Open Graph meta (og:type=profile) for shareable preview.

Privacy manifest:
- Added a bullet noting public content is world-visible on profile
  and indexable by search engines.
- Bumped PRIVACY_LAST_UPDATED to 2026-04-20 + rendered legal-archive
  snapshot.

Tests:
- 13 unit tests for canView covering the full matrix.
- 6 E2E tests in e2e/public-content.test.ts covering:
  - Private route → 404 for logged-out visitor
  - Public route → reachable + OG tags present (og:title, og:type,
    og:site_name)
  - Owner still sees own private content
  - Profile 404 when no public content
  - Profile renders when at least one public route exists
  - Unlisted route reachable via direct URL but hidden from profile
- Test file runs serially (describe.configure mode=serial) to avoid
  WebAuthn virtual-authenticator races under Playwright's default
  parallel workers.
- New public-content Playwright project added to config.

Rollout safety: every existing row in prod keeps visibility='private'
by default — nothing becomes visible to outsiders until an owner
explicitly opts in.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 09:11:39 +02:00
Ullrich Schäfer
74dca52ecc
Merge pull request #255 from trails-cool/spec/social-mvp
Propose social MVP: public-content-visibility + demo-activity-bot
2026-04-19 08:50:58 +02:00
Ullrich Schäfer
dcbd703f47
Propose social MVP: public-content-visibility + demo-activity-bot
Two stacked OpenSpec change proposals for a demoable social layer,
with scope deliberately minimal ("enough to send a URL and have it
open to real-looking content without signup"):

1. public-content-visibility
   - Adds visibility enum {private, unlisted, public} on routes +
     activities, defaulting to private for every existing row.
   - Detail pages (/routes/:id, /activities/:id) become accessible to
     logged-out visitors when content is public or unlisted; private
     → 404 (not 403) to avoid existence leaks.
   - Broadens /users/:username into a public profile listing only
     public routes + activities; 404s when there's no public content
     to prevent account enumeration.
   - Open Graph / Twitter Card meta on public detail + profile pages.
   - Visibility selector in the owner's edit flow.
   - Out of scope: follow/follower, cross-user feed, reactions,
     federation.

2. demo-activity-bot  (depends on #1)
   - A single bot user, Bruno the trail dog, seeded on worker startup
     when DEMO_BOT_ENABLED=true. Reserved username, sentinel email,
     no credentials.
   - pg-boss recurring job fires every 90 min, decides-to-walk with
     p=0.12 during 07:00-21:00 local, yielding ~2-3 walks/day at
     organic times.
   - Each walk: random start + end within inner-Berlin bbox, trekking
     only (dogs don't ride bikes), 2-12 km crow. BRouter plans the
     route; the route GPX is also attached as the activity's trace.
   - Everything inserted with visibility=public, synthetic=true.
   - Daily prune deletes synthetic rows > DEMO_BOT_RETENTION_DAYS old
     (default 14). Hard cap of 40 items/14d protects against runaway
     growth.
   - Small "🐕 demo account" badge on /users/bruno for honesty.

Both changes are artifacts only — no code lands with this PR. Apply
in order after merge: public-content-visibility first, then
demo-activity-bot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 08:49:59 +02:00
Ullrich Schäfer
94ce350565
Merge pull request #254 from trails-cool/docs/park-mobile-specs
Park mobile-activity-recording + mobile-nearby-sync under docs/ideas
2026-04-19 08:24:18 +02:00