// desktop-screen-admin.jsx — Job Monitor / Admin screen for desktop function AdminDesktop({ active = 'jobs', onNav = () => {}, ctx = {}, onReload = () => {}, topBarProps = {} } = {}) { const jobs = ctx.admin?.jobs || window.JOBS || []; const [kindFilter, setKindFilter] = React.useState('all'); const audits = ctx.admin?.audits || []; const stats = ctx.admin?.stats || {}; if (!ctx.user?.isAdmin && !ctx.admin) { return } />; } const counts = { running: jobs.filter(j => j.status === 'running').length, queued: jobs.filter(j => j.status === 'queued').length, completed: jobs.filter(j => j.status === 'completed').length, failed: jobs.filter(j => j.status === 'failed' || j.status === 'skipped').length, }; const visibleJobs = jobs.filter(j => kindFilter === 'all' || j.kind === kindFilter); return (
}>Worker online } onClick={onReload}>Aktualisieren } onClick={() => ctx.reset?.('searchBuilder')}>Job manuell starten } {...topBarProps} />
{/* Stat strip */}
} delta="2,4s Ø Latenz" /> } delta="nächster: 09:15" /> } delta="+14 vs. gestern" trend="up" accent /> } delta={counts.failed > 0 ? 'prüfen' : 'keine'} trend={counts.failed > 0 ? 'down' : null} />
{/* Two-col: Jobs table + debug console */}
{/* Jobs table */}

Aktive & geplante Jobs

setKindFilter('all')}>Alle setKindFilter('scrape')}>Scrape setKindFilter('ai-eval')}>KI-Eval
Job / Quelle Suche Fortschritt Items Gestartet
{visibleJobs.map((j, i) => { const searchId = j.taskId || j.task_id || j.searchId; if (searchId) ctx.navigate?.('results', { searchId }); }} />)} {visibleJobs.length === 0 && (
Keine Jobs in diesem Filter.
)}
{/* Audit stream */}

Audit-Log

letzte Systemereignisse
a.need_attention) ? 'warn' : 'success'} icon={}> {audits.length} Einträge
{audits.length === 0 ? ( keine aktuellen Audit-Einträge ) : audits.slice(0, 16).map(a => ( {a.type}: {a.message} ))}
{/* Live backend totals */}

System-Zahlen

Queue-Status

); } function LogLine({ ts, kind, children, cursor }) { const c = kind === 'error' ? '#E07C74' : kind === 'warn' ? '#F4B04D' : '#A9D2B5'; return (
[{ts}] {kind === 'error' ? 'error: ' : kind === 'warn' ? 'warn: ' : ''}{children} {cursor && }
); } function AdminMetric({ label, value }) { return (
{label}
{Number(value || 0).toLocaleString('de-DE')}
); } function JobRowDesktop({ job, last, onOpen }) { const statusCfg = { running: { c: 'var(--info)', bg: 'var(--info-soft)', label: 'läuft' }, queued: { c: 'var(--warn)', bg: 'var(--warn-soft)', label: 'wartet' }, completed: { c: 'var(--success)', bg: 'var(--success-soft)', label: 'fertig' }, failed: { c: 'var(--danger)', bg: 'var(--danger-soft)', label: 'fehler' }, skipped: { c: 'var(--ink-muted)', bg: 'var(--surface-2)', label: 'überspr.' }, }; const s = statusCfg[job.status] || statusCfg.queued; const startedAt = job.started || job.createdAt || null; return (
{job.kind === 'ai-eval' ? : } {job.kind === 'ai-eval' ? 'KI-Eval' : 'Scrape'} · {job.target}
{job.error && (
{job.error}
)}
{job.search}
{s.label} {job.status === 'running' && (
)}
{job.items}
{!startedAt || startedAt === '—' ? '—' : new Date(startedAt).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}
); } function SourceHealth({ name, latency, successRate, status, last }) { const stCfg = { ok: { c: 'var(--success)', label: 'OK' }, degraded: { c: 'var(--warn)', label: 'Beeinträchtigt' }, off: { c: 'var(--ink-muted)', label: 'Aus' }, }; const s = stCfg[status]; return (
{name}
{successRate == null ? `${Number(latency || 0).toLocaleString('de-DE')} aktuell` : `${latency ? `${latency} ms Ø Latenz` : 'inaktiv'} · Erfolg ${successRate}%`}
{s.label}
); } // blink keyframe for cursor if (!document.getElementById('admin-css')) { const st = document.createElement('style'); st.id = 'admin-css'; st.textContent = '@keyframes blink { 50% { opacity: 0 } }'; document.head.appendChild(st); } Object.assign(window, { AdminDesktop });