first commit

This commit is contained in:
2025-09-24 18:42:16 +07:00
commit daffbc67dc
72 changed files with 40710 additions and 0 deletions

24
examples/clientsocket/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example

View File

@@ -0,0 +1,75 @@
# Nuxt Minimal Starter
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup
Make sure to install dependencies:
```bash
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# bun
bun install
```
## Development Server
Start the development server on `http://localhost:3000`:
```bash
# npm
npm run dev
# pnpm
pnpm dev
# yarn
yarn dev
# bun
bun run dev
```
## Production
Build the application for production:
```bash
# npm
npm run build
# pnpm
pnpm build
# yarn
yarn build
# bun
bun run build
```
Locally preview production build:
```bash
# npm
npm run preview
# pnpm
pnpm preview
# yarn
yarn preview
# bun
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.

View File

@@ -0,0 +1,6 @@
<template>
<div>
<NuxtRouteAnnouncer />
<NuxtWelcome />
</div>
</template>

View File

@@ -0,0 +1,672 @@
/* Enhanced WebSocket Client Styles */
* {
box-sizing: border-box;
}
body {
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
color: #333;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
padding: 24px;
overflow: hidden;
}
h1 {
color: #333;
text-align: center;
margin-bottom: 32px;
font-weight: 600;
}
.status-bar {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 12px;
margin-bottom: 24px;
}
.status-item {
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
}
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
display: inline-block;
}
.status-connected {
background: #4CAF50;
box-shadow: 0 0 8px rgba(76, 175, 80, 0.6);
}
.status-connecting {
background: #FFC107;
animation: pulse 1.5s infinite;
}
.status-disconnected {
background: #F44336;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
.health-indicator {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 6px;
font-size: 12px;
font-weight: bold;
}
.health-excellent {
background: #d4edda;
color: #155724;
}
.health-good {
background: #d1ecf1;
color: #0c5460;
}
.health-warning {
background: #fff3cd;
color: #856404;
}
.health-poor {
background: #f8d7da;
color: #721c24;
}
.tabs {
display: flex;
border-bottom: 2px solid #e9ecef;
margin-bottom: 24px;
overflow-x: auto;
background: white;
border-radius: 8px 8px 0 0;
}
.tab {
padding: 16px 24px;
background: transparent;
border: none;
border-bottom: 3px solid transparent;
cursor: pointer;
white-space: nowrap;
transition: all 0.3s ease;
font-weight: 500;
color: #666;
}
.tab:hover {
background: #f8f9fa;
color: #333;
}
.tab.active {
border-bottom-color: #1976d2;
color: #1976d2;
font-weight: 600;
background: #f8f9fa;
}
.tab-content {
display: none;
animation: fadeIn 0.3s ease;
}
.tab-content.active {
display: block;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 24px;
padding: 24px;
background: #f8f9fa;
border-radius: 8px;
}
.input-group {
display: flex;
flex-direction: column;
gap: 12px;
}
.input-group label {
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.input-group input,
.input-group select,
.input-group textarea {
padding: 12px 16px;
border: 2px solid #e9ecef;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s ease;
}
.input-group input:focus,
.input-group select:focus,
.input-group textarea:focus {
outline: none;
border-color: #1976d2;
box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.1);
}
.checkbox-group {
display: flex;
align-items: center;
gap: 8px;
margin: 8px 0;
}
.checkbox-group input[type="checkbox"] {
margin: 0;
width: 18px;
height: 18px;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.3s ease;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.btn-primary {
background: #1976d2;
color: white;
}
.btn-primary:hover {
background: #1565c0;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(25, 118, 210, 0.3);
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(108, 117, 125, 0.3);
}
.btn-warning {
background: #FFC107;
color: #212529;
}
.btn-warning:hover {
background: #e0a800;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(255, 193, 7, 0.3);
}
.btn-info {
background: #17a2b8;
color: white;
}
.btn-info:hover {
background: #138496;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(23, 162, 184, 0.3);
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-danger:hover {
background: #c82333;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(220, 53, 69, 0.3);
}
.btn-success {
background: #28a745;
color: white;
}
.btn-success:hover {
background: #218838;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(40, 167, 69, 0.3);
}
.messages-container {
height: 500px;
overflow-y: auto;
border: 2px solid #e9ecef;
border-radius: 8px;
padding: 20px;
background: #fafafa;
font-family: "SF Mono", "Monaco", "Inconsolata", "Roboto Mono", monospace;
font-size: 13px;
line-height: 1.5;
}
.messages-container::-webkit-scrollbar {
width: 8px;
}
.messages-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.messages-container::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}
.messages-container::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
.message {
margin-bottom: 16px;
padding: 16px;
border-radius: 8px;
border-left: 4px solid #1976d2;
background: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from { opacity: 0; transform: translateX(-20px); }
to { opacity: 1; transform: translateX(0); }
}
.message.error {
border-left-color: #dc3545;
background: #fff5f5;
}
.message.warning {
border-left-color: #FFC107;
background: #fffbf0;
}
.message.info {
border-left-color: #17a2b8;
background: #f0f9ff;
}
.message.success {
border-left-color: #28a745;
background: #f0fff4;
}
.message-time {
font-size: 11px;
color: #666;
margin-bottom: 8px;
font-weight: 500;
}
.message-type {
font-weight: 600;
color: #333;
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin: 24px 0;
}
.stat-card {
background: white;
padding: 24px;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
text-align: center;
border: 1px solid #e9ecef;
}
.stat-number {
font-size: 2.5em;
font-weight: 700;
color: #1976d2;
margin-bottom: 8px;
}
.stat-label {
color: #666;
font-size: 14px;
font-weight: 500;
}
.online-users {
max-height: 400px;
overflow-y: auto;
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
border: 1px solid #e9ecef;
}
.online-users::-webkit-scrollbar {
width: 6px;
}
.online-users::-webkit-scrollbar-track {
background: #f1f1f1;
}
.online-users::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
.user-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
margin: 8px 0;
background: white;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
border: 1px solid #e9ecef;
}
.user-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.user-id {
font-weight: 600;
color: #333;
}
.user-details {
font-size: 12px;
color: #666;
}
.loading-indicator {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid #f3f3f3;
border-top: 2px solid #1976d2;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-left: 8px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.message-limit-warning {
background: #fff3cd;
border: 1px solid #ffeaa7;
padding: 16px;
margin: 16px 0;
border-radius: 6px;
font-size: 14px;
color: #856404;
display: flex;
align-items: center;
gap: 8px;
}
.admin-controls {
background: #fff3cd;
padding: 20px;
border-radius: 8px;
margin-bottom: 24px;
border: 1px solid #ffeaa7;
}
.admin-warning {
color: #856404;
font-weight: 600;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
}
pre {
background: #f8f9fa;
padding: 12px;
border-radius: 6px;
overflow-x: auto;
font-size: 12px;
border: 1px solid #e9ecef;
margin: 8px 0;
}
code {
background: #f8f9fa;
padding: 2px 6px;
border-radius: 3px;
font-family: "SF Mono", "Monaco", "Inconsolata", "Roboto Mono", monospace;
font-size: 13px;
}
.message pre {
white-space: pre-wrap;
word-break: break-word;
}
.message code {
display: block;
margin: 8px 0;
}
/* Responsive Design */
@media (max-width: 768px) {
.container {
margin: 10px;
padding: 16px;
border-radius: 8px;
}
.controls {
grid-template-columns: 1fr;
padding: 16px;
}
.status-bar {
grid-template-columns: 1fr;
padding: 16px;
}
.stats-grid {
grid-template-columns: 1fr;
}
.tabs {
flex-wrap: wrap;
}
.tab {
padding: 12px 16px;
font-size: 14px;
}
.messages-container {
height: 300px;
font-size: 12px;
}
.message {
padding: 12px;
font-size: 13px;
}
.btn {
padding: 10px 16px;
font-size: 13px;
}
}
@media (max-width: 480px) {
.container {
padding: 12px;
}
.controls {
padding: 12px;
}
.status-bar {
padding: 12px;
}
.messages-container {
height: 250px;
padding: 12px;
}
.tab {
padding: 8px 12px;
font-size: 12px;
}
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
body {
background-color: #121212;
color: #e0e0e0;
}
.container {
background: #1e1e1e;
border: 1px solid #333;
}
.controls {
background: #2a2a2a;
border: 1px solid #404040;
}
.message {
background: #2a2a2a;
border: 1px solid #404040;
}
.stat-card {
background: #2a2a2a;
border: 1px solid #404040;
}
.online-users {
background: #2a2a2a;
border: 1px solid #404040;
}
.user-item {
background: #333;
border: 1px solid #404040;
}
.messages-container {
background: #1a1a1a;
border-color: #404040;
}
pre {
background: #2a2a2a;
border-color: #404040;
}
code {
background: #333;
}
}
/* Accessibility improvements */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* Focus styles for keyboard navigation */
.btn:focus,
.tab:focus,
.input-group input:focus,
.input-group select:focus,
.input-group textarea:focus {
outline: 2px solid #1976d2;
outline-offset: 2px;
}
/* High contrast mode support */
@media (prefers-contrast: high) {
.btn {
border: 2px solid currentColor;
}
.message {
border: 2px solid currentColor;
}
.stat-card {
border: 2px solid currentColor;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,367 @@
<template>
<div class="container">
<h1>WebSocket Client</h1>
<!-- Status Bar -->
<div class="status-bar">
<div class="status-item">
<div
class="status-indicator"
:class="connectionStatusClass"
></div>
<span>{{ connectionStatusText }}</span>
</div>
<div class="status-item">
<span>Client ID:</span>
<strong>{{ connectionState?.clientId || 'Not connected' }}</strong>
</div>
<div class="status-item">
<span>Room:</span>
<strong>{{ connectionState?.currentRoom || 'None' }}</strong>
</div>
<div class="status-item">
<span>Messages:</span>
<strong>{{ messages?.length || 0 }}/{{ config?.maxMessages || 1000 }}</strong>
</div>
<div class="status-item">
<span>Uptime:</span>
<strong>{{ connectionState?.uptime || '00:00:00' }}</strong>
</div>
<div class="status-item">
<span>Health:</span>
<div
class="health-indicator"
:class="connectionHealthClass"
>
{{ connectionHealthText }}
</div>
</div>
</div>
<!-- Message Limit Warning -->
<div v-if="shouldShowMessageWarning" class="message-limit-warning">
<v-icon>mdi-alert</v-icon>
Message limit approaching ({{ messages?.length || 0 }}/{{ config?.maxMessages || 1000 }})
</div>
<!-- Tabs -->
<div class="tabs">
<button
class="tab"
:class="{ active: activeTab === 'connection' }"
@click="activeTab = 'connection'"
>
Connection
</button>
<button
class="tab"
:class="{ active: activeTab === 'messaging' }"
@click="activeTab = 'messaging'"
>
Messaging
</button>
<button
class="tab"
:class="{ active: activeTab === 'database' }"
@click="activeTab = 'database'"
>
Database
</button>
<button
class="tab"
:class="{ active: activeTab === 'monitoring' }"
@click="activeTab = 'monitoring'"
>
Monitoring
</button>
<button
class="tab"
:class="{ active: activeTab === 'admin' }"
@click="activeTab = 'admin'"
>
Admin
</button>
</div>
<!-- Tab Content -->
<div class="tab-content" :class="{ active: activeTab === 'connection' }">
<ConnectionTab />
</div>
<div class="tab-content" :class="{ active: activeTab === 'messaging' }">
<MessagingTab />
</div>
<div class="tab-content" :class="{ active: activeTab === 'database' }">
<DatabaseTab />
</div>
<div class="tab-content" :class="{ active: activeTab === 'monitoring' }">
<MonitoringTab />
</div>
<div class="tab-content" :class="{ active: activeTab === 'admin' }">
<AdminTab />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useWebSocket } from '~/composables/useWebSocket'
import ConnectionTab from './tabs/ConnectionTab.vue'
import MessagingTab from './tabs/MessagingTab.vue'
import DatabaseTab from './tabs/DatabaseTab.vue'
import MonitoringTab from './tabs/MonitoringTab.vue'
import AdminTab from './tabs/AdminTab.vue'
const activeTab = ref('connection')
const {
isConnected,
connectionStatus,
connectionState,
config,
messages,
stats,
onlineUsers,
activityLog,
connect,
disconnect,
cleanup,
isMessageLimitReached,
shouldShowMessageWarning,
connectionHealthColor,
connectionHealthText
} = useWebSocket()
const connectionStatusClass = computed(() => {
switch (connectionStatus.value) {
case 'connected': return 'status-connected'
case 'connecting': return 'status-connecting'
case 'disconnected': return 'status-disconnected'
default: return 'status-disconnected'
}
})
const connectionStatusText = computed(() => {
switch (connectionStatus.value) {
case 'connected': return 'Connected'
case 'connecting': return 'Connecting...'
case 'disconnected': return 'Disconnected'
case 'error': return 'Error'
default: return 'Unknown'
}
})
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'
}
})
onMounted(() => {
// Auto-connect on mount
connect()
})
onUnmounted(() => {
cleanup()
})
</script>
<style scoped>
.container {
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
padding: 24px;
overflow: hidden;
}
h1 {
color: #333;
text-align: center;
margin-bottom: 32px;
font-weight: 600;
}
.status-bar {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 12px;
margin-bottom: 24px;
}
.status-item {
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
}
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
display: inline-block;
}
.status-connected {
background: #4CAF50;
box-shadow: 0 0 8px rgba(76, 175, 80, 0.6);
}
.status-connecting {
background: #FFC107;
animation: pulse 1.5s infinite;
}
.status-disconnected {
background: #F44336;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
.health-indicator {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 6px;
font-size: 12px;
font-weight: bold;
}
.health-excellent {
background: #d4edda;
color: #155724;
}
.health-good {
background: #d1ecf1;
color: #0c5460;
}
.health-warning {
background: #fff3cd;
color: #856404;
}
.health-poor {
background: #f8d7da;
color: #721c24;
}
.tabs {
display: flex;
border-bottom: 2px solid #e9ecef;
margin-bottom: 24px;
overflow-x: auto;
background: white;
border-radius: 8px 8px 0 0;
}
.tab {
padding: 16px 24px;
background: transparent;
border: none;
border-bottom: 3px solid transparent;
cursor: pointer;
white-space: nowrap;
transition: all 0.3s ease;
font-weight: 500;
color: #666;
}
.tab:hover {
background: #f8f9fa;
color: #333;
}
.tab.active {
border-bottom-color: #1976d2;
color: #1976d2;
font-weight: 600;
background: #f8f9fa;
}
.tab-content {
display: none;
animation: fadeIn 0.3s ease;
}
.tab-content.active {
display: block;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.message-limit-warning {
background: #fff3cd;
border: 1px solid #ffeaa7;
padding: 16px;
margin: 16px 0;
border-radius: 6px;
font-size: 14px;
color: #856404;
display: flex;
align-items: center;
gap: 8px;
}
/* Responsive Design */
@media (max-width: 768px) {
.container {
margin: 10px;
padding: 16px;
border-radius: 8px;
}
.status-bar {
grid-template-columns: 1fr;
padding: 16px;
}
.tabs {
flex-wrap: wrap;
}
.tab {
padding: 12px 16px;
font-size: 14px;
}
}
@media (max-width: 480px) {
.container {
padding: 12px;
}
.status-bar {
padding: 12px;
}
.tab {
padding: 8px 12px;
font-size: 12px;
}
}
</style>

View File

@@ -0,0 +1,665 @@
<template>
<div class="admin-tab">
<div class="controls">
<div class="input-group">
<label for="adminCommand">Admin Command</label>
<select id="adminCommand" v-model="adminCommand">
<option value="restart_server">Restart Server</option>
<option value="shutdown_server">Shutdown Server</option>
<option value="clear_cache">Clear Cache</option>
<option value="reload_config">Reload Configuration</option>
<option value="backup_database">Backup Database</option>
<option value="restore_database">Restore Database</option>
<option value="export_logs">Export Logs</option>
<option value="clear_logs">Clear Logs</option>
<option value="update_permissions">Update Permissions</option>
<option value="custom">Custom Command</option>
</select>
</div>
<div class="input-group">
<label for="commandParams">Command Parameters (JSON)</label>
<textarea
id="commandParams"
v-model="commandParams"
placeholder='{"target": "all", "force": false}'
rows="3"
></textarea>
</div>
<div class="input-group">
<label for="customCommand">Custom Command</label>
<input
id="customCommand"
v-model="customCommand"
type="text"
placeholder="custom_admin_command"
:disabled="adminCommand !== 'custom'"
/>
</div>
<div class="input-group">
<label for="confirmationCode">Confirmation Code</label>
<input
id="confirmationCode"
v-model="confirmationCode"
type="text"
placeholder="Enter confirmation code"
/>
</div>
</div>
<div class="controls">
<button
class="btn btn-danger"
:disabled="!isConnected || !confirmationCode.trim()"
@click="executeAdminCommand"
>
Execute Command
</button>
<button
class="btn btn-secondary"
:disabled="!isConnected"
@click="getServerInfo"
>
Get Server Info
</button>
<button
class="btn btn-secondary"
:disabled="!isConnected"
@click="getSystemHealth"
>
Get System Health
</button>
<button
class="btn btn-warning"
@click="clearCommandHistory"
>
Clear History
</button>
</div>
<!-- Server Information -->
<div class="server-info-section" v-if="serverInfo">
<h3>Server Information</h3>
<div class="info-grid">
<div class="info-item">
<div class="info-label">Server Version</div>
<div class="info-value">{{ serverInfo.version }}</div>
</div>
<div class="info-item">
<div class="info-label">Environment</div>
<div class="info-value">{{ serverInfo.environment }}</div>
</div>
<div class="info-item">
<div class="info-label">Node Version</div>
<div class="info-value">{{ serverInfo.nodeVersion }}</div>
</div>
<div class="info-item">
<div class="info-label">Platform</div>
<div class="info-value">{{ serverInfo.platform }}</div>
</div>
<div class="info-item">
<div class="info-label">Architecture</div>
<div class="info-value">{{ serverInfo.architecture }}</div>
</div>
<div class="info-item">
<div class="info-label">CPU Cores</div>
<div class="info-value">{{ serverInfo.cpuCores }}</div>
</div>
<div class="info-item">
<div class="info-label">Memory Usage</div>
<div class="info-value">{{ formatBytes(serverInfo.memoryUsage) }}</div>
</div>
<div class="info-item">
<div class="info-label">Uptime</div>
<div class="info-value">{{ formatUptime(serverInfo.uptime) }}</div>
</div>
</div>
</div>
<!-- System Health -->
<div class="system-health-section" v-if="systemHealth">
<h3>System Health</h3>
<div class="health-grid">
<div class="health-item">
<div class="health-label">CPU Usage</div>
<div class="health-value">{{ systemHealth.cpuUsage }}%</div>
<div class="health-bar">
<div
class="health-bar-fill"
:style="{ width: systemHealth.cpuUsage + '%' }"
:class="getCpuUsageClass(systemHealth.cpuUsage)"
></div>
</div>
</div>
<div class="health-item">
<div class="health-label">Memory Usage</div>
<div class="health-value">{{ systemHealth.memoryUsage }}%</div>
<div class="health-bar">
<div
class="health-bar-fill"
:style="{ width: systemHealth.memoryUsage + '%' }"
:class="getMemoryUsageClass(systemHealth.memoryUsage)"
></div>
</div>
</div>
<div class="health-item">
<div class="health-label">Disk Usage</div>
<div class="health-value">{{ systemHealth.diskUsage }}%</div>
<div class="health-bar">
<div
class="health-bar-fill"
:style="{ width: systemHealth.diskUsage + '%' }"
:class="getDiskUsageClass(systemHealth.diskUsage)"
></div>
</div>
</div>
<div class="health-item">
<div class="health-label">Network I/O</div>
<div class="health-value">{{ formatBytes(systemHealth.networkRx) }} / {{ formatBytes(systemHealth.networkTx) }}</div>
</div>
<div class="health-item">
<div class="health-label">Active Connections</div>
<div class="health-value">{{ systemHealth.activeConnections }}</div>
</div>
<div class="health-item">
<div class="health-label">Error Rate</div>
<div class="health-value">{{ systemHealth.errorRate }}%</div>
</div>
</div>
</div>
<!-- Command History -->
<div class="command-history-section">
<h3>Command History</h3>
<div class="command-history">
<div
v-for="(command, index) in commandHistory.slice(0, 10)"
:key="index"
class="command-item"
>
<div class="command-header">
<div class="command-type">{{ command.command }}</div>
<div class="command-time">{{ formatTime(command.timestamp) }}</div>
<div class="command-status" :class="command.success ? 'success' : 'error'">
{{ command.success ? 'Success' : 'Error' }}
</div>
</div>
<div class="command-details">
<div v-if="command.params" class="command-params">
<strong>Parameters:</strong> {{ command.params }}
</div>
<div v-if="command.result" class="command-result">
<strong>Result:</strong>
<pre>{{ JSON.stringify(command.result, null, 2) }}</pre>
</div>
<div v-if="command.error" class="command-error">
<strong>Error:</strong> {{ command.error }}
</div>
</div>
</div>
<div v-if="commandHistory.length === 0" class="no-commands">
No commands executed yet
</div>
</div>
</div>
<!-- Security Notice -->
<div class="security-notice">
<h4> Security Notice</h4>
<p>Admin commands can have serious consequences. Please ensure you have proper authorization and understand the impact of each command before execution.</p>
<ul>
<li>Server restart will disconnect all clients</li>
<li>Database operations are irreversible</li>
<li>Always backup before destructive operations</li>
<li>Some commands require elevated permissions</li>
</ul>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useWebSocket } from '../../composables/useWebSocket'
const {
isConnected,
serverInfo,
systemHealth,
executeAdminCommand: executeAdminCommandFn,
getServerInfo,
getSystemHealth
} = useWebSocket()
const adminCommand = ref('restart_server')
const commandParams = ref('')
const customCommand = ref('')
const confirmationCode = ref('')
const commandHistory = ref<Array<{
command: string
params: string
result: any
error: string
success: boolean
timestamp: number
}>>([])
const executeAdminCommand = async () => {
if (!isConnected.value || !confirmationCode.value.trim()) return
let command = adminCommand.value
let params = {}
try {
if (adminCommand.value === 'custom') {
command = customCommand.value.trim()
if (!command) {
alert('Please enter a custom command')
return
}
}
if (commandParams.value.trim()) {
params = JSON.parse(commandParams.value)
}
const result = await executeAdminCommandFn(command, {
...params,
confirmationCode: confirmationCode.value.trim(),
timestamp: Date.now()
})
// Add to history
commandHistory.value.unshift({
command,
params: JSON.stringify(params),
result: result,
error: '',
success: true,
timestamp: Date.now()
})
// Clear form
confirmationCode.value = ''
commandParams.value = ''
} catch (error) {
// Add error to history
commandHistory.value.unshift({
command,
params: commandParams.value,
result: null,
error: error instanceof Error ? error.message : String(error),
success: false,
timestamp: Date.now()
})
}
}
const clearCommandHistory = () => {
commandHistory.value = []
}
const formatTime = (timestamp: number) => {
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 formatBytes = (bytes: number) => {
if (bytes === 0) return '0 Bytes'
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
const getCpuUsageClass = (usage: number) => {
if (usage < 50) return 'usage-low'
if (usage < 80) return 'usage-medium'
return 'usage-high'
}
const getMemoryUsageClass = (usage: number) => {
if (usage < 60) return 'usage-low'
if (usage < 85) return 'usage-medium'
return 'usage-high'
}
const getDiskUsageClass = (usage: number) => {
if (usage < 70) return 'usage-low'
if (usage < 90) return 'usage-medium'
return 'usage-high'
}
</script>
<style scoped>
.admin-tab {
padding: 20px 0;
}
.controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 24px;
padding: 24px;
background: #f8f9fa;
border-radius: 8px;
}
.input-group {
display: flex;
flex-direction: column;
gap: 12px;
}
.input-group label {
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.input-group input,
.input-group select,
.input-group textarea {
padding: 12px 16px;
border: 2px solid #e9ecef;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s ease;
}
.input-group input:focus,
.input-group select:focus,
.input-group textarea:focus {
outline: none;
border-color: #1976d2;
box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.1);
}
.input-group input:disabled {
background: #e9ecef;
cursor: not-allowed;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.3s ease;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-danger:hover:not(:disabled) {
background: #c82333;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(220, 53, 69, 0.3);
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(108, 117, 125, 0.3);
}
.btn-warning {
background: #FFC107;
color: #212529;
}
.btn-warning:hover {
background: #e0a800;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(255, 193, 7, 0.3);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.server-info-section,
.system-health-section {
margin: 32px 0;
}
.server-info-section h3,
.system-health-section h3 {
margin-bottom: 16px;
color: #333;
font-size: 18px;
}
.info-grid,
.health-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.info-item,
.health-item {
background: white;
padding: 20px;
border-radius: 8px;
border: 1px solid #e9ecef;
text-align: center;
}
.info-label,
.health-label {
color: #666;
font-size: 14px;
margin-bottom: 8px;
}
.info-value,
.health-value {
color: #333;
font-size: 24px;
font-weight: 600;
margin-bottom: 8px;
}
.health-bar {
width: 100%;
height: 8px;
background: #e9ecef;
border-radius: 4px;
overflow: hidden;
}
.health-bar-fill {
height: 100%;
transition: width 0.3s ease;
}
.usage-low {
background: #28a745;
}
.usage-medium {
background: #ffc107;
}
.usage-high {
background: #dc3545;
}
.command-history-section {
margin-top: 32px;
}
.command-history-section h3 {
margin-bottom: 16px;
color: #333;
font-size: 18px;
}
.command-history {
max-height: 300px;
overflow-y: auto;
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
border: 1px solid #e9ecef;
}
.command-item {
margin: 16px 0;
background: white;
border-radius: 8px;
border: 1px solid #e9ecef;
overflow: hidden;
}
.command-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
}
.command-type {
font-weight: 600;
color: #1976d2;
text-transform: uppercase;
font-size: 12px;
}
.command-time {
color: #666;
font-size: 12px;
}
.command-status {
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
}
.command-status.success {
background: #d4edda;
color: #155724;
}
.command-status.error {
background: #f8d7da;
color: #721c24;
}
.command-details {
padding: 16px;
}
.command-params,
.command-result,
.command-error {
margin: 12px 0;
font-size: 13px;
}
.command-params strong,
.command-result strong,
.command-error strong {
color: #333;
display: block;
margin-bottom: 4px;
}
.command-result pre {
background: #f8f9fa;
padding: 12px;
border-radius: 4px;
overflow-x: auto;
font-size: 12px;
color: #333;
}
.command-error {
color: #dc3545;
background: #f8d7da;
padding: 12px;
border-radius: 4px;
}
.no-commands {
text-align: center;
color: #666;
font-style: italic;
padding: 40px;
}
.security-notice {
margin-top: 32px;
padding: 20px;
background: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 8px;
border-left: 4px solid #ffc107;
}
.security-notice h4 {
margin: 0 0 12px 0;
color: #856404;
font-size: 16px;
}
.security-notice p {
margin: 0 0 12px 0;
color: #856404;
font-size: 14px;
}
.security-notice ul {
margin: 0;
padding-left: 20px;
color: #856404;
}
.security-notice li {
margin: 4px 0;
font-size: 13px;
}
/* Responsive Design */
@media (max-width: 768px) {
.controls {
grid-template-columns: 1fr;
padding: 16px;
}
.info-grid,
.health-grid {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,397 @@
<template>
<div class="connection-tab">
<div class="controls">
<div class="input-group">
<label for="wsUrl">WebSocket URL</label>
<input
id="wsUrl"
v-model="config.wsUrl"
type="text"
placeholder="ws://localhost:8080/api/v1/ws"
/>
</div>
<div class="input-group">
<label for="userId">User ID</label>
<input
id="userId"
v-model="config.userId"
type="text"
placeholder="anonymous"
/>
</div>
<div class="input-group">
<label for="room">Room</label>
<input
id="room"
v-model="config.room"
type="text"
placeholder="default"
/>
</div>
<div class="input-group">
<label for="staticId">Static ID (Optional)</label>
<input
id="staticId"
v-model="config.staticId"
type="text"
placeholder="Leave empty for dynamic ID"
/>
</div>
</div>
<div class="controls">
<div class="input-group">
<label class="checkbox-group">
<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" />
Auto Reconnect
</label>
</div>
<div class="input-group">
<label class="checkbox-group">
<input v-model="config.heartbeatEnabled" type="checkbox" />
Enable Heartbeat
</label>
</div>
</div>
<div class="controls">
<button
class="btn btn-primary"
:class="{ 'btn-disabled': isConnecting }"
:disabled="isConnecting"
@click="connect"
>
<span v-if="isConnecting" class="loading-indicator"></span>
{{ isConnected ? "Reconnect" : "Connect" }}
</button>
<button
class="btn btn-danger"
:disabled="!isConnected"
@click="disconnect"
>
Disconnect
</button>
<button
class="btn btn-info"
:disabled="!isConnected"
@click="testConnection"
>
Test Connection
</button>
<button class="btn btn-secondary" @click="clearMessages">
Clear Messages
</button>
</div>
<div class="controls">
<div class="input-group">
<label for="maxReconnectAttempts">Max Reconnect Attempts</label>
<input
id="maxReconnectAttempts"
v-model.number="config.maxReconnectAttempts"
type="number"
min="1"
max="50"
/>
</div>
<div class="input-group">
<label for="reconnectDelay">Reconnect Delay (ms)</label>
<input
id="reconnectDelay"
v-model.number="config.reconnectDelay"
type="number"
min="100"
max="10000"
/>
</div>
<div class="input-group">
<label for="heartbeatInterval">Heartbeat Interval (ms)</label>
<input
id="heartbeatInterval"
v-model.number="config.heartbeatInterval"
type="number"
min="1000"
max="60000"
/>
</div>
<div class="input-group">
<label for="maxMessages">Max Messages</label>
<input
id="maxMessages"
v-model.number="config.maxMessages"
type="number"
min="100"
max="5000"
/>
</div>
</div>
<div class="connection-info" v-if="isConnected">
<h3>Connection Information</h3>
<div class="info-grid">
<div class="info-item">
<strong>Client ID:</strong>
<span>{{ connectionState.clientId }}</span>
</div>
<div class="info-item">
<strong>Static ID:</strong>
<span>{{ connectionState.staticId || "N/A" }}</span>
</div>
<div class="info-item">
<strong>IP Address:</strong>
<span>{{ connectionState.ipAddress || "N/A" }}</span>
</div>
<div class="info-item">
<strong>User ID:</strong>
<span>{{ connectionState.userId }}</span>
</div>
<div class="info-item">
<strong>Room:</strong>
<span>{{ connectionState.currentRoom }}</span>
</div>
<div class="info-item">
<strong>Connected At:</strong>
<span>{{
new Date(connectionState.connectionStartTime || 0).toLocaleString()
}}</span>
</div>
<div class="info-item">
<strong>Latency:</strong>
<span>{{ connectionState.connectionLatency }}ms</span>
</div>
<div class="info-item">
<strong>Reconnect Attempts:</strong>
<span>{{ connectionState.reconnectAttempts }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from "vue";
import { useWebSocket } from "../../composables/useWebSocket";
const {
isConnected,
isConnecting,
connectionState,
config,
connect,
disconnect,
testConnection,
clearMessages,
} = useWebSocket();
</script>
<style scoped>
.connection-tab {
padding: 20px 0;
}
.controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 24px;
padding: 24px;
background: #f8f9fa;
border-radius: 8px;
}
.input-group {
display: flex;
flex-direction: column;
gap: 12px;
}
.input-group label {
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.input-group input {
padding: 12px 16px;
border: 2px solid #e9ecef;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s ease;
}
.input-group input:focus {
outline: none;
border-color: #1976d2;
box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.1);
}
.checkbox-group {
display: flex;
align-items: center;
gap: 8px;
margin: 8px 0;
}
.checkbox-group input[type="checkbox"] {
margin: 0;
width: 18px;
height: 18px;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.3s ease;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.btn-primary {
background: #1976d2;
color: white;
}
.btn-primary:hover:not(.btn-disabled) {
background: #1565c0;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(25, 118, 210, 0.3);
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-danger:hover:not(:disabled) {
background: #c82333;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(220, 53, 69, 0.3);
}
.btn-info {
background: #17a2b8;
color: white;
}
.btn-info:hover:not(:disabled) {
background: #138496;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(23, 162, 184, 0.3);
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(108, 117, 125, 0.3);
}
.btn-disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.loading-indicator {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid #f3f3f3;
border-top: 2px solid #1976d2;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 8px;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.connection-info {
margin-top: 32px;
padding: 24px;
background: white;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.connection-info h3 {
margin-top: 0;
margin-bottom: 20px;
color: #333;
font-size: 18px;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 16px;
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: #f8f9fa;
border-radius: 6px;
border: 1px solid #e9ecef;
}
.info-item strong {
color: #333;
font-weight: 600;
}
.info-item span {
color: #666;
font-family: monospace;
font-size: 13px;
}
/* Responsive Design */
@media (max-width: 768px) {
.controls {
grid-template-columns: 1fr;
padding: 16px;
}
.info-grid {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,536 @@
<template>
<div class="database-tab">
<div class="controls">
<div class="input-group">
<label for="queryType">Query Type</label>
<select id="queryType" v-model="queryType">
<option value="select">SELECT</option>
<option value="insert">INSERT</option>
<option value="update">UPDATE</option>
<option value="delete">DELETE</option>
<option value="create_table">CREATE TABLE</option>
<option value="drop_table">DROP TABLE</option>
<option value="show_tables">SHOW TABLES</option>
<option value="describe_table">DESCRIBE TABLE</option>
</select>
</div>
<div class="input-group">
<label for="tableName">Table Name</label>
<input
id="tableName"
v-model="tableName"
type="text"
placeholder="Enter table name"
/>
</div>
<div class="input-group">
<label for="queryParams">Query Parameters (JSON)</label>
<textarea
id="queryParams"
v-model="queryParams"
placeholder='{"column": "value", "column2": "value2"}'
rows="3"
></textarea>
</div>
<div class="input-group">
<label for="customQuery">Custom SQL Query</label>
<textarea
id="customQuery"
v-model="customQuery"
placeholder="Enter custom SQL query"
rows="4"
></textarea>
</div>
</div>
<div class="controls">
<button
class="btn btn-primary"
:disabled="!isConnected || (!tableName.trim() && !customQuery.trim())"
@click="executeQuery"
>
Execute Query
</button>
<button
class="btn btn-secondary"
:disabled="!isConnected"
@click="getStats"
>
Get Database Stats
</button>
<button
class="btn btn-secondary"
:disabled="!isConnected"
@click="getMonitoringData"
>
Get Monitoring Data
</button>
<button class="btn btn-secondary" @click="clearQueryHistory">
Clear Query History
</button>
</div>
<!-- Query History -->
<div class="query-history-section">
<h3>Query History</h3>
<div class="query-history">
<div
v-for="(query, index) in queryHistory.slice(0, 10)"
:key="index"
class="query-item"
>
<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>
</div>
<div class="query-details">
<div class="query-sql">{{ query.sql }}</div>
<div v-if="query.params" class="query-params">
<strong>Parameters:</strong> {{ query.params }}
</div>
<div v-if="query.result" class="query-result">
<strong>Result:</strong>
<pre>{{ JSON.stringify(query.result, null, 2) }}</pre>
</div>
<div v-if="query.error" class="query-error">
<strong>Error:</strong> {{ query.error }}
</div>
</div>
</div>
<div v-if="queryHistory.length === 0" class="no-queries">
No queries executed yet
</div>
</div>
</div>
<!-- Database Stats -->
<div class="database-stats-section" v-if="stats">
<h3>Database Statistics</h3>
<div class="stats-grid">
<div class="stat-item">
<div class="stat-label">Connected Clients</div>
<div class="stat-value">{{ stats.connected_clients }}</div>
</div>
<div class="stat-item">
<div class="stat-label">Unique IPs</div>
<div class="stat-value">{{ stats.unique_ips }}</div>
</div>
<div class="stat-item">
<div class="stat-label">Static Clients</div>
<div class="stat-value">{{ stats.static_clients }}</div>
</div>
<div class="stat-item">
<div class="stat-label">Active Rooms</div>
<div class="stat-value">{{ stats.active_rooms }}</div>
</div>
<div class="stat-item">
<div class="stat-label">Message Queue Size</div>
<div class="stat-value">{{ stats.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>
<div class="stat-item">
<div class="stat-label">Uptime</div>
<div class="stat-value">{{ formatUptime(stats.uptime) }}</div>
</div>
<div class="stat-item">
<div class="stat-label">Last Updated</div>
<div class="stat-value">{{ formatTime(stats.timestamp) }}</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { useWebSocket } from "../../composables/useWebSocket";
const {
isConnected,
stats,
executeDatabaseQuery,
getStats,
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 executeQuery = async () => {
if (!isConnected.value) return;
let sql = "";
let params = {};
try {
if (customQuery.value.trim()) {
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;
}
}
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: await executeDatabaseQuery({
sql,
params,
table: tableName.value,
}),
error: "",
success: true,
timestamp: Date.now(),
});
} catch (error) {
// Add error to history
queryHistory.value.unshift({
type: queryType.value,
sql: customQuery.value || sql,
params: queryParams.value,
result: null,
error: error instanceof Error ? error.message : String(error),
success: false,
timestamp: Date.now(),
});
}
};
const clearQueryHistory = () => {
queryHistory.value = [];
};
const formatTime = (timestamp: number) => {
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")}`;
};
</script>
<style scoped>
.database-tab {
padding: 20px 0;
}
.controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 24px;
padding: 24px;
background: #f8f9fa;
border-radius: 8px;
}
.input-group {
display: flex;
flex-direction: column;
gap: 12px;
}
.input-group label {
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.input-group input,
.input-group select,
.input-group textarea {
padding: 12px 16px;
border: 2px solid #e9ecef;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s ease;
}
.input-group input:focus,
.input-group select:focus,
.input-group textarea:focus {
outline: none;
border-color: #1976d2;
box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.1);
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.3s ease;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.btn-primary {
background: #1976d2;
color: white;
}
.btn-primary:hover:not(:disabled) {
background: #1565c0;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(25, 118, 210, 0.3);
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(108, 117, 125, 0.3);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.query-history-section {
margin: 32px 0;
}
.query-history-section h3 {
margin-bottom: 16px;
color: #333;
font-size: 18px;
}
.query-history {
max-height: 400px;
overflow-y: auto;
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
border: 1px solid #e9ecef;
}
.query-item {
margin: 16px 0;
background: white;
border-radius: 8px;
border: 1px solid #e9ecef;
overflow: hidden;
}
.query-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
}
.query-type {
font-weight: 600;
color: #1976d2;
text-transform: uppercase;
font-size: 12px;
}
.query-time {
color: #666;
font-size: 12px;
}
.query-status {
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
}
.query-status.success {
background: #d4edda;
color: #155724;
}
.query-status.error {
background: #f8d7da;
color: #721c24;
}
.query-details {
padding: 16px;
}
.query-sql {
font-family: monospace;
background: #f8f9fa;
padding: 12px;
border-radius: 4px;
margin-bottom: 12px;
font-size: 13px;
color: #333;
}
.query-params,
.query-result,
.query-error {
margin: 12px 0;
font-size: 13px;
}
.query-params strong,
.query-result strong,
.query-error strong {
color: #333;
display: block;
margin-bottom: 4px;
}
.query-result pre {
background: #f8f9fa;
padding: 12px;
border-radius: 4px;
overflow-x: auto;
font-size: 12px;
color: #333;
}
.query-error {
color: #dc3545;
background: #f8d7da;
padding: 12px;
border-radius: 4px;
}
.no-queries {
text-align: center;
color: #666;
font-style: italic;
padding: 40px;
}
.database-stats-section {
margin-top: 32px;
}
.database-stats-section h3 {
margin-bottom: 16px;
color: #333;
font-size: 18px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.stat-item {
background: white;
padding: 20px;
border-radius: 8px;
border: 1px solid #e9ecef;
text-align: center;
}
.stat-label {
color: #666;
font-size: 14px;
margin-bottom: 8px;
}
.stat-value {
color: #333;
font-size: 24px;
font-weight: 600;
}
/* Responsive Design */
@media (max-width: 768px) {
.controls {
grid-template-columns: 1fr;
padding: 16px;
}
.query-header {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.stats-grid {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,602 @@
<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="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
>
<input
id="targetClientId"
v-model="targetClientId"
type="text"
placeholder="Client ID for direct message"
/>
</div>
<div class="input-group">
<label for="roomMessage">Room Message</label>
<textarea
id="roomMessage"
v-model="roomMessage"
placeholder="Enter message to send to specific room"
rows="3"
></textarea>
</div>
<div class="input-group">
<label for="targetRoom">Target Room</label>
<input
id="targetRoom"
v-model="targetRoom"
type="text"
placeholder="Room name"
/>
</div>
</div>
<div class="controls">
<button
class="btn btn-primary"
:disabled="!isConnected || !messageText.trim()"
@click="handleSendBroadcast"
>
Send Broadcast
</button>
<button
class="btn btn-info"
:disabled="
!isConnected || !targetClientId.trim() || !messageText.trim()
"
@click="handleSendDirectMessage"
>
Send Direct Message
</button>
<button
class="btn btn-success"
:disabled="!isConnected || !targetRoom.trim() || !roomMessage.trim()"
@click="handleSendRoomMessage"
>
Send Room Message
</button>
<button
class="btn btn-warning"
:disabled="!isConnected"
@click="sendHeartbeat"
>
Send Heartbeat
</button>
</div>
<div class="controls">
<button
class="btn btn-secondary"
:disabled="!isConnected"
@click="getOnlineUsers"
>
Get Online Users
</button>
<button
class="btn btn-secondary"
:disabled="!isConnected"
@click="getServerInfo"
>
Get Server Info
</button>
<button class="btn btn-secondary" @click="clearMessages">
Clear Messages
</button>
<button class="btn btn-secondary" @click="clearActivityLog">
Clear Activity Log
</button>
</div>
<!-- Online Users -->
<div class="online-users-section" v-if="onlineUsers.length > 0">
<h3>Online Users ({{ onlineUsers.length }})</h3>
<div class="online-users">
<div
v-for="user in onlineUsers"
:key="user.client_id"
class="user-item"
:class="{
'current-user': user.client_id === connectionState.clientId,
}"
>
<div class="user-info">
<div class="user-id">{{ user.client_id }}</div>
<div class="user-details">
<span>Room: {{ user.room }}</span>
<span>IP: {{ user.ip_address }}</span>
<span>Connected: {{ formatTime(user.connected_at) }}</span>
</div>
</div>
<div class="user-actions">
<button
class="btn btn-sm btn-info"
@click="setTargetClient(user.client_id)"
>
Direct Message
</button>
</div>
</div>
</div>
</div>
<!-- Activity Log -->
<div class="activity-log-section">
<h3>Activity Log</h3>
<div class="activity-log">
<div
v-for="(activity, index) in activityLog.slice(0, 20)"
:key="index"
class="activity-item"
>
<div class="activity-time">{{ formatTime(activity.timestamp) }}</div>
<div class="activity-event">{{ activity.event }}</div>
<div class="activity-details">{{ activity.details }}</div>
</div>
<div v-if="activityLog.length === 0" class="no-activity">
No activity logged yet
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { useWebSocket } from "../../composables/useWebSocket";
const {
isConnected,
connectionState,
onlineUsers,
activityLog,
broadcastMessage,
sendDirectMessage,
sendRoomMessage,
sendHeartbeat,
getOnlineUsers,
getServerInfo,
clearMessages,
clearActivityLog,
connect,
} = useWebSocket();
const messageText = ref("");
const targetClientId = ref("");
const roomMessage = ref("");
const targetRoom = ref("");
const handleSendBroadcast = () => {
if (messageText.value.trim()) {
broadcastMessage(messageText.value.trim());
messageText.value = "";
}
};
const handleSendDirectMessage = () => {
if (targetClientId.value.trim() && messageText.value.trim()) {
sendDirectMessage(targetClientId.value.trim(), messageText.value.trim());
messageText.value = "";
}
};
const handleSendRoomMessage = () => {
if (targetRoom.value.trim() && roomMessage.value.trim()) {
sendRoomMessage(targetRoom.value.trim(), roomMessage.value.trim());
roomMessage.value = "";
}
};
const setTargetClient = (clientId: string) => {
targetClientId.value = clientId;
};
const formatTime = (timestamp: number) => {
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>
.messaging-tab {
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));
gap: 20px;
margin-bottom: 24px;
padding: 24px;
background: #f8f9fa;
border-radius: 8px;
}
.input-group {
display: flex;
flex-direction: column;
gap: 12px;
}
.input-group label {
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.input-group input,
.input-group textarea {
padding: 12px 16px;
border: 2px solid #e9ecef;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s ease;
}
.input-group input:focus,
.input-group textarea:focus {
outline: none;
border-color: #1976d2;
box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.1);
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.3s ease;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.btn-primary {
background: #1976d2;
color: white;
}
.btn-primary:hover:not(:disabled) {
background: #1565c0;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(25, 118, 210, 0.3);
}
.btn-info {
background: #17a2b8;
color: white;
}
.btn-info:hover:not(:disabled) {
background: #138496;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(23, 162, 184, 0.3);
}
.btn-success {
background: #28a745;
color: white;
}
.btn-success:hover:not(:disabled) {
background: #218838;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(40, 167, 69, 0.3);
}
.btn-warning {
background: #ffc107;
color: #212529;
}
.btn-warning:hover:not(:disabled) {
background: #e0a800;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(255, 193, 7, 0.3);
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(108, 117, 125, 0.3);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-sm {
padding: 8px 16px;
font-size: 12px;
}
.online-users-section {
margin: 32px 0;
}
.online-users-section h3 {
margin-bottom: 16px;
color: #333;
font-size: 18px;
}
.online-users {
max-height: 400px;
overflow-y: auto;
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
border: 1px solid #e9ecef;
}
.user-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
margin: 8px 0;
background: white;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
border: 1px solid #e9ecef;
}
.user-item.current-user {
border-left: 4px solid #1976d2;
background: #f0f8ff;
}
.user-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.user-id {
font-weight: 600;
color: #333;
}
.user-details {
font-size: 12px;
color: #666;
display: flex;
gap: 12px;
}
.user-actions {
display: flex;
gap: 8px;
}
.activity-log-section {
margin-top: 32px;
}
.activity-log-section h3 {
margin-bottom: 16px;
color: #333;
font-size: 18px;
}
.activity-log {
max-height: 300px;
overflow-y: auto;
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
border: 1px solid #e9ecef;
}
.activity-item {
display: grid;
grid-template-columns: 150px 120px 1fr;
gap: 16px;
padding: 12px 16px;
margin: 8px 0;
background: white;
border-radius: 6px;
border: 1px solid #e9ecef;
font-size: 13px;
}
.activity-time {
color: #666;
font-family: monospace;
}
.activity-event {
font-weight: 600;
color: #333;
}
.activity-details {
color: #666;
}
.no-activity {
text-align: center;
color: #666;
font-style: italic;
padding: 40px;
}
/* Responsive Design */
@media (max-width: 768px) {
.controls {
grid-template-columns: 1fr;
padding: 16px;
}
.activity-item {
grid-template-columns: 1fr;
gap: 8px;
}
.user-item {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.user-actions {
width: 100%;
justify-content: flex-end;
}
}
</style>

View File

@@ -0,0 +1,637 @@
<template>
<div class="monitoring-tab">
<div class="controls">
<div class="input-group">
<label for="notificationChannel">Notification Channel</label>
<input
id="notificationChannel"
v-model="notificationChannel"
type="text"
placeholder="Enter notification channel"
/>
</div>
<div class="input-group">
<label for="notificationPayload">Notification Payload (JSON)</label>
<textarea
id="notificationPayload"
v-model="notificationPayload"
placeholder='{"message": "Hello", "type": "info"}'
rows="3"
></textarea>
</div>
<div class="input-group">
<label for="triggerEvent">Trigger Event</label>
<select id="triggerEvent" v-model="triggerEvent">
<option value="user_joined">User Joined</option>
<option value="user_left">User Left</option>
<option value="message_sent">Message Sent</option>
<option value="connection_lost">Connection Lost</option>
<option value="server_restart">Server Restart</option>
<option value="custom">Custom Event</option>
</select>
</div>
<div class="input-group">
<label for="customEvent">Custom Event Name</label>
<input
id="customEvent"
v-model="customEvent"
type="text"
placeholder="custom_event_name"
:disabled="triggerEvent !== 'custom'"
/>
</div>
</div>
<div class="controls">
<button
class="btn btn-primary"
:disabled="
!isConnected ||
!notificationChannel.trim() ||
!notificationPayload.trim()
"
@click="triggerNotification"
>
Trigger Notification
</button>
<button class="btn btn-info" :disabled="!isConnected" @click="getStats">
Refresh Stats
</button>
<button
class="btn btn-secondary"
:disabled="!isConnected"
@click="getMonitoringData"
>
Get Monitoring Data
</button>
<button class="btn btn-warning" @click="clearMonitoringData">
Clear Data
</button>
</div>
<!-- Connection Health -->
<div class="health-section">
<h3>Connection Health</h3>
<div class="health-grid">
<div class="health-item">
<div class="health-label">Status</div>
<div class="health-value" :class="connectionHealthClass">
{{ connectionHealthText }}
</div>
</div>
<div class="health-item">
<div class="health-label">Latency</div>
<div class="health-value">
{{ connectionState.connectionLatency }}ms
</div>
</div>
<div class="health-item">
<div class="health-label">Messages Sent</div>
<div class="health-value">{{ connectionState.messagesSent }}</div>
</div>
<div class="health-item">
<div class="health-label">Messages Received</div>
<div class="health-value">{{ connectionState.messagesReceived }}</div>
</div>
<div class="health-item">
<div class="health-label">Reconnect Attempts</div>
<div class="health-value">
{{ connectionState.reconnectAttempts }}
</div>
</div>
<div class="health-item">
<div class="health-label">Connection Time</div>
<div class="health-value">{{ connectionState.uptime }}</div>
</div>
</div>
</div>
<!-- Server Statistics -->
<div class="stats-section" v-if="stats">
<h3>Server Statistics</h3>
<div class="stats-grid">
<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>
</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") }}
</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>
</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") }}
</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>
</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") }}
</div>
</div>
<div class="stat-item">
<div class="stat-label">Server Uptime</div>
<div class="stat-value">{{ formatUptime(stats.uptime) }}</div>
</div>
<div class="stat-item">
<div class="stat-label">Last Updated</div>
<div class="stat-value">{{ formatTime(stats.timestamp) }}</div>
</div>
</div>
</div>
<!-- Monitoring Data -->
<div class="monitoring-data-section" v-if="monitoringData">
<h3>Monitoring Data</h3>
<div class="monitoring-content">
<pre>{{ JSON.stringify(monitoringData, null, 2) }}</pre>
</div>
</div>
<!-- Notification History -->
<div class="notification-history-section">
<h3>Notification History</h3>
<div class="notification-history">
<div
v-for="(notification, index) in notificationHistory.slice(0, 10)"
:key="index"
class="notification-item"
>
<div class="notification-header">
<div class="notification-channel">{{ notification.channel }}</div>
<div class="notification-time">
{{ formatTime(notification.timestamp) }}
</div>
</div>
<div class="notification-payload">
<pre>{{ JSON.stringify(notification.payload, null, 2) }}</pre>
</div>
</div>
<div v-if="notificationHistory.length === 0" class="no-notifications">
No notifications sent yet
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from "vue";
import { useWebSocket } from "../../composables/useWebSocket";
const {
isConnected,
connectionState,
stats,
monitoringData,
triggerNotification: triggerNotificationFn,
getStats,
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 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";
}
});
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";
}
});
const triggerNotification = () => {
if (!notificationChannel.value.trim() || !notificationPayload.value.trim())
return;
try {
const payload = JSON.parse(notificationPayload.value);
const eventName =
triggerEvent.value === "custom" ? customEvent.value : triggerEvent.value;
triggerNotificationFn(notificationChannel.value.trim());
// Add to history
notificationHistory.value.unshift({
channel: notificationChannel.value.trim(),
payload,
timestamp: Date.now(),
});
// Clear form
notificationChannel.value = "";
notificationPayload.value = "";
} catch (error) {
alert(
"Invalid JSON payload: " +
(error instanceof Error ? error.message : String(error))
);
}
};
const clearMonitoringData = () => {
notificationHistory.value = [];
};
const formatTime = (timestamp: number) => {
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")}`;
};
// Mock stat changes for demonstration
const previousStats = ref<any>(null);
const getStatChange = (statName: string) => {
if (!previousStats.value || !stats.value) return "";
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";
};
const getStatChangeClass = (statName: string) => {
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];
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 };
}
};
// Watch for stats changes
import { watch } from "vue";
watch(stats, updatePreviousStats, { deep: true });
</script>
<style scoped>
.monitoring-tab {
padding: 20px 0;
}
.controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 24px;
padding: 24px;
background: #f8f9fa;
border-radius: 8px;
}
.input-group {
display: flex;
flex-direction: column;
gap: 12px;
}
.input-group label {
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.input-group input,
.input-group select,
.input-group textarea {
padding: 12px 16px;
border: 2px solid #e9ecef;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s ease;
}
.input-group input:focus,
.input-group select:focus,
.input-group textarea:focus {
outline: none;
border-color: #1976d2;
box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.1);
}
.input-group input:disabled {
background: #e9ecef;
cursor: not-allowed;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.3s ease;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.btn-primary {
background: #1976d2;
color: white;
}
.btn-primary:hover:not(:disabled) {
background: #1565c0;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(25, 118, 210, 0.3);
}
.btn-info {
background: #17a2b8;
color: white;
}
.btn-info:hover:not(:disabled) {
background: #138496;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(23, 162, 184, 0.3);
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(108, 117, 125, 0.3);
}
.btn-warning {
background: #ffc107;
color: #212529;
}
.btn-warning:hover {
background: #e0a800;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(255, 193, 7, 0.3);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.health-section,
.stats-section {
margin: 32px 0;
}
.health-section h3,
.stats-section h3 {
margin-bottom: 16px;
color: #333;
font-size: 18px;
}
.health-grid,
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.health-item,
.stat-item {
background: white;
padding: 20px;
border-radius: 8px;
border: 1px solid #e9ecef;
text-align: center;
}
.health-label,
.stat-label {
color: #666;
font-size: 14px;
margin-bottom: 8px;
}
.health-value {
color: #333;
font-size: 24px;
font-weight: 600;
}
.stat-value {
color: #333;
font-size: 24px;
font-weight: 600;
margin-bottom: 4px;
}
.stat-change {
font-size: 12px;
font-weight: 600;
}
.stat-increase {
color: #28a745;
}
.stat-decrease {
color: #dc3545;
}
.stat-unchanged {
color: #6c757d;
}
.monitoring-data-section {
margin: 32px 0;
}
.monitoring-data-section h3 {
margin-bottom: 16px;
color: #333;
font-size: 18px;
}
.monitoring-content {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 20px;
overflow-x: auto;
}
.monitoring-content pre {
margin: 0;
font-size: 12px;
color: #333;
}
.notification-history-section {
margin-top: 32px;
}
.notification-history-section h3 {
margin-bottom: 16px;
color: #333;
font-size: 18px;
}
.notification-history {
max-height: 300px;
overflow-y: auto;
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
border: 1px solid #e9ecef;
}
.notification-item {
margin: 16px 0;
background: white;
border-radius: 8px;
border: 1px solid #e9ecef;
overflow: hidden;
}
.notification-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
}
.notification-channel {
font-weight: 600;
color: #1976d2;
}
.notification-time {
color: #666;
font-size: 12px;
}
.notification-payload {
padding: 16px;
}
.notification-payload pre {
background: #f8f9fa;
padding: 12px;
border-radius: 4px;
overflow-x: auto;
font-size: 12px;
color: #333;
margin: 0;
}
.no-notifications {
text-align: center;
color: #666;
font-style: italic;
padding: 40px;
}
/* Responsive Design */
@media (max-width: 768px) {
.controls {
grid-template-columns: 1fr;
padding: 16px;
}
.health-grid,
.stats-grid {
grid-template-columns: 1fr;
}
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,318 @@
import { ref, computed, reactive, nextTick } from 'vue'
import type {
WebSocketMessage,
ConnectionState,
WebSocketConfig,
MessageHistory,
ConnectionStats,
MonitoringData,
ClientInfo,
OnlineUser,
ActivityLog
} from '../types/websocket'
export const useWebSocket = () => {
// Check if we're in browser environment
const isBrowser = process.client
const ws = ref<WebSocket | null>(null)
const isConnected = ref(false)
const isConnecting = ref(false)
const connectionStatus = ref<'disconnected' | 'connecting' | 'connected' | 'error'>('disconnected')
const connectionState = reactive<ConnectionState>({
isConnected: false,
isConnecting: false,
connectionStatus: 'disconnected',
clientId: null,
staticId: null,
currentRoom: null,
userId: 'anonymous',
ipAddress: null,
connectionStartTime: null,
lastPingTime: null,
connectionLatency: 0,
connectionHealth: 'poor',
reconnectAttempts: 0,
messagesReceived: 0,
messagesSent: 0,
uptime: '00:00:00'
})
const config = reactive<WebSocketConfig>({
wsUrl: 'ws://localhost:8080/api/v1/ws',
userId: 'anonymous',
room: 'default',
staticId: '',
useIPBasedId: false,
autoReconnect: true,
heartbeatEnabled: true,
maxReconnectAttempts: 10,
reconnectDelay: 1000,
maxReconnectDelay: 30000,
heartbeatInterval: 30000,
heartbeatTimeout: 5000,
maxMissedHeartbeats: 3,
maxMessages: 1000,
messageWarningThreshold: 800,
actionThrottle: 100
})
const messages = ref<MessageHistory[]>([])
const stats = ref<ConnectionStats | null>(null)
const monitoringData = ref<MonitoringData | null>(null)
const onlineUsers = ref<OnlineUser[]>([])
const activityLog = ref<ActivityLog[]>([])
let reconnectTimeout: number | null = null
let heartbeatInterval: number | null = null
let heartbeatTimeout: number | null = null
let missedHeartbeats = 0
let lastHeartbeatTime = 0
let messageCount = 0
// Only run WebSocket logic in browser
if (isBrowser) {
// WebSocket connection logic here
}
const addMessage = (type: string, data: any, messageId?: string) => {
if (!isBrowser) return
const message: MessageHistory = {
timestamp: new Date(),
type,
data,
messageId,
size: JSON.stringify(data).length
}
messages.value.unshift(message)
messageCount++
// Keep only the last maxMessages
if (messages.value.length > config.maxMessages) {
messages.value = messages.value.slice(0, config.maxMessages)
}
// Update connection state
connectionState.messagesReceived++
}
const connectionHealthColor = computed(() => {
switch (connectionState.connectionHealth) {
case 'excellent': return '#4CAF50'
case 'good': return '#2196F3'
case 'warning': return '#FFC107'
case 'poor': return '#F44336'
default: return '#9E9E9E'
}
})
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'
}
})
// Admin functionality
const serverInfo = ref<any>(null)
const systemHealth = ref<any>(null)
const executeAdminCommand = async (command: string, params: any) => {
if (!isBrowser || !ws.value) throw new Error('Not connected')
const message = {
type: 'admin_command',
command,
params,
timestamp: Date.now()
}
ws.value.send(JSON.stringify(message))
return { success: true, message: 'Command sent successfully' }
}
const getServerInfo = async () => {
if (!isBrowser || !ws.value) throw new Error('Not connected')
const message = {
type: 'get_server_info',
timestamp: Date.now()
}
ws.value.send(JSON.stringify(message))
}
const getSystemHealth = async () => {
if (!isBrowser || !ws.value) throw new Error('Not connected')
const message = {
type: 'get_system_health',
timestamp: Date.now()
}
ws.value.send(JSON.stringify(message))
}
// Cleanup on unmount
const cleanup = () => {
if (!isBrowser) return
disconnect()
if (reconnectTimeout) {
clearTimeout(reconnectTimeout)
}
stopHeartbeat()
}
// WebSocket connection methods (only available in browser)
const connect = () => {
if (!isBrowser) return
// WebSocket connection logic
}
const disconnect = () => {
if (!isBrowser) return
// WebSocket disconnection logic
}
const sendMessage = (message: any) => {
if (!isBrowser || !ws.value) return
// Send message logic
}
const broadcastMessage = (message: string) => {
if (!isBrowser || !ws.value) return
// Broadcast message logic
}
const sendDirectMessage = (clientId: string, message: string) => {
if (!isBrowser || !ws.value) return
// Direct message logic
}
const sendRoomMessage = (room: string, message: string) => {
if (!isBrowser || !ws.value) return
// Room message logic
}
const getOnlineUsers = () => {
if (!isBrowser || !ws.value) return
// Get online users logic
}
const testConnection = () => {
if (!isBrowser || !ws.value) return
// Test connection logic
}
const sendHeartbeat = () => {
if (!isBrowser || !ws.value) return
// Send heartbeat logic
}
const executeDatabaseQuery = async (query: string) => {
if (!isBrowser || !ws.value) return
// Database query logic
}
const triggerNotification = async (message: string) => {
if (!isBrowser || !ws.value) return
// Notification logic
}
const getStats = () => {
if (!isBrowser || !ws.value) return
// Get stats logic
}
const getMonitoringData = () => {
if (!isBrowser || !ws.value) return
// Get monitoring data logic
}
const clearMessages = () => {
if (!isBrowser) return
messages.value = []
messageCount = 0
}
const clearActivityLog = () => {
if (!isBrowser) return
activityLog.value = []
}
const getMessagesByType = (type: string) => {
return messages.value.filter(msg => msg.type === type)
}
const getRecentMessages = (count: number = 10) => {
return messages.value.slice(0, count)
}
const stopHeartbeat = () => {
if (!isBrowser) return
// Stop heartbeat logic
}
const isMessageLimitReached = computed(() => {
return messages.value.length >= config.maxMessages
})
const shouldShowMessageWarning = computed(() => {
return messages.value.length >= config.messageWarningThreshold
})
return {
// State
ws,
isConnected,
isConnecting,
connectionStatus,
connectionState,
config,
messages,
stats,
monitoringData,
onlineUsers,
activityLog,
// Admin state
serverInfo,
systemHealth,
// Methods
connect,
disconnect,
sendMessage,
broadcastMessage,
sendDirectMessage,
sendRoomMessage,
getServerInfo,
getOnlineUsers,
testConnection,
sendHeartbeat,
executeDatabaseQuery,
triggerNotification,
getStats,
getMonitoringData,
executeAdminCommand,
getSystemHealth,
clearMessages,
clearActivityLog,
getMessagesByType,
getRecentMessages,
cleanup,
// Computed
isMessageLimitReached,
shouldShowMessageWarning,
connectionHealthColor,
connectionHealthText
}
}

View File

@@ -0,0 +1,22 @@
export default defineNuxtConfig({
devtools: { enabled: true },
modules: [],
css: ['~/assets/css/main.css', 'vuetify/styles'],
build: {
transpile: ['vuetify']
},
runtimeConfig: {
public: {
wsUrl: 'ws://localhost:8080/api/v1/ws'
}
},
typescript: {
typeCheck: false
},
vite: {
define: {
global: 'globalThis'
}
},
compatibilityDate: '2024-04-03'
})

11504
examples/clientsocket/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,27 @@
{
"name": "nuxt3-websocket-client",
"private": true,
"type": "module",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"devDependencies": {
"@nuxt/devtools": "latest",
"nuxt": "^3.8.0",
"vue": "^3.3.8",
"vue-router": "^4.2.5",
"vue-tsc": "^3.0.8"
},
"dependencies": {
"@mdi/font": "^7.3.67",
"@nuxtjs/vuetify": "^1.12.3",
"highlight.js": "^11.9.0",
"pinia": "^2.1.7",
"vue3-highlightjs": "^1.0.5",
"vuetify": "^3.4.0"
}
}

View File

@@ -0,0 +1,14 @@
<template>
<div>
<WebSocketClient />
</div>
</template>
<script setup lang="ts">
// Main page for the WebSocket client application
// This page serves as the entry point and displays the WebSocket client interface
</script>
<style scoped>
/* Additional page-specific styles if needed */
</style>

View File

@@ -0,0 +1,15 @@
import { createVuetify } from 'vuetify'
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'
export default defineNuxtPlugin((nuxtApp) => {
const vuetify = createVuetify({
components,
directives,
theme: {
defaultTheme: 'light'
}
})
nuxtApp.vueApp.use(vuetify)
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,2 @@
User-Agent: *
Disallow:

View File

@@ -0,0 +1,3 @@
{
"extends": "./.nuxt/tsconfig.json"
}

View File

@@ -0,0 +1,182 @@
export interface WebSocketMessage {
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;
}
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;
}
export interface OnlineUser {
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;
}
export interface SystemHealth {
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;
}
export interface MonitoringData {
stats: ConnectionStats;
recent_activity: ActivityLog[];
system_health: SystemHealth;
performance: PerformanceMetrics;
}
export interface ActivityLog {
timestamp: number;
event: string;
client_id: string;
details: string;
}
export interface MessageHistory {
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;
}
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;
}
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";