mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-19 15:18:33 +02:00
feat: add smirk toggle and more admin capabilities
This commit is contained in:
+109
-1
@@ -280,6 +280,7 @@
|
||||
<!-- Tabs -->
|
||||
<div class="tabs">
|
||||
<button class="tab-btn active" data-tab="overview">Overview</button>
|
||||
<button class="tab-btn" data-tab="queue">Queue</button>
|
||||
<button class="tab-btn" data-tab="payouts">Payouts</button>
|
||||
<button class="tab-btn" data-tab="games">Games</button>
|
||||
<button class="tab-btn" data-tab="users">Users</button>
|
||||
@@ -293,6 +294,34 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Queue -->
|
||||
<div id="tab-queue" class="tab-content">
|
||||
<div class="table-container">
|
||||
<div class="table-header">
|
||||
<h3>Game Queue</h3>
|
||||
<button onclick="refreshQueue()" style="background:#055; color:#0f0; border:1px solid #0f0; padding:4px 10px; cursor:pointer;">🔄 Refresh</button>
|
||||
</div>
|
||||
<p style="color:#888; margin: 10px 0; font-size:12px;">
|
||||
Entries waiting more than 10 minutes are highlighted in orange. Remove stuck entries that will never confirm.
|
||||
</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Server ID</th>
|
||||
<th>Waiting</th>
|
||||
<th>Payment ID</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="queueTable">
|
||||
<tr><td colspan="6" class="loading">Loading...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Payouts -->
|
||||
<div id="tab-payouts" class="tab-content">
|
||||
<div class="table-container">
|
||||
@@ -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 = '<tr><td colspan="6" class="loading" style="color:#0f0;">Queue is empty - no waiting players</td></tr>';
|
||||
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 \`
|
||||
<tr style="\${rowStyle}">
|
||||
<td>\${entry.position}</td>
|
||||
<td title="\${entry.serverId}">\${entry.serverId.substring(0, 12)}...</td>
|
||||
<td style="\${isStuck ? 'color:#f80; font-weight:bold;' : ''}">\${formatWait(entry.waitingSeconds)}</td>
|
||||
<td>\${entry.paymentId || '-'}</td>
|
||||
<td>\${statusText}</td>
|
||||
<td>
|
||||
<button class="btn btn-danger" onclick="removeQueueEntry('\${entry.serverId}')" style="background:#500; color:#f88; border:1px solid #f55; padding:3px 8px; cursor:pointer;">
|
||||
Remove
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
\`;
|
||||
}).join('');
|
||||
} catch (e) {
|
||||
console.error('Failed to load queue:', e);
|
||||
document.getElementById('queueTable').innerHTML = '<tr><td colspan="6" class="loading" style="color:#f00;">Failed to load queue</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
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}` : ''}`;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+14
-1
@@ -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!
|
||||
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user