diff --git a/public/audit.html b/public/audit.html
index 036a769..302017e 100644
--- a/public/audit.html
+++ b/public/audit.html
@@ -34,6 +34,7 @@
Записей пока нет.
+
diff --git a/public/js/audit.js b/public/js/audit.js
index fc093b1..5964455 100644
--- a/public/js/audit.js
+++ b/public/js/audit.js
@@ -21,6 +21,10 @@ const ACTION_LABELS = {
'backup.restore': 'Выполнено восстановление',
};
+const AUDIT_PAGE_SIZE = 50;
+let auditPage = 1;
+let auditTotal = 0;
+
function fmtTime(t) {
try { return new Date(t).toLocaleString('ru'); } catch { return t || ''; }
}
@@ -36,35 +40,65 @@ function dt(t) {
} catch { return esc(String(t).slice(0, 120)); }
}
+function renderPager(container, page, totalPages, onPageChange) {
+ if (totalPages <= 1) { container.innerHTML = ''; return; }
+ const pages = [];
+ const maxVisible = 5;
+ let start = Math.max(1, page - Math.floor(maxVisible / 2));
+ let end = Math.min(totalPages, start + maxVisible - 1);
+ if (end - start + 1 < maxVisible) start = Math.max(1, end - maxVisible + 1);
+ for (let i = start; i <= end; i++) pages.push(i);
+ let html = '';
+ if (page > 1) html += ``;
+ if (start > 1) { html += ``; if (start > 2) html += ``; }
+ for (const p of pages) html += ``;
+ if (end < totalPages) { if (end < totalPages - 1) html += ``; html += ``; }
+ if (page < totalPages) html += ``;
+ container.innerHTML = html;
+ container.querySelectorAll('button').forEach(b => b.addEventListener('click', () => onPageChange(parseInt(b.dataset.page, 10))));
+}
+
async function loadAudit() {
const el = document.getElementById('auditTable').querySelector('tbody');
el.innerHTML = '| Загрузка… |
';
- const res = await fetch(`${API}/api/audit?limit=500`, { headers: hdr() });
+ const p = new URLSearchParams();
+ p.set('limit', AUDIT_PAGE_SIZE);
+ p.set('offset', (auditPage - 1) * AUDIT_PAGE_SIZE);
+ const res = await fetch(`${API}/api/audit?${p}`, { headers: hdr() });
if (!res.ok) { el.innerHTML = '| Ошибка загрузки |
'; return; }
const rows = await res.json();
- document.getElementById('auditCount').textContent = rows.length ? `Записей: ${rows.length}` : '';
+ // Total count for pagination (approximate: if we got less than page size, we're on last page)
+ const totalPages = rows.length < AUDIT_PAGE_SIZE ? auditPage : auditPage + 1;
+ auditTotal = totalPages;
+ document.getElementById('auditCount').textContent = rows.length ? `Записей на странице: ${rows.length}` : '';
const q = (document.getElementById('auditFilter').value || '').toLowerCase().trim();
const filtered = q ? rows.filter(r => (ACTION_LABELS[r.action] || r.action).toLowerCase().includes(q)) : rows;
if (!filtered.length) {
document.getElementById('auditEmpty').style.display = 'block';
el.innerHTML = '';
- return;
+ } else {
+ document.getElementById('auditEmpty').style.display = 'none';
+ el.innerHTML = filtered.map(r => `
+
+ | ${fmtTime(r.created_at)} |
+ ${esc(ACTION_LABELS[r.action] || r.action)} |
+ ${dt(r.target)} |
+ ${esc(r.ip || '')} |
+
`).join('');
}
- document.getElementById('auditEmpty').style.display = 'none';
- el.innerHTML = filtered.map(r => `
-
- | ${fmtTime(r.created_at)} |
- ${esc(ACTION_LABELS[r.action] || r.action)} |
- ${dt(r.target)} |
- ${esc(r.ip || '')} |
-
`).join('');
+ renderPager(document.getElementById('auditPager'), auditPage, auditTotal, goAuditPage);
}
-document.getElementById('auditFilter').addEventListener('input', loadAudit);
+function goAuditPage(p) {
+ auditPage = p;
+ loadAudit();
+}
+
+document.getElementById('auditFilter').addEventListener('input', () => { auditPage = 1; loadAudit(); });
document.getElementById('auditRefreshBtn').addEventListener('click', loadAudit);
(async () => {
- if (await checkAuth()) {
+ if (await requireAdminPage()) {
buildSidebar(document.body.dataset.page);
loadAudit();
}
diff --git a/server.js b/server.js
index 2e662ff..02aa03b 100644
--- a/server.js
+++ b/server.js
@@ -607,11 +607,12 @@ app.put('/api/settings', requireAdmin, async (req, res) => {
app.get('/api/audit', requireAdmin, async (req, res) => {
const limit = Math.min(parseInt(req.query.limit, 10) || 100, 1000);
+ const offset = Math.max(parseInt(req.query.offset, 10) || 0, 0);
const { rows } = await pool.query(
`SELECT a.id, a.action, a.target, a.ip, a.created_at, a.user_id, u.username AS user_name
FROM audit_log a LEFT JOIN users u ON u.id = a.user_id
- ORDER BY a.id DESC LIMIT $1`,
- [limit]
+ ORDER BY a.id DESC LIMIT $1 OFFSET $2`,
+ [limit, offset]
);
res.json(rows);
});