Port the shared otm-login/otm-menu CSS+markup from
@otm/account-panel otm-login-core.ts 0.44.0 (verbatim, kept in sync
via header comments) into login.html and the sidebar account menu in
index.html/app.js. Accent is the studio's --plum, heading font via
the module's --otm-font-display variable. Behaviour is unchanged
(OTM-first / "Other ways to sign in" toggle, server-filled
__OTM_SSO_URL__, same reasons).
The account menu needs to know whether a session came in via OTM to
show the badge/"Manage OTM account" link, which the session didn't
track before — added a `via: "otm"` claim on the SSO callback's
session and exposed it from GET /api/me (additive; password and
operator-magic sessions are unaffected and read as not-linked).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014LCQmFvKshyAJsE8C3B5ET
Restyle login.html to the platform standard: brand (name + tagline)
above one centered card, "Sign in with OTM" as the full-width primary
button with a muted Face ID/Touch ID/passkey helper line under it,
and a small "Other ways to sign in" link that swaps in the
email/password form (which ends with its own small "Sign in with
OTM" link back). When OTM isn't wired (empty __OTM_SSO_URL__), the
password form shows directly, as before.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014LCQmFvKshyAJsE8C3B5ET
The server fills the OTM link into login.html from OTM_SSO_AUDIENCE (the
route itself has been mounted since 0.4.0); without the env the block stays
hidden and the password form is unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014LCQmFvKshyAJsE8C3B5ET
Three new doors, all wire-contract ports of the shared @otm/account-panel
factories (this backbone is plain Node — the Next factories can't mount):
- GET /api/auth/operator-magic — OTM 'Log in as admin'. HS256 verify against
OPERATOR_SHARED_SECRET (alg allowlist, constant-time, action-claim rejected
for flow separation), single-use jti via OperatorMagicConsumed (+ who/when
audit), 1-HOUR session with cookie Max-Age derived from the payload, 🔑
support-session banner in the app, every failure a 302 reason redirect.
- GET /api/auth/otm-sso — the OTM /account tile. Stricter sso-ticket verify
(exp mandatory, audience compared), email-then-role actor mapping, safeNext.
- 💡 suggestion jot chip — files a Gitea issue (shared attribution footer,
8k cap, 10s timeout); on any failure the jot is kept as a note instead.
Success also leaves a '#N — …' note so she has her own record.
- Footer version chip + one-tap self-update (HMAC-signed OTM proxy with
explicit field picking — foreign JSON can never reach the _setSession/_redirect
control keys), owner-gated, 10-min sessionStorage cache on the check.
Hardening that rode along: session key now derived from OPERATOR_SHARED_SECRET
(managed boot REFUSES the old derivable DATABASE_URL fallback), 1MB JSON body
cap, login.html prototype-lookup fix. NOTE: server.mjs previously contained a
literal NUL byte that made git treat it as binary — this commit re-encodes it
as an escape (behavior identical) and adds .gitattributes so source diffs can
never go blind again.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SmybqyQmZWfcqA1vMP4jbQ
Prototype stays untouched (Bonna's laptop daily driver until cutover).
app/ is the deployable bms-backbone: same routes/frontend, Prisma models
replacing node:sqlite, session-cookie login backed by a User table seeded
via the platform SEED_OWNER_* convention (bcryptjs), /api/health with
APP_VERSION, photos/files under DATA_DIR (/app/data uploads mount),
Dockerfile + entrypoint (prisma db push + owner seed + serve), and a
one-shot sqlite→postgres migration script for Bonna's existing data.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>