User Log and Persistent Faction Information
This commit is contained in:
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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
211
static/users_log.js
Normal 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();
|
||||
});
|
||||
Reference in New Issue
Block a user