update clietn

This commit is contained in:
2025-09-23 21:18:25 +07:00
parent 3b98f25e43
commit 21f70f1d3f
8 changed files with 3592 additions and 527 deletions

View File

@@ -45,30 +45,21 @@
<div class="controls">
<div class="input-group">
<label class="checkbox-group">
<input
v-model="config.useIPBasedId"
type="checkbox"
/>
<input v-model="config.useIPBasedId" type="checkbox" />
Use IP-based ID
</label>
</div>
<div class="input-group">
<label class="checkbox-group">
<input
v-model="config.autoReconnect"
type="checkbox"
/>
<input v-model="config.autoReconnect" type="checkbox" />
Auto Reconnect
</label>
</div>
<div class="input-group">
<label class="checkbox-group">
<input
v-model="config.heartbeatEnabled"
type="checkbox"
/>
<input v-model="config.heartbeatEnabled" type="checkbox" />
Enable Heartbeat
</label>
</div>
@@ -82,7 +73,7 @@
@click="connect"
>
<span v-if="isConnecting" class="loading-indicator"></span>
{{ isConnected ? 'Reconnect' : 'Connect' }}
{{ isConnected ? "Reconnect" : "Connect" }}
</button>
<button
@@ -101,10 +92,7 @@
Test Connection
</button>
<button
class="btn btn-secondary"
@click="clearMessages"
>
<button class="btn btn-secondary" @click="clearMessages">
Clear Messages
</button>
</div>
@@ -164,11 +152,11 @@
</div>
<div class="info-item">
<strong>Static ID:</strong>
<span>{{ connectionState.staticId || 'N/A' }}</span>
<span>{{ connectionState.staticId || "N/A" }}</span>
</div>
<div class="info-item">
<strong>IP Address:</strong>
<span>{{ connectionState.ipAddress || 'N/A' }}</span>
<span>{{ connectionState.ipAddress || "N/A" }}</span>
</div>
<div class="info-item">
<strong>User ID:</strong>
@@ -180,7 +168,9 @@
</div>
<div class="info-item">
<strong>Connected At:</strong>
<span>{{ new Date(connectionState.connectionStartTime || 0).toLocaleString() }}</span>
<span>{{
new Date(connectionState.connectionStartTime || 0).toLocaleString()
}}</span>
</div>
<div class="info-item">
<strong>Latency:</strong>
@@ -196,8 +186,8 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useWebSocket } from '../../composables/useWebSocket'
import { computed } from "vue";
import { useWebSocket } from "../../composables/useWebSocket";
const {
isConnected,
@@ -207,8 +197,8 @@ const {
connect,
disconnect,
testConnection,
clearMessages
} = useWebSocket()
clearMessages,
} = useWebSocket();
</script>
<style scoped>
@@ -343,8 +333,12 @@ const {
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.connection-info {

View File

@@ -71,10 +71,7 @@
Get Monitoring Data
</button>
<button
class="btn btn-secondary"
@click="clearQueryHistory"
>
<button class="btn btn-secondary" @click="clearQueryHistory">
Clear Query History
</button>
</div>
@@ -91,8 +88,11 @@
<div class="query-header">
<div class="query-type">{{ query.type }}</div>
<div class="query-time">{{ formatTime(query.timestamp) }}</div>
<div class="query-status" :class="query.success ? 'success' : 'error'">
{{ query.success ? 'Success' : 'Error' }}
<div
class="query-status"
:class="query.success ? 'success' : 'error'"
>
{{ query.success ? "Success" : "Error" }}
</div>
</div>
<div class="query-details">
@@ -157,89 +157,95 @@
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useWebSocket } from '../../composables/useWebSocket'
import { ref } from "vue";
import { useWebSocket } from "../../composables/useWebSocket";
const {
isConnected,
stats,
executeDatabaseQuery,
getStats,
getMonitoringData
} = useWebSocket()
getMonitoringData,
} = useWebSocket();
const queryType = ref('select')
const tableName = ref('')
const queryParams = ref('')
const customQuery = ref('')
const queryHistory = ref<Array<{
type: string
sql: string
params: string
result: any
error: string
success: boolean
timestamp: number
}>>([])
const queryType = ref("select");
const tableName = ref("");
const queryParams = ref("");
const customQuery = ref("");
const queryHistory = ref<
Array<{
type: string;
sql: string;
params: string;
result: any;
error: string;
success: boolean;
timestamp: number;
}>
>([]);
const executeQuery = async () => {
if (!isConnected.value) return
if (!isConnected.value) return;
let sql = ''
let params = {}
let sql = "";
let params = {};
try {
if (customQuery.value.trim()) {
sql = customQuery.value.trim()
params = queryParams.value ? JSON.parse(queryParams.value) : {}
sql = customQuery.value.trim();
params = queryParams.value ? JSON.parse(queryParams.value) : {};
} else {
switch (queryType.value) {
case 'select':
sql = `SELECT * FROM ${tableName.value}`
break
case 'insert':
params = JSON.parse(queryParams.value || '{}')
sql = `INSERT INTO ${tableName.value} SET ?`
break
case 'update':
params = JSON.parse(queryParams.value || '{}')
sql = `UPDATE ${tableName.value} SET ? WHERE id = ?`
break
case 'delete':
sql = `DELETE FROM ${tableName.value} WHERE id = ?`
break
case 'create_table':
sql = `CREATE TABLE IF NOT EXISTS ${tableName.value} (id INT AUTO_INCREMENT PRIMARY KEY, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)`
break
case 'drop_table':
sql = `DROP TABLE IF EXISTS ${tableName.value}`
break
case 'show_tables':
sql = 'SHOW TABLES'
break
case 'describe_table':
sql = `DESCRIBE ${tableName.value}`
break
case "select":
sql = `SELECT * FROM ${tableName.value}`;
break;
case "insert":
params = JSON.parse(queryParams.value || "{}");
sql = `INSERT INTO ${tableName.value} SET ?`;
break;
case "update":
params = JSON.parse(queryParams.value || "{}");
sql = `UPDATE ${tableName.value} SET ? WHERE id = ?`;
break;
case "delete":
sql = `DELETE FROM ${tableName.value} WHERE id = ?`;
break;
case "create_table":
sql = `CREATE TABLE IF NOT EXISTS ${tableName.value} (id INT AUTO_INCREMENT PRIMARY KEY, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)`;
break;
case "drop_table":
sql = `DROP TABLE IF EXISTS ${tableName.value}`;
break;
case "show_tables":
sql = "SHOW TABLES";
break;
case "describe_table":
sql = `DESCRIBE ${tableName.value}`;
break;
}
}
const result = await executeDatabaseQuery(queryType.value, {
sql,
params,
table: tableName.value
})
async function executeDatabaseQuery(queryData: {
sql: string;
params?: any;
table?: string;
}): Promise<any> {
// Function implementation
}
// Add to history
queryHistory.value.unshift({
type: queryType.value,
sql,
params: JSON.stringify(params),
result: result,
error: '',
result: await executeDatabaseQuery({
sql,
params,
table: tableName.value,
}),
error: "",
success: true,
timestamp: Date.now()
})
timestamp: Date.now(),
});
} catch (error) {
// Add error to history
queryHistory.value.unshift({
@@ -249,26 +255,28 @@ const executeQuery = async () => {
result: null,
error: error instanceof Error ? error.message : String(error),
success: false,
timestamp: Date.now()
})
timestamp: Date.now(),
});
}
}
};
const clearQueryHistory = () => {
queryHistory.value = []
}
queryHistory.value = [];
};
const formatTime = (timestamp: number) => {
return new Date(timestamp).toLocaleString()
}
return new Date(timestamp).toLocaleString();
};
const formatUptime = (uptime: number) => {
const seconds = Math.floor(uptime / 1000)
const hours = Math.floor(seconds / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
const secs = seconds % 60
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
}
const seconds = Math.floor(uptime / 1000);
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
return `${hours.toString().padStart(2, "0")}:${minutes
.toString()
.padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
};
</script>
<style scoped>

View File

@@ -1,18 +1,50 @@
<template>
<div class="messaging-tab">
<!-- Connection Status -->
<div class="connection-status" :class="connectionState.connectionStatus">
<div class="status-indicator">
<div class="status-dot" :class="connectionState.connectionStatus"></div>
<span class="status-text">
{{
connectionState.connectionStatus === "connected"
? "Connected"
: connectionState.connectionStatus === "connecting"
? "Connecting..."
: connectionState.connectionStatus === "error"
? "Connection Error"
: "Disconnected"
}}
</span>
</div>
<div class="connection-details" v-if="connectionState.clientId">
<span>Client ID: {{ connectionState.clientId }}</span>
<span v-if="connectionState.currentRoom"
>Room: {{ connectionState.currentRoom }}</span
>
<span v-if="connectionState.connectionLatency > 0"
>Latency: {{ connectionState.connectionLatency }}ms</span
>
<span v-if="connectionState.connectionHealth"
>Health: {{ connectionState.connectionHealth }}</span
>
</div>
</div>
<div class="controls">
<div class="input-group">
<label for="broadcastMessage">Broadcast Message</label>
<textarea
id="broadcastMessage"
v-model="broadcastMessage"
v-model="messageText"
placeholder="Enter message to broadcast to all clients"
rows="3"
></textarea>
</div>
<div class="input-group">
<label for="targetClientId">Target Client ID (for direct messages)</label>
<label for="targetClientId"
>Target Client ID (for direct messages)</label
>
<input
id="targetClientId"
v-model="targetClientId"
@@ -45,16 +77,18 @@
<div class="controls">
<button
class="btn btn-primary"
:disabled="!isConnected || !broadcastMessage.trim()"
@click="sendBroadcast"
:disabled="!isConnected || !messageText.trim()"
@click="handleSendBroadcast"
>
Send Broadcast
</button>
<button
class="btn btn-info"
:disabled="!isConnected || !targetClientId.trim() || !broadcastMessage.trim()"
@click="sendDirectMessage"
:disabled="
!isConnected || !targetClientId.trim() || !messageText.trim()
"
@click="handleSendDirectMessage"
>
Send Direct Message
</button>
@@ -62,7 +96,7 @@
<button
class="btn btn-success"
:disabled="!isConnected || !targetRoom.trim() || !roomMessage.trim()"
@click="sendRoomMessage"
@click="handleSendRoomMessage"
>
Send Room Message
</button>
@@ -93,17 +127,11 @@
Get Server Info
</button>
<button
class="btn btn-secondary"
@click="clearMessages"
>
<button class="btn btn-secondary" @click="clearMessages">
Clear Messages
</button>
<button
class="btn btn-secondary"
@click="clearActivityLog"
>
<button class="btn btn-secondary" @click="clearActivityLog">
Clear Activity Log
</button>
</div>
@@ -116,7 +144,9 @@
v-for="user in onlineUsers"
:key="user.client_id"
class="user-item"
:class="{ 'current-user': user.client_id === connectionState.clientId }"
:class="{
'current-user': user.client_id === connectionState.clientId,
}"
>
<div class="user-info">
<div class="user-id">{{ user.client_id }}</div>
@@ -160,57 +190,67 @@
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useWebSocket } from '../../composables/useWebSocket'
import { ref, onMounted } from "vue";
import { useWebSocket } from "../../composables/useWebSocket";
const {
isConnected,
connectionState,
onlineUsers,
activityLog,
broadcastMessage: broadcastMessageFn,
sendDirectMessage: sendDirectMessageFn,
sendRoomMessage: sendRoomMessageFn,
broadcastMessage,
sendDirectMessage,
sendRoomMessage,
sendHeartbeat,
getOnlineUsers,
getServerInfo,
clearMessages,
clearActivityLog
} = useWebSocket()
clearActivityLog,
connect,
} = useWebSocket();
const broadcastMessage = ref('')
const targetClientId = ref('')
const roomMessage = ref('')
const targetRoom = ref('')
const messageText = ref("");
const targetClientId = ref("");
const roomMessage = ref("");
const targetRoom = ref("");
const sendBroadcast = () => {
if (broadcastMessage.value.trim()) {
broadcastMessageFn(broadcastMessage.value.trim())
broadcastMessage.value = ''
const handleSendBroadcast = () => {
if (messageText.value.trim()) {
broadcastMessage(messageText.value.trim());
messageText.value = "";
}
}
};
const sendDirectMessage = () => {
if (targetClientId.value.trim() && broadcastMessage.value.trim()) {
sendDirectMessageFn(targetClientId.value.trim(), broadcastMessage.value.trim())
broadcastMessage.value = ''
const handleSendDirectMessage = () => {
if (targetClientId.value.trim() && messageText.value.trim()) {
sendDirectMessage(targetClientId.value.trim(), messageText.value.trim());
messageText.value = "";
}
}
};
const sendRoomMessage = () => {
const handleSendRoomMessage = () => {
if (targetRoom.value.trim() && roomMessage.value.trim()) {
sendRoomMessageFn(targetRoom.value.trim(), roomMessage.value.trim())
roomMessage.value = ''
sendRoomMessage(targetRoom.value.trim(), roomMessage.value.trim());
roomMessage.value = "";
}
}
};
const setTargetClient = (clientId: string) => {
targetClientId.value = clientId
}
targetClientId.value = clientId;
};
const formatTime = (timestamp: number) => {
return new Date(timestamp).toLocaleString()
}
return new Date(timestamp).toLocaleString();
};
// Initialize connection on component mount
onMounted(() => {
console.log("MessagingTab mounted, initializing WebSocket connection...");
if (!isConnected.value) {
console.log("Attempting to connect to WebSocket...");
connect();
}
});
</script>
<style scoped>
@@ -218,6 +258,98 @@ const formatTime = (timestamp: number) => {
padding: 20px 0;
}
/* Connection Status Styles */
.connection-status {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
margin-bottom: 24px;
border-radius: 8px;
border: 2px solid;
font-weight: 600;
}
.connection-status.connected {
background: #e8f5e8;
border-color: #4caf50;
color: #2e7d32;
}
.connection-status.connecting {
background: #fff3e0;
border-color: #ff9800;
color: #ef6c00;
}
.connection-status.error {
background: #ffebee;
border-color: #f44336;
color: #c62828;
}
.connection-status.disconnected {
background: #f5f5f5;
border-color: #9e9e9e;
color: #616161;
}
.status-indicator {
display: flex;
align-items: center;
gap: 12px;
}
.status-dot {
width: 12px;
height: 12px;
border-radius: 50%;
animation: pulse 2s infinite;
}
.status-dot.connected {
background: #4caf50;
}
.status-dot.connecting {
background: #ff9800;
animation: pulse 1s infinite;
}
.status-dot.error {
background: #f44336;
animation: pulse 0.5s infinite;
}
.status-dot.disconnected {
background: #9e9e9e;
}
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.5;
}
100% {
opacity: 1;
}
}
.connection-details {
display: flex;
gap: 20px;
font-size: 12px;
opacity: 0.8;
}
.connection-details span {
padding: 4px 8px;
background: rgba(255, 255, 255, 0.3);
border-radius: 4px;
}
.controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
@@ -302,7 +434,7 @@ const formatTime = (timestamp: number) => {
}
.btn-warning {
background: #FFC107;
background: #ffc107;
color: #212529;
}

View File

@@ -48,17 +48,17 @@
<div class="controls">
<button
class="btn btn-primary"
:disabled="!isConnected || !notificationChannel.trim() || !notificationPayload.trim()"
:disabled="
!isConnected ||
!notificationChannel.trim() ||
!notificationPayload.trim()
"
@click="triggerNotification"
>
Trigger Notification
</button>
<button
class="btn btn-info"
:disabled="!isConnected"
@click="getStats"
>
<button class="btn btn-info" :disabled="!isConnected" @click="getStats">
Refresh Stats
</button>
@@ -70,10 +70,7 @@
Get Monitoring Data
</button>
<button
class="btn btn-warning"
@click="clearMonitoringData"
>
<button class="btn btn-warning" @click="clearMonitoringData">
Clear Data
</button>
</div>
@@ -90,7 +87,9 @@
</div>
<div class="health-item">
<div class="health-label">Latency</div>
<div class="health-value">{{ connectionState.connectionLatency }}ms</div>
<div class="health-value">
{{ connectionState.connectionLatency }}ms
</div>
</div>
<div class="health-item">
<div class="health-label">Messages Sent</div>
@@ -102,7 +101,9 @@
</div>
<div class="health-item">
<div class="health-label">Reconnect Attempts</div>
<div class="health-value">{{ connectionState.reconnectAttempts }}</div>
<div class="health-value">
{{ connectionState.reconnectAttempts }}
</div>
</div>
<div class="health-item">
<div class="health-label">Connection Time</div>
@@ -118,43 +119,52 @@
<div class="stat-item">
<div class="stat-label">Connected Clients</div>
<div class="stat-value">{{ stats.connected_clients }}</div>
<div class="stat-change" :class="getStatChangeClass('connected_clients')">
{{ getStatChange('connected_clients') }}
<div
class="stat-change"
:class="getStatChangeClass('connected_clients')"
>
{{ getStatChange("connected_clients") }}
</div>
</div>
<div class="stat-item">
<div class="stat-label">Unique IPs</div>
<div class="stat-value">{{ stats.unique_ips }}</div>
<div class="stat-change" :class="getStatChangeClass('unique_ips')">
{{ getStatChange('unique_ips') }}
{{ getStatChange("unique_ips") }}
</div>
</div>
<div class="stat-item">
<div class="stat-label">Static Clients</div>
<div class="stat-value">{{ stats.static_clients }}</div>
<div class="stat-change" :class="getStatChangeClass('static_clients')">
{{ getStatChange('static_clients') }}
<div
class="stat-change"
:class="getStatChangeClass('static_clients')"
>
{{ getStatChange("static_clients") }}
</div>
</div>
<div class="stat-item">
<div class="stat-label">Active Rooms</div>
<div class="stat-value">{{ stats.active_rooms }}</div>
<div class="stat-change" :class="getStatChangeClass('active_rooms')">
{{ getStatChange('active_rooms') }}
{{ getStatChange("active_rooms") }}
</div>
</div>
<div class="stat-item">
<div class="stat-label">Message Queue</div>
<div class="stat-value">{{ stats.message_queue_size }}</div>
<div class="stat-change" :class="getStatChangeClass('message_queue_size')">
{{ getStatChange('message_queue_size') }}
<div
class="stat-change"
:class="getStatChangeClass('message_queue_size')"
>
{{ getStatChange("message_queue_size") }}
</div>
</div>
<div class="stat-item">
<div class="stat-label">Queue Workers</div>
<div class="stat-value">{{ stats.queue_workers }}</div>
<div class="stat-change" :class="getStatChangeClass('queue_workers')">
{{ getStatChange('queue_workers') }}
{{ getStatChange("queue_workers") }}
</div>
</div>
<div class="stat-item">
@@ -187,7 +197,9 @@
>
<div class="notification-header">
<div class="notification-channel">{{ notification.channel }}</div>
<div class="notification-time">{{ formatTime(notification.timestamp) }}</div>
<div class="notification-time">
{{ formatTime(notification.timestamp) }}
</div>
</div>
<div class="notification-payload">
<pre>{{ JSON.stringify(notification.payload, null, 2) }}</pre>
@@ -202,8 +214,8 @@
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useWebSocket } from '../../composables/useWebSocket'
import { ref, computed } from "vue";
import { useWebSocket } from "../../composables/useWebSocket";
const {
isConnected,
@@ -212,119 +224,135 @@ const {
monitoringData,
triggerNotification: triggerNotificationFn,
getStats,
getMonitoringData
} = useWebSocket()
getMonitoringData,
} = useWebSocket();
const notificationChannel = ref('')
const notificationPayload = ref('')
const triggerEvent = ref('user_joined')
const customEvent = ref('')
const notificationHistory = ref<Array<{
channel: string
payload: any
timestamp: number
}>>([])
const notificationChannel = ref("");
const notificationPayload = ref("");
const triggerEvent = ref("user_joined");
const customEvent = ref("");
const notificationHistory = ref<
Array<{
channel: string;
payload: any;
timestamp: number;
}>
>([]);
const connectionHealthClass = computed(() => {
switch (connectionState.connectionHealth) {
case 'excellent': return 'health-excellent'
case 'good': return 'health-good'
case 'warning': return 'health-warning'
case 'poor': return 'health-poor'
default: return 'health-poor'
case "excellent":
return "health-excellent";
case "good":
return "health-good";
case "warning":
return "health-warning";
case "poor":
return "health-poor";
default:
return "health-poor";
}
})
});
const connectionHealthText = computed(() => {
switch (connectionState.connectionHealth) {
case 'excellent': return 'Excellent'
case 'good': return 'Good'
case 'warning': return 'Warning'
case 'poor': return 'Poor'
default: return 'Unknown'
case "excellent":
return "Excellent";
case "good":
return "Good";
case "warning":
return "Warning";
case "poor":
return "Poor";
default:
return "Unknown";
}
})
});
const triggerNotification = () => {
if (!notificationChannel.value.trim() || !notificationPayload.value.trim()) return
if (!notificationChannel.value.trim() || !notificationPayload.value.trim())
return;
try {
const payload = JSON.parse(notificationPayload.value)
const eventName = triggerEvent.value === 'custom' ? customEvent.value : triggerEvent.value
const payload = JSON.parse(notificationPayload.value);
const eventName =
triggerEvent.value === "custom" ? customEvent.value : triggerEvent.value;
triggerNotificationFn(notificationChannel.value.trim(), {
...payload,
event: eventName,
timestamp: Date.now()
})
triggerNotificationFn(notificationChannel.value.trim());
// Add to history
notificationHistory.value.unshift({
channel: notificationChannel.value.trim(),
payload,
timestamp: Date.now()
})
timestamp: Date.now(),
});
// Clear form
notificationChannel.value = ''
notificationPayload.value = ''
notificationChannel.value = "";
notificationPayload.value = "";
} catch (error) {
alert('Invalid JSON payload: ' + (error instanceof Error ? error.message : String(error)))
alert(
"Invalid JSON payload: " +
(error instanceof Error ? error.message : String(error))
);
}
}
};
const clearMonitoringData = () => {
notificationHistory.value = []
}
notificationHistory.value = [];
};
const formatTime = (timestamp: number) => {
return new Date(timestamp).toLocaleString()
}
return new Date(timestamp).toLocaleString();
};
const formatUptime = (uptime: number) => {
const seconds = Math.floor(uptime / 1000)
const hours = Math.floor(seconds / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
const secs = seconds % 60
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
}
const seconds = Math.floor(uptime / 1000);
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
return `${hours.toString().padStart(2, "0")}:${minutes
.toString()
.padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
};
// Mock stat changes for demonstration
const previousStats = ref<any>(null)
const previousStats = ref<any>(null);
const getStatChange = (statName: string) => {
if (!previousStats.value || !stats.value) return ''
if (!previousStats.value || !stats.value) return "";
const current = stats.value[statName as keyof typeof stats.value]
const previous = previousStats.value[statName as keyof typeof previousStats.value]
const current = stats.value[statName as keyof typeof stats.value] as number;
const previous =
previousStats.value[statName as keyof typeof previousStats.value];
if (current > previous) return `+${current - previous}`
if (current < previous) return `-${previous - current}`
return '0'
}
if (current > previous) return `+${current - previous}`;
if (current < previous) return `-${previous - current}`;
return "0";
};
const getStatChangeClass = (statName: string) => {
if (!previousStats.value || !stats.value) return ''
if (!previousStats.value || !stats.value) return "";
const current = stats.value[statName as keyof typeof stats.value]
const previous = previousStats.value[statName as keyof typeof previousStats.value]
const current = stats.value[statName as keyof typeof stats.value];
const previous =
previousStats.value[statName as keyof typeof previousStats.value];
if (current > previous) return 'stat-increase'
if (current < previous) return 'stat-decrease'
return 'stat-unchanged'
}
if (current > previous) return "stat-increase";
if (current < previous) return "stat-decrease";
return "stat-unchanged";
};
// Update previous stats when new stats arrive
const updatePreviousStats = () => {
if (stats.value) {
previousStats.value = { ...stats.value }
previousStats.value = { ...stats.value };
}
}
};
// Watch for stats changes
import { watch } from 'vue'
watch(stats, updatePreviousStats, { deep: true })
import { watch } from "vue";
watch(stats, updatePreviousStats, { deep: true });
</script>
<style scoped>
@@ -423,7 +451,7 @@ watch(stats, updatePreviousStats, { deep: true })
}
.btn-warning {
background: #FFC107;
background: #ffc107;
color: #212529;
}

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,180 +1,182 @@
export interface WebSocketMessage {
type: string
data: any
timestamp?: number
client_id?: string
message_id?: string
type: string;
data: any;
timestamp?: number;
client_id?: string;
message_id?: string;
}
export interface ConnectionInfo {
client_id: string
static_id: string
ip_address: string
room: string
user_id: string
connected_at: number
id_type: string
client_id: string;
static_id: string;
ip_address: string;
room: string;
user_id: string;
connected_at: number;
id_type: string;
}
export interface ClientInfo {
id: string
static_id: string
ip_address: string
user_id: string
room: string
connected_at: number
last_ping: number
is_active?: boolean
id: string;
static_id: string;
ip_address: string;
user_id: string;
room: string;
connected_at: number;
last_ping: number;
is_active?: boolean;
}
export interface OnlineUser {
client_id: string
static_id: string
user_id: string
room: string
ip_address: string
connected_at: number
last_ping: number
client_id: string;
static_id: string;
user_id: string;
room: string;
ip_address: string;
connected_at: number;
last_ping: number;
}
export interface ConnectionStats {
connected_clients: number
unique_ips: number
static_clients: number
active_rooms: number
ip_distribution: Record<string, number>
room_distribution: Record<string, number>
message_queue_size: number
queue_workers: number
uptime: number
timestamp: number
connected_clients: number;
unique_ips: number;
static_clients: number;
active_rooms: number;
ip_distribution: Record<string, number>;
room_distribution: Record<string, number>;
message_queue_size: number;
queue_workers: number;
uptime: number;
timestamp: number;
}
export interface SystemHealth {
databases: any
available_dbs: string[]
websocket_status: string
uptime_seconds: number
databases: any;
available_dbs: string[];
websocket_status: string;
uptime_seconds: number;
}
export interface PerformanceMetrics {
messages_per_second: number
average_latency_ms: number
error_rate_percent: number
memory_usage_bytes: number
messages_per_second: number;
average_latency_ms: number;
error_rate_percent: number;
memory_usage_bytes: number;
}
export interface MonitoringData {
stats: ConnectionStats
recent_activity: ActivityLog[]
system_health: SystemHealth
performance: PerformanceMetrics
stats: ConnectionStats;
recent_activity: ActivityLog[];
system_health: SystemHealth;
performance: PerformanceMetrics;
}
export interface ActivityLog {
timestamp: number
event: string
client_id: string
details: string
timestamp: number;
event: string;
client_id: string;
details: string;
}
export interface MessageHistory {
timestamp: Date
type: string
data: any
messageId?: string
size: number
timestamp: Date;
type: string;
data: any;
messageId?: string;
size: number;
icon?: string;
timeString?: string;
}
export interface ConnectionState {
isConnected: boolean
isConnecting: boolean
connectionStatus: 'disconnected' | 'connecting' | 'connected' | 'error'
clientId: string | null
staticId: string | null
currentRoom: string | null
userId: string
ipAddress: string | null
connectionStartTime: number | null
lastPingTime: number | null
connectionLatency: number
connectionHealth: 'poor' | 'warning' | 'good' | 'excellent'
reconnectAttempts: number
messagesReceived: number
messagesSent: number
uptime: string
isConnected: boolean;
isConnecting: boolean;
connectionStatus: "disconnected" | "connecting" | "connected" | "error";
clientId: string | null;
staticId: string | null;
currentRoom: string | null;
userId: string;
ipAddress: string | null;
connectionStartTime: number | null;
lastPingTime: number | null;
connectionLatency: number;
connectionHealth: "poor" | "warning" | "good" | "excellent";
reconnectAttempts: number;
messagesReceived: number;
messagesSent: number;
uptime: string;
}
export interface WebSocketConfig {
wsUrl: string
userId: string
room: string
staticId?: string
useIPBasedId?: boolean
autoReconnect: boolean
heartbeatEnabled: boolean
maxReconnectAttempts: number
reconnectDelay: number
maxReconnectDelay: number
heartbeatInterval: number
heartbeatTimeout: number
maxMissedHeartbeats: number
maxMessages: number
messageWarningThreshold: number
actionThrottle: number
wsUrl: string;
userId: string;
room: string;
staticId?: string;
useIPBasedId?: boolean;
autoReconnect: boolean;
heartbeatEnabled: boolean;
maxReconnectAttempts: number;
reconnectDelay: number;
maxReconnectDelay: number;
heartbeatInterval: number;
heartbeatTimeout: number;
maxMissedHeartbeats: number;
maxMessages: number;
messageWarningThreshold: number;
actionThrottle: number;
}
export type MessageType =
| 'welcome'
| 'broadcast'
| 'direct_message'
| 'room_message'
| 'ping'
| 'pong'
| 'heartbeat'
| 'heartbeat_ack'
| 'connection_test'
| 'connection_test_result'
| 'get_online_users'
| 'online_users'
| 'get_server_info'
| 'server_info'
| 'error'
| 'message_received'
| 'broadcast_sent'
| 'direct_message_sent'
| 'room_message_sent'
| 'db_insert'
| 'db_query'
| 'db_custom_query'
| 'query_result'
| 'admin_kick_client'
| 'admin_kill_server'
| 'get_server_stats'
| 'get_system_health'
| 'admin_clear_logs'
| 'get_stats'
| 'get_room_info'
| 'join_room'
| 'leave_room'
| 'database_change'
| 'data_stream'
| 'server_heartbeat'
| 'system_status'
| 'clients_by_ip'
| 'client_info'
| 'get_clients_by_ip'
| 'get_client_info'
| 'health_check'
| 'database_list'
| 'connection_stats'
| 'trigger_notification'
| 'notification_sent'
| 'API_TEST'
| 'manual_test'
| 'retribusi_created'
| 'retribusi_updated'
| 'retribusi_deleted'
| 'peserta_changes'
| 'retribusi_changes'
| 'system_changes'
| "welcome"
| "broadcast"
| "direct_message"
| "room_message"
| "ping"
| "pong"
| "heartbeat"
| "heartbeat_ack"
| "connection_test"
| "connection_test_result"
| "get_online_users"
| "online_users"
| "get_server_info"
| "server_info"
| "error"
| "message_received"
| "broadcast_sent"
| "direct_message_sent"
| "room_message_sent"
| "db_insert"
| "db_query"
| "db_custom_query"
| "query_result"
| "admin_kick_client"
| "admin_kill_server"
| "get_server_stats"
| "get_system_health"
| "admin_clear_logs"
| "get_stats"
| "get_room_info"
| "join_room"
| "leave_room"
| "database_change"
| "data_stream"
| "server_heartbeat"
| "system_status"
| "clients_by_ip"
| "client_info"
| "get_clients_by_ip"
| "get_client_info"
| "health_check"
| "database_list"
| "connection_stats"
| "trigger_notification"
| "notification_sent"
| "API_TEST"
| "manual_test"
| "retribusi_created"
| "retribusi_updated"
| "retribusi_deleted"
| "peserta_changes"
| "retribusi_changes"
| "system_changes";