Pre-demo walkthrough fixes — completion notes
Date: 2026-08-25 · Commit: a3dc460 · Spec:
docs/superpowers/specs/2026-08-25-account-email-otp-and-recovery-design.md
Six findings from Efran's click-through of the public site three days before
the 28 Aug hands-on. All six are done; one of them is deployed to a host that
turned out not to be the one the staff URL serves — see Open item.
What shipped
| # | Finding | Fix | Verified |
|---|---|---|---|
| 1 | Blue focus ring on the composer textarea | Focus moved to the rounded wrapper; textarea gets a specified transparent outline | computed outline: solid 2px rgba(0,0,0,0) on staging |
| 2 | No confirm-password on register | "Ulangi kata sandi" field, register-only | dialog renders 5 fields |
| 3 | No email: OTP verification, no recovery | new @ahu/mail + OTP registration + reset on both surfaces, all behind a mail flag |
63 unit + 21 route tests; no-mail path live on staging |
| 4 | Mandarin mid-answer | prompt rule 7 + HanStreamFilter |
19 tests; sampled answer clean |
| 5 | No space above "Lanjutkan dengan" | margin-top: 20px on .suggest |
measured 20px gap on staging |
| 6 | PDF export garbled | hide collapsed <details> bodies before capture |
10-citation answer exports clean on one page (was two, overlapping) |
Test count: 399 (+53), pnpm check green.
Things that were not obvious
The focus ring was not outline: none missing — it was outline: none
losing. The reset has set input, textarea { outline: none } since day one
and Chrome honours it (verified: no ring). The report came from Zen, a
Firefox-family browser, where the UA outline-style: auto on form controls
survives none in some configurations. A specified outline replaces auto
outright, so the fix is outline: 2px solid transparent, not another none.
The CSS halo I first wrote was dead code. useTanyaMotion.ts (Efran's
uncommitted WIP) already animates a lift + drop shadow on composer focusin
via GSAP, which writes an inline style and beats any stylesheet rule. Caught it
by reading computed styles on staging rather than trusting the source; the
box-shadow was removed and the comment now says why. The Data-surface
composer has no motion hook, so its halo stays.
html2canvas does not know what a closed <details> is. The browser hides
collapsed content through the UA stylesheet's content-visibility, not through
a computed display: none, and html2canvas only honours the latter. So it laid
all ten collapsed source snippets out at full size and painted them over the
summary rows — the garbled citation list. Hiding the children of closed
<details> before capture matches what the reader sees, which is what the
exporter promises in its own docstring.
Mocking nodemailer from apps/public-web does not bind. Vitest resolves
a mock specifier relative to the test file, and nodemailer is a dependency of
packages/mail, not of public-web — so the mock registered against a path
nothing imports and every test tried to reach a real relay (502). The seam that
works is vi.mock("@ahu/mail", …) overriding only sendMail, which keeps the
templates, store, throttle, and validation real.
Han runs become a space, not nothing. Deleting 需要 from
Dokumen需要untuk yields Dokumenuntuk — harder to read than the leak. The
filter substitutes a space and collapses doubles, and does so only on lines it
actually touched, so markdown indentation elsewhere is never reflowed.
Open item — the staff surface is on a host I cannot reach
x056.ahu-demo.chatbot-neo-staff.val.id proxies to 192.168.82.122:3510
(ahu-int-01), not to 192.168.83.20:3510 where CLAUDE.md and
deploy-staging.sh point. The gateway vhost was retargeted on 2026-08-25 after
ahu-ai-chatbot-internal moved hosts on 2026-08-24.
Consequence: a3dc460 is deployed and verified on 192.168.83.20, but the staff
URL still serves the previous build, so /api/auth/mail-status,
/api/auth/forgot-password, and /reset-password return 404 there. The
demo-mode admin toggle from 76ad847 is present on 82.122, so only today's
commit is behind.
There is no SSH to 192.168.82.122 from the container, from Server 2, or from
the gateway — only port 3510 is open. The image and instructions are staged at
192.168.83.20:/home/efran/ahu-ai-staging/inbox/:
ahu-ai-chatbot-internal-a3dc460.tar.gzREADME-a3dc460-internal.txt
The vhost was deliberately not pointed back at 192.168.83.20. The migration
looks intentional and reverting it is Efran's call.
The public surface is unaffected: x056.ahu-demo.chatbot-neo.val.id still
points at 192.168.83.20:3500 and runs a3dc460.
Also worth knowing
apps/public-web/src/app/globals.css and TanyaComposer.tsx remain
uncommitted — they carry Efran's in-progress beranda/motion work, and fixes 1
and 5 live in that CSS file. Staging builds from the working tree so both are
live, but a git checkout of that file would take them with it. To keep them:
git add apps/public-web/src/app/globals.css && git commit
Turning mail on
Set SMTP_HOST (+ SMTP_USER/SMTP_PASS if the relay authenticates) and
APP_PUBLIC_URL in infra/env/public.env and internal.env, then redeploy.
For SES, use its SMTP interface — email-smtp.<region>.amazonaws.com:587 with
SES SMTP credentials, which are not AWS access keys. MAIL_ENABLED=off turns a
configured relay back off without a redeploy of code.
Leaving SMTP_HOST unset is a supported configuration and is what staging runs
today.