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:
@@ -230,6 +230,44 @@
|
||||
"updateSuccess": "Map wurde erfolgreich aktualisiert!",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,8 @@
|
||||
"match3": "Match3 Level",
|
||||
"taxiTools": "Taxi-Tools"
|
||||
},
|
||||
"chatrooms": "Chaträume"
|
||||
"chatrooms": "Chaträume",
|
||||
"servicesStatus": "Service-Status"
|
||||
},
|
||||
"m-friends": {
|
||||
"manageFriends": "Freunde verwalten",
|
||||
|
||||
@@ -230,6 +230,44 @@
|
||||
"updateSuccess": "Map updated 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,8 @@
|
||||
"match3": "Match3 Levels",
|
||||
"taxiTools": "Taxi Tools"
|
||||
},
|
||||
"chatrooms": "Chat rooms"
|
||||
"chatrooms": "Chat rooms",
|
||||
"servicesStatus": "Service Status"
|
||||
},
|
||||
"m-friends": {
|
||||
"manageFriends": "Manage friends",
|
||||
|
||||
@@ -8,6 +8,7 @@ import AdminMinigamesView from '../views/admin/MinigamesView.vue';
|
||||
import AdminTaxiToolsView from '../views/admin/TaxiToolsView.vue';
|
||||
import AdminUsersView from '../views/admin/UsersView.vue';
|
||||
import UserStatisticsView from '../views/admin/UserStatisticsView.vue';
|
||||
import ServicesStatusView from '../views/admin/ServicesStatusView.vue';
|
||||
|
||||
const adminRoutes = [
|
||||
{
|
||||
@@ -69,6 +70,12 @@ const adminRoutes = [
|
||||
name: 'AdminTaxiTools',
|
||||
component: AdminTaxiToolsView,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/admin/services/status',
|
||||
name: 'AdminServicesStatus',
|
||||
component: ServicesStatusView,
|
||||
meta: { requiresAuth: true }
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
419
frontend/src/views/admin/ServicesStatusView.vue
Normal file
419
frontend/src/views/admin/ServicesStatusView.vue
Normal 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>
|
||||
|
||||
Reference in New Issue
Block a user