Files
EonaCat.LogStack/EonaCat.LogStack.Status/Pages/Logs.cshtml
2026-04-06 08:15:54 +02:00

134 lines
5.9 KiB
Plaintext

@page
@model LogsModel
@{
ViewData["Title"] = "Log Stream";
ViewData["Page"] = "logs";
}
@section Styles {
<style>
.chart-wrap canvas { height: 180px !important; }
</style>
}
<div class="section-header">
<span class="section-title">
Log Stream
<span class="live-count" id="live-count"></span>
</span>
<span class="mono" style="font-size:11px;color:var(--text-muted)">@Model.TotalCount entries</span>
</div>
<!-- Log volume chart (last 24 h) -->
<div class="chart-wrap">
<div style="font-family:var(--font-mono);font-size:10px;color:var(--text-muted);letter-spacing:1px;margin-bottom:10px">LOG VOLUME - LAST 24H</div>
<canvas id="log-stats-chart"></canvas>
</div>
<form method="get" class="filter-bar mt-2">
<select name="Level" class="form-control" onchange="this.form.submit()">
<option value="">All Levels</option>
@foreach (var lvl in new[] { "debug", "info", "warn", "error", "critical" })
{
<option value="@lvl" selected="@(Model.Level?.ToLower() == lvl)">@lvl.ToUpper()</option>
}
</select>
<select name="Source" class="form-control" onchange="this.form.submit()">
<option value="">All Sources</option>
@foreach (var s in Model.Sources)
{
<option value="@s" selected="@(Model.Source == s)">@s</option>
}
</select>
<input id="log-search" type="text" name="Search" value="@Model.Search"
placeholder="Search messages… (/ to focus)" class="form-control" style="max-width:280px" />
<input type="date" name="FromDate" value="@Model.FromDate?.ToString("yyyy-MM-dd")" class="form-control" style="max-width:145px" title="From date" />
<input type="date" name="ToDate" value="@Model.ToDate?.ToString("yyyy-MM-dd")" class="form-control" style="max-width:145px" title="To date" />
<button type="submit" class="btn btn-outline btn-sm">Apply</button>
<a href="/logs" class="btn btn-outline btn-sm">Clear</a>
<button type="button" class="btn btn-outline btn-sm" onclick="exportLogs()" title="Export visible entries as JSON">⬇ Export</button>
</form>
<!-- Log toolbar -->
<div class="log-toolbar">
<span class="mono" style="font-size:10px;color:var(--text-muted)">ENTRIES</span>
<span style="margin-left:auto;display:flex;gap:8px;align-items:center">
<span class="kbd" title="Keyboard shortcut">/</span> <span style="font-size:11px;color:var(--text-muted)">search</span>
<button class="btn btn-outline btn-sm" onclick="scrollLogsToBottom()" title="Jump to bottom">↓ Bottom</button>
<button class="btn btn-outline btn-sm" id="btn-scroll-top" onclick="document.getElementById('log-stream').scrollTop=0" title="Jump to top">↑ Top</button>
</span>
</div>
<div class="log-stream" id="log-stream" data-last-id="@(Model.Entries.FirstOrDefault()?.Id ?? 0)">
@if (!Model.Entries.Any())
{
<div class="empty-state">
<div class="empty-state-icon">▦</div>
<div class="empty-state-text">No log entries match your filters</div>
</div>
}
@foreach (var e in Model.Entries.AsEnumerable().Reverse())
{
<div class="log-entry">
<span class="log-ts">@e.Timestamp.ToString("yyyy-MM-dd HH:mm:ss")</span>
<span class="log-level @e.Level.ToLower()">@e.Level.ToUpper()</span>
<span class="log-source" title="@e.Source">@e.Source</span>
<span class="log-message">
@e.Message
@if (!string.IsNullOrEmpty(e.TraceId))
{
<span class="mono" style="font-size:9px;color:var(--text-muted);margin-left:6px">trace=@e.TraceId</span>
}
@if (!string.IsNullOrEmpty(e.Exception))
{
<details style="margin-top:3px">
<summary style="color:var(--down);cursor:pointer;font-size:10px">Exception ▾</summary>
<pre style="font-size:10px;color:var(--text-muted);margin-top:4px;white-space:pre-wrap">@e.Exception</pre>
</details>
}
</span>
</div>
}
</div>
@if (Model.TotalPages > 1)
{
<div class="flex gap-2 mt-2 align-center">
@if (Model.PageIndex > 1)
{
<a href="@Url.Page("/Logs", null, new { PageIndex = Model.PageIndex - 1, Level = Model.Level, Source = Model.Source, Search = Model.Search }, null)" class="btn btn-outline btn-sm">← Prev</a>
}
<span class="mono" style="font-size:11px;color:var(--text-muted)">Page @Model.PageIndex of @Model.TotalPages</span>
@if (Model.PageIndex < Model.TotalPages)
{
<a href="@Url.Page("/Logs", null, new { PageIndex = Model.PageIndex + 1, Level = Model.Level, Source = Model.Source, Search = Model.Search }, null)" class="btn btn-outline btn-sm">Next →</a>
}
</div>
}
@section Scripts {
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js" crossorigin="anonymous"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
loadLogStatsChart('log-stats-chart');
});
function exportLogs() {
const entries = [];
document.querySelectorAll('.log-entry').forEach(row => {
entries.push({
timestamp: row.querySelector('.log-ts')?.textContent?.trim(),
level: row.querySelector('.log-level')?.textContent?.trim(),
source: row.querySelector('.log-source')?.textContent?.trim(),
message: row.querySelector('.log-message')?.childNodes[0]?.textContent?.trim()
});
});
const blob = new Blob([JSON.stringify(entries, null, 2)], { type: 'application/json' });
const a = document.createElement('a'); a.href = URL.createObjectURL(blob);
a.download = `logs_${new Date().toISOString().slice(0,10)}.json`;
a.click();
showToast('Exported ' + entries.length + ' entries', 'success');
}
</script>
}