129 lines
5.2 KiB
React
129 lines
5.2 KiB
React
import { useState, useEffect, useCallback } from 'react';
|
|
import { api } from '../../api.js';
|
|
import { fmt, StatusBadge } from './adminHelpers.jsx';
|
|
|
|
const KNOWN_JOBS = [
|
|
{ name: 'auto_statut_retard', label: 'Passage automatique en retard' },
|
|
];
|
|
|
|
export default function JobLogsSection() {
|
|
const [logs, setLogs] = useState([]);
|
|
const [total, setTotal] = useState(0);
|
|
const [loading, setLoading] = useState(true);
|
|
const [running, setRunning] = useState(null);
|
|
const [runResult, setRunResult] = useState(null);
|
|
const [err, setErr] = useState(null);
|
|
const [page, setPage] = useState(0);
|
|
const PER_PAGE = 20;
|
|
|
|
const load = useCallback(async () => {
|
|
try {
|
|
setLoading(true);
|
|
const data = await api.get('/admin/job-logs', { limit: PER_PAGE, offset: page * PER_PAGE });
|
|
setLogs(data.rows);
|
|
setTotal(data.total);
|
|
} catch (e) { setErr(e.message); }
|
|
finally { setLoading(false); }
|
|
}, [page]);
|
|
|
|
useEffect(() => { load(); }, [load]);
|
|
|
|
const runJob = async (jobName) => {
|
|
setRunning(jobName); setRunResult(null);
|
|
try {
|
|
const r = await api.post(`/admin/jobs/${jobName}/run`, {});
|
|
setRunResult({ ok: true, msg: `Exécution terminée — ${r.nb_changes} modification(s)` });
|
|
load();
|
|
} catch (e) {
|
|
setRunResult({ ok: false, msg: e.message });
|
|
} finally { setRunning(null); }
|
|
};
|
|
|
|
const pages = Math.ceil(total / PER_PAGE);
|
|
|
|
return (
|
|
<div className="card">
|
|
<h3 style={{ margin: '0 0 4px' }}>Logs des jobs automatiques</h3>
|
|
<p className="text-muted" style={{ margin: '0 0 20px', fontSize: 'var(--fs-sm)' }}>
|
|
Historique d'exécution des tâches planifiées et lancement manuel.
|
|
</p>
|
|
|
|
<div style={{ display: 'flex', gap: 12, alignItems: 'center', marginBottom: 20, flexWrap: 'wrap' }}>
|
|
{KNOWN_JOBS.map(j => (
|
|
<button
|
|
key={j.name}
|
|
className="btn btn-outline"
|
|
style={{ display: 'flex', alignItems: 'center', gap: 7 }}
|
|
disabled={running === j.name}
|
|
onClick={() => runJob(j.name)}
|
|
>
|
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor"
|
|
strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
|
|
<polygon points="4,2 14,8 4,14"/>
|
|
</svg>
|
|
{running === j.name ? 'Exécution…' : `Lancer : ${j.label}`}
|
|
</button>
|
|
))}
|
|
{runResult && (
|
|
<span style={{
|
|
fontSize: 13, padding: '4px 12px', borderRadius: 6,
|
|
background: runResult.ok ? 'rgba(34,197,94,.1)' : 'rgba(239,68,68,.1)',
|
|
color: runResult.ok ? '#16a34a' : '#dc2626',
|
|
border: `1px solid ${runResult.ok ? 'rgba(34,197,94,.3)' : 'rgba(239,68,68,.3)'}`,
|
|
}}>
|
|
{runResult.msg}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{loading && <p style={{ color: 'var(--text-muted)' }}>Chargement…</p>}
|
|
{err && <p style={{ color: '#ef4444' }}>{err}</p>}
|
|
{!loading && !err && !logs.length && <p style={{ color: 'var(--text-muted)' }}>Aucun log disponible.</p>}
|
|
|
|
{logs.length > 0 && (
|
|
<>
|
|
<p style={{ fontSize: 13, color: 'var(--text-muted)', marginBottom: 12 }}>
|
|
{total} entrée{total > 1 ? 's' : ''} au total
|
|
</p>
|
|
<table style={{ fontSize: 13 }}>
|
|
<thead>
|
|
<tr>
|
|
<th>Date</th><th>Job</th><th>Statut</th>
|
|
<th className="num">Modifs</th><th>Détails</th><th>Erreur</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{logs.map(l => (
|
|
<tr key={l.id}>
|
|
<td style={{ whiteSpace: 'nowrap', color: 'var(--text-muted)' }}>{fmt(l.run_at)}</td>
|
|
<td style={{ fontFamily: 'monospace', fontSize: 12 }}>{l.job_name}</td>
|
|
<td><StatusBadge status={l.status} /></td>
|
|
<td className="num">
|
|
{l.nb_changes > 0
|
|
? <span style={{ fontWeight: 700, color: '#f97316' }}>{l.nb_changes}</span>
|
|
: <span style={{ color: 'var(--text-muted)' }}>0</span>
|
|
}
|
|
</td>
|
|
<td style={{ maxWidth: 280, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={l.details || ''}>
|
|
{l.details || <span style={{ color: 'var(--text-muted)' }}>—</span>}
|
|
</td>
|
|
<td style={{ color: '#ef4444', fontSize: 12 }}>
|
|
{l.error_msg || <span style={{ color: 'var(--text-muted)' }}>—</span>}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
{pages > 1 && (
|
|
<div style={{ display: 'flex', gap: 8, marginTop: 16, alignItems: 'center' }}>
|
|
<button className="btn btn-sm btn-outline" disabled={page === 0} onClick={() => setPage(p => p - 1)}>← Précédent</button>
|
|
<span style={{ fontSize: 13, color: 'var(--text-muted)' }}>Page {page + 1} / {pages}</span>
|
|
<button className="btn btn-sm btn-outline" disabled={page >= pages - 1} onClick={() => setPage(p => p + 1)}>Suivant →</button>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|