<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>Sockudo Multinode Test Client</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
.message-log {
overflow-y: auto;
min-height: 0;
}
.message-log::-webkit-scrollbar {
width: 8px;
}
.message-log::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.message-log::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
.message-log::-webkit-scrollbar-thumb:hover {
background: #555;
}
</style>
</head>
<body class="bg-gray-100 min-h-screen">
<div class="flex h-screen">
<div class="w-80 bg-white shadow-lg p-4 overflow-y-auto">
<h1 class="text-lg font-bold text-gray-800 mb-4">Sockudo Test Client</h1>
<div class="mb-6">
<h2 class="text-base font-semibold mb-3 text-gray-700">Configuration</h2>
<div class="mb-4">
<h3 class="font-medium text-gray-600 text-sm mb-2">Node 1</h3>
<div class="space-y-2">
<input type="text" id="node1-scheme" value="wss" placeholder="wss"
class="w-full px-2 py-1 border rounded text-xs">
<input type="text" id="node1-host" value="node1.ghslocal.com" placeholder="Host"
class="w-full px-2 py-1 border rounded text-xs">
<input type="text" id="node1-port" value="444" placeholder="Port"
class="w-full px-2 py-1 border rounded text-xs">
</div>
</div>
<div class="mb-4">
<h3 class="font-medium text-gray-600 text-sm mb-2">Node 2</h3>
<div class="space-y-2">
<input type="text" id="node2-scheme" value="wss" placeholder="wss"
class="w-full px-2 py-1 border rounded text-xs">
<input type="text" id="node2-host" value="node2.ghslocal.com" placeholder="Host"
class="w-full px-2 py-1 border rounded text-xs">
<input type="text" id="node2-port" value="444" placeholder="Port"
class="w-full px-2 py-1 border rounded text-xs">
</div>
</div>
<div class="space-y-3">
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">App Key</label>
<input type="text" id="app-key" value="app-key" placeholder="app-key"
class="w-full px-2 py-1 border rounded text-xs">
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">Channel</label>
<input type="text" id="channel" value="test-channel" placeholder="Channel"
class="w-full px-2 py-1 border rounded text-xs">
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">Event</label>
<input type="text" id="event-name" value="client-message" placeholder="Event"
class="w-full px-2 py-1 border rounded text-xs">
</div>
</div>
</div>
<div class="text-xs text-gray-500 bg-gray-50 p-3 rounded">
<p class="mb-2"><strong>Redis Recovery Test:</strong></p>
<p class="mb-1">1. Stop Redis container</p>
<p class="mb-1">2. Try sending messages (will fail)</p>
<p class="mb-1">3. Start Redis container</p>
<p>4. Send messages again (should work)</p>
</div>
</div>
<div class="flex-1 p-4 flex flex-col h-screen">
<div class="bg-white rounded-lg shadow-md p-4 mb-4 flex-shrink-0">
<div class="flex gap-2">
<select id="broadcast-node" class="px-3 py-2 border rounded-md text-sm">
<option value="1">Node 1</option>
<option value="2">Node 2</option>
</select>
<input type="text" id="broadcast-message" placeholder="Broadcast message via API..."
class="flex-1 px-3 py-2 border rounded-md text-sm">
<button onclick="broadcastMessage()"
class="bg-indigo-500 text-white px-6 py-2 rounded-md hover:bg-indigo-600 transition text-sm">
Broadcast
</button>
</div>
</div>
<div class="grid grid-cols-2 gap-4 flex-1 min-h-0">
<div class="bg-white rounded-lg shadow-md p-4 flex flex-col min-h-0">
<div class="flex justify-between items-center mb-3 flex-shrink-0">
<h2 class="text-base font-semibold text-gray-700">Node 1</h2>
<div class="flex items-center gap-2">
<span id="node1-status" class="w-2 h-2 bg-red-500 rounded-full"></span>
<span id="node1-status-text" class="text-xs text-gray-600">Disconnected</span>
</div>
</div>
<div class="flex flex-col flex-1 min-h-0">
<div class="flex gap-1 mb-2 flex-shrink-0">
<button id="node1-connect" onclick="toggleConnection(1)"
class="flex-1 bg-blue-500 text-white px-2 py-1 rounded text-xs hover:bg-blue-600 transition">
Connect
</button>
<button onclick="subscribeToChannel(1)"
class="flex-1 bg-green-500 text-white px-2 py-1 rounded text-xs hover:bg-green-600 transition disabled:opacity-50 disabled:cursor-not-allowed"
id="node1-subscribe">
Subscribe
</button>
</div>
<div class="message-log bg-gray-50 rounded p-2 text-xs flex-1 mb-2 min-h-0" id="node1-messages">
<div class="text-gray-400">Not connected</div>
</div>
<div class="flex gap-1 flex-shrink-0">
<input type="text" id="node1-message" placeholder="Type a message..."
class="flex-1 px-2 py-1 border rounded text-xs">
<button onclick="sendMessage(1)"
class="bg-purple-500 text-white px-2 py-1 rounded text-xs hover:bg-purple-600 transition disabled:opacity-50"
id="node1-send" disabled>
Send
</button>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow-md p-4 flex flex-col min-h-0">
<div class="flex justify-between items-center mb-3 flex-shrink-0">
<h2 class="text-base font-semibold text-gray-700">Node 2</h2>
<div class="flex items-center gap-2">
<span id="node2-status" class="w-2 h-2 bg-red-500 rounded-full"></span>
<span id="node2-status-text" class="text-xs text-gray-600">Disconnected</span>
</div>
</div>
<div class="flex flex-col flex-1 min-h-0">
<div class="flex gap-1 mb-2 flex-shrink-0">
<button id="node2-connect" onclick="toggleConnection(2)"
class="flex-1 bg-blue-500 text-white px-2 py-1 rounded text-xs hover:bg-blue-600 transition">
Connect
</button>
<button onclick="subscribeToChannel(2)"
class="flex-1 bg-green-500 text-white px-2 py-1 rounded text-xs hover:bg-green-600 transition disabled:opacity-50 disabled:cursor-not-allowed"
id="node2-subscribe">
Subscribe
</button>
</div>
<div class="message-log bg-gray-50 rounded p-2 text-xs flex-1 mb-2 min-h-0" id="node2-messages">
<div class="text-gray-400">Not connected</div>
</div>
<div class="flex gap-1 flex-shrink-0">
<input type="text" id="node2-message" placeholder="Type a message..."
class="flex-1 px-2 py-1 border rounded text-xs">
<button onclick="sendMessage(2)"
class="bg-purple-500 text-white px-2 py-1 rounded text-xs hover:bg-purple-600 transition disabled:opacity-50"
id="node2-send" disabled>
Send
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
let connections = {
1: null,
2: null
};
let subscribed = {
1: false,
2: false
};
function saveConfig() {
const config = {
node1: {
scheme: document.getElementById('node1-scheme').value,
host: document.getElementById('node1-host').value,
port: document.getElementById('node1-port').value
},
node2: {
scheme: document.getElementById('node2-scheme').value,
host: document.getElementById('node2-host').value,
port: document.getElementById('node2-port').value
},
appKey: document.getElementById('app-key').value,
channel: document.getElementById('channel').value,
eventName: document.getElementById('event-name').value
};
localStorage.setItem('sockudoTestConfig', JSON.stringify(config));
console.log('Configuration saved:', config);
}
function loadConfig() {
const saved = localStorage.getItem('sockudoTestConfig');
console.log('Loading configuration:', saved);
if (saved) {
try {
const config = JSON.parse(saved);
if (config.node1) {
document.getElementById('node1-scheme').value = config.node1.scheme || 'wss';
document.getElementById('node1-host').value = config.node1.host || 'node1.ghslocal.com';
document.getElementById('node1-port').value = config.node1.port || '444';
}
if (config.node2) {
document.getElementById('node2-scheme').value = config.node2.scheme || 'wss';
document.getElementById('node2-host').value = config.node2.host || 'node2.ghslocal.com';
document.getElementById('node2-port').value = config.node2.port || '444';
}
if (config.appKey) document.getElementById('app-key').value = config.appKey;
if (config.channel) document.getElementById('channel').value = config.channel;
if (config.eventName) document.getElementById('event-name').value = config.eventName;
console.log('Configuration loaded successfully');
} catch (e) {
console.warn('Failed to load saved configuration:', e);
}
} else {
console.log('No saved configuration found');
}
}
function getNodeConfig(nodeNum) {
const scheme = document.getElementById(`node${nodeNum}-scheme`).value;
const host = document.getElementById(`node${nodeNum}-host`).value;
const port = document.getElementById(`node${nodeNum}-port`).value;
const appKey = document.getElementById('app-key').value;
return `${scheme}://${host}:${port}/app/${appKey}?protocol=7&client=js`;
}
function updateStatus(nodeNum, connected) {
const statusDot = document.getElementById(`node${nodeNum}-status`);
const statusText = document.getElementById(`node${nodeNum}-status-text`);
const connectBtn = document.getElementById(`node${nodeNum}-connect`);
const subscribeBtn = document.getElementById(`node${nodeNum}-subscribe`);
const sendBtn = document.getElementById(`node${nodeNum}-send`);
if (connected) {
statusDot.className = 'w-2 h-2 bg-green-500 rounded-full';
statusText.textContent = 'Connected';
connectBtn.textContent = 'Disconnect';
connectBtn.className = 'flex-1 bg-red-500 text-white px-2 py-1 rounded text-xs hover:bg-red-600 transition';
subscribeBtn.disabled = false;
sendBtn.disabled = !subscribed[nodeNum];
} else {
statusDot.className = 'w-2 h-2 bg-red-500 rounded-full';
statusText.textContent = 'Disconnected';
connectBtn.textContent = 'Connect';
connectBtn.className = 'flex-1 bg-blue-500 text-white px-2 py-1 rounded text-xs hover:bg-blue-600 transition';
subscribeBtn.disabled = true;
sendBtn.disabled = true;
subscribed[nodeNum] = false;
}
}
function addMessage(nodeNum, message, type = 'received') {
const messagesDiv = document.getElementById(`node${nodeNum}-messages`);
const timestamp = new Date().toLocaleTimeString();
const colors = {
'received': 'bg-blue-100 text-blue-800',
'sent': 'bg-green-100 text-green-800',
'system': 'bg-gray-100 text-gray-700',
'error': 'bg-red-100 text-red-800'
};
const messageHtml = `
<div class="mb-2 p-2 rounded ${colors[type]}">
<div class="text-xs opacity-75">${timestamp}</div>
<div class="text-sm font-mono break-all">${message}</div>
</div>
`;
messagesDiv.innerHTML = messagesDiv.innerHTML.replace('<div class="text-gray-400">Not connected</div>', '');
messagesDiv.innerHTML += messageHtml;
setTimeout(() => {
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}, 10);
}
function toggleConnection(nodeNum) {
if (connections[nodeNum]) {
connections[nodeNum].close();
connections[nodeNum] = null;
updateStatus(nodeNum, false);
document.getElementById(`node${nodeNum}-messages`).innerHTML = '<div class="text-gray-400">Not connected</div>';
} else {
connect(nodeNum);
}
}
function connect(nodeNum) {
const url = getNodeConfig(nodeNum);
addMessage(nodeNum, `Connecting to ${url}...`, 'system');
const ws = new WebSocket(url);
ws.onopen = () => {
connections[nodeNum] = ws;
updateStatus(nodeNum, true);
addMessage(nodeNum, 'Connected successfully!', 'system');
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.event === 'pusher:connection_established') {
addMessage(nodeNum, `Connection established: ${JSON.stringify(data.data)}`, 'system');
} else if (data.event === 'pusher_internal:subscription_succeeded') {
addMessage(nodeNum, `Subscribed to channel: ${data.channel}`, 'system');
subscribed[nodeNum] = true;
document.getElementById(`node${nodeNum}-send`).disabled = false;
} else {
addMessage(nodeNum, `${data.event}: ${JSON.stringify(data.data)}`, 'received');
}
};
ws.onerror = (error) => {
addMessage(nodeNum, `Error: ${error.message || 'Connection failed'}`, 'error');
};
ws.onclose = () => {
connections[nodeNum] = null;
updateStatus(nodeNum, false);
addMessage(nodeNum, 'Connection closed', 'system');
};
}
function subscribeToChannel(nodeNum) {
if (!connections[nodeNum]) return;
const channel = document.getElementById('channel').value;
const message = {
event: 'pusher:subscribe',
data: { channel: channel }
};
connections[nodeNum].send(JSON.stringify(message));
addMessage(nodeNum, `Subscribing to channel: ${channel}`, 'system');
}
async function sendMessage(nodeNum) {
const messageInput = document.getElementById(`node${nodeNum}-message`);
const message = messageInput.value.trim();
if (!message) return;
await sendApiMessage(nodeNum, message, `node${nodeNum}`);
messageInput.value = '';
}
function generateMD5(text) {
function md5(string) {
function RotateLeft(lValue, lCount) {
return (lValue << lCount) | (lValue >>> (32 - lCount));
}
function AddUnsigned(lX, lY) {
var lX4, lY4, lX8, lY8, lResult;
lX8 = (lX & 0x80000000);
lY8 = (lY & 0x80000000);
lX4 = (lX & 0x40000000);
lY4 = (lY & 0x40000000);
lResult = (lX & 0x3FFFFFFF) + (lY & 0x3FFFFFFF);
if (lX4 & lY4) {
return (lResult ^ 0x80000000 ^ lX8 ^ lY8);
}
if (lX4 | lY4) {
if (lResult & 0x40000000) {
return (lResult ^ 0xC0000000 ^ lX8 ^ lY8);
} else {
return (lResult ^ 0x40000000 ^ lX8 ^ lY8);
}
} else {
return (lResult ^ lX8 ^ lY8);
}
}
function F(x, y, z) { return (x & y) | ((~x) & z); }
function G(x, y, z) { return (x & z) | (y & (~z)); }
function H(x, y, z) { return (x ^ y ^ z); }
function I(x, y, z) { return (y ^ (x | (~z))); }
function FF(a, b, c, d, x, s, ac) {
a = AddUnsigned(a, AddUnsigned(AddUnsigned(F(b, c, d), x), ac));
return AddUnsigned(RotateLeft(a, s), b);
};
function GG(a, b, c, d, x, s, ac) {
a = AddUnsigned(a, AddUnsigned(AddUnsigned(G(b, c, d), x), ac));
return AddUnsigned(RotateLeft(a, s), b);
};
function HH(a, b, c, d, x, s, ac) {
a = AddUnsigned(a, AddUnsigned(AddUnsigned(H(b, c, d), x), ac));
return AddUnsigned(RotateLeft(a, s), b);
};
function II(a, b, c, d, x, s, ac) {
a = AddUnsigned(a, AddUnsigned(AddUnsigned(I(b, c, d), x), ac));
return AddUnsigned(RotateLeft(a, s), b);
};
function ConvertToWordArray(string) {
var lWordCount;
var lMessageLength = string.length;
var lNumberOfWords_temp1 = lMessageLength + 8;
var lNumberOfWords_temp2 = (lNumberOfWords_temp1 - (lNumberOfWords_temp1 % 64)) / 64;
var lNumberOfWords = (lNumberOfWords_temp2 + 1) * 16;
var lWordArray = Array(lNumberOfWords - 1);
var lBytePosition = 0;
var lByteCount = 0;
while (lByteCount < lMessageLength) {
lWordCount = (lByteCount - (lByteCount % 4)) / 4;
lBytePosition = (lByteCount % 4) * 8;
lWordArray[lWordCount] = (lWordArray[lWordCount] | (string.charCodeAt(lByteCount) << lBytePosition));
lByteCount++;
}
lWordCount = (lByteCount - (lByteCount % 4)) / 4;
lBytePosition = (lByteCount % 4) * 8;
lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80 << lBytePosition);
lWordArray[lNumberOfWords - 2] = lMessageLength << 3;
lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29;
return lWordArray;
};
function WordToHex(lValue) {
var WordToHexValue = "", WordToHexValue_temp = "", lByte, lCount;
for (lCount = 0; lCount <= 3; lCount++) {
lByte = (lValue >>> (lCount * 8)) & 255;
WordToHexValue_temp = "0" + lByte.toString(16);
WordToHexValue = WordToHexValue + WordToHexValue_temp.substr(WordToHexValue_temp.length - 2, 2);
}
return WordToHexValue;
};
function Utf8Encode(string) {
string = string.replace(/\r\n/g, "\n");
var utftext = "";
for (var n = 0; n < string.length; n++) {
var c = string.charCodeAt(n);
if (c < 128) {
utftext += String.fromCharCode(c);
}
else if ((c > 127) && (c < 2048)) {
utftext += String.fromCharCode((c >> 6) | 192);
utftext += String.fromCharCode((c & 63) | 128);
}
else {
utftext += String.fromCharCode((c >> 12) | 224);
utftext += String.fromCharCode(((c >> 6) & 63) | 128);
utftext += String.fromCharCode((c & 63) | 128);
}
}
return utftext;
};
var x = Array();
var k, AA, BB, CC, DD, a, b, c, d;
var S11 = 7, S12 = 12, S13 = 17, S14 = 22;
var S21 = 5, S22 = 9, S23 = 14, S24 = 20;
var S31 = 4, S32 = 11, S33 = 16, S34 = 23;
var S41 = 6, S42 = 10, S43 = 15, S44 = 21;
string = Utf8Encode(string);
x = ConvertToWordArray(string);
a = 0x67452301; b = 0xEFCDAB89; c = 0x98BADCFE; d = 0x10325476;
for (k = 0; k < x.length; k += 16) {
AA = a; BB = b; CC = c; DD = d;
a = FF(a, b, c, d, x[k + 0], S11, 0xD76AA478);
d = FF(d, a, b, c, x[k + 1], S12, 0xE8C7B756);
c = FF(c, d, a, b, x[k + 2], S13, 0x242070DB);
b = FF(b, c, d, a, x[k + 3], S14, 0xC1BDCEEE);
a = FF(a, b, c, d, x[k + 4], S11, 0xF57C0FAF);
d = FF(d, a, b, c, x[k + 5], S12, 0x4787C62A);
c = FF(c, d, a, b, x[k + 6], S13, 0xA8304613);
b = FF(b, c, d, a, x[k + 7], S14, 0xFD469501);
a = FF(a, b, c, d, x[k + 8], S11, 0x698098D8);
d = FF(d, a, b, c, x[k + 9], S12, 0x8B44F7AF);
c = FF(c, d, a, b, x[k + 10], S13, 0xFFFF5BB1);
b = FF(b, c, d, a, x[k + 11], S14, 0x895CD7BE);
a = FF(a, b, c, d, x[k + 12], S11, 0x6B901122);
d = FF(d, a, b, c, x[k + 13], S12, 0xFD987193);
c = FF(c, d, a, b, x[k + 14], S13, 0xA679438E);
b = FF(b, c, d, a, x[k + 15], S14, 0x49B40821);
a = GG(a, b, c, d, x[k + 1], S21, 0xF61E2562);
d = GG(d, a, b, c, x[k + 6], S22, 0xC040B340);
c = GG(c, d, a, b, x[k + 11], S23, 0x265E5A51);
b = GG(b, c, d, a, x[k + 0], S24, 0xE9B6C7AA);
a = GG(a, b, c, d, x[k + 5], S21, 0xD62F105D);
d = GG(d, a, b, c, x[k + 10], S22, 0x2441453);
c = GG(c, d, a, b, x[k + 15], S23, 0xD8A1E681);
b = GG(b, c, d, a, x[k + 4], S24, 0xE7D3FBC8);
a = GG(a, b, c, d, x[k + 9], S21, 0x21E1CDE6);
d = GG(d, a, b, c, x[k + 14], S22, 0xC33707D6);
c = GG(c, d, a, b, x[k + 3], S23, 0xF4D50D87);
b = GG(b, c, d, a, x[k + 8], S24, 0x455A14ED);
a = GG(a, b, c, d, x[k + 13], S21, 0xA9E3E905);
d = GG(d, a, b, c, x[k + 2], S22, 0xFCEFA3F8);
c = GG(c, d, a, b, x[k + 7], S23, 0x676F02D9);
b = GG(b, c, d, a, x[k + 12], S24, 0x8D2A4C8A);
a = HH(a, b, c, d, x[k + 5], S31, 0xFFFA3942);
d = HH(d, a, b, c, x[k + 8], S32, 0x8771F681);
c = HH(c, d, a, b, x[k + 11], S33, 0x6D9D6122);
b = HH(b, c, d, a, x[k + 14], S34, 0xFDE5380C);
a = HH(a, b, c, d, x[k + 1], S31, 0xA4BEEA44);
d = HH(d, a, b, c, x[k + 4], S32, 0x4BDECFA9);
c = HH(c, d, a, b, x[k + 7], S33, 0xF6BB4B60);
b = HH(b, c, d, a, x[k + 10], S34, 0xBEBFBC70);
a = HH(a, b, c, d, x[k + 13], S31, 0x289B7EC6);
d = HH(d, a, b, c, x[k + 0], S32, 0xEAA127FA);
c = HH(c, d, a, b, x[k + 3], S33, 0xD4EF3085);
b = HH(b, c, d, a, x[k + 6], S34, 0x4881D05);
a = HH(a, b, c, d, x[k + 9], S31, 0xD9D4D039);
d = HH(d, a, b, c, x[k + 12], S32, 0xE6DB99E5);
c = HH(c, d, a, b, x[k + 15], S33, 0x1FA27CF8);
b = HH(b, c, d, a, x[k + 2], S34, 0xC4AC5665);
a = II(a, b, c, d, x[k + 0], S41, 0xF4292244);
d = II(d, a, b, c, x[k + 7], S42, 0x432AFF97);
c = II(c, d, a, b, x[k + 14], S43, 0xAB9423A7);
b = II(b, c, d, a, x[k + 5], S44, 0xFC93A039);
a = II(a, b, c, d, x[k + 12], S41, 0x655B59C3);
d = II(d, a, b, c, x[k + 3], S42, 0x8F0CCC92);
c = II(c, d, a, b, x[k + 10], S43, 0xFFEFF47D);
b = II(b, c, d, a, x[k + 1], S44, 0x85845DD1);
a = II(a, b, c, d, x[k + 8], S41, 0x6FA87E4F);
d = II(d, a, b, c, x[k + 15], S42, 0xFE2CE6E0);
c = II(c, d, a, b, x[k + 6], S43, 0xA3014314);
b = II(b, c, d, a, x[k + 13], S44, 0x4E0811A1);
a = II(a, b, c, d, x[k + 4], S41, 0xF7537E82);
d = II(d, a, b, c, x[k + 11], S42, 0xBD3AF235);
c = II(c, d, a, b, x[k + 2], S43, 0x2AD7D2BB);
b = II(b, c, d, a, x[k + 9], S44, 0xEB86D391);
a = AddUnsigned(a, AA);
b = AddUnsigned(b, BB);
c = AddUnsigned(c, CC);
d = AddUnsigned(d, DD);
}
return (WordToHex(a) + WordToHex(b) + WordToHex(c) + WordToHex(d)).toLowerCase();
}
return md5(text);
}
async function generateSignature(stringToSign, secret) {
const encoder = new TextEncoder();
const keyData = encoder.encode(secret);
const messageData = encoder.encode(stringToSign);
const cryptoKey = await crypto.subtle.importKey(
'raw',
keyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signature = await crypto.subtle.sign('HMAC', cryptoKey, messageData);
const hashArray = Array.from(new Uint8Array(signature));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}
async function sendApiMessage(nodeNum, messageText, fromSource) {
const channel = document.getElementById('channel').value;
const eventName = document.getElementById('event-name').value;
const appKey = document.getElementById('app-key').value;
const appSecret = 'app-secret'; const appId = 'app-id';
const apiData = {
name: eventName,
channel: channel,
data: {
message: messageText,
from: fromSource,
timestamp: new Date().toISOString()
}
};
try {
const bodyStr = JSON.stringify(apiData);
const bodyMd5 = generateMD5(bodyStr);
const timestamp = Math.floor(Date.now() / 1000).toString();
const path = `/apps/${appId}/events`;
const authParams = new URLSearchParams({
auth_key: appKey,
auth_timestamp: timestamp,
auth_version: '1.0',
body_md5: bodyMd5
});
const queryString = authParams.toString();
const stringToSign = `POST\n${path}\n${queryString}`;
const signature = await generateSignature(stringToSign, appSecret);
authParams.append('auth_signature', signature);
const scheme = 'https'; const host = document.getElementById(`node${nodeNum}-host`).value;
const port = document.getElementById(`node${nodeNum}-port`).value;
const url = `${scheme}://${host}:${port}${path}?${authParams.toString()}`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: bodyStr
});
if (response.ok) {
addMessage(nodeNum, `API ${fromSource}: ${messageText}`, 'sent');
} else {
const errorText = await response.text();
addMessage(nodeNum, `API ${fromSource} failed: ${response.status} ${errorText}`, 'error');
}
} catch (error) {
addMessage(nodeNum, `API ${fromSource} error: ${error.message}`, 'error');
}
}
async function broadcastMessage() {
const message = document.getElementById('broadcast-message').value.trim();
if (!message) return;
const selectedNode = parseInt(document.getElementById('broadcast-node').value);
await sendApiMessage(selectedNode, message, 'broadcast');
document.getElementById('broadcast-message').value = '';
}
function initConfig() {
loadConfig();
const configInputs = [
'node1-scheme', 'node1-host', 'node1-port',
'node2-scheme', 'node2-host', 'node2-port',
'app-key', 'channel', 'event-name'
];
configInputs.forEach(id => {
const element = document.getElementById(id);
if (element) {
element.addEventListener('change', saveConfig);
element.addEventListener('input', saveConfig);
console.log(`Added listeners to ${id}`);
} else {
console.warn(`Element ${id} not found`);
}
});
}
function autoScrollMessageLogs() {
const node1Messages = document.getElementById('node1-messages');
const node2Messages = document.getElementById('node2-messages');
if (node1Messages) {
node1Messages.scrollTop = node1Messages.scrollHeight;
}
if (node2Messages) {
node2Messages.scrollTop = node2Messages.scrollHeight;
}
}
function setupKeyListeners() {
document.getElementById('node1-message').addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendMessage(1);
});
document.getElementById('node2-message').addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendMessage(2);
});
document.getElementById('broadcast-message').addEventListener('keypress', (e) => {
if (e.key === 'Enter') broadcastMessage();
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
initConfig();
setupKeyListeners();
});
} else {
initConfig();
setupKeyListeners();
}
</script>
</body>
</html>