implement activity logging system for user actions and system events

This commit is contained in:
CPTN Cosmo 2026-06-19 03:35:51 +02:00
parent 81f7031555
commit 88e38535fc
5 changed files with 958 additions and 500 deletions

View file

@ -31,8 +31,13 @@ document.addEventListener('DOMContentLoaded', () => {
const addOtpModal = document.getElementById('add-otp-secret-modal');
const otpSecretIdSelect = document.getElementById('otp-secret-id');
const otpSecretsList = document.getElementById('otp-secrets-list');
const logsToggle = document.getElementById('logs-toggle');
const logsSection = document.getElementById('logs-section');
const logsViewer = document.getElementById('logs-viewer');
const logsRefreshBtn = document.getElementById('logs-refresh-btn');
let config = { firewall_host_ip: null };
let logsInterval = null;
let currentUser = null;
let editId = null;
let editOtpId = null;
@ -87,12 +92,18 @@ document.addEventListener('DOMContentLoaded', () => {
loginOverlay.style.display = 'flex';
logoutBtn.style.display = 'none';
mainNav.style.display = 'none';
if (logsSection) logsSection.style.display = 'none';
stopLogsPolling();
};
const hideLogin = () => {
loginOverlay.style.display = 'none';
logoutBtn.style.display = 'flex';
mainNav.style.display = 'flex';
if (logsToggle && logsToggle.checked) {
if (logsSection) logsSection.style.display = 'block';
startLogsPolling();
}
};
// Tab Switching
@ -110,9 +121,15 @@ document.addEventListener('DOMContentLoaded', () => {
});
const updateFirewallCmds = () => {
const hostIp = config.firewall_host_ip || window.location.hostname;
const loginFirewallIp = document.getElementById('login-firewall-ip');
if (loginFirewallIp) {
loginFirewallIp.textContent = hostIp;
}
if (!ufwCmd || !iptablesCmd || !winCmd) return;
const port = portInput.value || '4646';
const hostIp = config.firewall_host_ip || window.location.hostname;
ufwCmd.textContent = `sudo ufw allow from ${hostIp} to any port ${port} proto tcp`;
iptablesCmd.textContent = `sudo iptables -I INPUT -p tcp -s ${hostIp} --dport ${port} -j ACCEPT`;
winCmd.textContent = `New-NetFirewallRule -DisplayName "XIVLauncher OTP" -Direction Inbound -RemoteAddress ${hostIp} -LocalPort ${port} -Protocol TCP -Action Allow`;
@ -121,7 +138,6 @@ document.addEventListener('DOMContentLoaded', () => {
const fetchConfig = async () => {
try {
const res = await fetch('/api/config');
if (res.status === 401) return showLogin();
if (res.ok) {
config = await res.json();
updateFirewallCmds();
@ -193,13 +209,10 @@ document.addEventListener('DOMContentLoaded', () => {
</div>
<div class="instance-actions">
<button class="btn btn-icon" onclick="editInstance(${inst.id})" title="Edit">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>
<svg class="icon-oxygen" width="18" height="18" viewBox="0 0 48 48" fill="none"><path d="M6 36l3-9 20-20 6 6-20 20-9 3z" fill="url(#pencilYellow)" stroke="#664d00" stroke-width="1.5"/><path d="M6 36l3-9 6 6z" fill="#e5c158"/><path d="M6 36l1.5-4.5 3 3z" fill="#333"/><path d="M29 7l6 6" stroke="#664d00" stroke-width="1.5"/><path d="M35 13l3.5-3.5a3 3 0 0 0 0-4.2l-1.8-1.8a3 3 0 0 0-4.2 0L29 7z" fill="#ff9999" stroke="#803333" stroke-width="1.5"/></svg>
</button>
<button class="delete-btn" onclick="deleteInstance(${inst.id})" title="Delete">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
<svg class="icon-oxygen" width="18" height="18" viewBox="0 0 48 48" fill="none"><circle cx="24" cy="24" r="21" fill="url(#redBall)" stroke="#7f1d1d" stroke-width="1.5"/><ellipse cx="24" cy="11" rx="14" ry="7" fill="url(#glassShineRed)"/><path d="M16 16l16 16M32 16L16 32" stroke="#ffffff" stroke-width="5" stroke-linecap="round"/></svg>
</button>
</div>
</div>
@ -220,10 +233,10 @@ document.addEventListener('DOMContentLoaded', () => {
</div>
<div class="otp-secret-actions">
<button class="btn btn-icon" onclick="editOTPSecret(${sec.id})" title="Edit">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>
<svg class="icon-oxygen" width="18" height="18" viewBox="0 0 48 48" fill="none"><path d="M6 36l3-9 20-20 6 6-20 20-9 3z" fill="url(#pencilYellow)" stroke="#664d00" stroke-width="1.5"/><path d="M6 36l3-9 6 6z" fill="#e5c158"/><path d="M6 36l1.5-4.5 3 3z" fill="#333"/><path d="M29 7l6 6" stroke="#664d00" stroke-width="1.5"/><path d="M35 13l3.5-3.5a3 3 0 0 0 0-4.2l-1.8-1.8a3 3 0 0 0-4.2 0L29 7z" fill="#ff9999" stroke="#803333" stroke-width="1.5"/></svg>
</button>
<button class="delete-btn" onclick="deleteOTPSecret(${sec.id})" title="Delete">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2-2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>
<svg class="icon-oxygen" width="18" height="18" viewBox="0 0 48 48" fill="none"><circle cx="24" cy="24" r="21" fill="url(#redBall)" stroke="#7f1d1d" stroke-width="1.5"/><ellipse cx="24" cy="11" rx="14" ry="7" fill="url(#glassShineRed)"/><path d="M16 16l16 16M32 16L16 32" stroke="#ffffff" stroke-width="5" stroke-linecap="round"/></svg>
</button>
</div>
</div>
@ -238,10 +251,10 @@ document.addEventListener('DOMContentLoaded', () => {
</div>
<div class="user-actions">
<button class="btn btn-icon" onclick="resetUserPassword(${user.id})" title="Reset Password">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3L15.5 7.5z"></path></svg>
<svg class="icon-oxygen" width="18" height="18" viewBox="0 0 48 48" fill="none"><circle cx="15" cy="24" r="9" fill="url(#goldKeyGrad)" stroke="#806600" stroke-width="1.5"/><circle cx="15" cy="24" r="4" fill="#f5f5f5" stroke="#806600" stroke-width="1.5"/><path d="M24 20h18v8h-4v-4h-4v4h-4v-4h-2z" fill="url(#goldKeyGrad)" stroke="#806600" stroke-width="1.5"/></svg>
</button>
<button class="delete-btn" onclick="deleteUser(${user.id})" title="Delete User">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>
<svg class="icon-oxygen" width="18" height="18" viewBox="0 0 48 48" fill="none"><circle cx="24" cy="24" r="21" fill="url(#redBall)" stroke="#7f1d1d" stroke-width="1.5"/><ellipse cx="24" cy="11" rx="14" ry="7" fill="url(#glassShineRed)"/><path d="M16 16l16 16M32 16L16 32" stroke="#ffffff" stroke-width="5" stroke-linecap="round"/></svg>
</button>
</div>
</div>
@ -514,7 +527,7 @@ document.addEventListener('DOMContentLoaded', () => {
await navigator.clipboard.writeText(text);
const originalHtml = btn.innerHTML;
btn.classList.add('copied');
btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>';
btn.innerHTML = '<svg class="icon-oxygen" width="14" height="14" viewBox="0 0 48 48" fill="none"><circle cx="24" cy="24" r="21" fill="url(#greenBall)" stroke="#0d3c12" stroke-width="1.5"/><ellipse cx="24" cy="11" rx="14" ry="7" fill="url(#glassShineRed)"/><path d="M14 26l7 7 15-15" stroke="#ffffff" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/></svg>';
setTimeout(() => {
btn.classList.remove('copied');
btn.innerHTML = originalHtml;
@ -526,6 +539,79 @@ document.addEventListener('DOMContentLoaded', () => {
portInput.addEventListener('input', updateFirewallCmds);
// Bind help button on login overlay
const loginShowHelpBtn = document.getElementById('login-show-help-btn');
if (loginShowHelpBtn) {
loginShowHelpBtn.addEventListener('click', () => openModal(helpModal));
}
// Logging helpers
const fetchLogs = async () => {
if (!currentUser) return;
try {
const res = await fetch('/api/logs');
if (res.status === 401) return showLogin();
if (res.ok) {
const logs = await res.json();
renderLogs(logs);
}
} catch (e) {
console.error('Failed to fetch logs:', e);
}
};
const startLogsPolling = () => {
stopLogsPolling();
fetchLogs();
logsInterval = setInterval(fetchLogs, 5000);
};
const stopLogsPolling = () => {
if (logsInterval) {
clearInterval(logsInterval);
logsInterval = null;
}
};
const renderLogs = (logs) => {
if (!logsViewer) return;
if (logs.length === 0) {
logsViewer.innerHTML = '<div style="color: var(--text-muted); text-align: center; padding: 1rem;">No logs available.</div>';
return;
}
logsViewer.innerHTML = logs.slice().reverse().map(log => {
const date = new Date(log.timestamp * 1000);
const timeStr = date.toLocaleTimeString() + '.' + String(date.getMilliseconds()).padStart(3, '0');
const levelClass = `log-level-${log.level.toLowerCase()}`;
return `
<div class="log-entry">
<span class="log-time">[${timeStr}]</span>
<span class="log-level ${levelClass}">[${log.level}]</span>
<span class="log-message">${escapeHtml(log.message)}</span>
</div>
`;
}).join('');
logsViewer.scrollTop = logsViewer.scrollHeight;
};
if (logsToggle) {
logsToggle.addEventListener('change', () => {
if (logsToggle.checked) {
logsSection.style.display = 'block';
startLogsPolling();
} else {
logsSection.style.display = 'none';
stopLogsPolling();
}
});
}
if (logsRefreshBtn) {
logsRefreshBtn.addEventListener('click', fetchLogs);
}
const checkAuth = async () => {
try {
const res = await fetch('/api/auth/status');
@ -534,7 +620,7 @@ document.addEventListener('DOMContentLoaded', () => {
currentUser = data.user;
setupUIForUser();
hideLogin();
fetchConfig();
updateFirewallCmds();
fetchInstances();
fetchOTPSecrets();
} else {
@ -555,7 +641,10 @@ document.addEventListener('DOMContentLoaded', () => {
.replace(/'/g, "&#039;");
}
// Unconditionally fetch config on startup to show firewall info on login overlay
fetchConfig();
checkAuth();
setInterval(() => {
if (loginOverlay.style.display === 'none' && document.getElementById('instances-section').classList.contains('active')) {
fetchInstances();