feat: full feature buildout — streaming, i18n, mastery map, admin, jobs
Progressive lesson streaming via onSegment callback (fixes SSE for non-English users — locale was shadowed in lesson-reader useEffect). Adds: BullMQ workers, Redis stream buffer, token budget enforcement, Langfuse tracing, golden-eval runner, Playwright e2e scaffolding, lesson depth/locale/preferences schema, mastery map UI, admin panel (blueprints/users/reports/quality/misconceptions), image queries, source citations, view transitions, reading animations, i18n (next-intl), PDF export, surprise endpoint, and 402 passing unit tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@ import { useRouter } from 'next/navigation';
|
||||
|
||||
type UserRow = { id: string; name: string | null; email: string | null; role: 'learner' | 'admin' | 'suspended'; createdAt: Date };
|
||||
|
||||
export default function AdminUsersClient({ users }: { users: UserRow[] }) {
|
||||
export default function AdminUsersClient({ users, selfId }: { users: UserRow[]; selfId: string }) {
|
||||
const router = useRouter();
|
||||
const [busy, setBusy] = useState<string | null>(null);
|
||||
|
||||
@@ -48,24 +48,35 @@ export default function AdminUsersClient({ users }: { users: UserRow[] }) {
|
||||
<td style={s.td}>{u.role}</td>
|
||||
<td style={s.td}>{new Date(u.createdAt).toLocaleDateString()}</td>
|
||||
<td style={{ ...s.td, display: 'flex', gap: 'var(--space-2)', flexWrap: 'wrap' as const }}>
|
||||
{u.role !== 'admin' && (
|
||||
<button disabled={busy === u.id} onClick={() => action(u.id, { role: 'admin' })} style={s.btn}>
|
||||
Make admin
|
||||
</button>
|
||||
{u.id === selfId ? (
|
||||
<span style={s.selfNote}>You</span>
|
||||
) : (
|
||||
<>
|
||||
{u.role !== 'admin' && (
|
||||
<button disabled={busy === u.id} onClick={() => action(u.id, { role: 'admin' })} style={s.btn}>
|
||||
Make admin
|
||||
</button>
|
||||
)}
|
||||
{u.role === 'admin' && (
|
||||
<button disabled={busy === u.id} onClick={() => action(u.id, { role: 'learner' })} style={s.btn}>
|
||||
Make learner
|
||||
</button>
|
||||
)}
|
||||
{u.role !== 'suspended' && (
|
||||
<button disabled={busy === u.id} onClick={() => action(u.id, { role: 'suspended' })} style={s.btn}>
|
||||
Suspend
|
||||
</button>
|
||||
)}
|
||||
{u.role === 'suspended' && (
|
||||
<button disabled={busy === u.id} onClick={() => action(u.id, { role: 'learner' })} style={s.btn}>
|
||||
Unsuspend
|
||||
</button>
|
||||
)}
|
||||
<button disabled={busy === u.id} onClick={() => del(u.id)} style={s.dangerBtn}>
|
||||
Delete
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{u.role !== 'suspended' && (
|
||||
<button disabled={busy === u.id} onClick={() => action(u.id, { role: 'suspended' })} style={s.btn}>
|
||||
Suspend
|
||||
</button>
|
||||
)}
|
||||
{u.role === 'suspended' && (
|
||||
<button disabled={busy === u.id} onClick={() => action(u.id, { role: 'learner' })} style={s.btn}>
|
||||
Unsuspend
|
||||
</button>
|
||||
)}
|
||||
<button disabled={busy === u.id} onClick={() => del(u.id)} style={s.dangerBtn}>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
@@ -83,4 +94,5 @@ const s = {
|
||||
td: { padding: 'var(--space-2) var(--space-3)', borderBottom: '1px solid var(--color-border-subtle)', color: 'var(--color-ink)' },
|
||||
btn: { padding: 'var(--space-1) var(--space-3)', background: 'var(--color-accent-subtle)', color: 'var(--color-accent)', border: '1px solid var(--color-accent)', borderRadius: '4px', cursor: 'pointer', fontSize: '0.8125rem' },
|
||||
dangerBtn: { padding: 'var(--space-1) var(--space-3)', background: 'var(--color-misconception-subtle)', color: 'var(--color-misconception)', border: '1px solid var(--color-misconception)', borderRadius: '4px', cursor: 'pointer', fontSize: '0.8125rem' },
|
||||
selfNote: { color: 'var(--color-ink-faint)', fontSize: '0.8125rem' },
|
||||
} as const;
|
||||
|
||||
Reference in New Issue
Block a user