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

View File

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

View File

@@ -1,18 +1,50 @@
<template> <template>
<div class="messaging-tab"> <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="controls">
<div class="input-group"> <div class="input-group">
<label for="broadcastMessage">Broadcast Message</label> <label for="broadcastMessage">Broadcast Message</label>
<textarea <textarea
id="broadcastMessage" id="broadcastMessage"
v-model="broadcastMessage" v-model="messageText"
placeholder="Enter message to broadcast to all clients" placeholder="Enter message to broadcast to all clients"
rows="3" rows="3"
></textarea> ></textarea>
</div> </div>
<div class="input-group"> <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 <input
id="targetClientId" id="targetClientId"
v-model="targetClientId" v-model="targetClientId"
@@ -45,16 +77,18 @@
<div class="controls"> <div class="controls">
<button <button
class="btn btn-primary" class="btn btn-primary"
:disabled="!isConnected || !broadcastMessage.trim()" :disabled="!isConnected || !messageText.trim()"
@click="sendBroadcast" @click="handleSendBroadcast"
> >
Send Broadcast Send Broadcast
</button> </button>
<button <button
class="btn btn-info" class="btn btn-info"
:disabled="!isConnected || !targetClientId.trim() || !broadcastMessage.trim()" :disabled="
@click="sendDirectMessage" !isConnected || !targetClientId.trim() || !messageText.trim()
"
@click="handleSendDirectMessage"
> >
Send Direct Message Send Direct Message
</button> </button>
@@ -62,7 +96,7 @@
<button <button
class="btn btn-success" class="btn btn-success"
:disabled="!isConnected || !targetRoom.trim() || !roomMessage.trim()" :disabled="!isConnected || !targetRoom.trim() || !roomMessage.trim()"
@click="sendRoomMessage" @click="handleSendRoomMessage"
> >
Send Room Message Send Room Message
</button> </button>
@@ -93,17 +127,11 @@
Get Server Info Get Server Info
</button> </button>
<button <button class="btn btn-secondary" @click="clearMessages">
class="btn btn-secondary"
@click="clearMessages"
>
Clear Messages Clear Messages
</button> </button>
<button <button class="btn btn-secondary" @click="clearActivityLog">
class="btn btn-secondary"
@click="clearActivityLog"
>
Clear Activity Log Clear Activity Log
</button> </button>
</div> </div>
@@ -116,7 +144,9 @@
v-for="user in onlineUsers" v-for="user in onlineUsers"
:key="user.client_id" :key="user.client_id"
class="user-item" 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-info">
<div class="user-id">{{ user.client_id }}</div> <div class="user-id">{{ user.client_id }}</div>
@@ -160,57 +190,67 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref, onMounted } from "vue";
import { useWebSocket } from '../../composables/useWebSocket' import { useWebSocket } from "../../composables/useWebSocket";
const { const {
isConnected, isConnected,
connectionState, connectionState,
onlineUsers, onlineUsers,
activityLog, activityLog,
broadcastMessage: broadcastMessageFn, broadcastMessage,
sendDirectMessage: sendDirectMessageFn, sendDirectMessage,
sendRoomMessage: sendRoomMessageFn, sendRoomMessage,
sendHeartbeat, sendHeartbeat,
getOnlineUsers, getOnlineUsers,
getServerInfo, getServerInfo,
clearMessages, clearMessages,
clearActivityLog clearActivityLog,
} = useWebSocket() connect,
} = useWebSocket();
const broadcastMessage = ref('') const messageText = ref("");
const targetClientId = ref('') const targetClientId = ref("");
const roomMessage = ref('') const roomMessage = ref("");
const targetRoom = ref('') const targetRoom = ref("");
const sendBroadcast = () => { const handleSendBroadcast = () => {
if (broadcastMessage.value.trim()) { if (messageText.value.trim()) {
broadcastMessageFn(broadcastMessage.value.trim()) broadcastMessage(messageText.value.trim());
broadcastMessage.value = '' messageText.value = "";
}
} }
};
const sendDirectMessage = () => { const handleSendDirectMessage = () => {
if (targetClientId.value.trim() && broadcastMessage.value.trim()) { if (targetClientId.value.trim() && messageText.value.trim()) {
sendDirectMessageFn(targetClientId.value.trim(), broadcastMessage.value.trim()) sendDirectMessage(targetClientId.value.trim(), messageText.value.trim());
broadcastMessage.value = '' messageText.value = "";
}
} }
};
const sendRoomMessage = () => { const handleSendRoomMessage = () => {
if (targetRoom.value.trim() && roomMessage.value.trim()) { if (targetRoom.value.trim() && roomMessage.value.trim()) {
sendRoomMessageFn(targetRoom.value.trim(), roomMessage.value.trim()) sendRoomMessage(targetRoom.value.trim(), roomMessage.value.trim());
roomMessage.value = '' roomMessage.value = "";
}
} }
};
const setTargetClient = (clientId: string) => { const setTargetClient = (clientId: string) => {
targetClientId.value = clientId targetClientId.value = clientId;
} };
const formatTime = (timestamp: number) => { 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> </script>
<style scoped> <style scoped>
@@ -218,6 +258,98 @@ const formatTime = (timestamp: number) => {
padding: 20px 0; 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 { .controls {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
@@ -302,7 +434,7 @@ const formatTime = (timestamp: number) => {
} }
.btn-warning { .btn-warning {
background: #FFC107; background: #ffc107;
color: #212529; color: #212529;
} }

View File

@@ -48,17 +48,17 @@
<div class="controls"> <div class="controls">
<button <button
class="btn btn-primary" class="btn btn-primary"
:disabled="!isConnected || !notificationChannel.trim() || !notificationPayload.trim()" :disabled="
!isConnected ||
!notificationChannel.trim() ||
!notificationPayload.trim()
"
@click="triggerNotification" @click="triggerNotification"
> >
Trigger Notification Trigger Notification
</button> </button>
<button <button class="btn btn-info" :disabled="!isConnected" @click="getStats">
class="btn btn-info"
:disabled="!isConnected"
@click="getStats"
>
Refresh Stats Refresh Stats
</button> </button>
@@ -70,10 +70,7 @@
Get Monitoring Data Get Monitoring Data
</button> </button>
<button <button class="btn btn-warning" @click="clearMonitoringData">
class="btn btn-warning"
@click="clearMonitoringData"
>
Clear Data Clear Data
</button> </button>
</div> </div>
@@ -90,7 +87,9 @@
</div> </div>
<div class="health-item"> <div class="health-item">
<div class="health-label">Latency</div> <div class="health-label">Latency</div>
<div class="health-value">{{ connectionState.connectionLatency }}ms</div> <div class="health-value">
{{ connectionState.connectionLatency }}ms
</div>
</div> </div>
<div class="health-item"> <div class="health-item">
<div class="health-label">Messages Sent</div> <div class="health-label">Messages Sent</div>
@@ -102,7 +101,9 @@
</div> </div>
<div class="health-item"> <div class="health-item">
<div class="health-label">Reconnect Attempts</div> <div class="health-label">Reconnect Attempts</div>
<div class="health-value">{{ connectionState.reconnectAttempts }}</div> <div class="health-value">
{{ connectionState.reconnectAttempts }}
</div>
</div> </div>
<div class="health-item"> <div class="health-item">
<div class="health-label">Connection Time</div> <div class="health-label">Connection Time</div>
@@ -118,43 +119,52 @@
<div class="stat-item"> <div class="stat-item">
<div class="stat-label">Connected Clients</div> <div class="stat-label">Connected Clients</div>
<div class="stat-value">{{ stats.connected_clients }}</div> <div class="stat-value">{{ stats.connected_clients }}</div>
<div class="stat-change" :class="getStatChangeClass('connected_clients')"> <div
{{ getStatChange('connected_clients') }} class="stat-change"
:class="getStatChangeClass('connected_clients')"
>
{{ getStatChange("connected_clients") }}
</div> </div>
</div> </div>
<div class="stat-item"> <div class="stat-item">
<div class="stat-label">Unique IPs</div> <div class="stat-label">Unique IPs</div>
<div class="stat-value">{{ stats.unique_ips }}</div> <div class="stat-value">{{ stats.unique_ips }}</div>
<div class="stat-change" :class="getStatChangeClass('unique_ips')"> <div class="stat-change" :class="getStatChangeClass('unique_ips')">
{{ getStatChange('unique_ips') }} {{ getStatChange("unique_ips") }}
</div> </div>
</div> </div>
<div class="stat-item"> <div class="stat-item">
<div class="stat-label">Static Clients</div> <div class="stat-label">Static Clients</div>
<div class="stat-value">{{ stats.static_clients }}</div> <div class="stat-value">{{ stats.static_clients }}</div>
<div class="stat-change" :class="getStatChangeClass('static_clients')"> <div
{{ getStatChange('static_clients') }} class="stat-change"
:class="getStatChangeClass('static_clients')"
>
{{ getStatChange("static_clients") }}
</div> </div>
</div> </div>
<div class="stat-item"> <div class="stat-item">
<div class="stat-label">Active Rooms</div> <div class="stat-label">Active Rooms</div>
<div class="stat-value">{{ stats.active_rooms }}</div> <div class="stat-value">{{ stats.active_rooms }}</div>
<div class="stat-change" :class="getStatChangeClass('active_rooms')"> <div class="stat-change" :class="getStatChangeClass('active_rooms')">
{{ getStatChange('active_rooms') }} {{ getStatChange("active_rooms") }}
</div> </div>
</div> </div>
<div class="stat-item"> <div class="stat-item">
<div class="stat-label">Message Queue</div> <div class="stat-label">Message Queue</div>
<div class="stat-value">{{ stats.message_queue_size }}</div> <div class="stat-value">{{ stats.message_queue_size }}</div>
<div class="stat-change" :class="getStatChangeClass('message_queue_size')"> <div
{{ getStatChange('message_queue_size') }} class="stat-change"
:class="getStatChangeClass('message_queue_size')"
>
{{ getStatChange("message_queue_size") }}
</div> </div>
</div> </div>
<div class="stat-item"> <div class="stat-item">
<div class="stat-label">Queue Workers</div> <div class="stat-label">Queue Workers</div>
<div class="stat-value">{{ stats.queue_workers }}</div> <div class="stat-value">{{ stats.queue_workers }}</div>
<div class="stat-change" :class="getStatChangeClass('queue_workers')"> <div class="stat-change" :class="getStatChangeClass('queue_workers')">
{{ getStatChange('queue_workers') }} {{ getStatChange("queue_workers") }}
</div> </div>
</div> </div>
<div class="stat-item"> <div class="stat-item">
@@ -187,7 +197,9 @@
> >
<div class="notification-header"> <div class="notification-header">
<div class="notification-channel">{{ notification.channel }}</div> <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>
<div class="notification-payload"> <div class="notification-payload">
<pre>{{ JSON.stringify(notification.payload, null, 2) }}</pre> <pre>{{ JSON.stringify(notification.payload, null, 2) }}</pre>
@@ -202,8 +214,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue' import { ref, computed } from "vue";
import { useWebSocket } from '../../composables/useWebSocket' import { useWebSocket } from "../../composables/useWebSocket";
const { const {
isConnected, isConnected,
@@ -212,119 +224,135 @@ const {
monitoringData, monitoringData,
triggerNotification: triggerNotificationFn, triggerNotification: triggerNotificationFn,
getStats, getStats,
getMonitoringData getMonitoringData,
} = useWebSocket() } = useWebSocket();
const notificationChannel = ref('') const notificationChannel = ref("");
const notificationPayload = ref('') const notificationPayload = ref("");
const triggerEvent = ref('user_joined') const triggerEvent = ref("user_joined");
const customEvent = ref('') const customEvent = ref("");
const notificationHistory = ref<Array<{ const notificationHistory = ref<
channel: string Array<{
payload: any channel: string;
timestamp: number payload: any;
}>>([]) timestamp: number;
}>
>([]);
const connectionHealthClass = computed(() => { const connectionHealthClass = computed(() => {
switch (connectionState.connectionHealth) { switch (connectionState.connectionHealth) {
case 'excellent': return 'health-excellent' case "excellent":
case 'good': return 'health-good' return "health-excellent";
case 'warning': return 'health-warning' case "good":
case 'poor': return 'health-poor' return "health-good";
default: return 'health-poor' case "warning":
return "health-warning";
case "poor":
return "health-poor";
default:
return "health-poor";
} }
}) });
const connectionHealthText = computed(() => { const connectionHealthText = computed(() => {
switch (connectionState.connectionHealth) { switch (connectionState.connectionHealth) {
case 'excellent': return 'Excellent' case "excellent":
case 'good': return 'Good' return "Excellent";
case 'warning': return 'Warning' case "good":
case 'poor': return 'Poor' return "Good";
default: return 'Unknown' case "warning":
return "Warning";
case "poor":
return "Poor";
default:
return "Unknown";
} }
}) });
const triggerNotification = () => { const triggerNotification = () => {
if (!notificationChannel.value.trim() || !notificationPayload.value.trim()) return if (!notificationChannel.value.trim() || !notificationPayload.value.trim())
return;
try { try {
const payload = JSON.parse(notificationPayload.value) const payload = JSON.parse(notificationPayload.value);
const eventName = triggerEvent.value === 'custom' ? customEvent.value : triggerEvent.value const eventName =
triggerEvent.value === "custom" ? customEvent.value : triggerEvent.value;
triggerNotificationFn(notificationChannel.value.trim(), { triggerNotificationFn(notificationChannel.value.trim());
...payload,
event: eventName,
timestamp: Date.now()
})
// Add to history // Add to history
notificationHistory.value.unshift({ notificationHistory.value.unshift({
channel: notificationChannel.value.trim(), channel: notificationChannel.value.trim(),
payload, payload,
timestamp: Date.now() timestamp: Date.now(),
}) });
// Clear form // Clear form
notificationChannel.value = '' notificationChannel.value = "";
notificationPayload.value = '' notificationPayload.value = "";
} catch (error) { } 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 = () => { const clearMonitoringData = () => {
notificationHistory.value = [] notificationHistory.value = [];
} };
const formatTime = (timestamp: number) => { const formatTime = (timestamp: number) => {
return new Date(timestamp).toLocaleString() return new Date(timestamp).toLocaleString();
} };
const formatUptime = (uptime: number) => { const formatUptime = (uptime: number) => {
const seconds = Math.floor(uptime / 1000) const seconds = Math.floor(uptime / 1000);
const hours = Math.floor(seconds / 3600) const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60) const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60 const secs = seconds % 60;
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}` return `${hours.toString().padStart(2, "0")}:${minutes
} .toString()
.padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
};
// Mock stat changes for demonstration // Mock stat changes for demonstration
const previousStats = ref<any>(null) const previousStats = ref<any>(null);
const getStatChange = (statName: string) => { 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 current = stats.value[statName as keyof typeof stats.value] as number;
const previous = previousStats.value[statName as keyof typeof previousStats.value] const previous =
previousStats.value[statName as keyof typeof previousStats.value];
if (current > previous) return `+${current - previous}` if (current > previous) return `+${current - previous}`;
if (current < previous) return `-${previous - current}` if (current < previous) return `-${previous - current}`;
return '0' return "0";
} };
const getStatChangeClass = (statName: string) => { 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 current = stats.value[statName as keyof typeof stats.value];
const previous = previousStats.value[statName as keyof typeof previousStats.value] const previous =
previousStats.value[statName as keyof typeof previousStats.value];
if (current > previous) return 'stat-increase' if (current > previous) return "stat-increase";
if (current < previous) return 'stat-decrease' if (current < previous) return "stat-decrease";
return 'stat-unchanged' return "stat-unchanged";
} };
// Update previous stats when new stats arrive // Update previous stats when new stats arrive
const updatePreviousStats = () => { const updatePreviousStats = () => {
if (stats.value) { if (stats.value) {
previousStats.value = { ...stats.value } previousStats.value = { ...stats.value };
}
} }
};
// Watch for stats changes // Watch for stats changes
import { watch } from 'vue' import { watch } from "vue";
watch(stats, updatePreviousStats, { deep: true }) watch(stats, updatePreviousStats, { deep: true });
</script> </script>
<style scoped> <style scoped>
@@ -423,7 +451,7 @@ watch(stats, updatePreviousStats, { deep: true })
} }
.btn-warning { .btn-warning {
background: #FFC107; background: #ffc107;
color: #212529; 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 { export interface WebSocketMessage {
type: string type: string;
data: any data: any;
timestamp?: number timestamp?: number;
client_id?: string client_id?: string;
message_id?: string message_id?: string;
} }
export interface ConnectionInfo { export interface ConnectionInfo {
client_id: string client_id: string;
static_id: string static_id: string;
ip_address: string ip_address: string;
room: string room: string;
user_id: string user_id: string;
connected_at: number connected_at: number;
id_type: string id_type: string;
} }
export interface ClientInfo { export interface ClientInfo {
id: string id: string;
static_id: string static_id: string;
ip_address: string ip_address: string;
user_id: string user_id: string;
room: string room: string;
connected_at: number connected_at: number;
last_ping: number last_ping: number;
is_active?: boolean is_active?: boolean;
} }
export interface OnlineUser { export interface OnlineUser {
client_id: string client_id: string;
static_id: string static_id: string;
user_id: string user_id: string;
room: string room: string;
ip_address: string ip_address: string;
connected_at: number connected_at: number;
last_ping: number last_ping: number;
} }
export interface ConnectionStats { export interface ConnectionStats {
connected_clients: number connected_clients: number;
unique_ips: number unique_ips: number;
static_clients: number static_clients: number;
active_rooms: number active_rooms: number;
ip_distribution: Record<string, number> ip_distribution: Record<string, number>;
room_distribution: Record<string, number> room_distribution: Record<string, number>;
message_queue_size: number message_queue_size: number;
queue_workers: number queue_workers: number;
uptime: number uptime: number;
timestamp: number timestamp: number;
} }
export interface SystemHealth { export interface SystemHealth {
databases: any databases: any;
available_dbs: string[] available_dbs: string[];
websocket_status: string websocket_status: string;
uptime_seconds: number uptime_seconds: number;
} }
export interface PerformanceMetrics { export interface PerformanceMetrics {
messages_per_second: number messages_per_second: number;
average_latency_ms: number average_latency_ms: number;
error_rate_percent: number error_rate_percent: number;
memory_usage_bytes: number memory_usage_bytes: number;
} }
export interface MonitoringData { export interface MonitoringData {
stats: ConnectionStats stats: ConnectionStats;
recent_activity: ActivityLog[] recent_activity: ActivityLog[];
system_health: SystemHealth system_health: SystemHealth;
performance: PerformanceMetrics performance: PerformanceMetrics;
} }
export interface ActivityLog { export interface ActivityLog {
timestamp: number timestamp: number;
event: string event: string;
client_id: string client_id: string;
details: string details: string;
} }
export interface MessageHistory { export interface MessageHistory {
timestamp: Date timestamp: Date;
type: string type: string;
data: any data: any;
messageId?: string messageId?: string;
size: number size: number;
icon?: string;
timeString?: string;
} }
export interface ConnectionState { export interface ConnectionState {
isConnected: boolean isConnected: boolean;
isConnecting: boolean isConnecting: boolean;
connectionStatus: 'disconnected' | 'connecting' | 'connected' | 'error' connectionStatus: "disconnected" | "connecting" | "connected" | "error";
clientId: string | null clientId: string | null;
staticId: string | null staticId: string | null;
currentRoom: string | null currentRoom: string | null;
userId: string userId: string;
ipAddress: string | null ipAddress: string | null;
connectionStartTime: number | null connectionStartTime: number | null;
lastPingTime: number | null lastPingTime: number | null;
connectionLatency: number connectionLatency: number;
connectionHealth: 'poor' | 'warning' | 'good' | 'excellent' connectionHealth: "poor" | "warning" | "good" | "excellent";
reconnectAttempts: number reconnectAttempts: number;
messagesReceived: number messagesReceived: number;
messagesSent: number messagesSent: number;
uptime: string uptime: string;
} }
export interface WebSocketConfig { export interface WebSocketConfig {
wsUrl: string wsUrl: string;
userId: string userId: string;
room: string room: string;
staticId?: string staticId?: string;
useIPBasedId?: boolean useIPBasedId?: boolean;
autoReconnect: boolean autoReconnect: boolean;
heartbeatEnabled: boolean heartbeatEnabled: boolean;
maxReconnectAttempts: number maxReconnectAttempts: number;
reconnectDelay: number reconnectDelay: number;
maxReconnectDelay: number maxReconnectDelay: number;
heartbeatInterval: number heartbeatInterval: number;
heartbeatTimeout: number heartbeatTimeout: number;
maxMissedHeartbeats: number maxMissedHeartbeats: number;
maxMessages: number maxMessages: number;
messageWarningThreshold: number messageWarningThreshold: number;
actionThrottle: number actionThrottle: number;
} }
export type MessageType = export type MessageType =
| 'welcome' | "welcome"
| 'broadcast' | "broadcast"
| 'direct_message' | "direct_message"
| 'room_message' | "room_message"
| 'ping' | "ping"
| 'pong' | "pong"
| 'heartbeat' | "heartbeat"
| 'heartbeat_ack' | "heartbeat_ack"
| 'connection_test' | "connection_test"
| 'connection_test_result' | "connection_test_result"
| 'get_online_users' | "get_online_users"
| 'online_users' | "online_users"
| 'get_server_info' | "get_server_info"
| 'server_info' | "server_info"
| 'error' | "error"
| 'message_received' | "message_received"
| 'broadcast_sent' | "broadcast_sent"
| 'direct_message_sent' | "direct_message_sent"
| 'room_message_sent' | "room_message_sent"
| 'db_insert' | "db_insert"
| 'db_query' | "db_query"
| 'db_custom_query' | "db_custom_query"
| 'query_result' | "query_result"
| 'admin_kick_client' | "admin_kick_client"
| 'admin_kill_server' | "admin_kill_server"
| 'get_server_stats' | "get_server_stats"
| 'get_system_health' | "get_system_health"
| 'admin_clear_logs' | "admin_clear_logs"
| 'get_stats' | "get_stats"
| 'get_room_info' | "get_room_info"
| 'join_room' | "join_room"
| 'leave_room' | "leave_room"
| 'database_change' | "database_change"
| 'data_stream' | "data_stream"
| 'server_heartbeat' | "server_heartbeat"
| 'system_status' | "system_status"
| 'clients_by_ip' | "clients_by_ip"
| 'client_info' | "client_info"
| 'get_clients_by_ip' | "get_clients_by_ip"
| 'get_client_info' | "get_client_info"
| 'health_check' | "health_check"
| 'database_list' | "database_list"
| 'connection_stats' | "connection_stats"
| 'trigger_notification' | "trigger_notification"
| 'notification_sent' | "notification_sent"
| 'API_TEST' | "API_TEST"
| 'manual_test' | "manual_test"
| 'retribusi_created' | "retribusi_created"
| 'retribusi_updated' | "retribusi_updated"
| 'retribusi_deleted' | "retribusi_deleted"
| 'peserta_changes' | "peserta_changes"
| 'retribusi_changes' | "retribusi_changes"
| 'system_changes' | "system_changes";