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:
parent
bce361551f
commit
0572e7ad61
2 changed files with 341 additions and 3 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue