silserv/dashboard.html
2025-08-08 15:55:55 +07:00

509 lines
18 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Silserv Logs Dashboard</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 20px;
border-bottom: 1px solid #eee;
}
.filters {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 20px;
padding: 15px;
background-color: #f8f9fa;
border-radius: 6px;
}
.filter-group {
display: flex;
flex-direction: column;
}
.filter-group label {
font-weight: bold;
margin-bottom: 5px;
color: #333;
}
.filter-group input,
.filter-group select {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.log-entry {
border-left: 4px solid #ccc;
margin-bottom: 10px;
padding: 10px;
background-color: #f9f9f9;
border-radius: 4px;
}
.log-entry.error {
border-left-color: #dc3545;
background-color: #f8d7da;
}
.log-entry.warn {
border-left-color: #ffc107;
background-color: #fff3cd;
}
.log-entry.info {
border-left-color: #17a2b8;
background-color: #d1ecf1;
}
.log-entry.debug {
border-left-color: #6c757d;
background-color: #e2e3e5;
}
.log-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
}
.log-timestamp {
color: #666;
font-size: 12px;
}
.log-level {
padding: 2px 8px;
border-radius: 12px;
font-size: 11px;
font-weight: bold;
text-transform: uppercase;
}
.log-level.error {
background-color: #dc3545;
color: white;
}
.log-level.warn {
background-color: #ffc107;
color: black;
}
.log-level.info {
background-color: #17a2b8;
color: white;
}
.log-level.debug {
background-color: #6c757d;
color: white;
}
.log-message {
font-family: "Courier New", monospace;
margin: 5px 0;
}
.log-details {
display: flex;
gap: 15px;
font-size: 12px;
color: #666;
}
.pagination {
display: flex;
justify-content: center;
gap: 10px;
margin-top: 20px;
}
.pagination button {
padding: 8px 12px;
border: 1px solid #ddd;
background: white;
cursor: pointer;
border-radius: 4px;
}
.pagination button:hover {
background-color: #f5f5f5;
}
.pagination button.active {
background-color: #007bff;
color: white;
border-color: #007bff;
}
.loading {
text-align: center;
padding: 40px;
color: #666;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.stat-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px;
border-radius: 6px;
text-align: center;
}
.stat-number {
font-size: 24px;
font-weight: bold;
}
.stat-label {
font-size: 12px;
opacity: 0.9;
}
.auto-refresh {
display: flex;
align-items: center;
gap: 10px;
}
.json-metadata {
background-color: #f1f3f4;
padding: 8px;
border-radius: 4px;
font-family: "Courier New", monospace;
font-size: 11px;
margin-top: 5px;
max-height: 100px;
overflow-y: auto;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔍 Silserv Logs Dashboard</h1>
<div class="auto-refresh">
<label>
<input type="checkbox" id="autoRefresh" /> Auto-refresh
(30s)
</label>
<button onclick="loadLogs()">🔄 Refresh</button>
</div>
</div>
<div class="stats" id="stats">
<!-- Stats will be populated here -->
</div>
<div class="filters">
<div class="filter-group">
<label>Date:</label>
<select id="dateFilter">
<option value="">Today</option>
</select>
</div>
<div class="filter-group">
<label>Level:</label>
<select id="levelFilter">
<option value="">All Levels</option>
<option value="error">Error</option>
<option value="warn">Warning</option>
<option value="info">Info</option>
<option value="debug">Debug</option>
<option value="trace">Trace</option>
</select>
</div>
<div class="filter-group">
<label>Component:</label>
<select id="componentFilter">
<option value="">All Components</option>
<option value="updater">Updater</option>
<option value="discovery">Discovery</option>
<option value="api">API</option>
<option value="health">Health</option>
</select>
</div>
<div class="filter-group">
<label>Container:</label>
<input
type="text"
id="containerFilter"
placeholder="Container name/ID"
/>
</div>
<div class="filter-group">
<label>Search:</label>
<input
type="text"
id="searchFilter"
placeholder="Search in messages"
/>
</div>
<div class="filter-group">
<label>Limit:</label>
<select id="limitFilter">
<option value="50">50</option>
<option value="100" selected>100</option>
<option value="200">200</option>
<option value="500">500</option>
</select>
</div>
</div>
<div id="logsContainer">
<div class="loading">Loading logs...</div>
</div>
<div class="pagination" id="pagination">
<!-- Pagination will be populated here -->
</div>
</div>
<script>
const API_BASE = "http://localhost:36530/api";
let currentPage = 1;
let currentFilters = {};
let autoRefreshInterval = null;
// Initialize the dashboard
document.addEventListener("DOMContentLoaded", function () {
loadAvailableDates();
loadLogs();
setupEventListeners();
});
function setupEventListeners() {
// Filter change listeners
document
.getElementById("dateFilter")
.addEventListener("change", applyFilters);
document
.getElementById("levelFilter")
.addEventListener("change", applyFilters);
document
.getElementById("componentFilter")
.addEventListener("change", applyFilters);
document
.getElementById("containerFilter")
.addEventListener("input", debounce(applyFilters, 500));
document
.getElementById("searchFilter")
.addEventListener("input", debounce(applyFilters, 500));
document
.getElementById("limitFilter")
.addEventListener("change", applyFilters);
// Auto-refresh toggle
document
.getElementById("autoRefresh")
.addEventListener("change", function (e) {
if (e.target.checked) {
autoRefreshInterval = setInterval(loadLogs, 30000);
} else {
clearInterval(autoRefreshInterval);
}
});
}
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
async function loadAvailableDates() {
try {
const response = await fetch(`${API_BASE}/logs/dates`);
const data = await response.json();
if (data.success) {
const dateSelect =
document.getElementById("dateFilter");
dateSelect.innerHTML =
'<option value="">Today</option>';
data.data.available_dates.forEach((date) => {
const option = document.createElement("option");
option.value = date;
option.textContent = date;
dateSelect.appendChild(option);
});
}
} catch (error) {
console.error("Failed to load available dates:", error);
}
}
function applyFilters() {
currentPage = 1;
currentFilters = {
date: document.getElementById("dateFilter").value,
level: document.getElementById("levelFilter").value,
component: document.getElementById("componentFilter").value,
container: document.getElementById("containerFilter").value,
search: document.getElementById("searchFilter").value,
limit: document.getElementById("limitFilter").value,
};
loadLogs();
}
async function loadLogs(page = 1) {
const container = document.getElementById("logsContainer");
container.innerHTML =
'<div class="loading">Loading logs...</div>';
try {
const params = new URLSearchParams({
page: page,
...currentFilters,
});
// Remove empty parameters
for (const [key, value] of [...params.entries()]) {
if (!value) {
params.delete(key);
}
}
const response = await fetch(`${API_BASE}/logs?${params}`);
const data = await response.json();
if (data.success) {
displayLogs(data.data);
updatePagination(data.data);
updateStats(data.data);
} else {
container.innerHTML = `<div class="loading">Error: ${data.message || "Failed to load logs"}</div>`;
}
} catch (error) {
container.innerHTML = `<div class="loading">Error: ${error.message}</div>`;
console.error("Failed to load logs:", error);
}
}
function displayLogs(logsData) {
const container = document.getElementById("logsContainer");
if (logsData.logs.length === 0) {
container.innerHTML =
'<div class="loading">No logs found for the selected filters.</div>';
return;
}
const logsHtml = logsData.logs
.map((log) => {
const timestamp = new Date(
log.timestamp,
).toLocaleString();
const metadata = log.metadata
? `<div class="json-metadata">${JSON.stringify(log.metadata, null, 2)}</div>`
: "";
return `
<div class="log-entry ${log.level}">
<div class="log-meta">
<span class="log-timestamp">${timestamp}</span>
<span class="log-level ${log.level}">${log.level}</span>
</div>
<div class="log-message">${escapeHtml(log.message)}</div>
<div class="log-details">
<span><strong>Component:</strong> ${log.component}</span>
${log.container_name ? `<span><strong>Container:</strong> ${log.container_name}</span>` : ""}
${log.container_id ? `<span><strong>ID:</strong> ${log.container_id.substring(0, 12)}</span>` : ""}
</div>
${metadata}
</div>
`;
})
.join("");
container.innerHTML = logsHtml;
}
function updatePagination(logsData) {
const pagination = document.getElementById("pagination");
const totalPages = Math.ceil(logsData.total / logsData.limit);
if (totalPages <= 1) {
pagination.innerHTML = "";
return;
}
let paginationHtml = "";
// Previous button
if (logsData.page > 1) {
paginationHtml += `<button onclick="changePage(${logsData.page - 1})">« Previous</button>`;
}
// Page numbers
const startPage = Math.max(1, logsData.page - 2);
const endPage = Math.min(totalPages, logsData.page + 2);
for (let i = startPage; i <= endPage; i++) {
const activeClass = i === logsData.page ? "active" : "";
paginationHtml += `<button class="${activeClass}" onclick="changePage(${i})">${i}</button>`;
}
// Next button
if (logsData.page < totalPages) {
paginationHtml += `<button onclick="changePage(${logsData.page + 1})">Next »</button>`;
}
pagination.innerHTML = paginationHtml;
}
function updateStats(logsData) {
const stats = document.getElementById("stats");
// Count logs by level
const levelCounts = logsData.logs.reduce((acc, log) => {
acc[log.level] = (acc[log.level] || 0) + 1;
return acc;
}, {});
const statsHtml = `
<div class="stat-card">
<div class="stat-number">${logsData.total}</div>
<div class="stat-label">Total Logs</div>
</div>
<div class="stat-card">
<div class="stat-number">${levelCounts.error || 0}</div>
<div class="stat-label">Errors</div>
</div>
<div class="stat-card">
<div class="stat-number">${levelCounts.warn || 0}</div>
<div class="stat-label">Warnings</div>
</div>
<div class="stat-card">
<div class="stat-number">${levelCounts.info || 0}</div>
<div class="stat-label">Info</div>
</div>
`;
stats.innerHTML = statsHtml;
}
function changePage(page) {
currentPage = page;
loadLogs(page);
}
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
</script>
</body>
</html>