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:
@@ -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']
|
||||||
]);
|
]);
|
||||||
|
|||||||
Reference in New Issue
Block a user