Add WebSocket Log feature to Services Status View

- Introduced a WebSocket Log section in the Services Status View, allowing users to view real-time logs.
- Updated localization files for both German and English to include WebSocket Log messages.
- Enhanced the UI with a button to open the WebSocket Log dialog, improving user interaction and monitoring capabilities.
This commit is contained in:
Torsten Schulz (local)
2025-11-22 13:21:13 +01:00
parent dc7001a80c
commit 735075d1bd
4 changed files with 338 additions and 3 deletions

View File

@@ -0,0 +1,263 @@
<template>
<DialogWidget ref="dialog" :title="$t('admin.servicesStatus.daemon.websocketLog.title')"
:show-close="true" :buttons="buttons" name="WebSocketLogDialog" :modal="true" :isTitleTranslated="true"
:width="'90%'" :height="'80%'">
<div class="websocket-log-container">
<div class="log-controls">
<button @click="fetchLog" class="refresh-btn" :disabled="loading">
{{ loading ? $t('admin.servicesStatus.daemon.websocketLog.loading') : $t('admin.servicesStatus.daemon.websocketLog.refresh') }}
</button>
<span v-if="logEntries.length > 0" class="entry-count">
{{ $t('admin.servicesStatus.daemon.websocketLog.entryCount', { count: logEntries.length }) }}
</span>
</div>
<div v-if="error" class="error-message">
{{ error }}
</div>
<div v-if="logEntries.length === 0 && !loading" class="no-entries">
{{ $t('admin.servicesStatus.daemon.websocketLog.noEntries') }}
</div>
<div v-else class="log-entries">
<table class="log-table">
<thead>
<tr>
<th>{{ $t('admin.servicesStatus.daemon.websocketLog.timestamp') }}</th>
<th>{{ $t('admin.servicesStatus.daemon.websocketLog.direction') }}</th>
<th>{{ $t('admin.servicesStatus.daemon.websocketLog.peer') }}</th>
<th>{{ $t('admin.servicesStatus.daemon.websocketLog.connUser') }}</th>
<th>{{ $t('admin.servicesStatus.daemon.websocketLog.targetUser') }}</th>
<th>{{ $t('admin.servicesStatus.daemon.websocketLog.event') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(entry, index) in logEntries" :key="index" class="log-entry">
<td>{{ formatTimestamp(entry.timestamp) }}</td>
<td>
<span :class="['direction-badge', entry.direction === 'broker->client' ? 'incoming' : 'outgoing']">
{{ entry.direction }}
</span>
</td>
<td>{{ entry.peer || '-' }}</td>
<td>{{ entry.conn_user || '-' }}</td>
<td>{{ entry.target_user || '-' }}</td>
<td>{{ entry.event || '-' }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</DialogWidget>
</template>
<script>
import DialogWidget from '@/components/DialogWidget.vue';
import { mapState } from 'vuex';
export default {
name: 'WebSocketLogDialog',
components: { DialogWidget },
data() {
return {
logEntries: [],
loading: false,
error: null,
buttons: [
{ text: 'admin.servicesStatus.daemon.websocketLog.close', action: () => this.closeDialog() }
]
};
},
computed: {
...mapState(['daemonSocket'])
},
methods: {
open() {
this.$refs.dialog.open();
this.fetchLog();
},
closeDialog() {
this.$refs.dialog.close();
},
async fetchLog() {
if (!this.daemonSocket || this.daemonSocket.readyState !== WebSocket.OPEN) {
this.error = this.$t('admin.servicesStatus.daemon.websocketLog.notConnected');
return;
}
this.loading = true;
this.error = null;
try {
const message = JSON.stringify({
event: 'getWebsocketLog'
});
this.daemonSocket.send(message);
// Die Antwort wird über handleDaemonMessage verarbeitet
} catch (err) {
console.error('Fehler beim Senden von getWebsocketLog:', err);
this.error = this.$t('admin.servicesStatus.daemon.websocketLog.sendError');
this.loading = false;
}
},
handleDaemonMessage(event) {
try {
const data = JSON.parse(event.data);
if (data.event === 'getWebsocketLogResponse') {
this.logEntries = data.entries || [];
this.loading = false;
this.error = null;
}
} catch (err) {
console.error('Fehler beim Verarbeiten der Daemon-Nachricht:', err);
this.error = this.$t('admin.servicesStatus.daemon.websocketLog.parseError');
this.loading = false;
}
},
formatTimestamp(timestamp) {
if (!timestamp) return '-';
const date = new Date(timestamp * 1000);
return date.toLocaleString('de-DE', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
},
mounted() {
// Event-Listener für Daemon-Nachrichten registrieren
if (this.daemonSocket) {
this.daemonSocket.addEventListener('message', this.handleDaemonMessage);
}
},
beforeUnmount() {
// Event-Listener entfernen
if (this.daemonSocket) {
this.daemonSocket.removeEventListener('message', this.handleDaemonMessage);
}
},
watch: {
daemonSocket(newSocket, oldSocket) {
// Event-Listener bei Socket-Änderung aktualisieren
if (oldSocket) {
oldSocket.removeEventListener('message', this.handleDaemonMessage);
}
if (newSocket) {
newSocket.addEventListener('message', this.handleDaemonMessage);
}
}
}
};
</script>
<style scoped>
.websocket-log-container {
display: flex;
flex-direction: column;
height: 100%;
}
.log-controls {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #ddd;
}
.refresh-btn {
padding: 8px 16px;
background: #1976d2;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.refresh-btn:hover:not(:disabled) {
background: #1565c0;
}
.refresh-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.entry-count {
color: #666;
font-size: 14px;
}
.error-message {
padding: 10px;
background: #ffebee;
border: 1px solid #f44336;
border-radius: 4px;
color: #c62828;
margin-bottom: 15px;
}
.no-entries {
text-align: center;
padding: 40px;
color: #666;
font-style: italic;
}
.log-entries {
flex: 1;
overflow-y: auto;
}
.log-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.log-table thead {
position: sticky;
top: 0;
background: #f5f5f5;
z-index: 10;
}
.log-table th {
padding: 10px;
text-align: left;
border-bottom: 2px solid #ddd;
font-weight: bold;
background: #f5f5f5;
}
.log-table td {
padding: 8px 10px;
border-bottom: 1px solid #eee;
}
.log-entry:hover {
background: #f9f9f9;
}
.direction-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 12px;
font-size: 11px;
font-weight: bold;
}
.direction-badge.incoming {
background: #e3f2fd;
color: #1976d2;
}
.direction-badge.outgoing {
background: #f3e5f5;
color: #7b1fa2;
}
</style>

View File

@@ -267,6 +267,24 @@
"notConnected": "Daemon nicht verbunden", "notConnected": "Daemon nicht verbunden",
"sendError": "Fehler beim Senden der Anfrage", "sendError": "Fehler beim Senden der Anfrage",
"error": "Fehler beim Abrufen der Verbindungen" "error": "Fehler beim Abrufen der Verbindungen"
},
"websocketLog": {
"title": "WebSocket-Log",
"showLog": "WebSocket-Log anzeigen",
"refresh": "Aktualisieren",
"loading": "Lädt...",
"close": "Schließen",
"entryCount": "{count} Einträge",
"noEntries": "Keine Log-Einträge vorhanden",
"notConnected": "Daemon nicht verbunden",
"sendError": "Fehler beim Senden der Anfrage",
"parseError": "Fehler beim Verarbeiten der Antwort",
"timestamp": "Zeitstempel",
"direction": "Richtung",
"peer": "Peer",
"connUser": "Verbindungs-User",
"targetUser": "Ziel-User",
"event": "Event"
} }
} }
} }

View File

@@ -267,6 +267,24 @@
"notConnected": "Daemon not connected", "notConnected": "Daemon not connected",
"sendError": "Error sending request", "sendError": "Error sending request",
"error": "Error fetching connections" "error": "Error fetching connections"
},
"websocketLog": {
"title": "WebSocket Log",
"showLog": "Show WebSocket Log",
"refresh": "Refresh",
"loading": "Loading...",
"close": "Close",
"entryCount": "{count} entries",
"noEntries": "No log entries available",
"notConnected": "Daemon not connected",
"sendError": "Error sending request",
"parseError": "Error parsing response",
"timestamp": "Timestamp",
"direction": "Direction",
"peer": "Peer",
"connUser": "Connection User",
"targetUser": "Target User",
"event": "Event"
} }
} }
} }

View File

@@ -51,7 +51,12 @@
<!-- Daemon Connections --> <!-- Daemon Connections -->
<div v-if="daemonStatus === 'connected'" class="connections-section"> <div v-if="daemonStatus === 'connected'" class="connections-section">
<div class="section-header">
<h3>{{ $t('admin.servicesStatus.daemon.connections.title') }}</h3> <h3>{{ $t('admin.servicesStatus.daemon.connections.title') }}</h3>
<button @click="openWebSocketLogDialog" class="log-button">
{{ $t('admin.servicesStatus.daemon.websocketLog.showLog') }}
</button>
</div>
<div v-if="connections.length === 0" class="no-connections"> <div v-if="connections.length === 0" class="no-connections">
<p>{{ $t('admin.servicesStatus.daemon.connections.none') }}</p> <p>{{ $t('admin.servicesStatus.daemon.connections.none') }}</p>
</div> </div>
@@ -91,6 +96,9 @@
</div> </div>
</div> </div>
</div> </div>
<!-- WebSocket Log Dialog -->
<WebSocketLogDialog ref="webSocketLogDialog" />
</div> </div>
</template> </template>
@@ -98,11 +106,13 @@
import { mapState, mapGetters } from 'vuex'; import { mapState, mapGetters } from 'vuex';
import SimpleTabs from '@/components/SimpleTabs.vue'; import SimpleTabs from '@/components/SimpleTabs.vue';
import apiClient from '@/utils/axios.js'; import apiClient from '@/utils/axios.js';
import WebSocketLogDialog from '@/dialogues/admin/WebSocketLogDialog.vue';
export default { export default {
name: 'ServicesStatusView', name: 'ServicesStatusView',
components: { components: {
SimpleTabs SimpleTabs,
WebSocketLogDialog
}, },
data() { data() {
return { return {
@@ -271,6 +281,11 @@ export default {
const minutes = Math.floor((seconds % 3600) / 60); const minutes = Math.floor((seconds % 3600) / 60);
return `${hours}h ${minutes}m`; return `${hours}h ${minutes}m`;
} }
},
openWebSocketLogDialog() {
if (this.$refs.webSocketLogDialog) {
this.$refs.webSocketLogDialog.open();
}
} }
}, },
created() { created() {
@@ -364,10 +379,31 @@ export default {
margin-top: 30px; margin-top: 30px;
} }
.connections-section h3 { .section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px; margin-bottom: 15px;
} }
.section-header h3 {
margin: 0;
}
.log-button {
padding: 8px 16px;
background: #1976d2;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.log-button:hover {
background: #1565c0;
}
.no-connections { .no-connections {
padding: 20px; padding: 20px;
text-align: center; text-align: center;