diff --git a/discord-rare-monitor/discord_rare_monitor.py b/discord-rare-monitor/discord_rare_monitor.py
index 7063691c..849d3963 100644
--- a/discord-rare-monitor/discord_rare_monitor.py
+++ b/discord-rare-monitor/discord_rare_monitor.py
@@ -73,22 +73,7 @@ class DiscordRareMonitor:
# Setup Discord event handlers
self.setup_discord_handlers()
-
- async def cancel_websocket_task(self):
- """Safely cancel the existing websocket task if running.
-
- This prevents duplicate tasks from running in parallel, which would
- cause duplicate Discord messages for each rare event.
- """
- if self.websocket_task and not self.websocket_task.done():
- logger.info("🛑 Cancelling existing WebSocket task before creating new one")
- self.websocket_task.cancel()
- try:
- await self.websocket_task
- except asyncio.CancelledError:
- pass
- logger.info("✅ Old WebSocket task cancelled")
-
+
def setup_discord_handlers(self):
"""Setup Discord client event handlers."""
@@ -125,22 +110,13 @@ class DiscordRareMonitor:
logger.info(f"📍 Great rares channel: #{great_channel.name}")
logger.info("🎯 Bot ready to receive messages!")
-
- # Cancel any existing WebSocket task first (prevents duplicates if on_ready fires twice)
- await self.cancel_websocket_task()
-
+
# Start WebSocket monitoring
self.running = True
self.websocket_task = asyncio.create_task(self.monitor_websocket())
logger.info("🔄 Started WebSocket monitoring task")
-
- # Start health monitoring task (also cancel if exists)
- if self.health_monitor_task and not self.health_monitor_task.done():
- self.health_monitor_task.cancel()
- try:
- await self.health_monitor_task
- except asyncio.CancelledError:
- pass
+
+ # Start health monitoring task
self.health_monitor_task = asyncio.create_task(self.monitor_websocket_health())
logger.info("💓 Started WebSocket health monitoring task")
@@ -157,8 +133,6 @@ class DiscordRareMonitor:
if not self.websocket_task or self.websocket_task.done():
logger.warning("🔧 WebSocket task was lost during Discord disconnect - restarting")
await self.post_status_to_aclog("🔄 Discord resumed: WebSocket was lost, restarting connection")
- # Cancel any zombie task first (safety measure)
- await self.cancel_websocket_task()
self.websocket_task = asyncio.create_task(self.monitor_websocket())
else:
logger.info("✅ WebSocket task still healthy after Discord resume")
@@ -247,8 +221,6 @@ class DiscordRareMonitor:
# Restart the WebSocket monitoring task
logger.info("🔧 Restarting WebSocket monitoring task")
await self.post_status_to_aclog("🚨 Health check detected WebSocket failure - restarting connection")
- # Cancel any existing task first to prevent duplicates
- await self.cancel_websocket_task()
self.websocket_task = asyncio.create_task(self.monitor_websocket())
else:
logger.debug("💓 WebSocket task health check passed")
diff --git a/main.py b/main.py
index 90db523f..0db32326 100644
--- a/main.py
+++ b/main.py
@@ -65,7 +65,6 @@ INVENTORY_SERVICE_URL = os.getenv('INVENTORY_SERVICE_URL', 'http://inventory-ser
_cached_live: dict = {"players": []}
_cached_trails: dict = {"trails": []}
_cached_total_rares: dict = {"all_time": 0, "today": 0, "last_updated": None}
-_cached_total_kills: dict = {"total": 0, "last_updated": None}
_cache_task: asyncio.Task | None = None
_rares_cache_task: asyncio.Task | None = None
_cleanup_task: asyncio.Task | None = None
@@ -740,26 +739,14 @@ async def _refresh_total_rares_cache() -> None:
except Exception as e:
logger.debug(f"rare_events table not available or empty: {e}")
today_total = 0
-
- # Get total kills from char_stats table (all-time, all characters)
- try:
- kills_query = "SELECT COALESCE(SUM(total_kills), 0) as total FROM char_stats"
- kills_result = await conn.fetch_one(kills_query)
- total_kills = kills_result["total"] if kills_result else 0
- except Exception as e:
- logger.debug(f"char_stats table not available: {e}")
- total_kills = 0
-
- # Update caches
+
+ # Update cache
_cached_total_rares["all_time"] = all_time_total
_cached_total_rares["today"] = today_total
_cached_total_rares["last_updated"] = datetime.now(timezone.utc)
-
- _cached_total_kills["total"] = total_kills
- _cached_total_kills["last_updated"] = datetime.now(timezone.utc)
-
+
consecutive_failures = 0
- logger.debug(f"Stats cache updated: Rares all-time: {all_time_total}, today: {today_total}, Kills: {total_kills}")
+ logger.debug(f"Total rares cache updated: All-time: {all_time_total}, Today: {today_total}")
except Exception as e:
consecutive_failures += 1
@@ -1192,17 +1179,6 @@ async def get_total_rares():
raise HTTPException(status_code=500, detail="Internal server error")
-@app.get("/total-kills")
-@app.get("/total-kills/")
-async def get_total_kills():
- """Return cached total kills statistics (updated every 5 minutes)."""
- try:
- return JSONResponse(content=jsonable_encoder(_cached_total_kills))
- except Exception as e:
- logger.error(f"Failed to get total kills: {e}", exc_info=True)
- raise HTTPException(status_code=500, detail="Internal server error")
-
-
# --- GET Spawn Heat Map Endpoint ---------------------------------
@app.get("/spawns/heatmap")
async def get_spawn_heatmap_data(
diff --git a/static/script.js b/static/script.js
index 20b926f4..b37fbecf 100644
--- a/static/script.js
+++ b/static/script.js
@@ -33,170 +33,6 @@ const btnContainer = document.getElementById('sortButtons');
const tooltip = document.getElementById('tooltip');
const coordinates = document.getElementById('coordinates');
-/* ---------- Element Pooling System for Performance ------------- */
-// Pools for reusing DOM elements to eliminate recreation overhead
-const elementPools = {
- dots: [],
- listItems: [],
- activeDots: new Set(),
- activeListItems: new Set()
-};
-
-// Performance tracking
-let performanceStats = {
- // Lifetime totals
- dotsCreated: 0,
- dotsReused: 0,
- listItemsCreated: 0,
- listItemsReused: 0,
- // Per-render stats (reset each render)
- renderDotsCreated: 0,
- renderDotsReused: 0,
- renderListItemsCreated: 0,
- renderListItemsReused: 0,
- lastRenderTime: 0,
- renderCount: 0
-};
-
-function createNewDot() {
- const dot = document.createElement('div');
- dot.className = 'dot';
- performanceStats.dotsCreated++;
- performanceStats.renderDotsCreated++;
-
- // Add event listeners once when creating
- dot.addEventListener('mouseenter', e => showTooltip(e, dot.playerData));
- dot.addEventListener('mousemove', e => showTooltip(e, dot.playerData));
- dot.addEventListener('mouseleave', hideTooltip);
- dot.addEventListener('click', () => {
- if (dot.playerData) {
- const { x, y } = worldToPx(dot.playerData.ew, dot.playerData.ns);
- selectPlayer(dot.playerData, x, y);
- }
- });
-
- return dot;
-}
-
-function createNewListItem() {
- const li = document.createElement('li');
- li.className = 'player-item';
- performanceStats.listItemsCreated++;
- performanceStats.renderListItemsCreated++;
-
- // Create the grid content container
- const gridContent = document.createElement('div');
- gridContent.className = 'grid-content';
- li.appendChild(gridContent);
-
- // Create buttons once and keep them (no individual event listeners needed)
- const buttonsContainer = document.createElement('div');
- buttonsContainer.className = 'buttons-container';
-
- const chatBtn = document.createElement('button');
- chatBtn.className = 'chat-btn';
- chatBtn.textContent = 'Chat';
- chatBtn.addEventListener('click', (e) => {
- console.log('🔥 CHAT BUTTON CLICKED!', e.target, e.currentTarget);
- e.stopPropagation();
- // Try button's own playerData first, fallback to DOM traversal
- const playerData = e.currentTarget.playerData || e.target.closest('li.player-item')?.playerData;
- console.log('🔥 Player data found:', playerData);
- if (playerData) {
- console.log('🔥 Opening chat for:', playerData.character_name);
- showChatWindow(playerData.character_name);
- } else {
- console.log('🔥 No player data found!');
- }
- });
-
- const statsBtn = document.createElement('button');
- statsBtn.className = 'stats-btn';
- statsBtn.textContent = 'Stats';
- statsBtn.addEventListener('click', (e) => {
- console.log('📊 STATS BUTTON CLICKED!', e.target, e.currentTarget);
- e.stopPropagation();
- // Try button's own playerData first, fallback to DOM traversal
- const playerData = e.currentTarget.playerData || e.target.closest('li.player-item')?.playerData;
- console.log('📊 Player data found:', playerData);
- if (playerData) {
- console.log('📊 Opening stats for:', playerData.character_name);
- showStatsWindow(playerData.character_name);
- } else {
- console.log('📊 No player data found!');
- }
- });
-
- const inventoryBtn = document.createElement('button');
- inventoryBtn.className = 'inventory-btn';
- inventoryBtn.textContent = 'Inventory';
- inventoryBtn.addEventListener('click', (e) => {
- console.log('🎒 INVENTORY BUTTON CLICKED!', e.target, e.currentTarget);
- e.stopPropagation();
- // Try button's own playerData first, fallback to DOM traversal
- const playerData = e.currentTarget.playerData || e.target.closest('li.player-item')?.playerData;
- console.log('🎒 Player data found:', playerData);
- if (playerData) {
- console.log('🎒 Opening inventory for:', playerData.character_name);
- showInventoryWindow(playerData.character_name);
- } else {
- console.log('🎒 No player data found!');
- }
- });
-
- buttonsContainer.appendChild(chatBtn);
- buttonsContainer.appendChild(statsBtn);
- buttonsContainer.appendChild(inventoryBtn);
- li.appendChild(buttonsContainer);
-
- // Store references for easy access
- li.gridContent = gridContent;
- li.chatBtn = chatBtn;
- li.statsBtn = statsBtn;
- li.inventoryBtn = inventoryBtn;
-
- return li;
-}
-
-function returnToPool() {
- // Return unused dots to pool
- elementPools.activeDots.forEach(dot => {
- if (!dot.parentNode) {
- elementPools.dots.push(dot);
- elementPools.activeDots.delete(dot);
- }
- });
-
- // Return unused list items to pool
- elementPools.activeListItems.forEach(li => {
- if (!li.parentNode) {
- elementPools.listItems.push(li);
- elementPools.activeListItems.delete(li);
- }
- });
-}
-
-/* ---------- Event Delegation System ---------------------------- */
-// Single event delegation handler for all player list interactions
-function setupEventDelegation() {
- list.addEventListener('click', e => {
- const li = e.target.closest('li.player-item');
- if (!li || !li.playerData) return;
-
- const player = li.playerData;
- const { x, y } = worldToPx(player.ew, player.ns);
-
- // Handle player selection (clicking anywhere else on the item, not on buttons)
- // Button clicks are now handled by direct event listeners
- if (!e.target.closest('button')) {
- selectPlayer(player, x, y);
- }
- });
-}
-
-// Initialize event delegation when DOM is ready
-document.addEventListener('DOMContentLoaded', setupEventDelegation);
-
// Global drag system to prevent event listener accumulation
let currentDragWindow = null;
let dragStartX = 0, dragStartY = 0, dragStartLeft = 0, dragStartTop = 0;
@@ -726,20 +562,16 @@ function debounce(fn, ms) {
// Show or create a stats window for a character
function showStatsWindow(name) {
- console.log('📊 showStatsWindow called for:', name);
if (statsWindows[name]) {
const existing = statsWindows[name];
- console.log('📊 Existing stats window found, showing it:', existing);
- // Always show the window (no toggle)
- existing.style.display = 'flex';
- // Bring to front when opening
- if (!window.__chatZ) window.__chatZ = 10000;
- window.__chatZ += 1;
- existing.style.zIndex = window.__chatZ;
- console.log('📊 Stats window shown with zIndex:', window.__chatZ);
+ // Toggle: close if already visible, open if hidden
+ if (existing.style.display === 'flex') {
+ existing.style.display = 'none';
+ } else {
+ existing.style.display = 'flex';
+ }
return;
}
- console.log('📊 Creating new stats window for:', name);
const win = document.createElement('div');
win.className = 'stats-window';
win.dataset.character = name;
@@ -783,10 +615,8 @@ function showStatsWindow(name) {
content.className = 'chat-messages';
content.textContent = 'Loading stats...';
win.appendChild(content);
- console.log('📊 Appending stats window to DOM:', win);
document.body.appendChild(win);
statsWindows[name] = win;
- console.log('📊 Stats window added to DOM, total children:', document.body.children.length);
// Load initial stats with default 24h range
updateStatsTimeRange(content, name, 'now-24h');
// Enable dragging using the global drag system
@@ -821,20 +651,16 @@ function updateStatsTimeRange(content, name, timeRange) {
// Show or create an inventory window for a character
function showInventoryWindow(name) {
- console.log('🎒 showInventoryWindow called for:', name);
if (inventoryWindows[name]) {
const existing = inventoryWindows[name];
- console.log('🎒 Existing inventory window found, showing it:', existing);
- // Always show the window (no toggle)
- existing.style.display = 'flex';
- // Bring to front when opening
- if (!window.__chatZ) window.__chatZ = 10000;
- window.__chatZ += 1;
- existing.style.zIndex = window.__chatZ;
- console.log('🎒 Inventory window shown with zIndex:', window.__chatZ);
+ // Toggle: close if already visible, open if hidden
+ if (existing.style.display === 'flex') {
+ existing.style.display = 'none';
+ } else {
+ existing.style.display = 'flex';
+ }
return;
}
- console.log('🎒 Creating new inventory window for:', name);
const win = document.createElement('div');
win.className = 'inventory-window';
win.dataset.character = name;
@@ -1026,10 +852,8 @@ function showInventoryWindow(name) {
console.error('Inventory fetch failed:', err);
});
- console.log('🎒 Appending inventory window to DOM:', win);
document.body.appendChild(win);
inventoryWindows[name] = win;
- console.log('🎒 Inventory window added to DOM, total children:', document.body.children.length);
// Enable dragging using the global drag system
makeDraggable(win, header);
@@ -1349,23 +1173,6 @@ function updateTotalRaresDisplay(data) {
}
}
-async function pollTotalKills() {
- try {
- const response = await fetch(`${API_BASE}/total-kills/`);
- const data = await response.json();
- updateTotalKillsDisplay(data);
- } catch (e) {
- console.error('Total kills fetch failed:', e);
- }
-}
-
-function updateTotalKillsDisplay(data) {
- const killsElement = document.getElementById('totalKillsCount');
- if (killsElement && data.total !== undefined) {
- killsElement.textContent = data.total.toLocaleString();
- }
-}
-
async function pollServerHealth() {
try {
const response = await fetch(`${API_BASE}/server-health`);
@@ -1441,13 +1248,10 @@ function startPolling() {
if (pollID !== null) return;
pollLive();
pollTotalRares(); // Initial fetch
- pollTotalKills(); // Initial fetch
pollServerHealth(); // Initial server health check
pollID = setInterval(pollLive, POLL_MS);
// Poll total rares every 5 minutes (300,000 ms)
setInterval(pollTotalRares, 300000);
- // Poll total kills every 5 minutes (300,000 ms)
- setInterval(pollTotalKills, 300000);
// Poll server health every 30 seconds (30,000 ms)
setInterval(pollServerHealth, 30000);
}
@@ -1488,64 +1292,9 @@ function renderList() {
render(sorted);
}
-// Track when user might be interacting to avoid DOM manipulation during clicks
-let userInteracting = false;
-let interactionTimeout = null;
-
-// Add global mousedown/mouseup tracking to detect when user is clicking
-document.addEventListener('mousedown', () => {
- userInteracting = true;
- if (interactionTimeout) clearTimeout(interactionTimeout);
-});
-
-document.addEventListener('mouseup', () => {
- // Give a small buffer after mouseup to ensure click events complete
- if (interactionTimeout) clearTimeout(interactionTimeout);
- interactionTimeout = setTimeout(() => {
- userInteracting = false;
- }, 50); // 50ms buffer
-});
-
function render(players) {
- const startTime = performance.now();
- console.log('🔄 RENDER STARTING:', new Date().toISOString());
-
- // If user is actively clicking, defer this render briefly
- if (userInteracting) {
- console.log('🔄 RENDER DEFERRED: User interaction detected');
- setTimeout(() => render(players), 100);
- return;
- }
-
- // Reset per-render stats
- performanceStats.renderDotsCreated = 0;
- performanceStats.renderDotsReused = 0;
- performanceStats.renderListItemsCreated = 0;
- performanceStats.renderListItemsReused = 0;
- performanceStats.renderCount++;
-
- // Get existing elements and map them by player name for reuse
- const existingDots = Array.from(dots.children);
- const existingListItems = Array.from(list.children);
-
- // Create maps for efficient lookup by player name
- const dotsByPlayer = new Map();
- const listItemsByPlayer = new Map();
-
- existingDots.forEach(dot => {
- if (dot.playerData && dot.playerData.character_name) {
- dotsByPlayer.set(dot.playerData.character_name, dot);
- }
- });
-
- existingListItems.forEach(li => {
- if (li.playerData && li.playerData.character_name) {
- listItemsByPlayer.set(li.playerData.character_name, li);
- }
- });
-
-
- // DON'T clear containers - we need to reuse elements
+ dots.innerHTML = '';
+ list.innerHTML = '';
// Update header with active player count
const header = document.getElementById('activePlayersHeader');
@@ -1558,12 +1307,12 @@ function render(players) {
const kphElement = document.getElementById('serverKphCount');
if (kphElement) {
// Format with commas and one decimal place for EPIC display
- const formattedKPH = totalKPH.toLocaleString('en-US', {
- minimumFractionDigits: 1,
- maximumFractionDigits: 1
+ const formattedKPH = totalKPH.toLocaleString('en-US', {
+ minimumFractionDigits: 1,
+ maximumFractionDigits: 1
});
kphElement.textContent = formattedKPH;
-
+
// Add extra epic effect for high KPH
const container = document.getElementById('serverKphCounter');
if (container) {
@@ -1575,89 +1324,49 @@ function render(players) {
}
}
- // Total kills is now fetched from the /total-kills/ API endpoint
- // (see pollTotalKills function) to include ALL characters, not just online ones
+ // Calculate and update total kills
+ const totalKills = players.reduce((sum, p) => sum + (p.total_kills || 0), 0);
+ const killsElement = document.getElementById('totalKillsCount');
+ if (killsElement) {
+ // Format with commas for readability
+ const formattedKills = totalKills.toLocaleString();
+ killsElement.textContent = formattedKills;
+ }
- players.forEach((p) => {
+ players.forEach(p => {
const { x, y } = worldToPx(p.ew, p.ns);
- // Reuse existing dot by player name or create new one
- let dot = dotsByPlayer.get(p.character_name);
- if (!dot) {
- dot = createNewDot();
- dots.appendChild(dot);
- } else {
- performanceStats.dotsReused++;
- performanceStats.renderDotsReused++;
- // Remove from the map so we don't count it as unused later
- dotsByPlayer.delete(p.character_name);
- }
-
- // Update dot properties
- dot.style.left = `${x}px`;
- dot.style.top = `${y}px`;
+ // dot
+ const dot = document.createElement('div');
+ dot.className = 'dot';
+ dot.style.left = `${x}px`;
+ dot.style.top = `${y}px`;
dot.style.background = getColorFor(p.character_name);
- dot.playerData = p; // Store for event handlers
- // Update highlight state
- if (p.character_name === selected) {
- dot.classList.add('highlight');
- } else {
- dot.classList.remove('highlight');
- }
+
- // Reuse existing list item by player name or create new one
- let li = listItemsByPlayer.get(p.character_name);
- if (!li) {
- li = createNewListItem();
- list.appendChild(li);
- } else {
- performanceStats.listItemsReused++;
- performanceStats.renderListItemsReused++;
- // Remove from the map so we don't count it as unused later
- listItemsByPlayer.delete(p.character_name);
- }
+ // custom tooltip
+ dot.addEventListener('mouseenter', e => showTooltip(e, p));
+ dot.addEventListener('mousemove', e => showTooltip(e, p));
+ dot.addEventListener('mouseleave', hideTooltip);
+
+ // click to select/zoom
+ dot.addEventListener('click', () => selectPlayer(p, x, y));
+
+ if (p.character_name === selected) dot.classList.add('highlight');
+ dots.appendChild(dot);
+ //sidebar
+ const li = document.createElement('li');
const color = getColorFor(p.character_name);
li.style.borderLeftColor = color;
- li.playerData = p; // Store for event handlers BEFORE any DOM movement
-
- // Also store playerData directly on buttons for more reliable access
- if (li.chatBtn) li.chatBtn.playerData = p;
- if (li.statsBtn) li.statsBtn.playerData = p;
- if (li.inventoryBtn) li.inventoryBtn.playerData = p;
-
- // Only reorder element if it's actually out of place for current sort order
- // Check if this element needs to be moved to maintain sort order
- const expectedIndex = players.indexOf(p);
- const currentIndex = Array.from(list.children).indexOf(li);
-
- if (currentIndex !== expectedIndex && li.parentNode) {
- // Find the correct position to insert
- if (expectedIndex === players.length - 1) {
- // Should be last - only move if it's not already last
- if (li !== list.lastElementChild) {
- list.appendChild(li);
- }
- } else {
- // Should be at a specific position
- const nextPlayer = players[expectedIndex + 1];
- const nextElement = Array.from(list.children).find(el =>
- el.playerData && el.playerData.character_name === nextPlayer.character_name
- );
- if (nextElement && li.nextElementSibling !== nextElement) {
- list.insertBefore(li, nextElement);
- }
- }
- }
-
+ li.className = 'player-item';
// Calculate KPR (Kills Per Rare)
- const playerTotalKills = p.total_kills || 0;
+ const totalKills = p.total_kills || 0;
const totalRares = p.total_rares || 0;
- const kpr = totalRares > 0 ? Math.round(playerTotalKills / totalRares) : '∞';
-
- // Update only the grid content via innerHTML (buttons preserved)
- li.gridContent.innerHTML = `
+ const kpr = totalRares > 0 ? Math.round(totalKills / totalRares) : '∞';
+
+ li.innerHTML = `
${p.character_name}${createVitaeIndicator(p.character_name)} ${loc(p.ns, p.ew)}
${createVitalsHTML(p.character_name)}
${p.kills}
@@ -1676,61 +1385,44 @@ function render(players) {
if (metaSpan) {
const goodStates = ['default', 'default2', 'hunt', 'combat'];
const state = (p.vt_state || '').toString().toLowerCase();
- metaSpan.classList.remove('green', 'red'); // Clear previous
if (goodStates.includes(state)) {
metaSpan.classList.add('green');
} else {
metaSpan.classList.add('red');
}
}
-
- // Update selected state
- if (p.character_name === selected) {
- li.classList.add('selected');
- } else {
- li.classList.remove('selected');
- }
+
+ li.addEventListener('click', () => selectPlayer(p, x, y));
+ if (p.character_name === selected) li.classList.add('selected');
+ // Chat button
+ const chatBtn = document.createElement('button');
+ chatBtn.className = 'chat-btn';
+ chatBtn.textContent = 'Chat';
+ chatBtn.addEventListener('click', e => {
+ e.stopPropagation();
+ showChatWindow(p.character_name);
+ });
+ li.appendChild(chatBtn);
+ // Stats button
+ const statsBtn = document.createElement('button');
+ statsBtn.className = 'stats-btn';
+ statsBtn.textContent = 'Stats';
+ statsBtn.addEventListener('click', e => {
+ e.stopPropagation();
+ showStatsWindow(p.character_name);
+ });
+ li.appendChild(statsBtn);
+ // Inventory button
+ const inventoryBtn = document.createElement('button');
+ inventoryBtn.className = 'inventory-btn';
+ inventoryBtn.textContent = 'Inventory';
+ inventoryBtn.addEventListener('click', e => {
+ e.stopPropagation();
+ showInventoryWindow(p.character_name);
+ });
+ li.appendChild(inventoryBtn);
+ list.appendChild(li);
});
-
- // Remove unused elements (any elements left in the maps are unused)
- // These are dots for players that are no longer in the current player list
- dotsByPlayer.forEach((dot, playerName) => {
- dots.removeChild(dot);
- });
-
- // These are list items for players that are no longer in the current player list
- listItemsByPlayer.forEach((li, playerName) => {
- list.removeChild(li);
- });
-
- // Update performance stats
- performanceStats.lastRenderTime = performance.now() - startTime;
-
- // Determine optimization status
- const totalCreatedThisRender = performanceStats.renderDotsCreated + performanceStats.renderListItemsCreated;
- const totalReusedThisRender = performanceStats.renderDotsReused + performanceStats.renderListItemsReused;
- const isOptimized = totalCreatedThisRender === 0 && totalReusedThisRender > 0;
- const isPartiallyOptimized = totalCreatedThisRender < players.length && totalReusedThisRender > 0;
-
- // Choose icon and color
- let statusIcon = '🚀';
- let colorStyle = '';
- if (isOptimized) {
- statusIcon = '✨';
- colorStyle = 'background: #1a4a1a; color: #4ade80; font-weight: bold;';
- } else if (isPartiallyOptimized) {
- statusIcon = '⚡';
- colorStyle = 'background: #4a3a1a; color: #fbbf24; font-weight: bold;';
- } else {
- statusIcon = '🔥';
- colorStyle = 'background: #4a1a1a; color: #f87171; font-weight: bold;';
- }
-
- // Performance stats are tracked but not logged to keep console clean
- // Optimization is achieving 100% element reuse consistently
-
- const renderTime = performance.now() - startTime;
- console.log('🔄 RENDER COMPLETED:', renderTime.toFixed(2) + 'ms');
}
/* ---------- rendering trails ------------------------------- */
@@ -1798,20 +1490,20 @@ function initWebSocket() {
// Display or create a chat window for a character
function showChatWindow(name) {
- console.log('💬 showChatWindow called for:', name);
if (chatWindows[name]) {
const existing = chatWindows[name];
- console.log('💬 Existing chat window found, showing it:', existing);
- // Always show the window (no toggle)
- existing.style.display = 'flex';
- // Bring to front when opening
- if (!window.__chatZ) window.__chatZ = 10000;
- window.__chatZ += 1;
- existing.style.zIndex = window.__chatZ;
- console.log('💬 Chat window shown with zIndex:', window.__chatZ);
+ // Toggle: close if already visible, open if hidden
+ if (existing.style.display === 'flex') {
+ existing.style.display = 'none';
+ } else {
+ existing.style.display = 'flex';
+ // Bring to front when opening
+ if (!window.__chatZ) window.__chatZ = 10000;
+ window.__chatZ += 1;
+ existing.style.zIndex = window.__chatZ;
+ }
return;
}
- console.log('💬 Creating new chat window for:', name);
const win = document.createElement('div');
win.className = 'chat-window';
win.dataset.character = name;
@@ -1848,10 +1540,8 @@ function showChatWindow(name) {
input.value = '';
});
win.appendChild(form);
- console.log('💬 Appending chat window to DOM:', win);
document.body.appendChild(win);
chatWindows[name] = win;
- console.log('💬 Chat window added to DOM, total children:', document.body.children.length);
// Enable dragging using the global drag system
makeDraggable(win, header);