<!DOCTYPE html>
<html lang="en">
<script src="/resources/jquery.min.js"></script>
<script src="/resources/jquery-ui.min.js"></script>
<link rel="stylesheet" href="/resources/jquery-ui.css">
<head>
<meta charset="UTF-8">
<title>Log Viewer</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div id="darkModeToggle">
<label class="dark-mode-label">
<span class="label-text">Dark Mode</span>
<input type="checkbox" id="darkModeCheckbox">
<span class="slider"></span>
</label>
</div>
<div id="sidebar">
<h1>Available Hashes</h1>
<ul id="hashes"></ul>
</div>
<div id="main">
<div style="display: flex; align-items: center;">
<h2 style="margin-right: 10px;">Log Levels</h2>
<div class="separator"></div>
<div id="levels">
<span class="badge INFO selected" data-level="INFO">INFO</span>
<span class="badge WARN selected" data-level="WARN">WARN</span>
<span class="badge ERROR selected" data-level="ERROR">ERROR</span>
<span class="badge DEBUG selected" data-level="DEBUG">DEBUG</span>
</div>
</div>
<div id="utilityContainer">
<div id="timeInputs">
<label for="startTime">Start Time:</label>
<input type="datetime-local" id="startTime" name="startTime">
<label for="endTime">End Time:</label>
<input type="datetime-local" id="endTime" name="endTime">
</div>
</div>
<div id="utilityContainer">
<div style="display: flex; align-items: center;">
<button id="dumpButton">Download Visible Logs</button>
<button id="copyButton">Copy Visible Logs</button>
<label for="logCountInput" style="margin-right: 10px;">Logs to fetch:</label>
<input type="number" id="logCountInput" value="100" min="1">
</div>
</div>
<div id="utilityContainer">
<div style="display: flex; align-items: center;">
<input type="text" id="searchInput" placeholder="Search logs..." style="height: 1.5em; padding: 0.2em;"/>
</div>
</div>
<table id="logs-table">
<thead>
<tr>
<th class="hash-column">Hash</th>
<th class="timestamp-column">Timestamp</th>
<th class="level-column">Level</th>
<th>Message</th>
</tr>
</thead>
<tbody id="logs"></tbody>
</table>
</div>
<script>
const logsElement = document.getElementById('logs');
const hashesList = document.getElementById('hashes');
const dumpButton = document.getElementById('dumpButton');
const searchInput = document.getElementById('searchInput');
const copyButton = document.getElementById('copyButton');
let selectedHashes = new Set();
let selectedLevels = new Set(['INFO', 'WARN', 'ERROR', 'DEBUG']);
let hashColors = {};
function getPastelColor(hashString) {
let hash = 0;
for (let i = 0; i < hashString.length; i++) {
const char = hashString.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
const hue = Math.abs(hash % 360);
const saturation = 65 + (hash % 20);
const lightness = 55 + (hash % 15);
return `hsla(${hue}, ${saturation}%, ${lightness}%, 0.9)`;
}
function fetchHashes() {
fetch('/api/hashes')
.then(response => response.json())
.then(hashes => {
hashesList.innerHTML = '';
hashes.forEach(hash => {
if (!hashColors[hash]) {
hashColors[hash] = getPastelColor(hash);
}
const li = document.createElement('li');
li.innerHTML = `
<span class="hash-badge" style="background-color: ${hashColors[hash]};"> </span>
<span class="hash-text">${hash}</span>
`;
if (selectedHashes.has(hash)) {
li.classList.add('selected');
}
li.onclick = () => {
toggleHashSelection(hash, li);
};
hashesList.appendChild(li);
});
});
}
function toggleHashSelection(hash, element) {
if (selectedHashes.has(hash)) {
selectedHashes.delete(hash);
element.classList.remove('selected');
} else {
selectedHashes.add(hash);
element.classList.add('selected');
}
fetchLogs();
}
document.querySelectorAll('#levels .badge').forEach(badge => {
const level = badge.getAttribute('data-level');
badge.onclick = () => {
if (selectedLevels.has(level)) {
selectedLevels.delete(level);
badge.classList.remove('selected');
} else {
selectedLevels.add(level);
badge.classList.add('selected');
}
fetchLogs();
};
});
function fetchLogs() {
if (selectedHashes.size === 0) {
logsElement.innerHTML = '<tr><td colspan="4">No hashes selected.</td></tr>';
return;
}
const logCount = parseInt(document.getElementById('logCountInput').value) || 100;
const startTimeInput = $("#startTime").val();
const endTimeInput = $("#endTime").val();
const startTimeStr = startTimeInput.length === 16 ? startTimeInput + ':00' : startTimeInput;
const endTimeStr = endTimeInput.length === 16 ? endTimeInput + ':00' : endTimeInput;
const startTime = new Date(startTimeStr);
const endTime = new Date(endTimeStr);
const adjustedStartTime = new Date(startTime.getTime() - 24 * 60 * 60 * 1000);
const adjustedEndTime = new Date(endTime.getTime() + 24 * 60 * 60 * 1000);
const adjustedStartTimeStr = adjustedStartTime.toISOString().slice(0, 19);
const adjustedEndTimeStr = adjustedEndTime.toISOString().slice(0, 19);
const promises = [];
selectedHashes.forEach(hash => {
const url = `/api/logs/${hash}?count=${logCount}&start=${encodeURIComponent(adjustedStartTimeStr)}&end=${encodeURIComponent(adjustedEndTimeStr)}`;
const promise = fetch(url)
.then(response => response.json())
.then(logs => logs);
promises.push(promise);
});
Promise.all(promises).then(results => {
logsElement.innerHTML = '';
const allLogs = results.flat();
const searchQuery = searchInput.value.toLowerCase();
const filteredLogs = allLogs.filter(log =>
(selectedLevels.size === 0 || selectedLevels.has(log.level)) &&
(!searchQuery || log.message.toLowerCase().includes(searchQuery))
);
filteredLogs.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
filteredLogs.forEach(log => {
const row = document.createElement('tr');
const date = new Date(log.timestamp);
const formattedTimestamp = date.toLocaleString(undefined, {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
row.innerHTML = `
<td class="hash-column">
<span class="hash-badge-table" style="background-color: ${hashColors[log.hash]};">${log.hash}</span>
</td>
<td class="timestamp-column">${formattedTimestamp}</td>
<td class="level-column">
<span class="level-label level-${log.level}">${log.level}</span>
</td>
<td>${log.message}</td>
`;
logsElement.appendChild(row);
});
});
}
copyButton.onclick = () => {
const rows = logsElement.querySelectorAll('tr');
if (rows.length === 0) {
alert('No logs to copy.');
return;
}
let clipboardContent = 'Hash\tLevel\tTimestamp\tMessage\n';
rows.forEach(row => {
const cols = row.querySelectorAll('td');
const data = [];
cols.forEach(col => {
data.push(col.textContent.trim());
});
clipboardContent += data.join('\t') + '\n';
});
navigator.clipboard.writeText(clipboardContent).then(() => {
alert('Logs copied to clipboard.');
}, () => {
alert('Failed to copy logs to clipboard.');
});
};
dumpButton.onclick = () => {
const rows = logsElement.querySelectorAll('tr');
if (rows.length === 0) {
alert('No logs to download.');
return;
}
let csvContent = 'Hash,Level,Timestamp,Message\n';
rows.forEach(row => {
const cols = row.querySelectorAll('td');
const data = [];
cols.forEach(col => data.push('"' + col.innerText.replace(/"/g, '""') + '"'));
csvContent += data.join(',') + '\n';
});
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.setAttribute('href', url);
link.setAttribute('download', 'logs.csv');
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
searchInput.addEventListener('input', () => {
fetchLogs();
});
const darkModeCheckbox = document.getElementById('darkModeCheckbox');
const bodyElement = document.body;
if (localStorage.getItem('darkMode') === 'enabled') {
bodyElement.classList.add('dark-mode');
darkModeCheckbox.checked = true;
}
darkModeCheckbox.addEventListener('change', () => {
if (darkModeCheckbox.checked) {
bodyElement.classList.add('dark-mode');
localStorage.setItem('darkMode', 'enabled');
} else {
bodyElement.classList.remove('dark-mode');
localStorage.setItem('darkMode', 'disabled');
}
});
async function fetchDateRange() {
try {
const response = await fetch('/api/date_range');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (!data.min_date || !data.max_date) {
throw new Error('Server returned invalid date range');
}
let minDate = new Date(data.min_date);
let maxDate = new Date(data.max_date);
if (isNaN(minDate.getTime()) || isNaN(maxDate.getTime())) {
throw new Error('Invalid date parsed from server data');
}
if (maxDate.getTime() - minDate.getTime() < 3600000) {
minDate = new Date(maxDate.getTime() - 24 * 3600000);
console.warn('Date range too narrow, adjusting minDate to 24 hours before maxDate');
}
const minDateStr = minDate.toISOString().slice(0, 16);
const maxDateStr = maxDate.toISOString().slice(0, 16);
$("#startTime").attr('min', minDateStr);
$("#startTime").attr('max', maxDateStr);
$("#endTime").attr('min', minDateStr);
$("#endTime").attr('max', maxDateStr);
if (!$("#startTime").val()) {
$("#startTime").val(minDateStr);
}
if (!$("#endTime").val()) {
$("#endTime").val(maxDateStr);
}
} catch (error) {
console.error('Error in fetchDateRange:', error);
const fallbackMinDate = new Date(Date.now() - 60 * 1000);
const fallbackMaxDate = new Date();
const minDateStr = fallbackMinDate.toISOString().slice(0, 16);
const maxDateStr = fallbackMaxDate.toISOString().slice(0, 16);
$("#startTime").attr('min', minDateStr);
$("#startTime").attr('max', maxDateStr);
$("#endTime").attr('min', minDateStr);
$("#endTime").attr('max', maxDateStr);
if (!$("#startTime").val()) {
$("#startTime").val(minDateStr);
}
if (!$("#endTime").val()) {
$("#endTime").val(maxDateStr);
}
}
}
$(async function () {
await fetchDateRange();
$("#startTime, #endTime").on("change", function () {
fetchLogs();
});
fetchLogs();
});
fetchHashes();
setInterval(fetchHashes, 3000);
setInterval(fetchLogs, 1000);
</script>
</body>
</html>