This commit is contained in:
2026-01-08 15:18:34 +01:00
parent d385119ca2
commit 33a0b77bf1
111 changed files with 85489 additions and 24 deletions

View File

@@ -0,0 +1,26 @@
@page
@model ErrorModel
@{
ViewData["Title"] = "Error";
}
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (Model.ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@Model.RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to the <strong>Development</strong> environment displays detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>

View File

@@ -0,0 +1,21 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.Diagnostics;
namespace EonaCat.Logger.LogServer.Pages
{
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
public string? RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
public void OnGet()
{
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
}
}
}

View File

@@ -0,0 +1,369 @@
@page
@model IndexModel
@{
ViewData["Title"] = "Home page";
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LogCentral - Enterprise Logging Dashboard</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
.log-trace {
@@apply bg-gray-100 border-l-4 border-gray-400;
}
.log-debug {
@@apply bg-blue-50 border-l-4 border-blue-400;
}
.log-info {
@@apply bg-green-50 border-l-4 border-green-400;
}
.log-warning {
@@apply bg-yellow-50 border-l-4 border-yellow-400;
}
.log-error {
@@apply bg-red-50 border-l-4 border-red-400;
}
.log-critical {
@@apply bg-purple-50 border-l-4 border-purple-600;
}
.log-security {
@@apply bg-orange-50 border-l-4 border-orange-500;
}
.log-analytics {
@@apply bg-teal-50 border-l-4 border-teal-400;
}
</style>
</head>
<body class="bg-gray-50">
<div class="min-h-screen">
<!-- Header -->
<header class="bg-gradient-to-r from-blue-600 to-blue-800 text-white shadow-lg">
<div class="container mx-auto px-6 py-4">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-4">
<svg class="w-10 h-10" fill="currentColor" viewBox="0 0 20 20">
<path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z" />
<path fill-rule="evenodd" d="M4 5a2 2 0 012-2 3 3 0 003 3h2a3 3 0 003-3 2 2 0 012 2v11a2 2 0 01-2 2H6a2 2 0 01-2-2V5zm3 4a1 1 0 000 2h.01a1 1 0 100-2H7zm3 0a1 1 0 000 2h3a1 1 0 100-2h-3zm-3 4a1 1 0 100 2h.01a1 1 0 100-2H7zm3 0a1 1 0 100 2h3a1 1 0 100-2h-3z" />
</svg>
<div>
<h1 class="text-2xl font-bold">LogCentral</h1>
<p class="text-blue-200 text-sm">Enterprise Logging Platform</p>
</div>
</div>
<nav class="flex space-x-6">
<button onclick="showTab('dashboard')" class="tab-btn hover:text-blue-200 transition">Dashboard</button>
<button onclick="showTab('logs')" class="tab-btn hover:text-blue-200 transition">Logs</button>
<button onclick="showTab('analytics')" class="tab-btn hover:text-blue-200 transition">Analytics</button>
<button onclick="showTab('search')" class="tab-btn hover:text-blue-200 transition">Search</button>
</nav>
</div>
</div>
</header>
<div class="container mx-auto px-6 py-8">
<!-- Dashboard Tab -->
<div id="dashboard-tab" class="tab-content">
<!-- Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div class="bg-white rounded-lg shadow-md p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm font-medium">Total Logs</p>
<p id="stat-total" class="text-3xl font-bold text-gray-800 mt-2">0</p>
</div>
<div class="bg-blue-100 rounded-full p-3">
<svg class="w-8 h-8 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
<path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z" />
<path fill-rule="evenodd" d="M4 5a2 2 0 012-2 3 3 0 003 3h2a3 3 0 003-3 2 2 0 012 2v11a2 2 0 01-2 2H6a2 2 0 01-2-2V5zm9.707 5.707a1 1 0 00-1.414-1.414L9 12.586l-1.293-1.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" />
</svg>
</div>
</div>
<p class="text-green-600 text-sm mt-4">
<span id="stat-last24h">0</span> in last 24h
</p>
</div>
<div class="bg-white rounded-lg shadow-md p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm font-medium">Errors</p>
<p id="stat-errors" class="text-3xl font-bold text-red-600 mt-2">0</p>
</div>
<div class="bg-red-100 rounded-full p-3">
<svg class="w-8 h-8 text-red-600" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" />
</svg>
</div>
</div>
<p class="text-gray-500 text-sm mt-4">Critical issues</p>
</div>
<div class="bg-white rounded-lg shadow-md p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm font-medium">Warnings</p>
<p id="stat-warnings" class="text-3xl font-bold text-yellow-600 mt-2">0</p>
</div>
<div class="bg-yellow-100 rounded-full p-3">
<svg class="w-8 h-8 text-yellow-600" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" />
</svg>
</div>
</div>
<p class="text-gray-500 text-sm mt-4">Needs attention</p>
</div>
<div class="bg-white rounded-lg shadow-md p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm font-medium">Applications</p>
<p id="stat-apps" class="text-3xl font-bold text-purple-600 mt-2">0</p>
</div>
<div class="bg-purple-100 rounded-full p-3">
<svg class="w-8 h-8 text-purple-600" fill="currentColor" viewBox="0 0 20 20">
<path d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zM3 10a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H4a1 1 0 01-1-1v-6zM14 9a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1v-6a1 1 0 00-1-1h-2z" />
</svg>
</div>
</div>
<p class="text-gray-500 text-sm mt-4">Active services</p>
</div>
</div>
<!-- Charts -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<div class="bg-white rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4">Logs by Level</h3>
<canvas id="levelChart"></canvas>
</div>
<div class="bg-white rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4">Log Trends (24h)</h3>
<canvas id="trendChart"></canvas>
</div>
</div>
<!-- Recent Errors -->
<div class="bg-white rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4">Recent Critical Events</h3>
<div id="recent-errors" class="space-y-3"></div>
</div>
</div>
<!-- Logs Tab -->
<div id="logs-tab" class="tab-content hidden">
<!-- Filters -->
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4">Filters</h3>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<input type="text" id="filter-search" placeholder="Search logs..." class="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500">
<select id="filter-app" class="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500">
<option value="">All Applications</option>
</select>
<select id="filter-level" class="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500">
<option value="">All Levels</option>
<option value="0">Trace</option>
<option value="1">Debug</option>
<option value="2">Info</option>
<option value="3">Warning</option>
<option value="4">Error</option>
<option value="5">Critical</option>
<option value="6">Security</option>
<option value="7">Analytics</option>
</select>
<button onclick="applyFilters()" class="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 transition">Apply Filters</button>
</div>
</div>
<!-- Log List -->
<div class="bg-white rounded-lg shadow-md p-6">
<div id="logs-container" class="space-y-3"></div>
<div class="mt-6 flex justify-between items-center">
<button onclick="previousPage()" class="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50">Previous</button>
<span id="page-info" class="text-gray-600">Page 1</span>
<button onclick="nextPage()" class="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50">Next</button>
</div>
</div>
</div>
<!-- Analytics Tab -->
<div id="analytics-tab" class="tab-content hidden">
<div class="bg-white rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4">Analytics Dashboard</h3>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div>
<h4 class="text-md font-medium text-gray-700 mb-3">Top Events</h4>
<canvas id="analyticsChart"></canvas>
</div>
<div id="analytics-details" class="space-y-3"></div>
</div>
</div>
</div>
<!-- Search Tab -->
<div id="search-tab" class="tab-content hidden">
<div class="bg-white rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4">Advanced Search</h3>
<div class="flex gap-4 mb-6">
<input type="text" id="search-query" placeholder="Enter search query..." class="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500">
<button onclick="performSearch()" class="bg-blue-600 text-white px-8 py-2 rounded-lg hover:bg-blue-700 transition">Search</button>
</div>
<div id="search-results" class="space-y-3"></div>
</div>
</div>
</div>
</div>
<script>
let currentPage = 1;
let levelChart, trendChart, analyticsChart;
async function loadStats() {
try {
const response = await fetch('/api/logs/stats');
const stats = await response.json();
document.getElementById('stat-total').textContent = stats.totalLogs.toLocaleString();
document.getElementById('stat-last24h').textContent = stats.last24Hours.toLocaleString();
document.getElementById('stat-errors').textContent = stats.errorCount.toLocaleString();
document.getElementById('stat-warnings').textContent = stats.warningCount.toLocaleString();
document.getElementById('stat-apps').textContent = stats.applications.toLocaleString();
updateLevelChart(stats.byLevel);
} catch (error) {
console.error('Error loading stats:', error);
}
}
function updateLevelChart(byLevel) {
const ctx = document.getElementById('levelChart').getContext('2d');
const labels = ['Trace', 'Debug', 'Info', 'Warning', 'Error', 'Critical', 'Security', 'Analytics'];
const colors = ['#9CA3AF', '#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#9333EA', '#F97316', '#14B8A6'];
const data = Array(8).fill(0);
byLevel.forEach(item => {
data[item.level] = item.count;
});
if (levelChart) levelChart.destroy();
levelChart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: labels,
datasets: [{
data: data,
backgroundColor: colors
}]
},
options: {
responsive: true,
maintainAspectRatio: true
}
});
}
async function loadLogs() {
try {
const params = new URLSearchParams({
page: currentPage,
pageSize: 20,
search: document.getElementById('filter-search')?.value || '',
application: document.getElementById('filter-app')?.value || '',
level: document.getElementById('filter-level')?.value || ''
});
const response = await fetch(`/api/logs?${params}`);
const result = await response.json();
displayLogs(result.items);
document.getElementById('page-info').textContent = `Page ${result.page} of ${result.totalPages}`;
} catch (error) {
console.error('Error loading logs:', error);
}
}
function displayLogs(logs) {
const container = document.getElementById('logs-container');
const levels = ['trace', 'debug', 'info', 'warning', 'error', 'critical', 'security', 'analytics'];
const levelNames = ['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'CRIT', 'SEC', 'ANLYTC'];
container.innerHTML = logs.map(log => `
<div class="log-${levels[log.level]} p-4 rounded-lg">
<div class="flex justify-between items-start mb-2">
<div class="flex items-center space-x-3">
<span class="px-2 py-1 text-xs font-semibold rounded">${levelNames[log.level]}</span>
<span class="text-sm font-medium text-gray-700">${log.applicationName}</span>
<span class="text-xs text-gray-500">${log.category}</span>
</div>
<span class="text-xs text-gray-500">${new Date(log.timestamp).toLocaleString()}</span>
</div>
<p class="text-gray-800 font-medium mb-1">${log.message}</p>
${log.exception ? `<pre class="text-xs text-gray-600 mt-2 overflow-x-auto bg-gray-50 p-2 rounded">${log.exception}</pre>` : ''}
<div class="flex gap-2 mt-2 text-xs text-gray-500">
<span>Machine: ${log.machineName}</span>
${log.userId ? `<span>User: ${log.userId}</span>` : ''}
</div>
</div>
`).join('');
}
function showTab(tab) {
document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
document.getElementById(`${tab}-tab`).classList.remove('hidden');
if (tab === 'dashboard') loadStats();
if (tab === 'logs') loadLogs();
if (tab === 'analytics') loadAnalytics();
}
function applyFilters() {
currentPage = 1;
loadLogs();
}
function previousPage() {
if (currentPage > 1) {
currentPage--;
loadLogs();
}
}
function nextPage() {
currentPage++;
loadLogs();
}
async function performSearch() {
const query = document.getElementById('search-query').value;
if (!query) return;
try {
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
const results = await response.json();
displayLogs(results);
} catch (error) {
console.error('Error searching:', error);
}
}
async function loadAnalytics() {
// Load analytics data
}
// Initialize
loadStats();
setInterval(loadStats, 30000); // Refresh every 30 seconds
</script>
</body>
</html>

View File

@@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace EonaCat.Logger.LogServer.Pages
{
public class IndexModel : PageModel
{
public void OnGet()
{
}
}
}

View File

@@ -0,0 +1,8 @@
@page
@model PrivacyModel
@{
ViewData["Title"] = "Privacy Policy";
}
<h1>@ViewData["Title"]</h1>
<p>Use this page to detail your site's privacy policy.</p>

View File

@@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace EonaCat.Logger.LogServer.Pages
{
public class PrivacyModel : PageModel
{
public void OnGet()
{
}
}
}

View File

@@ -0,0 +1,52 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - EonaCat.Logger.LogServer</title>
<script type="importmap"></script>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
<link rel="stylesheet" href="~/EonaCat.Logger.LogServer.styles.css" asp-append-version="true" />
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container">
<a class="navbar-brand" asp-area="" asp-page="/Index">EonaCat.Logger.LogServer</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Privacy">Privacy</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>
<footer class="border-top footer text-muted">
<div class="container">
&copy; 2026 - EonaCat.Logger.LogServer - <a asp-area="" asp-page="/Privacy">Privacy</a>
</div>
</footer>
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>

View File

@@ -0,0 +1,48 @@
/* Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification
for details on configuring this project to bundle and minify static web assets. */
a.navbar-brand {
white-space: normal;
text-align: center;
word-break: break-all;
}
a {
color: #0077cc;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.nav-pills .nav-link.active, .nav-pills .show > .nav-link {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.border-top {
border-top: 1px solid #e5e5e5;
}
.border-bottom {
border-bottom: 1px solid #e5e5e5;
}
.box-shadow {
box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05);
}
button.accept-policy {
font-size: 1rem;
line-height: inherit;
}
.footer {
position: absolute;
bottom: 0;
width: 100%;
white-space: nowrap;
line-height: 60px;
}

View File

@@ -0,0 +1,2 @@
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/dist/jquery.validate.unobtrusive.min.js"></script>

View File

@@ -0,0 +1,3 @@
@using EonaCat.Logger.LogServer
@namespace EonaCat.Logger.LogServer.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

View File

@@ -0,0 +1,3 @@
@{
Layout = "_Layout";
}