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.
This commit is contained in:
Torsten Schulz (local)
2026-05-12 15:43:52 +02:00
parent 8d323ceab1
commit 37d752cce9

View File

@@ -362,31 +362,135 @@ export function setupBroadcast(io, __dirname) {
.slice(0, limit); .slice(0, limit);
} }
function buildAllStats(records) { function readStartsLogStats() {
if (records.length === 0) { const path = join(ensureLogsDir(__dirname), 'starts.log');
return ['Keine Login-Daten vorhanden.']; 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 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 uniqueNames = new Set(records.map((r) => r.userName)).size;
const ages = records.map((r) => r.age); const ages = records.map((r) => r.age);
const minAge = Math.min(...ages); const minAge = Math.min(...ages);
const maxAge = Math.max(...ages); const maxAge = Math.max(...ages);
const youngest = records.find((r) => r.age === minAge); const youngest = records.find((r) => r.age === minAge);
const oldest = records.find((r) => r.age === maxAge); const oldest = records.find((r) => r.age === maxAge);
const topCountries = aggregateTop(records, (r) => r.country, 5)
.map(([name, count]) => `${name}(${count})`)
.join(', ');
return [ push('Zeitraum (Kalendertage)', `${dayMin}${dayMax}`);
`Logins gesamt: ${records.length}`, push('Erster Login (Timestamp)', firstTs);
`Logins heute (${today}): ${todayCount}`, push('Letzter Login (Timestamp)', lastTs);
`Unterschiedliche Namen: ${uniqueNames}`, push('Logins gesamt', records.length);
`Jüngster Nutzer: ${youngest.userName} (${youngest.age})`, push(`Logins heute (${today} UTC)`, dayRecords.length);
`Ältester Nutzer: ${oldest.userName} (${oldest.age})`, push('Verschiedene Nutzer heute (UTC)', uniqueToday);
`Top Länder: ${topCountries || 'keine'}` 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) { function executeStatsCommand(socket, client, parts) {
@@ -411,7 +515,7 @@ export function setupBroadcast(io, __dirname) {
['/stat ages', 'Jüngster und ältester Nutzer'], ['/stat ages', 'Jüngster und ältester Nutzer'],
['/stat names', 'Häufigkeit der verwendeten Namen'], ['/stat names', 'Häufigkeit der verwendeten Namen'],
['/stat countries', 'Häufigkeit der Länder'], ['/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; return;
} }
@@ -513,15 +617,8 @@ export function setupBroadcast(io, __dirname) {
sendCommandResult(socket, 'Keine Berechtigung: Recht "stat" fehlt.'); sendCommandResult(socket, 'Keine Berechtigung: Recht "stat" fehlt.');
return; return;
} }
const lines = buildAllStats(readLoginRecords()); const rows = buildFullAllStatsRows(readLoginRecords());
const rows = lines.map((line) => { sendCommandTable(socket, 'Statistik: Vollständige Übersicht (/all-stats)', ['Metrik', 'Wert'], rows);
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);
} }
function executeKickCommand(socket, client, parts) { function executeKickCommand(socket, client, parts) {
@@ -665,7 +762,7 @@ export function setupBroadcast(io, __dirname) {
['/logout-admin', 'Admin-/Command-Login beenden'], ['/logout-admin', 'Admin-/Command-Login beenden'],
['/whoami-rights', 'Aktuelle Admin-Rechte anzeigen'], ['/whoami-rights', 'Aktuelle Admin-Rechte anzeigen'],
['/stat help', 'Hilfe zu Statistikbefehlen 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 <username>', 'Benutzer aus dem Chat werfen'], ['/kick <username>', 'Benutzer aus dem Chat werfen'],
['/help oder /?', 'Diese Hilfe anzeigen'] ['/help oder /?', 'Diese Hilfe anzeigen']
]); ]);