From 448f2ffb6feaf8ff7faa8399cd83df130a45168a Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Thu, 19 Mar 2026 14:00:08 +0100 Subject: [PATCH] Add command table functionality to chat store and ChatView component - Introduced `commandTable` state in chat store to manage command output. - Implemented WebSocket listener for `commandTable` messages. - Enhanced ChatView.vue to display command table with dynamic content and styling. - Added `clearCommandTable` method to reset command table state. - Updated server broadcast logic to send structured command table data for various statistics. --- client/src/stores/chat.js | 21 ++++++++++ client/src/views/ChatView.vue | 73 +++++++++++++++++++++++++++++++++++ server/broadcast.js | 49 ++++++++++++++++------- 3 files changed, 130 insertions(+), 13 deletions(-) diff --git a/client/src/stores/chat.js b/client/src/stores/chat.js index 36f4fb6..ce3c8c2 100644 --- a/client/src/stores/chat.js +++ b/client/src/stores/chat.js @@ -21,6 +21,7 @@ export const useChatStore = defineStore('chat', () => { const historyResults = ref([]); const unreadChatsCount = ref(0); const errorMessage = ref(null); + const commandTable = ref(null); const remainingSecondsToTimeout = ref(1800); const awaitingLoginUsername = ref(false); const awaitingLoginPassword = ref(false); @@ -191,6 +192,10 @@ export const useChatStore = defineStore('chat', () => { handleWebSocketMessage({ type: 'commandResult', ...data }); }); + socketInstance.on('commandTable', (data) => { + handleWebSocketMessage({ type: 'commandTable', ...data }); + }); + socketInstance.on('unreadChats', (data) => { handleWebSocketMessage({ type: 'unreadChats', ...data }); }); @@ -315,6 +320,15 @@ export const useChatStore = defineStore('chat', () => { }, 5000); break; } + case 'commandTable': { + const title = data.title || 'Ausgabe'; + const columns = Array.isArray(data.columns) ? data.columns : []; + const rows = Array.isArray(data.rows) ? data.rows : []; + commandTable.value = { title, columns, rows }; + // Tabelle ist persistent; temporäre Fehlermeldung löschen + errorMessage.value = null; + break; + } case 'unreadChats': unreadChatsCount.value = data.count || 0; break; @@ -543,6 +557,10 @@ export const useChatStore = defineStore('chat', () => { socket.value.emit('requestOpenConversations'); } + function clearCommandTable() { + commandTable.value = null; + } + function setView(view) { currentView.value = view; @@ -574,6 +592,7 @@ export const useChatStore = defineStore('chat', () => { searchResults.value = []; inboxResults.value = []; historyResults.value = []; + commandTable.value = null; searchData.value = { nameIncludes: '', minAge: null, @@ -689,6 +708,7 @@ export const useChatStore = defineStore('chat', () => { unreadChatsCount, remainingSecondsToTimeout, errorMessage, + commandTable, searchData, awaitingLoginUsername, awaitingLoginPassword, @@ -703,6 +723,7 @@ export const useChatStore = defineStore('chat', () => { userSearch, requestHistory, requestOpenConversations, + clearCommandTable, setView, logout, restoreSession diff --git a/client/src/views/ChatView.vue b/client/src/views/ChatView.vue index 8cd2443..d760180 100644 --- a/client/src/views/ChatView.vue +++ b/client/src/views/ChatView.vue @@ -22,6 +22,30 @@
{{ chatStore.errorMessage }}
+
+
+ {{ chatStore.commandTable.title }} + +
+
+ + + + + + + + + + + +
+ {{ column }} +
+ {{ cell }} +
+
+

{{ chatStore.currentConversation }} ({{ currentUserInfo.gender }})

@@ -159,5 +183,54 @@ onMounted(async () => { text-align: center; font-weight: bold; } + +.command-table-container { + margin: 0.8em; + border: 1px solid #ccc; + border-radius: 6px; + background: #fff; + overflow: hidden; +} + +.command-table-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.6em 0.8em; + background: #f4f6f8; + border-bottom: 1px solid #ddd; +} + +.command-table-close { + border: 1px solid #bbb; + background: #fff; + padding: 0.2em 0.6em; + cursor: pointer; + border-radius: 4px; +} + +.command-table-scroll { + max-height: 220px; + overflow: auto; +} + +.command-table { + width: 100%; + border-collapse: collapse; + font-size: 0.9em; +} + +.command-table th, +.command-table td { + padding: 0.45em 0.6em; + border-bottom: 1px solid #eee; + text-align: left; +} + +.command-table th { + background: #fafafa; + position: sticky; + top: 0; +} diff --git a/server/broadcast.js b/server/broadcast.js index 4ff6910..94aa533 100644 --- a/server/broadcast.js +++ b/server/broadcast.js @@ -325,6 +325,14 @@ export function setupBroadcast(io, __dirname) { socket.emit('commandResult', { lines: payload, kind }); } + function sendCommandTable(socket, title, columns, rows) { + socket.emit('commandTable', { + title: String(title || 'Ausgabe'), + columns: Array.isArray(columns) ? columns.map((c) => String(c)) : [], + rows: Array.isArray(rows) ? rows : [] + }); + } + function hasRight(client, right) { return !!client.chatAuth && client.chatAuth.rights instanceof Set && client.chatAuth.rights.has(right); } @@ -412,7 +420,7 @@ export function setupBroadcast(io, __dirname) { if (sub === 'today') { const day = new Date().toISOString().slice(0, 10); const dayRecords = records.filter((r) => r.day === day); - sendCommandResult(socket, `Logins heute (${day}): ${dayRecords.length}`); + sendCommandTable(socket, 'Statistik: Heute', ['Tag', 'Logins'], [[day, dayRecords.length]]); return; } @@ -423,7 +431,7 @@ export function setupBroadcast(io, __dirname) { return; } const dayRecords = records.filter((r) => r.day === day); - sendCommandResult(socket, `Logins am ${day}: ${dayRecords.length}`); + sendCommandTable(socket, 'Statistik: Datum', ['Tag', 'Logins'], [[day, dayRecords.length]]); return; } @@ -437,8 +445,8 @@ export function setupBroadcast(io, __dirname) { const filtered = records.filter((r) => r.day >= from && r.day <= to); const perDay = aggregateTop(filtered, (r) => r.day, 1000) .sort((a, b) => a[0].localeCompare(b[0])) - .map(([day, count]) => `${day}: ${count}`); - sendCommandResult(socket, [`Logins ${from} bis ${to}: ${filtered.length}`, ...perDay]); + .map(([day, count]) => [day, count]); + sendCommandTable(socket, `Statistik: Zeitraum ${from} bis ${to}`, ['Tag', 'Logins'], perDay); return; } @@ -448,25 +456,32 @@ export function setupBroadcast(io, __dirname) { const maxAge = Math.max(...ages); const youngest = records.find((r) => r.age === minAge); const oldest = records.find((r) => r.age === maxAge); - sendCommandResult(socket, [ - `Jüngster Nutzer: ${youngest.userName} (${youngest.age})`, - `Ältester Nutzer: ${oldest.userName} (${oldest.age})` + sendCommandTable(socket, 'Statistik: Alter', ['Kategorie', 'Name', 'Alter'], [ + ['Jüngster Nutzer', youngest.userName, youngest.age], + ['Ältester Nutzer', oldest.userName, oldest.age] ]); return; } if (sub === 'names') { const topNames = aggregateTop(records, (r) => r.userName, 20); - sendCommandResult(socket, [ - `Namen gesamt (verschieden): ${new Set(records.map((r) => r.userName)).size}`, - ...topNames.map(([name, count]) => `${name}: ${count}`) - ]); + sendCommandTable( + socket, + `Statistik: Namen (gesamt verschieden: ${new Set(records.map((r) => r.userName)).size})`, + ['Name', 'Anzahl'], + topNames.map(([name, count]) => [name, count]) + ); return; } if (sub === 'countries') { const topCountries = aggregateTop(records, (r) => r.country, 20); - sendCommandResult(socket, topCountries.map(([country, count]) => `${country}: ${count}`)); + sendCommandTable( + socket, + 'Statistik: Länder', + ['Land', 'Anzahl'], + topCountries.map(([country, count]) => [country, count]) + ); return; } @@ -478,7 +493,15 @@ export function setupBroadcast(io, __dirname) { sendCommandResult(socket, 'Keine Berechtigung: Recht "stat" fehlt.'); return; } - sendCommandResult(socket, buildAllStats(readLoginRecords())); + 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); } function executeKickCommand(socket, client, parts) {