new_functions #2

Merged
gru merged 8 commits from new_functions into master 2025-11-04 09:04:37 +01:00
2 changed files with 172 additions and 125 deletions
Showing only changes of commit 3e7861f489 - Show all commits

View File

@@ -1,82 +1,89 @@
/** /**
* HAProxy Logs Management * HAProxy Logs Management
* Pagination, filtering, and formatting of logs * Pagination, filtering, and proper formatting
*/ */
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
let currentPage = 1; let currentPage = 1;
let perPage = 50; let perPage = 50;
let totalLogs = parseInt(document.getElementById('total_count').textContent); let totalLogs = parseInt(document.getElementById('total_count').textContent);
let filterRegex = null;
let wrapEnabled = false;
let allLoadedLogs = []; let allLoadedLogs = [];
let excludePhrases = [];
const logsContainer = document.getElementById('logs_container'); const logsContainer = document.getElementById('logs_container');
const searchFilter = document.getElementById('search_filter'); const searchFilter = document.getElementById('search_filter');
const excludeFilter = document.getElementById('exclude_filter');
const excludeBtn = document.getElementById('exclude_btn');
const perPageSelect = document.getElementById('logs_per_page'); const perPageSelect = document.getElementById('logs_per_page');
const refreshBtn = document.getElementById('refresh_logs_btn'); const refreshBtn = document.getElementById('refresh_logs_btn');
const prevBtn = document.getElementById('prev_btn'); const prevBtn = document.getElementById('prev_btn');
const nextBtn = document.getElementById('next_btn'); const nextBtn = document.getElementById('next_btn');
const loadAllBtn = document.getElementById('load_all_btn'); const loadAllBtn = document.getElementById('load_all_btn');
const clearFilterBtn = document.getElementById('clear_filter_btn'); const clearFilterBtn = document.getElementById('clear_filter_btn');
const toggleWrapBtn = document.getElementById('toggle_wrap_btn');
const loadedSpan = document.getElementById('loaded_count'); const loadedSpan = document.getElementById('loaded_count');
const matchSpan = document.getElementById('match_count'); const matchSpan = document.getElementById('match_count');
const currentPageSpan = document.getElementById('current_page'); const currentPageSpan = document.getElementById('current_page');
const totalPagesSpan = document.getElementById('total_pages'); const totalPagesSpan = document.getElementById('total_pages');
const logsWrapper = document.getElementById('logs_container_wrapper');
// Event Listeners // Event Listeners
searchFilter.addEventListener('keyup', function() { searchFilter.addEventListener('keyup', debounce(function() {
currentPage = 1; currentPage = 1;
try { applyFilters();
filterRegex = new RegExp(this.value, 'gi'); }, 300));
} catch(e) {
filterRegex = null; excludeBtn.addEventListener('click', function() {
const phrase = excludeFilter.value.trim();
if (phrase) {
if (!excludePhrases.includes(phrase)) {
excludePhrases.push(phrase);
updateExcludeUI();
applyFilters();
} }
applyFilter(); excludeFilter.value = '';
}
});
excludeFilter.addEventListener('keypress', function(e) {
if (e.key === 'Enter') excludeBtn.click();
}); });
clearFilterBtn.addEventListener('click', function() { clearFilterBtn.addEventListener('click', function() {
searchFilter.value = ''; searchFilter.value = '';
filterRegex = null; excludePhrases = [];
excludeFilter.value = '';
updateExcludeUI();
currentPage = 1; currentPage = 1;
applyFilter(); applyFilters();
}); });
toggleWrapBtn.addEventListener('click', function() { perPageSelect.addEventListener('change', function() {
wrapEnabled = !wrapEnabled; perPage = parseInt(this.value);
const pre = document.getElementById('logs_container');
pre.style.whiteSpace = wrapEnabled ? 'pre-wrap' : 'pre';
toggleWrapBtn.classList.toggle('active', wrapEnabled);
});
perPageSelect.addEventListener('change', function(e) {
perPage = parseInt(e.target.value);
currentPage = 1; currentPage = 1;
applyFilter(); applyFilters();
}); });
refreshBtn.addEventListener('click', function() { refreshBtn.addEventListener('click', function() {
currentPage = 1;
filterRegex = null;
searchFilter.value = ''; searchFilter.value = '';
excludePhrases = [];
excludeFilter.value = '';
updateExcludeUI();
currentPage = 1;
loadLogs(); loadLogs();
}); });
prevBtn.addEventListener('click', function() { prevBtn.addEventListener('click', function() {
if (currentPage > 1) { if (currentPage > 1) {
currentPage--; currentPage--;
applyFilter(); applyFilters();
} }
}); });
nextBtn.addEventListener('click', function() { nextBtn.addEventListener('click', function() {
const filtered = filterRegex ? allLoadedLogs.filter(log => filterRegex.test(log)) : allLoadedLogs; const filtered = getFilteredLogs();
const totalPages = Math.ceil(filtered.length / perPage); const totalPages = Math.ceil(filtered.length / perPage);
if (currentPage < totalPages) { if (currentPage < totalPages) {
currentPage++; currentPage++;
applyFilter(); applyFilters();
} }
}); });
@@ -84,142 +91,177 @@ document.addEventListener('DOMContentLoaded', function() {
perPage = totalLogs; perPage = totalLogs;
currentPage = 1; currentPage = 1;
perPageSelect.value = totalLogs; perPageSelect.value = totalLogs;
applyFilter(); applyFilters();
}); });
/**
* Debounce function
*/
function debounce(func, wait) {
let timeout;
return function() {
clearTimeout(timeout);
timeout = setTimeout(func, wait);
};
}
/** /**
* Load logs from API * Load logs from API
*/ */
function loadLogs() { function loadLogs() {
console.log(`[Logs] Loading page ${currentPage} with ${perPage} per page`); logsContainer.innerHTML = '<tr><td class="text-center text-muted py-4">Loading logs...</td></tr>';
fetch('/api/logs', { fetch('/api/logs', {
method: 'POST', method: 'POST',
headers: { headers: { 'Content-Type': 'application/json' },
'Content-Type': 'application/json',
},
body: JSON.stringify({ body: JSON.stringify({
page: currentPage, page: currentPage,
per_page: perPage per_page: perPage
}) })
}) })
.then(response => response.json()) .then(r => r.json())
.then(data => { .then(data => {
if (data.success) { if (data.success) {
allLoadedLogs = data.logs; allLoadedLogs = data.logs;
loadedSpan.textContent = data.logs.length; loadedSpan.textContent = data.logs.length;
updatePagination(data); applyFilters();
applyFilter();
console.log(`[Logs] Successfully loaded page ${data.page}/${Math.ceil(data.total / data.per_page)}`);
} else { } else {
showError(data.error || 'Unknown error'); showError(data.error);
} }
}) })
.catch(error => { .catch(e => {
console.error('[Logs] Error loading logs:', error); console.error('Error:', e);
showError('Failed to load logs. Please try again.'); showError('Failed to load logs');
}); });
} }
/** /**
* Apply filter and display logs * Get filtered logs
*/ */
function applyFilter() { function getFilteredLogs() {
let filtered = allLoadedLogs; let filtered = allLoadedLogs;
if (filterRegex) { // Apply search filter
filtered = allLoadedLogs.filter(log => { if (searchFilter.value.trim()) {
const fullLog = `${log.timestamp || ''} ${log.source || ''} ${log.message || ''}`; const query = searchFilter.value.toLowerCase();
return filterRegex.test(fullLog); filtered = filtered.filter(log => {
const text = `${log.timestamp || ''} ${log.source || ''} ${log.message || ''}`.toLowerCase();
return text.includes(query);
}); });
filterRegex.lastIndex = 0; // Reset regex for next test
} }
// Apply exclude phrases
if (excludePhrases.length > 0) {
filtered = filtered.filter(log => {
const text = `${log.timestamp || ''} ${log.source || ''} ${log.message || ''}`;
return !excludePhrases.some(phrase => text.includes(phrase));
});
}
return filtered;
}
/**
* Apply all filters and render
*/
function applyFilters() {
const filtered = getFilteredLogs();
matchSpan.textContent = filtered.length; matchSpan.textContent = filtered.length;
const totalPages = Math.ceil(filtered.length / perPage) || 1; const totalPages = Math.ceil(filtered.length / perPage) || 1;
totalPagesSpan.textContent = totalPages; totalPagesSpan.textContent = totalPages;
currentPageSpan.textContent = currentPage;
const offset = (currentPage - 1) * perPage; const offset = (currentPage - 1) * perPage;
const paginated = filtered.slice(offset, offset + perPage); const paginated = filtered.slice(offset, offset + perPage);
renderLogs(paginated, filtered); renderLogs(paginated);
prevBtn.disabled = currentPage === 1; prevBtn.disabled = currentPage === 1;
nextBtn.disabled = offset + perPage >= filtered.length; nextBtn.disabled = offset + perPage >= filtered.length;
} }
/** /**
* Render logs with syntax highlighting * Render logs as table rows
*/ */
function renderLogs(logs, allFiltered) { function renderLogs(logs) {
if (!logs || logs.length === 0) { if (!logs || logs.length === 0) {
logsContainer.textContent = '(No logs available)'; logsContainer.innerHTML = '<tr><td class="text-center text-muted py-4">No logs matching criteria</td></tr>';
return; return;
} }
const output = logs.map((entry, idx) => { logsContainer.innerHTML = logs.map((entry, idx) => {
const timestamp = entry.timestamp || 'N/A'; const timestamp = entry.timestamp || '-';
const source = entry.source || 'N/A'; const source = entry.source || '-';
const message = entry.message || 'N/A'; const message = entry.message || '-';
// Format with colors // Color code by status code if present
let formatted = ''; let statusClass = '';
if (message.includes('200')) statusClass = 'table-success';
else if (message.includes('404')) statusClass = 'table-warning';
else if (message.includes('500')) statusClass = 'table-danger';
// Timestamp (blue) return `
formatted += `\x1b[36m${timestamp}\x1b[0m `; <tr class="${statusClass}" style="font-family: monospace; font-size: 11px;">
<td>
<small class="text-info">${escapeHtml(timestamp)}</small><br>
<small class="text-warning">${escapeHtml(source)}</small><br>
<small style="color: #ddd; word-break: break-all;">${escapeHtml(message)}</small>
</td>
</tr>
`;
}).join('');
}
// Source IP (yellow) /**
formatted += `\x1b[33m${source}\x1b[0m `; * Update exclude UI to show active filters
*/
function updateExcludeUI() {
if (excludePhrases.length > 0) {
const tags = excludePhrases.map((phrase, idx) => `
<span class="badge bg-warning text-dark me-2" style="cursor: pointer;" onclick="window.removeExcludePhrase(${idx})">
${escapeHtml(phrase)} <i class="bi bi-x"></i>
</span>
`).join('');
// Message (white) const container = document.createElement('div');
formatted += `${message}`; container.className = 'small mt-2';
container.innerHTML = `<strong>Hiding:</strong> ${tags}`;
return `${idx + 1}. ${formatted}`; const existing = document.getElementById('exclude_ui');
}).join('\n'); if (existing) existing.remove();
// Convert ANSI-like colors to HTML-like (for better compatibility) container.id = 'exclude_ui';
logsContainer.textContent = output; excludeFilter.parentElement.parentElement.after(container);
} else {
// Highlight search matches if filter is active const existing = document.getElementById('exclude_ui');
if (filterRegex) { if (existing) existing.remove();
const text = logsContainer.textContent;
const highlighted = text.replace(filterRegex, match => `>>> ${match} <<<`);
logsContainer.textContent = highlighted;
filterRegex.lastIndex = 0;
} }
} }
/** /**
* Update pagination info * Global function to remove exclude phrase
*/ */
function updatePagination(data) { window.removeExcludePhrase = function(idx) {
const totalPages = Math.ceil(data.total / data.per_page); excludePhrases.splice(idx, 1);
currentPageSpan.textContent = data.page; updateExcludeUI();
totalPagesSpan.textContent = totalPages; applyFilters();
} };
/** /**
* Show error message * Show error
*/ */
function showError(message) { function showError(msg) {
logsContainer.textContent = `ERROR: ${escapeHtml(message)}`; logsContainer.innerHTML = `<tr><td class="alert alert-danger mb-0">${escapeHtml(msg)}</td></tr>`;
} }
/** /**
* Escape HTML * Escape HTML
*/ */
function escapeHtml(text) { function escapeHtml(text) {
const map = { const map = {'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;'};
'&': '&amp;', return (text || '').replace(/[&<>"']/g, m => map[m]);
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, m => map[m]);
} }
// Initial load
loadLogs(); loadLogs();
}); });

View File

@@ -27,67 +27,72 @@
</div> </div>
{% endif %} {% endif %}
<!-- Search & Filter Controls --> <!-- Controls Row -->
<div class="row mb-3 g-2"> <div class="row g-2 mb-3">
<div class="col-md-4"> <div class="col-md-3">
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
<span class="input-group-text"><i class="bi bi-search"></i></span> <span class="input-group-text"><i class="bi bi-search"></i></span>
<input type="text" class="form-control" id="search_filter" placeholder="Filter logs (regex)..."> <input type="text" class="form-control" id="search_filter" placeholder="Search logs...">
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="col-md-2">
<label for="logs_per_page" class="form-label form-label-sm">Logs Per Page</label> <button class="btn btn-sm btn-outline-secondary w-100" id="clear_filter_btn" title="Clear search">
<i class="bi bi-x-circle me-1"></i>Clear
</button>
</div>
<div class="col-md-2">
<select class="form-select form-select-sm" id="logs_per_page"> <select class="form-select form-select-sm" id="logs_per_page">
<option value="25" selected>25</option> <option value="25" selected>25 per page</option>
<option value="50">50</option> <option value="50">50 per page</option>
<option value="100">100</option> <option value="100">100 per page</option>
<option value="200">200</option> <option value="200">200 per page</option>
</select> </select>
</div> </div>
<div class="col-md-2"> <div class="col-md-2">
<button class="btn btn-primary btn-sm w-100" id="refresh_logs_btn"> <button class="btn btn-sm btn-primary w-100" id="refresh_logs_btn">
<i class="bi bi-arrow-clockwise me-1"></i>Refresh <i class="bi bi-arrow-clockwise"></i>
</button> </button>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<div class="btn-group btn-group-sm w-100" role="group"> <div class="input-group input-group-sm">
<button type="button" class="btn btn-outline-secondary" id="clear_filter_btn"> <span class="input-group-text"><i class="bi bi-funnel"></i></span>
<i class="bi bi-x-circle me-1"></i>Clear <input type="text" class="form-control" id="exclude_filter" placeholder="Hide phrase (e.g. /stats)">
</button> <button class="btn btn-outline-warning btn-sm" id="exclude_btn" type="button">Hide</button>
<button type="button" class="btn btn-outline-secondary" id="toggle_wrap_btn">
<i class="bi bi-arrow-left-right me-1"></i>Wrap
</button>
</div> </div>
</div> </div>
</div> </div>
<!-- Statistics --> <!-- Statistics -->
<div class="alert alert-info small"> <div class="alert alert-info small mb-3">
<i class="bi bi-info-circle me-2"></i> <i class="bi bi-info-circle me-2"></i>
<strong>Loaded:</strong> <span id="loaded_count">{{ loaded_count|default(0) }}</span> /
<strong>Total:</strong> <span id="total_count">{{ total_logs|default(0) }}</span> logs | <strong>Total:</strong> <span id="total_count">{{ total_logs|default(0) }}</span> logs |
<strong>Matches:</strong> <span id="match_count">0</span> <strong>Loaded:</strong> <span id="loaded_count">{{ loaded_count|default(0) }}</span> |
<strong>Displayed:</strong> <span id="match_count">0</span>
</div> </div>
<!-- Logs Container (Dark Theme) --> <!-- Logs Container (Dark Theme) -->
<div id="logs_container_wrapper" style="max-height: 700px; overflow-y: auto; border: 1px solid #444; border-radius: 4px; background: #1a1a1a; position: relative;"> <div id="logs_container_wrapper" style="max-height: 650px; overflow-y: auto; border: 1px solid #444; border-radius: 4px; background: #0d1117;">
<pre id="logs_container" style="margin: 0; padding: 12px; color: #e0e0e0; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace; font-size: 12px; line-height: 1.6; background: #1a1a1a; white-space: pre-wrap; word-wrap: break-word;">Loading logs...</pre> <table class="table table-sm table-dark mb-0" id="logs_table">
<tbody id="logs_container">
<tr><td class="text-center text-muted py-4">Loading logs...</td></tr>
</tbody>
</table>
</div> </div>
<!-- Pagination --> <!-- Pagination -->
<div class="mt-3 d-flex justify-content-between align-items-center flex-wrap gap-2"> <div class="mt-3 d-flex justify-content-between align-items-center flex-wrap gap-2">
<small class="text-muted"> <small class="text-muted">
Page <span id="current_page">1</span> of <span id="total_pages">1</span> Page <span id="current_page">1</span> / <span id="total_pages">1</span>
</small> </small>
<div class="btn-group" role="group"> <div class="btn-group btn-group-sm" role="group">
<button class="btn btn-sm btn-outline-primary" id="prev_btn" disabled> <button class="btn btn-outline-primary" id="prev_btn" disabled>
<i class="bi bi-chevron-left"></i> Previous <i class="bi bi-chevron-left"></i> Prev
</button> </button>
<button class="btn btn-sm btn-outline-primary" id="next_btn"> <button class="btn btn-outline-primary" id="next_btn">
Next <i class="bi bi-chevron-right"></i> Next <i class="bi bi-chevron-right"></i>
</button> </button>
<button class="btn btn-sm btn-outline-secondary" id="load_all_btn"> <button class="btn btn-outline-secondary" id="load_all_btn">
<i class="bi bi-download me-1"></i>Load All <i class="bi bi-download"></i> All
</button> </button>
</div> </div>
</div> </div>