Files
antrean-anjungan/client.html
2025-10-23 04:25:28 +07:00

2252 lines
64 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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 = {
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#x27;",
"&": "&amp;",
};
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>