mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2026-02-18 00:41:46 +01:00
255 lines
6.6 KiB
Svelte
255 lines
6.6 KiB
Svelte
<!--
|
|
@component Systemd Journal Log Viewer (Admin only)
|
|
|
|
Properties:
|
|
- `isAdmin Bool!`: Is currently logged in user admin authority
|
|
-->
|
|
|
|
<script>
|
|
import {
|
|
Card,
|
|
CardHeader,
|
|
CardBody,
|
|
Table,
|
|
Input,
|
|
Button,
|
|
Badge,
|
|
Spinner,
|
|
InputGroup,
|
|
InputGroupText,
|
|
Icon,
|
|
} from "@sveltestrap/sveltestrap";
|
|
|
|
let { isAdmin } = $props();
|
|
|
|
const timeRanges = [
|
|
{ label: "Last 15 minutes", value: "15 min ago" },
|
|
{ label: "Last 1 hour", value: "1 hour ago" },
|
|
{ label: "Last 6 hours", value: "6 hours ago" },
|
|
{ label: "Last 24 hours", value: "24 hours ago" },
|
|
{ label: "Last 7 days", value: "7 days ago" },
|
|
];
|
|
|
|
const levels = [
|
|
{ label: "All levels", value: "" },
|
|
{ label: "Emergency (0)", value: "0" },
|
|
{ label: "Alert (1)", value: "1" },
|
|
{ label: "Critical (2)", value: "2" },
|
|
{ label: "Error (3)", value: "3" },
|
|
{ label: "Warning (4)", value: "4" },
|
|
{ label: "Notice (5)", value: "5" },
|
|
{ label: "Info (6)", value: "6" },
|
|
{ label: "Debug (7)", value: "7" },
|
|
];
|
|
|
|
const refreshIntervals = [
|
|
{ label: "Off", value: 0 },
|
|
{ label: "5s", value: 5000 },
|
|
{ label: "10s", value: 10000 },
|
|
{ label: "30s", value: 30000 },
|
|
];
|
|
|
|
let since = $state("1 hour ago");
|
|
let level = $state("");
|
|
let search = $state("");
|
|
let linesParam = $state("200");
|
|
let refreshInterval = $state(0);
|
|
let entries = $state([]);
|
|
let loading = $state(false);
|
|
let error = $state(null);
|
|
let timer = $state(null);
|
|
|
|
function levelColor(priority) {
|
|
if (priority <= 2) return "danger";
|
|
if (priority === 3) return "warning";
|
|
if (priority === 4) return "info";
|
|
if (priority <= 6) return "secondary";
|
|
return "light";
|
|
}
|
|
|
|
function levelName(priority) {
|
|
const names = [
|
|
"EMERG",
|
|
"ALERT",
|
|
"CRIT",
|
|
"ERR",
|
|
"WARN",
|
|
"NOTICE",
|
|
"INFO",
|
|
"DEBUG",
|
|
];
|
|
return names[priority] || "UNKNOWN";
|
|
}
|
|
|
|
function formatTimestamp(usec) {
|
|
if (!usec) return "";
|
|
const ms = parseInt(usec) / 1000;
|
|
const d = new Date(ms);
|
|
return d.toLocaleString();
|
|
}
|
|
|
|
async function fetchLogs() {
|
|
loading = true;
|
|
error = null;
|
|
try {
|
|
const params = new URLSearchParams();
|
|
params.set("since", since);
|
|
params.set("lines", linesParam);
|
|
if (level) params.set("level", level);
|
|
if (search.trim()) params.set("search", search.trim());
|
|
|
|
const resp = await fetch(`/frontend/logs/?${params.toString()}`);
|
|
if (!resp.ok) {
|
|
const body = await resp.json();
|
|
throw new Error(body.error || `HTTP ${resp.status}`);
|
|
}
|
|
entries = await resp.json();
|
|
} catch (e) {
|
|
error = e.message;
|
|
entries = [];
|
|
} finally {
|
|
loading = false;
|
|
}
|
|
}
|
|
|
|
function setupAutoRefresh(interval) {
|
|
if (timer) {
|
|
clearInterval(timer);
|
|
timer = null;
|
|
}
|
|
if (interval > 0) {
|
|
timer = setInterval(fetchLogs, interval);
|
|
}
|
|
}
|
|
|
|
$effect(() => {
|
|
setupAutoRefresh(refreshInterval);
|
|
return () => {
|
|
if (timer) clearInterval(timer);
|
|
};
|
|
});
|
|
|
|
// Fetch on mount
|
|
$effect(() => {
|
|
fetchLogs();
|
|
});
|
|
</script>
|
|
|
|
{#if !isAdmin}
|
|
<Card>
|
|
<CardBody>
|
|
<p>Access denied. Admin privileges required.</p>
|
|
</CardBody>
|
|
</Card>
|
|
{:else}
|
|
<Card class="mb-3">
|
|
<CardHeader>
|
|
<div class="d-flex flex-wrap align-items-center gap-2">
|
|
<InputGroup size="sm" style="max-width: 200px;">
|
|
<Input type="select" bind:value={since}>
|
|
{#each timeRanges as tr}
|
|
<option value={tr.value}>{tr.label}</option>
|
|
{/each}
|
|
</Input>
|
|
</InputGroup>
|
|
|
|
<InputGroup size="sm" style="max-width: 180px;">
|
|
<Input type="select" bind:value={level}>
|
|
{#each levels as lv}
|
|
<option value={lv.value}>{lv.label}</option>
|
|
{/each}
|
|
</Input>
|
|
</InputGroup>
|
|
|
|
<InputGroup size="sm" style="max-width: 150px;">
|
|
<InputGroupText>Lines</InputGroupText>
|
|
<Input type="select" bind:value={linesParam}>
|
|
<option value="100">100</option>
|
|
<option value="200">200</option>
|
|
<option value="500">500</option>
|
|
<option value="1000">1000</option>
|
|
</Input>
|
|
</InputGroup>
|
|
|
|
<InputGroup size="sm" style="max-width: 250px;">
|
|
<Input
|
|
type="text"
|
|
placeholder="Search..."
|
|
bind:value={search}
|
|
onkeydown={(e) => {
|
|
if (e.key === "Enter") fetchLogs();
|
|
}}
|
|
/>
|
|
</InputGroup>
|
|
|
|
<Button
|
|
size="sm"
|
|
color="primary"
|
|
onclick={fetchLogs}
|
|
disabled={loading}
|
|
>
|
|
{#if loading}
|
|
<Spinner size="sm" />
|
|
{:else}
|
|
<Icon name="arrow-clockwise" />
|
|
{/if}
|
|
Refresh
|
|
</Button>
|
|
|
|
<InputGroup size="sm" style="max-width: 140px;">
|
|
<InputGroupText>Auto</InputGroupText>
|
|
<Input type="select" bind:value={refreshInterval}>
|
|
{#each refreshIntervals as ri}
|
|
<option value={ri.value}>{ri.label}</option>
|
|
{/each}
|
|
</Input>
|
|
</InputGroup>
|
|
|
|
{#if entries.length > 0}
|
|
<small class="text-muted ms-auto">{entries.length} entries</small>
|
|
{/if}
|
|
</div>
|
|
</CardHeader>
|
|
<CardBody style="padding: 0;">
|
|
{#if error}
|
|
<div class="alert alert-danger m-3">{error}</div>
|
|
{/if}
|
|
|
|
<div style="max-height: 75vh; overflow-y: auto;">
|
|
<Table size="sm" striped hover responsive class="mb-0">
|
|
<thead class="sticky-top bg-white">
|
|
<tr>
|
|
<th style="width: 170px;">Timestamp</th>
|
|
<th style="width: 80px;">Level</th>
|
|
<th>Message</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody style="font-family: monospace; font-size: 0.85rem;">
|
|
{#each entries as entry}
|
|
<tr>
|
|
<td class="text-nowrap">{formatTimestamp(entry.timestamp)}</td>
|
|
<td
|
|
><Badge color={levelColor(entry.priority)}
|
|
>{levelName(entry.priority)}</Badge
|
|
></td
|
|
>
|
|
<td style="white-space: pre-wrap; word-break: break-all;"
|
|
>{entry.message}</td
|
|
>
|
|
</tr>
|
|
{:else}
|
|
{#if !loading && !error}
|
|
<tr
|
|
><td colspan="3" class="text-center text-muted py-3"
|
|
>No log entries found</td
|
|
></tr
|
|
>
|
|
{/if}
|
|
{/each}
|
|
</tbody>
|
|
</Table>
|
|
</div>
|
|
</CardBody>
|
|
</Card>
|
|
{/if}
|