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 @@
+
+
+
+ Entries waiting more than 10 minutes are highlighted in orange. Remove stuck entries that will never confirm.
+
+
+
+
+ | # |
+ Server ID |
+ Waiting |
+ Payment ID |
+ Status |
+ Actions |
+
+
+
+ | 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;