// 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 });