Feature: Add Services Status page and update navigation

- Introduced a new Services Status page to monitor the status of Backend, Chat, and Daemon services.
- Updated navigation structure to include the new Services Status link for main admin users.
- Added German and English localization for the Services Status page, including titles, descriptions, and status messages.
This commit is contained in:
Torsten Schulz (local)
2025-11-20 15:49:08 +01:00
parent e7f5918013
commit eadec50e30
7 changed files with 510 additions and 2 deletions

View File

@@ -251,6 +251,10 @@ const menuStructure = {
visible: ["mainadmin", "chatrooms"], visible: ["mainadmin", "chatrooms"],
path: "/admin/chatrooms" path: "/admin/chatrooms"
}, },
servicesStatus: {
visible: ["mainadmin"],
path: "/admin/services/status"
},
interests: { interests: {
visible: ["mainadmin", "interests"], visible: ["mainadmin", "interests"],
path: "/admin/interests" path: "/admin/interests"

View File

@@ -230,6 +230,44 @@
"updateSuccess": "Map wurde erfolgreich aktualisiert!", "updateSuccess": "Map wurde erfolgreich aktualisiert!",
"deleteSuccess": "Map wurde erfolgreich gelöscht!" "deleteSuccess": "Map wurde erfolgreich gelöscht!"
} }
},
"servicesStatus": {
"title": "Service-Status",
"description": "Überwache den Status von Backend, Chat und Daemon",
"status": {
"connected": "Verbunden",
"connecting": "Verbinde...",
"disconnected": "Nicht verbunden",
"error": "Fehler",
"unknown": "Unbekannt"
},
"backend": {
"title": "Backend",
"connected": "Backend-Service ist erreichbar und verbunden"
},
"chat": {
"title": "Chat",
"connected": "Chat-Service ist erreichbar und verbunden"
},
"daemon": {
"title": "Daemon",
"connected": "Daemon-Service ist erreichbar und verbunden",
"connections": {
"title": "Aktive Verbindungen",
"none": "Keine aktiven Verbindungen",
"userId": "Benutzer-ID",
"connections": "Verbindungen",
"duration": "Verbindungsdauer",
"lastPong": "Zeit seit letztem Pong",
"pingTimeouts": "Ping-Timeouts",
"pongReceived": "Pong empfangen",
"yes": "Ja",
"no": "Nein",
"notConnected": "Daemon nicht verbunden",
"sendError": "Fehler beim Senden der Anfrage",
"error": "Fehler beim Abrufen der Verbindungen"
}
}
} }
} }
} }

View File

@@ -64,7 +64,8 @@
"match3": "Match3 Level", "match3": "Match3 Level",
"taxiTools": "Taxi-Tools" "taxiTools": "Taxi-Tools"
}, },
"chatrooms": "Chaträume" "chatrooms": "Chaträume",
"servicesStatus": "Service-Status"
}, },
"m-friends": { "m-friends": {
"manageFriends": "Freunde verwalten", "manageFriends": "Freunde verwalten",

View File

@@ -230,6 +230,44 @@
"updateSuccess": "Map updated successfully!", "updateSuccess": "Map updated successfully!",
"deleteSuccess": "Map deleted successfully!" "deleteSuccess": "Map deleted successfully!"
} }
},
"servicesStatus": {
"title": "Service Status",
"description": "Monitor the status of Backend, Chat and Daemon",
"status": {
"connected": "Connected",
"connecting": "Connecting...",
"disconnected": "Disconnected",
"error": "Error",
"unknown": "Unknown"
},
"backend": {
"title": "Backend",
"connected": "Backend service is reachable and connected"
},
"chat": {
"title": "Chat",
"connected": "Chat service is reachable and connected"
},
"daemon": {
"title": "Daemon",
"connected": "Daemon service is reachable and connected",
"connections": {
"title": "Active Connections",
"none": "No active connections",
"userId": "User ID",
"connections": "connections",
"duration": "Connection Duration",
"lastPong": "Time Since Last Pong",
"pingTimeouts": "Ping Timeouts",
"pongReceived": "Pong Received",
"yes": "Yes",
"no": "No",
"notConnected": "Daemon not connected",
"sendError": "Error sending request",
"error": "Error fetching connections"
}
}
} }
} }
} }

View File

@@ -64,7 +64,8 @@
"match3": "Match3 Levels", "match3": "Match3 Levels",
"taxiTools": "Taxi Tools" "taxiTools": "Taxi Tools"
}, },
"chatrooms": "Chat rooms" "chatrooms": "Chat rooms",
"servicesStatus": "Service Status"
}, },
"m-friends": { "m-friends": {
"manageFriends": "Manage friends", "manageFriends": "Manage friends",

View File

@@ -8,6 +8,7 @@ import AdminMinigamesView from '../views/admin/MinigamesView.vue';
import AdminTaxiToolsView from '../views/admin/TaxiToolsView.vue'; import AdminTaxiToolsView from '../views/admin/TaxiToolsView.vue';
import AdminUsersView from '../views/admin/UsersView.vue'; import AdminUsersView from '../views/admin/UsersView.vue';
import UserStatisticsView from '../views/admin/UserStatisticsView.vue'; import UserStatisticsView from '../views/admin/UserStatisticsView.vue';
import ServicesStatusView from '../views/admin/ServicesStatusView.vue';
const adminRoutes = [ const adminRoutes = [
{ {
@@ -69,6 +70,12 @@ const adminRoutes = [
name: 'AdminTaxiTools', name: 'AdminTaxiTools',
component: AdminTaxiToolsView, component: AdminTaxiToolsView,
meta: { requiresAuth: true } meta: { requiresAuth: true }
},
{
path: '/admin/services/status',
name: 'AdminServicesStatus',
component: ServicesStatusView,
meta: { requiresAuth: true }
} }
]; ];

View File

@@ -0,0 +1,419 @@
<template>
<div class="contenthidden">
<div class="contentscroll">
<div class="admin-header">
<h1>{{ $t('admin.servicesStatus.title') }}</h1>
<p>{{ $t('admin.servicesStatus.description') }}</p>
</div>
<SimpleTabs v-model="activeTab" :tabs="tabs" />
<!-- Backend Tab -->
<div v-if="activeTab === 'backend'" class="service-status">
<div class="status-card">
<h2>{{ $t('admin.servicesStatus.backend.title') }}</h2>
<div class="status-indicator" :class="backendStatus">
<span class="status-dot"></span>
<span class="status-text">{{ $t(`admin.servicesStatus.status.${backendStatus}`) }}</span>
</div>
<div v-if="backendStatus === 'connected'" class="status-details">
<p>{{ $t('admin.servicesStatus.backend.connected') }}</p>
</div>
</div>
</div>
<!-- Chat Tab -->
<div v-if="activeTab === 'chat'" class="service-status">
<div class="status-card">
<h2>{{ $t('admin.servicesStatus.chat.title') }}</h2>
<div class="status-indicator" :class="chatStatus">
<span class="status-dot"></span>
<span class="status-text">{{ $t(`admin.servicesStatus.status.${chatStatus}`) }}</span>
</div>
<div v-if="chatStatus === 'connected'" class="status-details">
<p>{{ $t('admin.servicesStatus.chat.connected') }}</p>
</div>
</div>
</div>
<!-- Daemon Tab -->
<div v-if="activeTab === 'daemon'" class="service-status">
<div class="status-card">
<h2>{{ $t('admin.servicesStatus.daemon.title') }}</h2>
<div class="status-indicator" :class="daemonStatus">
<span class="status-dot"></span>
<span class="status-text">{{ $t(`admin.servicesStatus.status.${daemonStatus}`) }}</span>
</div>
<div v-if="daemonStatus === 'connected'" class="status-details">
<p>{{ $t('admin.servicesStatus.daemon.connected') }}</p>
</div>
</div>
<!-- Daemon Connections -->
<div v-if="daemonStatus === 'connected'" class="connections-section">
<h3>{{ $t('admin.servicesStatus.daemon.connections.title') }}</h3>
<div v-if="connections.length === 0" class="no-connections">
<p>{{ $t('admin.servicesStatus.daemon.connections.none') }}</p>
</div>
<div v-else class="connections-list">
<div v-for="(userConn, index) in connections" :key="index" class="connection-group">
<div class="connection-header">
<strong>{{ $t('admin.servicesStatus.daemon.connections.userId') }}:</strong> {{ userConn.userId }}
<span class="connection-count">({{ userConn.connectionCount }} {{ $t('admin.servicesStatus.daemon.connections.connections') }})</span>
</div>
<div v-for="(conn, connIndex) in userConn.connections" :key="connIndex" class="connection-details">
<div class="connection-info">
<div class="info-row">
<span class="info-label">{{ $t('admin.servicesStatus.daemon.connections.duration') }}:</span>
<span class="info-value">{{ formatDuration(conn.connectionDurationSeconds) }}</span>
</div>
<div class="info-row">
<span class="info-label">{{ $t('admin.servicesStatus.daemon.connections.lastPong') }}:</span>
<span class="info-value">{{ formatDuration(conn.timeSinceLastPongSeconds) }}</span>
</div>
<div class="info-row">
<span class="info-label">{{ $t('admin.servicesStatus.daemon.connections.pingTimeouts') }}:</span>
<span class="info-value">{{ conn.pingTimeoutCount }}</span>
</div>
<div class="info-row">
<span class="info-label">{{ $t('admin.servicesStatus.daemon.connections.pongReceived') }}:</span>
<span class="info-value" :class="{ 'status-ok': conn.pongReceived, 'status-warning': !conn.pongReceived }">
{{ conn.pongReceived ? $t('admin.servicesStatus.daemon.connections.yes') : $t('admin.servicesStatus.daemon.connections.no') }}
</span>
</div>
</div>
</div>
</div>
</div>
<div v-if="error" class="error-message">
<p>{{ error }}</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapState, mapGetters } from 'vuex';
import SimpleTabs from '@/components/SimpleTabs.vue';
export default {
name: 'ServicesStatusView',
components: {
SimpleTabs
},
data() {
return {
activeTab: 'backend',
tabs: [
{
value: 'backend',
label: this.$t('admin.servicesStatus.backend.title')
},
{
value: 'chat',
label: this.$t('admin.servicesStatus.chat.title')
},
{
value: 'daemon',
label: this.$t('admin.servicesStatus.daemon.title')
}
],
connections: [],
error: null,
daemonUpdateInterval: null
};
},
computed: {
...mapState(['socket', 'daemonSocket', 'connectionStatus', 'daemonConnectionStatus']),
backendStatus() {
if (!this.socket) return 'disconnected';
if (this.connectionStatus === 'connected') return 'connected';
if (this.connectionStatus === 'connecting') return 'connecting';
if (this.connectionStatus === 'error') return 'error';
return 'disconnected';
},
chatStatus() {
// Chat-Status überprüfen - hier müsste der Chat-WebSocket-Status aus dem Store kommen
// Für jetzt nehmen wir an, dass Chat über einen separaten Mechanismus läuft
return 'unknown';
},
daemonStatus() {
if (!this.daemonSocket) return 'disconnected';
if (this.daemonConnectionStatus === 'connected') return 'connected';
if (this.daemonConnectionStatus === 'connecting') return 'connecting';
if (this.daemonConnectionStatus === 'error') return 'error';
return 'disconnected';
}
},
watch: {
activeTab(newTab) {
if (newTab === 'daemon' && this.daemonStatus === 'connected') {
this.startDaemonUpdates();
} else {
this.stopDaemonUpdates();
}
},
daemonStatus(newStatus) {
if (newStatus === 'connected' && this.activeTab === 'daemon') {
this.startDaemonUpdates();
} else {
this.stopDaemonUpdates();
}
},
daemonSocket(newSocket, oldSocket) {
// Event-Listener bei Socket-Änderung aktualisieren
if (oldSocket) {
oldSocket.removeEventListener('message', this.handleDaemonMessage);
}
if (newSocket) {
newSocket.addEventListener('message', this.handleDaemonMessage);
if (this.activeTab === 'daemon' && this.daemonStatus === 'connected') {
this.startDaemonUpdates();
}
}
}
},
mounted() {
if (this.activeTab === 'daemon' && this.daemonStatus === 'connected') {
this.startDaemonUpdates();
}
},
beforeUnmount() {
this.stopDaemonUpdates();
},
methods: {
startDaemonUpdates() {
this.stopDaemonUpdates();
this.fetchDaemonConnections();
this.daemonUpdateInterval = setInterval(() => {
this.fetchDaemonConnections();
}, 5000); // Alle 5 Sekunden
},
stopDaemonUpdates() {
if (this.daemonUpdateInterval) {
clearInterval(this.daemonUpdateInterval);
this.daemonUpdateInterval = null;
}
},
fetchDaemonConnections() {
if (!this.daemonSocket || this.daemonSocket.readyState !== WebSocket.OPEN) {
this.error = this.$t('admin.servicesStatus.daemon.connections.notConnected');
return;
}
try {
const message = JSON.stringify({
event: 'getConnections'
});
this.daemonSocket.send(message);
this.error = null;
} catch (err) {
console.error('Fehler beim Senden von getConnections:', err);
this.error = this.$t('admin.servicesStatus.daemon.connections.sendError');
}
},
handleDaemonMessage(event) {
try {
const data = JSON.parse(event.data);
if (data.event === 'getConnectionsResponse') {
if (data.success) {
this.connections = data.data || [];
this.error = null;
} else {
this.error = data.error || this.$t('admin.servicesStatus.daemon.connections.error');
this.connections = [];
}
}
} catch (err) {
console.error('Fehler beim Verarbeiten der Daemon-Nachricht:', err);
}
},
formatDuration(seconds) {
if (seconds < 60) {
return `${seconds}s`;
} else if (seconds < 3600) {
const minutes = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${minutes}m ${secs}s`;
} else {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return `${hours}h ${minutes}m`;
}
}
},
created() {
// Event-Listener für Daemon-Nachrichten registrieren
if (this.daemonSocket) {
this.daemonSocket.addEventListener('message', this.handleDaemonMessage);
}
},
beforeUnmount() {
// Event-Listener entfernen
this.stopDaemonUpdates();
if (this.daemonSocket) {
this.daemonSocket.removeEventListener('message', this.handleDaemonMessage);
}
}
};
</script>
<style scoped>
.service-status {
padding: 20px;
}
.status-card {
background: #f5f5f5;
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.status-card h2 {
margin-top: 0;
margin-bottom: 15px;
}
.status-indicator {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 15px;
}
.status-dot {
width: 12px;
height: 12px;
border-radius: 50%;
display: inline-block;
}
.status-indicator.connected .status-dot {
background-color: #4caf50;
}
.status-indicator.connecting .status-dot {
background-color: #ff9800;
animation: pulse 1.5s infinite;
}
.status-indicator.disconnected .status-dot {
background-color: #f44336;
}
.status-indicator.error .status-dot {
background-color: #f44336;
}
.status-indicator.unknown .status-dot {
background-color: #9e9e9e;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.status-text {
font-weight: bold;
}
.status-details {
margin-top: 10px;
color: #666;
}
.connections-section {
margin-top: 30px;
}
.connections-section h3 {
margin-bottom: 15px;
}
.no-connections {
padding: 20px;
text-align: center;
color: #666;
}
.connections-list {
display: flex;
flex-direction: column;
gap: 20px;
}
.connection-group {
background: white;
border: 1px solid #ddd;
border-radius: 4px;
padding: 15px;
}
.connection-header {
font-size: 16px;
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.connection-count {
color: #666;
font-size: 14px;
font-weight: normal;
}
.connection-details {
margin-top: 10px;
padding-left: 20px;
}
.connection-info {
display: flex;
flex-direction: column;
gap: 8px;
}
.info-row {
display: flex;
justify-content: space-between;
padding: 5px 0;
}
.info-label {
font-weight: 500;
color: #555;
}
.info-value {
color: #333;
}
.status-ok {
color: #4caf50;
font-weight: bold;
}
.status-warning {
color: #ff9800;
font-weight: bold;
}
.error-message {
margin-top: 15px;
padding: 10px;
background: #ffebee;
border: 1px solid #f44336;
border-radius: 4px;
color: #c62828;
}
</style>