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>
4.3 KiB
4.3 KiB
1. Persona type + schema
- 1.1 Define
DemoPersonaTypeScript interface inapps/journal/app/lib/demo-bot.server.tswithusername,displayName,bio,locales: ("en"|"de")[],content: { names: { en?: string[]; de?: string[] }, descriptions: { en?: string[]; de?: string[] } } - 1.2 Add a Zod schema mirroring the interface: username
^[a-z0-9][a-z0-9_-]{1,30}$, displayName 1–200 chars, bio 0–200 chars, locales a non-empty subset of["en","de"], each content array 3–50 non-empty strings, ensure every listed locale has bothnamesanddescriptionspopulated - 1.3 Move the current hardcoded Bruno strings into a
DEFAULT_PERSONA: DemoPersonaexport — confirm it round-trips through the Zod schema before export
2. Loader
- 2.1 Add
loadPersona(): DemoPersonathat readsDEMO_BOT_PERSONA: if unset →DEFAULT_PERSONA; if it starts withfile:→ read the rest as an absolute path viafs.readFileSync; otherwise treat as inline JSON - 2.2 Parse with the Zod schema; on validation failure or file-read failure, log a warn with the first error message and return
DEFAULT_PERSONA - 2.3 Cache the result in a module-level
Object.freezed constant so every caller sees the same instance - 2.4 Remove the now-unused
DEMO_USERNAME,DEMO_DISPLAY_NAME,DEMO_BIO,NAME_POOL_EN,NAME_POOL_DE,DESCRIPTION_POOL_EN,DESCRIPTION_POOL_DEconstants — every reference must flow through the persona
3. Wire into bootstrap + generation
- 3.1
ensureDemoUser()takes the loaded persona and writesusername,displayName,bio, and sentinel email${persona.username}@<domain> - 3.2 Before insert,
SELECTthe row matching the username; if it exists and was not inserted by a prior demo-bot boot (heuristic: email does not match the sentinel pattern), throwDemoPersonaUsernameClashError; server startup catches this and declines to schedule the demo jobs for the process - 3.3
templateName(startedAt, locale)reads frompersona.content.names[locale]with the existing seeded indexing; same fortemplateDescription - 3.4 Locale selection in
generateOneWalkpicks randomly frompersona.localesviapickLocale()(dropping the hardcoded 50/50 EN/DE coin flip)
4. UX — demo badge via loader flag
- 4.1 In
users.$username.tsxloader, computeisDemoUserby comparinguser.usernameagainstloadPersona().username; includeisDemoUserin the loader return - 4.2 Replace the client-side
user.username === "bruno"check with the loader-supplied boolean - 4.3 Keep the
demo.badgei18n key exactly as-is so existing translations continue to work
5. Env + docs
- 5.1 Add
DEMO_BOT_PERSONApassthrough ininfrastructure/docker-compose.yml(journal service) - 5.2 Add a commented
DEMO_BOT_PERSONAexample toinfrastructure/.env.examplewith the inline JSON form AND thefile:/path/to/persona.jsonform - 5.3 Write
docs/demo-persona.md— a one-page guide covering: schema shape, inline vsfile:modes, required pool sizes, how the built-in default looks, and an example persona for a non-Berlin instance
6. Tests
- 6.1 Unit:
loadPersona()— unset env returns default; valid inline JSON parses to the persona; invalid JSON falls back + warns; valid JSON that violates the Zod schema falls back + warns - 6.2 Unit:
loadPersona()withfile:— reads a real file via a temp path; unreadable file falls back + warns - 6.3 Unit:
templateName/templateDescriptiondraw from the supplied persona's pool and never return entries outside it - 6.4 Integration (
DEMO_BOT_INTEGRATION=1):ensureDemoUserwith the default persona is idempotent; clash with a pre-existing real user throwsDemoPersonaUsernameClashError - 6.5 E2E: no regression —
/users/brunostill shows the 🐕 badge for the built-in Bruno persona (re-rane2e/demo-bot.test.tsunchanged, both pass)
7. Rollout
- 7.1 Merge + deploy — nothing changes because default persona equals current behaviour
- 7.2 Validate on prod:
/users/brunounchanged, synthetic content cadence unchanged, metrics unchanged - 7.3 (Optional demo) On a staging or second instance, ship a non-Bruno persona via
DEMO_BOT_PERSONA=file:...and confirm the demo user renders with the new identity + voice