diff --git a/public/views/jobs.js b/public/views/jobs.js index f8c9d38..02d0d6f 100644 --- a/public/views/jobs.js +++ b/public/views/jobs.js @@ -1,29 +1,71 @@ -// A7 stub — full panel ships in D5. import { api } from '../api.js'; -import { el, mount } from '../dom.js'; +import { el, mount, clear } from '../dom.js'; -function row(j) { +function badge(state) { + const cls = state === 'completed' ? 'ok' + : state === 'failed' ? 'bad' + : state === 'active' ? 'warn' + : 'idle'; + return el('span', { class: 'status ' + cls }, state); +} + +function row(j, onActed) { return el('li', {}, - el('span', { class: 'status idle' }, j.state), + badge(j.state), ' ', + el('span', { style: { fontFamily: 'var(--font-mono)' } }, j.name), ' ', + el('span', { class: 'muted' }, (j.id || '').slice(0, 8)), ' ', + el('button', { + class: 'ghost', + onclick: async () => { + try { await api.post(`/api/jobs/${j.id}/retry`); onActed(); } + catch (e) { alert('retry failed: ' + e.message); } + } + }, 'retry'), ' ', - el('span', { style: { fontFamily: 'var(--font-mono)' } }, j.name), - ' ', - el('span', { class: 'muted' }, (j.id || '').slice(0, 8)) + el('button', { + class: 'ghost', + onclick: async () => { + try { await api.del(`/api/jobs/${j.id}`); onActed(); } + catch (e) { alert('delete failed: ' + e.message); } + } + }, 'delete') ); } +async function refresh(container) { + let rows; + try { rows = await api.get('/api/jobs?limit=100'); } + catch (e) { + clear(container); + container.appendChild(el('p', { class: 'muted' }, 'Could not load: ' + e.message)); + return; + } + clear(container); + if (!rows.length) { + container.appendChild(el('p', { class: 'muted' }, 'No jobs yet.')); + return; + } + const byState = new Map(); + for (const r of rows) { + if (!byState.has(r.state)) byState.set(r.state, []); + byState.get(r.state).push(r); + } + for (const [state, items] of byState) { + container.appendChild(el('div', { class: 'sb-title', style: { margin: '14px 0 4px' } }, + `${state} (${items.length})`)); + container.appendChild(el('ul', { class: 'plain' }, + items.map(j => row(j, () => refresh(container))))); + } +} + export async function render(main) { const wrap = el('div'); mount(main, el('h1', { class: 'view-h1' }, 'Jobs'), - el('p', { class: 'view-sub muted' }, 'pg-boss queue — recent jobs across states.'), + el('p', { class: 'view-sub muted' }, 'pg-boss queue — polls every 10 s.'), wrap ); - try { - const rows = await api.get('/api/jobs?limit=50'); - if (!rows.length) mount(wrap, el('p', { class: 'muted' }, 'No jobs yet.')); - else mount(wrap, el('ul', { class: 'plain' }, rows.map(row))); - } catch (e) { - mount(wrap, el('p', { class: 'muted' }, 'Could not load: ' + e.message)); - } + await refresh(wrap); + const handle = setInterval(() => refresh(wrap), 10_000); + window.addEventListener('hashchange', () => clearInterval(handle), { once: true }); }