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:
2026-07-08 22:08:14 +02:00
parent 4e38b5a791
commit 5bf6013460
161 changed files with 27152 additions and 1161 deletions
+30 -18
View File
@@ -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;