/* global React, PaperCard, Button, Chip, Stamp, ProgressBar, Icons */
// Palimpsest — Archive register (real API)
const { useState: jUseState, useMemo: jUseMemo, useEffect: jUseEffect } = React;
const FILTERS = ['all','done','active','queued','error'];
// Map server status → ledger chip
function chipForRow(state) {
if (state === 'processing') return active;
if (state === 'queued') return queued;
if (state === 'done') return done;
if (state === 'error') return error;
return null;
}
function matchesFilter(job, filter) {
if (filter === 'all') return true;
if (filter === 'active') return job.state === 'processing';
if (filter === 'done') return job.state === 'done';
if (filter === 'queued') return job.state === 'queued';
if (filter === 'error') return job.state === 'error';
return true;
}
// Normalize server job records to the ledger shape
function normalize(raw) {
const ts = raw.timestamp ? new Date(raw.timestamp) : null;
const date = ts ? ts.toLocaleDateString('en-GB', { day: '2-digit', month: 'short' }) : '';
const time = ts ? ts.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', hour12: false }) : '';
const total = raw.total || 0;
const page = raw.progress || 0;
const pct = total ? Math.round((page / total) * 100) : 0;
return {
id: raw.job_id,
raw,
date,
time,
file: raw.filename || raw.job_id,
state: raw.status,
model: (raw.model || '').replace(/-\d{8}$/, ''),
ocr: raw.engine || 'vision',
pages: total,
page,
progress: pct,
hasPdf: raw.has_pdf !== false,
error: raw.error,
};
}
function JobsPage() {
const [filter, setFilter] = jUseState('all');
const [q, setQ] = jUseState('');
const [jobs, setJobs] = jUseState([]);
const [loading, setLoading] = jUseState(true);
const [err, setErr] = jUseState(null);
const load = async () => {
try {
const res = await fetch('/api/jobs/list?limit=500');
if (!res.ok) throw new Error('http ' + res.status);
const j = await res.json();
const list = (j.jobs || []).map(normalize);
setJobs(list);
setErr(null);
} catch (e) {
setErr(String(e.message || e));
} finally {
setLoading(false);
}
};
jUseEffect(() => {
load();
const id = setInterval(load, 4000); // poll every 4s for live updates
return () => clearInterval(id);
}, []);
const filtered = jUseMemo(() => jobs.filter(j =>
matchesFilter(j, filter) && (!q || j.file.toLowerCase().includes(q.toLowerCase()))
), [jobs, filter, q]);
const stats = jUseMemo(() => ({
total: jobs.length,
done: jobs.filter(j => j.state === 'done').length,
active: jobs.filter(j => j.state === 'processing').length,
error: jobs.filter(j => j.state === 'error').length,
}), [jobs]);
const today = new Date();
const opened = `${String(today.getDate()).padStart(2,'0')}·${['i','ii','iii','iv','v','vi','vii','viii','ix','x','xi','xii'][today.getMonth()]}·${String(today.getFullYear()).slice(2)}`;
return (
);
}
function CommandBand() {
return (
);
}
function Masthead({ opened }) {
return (
‹ back to workshop
Archive register
all your processed documents · vol. iii
opened {opened}
opened {opened}
);
}
function StatsStrip({ stats, filter, setFilter, q, setQ }) {
return (
TOTAL
{stats.total}
documents · all time
DONE
{stats.done}
compiled · ready
ACTIVE
{stats.active}
in pipeline now
ERRATA
{stats.error}
need attention
);
}
function Ledger({ jobs, loading, err, totalCount }) {
return (
{loading && jobs.length === 0 && (
↳ loading register…
)}
{err && (
! cannot load registry: {err}
)}
{!loading && !err && jobs.length === 0 && totalCount === 0 && (
)}
{!loading && !err && jobs.length === 0 && totalCount > 0 && (
↳ no entries match. clear filter or try another query.
)}
{jobs.map(j =>
)}
);
}
function LedgerRow({ job }) {
const isActive = job.state === 'processing';
return (
{job.date}
{job.time}
#{job.id}
{job.file}
{chipForRow(job.state)}
{job.model || '—'}·{job.ocr}·{job.pages || '—'} p
{isActive && job.pages > 0 && (<>·{job.progress}% · page {job.page}/{job.pages}>)}
{job.state === 'error' && job.error && (
! {job.error}
)}
{isActive && job.pages > 0 && (
)}
);
}
function Footer({ count }) {
return (
);
}
Object.assign(window, { JobsPage });