2252 lines
64 KiB
HTML
2252 lines
64 KiB
HTML
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<title>WebSocket Test Client</title>
|
||
<style>
|
||
body {
|
||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||
margin: 20px;
|
||
background-color: #f5f5f5;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
background: white;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||
padding: 20px;
|
||
}
|
||
|
||
h1 {
|
||
color: #333;
|
||
text-align: center;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.controls {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||
gap: 15px;
|
||
margin-bottom: 20px;
|
||
padding: 20px;
|
||
background: #f8f9fa;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.input-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.input-group label {
|
||
font-weight: bold;
|
||
color: #333;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.input-group input,
|
||
.input-group select,
|
||
.input-group textarea {
|
||
padding: 8px 12px;
|
||
border: 1px solid #ccc;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.input-group input:focus,
|
||
.input-group select:focus,
|
||
.input-group textarea:focus {
|
||
outline: none;
|
||
border-color: #007bff;
|
||
box-shadow: 0 0 5px rgba(0, 123, 255, 0.3);
|
||
}
|
||
|
||
.checkbox-group {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin: 5px 0;
|
||
}
|
||
|
||
.checkbox-group input[type="checkbox"] {
|
||
margin: 0;
|
||
width: auto;
|
||
}
|
||
|
||
button {
|
||
padding: 10px 15px;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
font-weight: bold;
|
||
transition: background-color 0.2s;
|
||
margin: 2px;
|
||
}
|
||
|
||
.btn-primary {
|
||
background: #007bff;
|
||
color: white;
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
background: #0056b3;
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: #6c757d;
|
||
color: white;
|
||
}
|
||
|
||
.btn-secondary:hover {
|
||
background: #545b62;
|
||
}
|
||
|
||
.btn-warning {
|
||
background: #ffc107;
|
||
color: #212529;
|
||
}
|
||
|
||
.btn-warning:hover {
|
||
background: #e0a800;
|
||
}
|
||
|
||
.btn-info {
|
||
background: #17a2b8;
|
||
color: white;
|
||
}
|
||
|
||
.btn-info:hover {
|
||
background: #138496;
|
||
}
|
||
|
||
.btn-danger {
|
||
background: #dc3545;
|
||
color: white;
|
||
}
|
||
|
||
.btn-danger:hover {
|
||
background: #c82333;
|
||
}
|
||
|
||
.status-bar {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 15px;
|
||
padding: 15px;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
border-radius: 8px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.status-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.status-indicator {
|
||
width: 12px;
|
||
height: 12px;
|
||
border-radius: 50%;
|
||
display: inline-block;
|
||
}
|
||
|
||
.status-connected {
|
||
background: #28a745;
|
||
box-shadow: 0 0 8px rgba(40, 167, 69, 0.6);
|
||
}
|
||
|
||
.status-connecting {
|
||
background: #ffc107;
|
||
animation: pulse 1.5s infinite;
|
||
}
|
||
|
||
.status-disconnected {
|
||
background: #dc3545;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0% {
|
||
opacity: 1;
|
||
}
|
||
50% {
|
||
opacity: 0.5;
|
||
}
|
||
100% {
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
.tabs {
|
||
display: flex;
|
||
border-bottom: 2px solid #e9ecef;
|
||
margin-bottom: 20px;
|
||
overflow-x: auto;
|
||
}
|
||
|
||
.tab {
|
||
padding: 12px 20px;
|
||
background: transparent;
|
||
border: none;
|
||
border-bottom: 3px solid transparent;
|
||
cursor: pointer;
|
||
white-space: nowrap;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.tab:hover {
|
||
background: #f8f9fa;
|
||
}
|
||
|
||
.tab.active {
|
||
border-bottom-color: #007bff;
|
||
color: #007bff;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.tab-content {
|
||
display: none;
|
||
}
|
||
|
||
.tab-content.active {
|
||
display: block;
|
||
}
|
||
|
||
.messages-container {
|
||
height: 400px;
|
||
overflow-y: auto;
|
||
border: 1px solid #ddd;
|
||
border-radius: 8px;
|
||
padding: 15px;
|
||
background: #fafafa;
|
||
font-family: "Courier New", monospace;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.message {
|
||
margin-bottom: 10px;
|
||
padding: 8px 12px;
|
||
border-radius: 4px;
|
||
border-left: 4px solid #007bff;
|
||
background: white;
|
||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.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: 10px;
|
||
color: #666;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.message-type {
|
||
font-weight: bold;
|
||
color: #333;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.stats-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||
gap: 15px;
|
||
margin: 20px 0;
|
||
}
|
||
|
||
.stat-card {
|
||
background: white;
|
||
padding: 20px;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||
text-align: center;
|
||
}
|
||
|
||
.stat-number {
|
||
font-size: 2em;
|
||
font-weight: bold;
|
||
color: #007bff;
|
||
}
|
||
|
||
.stat-label {
|
||
color: #666;
|
||
margin-top: 5px;
|
||
}
|
||
|
||
.online-users {
|
||
max-height: 300px;
|
||
overflow-y: auto;
|
||
background: #f8f9fa;
|
||
border-radius: 8px;
|
||
padding: 15px;
|
||
}
|
||
|
||
.user-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 8px 12px;
|
||
margin: 5px 0;
|
||
background: white;
|
||
border-radius: 4px;
|
||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.user-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.user-id {
|
||
font-weight: bold;
|
||
}
|
||
|
||
.user-details {
|
||
font-size: 11px;
|
||
color: #666;
|
||
}
|
||
|
||
.loading-indicator {
|
||
display: inline-block;
|
||
width: 16px;
|
||
height: 16px;
|
||
border: 2px solid #f3f3f3;
|
||
border-top: 2px solid #007bff;
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
margin-left: 5px;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% {
|
||
transform: rotate(0deg);
|
||
}
|
||
100% {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
|
||
.health-indicator {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 8px 12px;
|
||
border-radius: 4px;
|
||
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;
|
||
}
|
||
|
||
.message-limit-warning {
|
||
background: #fff3cd;
|
||
border: 1px solid #ffeaa7;
|
||
padding: 10px;
|
||
margin: 10px 0;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.admin-controls {
|
||
background: #fff3cd;
|
||
padding: 15px;
|
||
border-radius: 8px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.admin-warning {
|
||
color: #856404;
|
||
font-weight: bold;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
pre {
|
||
background: #f8f9fa;
|
||
padding: 10px;
|
||
border-radius: 4px;
|
||
overflow-x: auto;
|
||
font-size: 11px;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.container {
|
||
margin: 10px;
|
||
padding: 15px;
|
||
}
|
||
|
||
.controls {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.status-bar {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.stats-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1>🌐 WebSocket Test Client - Enhanced & Optimized</h1>
|
||
|
||
<!-- Enhanced Status Bar -->
|
||
<div class="status-bar">
|
||
<div class="status-item">
|
||
<span
|
||
class="status-indicator status-disconnected"
|
||
id="statusIndicator"
|
||
></span>
|
||
<strong>Status:</strong>
|
||
<span id="connectionStatus">Disconnected</span>
|
||
<span id="loadingIndicator"></span>
|
||
</div>
|
||
<div class="status-item">
|
||
<strong>Health:</strong>
|
||
<div class="health-indicator health-poor" id="healthIndicator">
|
||
<span>🔴</span> Poor
|
||
</div>
|
||
</div>
|
||
<div class="status-item">
|
||
<strong>Uptime:</strong> <span id="connectionUptime">0s</span>
|
||
</div>
|
||
<div class="status-item">
|
||
<strong>Client ID:</strong>
|
||
<span id="currentClientId">Not connected</span>
|
||
</div>
|
||
<div class="status-item">
|
||
<strong>Reconnect:</strong> <span id="reconnectInfo">Not needed</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Message Limit Warning -->
|
||
<div
|
||
class="message-limit-warning"
|
||
id="messageLimitWarning"
|
||
style="display: none"
|
||
>
|
||
⚠️ Message history approaching limit. Older messages will be
|
||
automatically cleared.
|
||
</div>
|
||
|
||
<!-- Navigation Tabs -->
|
||
<div class="tabs">
|
||
<button class="tab active" onclick="switchTab('connection')">
|
||
🔗 Connection
|
||
</button>
|
||
<button class="tab" onclick="switchTab('messaging')">
|
||
💬 Messaging
|
||
</button>
|
||
<button class="tab" onclick="switchTab('database')">🗄️ Database</button>
|
||
<button class="tab" onclick="switchTab('monitoring')">
|
||
📊 Monitoring
|
||
</button>
|
||
<button class="tab" onclick="switchTab('admin')">⚙️ Admin</button>
|
||
</div>
|
||
|
||
<!-- Connection Tab - Enhanced -->
|
||
<div id="connection-tab" class="tab-content active">
|
||
<div class="controls">
|
||
<div class="input-group">
|
||
<label>🔗 Connection Settings</label>
|
||
<input
|
||
type="text"
|
||
id="userIdInput"
|
||
placeholder="User ID"
|
||
value="test_user"
|
||
/>
|
||
<input
|
||
type="text"
|
||
id="roomInput"
|
||
placeholder="Room"
|
||
value="test_room"
|
||
/>
|
||
<input
|
||
type="text"
|
||
id="staticIdInput"
|
||
placeholder="Static ID (optional)"
|
||
/>
|
||
<div class="checkbox-group">
|
||
<input type="checkbox" id="ipBasedCheck" />
|
||
<label for="ipBasedCheck">Use IP-based ID</label>
|
||
</div>
|
||
<div class="checkbox-group">
|
||
<input type="checkbox" id="autoReconnectCheck" checked />
|
||
<label for="autoReconnectCheck">Auto-reconnect</label>
|
||
</div>
|
||
<div class="checkbox-group">
|
||
<input type="checkbox" id="heartbeatCheck" checked />
|
||
<label for="heartbeatCheck">Enable Heartbeat</label>
|
||
</div>
|
||
<button class="btn-primary" onclick="connectWebSocket()">
|
||
🔌 Connect
|
||
</button>
|
||
<button class="btn-secondary" onclick="disconnect()">
|
||
🔌 Disconnect
|
||
</button>
|
||
</div>
|
||
|
||
<div class="input-group">
|
||
<label>🖥️ Server Commands</label>
|
||
<button class="btn-warning" onclick="pingServer()">
|
||
🏓 Ping Server
|
||
</button>
|
||
<button class="btn-info" onclick="testConnectionHealth()">
|
||
🩺 Health Check
|
||
</button>
|
||
<button class="btn-secondary" onclick="getOnlineUsers()">
|
||
👥 Online Users
|
||
</button>
|
||
<button class="btn-info" onclick="getConnectionStats()">
|
||
📈 Connection Stats
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Messaging Tab -->
|
||
<div id="messaging-tab" class="tab-content">
|
||
<div class="controls">
|
||
<div class="input-group">
|
||
<label>💬 Send Messages</label>
|
||
<input
|
||
type="text"
|
||
id="messageInput"
|
||
placeholder="Type your message here..."
|
||
onkeypress="if(event.key==='Enter') sendMessage()"
|
||
/>
|
||
<button class="btn-primary" onclick="sendMessage()">
|
||
📤 Send Message
|
||
</button>
|
||
</div>
|
||
|
||
<div class="input-group">
|
||
<label>🎯 Direct Message</label>
|
||
<input
|
||
type="text"
|
||
id="targetClientId"
|
||
placeholder="Target Client ID"
|
||
/>
|
||
<input
|
||
type="text"
|
||
id="directMessageInput"
|
||
placeholder="Direct message content"
|
||
/>
|
||
<button class="btn-info" onclick="sendDirectMessage()">
|
||
📨 Send Direct
|
||
</button>
|
||
</div>
|
||
|
||
<div class="input-group">
|
||
<label>🏠 Room Management</label>
|
||
<input
|
||
type="text"
|
||
id="roomJoinInput"
|
||
placeholder="Room name to join"
|
||
/>
|
||
<button class="btn-secondary" onclick="joinRoom()">
|
||
🚪 Join Room
|
||
</button>
|
||
<button class="btn-secondary" onclick="leaveCurrentRoom()">
|
||
🚪 Leave Room
|
||
</button>
|
||
<button class="btn-info" onclick="getRoomInfo()">
|
||
📋 Room Info
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Database Tab -->
|
||
<div id="database-tab" class="tab-content">
|
||
<div class="controls">
|
||
<div class="input-group">
|
||
<label>🗄️ Database Operations</label>
|
||
<input
|
||
type="text"
|
||
id="dbTableInput"
|
||
placeholder="Table name"
|
||
value="test_table"
|
||
/>
|
||
<textarea
|
||
id="dbDataInput"
|
||
placeholder="JSON data for insert/update"
|
||
rows="3"
|
||
>
|
||
{"name": "test", "value": "123"}</textarea
|
||
>
|
||
<button class="btn-primary" onclick="insertData()">
|
||
➕ Insert Data
|
||
</button>
|
||
<button class="btn-info" onclick="queryData()">
|
||
🔍 Query Data
|
||
</button>
|
||
</div>
|
||
|
||
<div class="input-group">
|
||
<label>🔧 Database Config</label>
|
||
<select id="dbSelectInput">
|
||
<option value="default">Default DB</option>
|
||
<option value="secondary">Secondary DB</option>
|
||
</select>
|
||
<input
|
||
type="text"
|
||
id="dbQueryInput"
|
||
placeholder="Custom SQL query"
|
||
/>
|
||
<button class="btn-warning" onclick="executeCustomQuery()">
|
||
⚡ Execute Query
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Monitoring Tab -->
|
||
<div id="monitoring-tab" class="tab-content">
|
||
<div class="stats-grid">
|
||
<div class="stat-card">
|
||
<div class="stat-number" id="messagesReceivedStat">0</div>
|
||
<div class="stat-label">Messages Received</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-number" id="messagesSentStat">0</div>
|
||
<div class="stat-label">Messages Sent</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-number" id="latencyStat">0ms</div>
|
||
<div class="stat-label">Connection Latency</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-number" id="reconnectsStat">0</div>
|
||
<div class="stat-label">Reconnection Attempts</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="controls">
|
||
<div class="input-group">
|
||
<label>👥 Online Users</label>
|
||
<button class="btn-info" onclick="refreshOnlineUsers()">
|
||
🔄 Refresh Users
|
||
</button>
|
||
<div class="online-users" id="onlineUsersList">
|
||
<div style="text-align: center; color: #666; padding: 20px">
|
||
No users data available. Click "Refresh Users" to load.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Admin Tab -->
|
||
<div id="admin-tab" class="tab-content">
|
||
<div class="admin-controls">
|
||
<div class="admin-warning">
|
||
⚠️ Admin functions - Use with caution!
|
||
</div>
|
||
<div class="controls">
|
||
<div class="input-group">
|
||
<label>🛠️ Server Management</label>
|
||
<input
|
||
type="text"
|
||
id="adminClientId"
|
||
placeholder="Client ID to manage"
|
||
/>
|
||
<button class="btn-warning" onclick="kickClient()">
|
||
👢 Kick Client
|
||
</button>
|
||
<button class="btn-danger" onclick="killServer()">
|
||
💀 Kill Server (Test)
|
||
</button>
|
||
</div>
|
||
|
||
<div class="input-group">
|
||
<label>📊 System Info</label>
|
||
<button class="btn-info" onclick="getServerStats()">
|
||
📈 Server Stats
|
||
</button>
|
||
<button class="btn-info" onclick="getSystemHealth()">
|
||
🏥 System Health
|
||
</button>
|
||
<button class="btn-secondary" onclick="clearServerLogs()">
|
||
🗑️ Clear Logs
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Messages Display -->
|
||
<div>
|
||
<h3>
|
||
📨 Messages
|
||
<span
|
||
class="message-limit-warning"
|
||
id="messageCount"
|
||
style="font-size: 12px; color: #666"
|
||
>0 messages</span
|
||
>
|
||
<button
|
||
class="btn-secondary"
|
||
onclick="clearMessages()"
|
||
style="float: right; margin-bottom: 10px"
|
||
>
|
||
🗑️ Clear Messages
|
||
</button>
|
||
<button
|
||
class="btn-info"
|
||
onclick="exportMessages()"
|
||
style="float: right; margin-bottom: 10px; margin-right: 10px"
|
||
>
|
||
💾 Export
|
||
</button>
|
||
</h3>
|
||
<div class="messages-container" id="messages"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// =============================================================================
|
||
// ENHANCED GLOBAL VARIABLES - Konfigurasi yang lebih lengkap
|
||
// =============================================================================
|
||
|
||
// Connection management
|
||
let ws = null;
|
||
let clientId = null;
|
||
let staticId = null;
|
||
let ipAddress = null;
|
||
let currentRoom = null;
|
||
let idType = null;
|
||
let connectionStartTime = null;
|
||
let uptimeInterval = null;
|
||
|
||
// Enhanced reconnection strategy
|
||
let reconnectAttempts = 0;
|
||
let maxReconnectAttempts = 10; // Diperbanyak
|
||
let reconnectDelay = 1000; // Base delay 1 detik
|
||
let maxReconnectDelay = 30000; // Max 30 detik
|
||
let reconnectTimeout = null;
|
||
let isManualDisconnect = false;
|
||
|
||
// Heartbeat mechanism - DIPERBAIKI
|
||
let heartbeatInterval = null;
|
||
let heartbeatTimeout = null;
|
||
let lastHeartbeatTime = null;
|
||
let missedHeartbeats = 0;
|
||
let maxMissedHeartbeats = 3;
|
||
const HEARTBEAT_INTERVAL = 30000; // 30 detik
|
||
const HEARTBEAT_TIMEOUT = 10000; // 10 detik timeout
|
||
|
||
// Message management - MEMORY OPTIMIZATION
|
||
let onlineUsers = [];
|
||
let messageHistory = [];
|
||
const MAX_MESSAGES = 1000; // Limit pesan
|
||
const MESSAGE_WARNING_THRESHOLD = 800;
|
||
|
||
// Performance monitoring
|
||
let lastPingTime = null;
|
||
let connectionLatency = 0;
|
||
let connectionHealth = "poor";
|
||
let messagesReceived = 0;
|
||
let messagesSent = 0;
|
||
|
||
// Rate limiting - SECURITY IMPROVEMENT
|
||
let lastActionTime = {};
|
||
const ACTION_THROTTLE = 1000; // 1 detik between actions
|
||
|
||
// =============================================================================
|
||
// ENHANCED CONNECTION MANAGEMENT - Stabilitas koneksi diperbaiki
|
||
// =============================================================================
|
||
|
||
function connectWebSocket() {
|
||
// Prevent rapid reconnection attempts
|
||
if (!canPerformAction("connect")) {
|
||
addMessage(
|
||
"⚠️ Connection attempt throttled. Please wait.",
|
||
"warning"
|
||
);
|
||
return;
|
||
}
|
||
|
||
// Clean up previous connection
|
||
if (ws) {
|
||
cleanupConnection();
|
||
}
|
||
|
||
const userId =
|
||
sanitizeInput(document.getElementById("userIdInput").value) ||
|
||
"test_user";
|
||
const room =
|
||
sanitizeInput(document.getElementById("roomInput").value) ||
|
||
"test_room";
|
||
const staticIdValue = sanitizeInput(
|
||
document.getElementById("staticIdInput").value
|
||
);
|
||
const ipBased = document.getElementById("ipBasedCheck").checked;
|
||
|
||
let url = `ws://localhost:8080/api/v1/ws?user_id=${encodeURIComponent(
|
||
userId
|
||
)}&room=${encodeURIComponent(room)}`;
|
||
|
||
if (ipBased) {
|
||
url += "&ip_based=true";
|
||
} else if (staticIdValue) {
|
||
url += `&static_id=${encodeURIComponent(staticIdValue)}`;
|
||
}
|
||
|
||
updateConnectionStatus("connecting");
|
||
isManualDisconnect = false;
|
||
connectionStartTime = Date.now();
|
||
startUptimeTimer();
|
||
|
||
try {
|
||
ws = new WebSocket(url);
|
||
|
||
// Enhanced connection timeout
|
||
const connectionTimeout = setTimeout(() => {
|
||
if (ws.readyState === WebSocket.CONNECTING) {
|
||
ws.close();
|
||
addMessage("❌ Connection timeout after 15 seconds", "error");
|
||
scheduleReconnect();
|
||
}
|
||
}, 15000);
|
||
|
||
ws.onopen = function (event) {
|
||
clearTimeout(connectionTimeout);
|
||
updateConnectionStatus("connected");
|
||
reconnectAttempts = 0;
|
||
reconnectDelay = 1000; // Reset delay
|
||
connectionHealth = "good";
|
||
updateHealthIndicator();
|
||
updateStats();
|
||
|
||
addMessage(
|
||
"🟢 Connected to WebSocket server",
|
||
"success",
|
||
"Connection established successfully"
|
||
);
|
||
|
||
// Start heartbeat if enabled
|
||
if (document.getElementById("heartbeatCheck").checked) {
|
||
startHeartbeat();
|
||
}
|
||
};
|
||
|
||
ws.onmessage = function (event) {
|
||
handleIncomingMessage(event);
|
||
};
|
||
|
||
ws.onclose = function (event) {
|
||
clearTimeout(connectionTimeout);
|
||
cleanupTimers();
|
||
updateConnectionStatus("disconnected");
|
||
|
||
const reason = getCloseReason(event.code);
|
||
addMessage(
|
||
`🔴 Disconnected: ${reason}`,
|
||
"error",
|
||
`Code: ${event.code}, Reason: ${event.reason || "Unknown"}`
|
||
);
|
||
|
||
// Auto-reconnect logic jika tidak manual disconnect
|
||
if (
|
||
!isManualDisconnect &&
|
||
document.getElementById("autoReconnectCheck").checked
|
||
) {
|
||
scheduleReconnect();
|
||
}
|
||
};
|
||
|
||
ws.onerror = function (error) {
|
||
updateConnectionStatus("error");
|
||
connectionHealth = "poor";
|
||
updateHealthIndicator();
|
||
addMessage(
|
||
"❌ WebSocket Error occurred",
|
||
"error",
|
||
error.toString()
|
||
);
|
||
};
|
||
} catch (error) {
|
||
updateConnectionStatus("error");
|
||
addMessage(
|
||
"❌ Failed to create WebSocket connection",
|
||
"error",
|
||
error.message
|
||
);
|
||
scheduleReconnect();
|
||
}
|
||
}
|
||
|
||
// Enhanced message handling dengan error boundary
|
||
function handleIncomingMessage(event) {
|
||
try {
|
||
messagesReceived++;
|
||
updateHealthIndicator();
|
||
updateStats();
|
||
|
||
const data = JSON.parse(event.data);
|
||
|
||
// Handle heartbeat responses
|
||
if (data.type === "pong" || data.type === "heartbeat_ack") {
|
||
handleHeartbeatResponse(data);
|
||
return;
|
||
}
|
||
|
||
// Handle connection test responses
|
||
if (data.type === "connection_test_result") {
|
||
handleConnectionTestResult(data);
|
||
return;
|
||
}
|
||
|
||
// Extract connection info from welcome message
|
||
if (data.type === "welcome" && data.data) {
|
||
extractConnectionInfo(data.data);
|
||
}
|
||
|
||
// Handle online users
|
||
if (data.type === "online_users" && data.data && data.data.users) {
|
||
onlineUsers = data.data.users;
|
||
updateOnlineUsers();
|
||
}
|
||
|
||
// Handle server info
|
||
if (data.type === "server_info" && data.data) {
|
||
updateStatsPanel(data.data);
|
||
}
|
||
|
||
// Store message with limit
|
||
storeMessage(data);
|
||
|
||
// Display message
|
||
displayMessage(data);
|
||
} catch (error) {
|
||
addMessage("❌ Error processing message", "error", error.message);
|
||
console.error("Message processing error:", error, event.data);
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// HEARTBEAT MECHANISM - Menjaga koneksi tetap hidup
|
||
// =============================================================================
|
||
|
||
function startHeartbeat() {
|
||
if (heartbeatInterval) {
|
||
clearInterval(heartbeatInterval);
|
||
}
|
||
|
||
heartbeatInterval = setInterval(() => {
|
||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||
sendHeartbeat();
|
||
} else {
|
||
stopHeartbeat();
|
||
}
|
||
}, HEARTBEAT_INTERVAL);
|
||
|
||
addMessage(
|
||
"💓 Heartbeat started",
|
||
"info",
|
||
`Interval: ${HEARTBEAT_INTERVAL / 1000}s`
|
||
);
|
||
}
|
||
|
||
function sendHeartbeat() {
|
||
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
||
|
||
lastHeartbeatTime = Date.now();
|
||
|
||
try {
|
||
ws.send(
|
||
JSON.stringify({
|
||
type: "heartbeat",
|
||
data: {
|
||
timestamp: lastHeartbeatTime,
|
||
client_uptime: connectionStartTime
|
||
? Date.now() - connectionStartTime
|
||
: 0,
|
||
},
|
||
})
|
||
);
|
||
|
||
// Set timeout untuk response
|
||
heartbeatTimeout = setTimeout(() => {
|
||
missedHeartbeats++;
|
||
addMessage(
|
||
`💔 Heartbeat missed (${missedHeartbeats}/${maxMissedHeartbeats})`,
|
||
"warning"
|
||
);
|
||
|
||
if (missedHeartbeats >= maxMissedHeartbeats) {
|
||
addMessage(
|
||
"💔 Too many missed heartbeats, reconnecting...",
|
||
"error"
|
||
);
|
||
connectionHealth = "poor";
|
||
updateHealthIndicator();
|
||
reconnectWebSocket();
|
||
}
|
||
}, HEARTBEAT_TIMEOUT);
|
||
} catch (error) {
|
||
addMessage("💔 Failed to send heartbeat", "error", error.message);
|
||
connectionHealth = "poor";
|
||
updateHealthIndicator();
|
||
}
|
||
}
|
||
|
||
function handleHeartbeatResponse(data) {
|
||
if (heartbeatTimeout) {
|
||
clearTimeout(heartbeatTimeout);
|
||
}
|
||
|
||
missedHeartbeats = 0;
|
||
connectionHealth = "excellent";
|
||
updateHealthIndicator();
|
||
|
||
if (lastHeartbeatTime) {
|
||
connectionLatency = Date.now() - lastHeartbeatTime;
|
||
if (connectionLatency > 5000) {
|
||
connectionHealth = "warning";
|
||
} else if (connectionLatency > 2000) {
|
||
connectionHealth = "good";
|
||
}
|
||
updateHealthIndicator();
|
||
updateStats();
|
||
}
|
||
}
|
||
|
||
function stopHeartbeat() {
|
||
if (heartbeatInterval) {
|
||
clearInterval(heartbeatInterval);
|
||
heartbeatInterval = null;
|
||
}
|
||
if (heartbeatTimeout) {
|
||
clearTimeout(heartbeatTimeout);
|
||
heartbeatTimeout = null;
|
||
}
|
||
missedHeartbeats = 0;
|
||
}
|
||
|
||
// =============================================================================
|
||
// RECONNECTION STRATEGY - Exponential backoff diperbaiki
|
||
// =============================================================================
|
||
|
||
function scheduleReconnect() {
|
||
if (isManualDisconnect || reconnectAttempts >= maxReconnectAttempts) {
|
||
if (reconnectAttempts >= maxReconnectAttempts) {
|
||
addMessage(
|
||
"❌ Max reconnection attempts reached",
|
||
"error",
|
||
"Please reconnect manually or refresh the page"
|
||
);
|
||
document.getElementById("reconnectInfo").textContent =
|
||
"Failed - Manual action required";
|
||
}
|
||
return;
|
||
}
|
||
|
||
reconnectAttempts++;
|
||
|
||
// Exponential backoff dengan jitter
|
||
const jitter = Math.random() * 1000;
|
||
const delay = Math.min(
|
||
reconnectDelay * Math.pow(1.5, reconnectAttempts - 1) + jitter,
|
||
maxReconnectDelay
|
||
);
|
||
|
||
document.getElementById(
|
||
"reconnectInfo"
|
||
).textContent = `Attempt ${reconnectAttempts}/${maxReconnectAttempts} in ${Math.round(
|
||
delay / 1000
|
||
)}s`;
|
||
|
||
addMessage(
|
||
`🔄 Reconnecting in ${Math.round(
|
||
delay / 1000
|
||
)} seconds (attempt ${reconnectAttempts}/${maxReconnectAttempts})`,
|
||
"info"
|
||
);
|
||
|
||
reconnectTimeout = setTimeout(() => {
|
||
addMessage(
|
||
`🔄 Reconnection attempt ${reconnectAttempts}/${maxReconnectAttempts}`,
|
||
"info"
|
||
);
|
||
connectWebSocket();
|
||
}, delay);
|
||
|
||
updateStats();
|
||
}
|
||
|
||
function reconnectWebSocket() {
|
||
if (ws) {
|
||
cleanupConnection();
|
||
}
|
||
scheduleReconnect();
|
||
}
|
||
|
||
// =============================================================================
|
||
// MEMORY MANAGEMENT - Optimasi memori diperbaiki
|
||
// =============================================================================
|
||
|
||
function storeMessage(data) {
|
||
// Add message to history dengan limit
|
||
messageHistory.push({
|
||
timestamp: new Date(),
|
||
type: data.type,
|
||
data: data.data,
|
||
messageId: data.message_id,
|
||
size: JSON.stringify(data).length,
|
||
});
|
||
|
||
// Cleanup old messages jika melebihi limit
|
||
if (messageHistory.length > MAX_MESSAGES) {
|
||
const removed = messageHistory.splice(
|
||
0,
|
||
messageHistory.length - MAX_MESSAGES
|
||
);
|
||
addMessage(
|
||
`🗑️ Removed ${removed.length} old messages to free memory`,
|
||
"info"
|
||
);
|
||
}
|
||
|
||
// Show warning jika mendekati limit
|
||
if (messageHistory.length > MESSAGE_WARNING_THRESHOLD) {
|
||
const warning = document.getElementById("messageLimitWarning");
|
||
warning.style.display = "block";
|
||
}
|
||
|
||
updateMessageCount();
|
||
}
|
||
|
||
function updateMessageCount() {
|
||
const count = document.getElementById("messageCount");
|
||
count.textContent = `${messageHistory.length}/${MAX_MESSAGES} messages`;
|
||
|
||
if (messageHistory.length > MESSAGE_WARNING_THRESHOLD) {
|
||
count.style.color = "#856404";
|
||
} else {
|
||
count.style.color = "#666";
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// SECURITY & INPUT VALIDATION - Keamanan diperbaiki
|
||
// =============================================================================
|
||
|
||
function sanitizeInput(input) {
|
||
if (typeof input !== "string") return "";
|
||
return input
|
||
.replace(/[<>'"&]/g, function (match) {
|
||
const escape = {
|
||
"<": "<",
|
||
">": ">",
|
||
'"': """,
|
||
"'": "'",
|
||
"&": "&",
|
||
};
|
||
return escape[match];
|
||
})
|
||
.trim();
|
||
}
|
||
|
||
function canPerformAction(action) {
|
||
const now = Date.now();
|
||
const lastTime = lastActionTime[action] || 0;
|
||
|
||
if (now - lastTime < ACTION_THROTTLE) {
|
||
return false;
|
||
}
|
||
|
||
lastActionTime[action] = now;
|
||
return true;
|
||
}
|
||
|
||
function validateMessage(message) {
|
||
if (!message || typeof message !== "string") {
|
||
return { valid: false, error: "Message must be a non-empty string" };
|
||
}
|
||
|
||
if (message.length > 5000) {
|
||
return {
|
||
valid: false,
|
||
error: "Message too long (max 5000 characters)",
|
||
};
|
||
}
|
||
|
||
return { valid: true };
|
||
}
|
||
|
||
// =============================================================================
|
||
// UI IMPROVEMENTS - User experience diperbaiki
|
||
// =============================================================================
|
||
|
||
function updateHealthIndicator() {
|
||
const indicator = document.getElementById("healthIndicator");
|
||
const classes = [
|
||
"health-excellent",
|
||
"health-good",
|
||
"health-warning",
|
||
"health-poor",
|
||
];
|
||
|
||
// Clear existing classes
|
||
indicator.classList.remove(...classes);
|
||
|
||
switch (connectionHealth) {
|
||
case "excellent":
|
||
indicator.classList.add("health-excellent");
|
||
indicator.innerHTML = "<span>🟢</span> Excellent";
|
||
break;
|
||
case "good":
|
||
indicator.classList.add("health-good");
|
||
indicator.innerHTML = "<span>🔵</span> Good";
|
||
break;
|
||
case "warning":
|
||
indicator.classList.add("health-warning");
|
||
indicator.innerHTML = "<span>🟡</span> Warning";
|
||
break;
|
||
default:
|
||
indicator.classList.add("health-poor");
|
||
indicator.innerHTML = "<span>🔴</span> Poor";
|
||
}
|
||
|
||
// Add latency info if available
|
||
if (connectionLatency > 0) {
|
||
const latencyText = document.createElement("small");
|
||
latencyText.textContent = ` (${connectionLatency}ms)`;
|
||
latencyText.style.color = "#666";
|
||
indicator.appendChild(latencyText);
|
||
}
|
||
}
|
||
|
||
function startUptimeTimer() {
|
||
if (uptimeInterval) {
|
||
clearInterval(uptimeInterval);
|
||
}
|
||
|
||
uptimeInterval = setInterval(() => {
|
||
if (connectionStartTime && ws && ws.readyState === WebSocket.OPEN) {
|
||
const uptime = Date.now() - connectionStartTime;
|
||
document.getElementById("connectionUptime").textContent =
|
||
formatUptime(uptime);
|
||
}
|
||
}, 1000);
|
||
}
|
||
|
||
function formatUptime(ms) {
|
||
const seconds = Math.floor(ms / 1000);
|
||
const minutes = Math.floor(seconds / 60);
|
||
const hours = Math.floor(minutes / 60);
|
||
|
||
if (hours > 0) {
|
||
return `${hours}h ${minutes % 60}m`;
|
||
} else if (minutes > 0) {
|
||
return `${minutes}m ${seconds % 60}s`;
|
||
} else {
|
||
return `${seconds}s`;
|
||
}
|
||
}
|
||
|
||
function updateStats() {
|
||
document.getElementById("messagesReceivedStat").textContent =
|
||
messagesReceived;
|
||
document.getElementById("messagesSentStat").textContent = messagesSent;
|
||
document.getElementById("latencyStat").textContent =
|
||
connectionLatency + "ms";
|
||
document.getElementById("reconnectsStat").textContent =
|
||
reconnectAttempts;
|
||
}
|
||
|
||
// =============================================================================
|
||
// ENHANCED MESSAGE FUNCTIONS - Pesan dengan validation
|
||
// =============================================================================
|
||
|
||
function sendMessage() {
|
||
if (!canPerformAction("sendMessage")) {
|
||
addMessage(
|
||
"⚠️ Please wait before sending another message",
|
||
"warning"
|
||
);
|
||
return;
|
||
}
|
||
|
||
const input = document.getElementById("messageInput");
|
||
const validation = validateMessage(input.value);
|
||
|
||
if (!validation.valid) {
|
||
addMessage(`❌ Invalid message: ${validation.error}`, "error");
|
||
return;
|
||
}
|
||
|
||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||
addMessage("❌ Cannot send message: Not connected", "error");
|
||
return;
|
||
}
|
||
|
||
try {
|
||
ws.send(
|
||
JSON.stringify({
|
||
type: "broadcast",
|
||
data: { message: sanitizeInput(input.value) },
|
||
})
|
||
);
|
||
|
||
messagesSent++;
|
||
updateStats();
|
||
addMessage(`📢 Broadcast sent: "${input.value}"`, "success");
|
||
input.value = "";
|
||
} catch (error) {
|
||
addMessage("❌ Failed to send message", "error", error.message);
|
||
}
|
||
}
|
||
|
||
function sendDirectMessage() {
|
||
if (!canPerformAction("sendDirectMessage")) {
|
||
addMessage(
|
||
"⚠️ Please wait before sending another message",
|
||
"warning"
|
||
);
|
||
return;
|
||
}
|
||
|
||
const targetId = sanitizeInput(
|
||
document.getElementById("targetClientId").value
|
||
);
|
||
const message = sanitizeInput(
|
||
document.getElementById("directMessageInput").value
|
||
);
|
||
|
||
const validation = validateMessage(message);
|
||
if (!validation.valid) {
|
||
addMessage(`❌ Invalid message: ${validation.error}`, "error");
|
||
return;
|
||
}
|
||
|
||
if (!targetId) {
|
||
addMessage("❌ Please specify a target client ID", "error");
|
||
return;
|
||
}
|
||
|
||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||
addMessage("❌ Cannot send message: Not connected", "error");
|
||
return;
|
||
}
|
||
|
||
try {
|
||
ws.send(
|
||
JSON.stringify({
|
||
type: "direct_message",
|
||
data: {
|
||
target_client_id: targetId,
|
||
message: message,
|
||
},
|
||
})
|
||
);
|
||
|
||
messagesSent++;
|
||
updateStats();
|
||
addMessage(
|
||
`📨 Direct message sent to ${targetId}: "${message}"`,
|
||
"success"
|
||
);
|
||
document.getElementById("directMessageInput").value = "";
|
||
} catch (error) {
|
||
addMessage(
|
||
"❌ Failed to send direct message",
|
||
"error",
|
||
error.message
|
||
);
|
||
}
|
||
}
|
||
|
||
function pingServer() {
|
||
if (!canPerformAction("ping")) {
|
||
addMessage("⚠️ Please wait before pinging again", "warning");
|
||
return;
|
||
}
|
||
|
||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||
addMessage("❌ Cannot ping: Not connected", "error");
|
||
return;
|
||
}
|
||
|
||
lastPingTime = Date.now();
|
||
|
||
try {
|
||
ws.send(
|
||
JSON.stringify({
|
||
type: "ping",
|
||
data: { timestamp: lastPingTime },
|
||
})
|
||
);
|
||
|
||
messagesSent++;
|
||
updateStats();
|
||
addMessage("🏓 Ping sent to server", "info");
|
||
} catch (error) {
|
||
addMessage("❌ Failed to send ping", "error", error.message);
|
||
}
|
||
}
|
||
|
||
function testConnectionHealth() {
|
||
if (!canPerformAction("healthCheck")) {
|
||
addMessage("⚠️ Please wait before testing again", "warning");
|
||
return;
|
||
}
|
||
|
||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||
addMessage("❌ Cannot test: Not connected", "error");
|
||
return;
|
||
}
|
||
|
||
const startTime = Date.now();
|
||
lastPingTime = startTime;
|
||
|
||
try {
|
||
ws.send(
|
||
JSON.stringify({
|
||
type: "connection_test",
|
||
data: { timestamp: startTime },
|
||
})
|
||
);
|
||
|
||
messagesSent++;
|
||
updateStats();
|
||
addMessage("🩺 Connection health test initiated...", "info");
|
||
} catch (error) {
|
||
addMessage("❌ Health test failed", "error", error.message);
|
||
}
|
||
}
|
||
|
||
function handleConnectionTestResult(data) {
|
||
if (lastPingTime) {
|
||
const latency = Date.now() - lastPingTime;
|
||
connectionLatency = latency;
|
||
|
||
if (latency < 100) {
|
||
connectionHealth = "excellent";
|
||
} else if (latency < 500) {
|
||
connectionHealth = "good";
|
||
} else if (latency < 2000) {
|
||
connectionHealth = "warning";
|
||
} else {
|
||
connectionHealth = "poor";
|
||
}
|
||
|
||
updateHealthIndicator();
|
||
updateStats();
|
||
addMessage(
|
||
`🩺 Health test result: ${latency}ms latency`,
|
||
"success",
|
||
`Connection status: ${connectionHealth}`
|
||
);
|
||
}
|
||
}
|
||
|
||
function getOnlineUsers() {
|
||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||
addMessage("❌ Cannot get users: Not connected", "error");
|
||
return;
|
||
}
|
||
|
||
try {
|
||
ws.send(
|
||
JSON.stringify({
|
||
type: "get_online_users",
|
||
data: {},
|
||
})
|
||
);
|
||
|
||
messagesSent++;
|
||
updateStats();
|
||
addMessage("👥 Requesting online users list...", "info");
|
||
} catch (error) {
|
||
addMessage("❌ Failed to get online users", "error", error.message);
|
||
}
|
||
}
|
||
|
||
function refreshOnlineUsers() {
|
||
getOnlineUsers();
|
||
}
|
||
|
||
function getConnectionStats() {
|
||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||
addMessage("❌ Cannot get stats: Not connected", "error");
|
||
return;
|
||
}
|
||
|
||
try {
|
||
ws.send(
|
||
JSON.stringify({
|
||
type: "get_stats",
|
||
data: {},
|
||
})
|
||
);
|
||
|
||
messagesSent++;
|
||
updateStats();
|
||
addMessage("📈 Requesting connection statistics...", "info");
|
||
} catch (error) {
|
||
addMessage("❌ Failed to get stats", "error", error.message);
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// ROOM MANAGEMENT FUNCTIONS
|
||
// =============================================================================
|
||
|
||
function joinRoom() {
|
||
const roomName = sanitizeInput(
|
||
document.getElementById("roomJoinInput").value
|
||
);
|
||
|
||
if (!roomName) {
|
||
addMessage("❌ Please enter a room name", "error");
|
||
return;
|
||
}
|
||
|
||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||
addMessage("❌ Cannot join room: Not connected", "error");
|
||
return;
|
||
}
|
||
|
||
try {
|
||
ws.send(
|
||
JSON.stringify({
|
||
type: "join_room",
|
||
data: { room: roomName },
|
||
})
|
||
);
|
||
|
||
messagesSent++;
|
||
updateStats();
|
||
addMessage(`🚪 Joining room: ${roomName}`, "info");
|
||
document.getElementById("roomJoinInput").value = "";
|
||
} catch (error) {
|
||
addMessage("❌ Failed to join room", "error", error.message);
|
||
}
|
||
}
|
||
|
||
function leaveCurrentRoom() {
|
||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||
addMessage("❌ Cannot leave room: Not connected", "error");
|
||
return;
|
||
}
|
||
|
||
try {
|
||
ws.send(
|
||
JSON.stringify({
|
||
type: "leave_room",
|
||
data: {},
|
||
})
|
||
);
|
||
|
||
messagesSent++;
|
||
updateStats();
|
||
addMessage("🚪 Leaving current room...", "info");
|
||
} catch (error) {
|
||
addMessage("❌ Failed to leave room", "error", error.message);
|
||
}
|
||
}
|
||
|
||
function getRoomInfo() {
|
||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||
addMessage("❌ Cannot get room info: Not connected", "error");
|
||
return;
|
||
}
|
||
|
||
try {
|
||
ws.send(
|
||
JSON.stringify({
|
||
type: "get_room_info",
|
||
data: {},
|
||
})
|
||
);
|
||
|
||
messagesSent++;
|
||
updateStats();
|
||
addMessage("📋 Requesting room information...", "info");
|
||
} catch (error) {
|
||
addMessage("❌ Failed to get room info", "error", error.message);
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// DATABASE FUNCTIONS
|
||
// =============================================================================
|
||
|
||
function insertData() {
|
||
const table = sanitizeInput(
|
||
document.getElementById("dbTableInput").value
|
||
);
|
||
const dataText = document.getElementById("dbDataInput").value;
|
||
|
||
if (!table) {
|
||
addMessage("❌ Please enter a table name", "error");
|
||
return;
|
||
}
|
||
|
||
let data;
|
||
try {
|
||
data = JSON.parse(dataText);
|
||
} catch (error) {
|
||
addMessage("❌ Invalid JSON data format", "error", error.message);
|
||
return;
|
||
}
|
||
|
||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||
addMessage("❌ Cannot insert data: Not connected", "error");
|
||
return;
|
||
}
|
||
|
||
try {
|
||
ws.send(
|
||
JSON.stringify({
|
||
type: "db_insert",
|
||
data: {
|
||
table: table,
|
||
data: data,
|
||
},
|
||
})
|
||
);
|
||
|
||
messagesSent++;
|
||
updateStats();
|
||
addMessage(`➕ Inserting data into table: ${table}`, "info");
|
||
} catch (error) {
|
||
addMessage("❌ Failed to insert data", "error", error.message);
|
||
}
|
||
}
|
||
|
||
function queryData() {
|
||
const table = sanitizeInput(
|
||
document.getElementById("dbTableInput").value
|
||
);
|
||
|
||
if (!table) {
|
||
addMessage("❌ Please enter a table name", "error");
|
||
return;
|
||
}
|
||
|
||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||
addMessage("❌ Cannot query data: Not connected", "error");
|
||
return;
|
||
}
|
||
|
||
try {
|
||
ws.send(
|
||
JSON.stringify({
|
||
type: "db_query",
|
||
data: {
|
||
table: table,
|
||
query: `SELECT * FROM ${table} ORDER BY id DESC LIMIT 10`,
|
||
},
|
||
})
|
||
);
|
||
|
||
messagesSent++;
|
||
updateStats();
|
||
addMessage(`🔍 Querying data from table: ${table}`, "info");
|
||
} catch (error) {
|
||
addMessage("❌ Failed to query data", "error", error.message);
|
||
}
|
||
}
|
||
|
||
function executeCustomQuery() {
|
||
const query = sanitizeInput(
|
||
document.getElementById("dbQueryInput").value
|
||
);
|
||
const database = document.getElementById("dbSelectInput").value;
|
||
|
||
if (!query) {
|
||
addMessage("❌ Please enter a SQL query", "error");
|
||
return;
|
||
}
|
||
|
||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||
addMessage("❌ Cannot execute query: Not connected", "error");
|
||
return;
|
||
}
|
||
|
||
try {
|
||
ws.send(
|
||
JSON.stringify({
|
||
type: "db_custom_query",
|
||
data: {
|
||
database: database,
|
||
query: query,
|
||
},
|
||
})
|
||
);
|
||
|
||
messagesSent++;
|
||
updateStats();
|
||
addMessage(`⚡ Executing custom query on ${database}`, "info");
|
||
document.getElementById("dbQueryInput").value = "";
|
||
} catch (error) {
|
||
addMessage("❌ Failed to execute query", "error", error.message);
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// ADMIN FUNCTIONS
|
||
// =============================================================================
|
||
|
||
function kickClient() {
|
||
const targetClientId = sanitizeInput(
|
||
document.getElementById("adminClientId").value
|
||
);
|
||
|
||
if (!targetClientId) {
|
||
addMessage("❌ Please enter a client ID to kick", "error");
|
||
return;
|
||
}
|
||
|
||
if (
|
||
!confirm(`Are you sure you want to kick client ${targetClientId}?`)
|
||
) {
|
||
return;
|
||
}
|
||
|
||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||
addMessage("❌ Cannot kick client: Not connected", "error");
|
||
return;
|
||
}
|
||
|
||
try {
|
||
ws.send(
|
||
JSON.stringify({
|
||
type: "admin_kick_client",
|
||
data: {
|
||
client_id: targetClientId,
|
||
},
|
||
})
|
||
);
|
||
|
||
messagesSent++;
|
||
updateStats();
|
||
addMessage(`👢 Kicking client: ${targetClientId}`, "warning");
|
||
} catch (error) {
|
||
addMessage("❌ Failed to kick client", "error", error.message);
|
||
}
|
||
}
|
||
|
||
function killServer() {
|
||
if (
|
||
!confirm(
|
||
"Are you sure you want to send a kill signal to the server? This is for testing purposes only."
|
||
)
|
||
) {
|
||
return;
|
||
}
|
||
|
||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||
addMessage("❌ Cannot send kill signal: Not connected", "error");
|
||
return;
|
||
}
|
||
|
||
try {
|
||
ws.send(
|
||
JSON.stringify({
|
||
type: "admin_kill_server",
|
||
data: {},
|
||
})
|
||
);
|
||
|
||
messagesSent++;
|
||
updateStats();
|
||
addMessage("💀 Kill signal sent to server (for testing)", "warning");
|
||
} catch (error) {
|
||
addMessage("❌ Failed to send kill signal", "error", error.message);
|
||
}
|
||
}
|
||
|
||
function getServerStats() {
|
||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||
addMessage("❌ Cannot get server stats: Not connected", "error");
|
||
return;
|
||
}
|
||
|
||
try {
|
||
ws.send(
|
||
JSON.stringify({
|
||
type: "get_server_stats",
|
||
data: {},
|
||
})
|
||
);
|
||
|
||
messagesSent++;
|
||
updateStats();
|
||
addMessage("📈 Requesting detailed server statistics...", "info");
|
||
} catch (error) {
|
||
addMessage("❌ Failed to get server stats", "error", error.message);
|
||
}
|
||
}
|
||
|
||
function getSystemHealth() {
|
||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||
addMessage("❌ Cannot get system health: Not connected", "error");
|
||
return;
|
||
}
|
||
|
||
try {
|
||
ws.send(
|
||
JSON.stringify({
|
||
type: "get_system_health",
|
||
data: {},
|
||
})
|
||
);
|
||
|
||
messagesSent++;
|
||
updateStats();
|
||
addMessage("🏥 Requesting system health information...", "info");
|
||
} catch (error) {
|
||
addMessage("❌ Failed to get system health", "error", error.message);
|
||
}
|
||
}
|
||
|
||
function clearServerLogs() {
|
||
if (!confirm("Are you sure you want to clear server logs?")) {
|
||
return;
|
||
}
|
||
|
||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||
addMessage("❌ Cannot clear logs: Not connected", "error");
|
||
return;
|
||
}
|
||
|
||
try {
|
||
ws.send(
|
||
JSON.stringify({
|
||
type: "admin_clear_logs",
|
||
data: {},
|
||
})
|
||
);
|
||
|
||
messagesSent++;
|
||
updateStats();
|
||
addMessage("🗑️ Server log clear request sent", "info");
|
||
} catch (error) {
|
||
addMessage("❌ Failed to clear logs", "error", error.message);
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// UTILITY FUNCTIONS
|
||
// =============================================================================
|
||
|
||
function clearMessages() {
|
||
if (confirm("Are you sure you want to clear all messages?")) {
|
||
document.getElementById("messages").innerHTML = "";
|
||
messageHistory = [];
|
||
updateMessageCount();
|
||
const warning = document.getElementById("messageLimitWarning");
|
||
warning.style.display = "none";
|
||
addMessage("🗑️ Message history cleared", "info");
|
||
}
|
||
}
|
||
|
||
function exportMessages() {
|
||
if (messageHistory.length === 0) {
|
||
addMessage("❌ No messages to export", "error");
|
||
return;
|
||
}
|
||
|
||
const exportData = {
|
||
exported_at: new Date().toISOString(),
|
||
client_info: {
|
||
client_id: clientId,
|
||
static_id: staticId,
|
||
room: currentRoom,
|
||
connection_start: connectionStartTime
|
||
? new Date(connectionStartTime).toISOString()
|
||
: null,
|
||
},
|
||
statistics: {
|
||
messages_received: messagesReceived,
|
||
messages_sent: messagesSent,
|
||
reconnect_attempts: reconnectAttempts,
|
||
connection_latency: connectionLatency,
|
||
},
|
||
messages: messageHistory,
|
||
};
|
||
|
||
const blob = new Blob([JSON.stringify(exportData, null, 2)], {
|
||
type: "application/json",
|
||
});
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement("a");
|
||
a.href = url;
|
||
a.download = `websocket-messages-${
|
||
new Date().toISOString().split("T")[0]
|
||
}.json`;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
URL.revokeObjectURL(url);
|
||
|
||
addMessage(`💾 Exported ${messageHistory.length} messages`, "success");
|
||
}
|
||
|
||
function updateOnlineUsers() {
|
||
const container = document.getElementById("onlineUsersList");
|
||
|
||
if (!onlineUsers || onlineUsers.length === 0) {
|
||
container.innerHTML =
|
||
'<div style="text-align: center; color: #666; padding: 20px;">No online users</div>';
|
||
return;
|
||
}
|
||
|
||
const userElements = onlineUsers
|
||
.map((user) => {
|
||
const isCurrentUser = user.id === clientId;
|
||
const statusIcon = user.is_active ? "🟢" : "🔴";
|
||
|
||
return `
|
||
<div class="user-item ${
|
||
isCurrentUser ? "current-user" : ""
|
||
}">
|
||
<div class="user-info">
|
||
<div class="user-id">${statusIcon} ${sanitizeInput(
|
||
user.id
|
||
)} ${isCurrentUser ? "(You)" : ""}</div>
|
||
<div class="user-details">
|
||
Room: ${sanitizeInput(user.room)} |
|
||
IP: ${sanitizeInput(user.ip_address)} |
|
||
Connected: ${new Date(
|
||
user.connected_at
|
||
).toLocaleTimeString()}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
})
|
||
.join("");
|
||
|
||
container.innerHTML = userElements;
|
||
}
|
||
|
||
function updateStatsPanel(data) {
|
||
// Update various stats panels based on server data
|
||
if (data.connected_clients !== undefined) {
|
||
// Update connection stats if available
|
||
addMessage(
|
||
"📊 Server statistics updated",
|
||
"info",
|
||
JSON.stringify(data, null, 2)
|
||
);
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// CLEANUP FUNCTIONS - Pembersihan resource diperbaiki
|
||
// =============================================================================
|
||
|
||
function cleanupConnection() {
|
||
if (ws) {
|
||
ws.onopen = null;
|
||
ws.onclose = null;
|
||
ws.onmessage = null;
|
||
ws.onerror = null;
|
||
ws.close();
|
||
ws = null;
|
||
}
|
||
|
||
cleanupTimers();
|
||
}
|
||
|
||
function cleanupTimers() {
|
||
if (reconnectTimeout) {
|
||
clearTimeout(reconnectTimeout);
|
||
reconnectTimeout = null;
|
||
}
|
||
|
||
if (uptimeInterval) {
|
||
clearInterval(uptimeInterval);
|
||
uptimeInterval = null;
|
||
}
|
||
|
||
stopHeartbeat();
|
||
}
|
||
|
||
function disconnect() {
|
||
isManualDisconnect = true;
|
||
reconnectAttempts = maxReconnectAttempts; // Prevent auto-reconnect
|
||
|
||
cleanupConnection();
|
||
updateConnectionStatus("disconnected");
|
||
document.getElementById("reconnectInfo").textContent =
|
||
"Manual disconnect";
|
||
connectionHealth = "poor";
|
||
updateHealthIndicator();
|
||
addMessage("🔌 Manually disconnected", "info");
|
||
}
|
||
|
||
// =============================================================================
|
||
// HELPER FUNCTIONS
|
||
// =============================================================================
|
||
|
||
function getCloseReason(code) {
|
||
const reasons = {
|
||
1000: "Normal Closure",
|
||
1001: "Going Away",
|
||
1002: "Protocol Error",
|
||
1003: "Unsupported Data",
|
||
1004: "Reserved",
|
||
1005: "No Status Received",
|
||
1006: "Abnormal Closure",
|
||
1007: "Invalid frame payload data",
|
||
1008: "Policy Violation",
|
||
1009: "Message Too Big",
|
||
1010: "Mandatory Extension",
|
||
1011: "Internal Server Error",
|
||
1015: "TLS Handshake Error",
|
||
};
|
||
|
||
return reasons[code] || `Unknown (${code})`;
|
||
}
|
||
|
||
function extractConnectionInfo(data) {
|
||
clientId = data.client_id;
|
||
staticId = data.static_id;
|
||
ipAddress = data.ip_address;
|
||
currentRoom = data.room;
|
||
idType = data.id_type;
|
||
updateConnectionInfo();
|
||
}
|
||
|
||
function displayMessage(data) {
|
||
const className = data.type.replace(/_/g, "-");
|
||
const messageContent =
|
||
typeof data.data === "object"
|
||
? JSON.stringify(data.data, null, 2)
|
||
: sanitizeInput(String(data.data));
|
||
|
||
addMessage(
|
||
`<div class="message-type">📋 ${data.type.toUpperCase()}</div><pre>${messageContent}</pre>`,
|
||
className,
|
||
`Message ID: ${data.message_id || "N/A"}`
|
||
);
|
||
}
|
||
|
||
// Enhanced addMessage dengan XSS protection
|
||
function addMessage(content, className = "", details = "") {
|
||
const div = document.createElement("div");
|
||
div.className = "message " + className;
|
||
|
||
const timestamp = new Date().toLocaleTimeString();
|
||
|
||
// Create safe HTML structure
|
||
const timeSpan = document.createElement("div");
|
||
timeSpan.className = "message-time";
|
||
timeSpan.textContent = timestamp;
|
||
|
||
const contentDiv = document.createElement("div");
|
||
contentDiv.innerHTML = content; // Content sudah di-sanitize sebelumnya
|
||
|
||
div.appendChild(timeSpan);
|
||
div.appendChild(contentDiv);
|
||
|
||
if (details) {
|
||
const detailsSmall = document.createElement("small");
|
||
detailsSmall.style.cssText =
|
||
"color: #6c757d; display: block; margin-top: 5px;";
|
||
detailsSmall.textContent = details;
|
||
div.appendChild(detailsSmall);
|
||
}
|
||
|
||
const messagesContainer = document.getElementById("messages");
|
||
messagesContainer.appendChild(div);
|
||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||
|
||
// Limit DOM messages untuk performance
|
||
const messages = messagesContainer.children;
|
||
if (messages.length > MAX_MESSAGES) {
|
||
for (let i = 0; i < messages.length - MAX_MESSAGES; i++) {
|
||
messagesContainer.removeChild(messages[0]);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Tab switching functions
|
||
function switchTab(tabName) {
|
||
// Hide all tab contents
|
||
document.querySelectorAll(".tab-content").forEach((tab) => {
|
||
tab.classList.remove("active");
|
||
});
|
||
|
||
// Remove active class from all tabs
|
||
document.querySelectorAll(".tab").forEach((tab) => {
|
||
tab.classList.remove("active");
|
||
});
|
||
|
||
// Show selected tab content
|
||
document.getElementById(tabName + "-tab").classList.add("active");
|
||
|
||
// Add active class to clicked tab
|
||
event.target.classList.add("active");
|
||
}
|
||
|
||
// Status management functions
|
||
function updateConnectionStatus(status) {
|
||
const indicator = document.getElementById("statusIndicator");
|
||
const statusText = document.getElementById("connectionStatus");
|
||
const loadingIndicator = document.getElementById("loadingIndicator");
|
||
|
||
// Clear existing classes
|
||
indicator.className = "status-indicator";
|
||
loadingIndicator.innerHTML = "";
|
||
|
||
switch (status) {
|
||
case "connected":
|
||
indicator.classList.add("status-connected");
|
||
statusText.textContent = "Connected";
|
||
document.getElementById("reconnectInfo").textContent = "Not needed";
|
||
break;
|
||
case "disconnected":
|
||
indicator.classList.add("status-disconnected");
|
||
statusText.textContent = "Disconnected";
|
||
connectionHealth = "poor";
|
||
updateHealthIndicator();
|
||
break;
|
||
case "connecting":
|
||
indicator.classList.add("status-connecting");
|
||
statusText.textContent = "Connecting...";
|
||
loadingIndicator.innerHTML =
|
||
'<div class="loading-indicator"></div>';
|
||
break;
|
||
case "error":
|
||
indicator.classList.add("status-disconnected");
|
||
statusText.textContent = "Error";
|
||
connectionHealth = "poor";
|
||
updateHealthIndicator();
|
||
break;
|
||
}
|
||
}
|
||
|
||
function updateConnectionInfo() {
|
||
document.getElementById("currentClientId").textContent =
|
||
clientId || "Not connected";
|
||
// Update other connection info displays if needed
|
||
}
|
||
|
||
// =============================================================================
|
||
// EVENT LISTENERS & INITIALIZATION
|
||
// =============================================================================
|
||
|
||
// Enhanced cleanup on page unload
|
||
window.addEventListener("beforeunload", function () {
|
||
isManualDisconnect = true;
|
||
cleanupConnection();
|
||
});
|
||
|
||
// Auto-connect dengan delay untuk stability
|
||
window.addEventListener("load", function () {
|
||
setTimeout(() => {
|
||
connectWebSocket();
|
||
}, 500);
|
||
});
|
||
|
||
// Visibility API untuk pause/resume saat tab tidak aktif
|
||
document.addEventListener("visibilitychange", function () {
|
||
if (document.hidden) {
|
||
// Tab tidak aktif, reduce heartbeat frequency
|
||
if (heartbeatInterval) {
|
||
clearInterval(heartbeatInterval);
|
||
heartbeatInterval = setInterval(() => {
|
||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||
sendHeartbeat();
|
||
}
|
||
}, HEARTBEAT_INTERVAL * 2); // Double interval saat hidden
|
||
}
|
||
} else {
|
||
// Tab aktif kembali, restore normal heartbeat
|
||
if (
|
||
document.getElementById("heartbeatCheck").checked &&
|
||
ws &&
|
||
ws.readyState === WebSocket.OPEN
|
||
) {
|
||
startHeartbeat();
|
||
}
|
||
}
|
||
});
|
||
|
||
// Keyboard shortcuts
|
||
document.addEventListener("keydown", function (event) {
|
||
// Ctrl+Enter untuk send message
|
||
if (event.ctrlKey && event.key === "Enter") {
|
||
const activeTab = document.querySelector(".tab-content.active").id;
|
||
if (activeTab === "messaging-tab") {
|
||
sendMessage();
|
||
}
|
||
}
|
||
|
||
// Escape untuk disconnect
|
||
if (event.key === "Escape" && event.ctrlKey) {
|
||
disconnect();
|
||
}
|
||
});
|
||
|
||
// Initialize stats
|
||
updateStats();
|
||
updateMessageCount();
|
||
|
||
// Auto-refresh online users setiap 30 detik jika connected
|
||
setInterval(() => {
|
||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||
// Only refresh if monitoring tab is active
|
||
const activeTab = document.querySelector(".tab.active").textContent;
|
||
if (activeTab.includes("Monitoring")) {
|
||
refreshOnlineUsers();
|
||
}
|
||
}
|
||
}, 30000);
|
||
|
||
console.log("🌐 Enhanced WebSocket Client loaded successfully!");
|
||
addMessage(
|
||
"🚀 Enhanced WebSocket Client initialized",
|
||
"info",
|
||
"All systems ready. Client will auto-connect in 500ms."
|
||
);
|
||
</script>
|
||
</body>
|
||
</html>
|