Initial version
This commit is contained in:
133
EonaCat.LogStack.Status/Pages/Logs.cshtml
Normal file
133
EonaCat.LogStack.Status/Pages/Logs.cshtml
Normal file
@@ -0,0 +1,133 @@
|
||||
@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>
|
||||
}
|
||||
Reference in New Issue
Block a user