fix(admin): removeNode URL bug, container empty-state text, PasswordResetModal replaces prompt()

This commit is contained in:
Zac Gaetano 2026-05-23 09:07:56 -04:00
parent 004bdd0778
commit 854775e322

View file

@ -85,6 +85,7 @@ function Users() {
const [tab, setTab] = React.useState("users");
const [showInvite, setShowInvite] = React.useState(false);
const [editingUser, setEditingUser] = React.useState(null);
const [resetUser, setResetUser] = React.useState(null);
const [menuFor, setMenuFor] = React.useState(null); // row id whose menu is open
const refreshUsers = React.useCallback(() => {
@ -139,15 +140,7 @@ function Users() {
.catch(e => alert('Delete failed: ' + e.message));
};
const resetPassword = (u) => {
setMenuFor(null);
const pw = prompt(`Reset password for ${u.username}\n\nNew password (≥ 8 characters):`);
if (!pw) return;
if (pw.length < 8) { alert('Password must be at least 8 characters.'); return; }
window.ZAMPP_API.fetch('/users/' + u.id, { method: 'PATCH', body: JSON.stringify({ password: pw }) })
.then(() => alert('Password reset for ' + u.username))
.catch(e => alert('Reset failed: ' + e.message));
};
const resetPassword = (u) => { setMenuFor(null); setResetUser(u); };
const changeRole = (u, newRole) => {
if (u.role === newRole) return;
@ -253,6 +246,12 @@ function Users() {
onSaved={() => { setEditingUser(null); refreshUsers(); }}
/>
)}
{resetUser && (
<PasswordResetModal user={resetUser}
onClose={() => setResetUser(null)}
onSaved={() => setResetUser(null)}
/>
)}
</div>
);
}
@ -293,6 +292,77 @@ function EditUserModal({ user, onClose, onSaved }) {
);
}
function PasswordResetModal({ user, onClose, onSaved }) {
const [pw, setPw] = React.useState('');
const [pw2, setPw2] = React.useState('');
const [show, setShow] = React.useState(false);
const [saving, setSaving] = React.useState(false);
const [err, setErr] = React.useState(null);
const [done, setDone] = React.useState(false);
const valid = pw.length >= 8 && pw === pw2;
const submit = () => {
if (!valid) return;
setSaving(true); setErr(null);
window.ZAMPP_API.fetch('/users/' + user.id, { method: 'PATCH', body: JSON.stringify({ password: pw }) })
.then(() => { setSaving(false); setDone(true); setTimeout(onSaved, 1200); })
.catch(e => { setSaving(false); setErr(e.message); });
};
return (
<div className="modal-backdrop" onClick={onClose}>
<div className="modal" style={{ width: 400 }} onClick={e => e.stopPropagation()}>
<div className="modal-head">
<div style={{ fontSize: 15, fontWeight: 600 }}>Reset password · @{user.username}</div>
<button className="icon-btn" onClick={onClose}><Icon name="x" /></button>
</div>
<div className="modal-body">
{done ? (
<div style={{ textAlign: 'center', padding: '12px 0', color: 'var(--success)', fontSize: 13 }}>
<Icon name="check" size={16} /> Password updated.
</div>
) : (<>
<div className="field">
<label className="field-label">New password</label>
<div style={{ position: 'relative' }}>
<input className="field-input" autoFocus type={show ? 'text' : 'password'}
value={pw} onChange={e => setPw(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter' && valid) submit(); }}
style={{ paddingRight: 36 }} />
<button className="icon-btn" style={{ position: 'absolute', right: 4, top: '50%', transform: 'translateY(-50%)' }}
onClick={() => setShow(s => !s)} type="button" tabIndex={-1}>
<Icon name={show ? 'eye-off' : 'eye'} size={13} />
</button>
</div>
<div style={{ fontSize: 11, color: pw.length > 0 && pw.length < 8 ? 'var(--danger)' : 'var(--text-3)', marginTop: 4 }}>
Minimum 8 characters
</div>
</div>
<div className="field">
<label className="field-label">Confirm password</label>
<input className="field-input" type={show ? 'text' : 'password'}
value={pw2} onChange={e => setPw2(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter' && valid) submit(); }} />
{pw2.length > 0 && pw !== pw2 && (
<div style={{ fontSize: 11, color: 'var(--danger)', marginTop: 4 }}>Passwords do not match</div>
)}
</div>
{err && <div style={{ fontSize: 12, color: 'var(--danger)', marginTop: 4 }}>{err}</div>}
</>)}
</div>
{!done && (
<div className="modal-foot">
<button className="btn ghost sm" onClick={onClose}>Cancel</button>
<button className="btn primary sm" onClick={submit} disabled={saving || !valid}>
{saving ? 'Saving…' : 'Reset password'}
</button>
</div>
)}
</div>
</div>
);
}
function GroupsPanel({ groups, users, onChange }) {
const [creating, setCreating] = React.useState(false);
const [newName, setNewName] = React.useState('');
@ -428,7 +498,6 @@ function GroupsPanel({ groups, users, onChange }) {
function Tokens() {
const [burned, setBurned] = React.useState(14340);
const [rate, setRate] = React.useState(2.4);
const [showCalc, setShowCalc] = React.useState(false);
React.useEffect(() => {
const i = setInterval(() => {
@ -776,8 +845,8 @@ function Containers() {
{containers !== null && containers.length === 0 && (
<div style={{ textAlign: 'center', padding: '64px 0', color: 'var(--text-3)' }}>
<div style={{ fontSize: 32, marginBottom: 12 }}>🐳</div>
<div style={{ fontWeight: 500, fontSize: 14 }}>No container data available</div>
<div style={{ fontSize: 12, marginTop: 6 }}>Container metrics endpoint not yet wired</div>
<div style={{ fontWeight: 500, fontSize: 14 }}>No containers returned</div>
<div style={{ fontSize: 12, marginTop: 6 }}>Confirm <code>/var/run/docker.sock</code> is mounted in the mam-api container</div>
</div>
)}
{containers !== null && containers.length > 0 && (
@ -1226,7 +1295,6 @@ function GpuSettingsCard() {
};
if (!cfg) return <SettingsCard icon="gpu" title="Proxy encoding" sub="Global proxy encoder applied to every ingested file"><div style={{ color: 'var(--text-3)', fontSize: 12.5 }}>Loading</div></SettingsCard>;
const set = (k, v) => setCfg(c => ({ ...c, [k]: v }));
const gpuEnabled = cfg.gpu_transcode_enabled === 'true' || cfg.gpu_transcode_enabled === true;