Add real-time activity feed and threat alert system

Introduces new API endpoints for activity, tickets, and threats, and updates the dashboard to display real-time data.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: a6871c20-3d5a-4c6e-bec7-be8a96afb6dc
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/SQxsvtx
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
sirpiglr 2025-12-08 04:37:02 +00:00
parent bce361551f
commit 0572e7ad61
2 changed files with 341 additions and 3 deletions

View file

@ -281,6 +281,50 @@ async function sendAlert(message, embed = null) {
}
client.sendAlert = sendAlert;
// =============================================================================
// ACTIVITY FEED SYSTEM (New - Dashboard Real-time)
// =============================================================================
const activityFeed = [];
const MAX_ACTIVITY_EVENTS = 100;
const threatAlerts = [];
const MAX_THREAT_ALERTS = 50;
function addActivity(type, data) {
const event = {
id: Date.now() + Math.random().toString(36).substr(2, 9),
type,
data,
timestamp: new Date().toISOString(),
};
activityFeed.unshift(event);
if (activityFeed.length > MAX_ACTIVITY_EVENTS) {
activityFeed.pop();
}
return event;
}
function addThreatAlert(level, message, details = {}) {
const alert = {
id: Date.now() + Math.random().toString(36).substr(2, 9),
level,
message,
details,
timestamp: new Date().toISOString(),
resolved: false,
};
threatAlerts.unshift(alert);
if (threatAlerts.length > MAX_THREAT_ALERTS) {
threatAlerts.pop();
}
return alert;
}
client.addActivity = addActivity;
client.activityFeed = activityFeed;
client.addThreatAlert = addThreatAlert;
client.threatAlerts = threatAlerts;
// =============================================================================
// COMMAND LOADING
// =============================================================================
@ -411,6 +455,14 @@ client.on("interactionCreate", async (interaction) => {
console.log(`[Command] Executing: ${interaction.commandName}`);
await command.execute(interaction, supabase, client);
console.log(`[Command] Completed: ${interaction.commandName}`);
addActivity('command', {
command: interaction.commandName,
user: interaction.user.tag,
userId: interaction.user.id,
guild: interaction.guild?.name || 'DM',
guildId: interaction.guildId,
});
} catch (error) {
console.error(`Error executing ${interaction.commandName}:`, error);
@ -610,6 +662,107 @@ http
uptime: Math.floor(process.uptime()),
activeTickets: activeTickets.size,
heatEvents: heatMap.size,
federationLinks: federationMappings.size,
}));
return;
}
if (req.url === "/activity" || req.url.startsWith("/activity?")) {
const url = new URL(req.url, `http://localhost:${healthPort}`);
const limit = parseInt(url.searchParams.get('limit') || '50');
const since = url.searchParams.get('since');
let events = activityFeed.slice(0, Math.min(limit, 100));
if (since) {
events = events.filter(e => new Date(e.timestamp) > new Date(since));
}
res.writeHead(200);
res.end(JSON.stringify({
events,
total: activityFeed.length,
timestamp: new Date().toISOString(),
}));
return;
}
if (req.url === "/tickets") {
const ticketList = Array.from(activeTickets.entries()).map(([channelId, data]) => ({
channelId,
userId: data.userId,
guildId: data.guildId,
reason: data.reason,
createdAt: new Date(data.createdAt).toISOString(),
age: Math.floor((Date.now() - data.createdAt) / 60000),
}));
res.writeHead(200);
res.end(JSON.stringify({
tickets: ticketList,
count: ticketList.length,
timestamp: new Date().toISOString(),
}));
return;
}
if (req.url === "/threats") {
const unresolvedThreats = threatAlerts.filter(t => !t.resolved);
const threatLevel = unresolvedThreats.length > 5 ? 'Critical' :
unresolvedThreats.length > 2 ? 'High' :
unresolvedThreats.length > 0 ? 'Medium' : 'Low';
res.writeHead(200);
res.end(JSON.stringify({
alerts: threatAlerts.slice(0, 50),
unresolvedCount: unresolvedThreats.length,
threatLevel,
heatMapSize: heatMap.size,
timestamp: new Date().toISOString(),
}));
return;
}
if (req.url === "/server-health") {
const guilds = client.guilds.cache.map(g => {
const botMember = g.members.cache.get(client.user.id);
return {
id: g.id,
name: g.name,
memberCount: g.memberCount,
online: true,
permissions: botMember?.permissions.has('Administrator') ? 'Admin' : 'Limited',
joinedAt: g.joinedAt?.toISOString(),
};
});
res.writeHead(200);
res.end(JSON.stringify({
guilds,
botStatus: client.isReady() ? 'online' : 'offline',
ping: client.ws.ping,
timestamp: new Date().toISOString(),
}));
return;
}
if (req.url === "/system-info") {
const memUsage = process.memoryUsage();
res.writeHead(200);
res.end(JSON.stringify({
uptime: Math.floor(process.uptime()),
memory: {
heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024),
heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024),
rss: Math.round(memUsage.rss / 1024 / 1024),
},
nodeVersion: process.version,
platform: process.platform,
ping: client.ws.ping,
guilds: client.guilds.cache.size,
commands: client.commands.size,
activityEvents: activityFeed.length,
whitelistedUsers: whitelistedUsers.length,
timestamp: new Date().toISOString(),
}));
return;
}

View file

@ -1334,6 +1334,184 @@
return num.toString();
}
// Format relative time
function formatTimeAgo(timestamp) {
const seconds = Math.floor((Date.now() - new Date(timestamp)) / 1000);
if (seconds < 60) return 'Just now';
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
return `${Math.floor(seconds / 86400)}d ago`;
}
// Activity type icons and colors
const activityConfig = {
command: { icon: '<polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/>', color: 'purple' },
member_join: { icon: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="19" y1="8" x2="19" y2="14"/><line x1="22" y1="11" x2="16" y2="11"/>', color: 'green' },
member_leave: { icon: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="17" y1="11" x2="23" y2="11"/>', color: 'orange' },
mod_action: { icon: '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>', color: 'red' },
ticket: { icon: '<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/>', color: 'blue' },
xp: { icon: '<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>', color: 'orange' },
alert: { icon: '<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"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>', color: 'red' },
};
// Render activity event
function renderActivityEvent(event) {
const config = activityConfig[event.type] || activityConfig.command;
let text = '';
switch (event.type) {
case 'command':
text = `<strong>${event.data.user}</strong> used <code>/${event.data.command}</code> in ${event.data.guild}`;
break;
case 'member_join':
text = `<strong>${event.data.user}</strong> joined <strong>${event.data.guild}</strong>`;
break;
case 'member_leave':
text = `<strong>${event.data.user}</strong> left <strong>${event.data.guild}</strong>`;
break;
case 'mod_action':
text = `<strong>${event.data.moderator}</strong> ${event.data.action} <strong>${event.data.target}</strong>`;
break;
case 'ticket':
text = `Ticket ${event.data.action}: <strong>${event.data.reason || 'Support request'}</strong>`;
break;
case 'xp':
text = `<strong>${event.data.user}</strong> earned ${event.data.amount} XP`;
break;
case 'alert':
text = `<span style="color: var(--danger);">${event.data.message}</span>`;
break;
default:
text = JSON.stringify(event.data);
}
return `
<div class="activity-item">
<div class="activity-icon stat-icon ${config.color}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">${config.icon}</svg>
</div>
<div class="activity-content">
<div class="activity-text">${text}</div>
<div class="activity-time">${formatTimeAgo(event.timestamp)}</div>
</div>
</div>
`;
}
// Fetch activity feed
async function fetchActivityFeed() {
try {
const response = await fetch('/activity?limit=20');
const data = await response.json();
const feedContainer = document.getElementById('activityFeed');
if (data.events && data.events.length > 0) {
feedContainer.innerHTML = data.events.map(renderActivityEvent).join('');
const badge = document.getElementById('activityBadge');
badge.textContent = data.events.length;
badge.style.display = 'inline';
} else {
feedContainer.innerHTML = `
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
<p>No recent activity</p>
</div>
`;
}
} catch (error) {
console.error('Failed to fetch activity:', error);
}
}
// Fetch tickets
async function fetchTickets() {
try {
const response = await fetch('/tickets');
const data = await response.json();
const ticketList = document.getElementById('ticketList');
const ticketBadge = document.getElementById('ticketBadge');
const ticketCountBadge = document.getElementById('ticketCountBadge');
ticketBadge.textContent = data.count || 0;
ticketCountBadge.textContent = `${data.count || 0} open`;
if (data.tickets && data.tickets.length > 0) {
ticketList.innerHTML = data.tickets.map(t => `
<div class="activity-item">
<div class="activity-icon stat-icon blue">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>
</div>
<div class="activity-content">
<div class="activity-text"><strong>${t.reason || 'Support Request'}</strong></div>
<div class="activity-time">Open for ${t.age} minutes</div>
</div>
<span class="badge badge-info">Open</span>
</div>
`).join('');
} else {
ticketList.innerHTML = `
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>
<p>No active tickets</p>
</div>
`;
}
} catch (error) {
console.error('Failed to fetch tickets:', error);
}
}
// Fetch threat alerts
async function fetchThreats() {
try {
const response = await fetch('/threats');
const data = await response.json();
document.getElementById('sentinelHeatMap').textContent = data.heatMapSize || 0;
document.getElementById('heatMapSize').textContent = data.heatMapSize || 0;
document.getElementById('threatLevel').textContent = data.threatLevel || 'Low';
const threatLevelEl = document.getElementById('threatLevel');
threatLevelEl.style.color = data.threatLevel === 'Critical' ? 'var(--danger)' :
data.threatLevel === 'High' ? 'var(--warning)' :
data.threatLevel === 'Medium' ? 'var(--info)' : 'var(--success)';
const threatMonitor = document.querySelector('#section-sentinel .card-body:last-child');
if (data.alerts && data.alerts.length > 0) {
threatMonitor.innerHTML = data.alerts.slice(0, 10).map(a => `
<div class="activity-item">
<div class="activity-icon stat-icon ${a.level === 'critical' ? 'red' : a.level === 'high' ? 'orange' : 'blue'}">
<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"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
</div>
<div class="activity-content">
<div class="activity-text">${a.message}</div>
<div class="activity-time">${formatTimeAgo(a.timestamp)}</div>
</div>
<span class="badge ${a.resolved ? 'badge-success' : 'badge-warning'}">${a.resolved ? 'Resolved' : 'Active'}</span>
</div>
`).join('');
}
} catch (error) {
console.error('Failed to fetch threats:', error);
}
}
// Fetch system info
async function fetchSystemInfo() {
try {
const response = await fetch('/system-info');
const data = await response.json();
document.getElementById('memoryUsage').textContent = `${data.memory.heapUsed}MB`;
document.getElementById('botPing').textContent = `${data.ping}ms`;
document.getElementById('nodeVersion').textContent = data.nodeVersion;
document.getElementById('whitelistedCount').textContent = data.whitelistedUsers || 0;
} catch (error) {
console.error('Failed to fetch system info:', error);
}
}
// Fetch and update data
async function refreshData() {
try {
@ -1356,8 +1534,9 @@
// Sentinel stats
document.getElementById('heatMapSize').textContent = health.heatMapSize || 0;
document.getElementById('activeTickets').textContent = stats.activeTickets || 0;
document.getElementById('federationCount').textContent = '0';
document.getElementById('federationCount').textContent = stats.federationLinks || 0;
document.getElementById('ticketBadge').textContent = stats.activeTickets || 0;
document.getElementById('sentinelFederation').textContent = stats.federationLinks || 0;
// Server list
const serverList = document.getElementById('serverList');
@ -1406,11 +1585,17 @@
// Sentinel stats (in section)
document.getElementById('sentinelHeatMap').textContent = health.heatMapSize || 0;
document.getElementById('threatLevel').textContent = health.heatMapSize > 10 ? 'Medium' : 'Low';
document.getElementById('sentinelFederation').textContent = '0';
// Last update
document.getElementById('lastUpdate').textContent = new Date().toLocaleTimeString();
// Fetch additional real-time data
await Promise.all([
fetchActivityFeed(),
fetchTickets(),
fetchThreats(),
fetchSystemInfo()
]);
} catch (error) {
console.error('Failed to fetch data:', error);