Improve user lookup functionality with enhanced data retrieval and UI updates

Implement GET /user-lookup/:query endpoint to aggregate Supabase user data (profile, warnings, mod history), and update the dashboard UI for a better user lookup experience.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: d918c1a9-0afe-4951-a804-0a3b0e38f04a
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/ocC7ZpF
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
sirpiglr 2025-12-08 04:56:13 +00:00
parent 43394c8fe1
commit aa291b3064
2 changed files with 446 additions and 5 deletions

View file

@ -1565,6 +1565,179 @@ http
return;
}
// GET /user-lookup/:query - Search for a user by ID or username
if (req.url.startsWith("/user-lookup/") && req.method === "GET") {
const query = decodeURIComponent(req.url.split("/user-lookup/")[1].split("?")[0]);
if (!query || query.length < 2) {
res.writeHead(400);
res.end(JSON.stringify({ error: "Query too short" }));
return;
}
(async () => {
try {
const results = [];
const isNumericId = /^\d{17,19}$/.test(query);
for (const guild of client.guilds.cache.values()) {
try {
if (isNumericId) {
const member = await guild.members.fetch(query).catch(() => null);
if (member) {
const existingIdx = results.findIndex(r => r.id === member.id);
if (existingIdx === -1) {
results.push({
id: member.id,
tag: member.user.tag,
username: member.user.username,
displayName: member.displayName,
avatar: member.user.displayAvatarURL({ size: 128 }),
bot: member.user.bot,
createdAt: member.user.createdAt.toISOString(),
joinedAt: member.joinedAt?.toISOString(),
roles: member.roles.cache.filter(r => r.name !== '@everyone').map(r => ({ id: r.id, name: r.name, color: r.hexColor })).slice(0, 10),
servers: [{ id: guild.id, name: guild.name }],
heat: getHeat(member.id),
});
} else {
results[existingIdx].servers.push({ id: guild.id, name: guild.name });
}
}
} else {
const members = await guild.members.fetch({ query, limit: 10 }).catch(() => new Map());
for (const member of members.values()) {
const existingIdx = results.findIndex(r => r.id === member.id);
if (existingIdx === -1) {
results.push({
id: member.id,
tag: member.user.tag,
username: member.user.username,
displayName: member.displayName,
avatar: member.user.displayAvatarURL({ size: 128 }),
bot: member.user.bot,
createdAt: member.user.createdAt.toISOString(),
joinedAt: member.joinedAt?.toISOString(),
roles: member.roles.cache.filter(r => r.name !== '@everyone').map(r => ({ id: r.id, name: r.name, color: r.hexColor })).slice(0, 10),
servers: [{ id: guild.id, name: guild.name }],
heat: getHeat(member.id),
});
} else {
results[existingIdx].servers.push({ id: guild.id, name: guild.name });
}
}
}
} catch (e) {}
}
// Fetch Supabase profile data if available
if (supabase && results.length > 0) {
for (const user of results) {
try {
const { data: link } = await supabase
.from('discord_links')
.select('user_id, primary_arm')
.eq('discord_id', user.id)
.single();
if (link) {
user.linked = true;
user.realm = link.primary_arm;
const { data: profile } = await supabase
.from('user_profiles')
.select('username, xp, daily_streak, badges')
.eq('id', link.user_id)
.single();
if (profile) {
user.aethexUsername = profile.username;
user.xp = profile.xp || 0;
user.level = Math.floor(Math.sqrt((profile.xp || 0) / 100));
user.dailyStreak = profile.daily_streak || 0;
user.badges = profile.badges || [];
}
}
const { data: warnings } = await supabase
.from('warnings')
.select('id, reason, created_at')
.eq('user_id', user.id)
.order('created_at', { ascending: false })
.limit(5);
user.warnings = warnings || [];
const { data: modActions } = await supabase
.from('mod_actions')
.select('action, reason, created_at')
.eq('user_id', user.id)
.order('created_at', { ascending: false })
.limit(5);
user.modHistory = modActions || [];
} catch (e) {}
}
}
res.writeHead(200);
res.end(JSON.stringify({
results: results.slice(0, 20),
count: results.length,
query,
timestamp: new Date().toISOString(),
}));
} catch (error) {
res.writeHead(500);
res.end(JSON.stringify({ error: error.message }));
}
})();
return;
}
// GET /mod-actions - Get recent moderation actions
if (req.url === "/mod-actions" && req.method === "GET") {
if (!supabase) {
res.writeHead(200);
res.end(JSON.stringify({ success: false, message: "Supabase not configured", actions: [] }));
return;
}
(async () => {
try {
const { data: actions, error } = await supabase
.from('mod_actions')
.select('*')
.order('created_at', { ascending: false })
.limit(50);
if (error) throw error;
const { count: warnCount } = await supabase.from('warnings').select('*', { count: 'exact', head: true });
const { count: banCount } = await supabase.from('mod_actions').select('*', { count: 'exact', head: true }).eq('action', 'ban');
const { count: kickCount } = await supabase.from('mod_actions').select('*', { count: 'exact', head: true }).eq('action', 'kick');
const { count: timeoutCount } = await supabase.from('mod_actions').select('*', { count: 'exact', head: true }).eq('action', 'timeout');
res.writeHead(200);
res.end(JSON.stringify({
success: true,
actions: actions || [],
counts: {
warnings: warnCount || 0,
bans: banCount || 0,
kicks: kickCount || 0,
timeouts: timeoutCount || 0,
},
timestamp: new Date().toISOString(),
}));
} catch (error) {
res.writeHead(500);
res.end(JSON.stringify({ success: false, error: error.message }));
}
})();
return;
}
res.writeHead(404);
res.end(JSON.stringify({ error: "Not found" }));
})

View file

@ -1548,20 +1548,37 @@
<section class="section" id="section-users">
<div class="card">
<div class="card-header">
<h3 class="card-title">User Lookup</h3>
<h3 class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
User Lookup
</h3>
</div>
<div class="card-body">
<div style="max-width: 400px; margin-bottom: 1.5rem;">
<input type="text" class="search-input" placeholder="Enter User ID or Username..." id="userSearch">
<div style="display: flex; gap: 0.75rem; max-width: 500px; margin-bottom: 1.5rem;">
<input type="text" class="search-input" placeholder="Enter User ID or Username..." id="userSearch" onkeypress="if(event.key==='Enter')searchUser()">
<button class="btn btn-primary" onclick="searchUser()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:16px;height:16px"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
Search
</button>
</div>
<div id="userSearchLoading" style="display: none;" class="loading"><div class="spinner"></div>Searching users...</div>
<div id="userResult">
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
<p>Search for a user to view their profile</p>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
<p>Search for a user by Discord ID or username</p>
<p style="font-size: 0.8rem; margin-top: 0.5rem;">View profile, XP, warnings, and mod history</p>
</div>
</div>
</div>
</div>
<div id="userProfileCard" class="card" style="display: none;">
<div class="card-header">
<h3 class="card-title">User Profile</h3>
<button class="btn btn-secondary btn-sm" onclick="clearUserSearch()">Clear</button>
</div>
<div class="card-body" id="userProfileContent"></div>
</div>
</section>
<!-- Moderation Section -->
@ -2771,6 +2788,257 @@
}
}
// ============================================
// USER LOOKUP FUNCTIONS
// ============================================
let currentUserResults = [];
async function searchUser() {
const query = document.getElementById('userSearch').value.trim();
if (!query || query.length < 2) {
showToast('Please enter at least 2 characters', 'warning');
return;
}
document.getElementById('userSearchLoading').style.display = 'flex';
document.getElementById('userResult').style.display = 'none';
document.getElementById('userProfileCard').style.display = 'none';
try {
const response = await fetch(`/user-lookup/${encodeURIComponent(query)}`);
const data = await response.json();
currentUserResults = data.results || [];
document.getElementById('userSearchLoading').style.display = 'none';
document.getElementById('userResult').style.display = 'block';
if (currentUserResults.length === 0) {
document.getElementById('userResult').innerHTML = `
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
<p>No users found for "${query}"</p>
<p style="font-size: 0.8rem; margin-top: 0.5rem;">Try a Discord ID or different username</p>
</div>
`;
return;
}
if (currentUserResults.length === 1) {
showUserProfile(currentUserResults[0]);
} else {
document.getElementById('userResult').innerHTML = `
<p style="margin-bottom: 1rem; color: var(--text-muted);">Found ${currentUserResults.length} users:</p>
${currentUserResults.map((user, idx) => `
<div class="server-item" style="cursor: pointer;" onclick="showUserProfile(currentUserResults[${idx}])">
<div class="leaderboard-avatar">
${user.avatar ? `<img src="${user.avatar}" alt="">` : user.username.charAt(0).toUpperCase()}
</div>
<div class="server-info">
<div class="server-name">${user.username} ${user.bot ? '<span class="badge badge-info" style="margin-left: 0.5rem;">BOT</span>' : ''}</div>
<div class="server-members" style="font-family: monospace;">${user.id}</div>
</div>
<span class="badge ${user.linked ? 'badge-success' : 'badge-warning'}">${user.linked ? 'Linked' : 'Not Linked'}</span>
</div>
`).join('')}
`;
}
} catch (error) {
console.error('User search failed:', error);
document.getElementById('userSearchLoading').style.display = 'none';
document.getElementById('userResult').style.display = 'block';
document.getElementById('userResult').innerHTML = `
<div class="empty-state">
<p>Search failed. Please try again.</p>
</div>
`;
}
}
function showUserProfile(user) {
document.getElementById('userResult').innerHTML = `
<div class="empty-state">
<p>Profile shown below</p>
</div>
`;
document.getElementById('userProfileCard').style.display = 'block';
const heatClass = user.heat >= 3 ? 'danger' : user.heat >= 1 ? 'warning' : 'success';
const rolesHtml = user.roles && user.roles.length > 0
? user.roles.map(r => `<span class="badge" style="background: ${r.color}20; color: ${r.color}; margin-right: 0.25rem; margin-bottom: 0.25rem;">${r.name}</span>`).join('')
: '<span class="badge badge-warning">No roles</span>';
const serversHtml = user.servers && user.servers.length > 0
? user.servers.map(s => `<span class="badge badge-info" style="margin-right: 0.25rem;">${s.name}</span>`).join('')
: 'None';
const warningsHtml = user.warnings && user.warnings.length > 0
? user.warnings.map(w => `
<div class="activity-item">
<div class="activity-icon stat-icon orange">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/></svg>
</div>
<div class="activity-content">
<div class="activity-text">${w.reason || 'No reason provided'}</div>
<div class="activity-time">${formatTimeAgo(w.created_at)}</div>
</div>
</div>
`).join('')
: '<p style="color: var(--text-muted);">No warnings</p>';
const modHistoryHtml = user.modHistory && user.modHistory.length > 0
? user.modHistory.map(m => `
<div class="activity-item">
<div class="activity-icon stat-icon ${m.action === 'ban' ? 'red' : m.action === 'kick' ? 'purple' : 'blue'}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
</div>
<div class="activity-content">
<div class="activity-text"><strong>${m.action.toUpperCase()}</strong> - ${m.reason || 'No reason'}</div>
<div class="activity-time">${formatTimeAgo(m.created_at)}</div>
</div>
</div>
`).join('')
: '<p style="color: var(--text-muted);">No mod actions</p>';
document.getElementById('userProfileContent').innerHTML = `
<div class="grid-2">
<div>
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 1.5rem;">
<div class="leaderboard-avatar" style="width: 80px; height: 80px; font-size: 2rem;">
${user.avatar ? `<img src="${user.avatar}" alt="">` : user.username.charAt(0).toUpperCase()}
</div>
<div>
<h2 style="margin-bottom: 0.25rem;">${user.displayName || user.username}</h2>
<p style="color: var(--text-muted); font-size: 0.9rem;">@${user.username} ${user.bot ? '<span class="badge badge-info">BOT</span>' : ''}</p>
<p style="font-family: monospace; font-size: 0.8rem; color: var(--text-muted);">${user.id}</p>
</div>
</div>
<div class="stats-grid" style="grid-template-columns: repeat(3, 1fr); margin-bottom: 1.5rem;">
<div style="text-align: center; padding: 0.75rem; background: var(--bg-card); border-radius: 8px;">
<div style="font-size: 1.5rem; font-weight: 700; color: var(--accent);">${user.level || 0}</div>
<div style="font-size: 0.75rem; color: var(--text-muted);">Level</div>
</div>
<div style="text-align: center; padding: 0.75rem; background: var(--bg-card); border-radius: 8px;">
<div style="font-size: 1.5rem; font-weight: 700; color: var(--success);">${formatNumber(user.xp || 0)}</div>
<div style="font-size: 0.75rem; color: var(--text-muted);">XP</div>
</div>
<div style="text-align: center; padding: 0.75rem; background: var(--bg-card); border-radius: 8px;">
<div style="font-size: 1.5rem; font-weight: 700; color: var(--${heatClass});">${user.heat || 0}</div>
<div style="font-size: 0.75rem; color: var(--text-muted);">Heat</div>
</div>
</div>
<div style="margin-bottom: 1rem;">
<h4 style="margin-bottom: 0.5rem;">Account Status</h4>
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
<span class="badge ${user.linked ? 'badge-success' : 'badge-warning'}">${user.linked ? 'AeThex Linked' : 'Not Linked'}</span>
${user.realm ? `<span class="badge badge-info">${user.realm}</span>` : ''}
${user.dailyStreak ? `<span class="badge badge-success">${user.dailyStreak} day streak</span>` : ''}
</div>
</div>
<div style="margin-bottom: 1rem;">
<h4 style="margin-bottom: 0.5rem;">Servers</h4>
<div style="display: flex; gap: 0.25rem; flex-wrap: wrap;">${serversHtml}</div>
</div>
<div style="margin-bottom: 1rem;">
<h4 style="margin-bottom: 0.5rem;">Roles</h4>
<div style="display: flex; gap: 0.25rem; flex-wrap: wrap;">${rolesHtml}</div>
</div>
<div style="margin-bottom: 1rem;">
<h4 style="margin-bottom: 0.5rem;">Dates</h4>
<p style="font-size: 0.875rem;"><strong>Created:</strong> ${new Date(user.createdAt).toLocaleDateString()}</p>
${user.joinedAt ? `<p style="font-size: 0.875rem;"><strong>Joined:</strong> ${new Date(user.joinedAt).toLocaleDateString()}</p>` : ''}
</div>
</div>
<div>
<div class="card" style="margin-bottom: 1rem;">
<div class="card-header">
<h4 class="card-title" style="font-size: 1rem;">Warnings (${user.warnings?.length || 0})</h4>
</div>
<div class="card-body" style="max-height: 200px; overflow-y: auto;">
${warningsHtml}
</div>
</div>
<div class="card">
<div class="card-header">
<h4 class="card-title" style="font-size: 1rem;">Mod History</h4>
</div>
<div class="card-body" style="max-height: 200px; overflow-y: auto;">
${modHistoryHtml}
</div>
</div>
<div style="margin-top: 1rem;">
<h4 style="margin-bottom: 0.5rem;">Quick Actions</h4>
<p style="color: var(--text-muted); font-size: 0.875rem;">Use Discord commands for moderation:</p>
<div class="commands-grid" style="margin-top: 0.5rem; grid-template-columns: repeat(2, 1fr);">
<div class="command-item">warn ${user.id}</div>
<div class="command-item">kick ${user.id}</div>
<div class="command-item">timeout ${user.id}</div>
<div class="command-item">ban ${user.id}</div>
</div>
</div>
</div>
</div>
`;
}
function clearUserSearch() {
document.getElementById('userSearch').value = '';
document.getElementById('userProfileCard').style.display = 'none';
document.getElementById('userResult').innerHTML = `
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
<p>Search for a user by Discord ID or username</p>
<p style="font-size: 0.8rem; margin-top: 0.5rem;">View profile, XP, warnings, and mod history</p>
</div>
`;
currentUserResults = [];
}
// ============================================
// MODERATION FETCH
// ============================================
async function fetchModActions() {
try {
const response = await fetch('/mod-actions');
const data = await response.json();
if (data.counts) {
document.getElementById('warningCount').textContent = data.counts.warnings || 0;
document.getElementById('banCount').textContent = data.counts.bans || 0;
document.getElementById('timeoutCount').textContent = data.counts.timeouts || 0;
document.getElementById('kickCount').textContent = data.counts.kicks || 0;
}
const modLogContent = document.getElementById('modLogContent');
if (data.actions && data.actions.length > 0) {
modLogContent.innerHTML = data.actions.slice(0, 10).map(a => `
<div class="activity-item">
<div class="activity-icon stat-icon ${a.action === 'ban' ? 'red' : a.action === 'kick' ? 'purple' : a.action === 'warn' ? 'orange' : 'blue'}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
</div>
<div class="activity-content">
<div class="activity-text"><strong>${a.action.toUpperCase()}</strong> ${a.user_tag || a.user_id} - ${a.reason || 'No reason'}</div>
<div class="activity-time">by ${a.moderator_tag || 'Unknown'} ${formatTimeAgo(a.created_at)}</div>
</div>
</div>
`).join('');
} else {
modLogContent.innerHTML = '<div class="empty-state"><p>No recent moderation actions</p></div>';
}
} catch (error) {
console.error('Failed to fetch mod actions:', error);
}
}
function showToast(message, type = 'success') {
const toast = document.createElement('div');
toast.className = `toast ${type}`;