NPC editor, NPC tokens from library, admin management

- NPC library (own + shared) built on the character wizard with a free NPC preset
- NPC tokens in map editor and lobby, DM sheet on token click, DM checks for NPCs
- Admins can promote/demote other users

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJhXuLD2Z6hv3Wqeq9UL9K
This commit is contained in:
Claude
2026-10-23 21:36:09 +00:00
parent 57f262b6aa
commit 1db8a45cae
13 changed files with 364 additions and 86 deletions
+37
View File
@@ -65,6 +65,42 @@ function Field({ f, value, onChange }) {
)
}
// Admins can make other accounts admins (and take it away again)
function AdminUsers() {
const [users, setUsers] = useState(null)
const [err, setErr] = useState('')
const [open, setOpen] = useState(false)
const load = () => fetch('/api/admin/users').then((r) => (r.ok ? r.json() : Promise.reject())).then(setUsers).catch(() => setErr('Could not load the users.'))
useEffect(() => { if (open && !users) load() }, [open]) // eslint-disable-line
const set = async (u, admin) => {
setErr('')
const r = await fetch(`/api/admin/users/${encodeURIComponent(u.username)}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ admin }) })
if (!r.ok) setErr((await r.json().catch(() => ({}))).detail || 'That did not work.')
load()
}
return (
<div className="card adminusers">
<button className="linklike" aria-expanded={open} onClick={() => setOpen(!open)}>{open ? '▾' : '▸'} Users and admins</button>
{open && (
<>
<p className="small muted">Admins can use this editor and make other users admins. Admins named in <code>AW_ADMINS</code> on the server can only be changed there.</p>
{err && <p className="warn small">{err}</p>}
<ul className="userlist">
{(users || []).map((u) => (
<li key={u.username} className="row">
<span><strong>{u.username}</strong>{u.me ? ' (you)' : ''} {u.admin && <span className="chip on">admin</span>}{u.fixed && <span className="muted small"> set on the server</span>}</span>
{u.admin
? <button className="ghost" disabled={u.fixed} onClick={() => window.confirm(`Take admin rights from ${u.username}?`) && set(u, false)}>Remove admin</button>
: <button className="ghost" onClick={() => window.confirm(`Make ${u.username} an admin? Admins can change the rules and make others admins.`) && set(u, true)}>Make admin</button>}
</li>
))}
</ul>
</>
)}
</div>
)
}
export default function Editor({ rules, onReload }) {
const [state, setState] = useState(null)
const [tab, setTab] = useState('equipment')
@@ -139,6 +175,7 @@ export default function Editor({ rules, onReload }) {
<h1>Rules editor</h1>
<p className="muted small">Changes are stored next to the database and replace the entries of rules.json for that list; the shipped file stays as it is.
Existing characters keep working. Talents, mutations and implants are still edited in rules.json.</p>
<AdminUsers />
<div className="tabs" role="tablist">
{Object.entries(D).map(([id, x]) => (
<button key={id} role="tab" aria-selected={tab === id} className={tab === id ? 'on' : ''} onClick={() => { setTab(id); setEditing(''); setQ(''); setMsg({ text: '', bad: false }) }}>