User Log and Persistent Faction Information

This commit is contained in:
2026-01-27 14:48:46 -05:00
parent 4ae3a9eb17
commit 4850c16b87
39 changed files with 782 additions and 71 deletions

View File

@@ -23,6 +23,21 @@ function toInt(v) {
return Number.isNaN(n) ? null : n;
}
// ---------------------------
// Activity logging
// ---------------------------
async function logAction(action, details = "") {
try {
await fetch("/api/log_action", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action, details })
});
} catch (err) {
console.error("Failed to log action:", err);
}
}
// ---------------------------
// Status CSS helpers
// ---------------------------
@@ -428,6 +443,11 @@ function setupDropZones() {
if (prev) prev.removeChild(member.domElement);
zone.appendChild(member.domElement);
}
// Log the assignment
if (member) {
await logAction("Assigned Member to Group", `${member.name} (${kind}) -> Group ${groupKey}`);
}
} else {
console.warn("Unexpected zone id format", zone.id);
}
@@ -443,6 +463,11 @@ function setupDropZones() {
if (prev) prev.removeChild(member.domElement);
container.appendChild(member.domElement);
}
// Log the removal
if (member) {
await logAction("Removed Member from Group", `${member.name} (${kind})`);
}
}
});
@@ -516,6 +541,9 @@ async function populateFriendly() {
// Refresh assignments & status UI
await loadMembers("enemy"); // in case population changed cross lists
await pollAssignments();
// Log the action
await logAction("Populated Friendly Faction", `Faction ID: ${id}, Members: ${data.members ? data.members.length : 0}`);
} catch (err) {
console.error("populateFriendly error:", err);
}
@@ -583,6 +611,9 @@ async function populateEnemy() {
// Refresh assignments & status UI
await loadMembers("friendly");
await pollAssignments();
// Log the action
await logAction("Populated Enemy Faction", `Faction ID: ${id}, Members: ${data.members ? data.members.length : 0}`);
} catch (err) {
console.error("populateEnemy error:", err);
}
@@ -620,6 +651,9 @@ async function toggleFriendlyStatus() {
btn.textContent = "Start";
btn.dataset.running = "false";
btn.style.backgroundColor = "";
// Notify server that status refresh stopped
await fetch("/api/stop_friendly_status", { method: "POST" });
await logAction("Stopped Friendly Status Refresh");
return;
}
@@ -637,6 +671,7 @@ async function toggleFriendlyStatus() {
btn.textContent = "Stop";
btn.dataset.running = "true";
btn.style.backgroundColor = "#ff6b6b";
await logAction("Started Friendly Status Refresh", `Interval: ${interval}s`);
}
async function toggleEnemyStatus() {
@@ -647,6 +682,9 @@ async function toggleEnemyStatus() {
btn.textContent = "Start";
btn.dataset.running = "false";
btn.style.backgroundColor = "";
// Notify server that status refresh stopped
await fetch("/api/stop_enemy_status", { method: "POST" });
await logAction("Stopped Enemy Status Refresh");
return;
}
@@ -664,6 +702,7 @@ async function toggleEnemyStatus() {
btn.textContent = "Stop";
btn.dataset.running = "true";
btn.style.backgroundColor = "#ff6b6b";
await logAction("Started Enemy Status Refresh", `Interval: ${interval}s`);
}
// ---------------------------
@@ -694,6 +733,9 @@ async function toggleBotControl() {
btn.style.backgroundColor = data.bot_running ? "#ff4444" : "#4CAF50";
console.log(`Bot ${data.bot_running ? "started" : "stopped"}`);
// Log the action
await logAction(data.bot_running ? "Started Bot" : "Stopped Bot");
} catch (err) {
console.error("toggleBotControl error:", err);
}
@@ -707,6 +749,8 @@ async function resetGroups() {
await clearAssignmentsOnServer();
// reload assignments & UI
await pollAssignments();
// Log the action
await logAction("Reset All Groups");
}
// ---------------------------
@@ -765,6 +809,104 @@ async function handleLogout() {
}
}
// ---------------------------
// Restore dashboard state from server
// ---------------------------
async function restoreDashboardState() {
try {
const res = await fetch("/api/dashboard_state", { cache: "no-store" });
if (!res.ok) {
console.log("No dashboard state to restore");
return;
}
const state = await res.json();
console.log("Restoring dashboard state:", state);
// Restore friendly faction
if (state.friendly_faction_id && state.friendly_members && state.friendly_members.length > 0) {
document.getElementById("friendly-id").value = state.friendly_faction_id;
// Load members into UI
for (const m of state.friendly_members) {
const newMember = {
id: m.id,
name: m.name,
level: m.level,
estimate: m.estimate,
status: m.status || "Unknown",
hits: m.hits || 0,
domElement: null
};
friendlyMembers.set(m.id, newMember);
const card = createMemberCard(newMember, "friendly");
friendlyContainer.appendChild(card);
}
console.log(`Restored ${state.friendly_members.length} friendly members`);
// Restore status refresh if it was running
if (state.friendly_status_running) {
const interval = state.friendly_status_interval || 10;
document.getElementById("friendly-refresh-interval").value = interval;
const btn = document.getElementById("friendly-status-btn");
friendlyStatusIntervalHandle = setInterval(() => refreshStatus("friendly"), interval * 1000);
refreshStatus("friendly");
btn.textContent = "Stop";
btn.dataset.running = "true";
btn.style.backgroundColor = "#ff6b6b";
console.log(`Restored friendly status refresh (${interval}s)`);
}
}
// Restore enemy faction
if (state.enemy_faction_id && state.enemy_members && state.enemy_members.length > 0) {
document.getElementById("enemy-id").value = state.enemy_faction_id;
// Load members into UI
for (const m of state.enemy_members) {
const newMember = {
id: m.id,
name: m.name,
level: m.level,
estimate: m.estimate,
status: m.status || "Unknown",
hits: m.hits || 0,
domElement: null
};
enemyMembers.set(m.id, newMember);
const card = createMemberCard(newMember, "enemy");
enemyContainer.appendChild(card);
}
console.log(`Restored ${state.enemy_members.length} enemy members`);
// Restore status refresh if it was running
if (state.enemy_status_running) {
const interval = state.enemy_status_interval || 10;
document.getElementById("enemy-refresh-interval").value = interval;
const btn = document.getElementById("enemy-status-btn");
enemyStatusIntervalHandle = setInterval(() => refreshStatus("enemy"), interval * 1000);
refreshStatus("enemy");
btn.textContent = "Stop";
btn.dataset.running = "true";
btn.style.backgroundColor = "#ff6b6b";
console.log(`Restored enemy status refresh (${interval}s)`);
}
}
// Refresh assignments after restoring members
if ((state.friendly_members && state.friendly_members.length > 0) ||
(state.enemy_members && state.enemy_members.length > 0)) {
await pollAssignments();
}
} catch (err) {
console.error("Error restoring dashboard state:", err);
console.error("Error stack:", err.stack);
}
}
// ---------------------------
// Initial load
// ---------------------------
@@ -772,9 +914,9 @@ document.addEventListener("DOMContentLoaded", async () => {
console.log(">>> DOMContentLoaded fired");
wireUp();
// DON'T load members on initial page load - wait for user to click Populate
// This prevents showing stale data from server STATE
// Restore previous state from server (faction IDs, members, status refresh)
await restoreDashboardState();
// Start polling for assignments (but there won't be any until members are populated)
// Start polling for assignments
startAssignmentsPolling();
});

View File

@@ -388,3 +388,112 @@ button:hover { background-color: #3399ff; }
.config-save-btn:hover {
background-color: #45a049;
}
/* Users & Activity Log Page */
.users-log-layout {
display: flex;
gap: 1.5rem;
height: calc(100vh - 150px);
}
.users-panel {
width: 300px;
flex-shrink: 0;
}
.log-panel {
flex: 1;
min-width: 0;
}
.log-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.users-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
max-height: 600px;
overflow-y: auto;
}
.user-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.6rem;
background-color: #2a2a3d;
border-radius: 6px;
}
.user-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #4CAF50;
box-shadow: 0 0 6px #4CAF50;
}
.user-name {
color: #f0f0f0;
font-size: 0.95rem;
}
.no-users, .no-logs {
color: #99a7bf;
font-style: italic;
text-align: center;
padding: 2rem;
}
.log-container {
background-color: #1a1a26;
border-radius: 8px;
padding: 1rem;
height: calc(100vh - 220px);
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 0.9rem;
}
.log-entry {
padding: 0.5rem 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
display: flex;
gap: 0.8rem;
flex-wrap: wrap;
}
.log-entry:last-child {
border-bottom: none;
}
.log-time {
color: #66ccff;
font-weight: bold;
min-width: 70px;
}
.log-user {
color: #ffcc66;
font-weight: bold;
min-width: 100px;
}
.log-action {
color: #4CAF50;
}
.log-details {
color: #99a7bf;
}
.info-text {
color: #99a7bf;
font-size: 0.85rem;
font-style: italic;
}

211
static/users_log.js Normal file
View File

@@ -0,0 +1,211 @@
// users_log.js - Activity log and active users display
let logsPollingInterval = null;
let usersPollingInterval = null;
async function fetchActiveUsers() {
try {
const res = await fetch("/api/active_users", { cache: "no-store" });
if (!res.ok) {
console.error("Failed to fetch active users:", res.status);
return;
}
const data = await res.json();
displayActiveUsers(data.users || []);
} catch (err) {
console.error("Error fetching active users:", err);
}
}
function displayActiveUsers(users) {
const container = document.getElementById("active-users-list");
if (users.length === 0) {
container.innerHTML = '<div class="no-users">No active users</div>';
return;
}
container.innerHTML = users.map(username => `
<div class="user-item">
<span class="user-indicator"></span>
<span class="user-name">${escapeHtml(username)}</span>
</div>
`).join('');
}
async function fetchActivityLogs() {
try {
const res = await fetch("/api/activity_logs?limit=200", { cache: "no-store" });
if (!res.ok) {
console.error("Failed to fetch activity logs:", res.status);
return;
}
const data = await res.json();
displayActivityLogs(data.logs || []);
} catch (err) {
console.error("Error fetching activity logs:", err);
}
}
function displayActivityLogs(logs) {
const container = document.getElementById("activity-log-container");
if (logs.length === 0) {
container.innerHTML = '<div class="no-logs">No activity logs yet</div>';
return;
}
container.innerHTML = logs.map(log => {
const time = formatTimestamp(log.timestamp);
const details = log.details ? ` - ${escapeHtml(log.details)}` : '';
return `
<div class="log-entry">
<span class="log-time">${time}</span>
<span class="log-user">${escapeHtml(log.username)}</span>
<span class="log-action">${escapeHtml(log.action)}</span>
${details ? `<span class="log-details">${details}</span>` : ''}
</div>
`;
}).join('');
// Auto-scroll to bottom on first load
if (!container.hasAttribute('data-scrolled')) {
container.scrollTop = container.scrollHeight;
container.setAttribute('data-scrolled', 'true');
}
}
function formatTimestamp(isoString) {
const date = new Date(isoString);
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${hours}:${minutes}:${seconds}`;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
async function copyLogsToClipboard() {
try {
const res = await fetch("/api/activity_logs?limit=1000", { cache: "no-store" });
if (!res.ok) {
alert("Failed to fetch logs");
return;
}
const data = await res.json();
const logs = data.logs || [];
if (logs.length === 0) {
alert("No logs to copy");
return;
}
// Format logs as plain text
const logText = logs.map(log => {
const time = formatTimestamp(log.timestamp);
const details = log.details ? ` - ${log.details}` : '';
return `[${time}] ${log.username}: ${log.action}${details}`;
}).join('\n');
// Copy to clipboard
await navigator.clipboard.writeText(logText);
// Show success feedback
const btn = document.getElementById("copy-log-btn");
const originalText = btn.textContent;
btn.textContent = "Copied!";
btn.style.backgroundColor = "#4CAF50";
setTimeout(() => {
btn.textContent = originalText;
btn.style.backgroundColor = "";
}, 2000);
} catch (err) {
console.error("Error copying logs:", err);
alert("Failed to copy logs to clipboard");
}
}
function startPolling() {
// Fetch immediately
fetchActiveUsers();
fetchActivityLogs();
// Then poll at intervals
usersPollingInterval = setInterval(fetchActiveUsers, 10000); // Every 10 seconds
logsPollingInterval = setInterval(fetchActivityLogs, 5000); // Every 5 seconds
}
function stopPolling() {
if (usersPollingInterval) {
clearInterval(usersPollingInterval);
usersPollingInterval = null;
}
if (logsPollingInterval) {
clearInterval(logsPollingInterval);
logsPollingInterval = null;
}
}
async function handleLogout() {
console.log("handleLogout called");
try {
console.log("Sending logout request to /auth/logout");
const response = await fetch("/auth/logout", {
method: "POST"
});
console.log("Logout response status:", response.status);
if (response.ok) {
console.log("Logout successful, redirecting to /login");
window.location.href = "/login";
} else {
console.error("Logout failed with status:", response.status);
window.location.href = "/login";
}
} catch (error) {
console.error("Error during logout:", error);
window.location.href = "/login";
}
}
function wireUp() {
// Attach copy button handler
const copyBtn = document.getElementById("copy-log-btn");
if (copyBtn) {
copyBtn.addEventListener("click", copyLogsToClipboard);
}
// Attach logout handler
const logoutBtn = document.getElementById("logout-btn");
if (logoutBtn) {
logoutBtn.addEventListener("click", handleLogout);
}
}
// Initialize when page loads
document.addEventListener("DOMContentLoaded", () => {
wireUp();
startPolling();
});
// Stop polling when page is hidden/unloaded
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
stopPolling();
} else {
startPolling();
}
});
window.addEventListener("beforeunload", () => {
stopPolling();
});