/* 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 (
// Palimpsest ~/archive
ws · live new job
); } 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
{FILTERS.map(f => ( ))}
); } 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 && (
↳ no entries yet. start a job to begin.
)} {!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 && (
)}
{isActive && ( view live )} {job.state === 'done' && ( <> {job.hasPdf && ( pdf )} tex {job.hasPdf ? 'pdf' : 'tex'} )} {job.state === 'error' && ( <> .tex retry )} {job.state === 'queued' && ( queued )}
); } function Footer({ count }) { return (
made with ♥ by @cmrabdu · about · github ↗ · cmrabdu.com vol. iii · {count} entries
); } Object.assign(window, { JobsPage });