feat(demo): DEMO_MODE lock, seed:demo and the demo banner (MODULE_API 1.18.0)
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
This commit is contained in:
47
client/src/components/DemoToast.jsx
Normal file
47
client/src/components/DemoToast.jsx
Normal file
@@ -0,0 +1,47 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user