522 lines
16 KiB
HTML
522 lines
16 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>WebSocket Test - Carbon Server</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
:root {
|
|
--primary: #3b82f6;
|
|
--primary-hover: #2563eb;
|
|
--success: #10b981;
|
|
--danger: #ef4444;
|
|
--bg: #0f172a;
|
|
--surface: #1e293b;
|
|
--border: #334155;
|
|
--text: #f1f5f9;
|
|
--text-dim: #94a3b8;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
min-height: 100vh;
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
header {
|
|
text-align: center;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 2rem;
|
|
font-weight: 700;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.subtitle {
|
|
color: var(--text-dim);
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 1.5rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
.card {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 0.75rem;
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.card-title {
|
|
font-size: 1.1rem;
|
|
font-weight: 600;
|
|
margin-bottom: 1rem;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.status {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
padding: 0.75rem 1rem;
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border-radius: 0.5rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.status-dot {
|
|
width: 0.75rem;
|
|
height: 0.75rem;
|
|
border-radius: 50%;
|
|
background: var(--danger);
|
|
animation: pulse 2s infinite;
|
|
}
|
|
|
|
.status-dot.connected {
|
|
background: var(--success);
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.6; }
|
|
}
|
|
|
|
.input-group {
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
label {
|
|
display: block;
|
|
margin-bottom: 0.4rem;
|
|
font-size: 0.9rem;
|
|
color: var(--text-dim);
|
|
}
|
|
|
|
input, select, textarea {
|
|
width: 100%;
|
|
padding: 0.65rem;
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border: 1px solid var(--border);
|
|
border-radius: 0.5rem;
|
|
color: var(--text);
|
|
font-size: 0.95rem;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
input:focus, select:focus, textarea:focus {
|
|
outline: none;
|
|
border-color: var(--primary);
|
|
background: rgba(255, 255, 255, 0.08);
|
|
}
|
|
|
|
textarea {
|
|
resize: vertical;
|
|
min-height: 80px;
|
|
font-family: 'Consolas', monospace;
|
|
}
|
|
|
|
.btn {
|
|
padding: 0.65rem 1.25rem;
|
|
border: none;
|
|
border-radius: 0.5rem;
|
|
font-size: 0.95rem;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: var(--primary);
|
|
color: white;
|
|
}
|
|
|
|
.btn-primary:hover:not(:disabled) {
|
|
background: var(--primary-hover);
|
|
}
|
|
|
|
.btn-success {
|
|
background: var(--success);
|
|
color: white;
|
|
}
|
|
|
|
.btn-danger {
|
|
background: var(--danger);
|
|
color: white;
|
|
}
|
|
|
|
.btn:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.btn-block {
|
|
width: 100%;
|
|
justify-content: center;
|
|
}
|
|
|
|
.btn-group {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.messages {
|
|
background: rgba(0, 0, 0, 0.2);
|
|
border-radius: 0.5rem;
|
|
padding: 1rem;
|
|
height: 400px;
|
|
overflow-y: auto;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.message {
|
|
margin-bottom: 0.75rem;
|
|
padding: 0.75rem;
|
|
border-radius: 0.5rem;
|
|
font-size: 0.9rem;
|
|
animation: fadeIn 0.3s;
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
from { opacity: 0; transform: translateY(5px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
|
|
.message.sent {
|
|
background: rgba(59, 130, 246, 0.2);
|
|
border-left: 3px solid var(--primary);
|
|
}
|
|
|
|
.message.received {
|
|
background: rgba(16, 185, 129, 0.2);
|
|
border-left: 3px solid var(--success);
|
|
}
|
|
|
|
.message.system {
|
|
background: rgba(148, 163, 184, 0.15);
|
|
border-left: 3px solid var(--text-dim);
|
|
text-align: center;
|
|
}
|
|
|
|
.message-time {
|
|
font-size: 0.75rem;
|
|
color: var(--text-dim);
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.message-content {
|
|
word-break: break-word;
|
|
}
|
|
|
|
.stats {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
|
gap: 1rem;
|
|
}
|
|
|
|
.stat {
|
|
text-align: center;
|
|
padding: 1rem;
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border-radius: 0.5rem;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 1.75rem;
|
|
font-weight: 700;
|
|
color: var(--primary);
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 0.8rem;
|
|
color: var(--text-dim);
|
|
margin-top: 0.25rem;
|
|
}
|
|
|
|
.messages::-webkit-scrollbar {
|
|
width: 6px;
|
|
}
|
|
|
|
.messages::-webkit-scrollbar-track {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.messages::-webkit-scrollbar-thumb {
|
|
background: var(--border);
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.messages::-webkit-scrollbar-thumb:hover {
|
|
background: var(--primary);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<header>
|
|
<h1>🔌 WebSocket Test</h1>
|
|
<p class="subtitle">Carbon Server</p>
|
|
</header>
|
|
|
|
<div class="grid">
|
|
<!-- Connection -->
|
|
<div class="card">
|
|
<h2 class="card-title">⚡ Connection</h2>
|
|
|
|
<div class="status">
|
|
<span class="status-dot" id="statusDot"></span>
|
|
<span id="statusText">Disconnected</span>
|
|
</div>
|
|
|
|
<div class="input-group">
|
|
<label>WebSocket URL</label>
|
|
<input type="text" id="wsUrl" value="" placeholder="ws://localhost:8080/">
|
|
</div>
|
|
|
|
<button class="btn btn-success btn-block" id="connectBtn">Connect</button>
|
|
|
|
<div style="margin-top: 1.5rem;">
|
|
<h3 class="card-title">🧪 Quick Tests</h3>
|
|
<div class="btn-group">
|
|
<button class="btn btn-primary" id="pingBtn" disabled>Ping</button>
|
|
<button class="btn btn-primary" id="echoBtn" disabled>Echo</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stats -->
|
|
<div class="card">
|
|
<h2 class="card-title">📊 Statistics</h2>
|
|
<div class="stats">
|
|
<div class="stat">
|
|
<div class="stat-value" id="statSent">0</div>
|
|
<div class="stat-label">Sent</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="stat-value" id="statReceived">0</div>
|
|
<div class="stat-label">Received</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="stat-value" id="statLatency">--</div>
|
|
<div class="stat-label">Latency (ms)</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Messages -->
|
|
<div class="card">
|
|
<h2 class="card-title">💬 Messages</h2>
|
|
|
|
<div class="messages" id="messages">
|
|
<div class="message system">
|
|
<div class="message-content">Ready to connect...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="input-group">
|
|
<textarea id="messageInput" placeholder="Type a message..." disabled></textarea>
|
|
</div>
|
|
|
|
<div class="btn-group">
|
|
<button class="btn btn-primary" id="sendBtn" disabled>Send</button>
|
|
<button class="btn btn-danger" id="clearBtn">Clear</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let ws = null;
|
|
let stats = { sent: 0, received: 0 };
|
|
let pingTime = null;
|
|
|
|
const elements = {
|
|
statusDot: document.getElementById('statusDot'),
|
|
statusText: document.getElementById('statusText'),
|
|
wsUrl: document.getElementById('wsUrl'),
|
|
connectBtn: document.getElementById('connectBtn'),
|
|
messages: document.getElementById('messages'),
|
|
messageInput: document.getElementById('messageInput'),
|
|
sendBtn: document.getElementById('sendBtn'),
|
|
clearBtn: document.getElementById('clearBtn'),
|
|
pingBtn: document.getElementById('pingBtn'),
|
|
echoBtn: document.getElementById('echoBtn'),
|
|
statSent: document.getElementById('statSent'),
|
|
statReceived: document.getElementById('statReceived'),
|
|
statLatency: document.getElementById('statLatency')
|
|
};
|
|
|
|
// Auto-detect WebSocket URL based on page protocol
|
|
function getDefaultWebSocketUrl() {
|
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
const host = window.location.hostname;
|
|
const port = window.location.port || (window.location.protocol === 'https:' ? '443' : '8080');
|
|
return `${protocol}//${host}:${port}/`;
|
|
}
|
|
|
|
// Initialize URL on page load
|
|
window.addEventListener('DOMContentLoaded', () => {
|
|
if (!elements.wsUrl.value) {
|
|
elements.wsUrl.value = getDefaultWebSocketUrl();
|
|
}
|
|
});
|
|
|
|
function addMessage(content, type = 'system') {
|
|
const msg = document.createElement('div');
|
|
msg.className = `message ${type}`;
|
|
msg.innerHTML = `
|
|
<div class="message-time">${new Date().toLocaleTimeString()}</div>
|
|
<div class="message-content">${content}</div>
|
|
`;
|
|
elements.messages.appendChild(msg);
|
|
elements.messages.scrollTop = elements.messages.scrollHeight;
|
|
}
|
|
|
|
function updateStatus(connected) {
|
|
if (connected) {
|
|
elements.statusDot.classList.add('connected');
|
|
elements.statusText.textContent = 'Connected';
|
|
elements.connectBtn.textContent = 'Disconnect';
|
|
elements.connectBtn.className = 'btn btn-danger btn-block';
|
|
elements.messageInput.disabled = false;
|
|
elements.sendBtn.disabled = false;
|
|
elements.pingBtn.disabled = false;
|
|
elements.echoBtn.disabled = false;
|
|
} else {
|
|
elements.statusDot.classList.remove('connected');
|
|
elements.statusText.textContent = 'Disconnected';
|
|
elements.connectBtn.textContent = 'Connect';
|
|
elements.connectBtn.className = 'btn btn-success btn-block';
|
|
elements.messageInput.disabled = true;
|
|
elements.sendBtn.disabled = true;
|
|
elements.pingBtn.disabled = true;
|
|
elements.echoBtn.disabled = true;
|
|
}
|
|
}
|
|
|
|
function updateStats() {
|
|
elements.statSent.textContent = stats.sent;
|
|
elements.statReceived.textContent = stats.received;
|
|
}
|
|
|
|
function connect() {
|
|
const url = elements.wsUrl.value.trim();
|
|
if (!url) return;
|
|
|
|
try {
|
|
ws = new WebSocket(url);
|
|
|
|
ws.onopen = () => {
|
|
updateStatus(true);
|
|
addMessage('✅ Connected', 'system');
|
|
};
|
|
|
|
ws.onmessage = (event) => {
|
|
stats.received++;
|
|
updateStats();
|
|
|
|
if (pingTime && event.data.includes('pong')) {
|
|
const latency = Date.now() - pingTime;
|
|
elements.statLatency.textContent = latency;
|
|
addMessage(`Pong! Latency: ${latency}ms`, 'received');
|
|
pingTime = null;
|
|
} else {
|
|
addMessage(event.data, 'received');
|
|
}
|
|
};
|
|
|
|
ws.onerror = () => {
|
|
addMessage('❌ Connection error', 'system');
|
|
};
|
|
|
|
ws.onclose = () => {
|
|
updateStatus(false);
|
|
addMessage('Connection closed', 'system');
|
|
ws = null;
|
|
};
|
|
} catch (error) {
|
|
addMessage(`❌ Failed: ${error.message}`, 'system');
|
|
}
|
|
}
|
|
|
|
function disconnect() {
|
|
if (ws) {
|
|
ws.close();
|
|
ws = null;
|
|
}
|
|
}
|
|
|
|
function sendMessage(text) {
|
|
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
|
|
ws.send(text);
|
|
stats.sent++;
|
|
updateStats();
|
|
addMessage(text, 'sent');
|
|
}
|
|
|
|
elements.connectBtn.addEventListener('click', () => {
|
|
ws ? disconnect() : connect();
|
|
});
|
|
|
|
elements.sendBtn.addEventListener('click', () => {
|
|
const text = elements.messageInput.value.trim();
|
|
if (text) {
|
|
sendMessage(text);
|
|
elements.messageInput.value = '';
|
|
}
|
|
});
|
|
|
|
elements.messageInput.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault();
|
|
elements.sendBtn.click();
|
|
}
|
|
});
|
|
|
|
elements.clearBtn.addEventListener('click', () => {
|
|
elements.messages.innerHTML = '<div class="message system"><div class="message-content">Log cleared</div></div>';
|
|
});
|
|
|
|
elements.pingBtn.addEventListener('click', () => {
|
|
pingTime = Date.now();
|
|
sendMessage(JSON.stringify({ type: 'ping', timestamp: pingTime }));
|
|
});
|
|
|
|
elements.echoBtn.addEventListener('click', () => {
|
|
sendMessage(JSON.stringify({ type: 'echo', message: 'Hello, Carbon!' }));
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|