From 37d752cce914df707e1c36b6699433db97ab5be6 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Tue, 12 May 2026 15:43:52 +0200 Subject: [PATCH] Enhance broadcast.js with comprehensive statistics reporting - Introduced a new function to read server start logs, providing insights into server uptime and usage. - Refactored the statistics reporting to include detailed metrics from live server connections, start logs, and login records. - Improved the presentation of user statistics, including unique users, login timestamps, and demographic data, enhancing overall clarity and usability of the statistics command. - Updated command descriptions for better understanding of the new functionality. These changes collectively enrich the statistics reporting capabilities, offering a more complete view of server and user activity. --- server/broadcast.js | 149 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 123 insertions(+), 26 deletions(-) diff --git a/server/broadcast.js b/server/broadcast.js index 99eb0fe..0dd0015 100644 --- a/server/broadcast.js +++ b/server/broadcast.js @@ -362,31 +362,135 @@ export function setupBroadcast(io, __dirname) { .slice(0, limit); } - function buildAllStats(records) { - if (records.length === 0) { - return ['Keine Login-Daten vorhanden.']; + function readStartsLogStats() { + const path = join(ensureLogsDir(__dirname), 'starts.log'); + if (!existsSync(path)) { + return { count: 0, first: null, last: null }; + } + const lines = readFileSync(path, 'utf-8') + .split('\n') + .map((l) => l.trim()) + .filter(Boolean); + if (lines.length === 0) { + return { count: 0, first: null, last: null }; + } + return { count: lines.length, first: lines[0], last: lines[lines.length - 1] }; + } + + /** + * Sammelt alle verfügbaren Kennzahlen: Live-Server, starts.log, vollständige Auswertung von logins.log + * (entspricht inhaltlich den /stat-Unterbefehlen in einer Tabelle). + */ + function buildFullAllStatsRows(records) { + const rows = []; + const push = (a, b) => rows.push([String(a), b == null ? '' : String(b)]); + + const liveOnline = Array.from(clients.values()).filter( + (c) => c.userName && c.socket && c.socket.connected + ).length; + const socketCount = + io.sockets && io.sockets.sockets && typeof io.sockets.sockets.size === 'number' + ? io.sockets.sockets.size + : 0; + let totalMessages = 0; + for (const msgs of conversations.values()) { + totalMessages += Array.isArray(msgs) ? msgs.length : 0; } + const starts = readStartsLogStats(); + + push('— Live (Server) —', ''); + push('Nutzer mit Chat-Login und aktiver Verbindung', liveOnline); + push('Socket.IO-Verbindungen (/)', socketCount); + push('Client-Sitzungen im Speicher', clients.size); + push('Unterhaltungen im Speicher (Paare)', conversations.size); + push('Nachrichten-Einträge gesamt (RAM)', totalMessages); + push('Server-Starts protokolliert (starts.log)', starts.count); + if (starts.first) { + push('Erster Start (Timestamp)', starts.first); + } + if (starts.last) { + push('Letzter Start (Timestamp)', starts.last); + } + + push('— Login-Protokoll (logins.log, UTC) —', ''); + if (records.length === 0) { + push('Keine Einträge', '—'); + return rows; + } + + const sortedByDate = records.slice().sort((a, b) => a.date - b.date); + const firstTs = sortedByDate[0].timestamp; + const lastTs = sortedByDate[sortedByDate.length - 1].timestamp; + const daysSorted = records.map((r) => r.day).sort(); + const dayMin = daysSorted[0]; + const dayMax = daysSorted[daysSorted.length - 1]; + const today = new Date().toISOString().slice(0, 10); - const todayCount = records.filter((r) => r.day === today).length; + const dayRecords = records.filter((r) => r.day === today); + const latestByUserToday = new Map(); + dayRecords + .slice() + .sort((a, b) => b.date - a.date) + .forEach((record) => { + if (!latestByUserToday.has(record.userName)) { + latestByUserToday.set(record.userName, record); + } + }); + const uniqueToday = latestByUserToday.size; + const namesToday = Array.from(latestByUserToday.keys()).sort((a, b) => a.localeCompare(b, 'de')); + const uniqueNames = new Set(records.map((r) => r.userName)).size; const ages = records.map((r) => r.age); const minAge = Math.min(...ages); const maxAge = Math.max(...ages); const youngest = records.find((r) => r.age === minAge); const oldest = records.find((r) => r.age === maxAge); - const topCountries = aggregateTop(records, (r) => r.country, 5) - .map(([name, count]) => `${name}(${count})`) - .join(', '); - return [ - `Logins gesamt: ${records.length}`, - `Logins heute (${today}): ${todayCount}`, - `Unterschiedliche Namen: ${uniqueNames}`, - `Jüngster Nutzer: ${youngest.userName} (${youngest.age})`, - `Ältester Nutzer: ${oldest.userName} (${oldest.age})`, - `Top Länder: ${topCountries || 'keine'}` - ]; + push('Zeitraum (Kalendertage)', `${dayMin} … ${dayMax}`); + push('Erster Login (Timestamp)', firstTs); + push('Letzter Login (Timestamp)', lastTs); + push('Logins gesamt', records.length); + push(`Logins heute (${today} UTC)`, dayRecords.length); + push('Verschiedene Nutzer heute (UTC)', uniqueToday); + const listToday = namesToday.join(', '); + push( + 'Nutzer heute (Liste)', + listToday.length > 500 + ? `${uniqueToday} Nutzer — Auszug: ${namesToday.slice(0, 8).join(', ')} … (vollständig: /stat today)` + : listToday || '—' + ); + push('Verschiedene Namen (historisch)', uniqueNames); + push('Ø Logins pro Namen (historisch)', (records.length / uniqueNames).toFixed(2)); + push('Jüngster Nutzer (erster Treffer im Log)', `${youngest.userName} (${youngest.age})`); + push('Ältester Nutzer (erster Treffer im Log)', `${oldest.userName} (${oldest.age})`); + + const topGenders = aggregateTop( + records, + (r) => (r.gender || '').trim() || '?', + 15 + ); + push('Geschlecht (Häufigkeit)', topGenders.map(([g, n]) => `${g}(${n})`).join(', ') || '—'); + + const topCountries = aggregateTop(records, (r) => r.country, 20); + push('Top 20 Länder', topCountries.map(([c, n]) => `${c}(${n})`).join(', ') || '—'); + + const topNames = aggregateTop(records, (r) => r.userName, 20); + const topNamesDetail = topNames + .map(([name, count]) => { + const latestRecord = records + .filter((r) => r.userName === name) + .sort((a, b) => b.date - a.date)[0]; + return `${name}(${count}, zuletzt ${latestRecord.timestamp})`; + }) + .join('; '); + push( + 'Top 20 Namen (Anzahl, letzter Login)', + topNamesDetail.length > 1200 ? `${topNamesDetail.slice(0, 1197)}…` : topNamesDetail + ); + push('Hinweis', 'Tabellen je Tag/Zeitraum: /stat today | /stat date | /stat range'); + + return rows; } function executeStatsCommand(socket, client, parts) { @@ -411,7 +515,7 @@ export function setupBroadcast(io, __dirname) { ['/stat ages', 'Jüngster und ältester Nutzer'], ['/stat names', 'Häufigkeit der verwendeten Namen'], ['/stat countries', 'Häufigkeit der Länder'], - ['/all-stats', 'Kurzübersicht nur aus Logindatei (logins.log), keine Live-Chat-Metriken'] + ['/all-stats', 'Alle Kennzahlen: Live-Server + starts.log + komplette logins.log-Auswertung'] ]); return; } @@ -513,15 +617,8 @@ export function setupBroadcast(io, __dirname) { sendCommandResult(socket, 'Keine Berechtigung: Recht "stat" fehlt.'); return; } - const lines = buildAllStats(readLoginRecords()); - const rows = lines.map((line) => { - const separatorIndex = line.indexOf(':'); - if (separatorIndex === -1) { - return [line, '']; - } - return [line.slice(0, separatorIndex).trim(), line.slice(separatorIndex + 1).trim()]; - }); - sendCommandTable(socket, 'Statistik: Übersicht', ['Metrik', 'Wert'], rows); + const rows = buildFullAllStatsRows(readLoginRecords()); + sendCommandTable(socket, 'Statistik: Vollständige Übersicht (/all-stats)', ['Metrik', 'Wert'], rows); } function executeKickCommand(socket, client, parts) { @@ -665,7 +762,7 @@ export function setupBroadcast(io, __dirname) { ['/logout-admin', 'Admin-/Command-Login beenden'], ['/whoami-rights', 'Aktuelle Admin-Rechte anzeigen'], ['/stat help', 'Hilfe zu Statistikbefehlen anzeigen'], - ['/all-stats', 'Kurzübersicht nur aus Logindatei (logins.log), keine Live-Chat-Metriken'], + ['/all-stats', 'Alle Kennzahlen: Live-Server + starts.log + komplette logins.log-Auswertung'], ['/kick ', 'Benutzer aus dem Chat werfen'], ['/help oder /?', 'Diese Hilfe anzeigen'] ]);