initialize
This commit is contained in:
commit
f6e3fdd3c2
22 changed files with 7447 additions and 0 deletions
509
dashboard.html
Normal file
509
dashboard.html
Normal file
|
|
@ -0,0 +1,509 @@
|
|||
<!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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue