509 lines
18 KiB
HTML
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>
|