- Add /api/v1/health endpoint with component-level checks - Verify DB connectivity, fail2ban socket, scheduler, session cache - Add SQLite WAL cleanup on startup (orphan crash files) - Migration 8: import_log.timestamp → INTEGER UNIX epoch - Align import_log timestamps with history_archive (already UNIX int) - Add unit tests for DB cleanup and health router Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
111 lines
4.2 KiB
TypeScript
111 lines
4.2 KiB
TypeScript
import { Button, Badge, Table, TableBody, TableCell, TableCellLayout, TableHeader, TableHeaderCell, TableRow, Text, MessageBar, MessageBarBody, Spinner } from "@fluentui/react-components";
|
|
import { ArrowClockwiseRegular } from "@fluentui/react-icons";
|
|
import { useCommonSectionStyles } from "../../components/commonStyles";
|
|
import { useImportLog } from "../../hooks/useImportLog";
|
|
import { useBlocklistStyles } from "./blocklistStyles";
|
|
import { formatDate } from "../../utils/formatDate";
|
|
|
|
function formatUnixTimestamp(unixTs: number, timezone: string | null | undefined): string {
|
|
return formatDate(new Date(unixTs * 1000).toISOString(), timezone);
|
|
}
|
|
|
|
export function BlocklistImportLogSection(): React.JSX.Element {
|
|
const styles = useBlocklistStyles();
|
|
const sectionStyles = useCommonSectionStyles();
|
|
const { data, loading, error, page, setPage, refresh } = useImportLog(undefined, 20);
|
|
|
|
return (
|
|
<div className={sectionStyles.section}>
|
|
<div className={sectionStyles.sectionHeader}>
|
|
<Text size={500} weight="semibold">
|
|
Import Log
|
|
</Text>
|
|
<Button icon={<ArrowClockwiseRegular />} appearance="secondary" onClick={refresh}>
|
|
Refresh
|
|
</Button>
|
|
</div>
|
|
|
|
{error && (
|
|
<MessageBar intent="error">
|
|
<MessageBarBody>{error}</MessageBarBody>
|
|
</MessageBar>
|
|
)}
|
|
|
|
{loading ? (
|
|
<div className={styles.centred}>
|
|
<Spinner label="Loading log…" />
|
|
</div>
|
|
) : !data || data.items.length === 0 ? (
|
|
<div className={styles.centred}>
|
|
<Text>No import runs recorded yet.</Text>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className={styles.tableWrapper}>
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHeaderCell>Timestamp</TableHeaderCell>
|
|
<TableHeaderCell>Source URL</TableHeaderCell>
|
|
<TableHeaderCell>Imported</TableHeaderCell>
|
|
<TableHeaderCell>Skipped</TableHeaderCell>
|
|
<TableHeaderCell>Status</TableHeaderCell>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{data.items.map((entry) => (
|
|
<TableRow key={entry.id} className={entry.errors ? styles.errorRow : undefined}>
|
|
<TableCell>
|
|
<TableCellLayout>
|
|
<span className={styles.mono}>{formatUnixTimestamp(entry.timestamp, undefined)}</span>
|
|
</TableCellLayout>
|
|
</TableCell>
|
|
<TableCell>
|
|
<TableCellLayout>
|
|
<span className={styles.mono}>{entry.source_url}</span>
|
|
</TableCellLayout>
|
|
</TableCell>
|
|
<TableCell>
|
|
<TableCellLayout>{entry.ips_imported}</TableCellLayout>
|
|
</TableCell>
|
|
<TableCell>
|
|
<TableCellLayout>{entry.ips_skipped}</TableCellLayout>
|
|
</TableCell>
|
|
<TableCell>
|
|
<TableCellLayout>
|
|
{entry.errors ? (
|
|
<Badge appearance="filled" color="danger">
|
|
Error
|
|
</Badge>
|
|
) : (
|
|
<Badge appearance="filled" color="success">
|
|
OK
|
|
</Badge>
|
|
)}
|
|
</TableCellLayout>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
|
|
{data.total_pages > 1 && (
|
|
<div className={styles.pagination}>
|
|
<Button size="small" appearance="secondary" disabled={page <= 1} onClick={() => { setPage(page - 1); }}>
|
|
Previous
|
|
</Button>
|
|
<Text size={200}>
|
|
Page {page} of {data.total_pages}
|
|
</Text>
|
|
<Button size="small" appearance="secondary" disabled={page >= data.total_pages} onClick={() => { setPage(page + 1); }}>
|
|
Next
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|