Files
boc/aamos-admin-upgrade/web/audit.html
T
Bernt 6de2455917 v1.2.0: Add Global Markets footer, translated to 9 languages
- Added GLOBAL_MARKETS_TITLE to all translation files
- Updated footer with 12 markets (4 active + 8 upcoming)
- Translated market section to: zh-cn, zh-tw, ja, ko, th, vi, id, ms, hi
- Built and deployed to production
- CloudFront invalidation: I3RTMXVFDJXWLG3SYX208OP1CC
2026-07-08 19:56:03 +00:00

303 lines
10 KiB
HTML

<!DOCTYPE html>
<html lang="sv" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AAMOS Admin — Audit Log</title>
<link rel="stylesheet" href="assets/admin.css">
<style>
.audit-toolbar {
display: flex;
gap: 12px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.filter-input {
padding: 8px 12px;
border: 1px solid var(--separator);
border-radius: var(--radius-sm);
background: var(--bg-input);
color: var(--text-primary);
font-size: 14px;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: var(--radius-sm);
font-size: 14px;
font-weight: 600;
cursor: pointer;
}
.btn-secondary { background: var(--bg-input); color: var(--text-primary); }
.btn-primary { background: var(--primary); color: white; }
.audit-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
.audit-table th {
text-align: left;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.4px;
color: var(--text-tertiary);
padding: 12px 16px;
border-bottom: 1px solid var(--separator);
background: var(--bg-input);
}
.audit-table td {
padding: 12px 16px;
border-bottom: 1px solid var(--separator);
}
.audit-table tr:hover { background: var(--bg-input); }
.level-pill {
font-size: 11px;
font-weight: 600;
padding: 2px 8px;
border-radius: 10px;
text-transform: uppercase;
}
.level-info { background: rgba(0,122,255,0.12); color: var(--primary); }
.level-success { background: rgba(52,199,89,0.12); color: var(--success); }
.level-warning { background: rgba(255,149,0,0.12); color: var(--warning); }
.level-danger { background: rgba(255,59,48,0.12); color: var(--danger); }
.audit-time { font-size: 12px; color: var(--text-tertiary); white-space: nowrap; }
.audit-user { font-weight: 500; }
.audit-action { font-family: 'SF Mono', monospace; font-size: 12px; }
.audit-resource { font-size: 12px; color: var(--text-tertiary); }
.pagination {
display: flex;
justify-content: center;
gap: 8px;
margin-top: 20px;
}
.page-btn {
padding: 8px 14px;
border: 1px solid var(--separator);
border-radius: var(--radius-sm);
background: var(--bg-card);
color: var(--text-primary);
cursor: pointer;
}
.page-btn.active { background: var(--primary); color: white; }
.page-btn:disabled { opacity: 0.4; cursor: not-allowed; }
</style>
</head>
<body>
<aside class="sidebar">
<div class="sidebar-brand">
<div class="brand-icon">
<svg viewBox="0 0 24 24"><path d="M12 2L2 7v10l10 5 10-5V7L12 2zm0 2.18L20 8.5v7L12 19.82 4 15.5v-7l8-4.32z"/></svg>
</div>
<div class="brand-text">
<div class="brand-name">AAMOS Admin</div>
<div class="brand-sub">Unified v2.0</div>
</div>
</div>
<nav class="sidebar-nav">
<div class="nav-section-label">Overview</div>
<a class="nav-item" href="unified-dashboard.html">
<span class="nav-icon">📊</span>
Dashboard
</a>
<a class="nav-item" href="users.html">
<span class="nav-icon">👥</span>
Users
</a>
<a class="nav-item" href="modules.html">
<span class="nav-icon">🔧</span>
Modules
</a>
<div class="nav-section-label">Systems</div>
<a class="nav-item" href="quixzoom.html">
<span class="nav-icon">📸</span>
quiXzoom
</a>
<a class="nav-item" href="landvex.html">
<span class="nav-icon">🌍</span>
LandveX
</a>
<a class="nav-item" href="economy.html">
<span class="nav-icon">💰</span>
Economy
</a>
<a class="nav-item" href="crm.html">
<span class="nav-icon">🤝</span>
CRM
</a>
<div class="nav-section-label">System</div>
<a class="nav-item active" href="audit.html">
<span class="nav-icon">📋</span>
Audit Log
</a>
<a class="nav-item" href="settings.html">
<span class="nav-icon">⚙️</span>
Settings
</a>
</nav>
</aside>
<div class="main">
<header class="topbar">
<div class="topbar-title">Audit Log</div>
<div class="topbar-actions">
<button class="btn btn-secondary" id="export-btn">📥 Export CSV</button>
</div>
</header>
<section class="page-content active">
<div class="audit-toolbar">
<input type="text" class="filter-input" id="filter-user" placeholder="User ID...">
<input type="text" class="filter-input" id="filter-action" placeholder="Action...">
<input type="text" class="filter-input" id="filter-resource" placeholder="Resource...">
<button class="btn btn-primary" id="filter-btn">Filter</button>
<button class="btn btn-secondary" id="clear-btn">Clear</button>
</div>
<div class="card">
<table class="audit-table">
<thead>
<tr>
<th>Time</th>
<th>Level</th>
<th>User</th>
<th>Action</th>
<th>Resource</th>
<th>IP</th>
<th>Details</th>
</tr>
</thead>
<tbody id="audit-tbody">
<tr><td colspan="7" style="text-align:center;padding:40px;color:var(--text-tertiary)">Loading...</td></tr>
</tbody>
</table>
</div>
<div class="pagination" id="pagination"></div>
</section>
</div>
<script src="assets/api.js"></script>
<script>
'use strict';
const api = createAdminAPI(window.location.origin);
let currentPage = 1;
function formatTime(iso) {
if (!iso) return '—';
const d = new Date(iso);
return d.toLocaleString('sv-SE', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
}
function getLevelClass(action) {
if (action?.includes('login') || action?.includes('create')) return 'level-success';
if (action?.includes('delete') || action?.includes('fail')) return 'level-danger';
if (action?.includes('update') || action?.includes('config')) return 'level-warning';
return 'level-info';
}
async function loadAudit(page = 1, filters = {}) {
try {
const data = await api.getAudit({ page, limit: 50, ...filters });
renderAudit(data.data || []);
renderPagination(data.total, data.page, data.limit);
} catch (err) {
document.getElementById('audit-tbody').innerHTML =
`<tr><td colspan="7" style="text-align:center;padding:40px;color:var(--danger)">Error: ${err.message}</td></tr>`;
}
}
function renderAudit(entries) {
const tbody = document.getElementById('audit-tbody');
if (!entries?.length) {
tbody.innerHTML = `<tr><td colspan="7" style="text-align:center;padding:40px;color:var(--text-tertiary)">No entries found</td></tr>`;
return;
}
tbody.innerHTML = entries.map(e => `
<tr>
<td class="audit-time">${formatTime(e.timestamp)}</td>
<td><span class="level-pill ${getLevelClass(e.action)}">${getLevelClass(e.action).replace('level-', '')}</span></td>
<td class="audit-user">${e.user_id?.slice(0, 8) || 'system'}</td>
<td class="audit-action">${e.action}</td>
<td class="audit-resource">${e.resource || '—'}</td>
<td class="audit-resource">${e.ip || '—'}</td>
<td class="audit-resource">${e.details || '—'}</td>
</tr>
`).join('');
}
function renderPagination(total, page, limit) {
const totalPages = Math.ceil(total / limit) || 1;
const container = document.getElementById('pagination');
let html = `<button class="page-btn" ${page <= 1 ? 'disabled' : ''} onclick="changePage(${page - 1})">←</button>`;
for (let i = 1; i <= totalPages; i++) {
if (i === 1 || i === totalPages || (i >= page - 1 && i <= page + 1)) {
html += `<button class="page-btn ${i === page ? 'active' : ''}" onclick="changePage(${i})">${i}</button>`;
} else if (i === page - 2 || i === page + 2) {
html += `<span style="padding:8px;color:var(--text-tertiary)">…</span>`;
}
}
html += `<button class="page-btn" ${page >= totalPages ? 'disabled' : ''} onclick="changePage(${page + 1})">→</button>`;
container.innerHTML = html;
}
window.changePage = (p) => {
currentPage = p;
const filters = {
user: document.getElementById('filter-user').value,
action: document.getElementById('filter-action').value,
resource: document.getElementById('filter-resource').value,
};
loadAudit(p, filters);
};
document.getElementById('filter-btn').addEventListener('click', () => {
currentPage = 1;
changePage(1);
});
document.getElementById('clear-btn').addEventListener('click', () => {
document.getElementById('filter-user').value = '';
document.getElementById('filter-action').value = '';
document.getElementById('filter-resource').value = '';
currentPage = 1;
changePage(1);
});
document.getElementById('export-btn').addEventListener('click', () => {
const rows = [['Time', 'User', 'Action', 'Resource', 'IP', 'Details']];
const tbody = document.getElementById('audit-tbody');
tbody.querySelectorAll('tr').forEach(tr => {
const cells = tr.querySelectorAll('td');
if (cells.length >= 6) {
rows.push([cells[0].textContent, cells[2].textContent, cells[3].textContent,
cells[4].textContent, cells[5].textContent, cells[6].textContent]);
}
});
const csv = rows.map(r => r.map(c => `"${c?.replace(/"/g, '""') || ''}"`).join(',')).join('\n');
const blob = new Blob([csv], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `audit-log-${new Date().toISOString().slice(0,10)}.csv`;
a.click();
URL.revokeObjectURL(url);
});
loadAudit();
</script>
</body>
</html>