feat(ui): Jobs panel with retry/delete + 10s polling
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,29 +1,71 @@
|
|||||||
// A7 stub — full panel ships in D5.
|
|
||||||
import { api } from '../api.js';
|
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', {},
|
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('button', {
|
||||||
' ',
|
class: 'ghost',
|
||||||
el('span', { class: 'muted' }, (j.id || '').slice(0, 8))
|
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) {
|
export async function render(main) {
|
||||||
const wrap = el('div');
|
const wrap = el('div');
|
||||||
mount(main,
|
mount(main,
|
||||||
el('h1', { class: 'view-h1' }, 'Jobs'),
|
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
|
wrap
|
||||||
);
|
);
|
||||||
try {
|
await refresh(wrap);
|
||||||
const rows = await api.get('/api/jobs?limit=50');
|
const handle = setInterval(() => refresh(wrap), 10_000);
|
||||||
if (!rows.length) mount(wrap, el('p', { class: 'muted' }, 'No jobs yet.'));
|
window.addEventListener('hashchange', () => clearInterval(handle), { once: true });
|
||||||
else mount(wrap, el('ul', { class: 'plain' }, rows.map(row)));
|
|
||||||
} catch (e) {
|
|
||||||
mount(wrap, el('p', { class: 'muted' }, 'Could not load: ' + e.message));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user