A post-onboarding, pre-chat React view in the Astral Hermes web app that captures
the operator's birth date, time, and location once, then writes a relationship: self
profile into the tenant's KB and seeds a one-line marker into the tenant's
hermes-home/memories/USER.md. After this gate, every future Hermes session can
silently answer "what does my chart say…" because the self-profile already exists.
The hand-off between marketing/onboarding and the first chat used to drop the user
into /chat with an empty KB. Their first turn was always "my birth is Y/M/D, X:XX,
City" — friction. This gate collapses that turn into a one-shot guided form so the
chat can open with context.
It is intentionally skippable: the skip-flag is stored as
localStorage['astral.birth-data.skipped'] = '1' and /chat does not (currently)
force-redirect back. The gate itself also checks /api/me/self-profile on mount and
short-circuits to /chat if the self-profile already exists, so reloading is safe.
Astral Hermes has no npm dep for spinners — we load Magi's Mobiscroll bundle as a
static asset and call it through window.mobiscroll. This keeps package.json light
and matches Magi's runtime exactly.
web/public/mobiscroll/{js,css}/mobiscroll.javascript.min.* (copied verbatim
from magi-vps/mobiscroll/).web/index.html gets a <link rel="stylesheet"> and a <script defer> tag for
those files (served from /mobiscroll/...).web/src/components/BirthDataGate.jsx uses a waitForMobiscroll() polling helper,
then in a useEffect calls window.mobiscroll.datepicker(node, { controls: ['date'],
theme: 'ios', themeVariant: 'dark', display: 'center', touchUi: true, ... }) —
exactly the same config block Magi's registerModal.js uses for #dateInputAuth /
#timeInputAuth./api/places server-side proxy pattern
Magi already ships (no Google browser-key UI, no leaked key).window.SpeechRecognition || window.webkitSpeechRecognition. On
result, the transcript is written into the location input — the user reviews
before submitting. Voice button toggles 🎤 ↔ 🛑.Both routes use the existing requireChatTenantAccess middleware and the existing
ssh() + shellQuote() helpers — no new transport, no new auth.
GET /api/me/self-profile?tenantId=<id> — SSHes into the worker host, opens the
tenant's ${ASTRAL_ROOT}/tenants/<id>/hermes-home/, resolves ASTRAL_KB_ROOT from
the tenant's .env (fallback: <home>/astral-kb-prototype/knowledge/), scans
entities/people/*.md for a frontmatter with relationship: self, and returns
{ ok, profile: { slug, name, path } | null }.
POST /api/me/birth-data — { tenantId, name, date (YYYY-MM-DD), time (HH:MM),
location }. Calls
https://transit-list-demo.apps.poofc.com/api/time/resolve to enrich with
{ utc, timezone, latitude, longitude }, then SSHes one python heredoc that
(a) prefers the tenant's bundled astral-tenant-kb/scripts/kb.py save_person …
--relationship self, (b) falls back to writing the frontmatter markdown directly
if no kb.py is found, and (c) appends/updates a single
Self profile: entities/people/<slug>.md (relationship: self). Birth: … line in
memories/USER.md (capped at 200 chars).
web/src/main.jsx — BirthDataGate is imported at the top; the router branch
switches /birth-data to <BirthDataGate /> before the /chat branch.ReadyStep (final onboarding wizard step) now redirects to /birth-data?tenant=…
instead of /chat?tenant=….ReadyStep inside
main.jsx flips the redirect target.# Mobiscroll bundle landed
ls web/public/mobiscroll/js/mobiscroll.javascript.min.js
ls web/public/mobiscroll/css/mobiscroll.javascript.min.css
# index.html wires it
grep mobiscroll web/index.html
# Component exists
wc -l web/src/components/BirthDataGate.jsx
# Routes are registered
grep -n "/api/me/" web/server.mjs
# Server still imports cleanly
cd web && node -e "import('./server.mjs').then(()=>console.log('OK'))"
Manual test: log in, hit /onboarding, complete the wizard. On ReadyStep you should
auto-redirect to /birth-data?tenant=<slug>. Fill in name + spinner date + spinner
time + location (try the 🎤 button on Chrome/Safari). Submit → land on /chat. SSH
to the worker and inspect
/srv/astral/tenants/<slug>/hermes-home/astral-kb-prototype/knowledge/entities/people/<slug>.md
and …/hermes-home/memories/USER.md.
/api/places on the
Astral web server, which is the same pattern Magi uses. If /api/places isn't
implemented yet on astral-hermes-platform, autocomplete simply returns no
suggestions; the field still works as a plain text input. (TODO: port the
/api/places Express handler from Magi if not already present; key lives in the
server .env as GOOGLE_PLACES_API_KEY.)redact(err.message). If
the tenant container is being provisioned at the same time, the user may see a
transient failure — they can hit "Skip for now" and add it later.Self profile: entities/people/… line in place rather than appending dupes. The
KB write uses kb.py save_person which is upsert-by-slug./account/tenant/<id>/birth-data settings page can reuse <BirthDataGate />
unchanged — the component already short-circuits when a profile exists, but the
same form can be wrapped in an "edit existing" mode by reading
/api/me/self-profile and pre-filling the form instead of redirecting.