You've already forked caddy-opnsense-blocker
Polish requests log mobile controls
This commit is contained in:
@@ -917,7 +917,8 @@ const queryLogHTML = `<!doctype html>
|
||||
.panel { background: #111827; border: 1px solid #334155; border-radius: .85rem; overflow: hidden; }
|
||||
.toolbar { display: flex; justify-content: space-between; align-items: center; gap: 1rem; margin-bottom: .75rem; flex-wrap: wrap; }
|
||||
.toolbar-actions { display: flex; align-items: center; gap: .65rem; flex-wrap: wrap; }
|
||||
.page-status { color: #cbd5e1; font-size: .92rem; }
|
||||
.pager { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
|
||||
.page-status { color: #cbd5e1; font-size: .92rem; min-width: 0; }
|
||||
.controls-panel summary { cursor: pointer; padding: 1rem; font-weight: 700; color: #e2e8f0; list-style: none; user-select: none; }
|
||||
.controls-panel summary::-webkit-details-marker { display: none; }
|
||||
.controls-panel[open] summary { border-bottom: 1px solid #334155; }
|
||||
@@ -932,6 +933,10 @@ const queryLogHTML = `<!doctype html>
|
||||
.field.inline-toggle input { width: auto; }
|
||||
.panel-actions { display: flex; align-items: center; gap: .65rem; flex-wrap: wrap; }
|
||||
.panel-actions .spacer { flex: 1; }
|
||||
.columns-field { grid-column: 1 / -1; }
|
||||
.columns-grid { display: flex; flex-wrap: wrap; gap: .5rem; }
|
||||
.column-chip { display: inline-flex; align-items: center; gap: .35rem; padding: .4rem .65rem; border-radius: 999px; border: 1px solid #334155; background: #0f172a; color: #cbd5e1; font-size: .85rem; }
|
||||
.column-chip input { width: auto; margin: 0; }
|
||||
.table-panel { padding: 1rem; }
|
||||
.table-shell { overflow: hidden; }
|
||||
table { width: 100%; border-collapse: collapse; table-layout: fixed; }
|
||||
@@ -949,7 +954,7 @@ const queryLogHTML = `<!doctype html>
|
||||
.col-status { width: 4.75rem; }
|
||||
.col-state { width: 6.5rem; }
|
||||
.col-reason { width: 11rem; overflow: hidden; }
|
||||
.col-actions { width: 7.5rem; }
|
||||
.col-actions { width: 4rem; }
|
||||
.col-request { width: auto; overflow: hidden; }
|
||||
.request-text, .reason-text { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.status { display: inline-block; padding: .15rem .45rem; border-radius: 999px; font-size: .8rem; background: #1e293b; }
|
||||
@@ -969,8 +974,10 @@ const queryLogHTML = `<!doctype html>
|
||||
button.secondary { background: #475569; }
|
||||
button.danger { background: #dc2626; }
|
||||
button[disabled] { opacity: .5; cursor: default; }
|
||||
.icon-button { min-width: 2.15rem; width: 2.15rem; height: 2.15rem; padding: 0; font-size: 1rem; }
|
||||
.pager-button .button-label { display: inline; }
|
||||
.actions { display: block; }
|
||||
.actions button { display: block; width: 100%; min-width: 0; }
|
||||
.actions button { display: block; width: 2.15rem; height: 2.15rem; min-width: 0; margin-left: auto; padding: 0; }
|
||||
.actions .muted { display: block; text-align: center; }
|
||||
.ip-cell { display: flex; align-items: center; gap: .45rem; min-width: 0; max-width: 100%; }
|
||||
.ip-link { display: block; flex: 1 1 auto; min-width: 0; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
@@ -997,6 +1004,9 @@ const queryLogHTML = `<!doctype html>
|
||||
@media (max-width: 720px) {
|
||||
header { padding: .9rem 1rem; }
|
||||
main { padding: 1rem; }
|
||||
.toolbar-actions { justify-content: space-between; }
|
||||
.pager { width: 100%; justify-content: space-between; }
|
||||
.pager-button .button-label { display: none; }
|
||||
.table-shell { overflow-x: auto; }
|
||||
table { min-width: 980px; }
|
||||
}
|
||||
@@ -1113,6 +1123,7 @@ const queryLogHTML = `<!doctype html>
|
||||
<div class="field">
|
||||
<label for="page-size">Rows per page</label>
|
||||
<select id="page-size">
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
<option value="200">200</option>
|
||||
@@ -1122,6 +1133,18 @@ const queryLogHTML = `<!doctype html>
|
||||
<input id="auto-refresh-toggle" type="checkbox" onchange="toggleAutoRefresh()">
|
||||
<label for="auto-refresh-toggle">Auto refresh</label>
|
||||
</div>
|
||||
<div class="field columns-field">
|
||||
<label>Columns</label>
|
||||
<div class="columns-grid">
|
||||
<label class="column-chip"><input id="column-time" type="checkbox" onchange="applyColumnChanges()">Time</label>
|
||||
<label class="column-chip"><input id="column-method" type="checkbox" onchange="applyColumnChanges()">Method</label>
|
||||
<label class="column-chip"><input id="column-source" type="checkbox" onchange="applyColumnChanges()">Source</label>
|
||||
<label class="column-chip"><input id="column-status" type="checkbox" onchange="applyColumnChanges()">Status</label>
|
||||
<label class="column-chip"><input id="column-state" type="checkbox" onchange="applyColumnChanges()">State</label>
|
||||
<label class="column-chip"><input id="column-reason" type="checkbox" onchange="applyColumnChanges()">Reason</label>
|
||||
<label class="column-chip"><input id="column-actions" type="checkbox" onchange="applyColumnChanges()">Actions</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="panel-actions">
|
||||
<button type="button" onclick="applyFilters()">Apply</button>
|
||||
@@ -1138,25 +1161,27 @@ const queryLogHTML = `<!doctype html>
|
||||
<div class="muted">Click an IP to open its detail page</div>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<button class="secondary" type="button" onclick="refreshNow()">Refresh now</button>
|
||||
<div class="page-status" id="page-status">Page 1</div>
|
||||
<button class="secondary" type="button" id="prev-page" onclick="goToPreviousPage()">Previous</button>
|
||||
<button class="secondary" type="button" id="next-page" onclick="goToNextPage()">Next</button>
|
||||
<button class="secondary pager-button" type="button" onclick="refreshNow()" title="Refresh now" aria-label="Refresh now">⟳<span class="button-label">Refresh</span></button>
|
||||
<div class="pager">
|
||||
<button class="secondary icon-button pager-button" type="button" data-prev-page onclick="goToPreviousPage()" title="Previous page" aria-label="Previous page">←<span class="button-label">Previous</span></button>
|
||||
<div class="page-status" data-page-status>Page 1</div>
|
||||
<button class="secondary icon-button pager-button" type="button" data-next-page onclick="goToNextPage()" title="Next page" aria-label="Next page">→<span class="button-label">Next</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-shell">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-time sortable" data-sort="time" onclick="applySort('time')">Time<span class="sort-indicator" id="sort-time"></span></th>
|
||||
<th class="col-ip sortable" data-sort="ip" onclick="applySort('ip')">IP<span class="sort-indicator" id="sort-ip"></span></th>
|
||||
<th class="col-method sortable" data-sort="method" onclick="applySort('method')">Method<span class="sort-indicator" id="sort-method"></span></th>
|
||||
<th class="col-source sortable" data-sort="source" onclick="applySort('source')">Source<span class="sort-indicator" id="sort-source"></span></th>
|
||||
<th class="col-request sortable" data-sort="request" onclick="applySort('request')">Request<span class="sort-indicator" id="sort-request"></span></th>
|
||||
<th class="col-status sortable" data-sort="status" onclick="applySort('status')">Status<span class="sort-indicator" id="sort-status"></span></th>
|
||||
<th class="col-state sortable" data-sort="state" onclick="applySort('state')">State<span class="sort-indicator" id="sort-state"></span></th>
|
||||
<th class="col-reason sortable" data-sort="reason" onclick="applySort('reason')">Reason<span class="sort-indicator" id="sort-reason"></span></th>
|
||||
<th class="col-actions">Actions</th>
|
||||
<th class="col-time sortable" data-sort="time" data-column="time" onclick="applySort('time')">Time<span class="sort-indicator" id="sort-time"></span></th>
|
||||
<th class="col-ip sortable" data-sort="ip" data-column="ip" onclick="applySort('ip')">IP<span class="sort-indicator" id="sort-ip"></span></th>
|
||||
<th class="col-method sortable" data-sort="method" data-column="method" onclick="applySort('method')">Method<span class="sort-indicator" id="sort-method"></span></th>
|
||||
<th class="col-source sortable" data-sort="source" data-column="source" onclick="applySort('source')">Source<span class="sort-indicator" id="sort-source"></span></th>
|
||||
<th class="col-request sortable" data-sort="request" data-column="request" onclick="applySort('request')">Request<span class="sort-indicator" id="sort-request"></span></th>
|
||||
<th class="col-status sortable" data-sort="status" data-column="status" onclick="applySort('status')">Status<span class="sort-indicator" id="sort-status"></span></th>
|
||||
<th class="col-state sortable" data-sort="state" data-column="state" onclick="applySort('state')">State<span class="sort-indicator" id="sort-state"></span></th>
|
||||
<th class="col-reason sortable" data-sort="reason" data-column="reason" onclick="applySort('reason')">Reason<span class="sort-indicator" id="sort-reason"></span></th>
|
||||
<th class="col-actions" data-column="actions">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="events-body">
|
||||
@@ -1164,6 +1189,16 @@ const queryLogHTML = `<!doctype html>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<div class="muted">Use the controls above to tune the view.</div>
|
||||
<div class="toolbar-actions">
|
||||
<div class="pager">
|
||||
<button class="secondary icon-button pager-button" type="button" data-prev-page onclick="goToPreviousPage()" title="Previous page" aria-label="Previous page">←<span class="button-label">Previous</span></button>
|
||||
<div class="page-status" data-page-status>Page 1</div>
|
||||
<button class="secondary icon-button pager-button" type="button" data-next-page onclick="goToNextPage()" title="Next page" aria-label="Next page">→<span class="button-label">Next</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<script>
|
||||
@@ -1175,7 +1210,9 @@ const queryLogHTML = `<!doctype html>
|
||||
let botFilter = loadStringPreference('cob.requests.botFilter', 'all');
|
||||
let sortBy = loadStringPreference('cob.requests.sortBy', 'time');
|
||||
let sortDir = loadStringPreference('cob.requests.sortDir', 'desc');
|
||||
let pageSize = loadStringPreference('cob.requests.pageSize', '100');
|
||||
const defaultVisibleColumns = { time: true, method: true, source: true, status: true, state: true, reason: true, actions: true };
|
||||
let visibleColumns = loadColumnPreferences();
|
||||
let pageSize = loadStringPreference('cob.requests.pageSizeV2', '25');
|
||||
let autoRefresh = loadBooleanPreference('cob.requests.autoRefresh', false);
|
||||
let panelOpen = loadBooleanPreference('cob.requests.panelOpen', false);
|
||||
let currentPage = 1;
|
||||
@@ -1207,6 +1244,29 @@ const queryLogHTML = `<!doctype html>
|
||||
localStorage.setItem(key, value);
|
||||
}
|
||||
|
||||
function loadColumnPreferences() {
|
||||
const next = { ...defaultVisibleColumns };
|
||||
try {
|
||||
const raw = localStorage.getItem('cob.requests.visibleColumns');
|
||||
if (!raw) {
|
||||
return next;
|
||||
}
|
||||
const parsed = JSON.parse(raw);
|
||||
for (const key of Object.keys(defaultVisibleColumns)) {
|
||||
if (typeof parsed[key] === 'boolean') {
|
||||
next[key] = parsed[key];
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return next;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function saveColumnPreferences() {
|
||||
localStorage.setItem('cob.requests.visibleColumns', JSON.stringify(visibleColumns));
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? '')
|
||||
.replaceAll('&', '&')
|
||||
@@ -1273,10 +1333,10 @@ const queryLogHTML = `<!doctype html>
|
||||
function renderActions(item) {
|
||||
const actions = item.actions || {};
|
||||
if (actions.can_unblock) {
|
||||
return '<div class="actions"><button class="secondary" data-ip="' + escapeHtml(item.client_ip) + '" onclick="sendAction(this.dataset.ip, \'unblock\', \'Reason for manual unblock\')">Unblock</button></div>';
|
||||
return '<div class="actions"><button class="secondary icon-button" data-ip="' + escapeHtml(item.client_ip) + '" title="Unblock this IP" aria-label="Unblock this IP" onclick="sendAction(this.dataset.ip, \'unblock\', \'Reason for manual unblock\')">🔓</button></div>';
|
||||
}
|
||||
if (actions.can_block) {
|
||||
return '<div class="actions"><button class="danger" data-ip="' + escapeHtml(item.client_ip) + '" onclick="sendAction(this.dataset.ip, \'block\', \'Reason for manual block\')">Block</button></div>';
|
||||
return '<div class="actions"><button class="danger icon-button" data-ip="' + escapeHtml(item.client_ip) + '" title="Block this IP" aria-label="Block this IP" onclick="sendAction(this.dataset.ip, \'block\', \'Reason for manual block\')">⛔</button></div>';
|
||||
}
|
||||
return '<div class="actions"><span class="muted">—</span></div>';
|
||||
}
|
||||
@@ -1292,6 +1352,34 @@ const queryLogHTML = `<!doctype html>
|
||||
return '';
|
||||
}
|
||||
|
||||
function applyColumnControls() {
|
||||
for (const key of Object.keys(defaultVisibleColumns)) {
|
||||
const input = document.getElementById('column-' + key);
|
||||
if (input) {
|
||||
input.checked = visibleColumns[key] !== false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applyVisibleColumns() {
|
||||
for (const key of Object.keys(defaultVisibleColumns)) {
|
||||
const visible = visibleColumns[key] !== false;
|
||||
document.querySelectorAll('[data-column="' + key + '"]').forEach(node => {
|
||||
node.style.display = visible ? '' : 'none';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function visibleColumnCount() {
|
||||
let total = 2;
|
||||
for (const key of Object.keys(defaultVisibleColumns)) {
|
||||
if (visibleColumns[key] !== false) {
|
||||
total += 1;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
function applyControls() {
|
||||
document.getElementById('source-filter').value = sourceFilter;
|
||||
document.getElementById('method-filter').value = methodFilter;
|
||||
@@ -1303,6 +1391,8 @@ const queryLogHTML = `<!doctype html>
|
||||
document.getElementById('page-size').value = pageSize;
|
||||
document.getElementById('auto-refresh-toggle').checked = autoRefresh;
|
||||
document.getElementById('options-panel').open = panelOpen;
|
||||
applyColumnControls();
|
||||
applyVisibleColumns();
|
||||
updateSortIndicators();
|
||||
updateControlsSummary();
|
||||
}
|
||||
@@ -1329,6 +1419,8 @@ const queryLogHTML = `<!doctype html>
|
||||
if (statusFilter) { parts.push('status=' + statusFilter); }
|
||||
if (stateFilter) { parts.push('state=' + stateFilter); }
|
||||
if (botFilter && botFilter !== 'all') { parts.push('bots=' + botFilter); }
|
||||
const hiddenColumns = Object.keys(defaultVisibleColumns).filter(key => visibleColumns[key] === false);
|
||||
if (hiddenColumns.length) { parts.push('hidden=' + hiddenColumns.join(',')); }
|
||||
parts.push('sort=' + sortBy + ' ' + sortDir);
|
||||
parts.push('page size=' + pageSize);
|
||||
if (autoRefresh) { parts.push('auto refresh'); }
|
||||
@@ -1343,9 +1435,19 @@ const queryLogHTML = `<!doctype html>
|
||||
saveStringPreference('cob.requests.botFilter', botFilter);
|
||||
saveStringPreference('cob.requests.sortBy', sortBy);
|
||||
saveStringPreference('cob.requests.sortDir', sortDir);
|
||||
saveStringPreference('cob.requests.pageSize', pageSize);
|
||||
saveStringPreference('cob.requests.pageSizeV2', pageSize);
|
||||
saveBooleanPreference('cob.requests.autoRefresh', autoRefresh);
|
||||
saveBooleanPreference('cob.requests.panelOpen', panelOpen);
|
||||
saveColumnPreferences();
|
||||
}
|
||||
|
||||
function readColumnControls() {
|
||||
for (const key of Object.keys(defaultVisibleColumns)) {
|
||||
const input = document.getElementById('column-' + key);
|
||||
if (input) {
|
||||
visibleColumns[key] = input.checked;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function readControls() {
|
||||
@@ -1358,6 +1460,15 @@ const queryLogHTML = `<!doctype html>
|
||||
sortDir = document.getElementById('sort-dir').value;
|
||||
pageSize = document.getElementById('page-size').value;
|
||||
autoRefresh = document.getElementById('auto-refresh-toggle').checked;
|
||||
readColumnControls();
|
||||
}
|
||||
|
||||
function applyColumnChanges() {
|
||||
readColumnControls();
|
||||
saveControls();
|
||||
applyVisibleColumns();
|
||||
updateControlsSummary();
|
||||
syncEmptyStateColspan();
|
||||
}
|
||||
|
||||
function applyFilters(event) {
|
||||
@@ -1380,8 +1491,9 @@ const queryLogHTML = `<!doctype html>
|
||||
botFilter = 'all';
|
||||
sortBy = 'time';
|
||||
sortDir = 'desc';
|
||||
pageSize = '100';
|
||||
pageSize = '25';
|
||||
autoRefresh = false;
|
||||
visibleColumns = { ...defaultVisibleColumns };
|
||||
saveControls();
|
||||
applyControls();
|
||||
currentPage = 1;
|
||||
@@ -1408,9 +1520,15 @@ const queryLogHTML = `<!doctype html>
|
||||
|
||||
function updatePager(payload) {
|
||||
const page = Number(payload.page || currentPage || 1);
|
||||
document.getElementById('page-status').textContent = 'Page ' + page + ' · ' + pageSize + ' rows';
|
||||
document.getElementById('prev-page').disabled = !payload.has_prev;
|
||||
document.getElementById('next-page').disabled = !payload.has_next;
|
||||
document.querySelectorAll('[data-page-status]').forEach(node => {
|
||||
node.textContent = 'Page ' + page + ' · ' + pageSize + ' rows';
|
||||
});
|
||||
document.querySelectorAll('[data-prev-page]').forEach(node => {
|
||||
node.disabled = !payload.has_prev;
|
||||
});
|
||||
document.querySelectorAll('[data-next-page]').forEach(node => {
|
||||
node.disabled = !payload.has_next;
|
||||
});
|
||||
}
|
||||
|
||||
function scheduleRefresh() {
|
||||
@@ -1465,25 +1583,42 @@ const queryLogHTML = `<!doctype html>
|
||||
refresh();
|
||||
}
|
||||
|
||||
function syncEmptyStateColspan() {
|
||||
const cell = document.querySelector('#events-body tr td');
|
||||
if (cell && cell.colSpan) {
|
||||
cell.colSpan = visibleColumnCount();
|
||||
}
|
||||
}
|
||||
|
||||
function renderEmptyState(message) {
|
||||
document.getElementById('events-body').innerHTML = '<tr><td colspan="' + visibleColumnCount() + '" class="muted">' + escapeHtml(message) + '</td></tr>';
|
||||
applyVisibleColumns();
|
||||
}
|
||||
|
||||
function renderEvents(payload) {
|
||||
const items = Array.isArray(payload.items) ? payload.items : [];
|
||||
const rows = items.map(item => {
|
||||
const requestLabel = item.uri || '—';
|
||||
return [
|
||||
'<tr>',
|
||||
' <td class="col-time">' + escapeHtml(formatDate(item.occurred_at)) + '</td>',
|
||||
' <td class="col-ip mono"><div class="ip-cell">' + renderBotChip(item.bot) + '<a class="ip-link" href="/ips/' + encodeURIComponent(item.client_ip) + '" title="' + escapeHtml(item.client_ip || '—') + '">' + escapeHtml(item.client_ip || '—') + '</a></div></td>',
|
||||
' <td class="col-method"><span class="method ' + escapeHtml(methodClass(item.method)) + '">' + escapeHtml(item.method || 'OTHER') + '</span></td>',
|
||||
' <td class="col-source">' + escapeHtml(item.source_name || '—') + '</td>',
|
||||
' <td class="col-request mono"><span class="request-text" title="' + escapeHtml(requestLabel) + '">' + escapeHtml(requestLabel) + '</span></td>',
|
||||
' <td class="col-status"><span class="status-code ' + escapeHtml(statusCodeClass(item.status)) + '">' + escapeHtml(String(item.status || 0)) + '</span></td>',
|
||||
' <td class="col-state"><span class="status ' + escapeHtml(item.current_state || 'observed') + '">' + escapeHtml(item.current_state || 'observed') + '</span></td>',
|
||||
' <td class="col-reason"><span class="reason-text" title="' + escapeHtml(item.decision_reason || '—') + '">' + escapeHtml(item.decision_reason || '—') + '</span></td>',
|
||||
' <td class="col-actions">' + renderActions(item) + '</td>',
|
||||
' <td class="col-time" data-column="time">' + escapeHtml(formatDate(item.occurred_at)) + '</td>',
|
||||
' <td class="col-ip mono" data-column="ip"><div class="ip-cell">' + renderBotChip(item.bot) + '<a class="ip-link" href="/ips/' + encodeURIComponent(item.client_ip) + '" title="' + escapeHtml(item.client_ip || '—') + '">' + escapeHtml(item.client_ip || '—') + '</a></div></td>',
|
||||
' <td class="col-method" data-column="method"><span class="method ' + escapeHtml(methodClass(item.method)) + '">' + escapeHtml(item.method || 'OTHER') + '</span></td>',
|
||||
' <td class="col-source" data-column="source">' + escapeHtml(item.source_name || '—') + '</td>',
|
||||
' <td class="col-request mono" data-column="request"><span class="request-text" title="' + escapeHtml(requestLabel) + '">' + escapeHtml(requestLabel) + '</span></td>',
|
||||
' <td class="col-status" data-column="status"><span class="status-code ' + escapeHtml(statusCodeClass(item.status)) + '">' + escapeHtml(String(item.status || 0)) + '</span></td>',
|
||||
' <td class="col-state" data-column="state"><span class="status ' + escapeHtml(item.current_state || 'observed') + '">' + escapeHtml(item.current_state || 'observed') + '</span></td>',
|
||||
' <td class="col-reason" data-column="reason"><span class="reason-text" title="' + escapeHtml(item.decision_reason || '—') + '">' + escapeHtml(item.decision_reason || '—') + '</span></td>',
|
||||
' <td class="col-actions" data-column="actions">' + renderActions(item) + '</td>',
|
||||
'</tr>'
|
||||
].join('');
|
||||
});
|
||||
document.getElementById('events-body').innerHTML = rows.length ? rows.join('') : '<tr><td colspan="9" class="muted">No requests match the current filters in the last 24 hours.</td></tr>';
|
||||
if (rows.length) {
|
||||
document.getElementById('events-body').innerHTML = rows.join('');
|
||||
applyVisibleColumns();
|
||||
} else {
|
||||
renderEmptyState('No requests match the current filters in the last 24 hours.');
|
||||
}
|
||||
updatePager(payload || {});
|
||||
}
|
||||
|
||||
@@ -1502,7 +1637,7 @@ const queryLogHTML = `<!doctype html>
|
||||
const response = await fetch('/api/events?' + params.toString());
|
||||
const payload = await response.json().catch(() => ({ items: [] }));
|
||||
if (!response.ok) {
|
||||
document.getElementById('events-body').innerHTML = '<tr><td colspan="9" class="muted">' + escapeHtml(payload.error || response.statusText) + '</td></tr>';
|
||||
renderEmptyState(payload.error || response.statusText);
|
||||
updatePager({ page: currentPage, has_prev: currentPage > 1, has_next: false });
|
||||
scheduleRefresh();
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user