feat: guest passage — slip through as a wanderer

POST /auth/guest mints a real user row (wanderer-<4 hex>, collision
retry, unusable random password) and issues the normal session cookie,
per-IP rate limited at 5/hour. EnterPage gains the guest action;
the séance shows a dismissible claim-a-name note for wanderer- users.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Indiana
2026-07-28 17:18:16 +00:00
parent 2e2c64d36b
commit f5320fcdec
10 changed files with 275 additions and 7 deletions

View File

@@ -108,6 +108,10 @@ function SeanceSession({ username }: { username: string }) {
const { t, i18n } = useTranslation()
const [departed, setDeparted] = useState(false)
// Guests are recognized by their generated name prefix — no backend flag.
// Dismissal is per-visit on purpose: the nudge should return next séance.
const isWanderer = username.startsWith('wanderer-')
const [wandererNoteDismissed, setWandererNoteDismissed] = useState(false)
const [question, setQuestion] = useState('')
const [dossierOpen, setDossierOpen] = useState(true)
const [view, setView] = useState<BoardView>('board')
@@ -306,6 +310,22 @@ function SeanceSession({ username }: { username: string }) {
</div>
)}
{/* ---- wanderer note: guests can claim a permanent name ---- */}
{isWanderer && !wandererNoteDismissed && (
<div className="seance-wanderer-note" role="note">
<span>{t('seance.wanderer.note')}</span>{' '}
<Link to="/enter">{t('seance.wanderer.claim')}</Link>
<button
type="button"
className="wanderer-note-dismiss"
aria-label={t('seance.wanderer.dismiss')}
onClick={() => setWandererNoteDismissed(true)}
>
×
</button>
</div>
)}
{/* ---- main: board + side column ---- */}
<main className="seance-main">
<section className="seance-board-wrap">