Core half of the public demo (docs/website/DEMO_MODE.md §10 step 1, website#222).
Nothing here runs unless DEMO_MODE=true.
- server/src/demo/: the lock at /api/v1, ahead of every router. It covers every
Tier A row of §3.3, the shared-login conditions, POST /auth/register (DM16),
DELETE /auth/me/sessions/:id on a demo account (DM17), and the PUT
/admin/settings keys that reach a Tier A lock. Any multipart request is refused
app-wide. noindex and robots.txt. A condition that cannot decide refuses.
- Loader: demoLocks in module.json (writes only, under the module's own mounts),
ctx.demoMode, and api.onDemoSeed (DM15). MODULE_API 1.18.0 on both halves.
- GET /public/settings carries demo: { resetsAt, logins } on the demo only.
- npm run seed:demo -- <fixture>: upserts the four accounts, sets the site live
with registration disabled, seeds content on the first run only, calls each
module's onDemoSeed, and stamps demo_state. It resolves $env: strings and
refuses to run off the demo.
- Client: the banner, one DEMO_LOCKED toast fed from the request primitive (so
module screens get it too), and the published logins on both sign-in pages.
Dashboard and register say the demo message instead of blaming a role.
- OpenAPI: DemoLocked schema, the demo block, and a note that any write may
answer DEMO_LOCKED on the demo.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
48 lines
1.4 KiB
JavaScript
48 lines
1.4 KiB
JavaScript
import { useEffect, useState } from 'react'
|
|
import { onDemoLocked } from '../lib/demoNotice.js'
|
|
|
|
// The one toast for a write the public demo refused (DEMO_MODE.md §3.1). Fed
|
|
// by api/client.js through lib/demoNotice.js, so every screen — and every
|
|
// installed module's screens — gets it without handling the code itself. On a
|
|
// real shard nothing is ever refused that way, so it never appears.
|
|
const SHOW_MS = 5000
|
|
|
|
export default function DemoToast() {
|
|
const [message, setMessage] = useState(null)
|
|
|
|
useEffect(() => onDemoLocked((text) => setMessage(text)), [])
|
|
|
|
useEffect(() => {
|
|
if (!message) return undefined
|
|
const t = setTimeout(() => setMessage(null), SHOW_MS)
|
|
return () => clearTimeout(t)
|
|
}, [message])
|
|
|
|
return (
|
|
<div
|
|
role="status"
|
|
aria-live="polite"
|
|
style={{ position: 'fixed', left: 16, right: 16, bottom: 20, zIndex: 1000, display: 'flex', justifyContent: 'center', pointerEvents: 'none' }}
|
|
>
|
|
{message && (
|
|
<div
|
|
className="sans"
|
|
style={{
|
|
maxWidth: 480,
|
|
padding: '11px 16px',
|
|
borderRadius: 10,
|
|
fontSize: '0.88rem',
|
|
color: 'var(--head)',
|
|
background: 'var(--bg-deep)',
|
|
border: '1px solid var(--accent)',
|
|
boxShadow: '0 12px 32px rgba(0,0,0,0.45)',
|
|
pointerEvents: 'auto',
|
|
}}
|
|
>
|
|
{message}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|