From 6cf3961da0278f23f895cc06541fa740759a5599 Mon Sep 17 00:00:00 2001 From: jwinterm Date: Mon, 2 Feb 2026 08:42:39 -0500 Subject: [PATCH] feat: add smirk toggle and more admin capabilities --- html/admin.html | 110 +++++++++++++++++++++++++++++- html/js/network/smirkAuth.js | 27 ++++++-- html/js/network/socketHandlers.js | 8 ++- src/.env.example | 15 +++- src/game/gameModeManager.js | 4 +- src/index.js | 85 +++++++++++++++++++++++ src/network/queueManager.js | 20 ++++++ 7 files changed, 261 insertions(+), 8 deletions(-) diff --git a/html/admin.html b/html/admin.html index f6db16d..6ce39dc 100644 --- a/html/admin.html +++ b/html/admin.html @@ -280,6 +280,7 @@
+ @@ -293,6 +294,34 @@
+ +
+
+
+

Game Queue

+ +
+

+ Entries waiting more than 10 minutes are highlighted in orange. Remove stuck entries that will never confirm. +

+ + + + + + + + + + + + + + +
#Server IDWaitingPayment IDStatusActions
Loading...
+
+
+
@@ -467,7 +496,8 @@ document.getElementById(`tab-${tab}`).classList.add('active'); // Load data for tab - if (tab === 'payouts') loadPayouts(); + if (tab === 'queue') loadQueue(); + else if (tab === 'payouts') loadPayouts(); else if (tab === 'games') loadGames(); else if (tab === 'users') loadUsers(); } @@ -563,6 +593,84 @@ }); } + // Queue management functions + async function loadQueue() { + try { + const res = await fetch('/api/admin/queue', { headers: { 'X-Admin-Key': apiKey } }); + const data = await res.json(); + + const tbody = document.getElementById('queueTable'); + if (!data.queue?.length) { + tbody.innerHTML = 'Queue is empty - no waiting players'; + return; + } + + tbody.innerHTML = data.queue.map(entry => { + const isStuck = entry.waitingSeconds > 600; // > 10 minutes + const rowStyle = isStuck ? 'background: rgba(255,100,0,0.2);' : ''; + const statusText = entry.confirmed ? '✅ Confirmed' : + entry.requiresConfirmation ? '⏳ Awaiting Confirmation' : '🎮 Ready'; + + const formatWait = (sec) => { + if (sec < 60) return sec + 's'; + if (sec < 3600) return Math.floor(sec / 60) + 'm ' + (sec % 60) + 's'; + return Math.floor(sec / 3600) + 'h ' + Math.floor((sec % 3600) / 60) + 'm'; + }; + + return \` + + \${entry.position} + \${entry.serverId.substring(0, 12)}... + \${formatWait(entry.waitingSeconds)} + \${entry.paymentId || '-'} + \${statusText} + + + + + \`; + }).join(''); + } catch (e) { + console.error('Failed to load queue:', e); + document.getElementById('queueTable').innerHTML = 'Failed to load queue'; + } + } + + function refreshQueue() { + loadQueue(); + } + + async function removeQueueEntry(serverId) { + if (!confirm(\`Remove queue entry for \${serverId.substring(0, 12)}...? This will cancel their queue position.\`)) return; + + const reason = prompt('Reason for removal (optional):') || 'Admin removal'; + + try { + const res = await fetch('/api/admin/queue/remove', { + method: 'POST', + headers: { + 'X-Admin-Key': apiKey, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ serverId, reason }) + }); + + const data = await res.json(); + + if (res.ok && data.success) { + alert('Queue entry removed successfully'); + loadQueue(); + loadDashboard(); // Refresh overview stats + } else { + alert('Error: ' + (data.message || 'Unknown error')); + } + } catch (e) { + alert('Failed to remove queue entry: ' + e.message); + } + } + async function loadPayouts() { const status = document.getElementById('payoutStatusFilter').value; const url = `/api/admin/stats/payouts?limit=50${status ? `&status=${status}` : ''}`; diff --git a/html/js/network/smirkAuth.js b/html/js/network/smirkAuth.js index b68f8c7..8159fa6 100644 --- a/html/js/network/smirkAuth.js +++ b/html/js/network/smirkAuth.js @@ -231,6 +231,18 @@ const SmirkAuth = { * Should be called after DOM is ready and socket is connected */ init() { + // Check if server has Smirk disabled (e.g., for Monero stagenet) + if (typeof SocketHandlers !== 'undefined' && SocketHandlers._smirkEnabled === false) { + console.log('Smirk integration disabled by server'); + return; + } + + // Prevent double-initialization + if (this._initialized) { + return; + } + this._initialized = true; + const container = $('#smirkButtonContainer'); if (!container.length) { console.log('Smirk button container not found'); @@ -291,12 +303,19 @@ const SmirkAuth = { } }; -// Auto-initialize when DOM is ready +// Auto-initialize when DOM is ready (with delay to allow game_mode_info to arrive first) $(document).ready(function() { - // Wait a short moment for socket to connect before initializing + // Wait for game_mode_info to arrive first (typically within 2 seconds) + // SocketHandlers will call SmirkAuth.init() when it receives game_mode_info + // This fallback is for cases where game_mode_info doesn't include smirkEnabled setTimeout(() => { - SmirkAuth.init(); - }, 1000); + // Only auto-init if not already initialized by SocketHandlers + // and if SocketHandlers._smirkEnabled is not explicitly false + if (!SmirkAuth._initialized && + (typeof SocketHandlers === 'undefined' || SocketHandlers._smirkEnabled !== false)) { + SmirkAuth.init(); + } + }, 3000); }); // Also re-check when socket connects (in case of reconnection) diff --git a/html/js/network/socketHandlers.js b/html/js/network/socketHandlers.js index 5f5fb9c..0ca7463 100644 --- a/html/js/network/socketHandlers.js +++ b/html/js/network/socketHandlers.js @@ -556,7 +556,13 @@ const SocketHandlers = { SocketHandlers._creditsPayoutsEnabled = !!data.creditsPayoutsEnabled; SocketHandlers._directPayoutsEnabled = !!data.directPayoutsEnabled; SocketHandlers._paymentsEnabled = !!data.paymentsEnabled; - + SocketHandlers._smirkEnabled = data.smirkEnabled !== false; // Default to true if not specified + + // Initialize Smirk auth if enabled and not already initialized + if (SocketHandlers._smirkEnabled && typeof SmirkAuth !== 'undefined' && !SmirkAuth._initialized) { + SmirkAuth.init(); + } + if (typeof UI !== 'undefined' && UI.updateGameTitle) { UI.updateGameTitle(data.cryptoType); } diff --git a/src/.env.example b/src/.env.example index 8717a04..9d01f57 100644 --- a/src/.env.example +++ b/src/.env.example @@ -354,7 +354,20 @@ HIGH_PENDING_THRESHOLD=10 ALERT_COOLDOWN_MS=3600000 # ============================================================================= -# SECTION 14: DEVELOPMENT/DEBUG OPTIONS +# SECTION 14: SMIRK WALLET INTEGRATION +# ============================================================================= +# Smirk is a browser extension wallet for Wownero/Monero. +# https://smirk.network +# +# Enable this for Wownero mainnet. Disable for Monero stagenet +# (Smirk doesn't support stagenet addresses). + +# Enable Smirk wallet connection button (default: true) +# Set to false for Monero stagenet deployments +SMIRK_ENABLED=true + +# ============================================================================= +# SECTION 15: DEVELOPMENT/DEBUG OPTIONS # ============================================================================= # These are for testing only. Don't use in production with real money! diff --git a/src/game/gameModeManager.js b/src/game/gameModeManager.js index 1e520e7..8d7f319 100644 --- a/src/game/gameModeManager.js +++ b/src/game/gameModeManager.js @@ -1194,7 +1194,9 @@ class GameModeManager { enabled: paymentConfig.earlyEntry?.enabled ?? false, allowInFreeMode: paymentConfig.earlyEntry?.allowInFreeMode ?? false, allowInCreditsMode: paymentConfig.earlyEntry?.allowInCreditsMode ?? false - } + }, + // Smirk wallet integration (disabled for Monero stagenet) + smirkEnabled: process.env.SMIRK_ENABLED !== 'false' }; } diff --git a/src/index.js b/src/index.js index 5ddaafa..16be69b 100644 --- a/src/index.js +++ b/src/index.js @@ -444,7 +444,11 @@ app.get('/api/user/:socketId/payouts', asyncHandler(async (req, res) => { // ============================================================================= // Smirk Wallet Authentication Endpoints // ============================================================================= +// Smirk is a browser extension wallet for Wownero/Monero. +// Disable for Monero stagenet deployments (Smirk doesn't support stagenet). +const smirkEnabled = process.env.SMIRK_ENABLED !== 'false'; +if (smirkEnabled) { /** * POST /api/auth/smirk/challenge * Generate a challenge for Smirk wallet signature verification @@ -604,6 +608,8 @@ app.get('/api/auth/smirk/status', asyncHandler(async (req, res) => { }); })); +} // End of smirkEnabled block + // ============================================================================= // Admin API Endpoints (requires ADMIN_API_KEY) // ============================================================================= @@ -911,6 +917,85 @@ app.post('/api/admin/alerts/test-email', adminAuth, asyncHandler(async (req, res }); })); +// ============================================================================= +// Admin Queue Management +// ============================================================================= + +/** + * GET /api/admin/queue + * Get current queue details for admin dashboard + * Returns full queue entries (not anonymized) + */ +app.get('/api/admin/queue', adminAuth, asyncHandler(async (req, res) => { + if (!socketHandlers?.queueManager) { + return res.status(503).json({ + success: false, + message: 'Queue manager not available' + }); + } + + const queueDetails = socketHandlers.queueManager.getQueueDetailsForAdmin(); + + res.json({ + success: true, + queue: queueDetails, + count: queueDetails.length + }); +})); + +/** + * POST /api/admin/queue/remove + * Remove a stuck queue entry by serverId + * Body: { serverId: string, reason?: string } + */ +app.post('/api/admin/queue/remove', adminAuth, asyncHandler(async (req, res) => { + const { serverId, reason } = req.body || {}; + + if (!serverId || typeof serverId !== 'string') { + throw new ValidationError('Missing serverId', { + safeMessage: 'serverId is required to remove a queue entry.' + }); + } + + if (!socketHandlers?.queueManager) { + return res.status(503).json({ + success: false, + message: 'Queue manager not available' + }); + } + + // Check if entry exists first + const isQueued = socketHandlers.queueManager.isPlayerQueued(serverId); + if (!isQueued) { + return res.status(404).json({ + success: false, + message: `No queue entry found for serverId: ${serverId}` + }); + } + + // Remove the entry + const removed = socketHandlers.queueManager.removePlayer(serverId); + + // Log admin action + console.log(`[Admin] Queue entry removed: serverId=${serverId}, reason="${reason || 'No reason provided'}", by admin at ${new Date().toISOString()}`); + + // Optionally notify the user via socket (if still connected) + try { + io.to(serverId).emit('queue_cancelled', { + reason: 'admin_removed', + message: 'Your queue entry was removed by an administrator.' + }); + } catch (notifyErr) { + // Ignore errors - user may be disconnected + } + + res.json({ + success: removed, + message: removed ? 'Queue entry removed successfully' : 'Failed to remove queue entry', + serverId: serverId + }); +})); + /** * GET /api/admin/users/search * Search for users by socket ID prefix or payout address diff --git a/src/network/queueManager.js b/src/network/queueManager.js index 5ae4c16..baf9b70 100644 --- a/src/network/queueManager.js +++ b/src/network/queueManager.js @@ -288,6 +288,26 @@ class QueueManager { waitingForConfirmation: entry.requiresConfirmation && !entry.confirmed })); } + + /** + * Get full queue details for admin dashboard (not anonymized) + * @returns {Array} Full queue entries with all details + */ + getQueueDetailsForAdmin() { + const now = Date.now(); + return this._waitingPlayers.map((entry, index) => ({ + position: index + 1, + serverId: entry.serverId, + playerId: entry.serverId.substring(0, 6), // For display + entryTime: entry.entryTime ? new Date(entry.entryTime).toISOString() : null, + waitingSeconds: entry.entryTime ? Math.floor((now - entry.entryTime) / 1000) : 0, + paymentId: entry.paymentId || null, + userId: entry.userId || null, + requiresConfirmation: !!entry.requiresConfirmation, + confirmed: !!entry.confirmed, + isValuable: this._isValuableEntry(entry) + })); + } } module.exports = QueueManager;