465 lines
15 KiB
HTML
465 lines
15 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>WebSocket Logs</title>
|
|
<style>
|
|
/* --------------------- */
|
|
/* Nord Theme Palette */
|
|
/* --------------------- */
|
|
:root {
|
|
--nord0: #2E3440; /* dark background */
|
|
--nord1: #3B4252; /* sidebar, controls */
|
|
--nord2: #434C5E; /* other messages */
|
|
--nord3: #4C566A; /* toast background */
|
|
--nord4: #D8DEE9; /* light text */
|
|
--nord5: #E5E9F0; /* light background */
|
|
--nord6: #ECEFF4; /* light body */
|
|
--nord7: #8FBCBB; /* cyan */
|
|
--nord8: #88C0D0; /* blue */
|
|
--nord9: #81A1C1;
|
|
--nord10: #5E81AC; /* darker blue */
|
|
--nord11: #BF616A; /* red */
|
|
--nord12: #D08770; /* orange */
|
|
--nord13: #EBCB8B; /* yellow */
|
|
--nord14: #A3BE8C; /* green */
|
|
--nord15: #B48EAD; /* purple */
|
|
}
|
|
|
|
:root {
|
|
/* Light theme */
|
|
--bubble-sender: #3b82f6; /* bright blue */
|
|
--bubble-receiver: #e5e7eb;
|
|
--text: #111;
|
|
--text-light: #fff;
|
|
}
|
|
|
|
[data-theme="dark"] {
|
|
--bubble-sender: #2563eb; /* deep blue */
|
|
--bubble-receiver: #374151;
|
|
--text: #f9fafb;
|
|
--text-light: #fff;
|
|
}
|
|
|
|
.message {
|
|
white-space: pre-wrap;
|
|
word-wrap: break-word;
|
|
line-height: 1.4em; /* adjust for readability */
|
|
}
|
|
|
|
.message.sender {
|
|
align-self: flex-end;
|
|
background: var(--bubble-sender);
|
|
color: #fff; /* force white text for dark bubble */
|
|
font-weight: 500;
|
|
}
|
|
|
|
.message.receiver {
|
|
align-self: flex-start;
|
|
background: var(--bubble-receiver);
|
|
color: var(--text); /* normal readable dark text */
|
|
}
|
|
|
|
/* --------------------- */
|
|
/* Global Styles */
|
|
/* --------------------- */
|
|
* { box-sizing:border-box; margin:0; padding:0; font-family:"Inter","Segoe UI",sans-serif; }
|
|
body { display:flex; height:100vh; background:var(--nord0); color:var(--nord4); overflow:hidden; }
|
|
body.light { background:var(--nord6); color:var(--nord2); }
|
|
|
|
/* --------------------- */
|
|
/* Sidebar */
|
|
/* --------------------- */
|
|
#sidebar {
|
|
width:200px;
|
|
background:var(--nord1);
|
|
padding:20px;
|
|
overflow-y:auto;
|
|
display:flex;
|
|
flex-direction:column;
|
|
gap:10px;
|
|
box-shadow:2px 0 5px rgba(0,0,0,0.3);
|
|
}
|
|
body.light #sidebar { background:var(--nord5); box-shadow:2px 0 5px rgba(0,0,0,0.1); }
|
|
#sidebar h3 { font-size:1em; margin-bottom:10px; text-align:center; }
|
|
.client-item { padding:8px 10px; border-radius:6px; background:var(--nord2); text-align:center; font-weight:600; color:var(--nord4); }
|
|
body.light .client-item { background:var(--nord6); color:var(--nord2); }
|
|
|
|
/* --------------------- */
|
|
/* Main Controls */
|
|
/* --------------------- */
|
|
#main { flex:1; display:flex; flex-direction:column; position:relative; }
|
|
#controls { display:flex; align-items:center; gap:10px; padding:10px; background:var(--nord1); z-index:2; }
|
|
body.light #controls { background:var(--nord5); color:var(--nord2); }
|
|
#controls select, #controls input { padding:5px 8px; border-radius:5px; border:none; outline:none; font-size:0.9em; }
|
|
#themeToggle { margin-left:auto; cursor:pointer; background:var(--nord2); color:var(--nord4); border:none; border-radius:6px; padding:5px 10px; }
|
|
#logoutBtn { background:var(--nord11); color:var(--nord0); cursor:pointer; border:none; border-radius:6px; padding:5px 10px; font-weight:600; }
|
|
|
|
/* --------------------- */
|
|
/* Chat Log */
|
|
/* --------------------- */
|
|
#log { flex:1; overflow-y:auto; padding:15px; display:flex; flex-direction:column; gap:10px; background:var(--nord0); color:var(--nord4); }
|
|
body.light #log { background:var(--nord6); color:var(--nord2); }
|
|
.log-entry { max-width:60%; padding:10px 12px; border-radius:15px; word-break:break-word; position:relative; display:inline-block; opacity:0; transform:translateY(20px); transition: all 0.3s ease; }
|
|
.log-entry.show { opacity:1; transform:translateY(0); }
|
|
|
|
/* --------------------- */
|
|
/* Chat Bubbles */
|
|
/* --------------------- */
|
|
.own-message {
|
|
align-self:flex-end;
|
|
background: #D1F0C0; /* lighter green */
|
|
color: var(--nord0);
|
|
border-bottom-right-radius:3px;
|
|
}
|
|
body.light .own-message { background: #A3BE8C; color: var(--nord0); }
|
|
|
|
.other-message {
|
|
align-self:flex-start;
|
|
background: #6C758C; /* lighter grey/blue */
|
|
color: var(--nord4);
|
|
border-bottom-left-radius:3px;
|
|
}
|
|
body.light .other-message { background: #D8DEE9; color: var(--nord2); }
|
|
|
|
/* Sender label styling */
|
|
.log-entry .client {
|
|
font-weight: 700;
|
|
margin-bottom: 4px;
|
|
display: block;
|
|
font-size: 0.85em;
|
|
}
|
|
.own-message .client { color: var(--nord10); } /* blue for own */
|
|
.other-message .client { color: var(--nord13); } /* yellow for others */
|
|
body.light .own-message .client { color: var(--nord10); }
|
|
body.light .other-message .client { color: var(--nord12); } /* orange in light mode */
|
|
|
|
/* --------------------- */
|
|
/* Toast Timestamp */
|
|
/* --------------------- */
|
|
.toast-timestamp {
|
|
position: absolute;
|
|
top: 50%;
|
|
background: var(--nord3);
|
|
color: var(--nord4);
|
|
padding:3px 8px;
|
|
border-radius:8px;
|
|
font-size:0.7em;
|
|
opacity:0;
|
|
pointer-events:none;
|
|
transition:opacity 0.2s ease;
|
|
white-space:nowrap;
|
|
z-index:5;
|
|
}
|
|
body.light .toast-timestamp { background:var(--nord5); color:var(--nord2); }
|
|
.log-entry:hover .toast-timestamp { opacity:1; }
|
|
|
|
/* Toast positions */
|
|
.own-message .toast-timestamp { right:100%; transform:translate(-8px,-50%); }
|
|
.other-message .toast-timestamp { left:100%; transform:translate(8px,-50%); }
|
|
|
|
/* --------------------- */
|
|
/* Message Input */
|
|
/* --------------------- */
|
|
#messageBar { display:flex; padding:10px; background:var(--nord1); gap:10px; }
|
|
body.light #messageBar { background:var(--nord5); }
|
|
#messageInput { flex:1; padding:8px; border-radius:6px; border:none; outline:none; font-size:0.95em; background:var(--nord2); color:var(--nord4); }
|
|
body.light #messageInput { background:var(--nord6); color:var(--nord2); }
|
|
#sendBtn { padding:8px 16px; border-radius:6px; border:none; background:var(--nord14); color:var(--nord0); cursor:pointer; font-weight:600; }
|
|
|
|
/* --------------------- */
|
|
/* Login Overlay */
|
|
/* --------------------- */
|
|
#loginOverlay { position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(46,52,64,0.9); display:flex; align-items:center; justify-content:center; z-index:10; }
|
|
body.light #loginOverlay { background:rgba(236,239,244,0.9); }
|
|
#loginBox { background:var(--nord1); padding:30px; border-radius:12px; text-align:center; color:var(--nord4); }
|
|
body.light #loginBox { background:var(--nord6); color:var(--nord2); }
|
|
#loginBox input { padding:8px; border-radius:6px; border:none; outline:none; font-size:1em; width:200px; background:var(--nord2); color:var(--nord4); }
|
|
body.light #loginBox input { background:var(--nord6); color:var(--nord2); }
|
|
#loginBox button { margin-top:10px; padding:8px 16px; border-radius:6px; border:none; background:var(--nord14); color:var(--nord0); cursor:pointer; font-weight:600; }
|
|
</style>
|
|
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/markdown-it/dist/markdown-it.min.js"></script>
|
|
</head>
|
|
<body>
|
|
|
|
<div id="sidebar">
|
|
<h3>Clients</h3>
|
|
<div id="clientsList"></div>
|
|
</div>
|
|
|
|
<div id="main">
|
|
<div id="controls">
|
|
<label>Filter type:
|
|
<select id="filterType">
|
|
<option value="">All</option>
|
|
<option value="log">Log</option>
|
|
<option value="error">Error</option>
|
|
<option value="warn">Warn</option>
|
|
<option value="info">Info</option>
|
|
<option value="debug">Debug</option>
|
|
<option value="message">Message</option>
|
|
</select>
|
|
</label>
|
|
<label>Search: <input id="search" type="text" placeholder="Search logs"></label>
|
|
<button id="themeToggle">Toggle Theme</button>
|
|
<button id="logoutBtn">Logout</button>
|
|
</div>
|
|
|
|
<div id="log"></div>
|
|
|
|
<div id="messageBar">
|
|
<textarea id="messageInput" placeholder="Type a message..."></textarea>
|
|
<button id="sendBtn">Send</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="loginOverlay">
|
|
<div id="loginBox">
|
|
<h2>Login</h2>
|
|
<input type="text" id="loginInput" placeholder="Enter your name">
|
|
<br>
|
|
<button id="loginBtn">Login</button>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// --- Cookies helpers ---
|
|
function getCookie(name){const m=document.cookie.match(new RegExp('(^| )'+name+'=([^;]+)'));return m?decodeURIComponent(m[2]):null;}
|
|
function setCookie(name,val,days=365){const d=new Date();d.setTime(d.getTime()+days*24*60*60*1000);document.cookie=`${name}=${encodeURIComponent(val)};expires=${d.toUTCString()};path=/`;}
|
|
function delCookie(name){document.cookie=name+'=; Max-Age=-99999999;'}
|
|
|
|
// --- Markdown and escape ---
|
|
const md = window.markdownit({
|
|
html: true, // <-- Allow HTML tags in Markdown
|
|
breaks: true,
|
|
linkify: true
|
|
});
|
|
function escapeHTML(str){const div=document.createElement('div'); div.textContent=str; return div.innerHTML;}
|
|
|
|
// --- Username colors ---
|
|
function stringToColor(str) {
|
|
// Step 1: Create a hash from full Unicode code points (emoji safe)
|
|
let hash = 0;
|
|
for (const char of str) {
|
|
const codePoint = char.codePointAt(0);
|
|
hash = codePoint + ((hash << 5) - hash);
|
|
}
|
|
|
|
// Step 2: Extract RGB values
|
|
let r = (hash >> 16) & 0xff;
|
|
let g = (hash >> 8) & 0xff;
|
|
let b = hash & 0xff;
|
|
|
|
// Step 3: Normalize to pastel range for readability
|
|
const min = 100;
|
|
const max = 200;
|
|
r = min + (r % (max - min));
|
|
g = min + (g % (max - min));
|
|
b = min + (b % (max - min));
|
|
|
|
// Step 4: Build hex string
|
|
const hex = [r, g, b]
|
|
.map(c => c.toString(16).padStart(2, "0"))
|
|
.join("")
|
|
.toUpperCase();
|
|
|
|
return `#${hex}`;
|
|
}
|
|
|
|
|
|
// --- DOM elements ---
|
|
let clientName = getCookie('logUsername');
|
|
const loginOverlay = document.getElementById('loginOverlay');
|
|
const loginInput = document.getElementById('loginInput');
|
|
const loginBtn = document.getElementById('loginBtn');
|
|
const logoutBtn = document.getElementById('logoutBtn');
|
|
const clientsList = document.getElementById('clientsList');
|
|
const logDiv = document.getElementById('log');
|
|
const filterType = document.getElementById('filterType');
|
|
const searchInput = document.getElementById('search');
|
|
const themeToggle = document.getElementById('themeToggle');
|
|
const messageInput = document.getElementById('messageInput');
|
|
const sendBtn = document.getElementById('sendBtn');
|
|
const input = document.getElementById("messageInput");
|
|
let ws = null;
|
|
|
|
// --- Show/hide login overlay ---
|
|
function showLogin(){ loginOverlay.style.display='flex'; }
|
|
function hideLogin(){ loginOverlay.style.display='none'; }
|
|
|
|
// --- Start app ---
|
|
function startApp(){
|
|
hideLogin();
|
|
connectWS();
|
|
}
|
|
|
|
// --- Logout ---
|
|
logoutBtn.onclick = () => {
|
|
delCookie('logUsername');
|
|
clientName = null;
|
|
if(ws){ ws.close(); ws = null; }
|
|
showLogin();
|
|
};
|
|
|
|
// --- Theme toggle ---
|
|
let theme=getCookie('logTheme')||'dark';
|
|
document.body.classList.toggle('light',theme==='light');
|
|
themeToggle.onclick = () => {
|
|
theme = theme==='light'?'dark':'light';
|
|
document.body.classList.toggle('light',theme==='light');
|
|
setCookie('logTheme',theme);
|
|
};
|
|
|
|
// --- Login ---
|
|
loginBtn.onclick = ()=>{
|
|
const val = loginInput.value.trim();
|
|
if(!val) return;
|
|
clientName = val;
|
|
setCookie('logUsername', clientName);
|
|
startApp();
|
|
};
|
|
loginInput.addEventListener('keydown', e=>{ if(e.key==='Enter') loginBtn.onclick(); });
|
|
|
|
// --- On page load ---
|
|
if(!clientName) showLogin();
|
|
else startApp();
|
|
|
|
// --- WebSocket ---
|
|
function connectWS(){
|
|
if(!clientName) return;
|
|
ws = new WebSocket(`wss://${location.host}/logs/${encodeURIComponent(clientName)}`);
|
|
|
|
ws.onopen = () => console.log("Connected to WSS logs");
|
|
|
|
ws.onmessage = e => {
|
|
const data = JSON.parse(e.data);
|
|
if(data.clients){
|
|
clientsList.innerHTML='';
|
|
data.clients.forEach(c=>{
|
|
const div=document.createElement('div');
|
|
div.className='client-item';
|
|
div.textContent=c;
|
|
clientsList.appendChild(div);
|
|
});
|
|
return;
|
|
}
|
|
const entry = createEntry(data);
|
|
logDiv.appendChild(entry);
|
|
requestAnimationFrame(()=>entry.classList.add('show'));
|
|
logDiv.scrollTop = logDiv.scrollHeight;
|
|
applyFilters();
|
|
};
|
|
|
|
ws.onclose = () => setTimeout(connectWS,2000);
|
|
}
|
|
|
|
// --- Send message ---
|
|
function sendMessage(){
|
|
const msg = messageInput.value.trim();
|
|
if(!msg || !ws || ws.readyState !== WebSocket.OPEN) return;
|
|
ws.send(msg);
|
|
messageInput.value='';
|
|
}
|
|
|
|
// Button click
|
|
sendBtn.addEventListener("click", sendMessage);
|
|
|
|
// Enter / Shift+Enter handling
|
|
input.addEventListener("keydown", (e) => {
|
|
// Only handle Enter keys
|
|
if (e.key === "Enter") {
|
|
if (e.shiftKey) {
|
|
// Insert newline at cursor position
|
|
const start = input.selectionStart;
|
|
const end = input.selectionEnd;
|
|
input.value = input.value.substring(0, start) + "\n" + input.value.substring(end);
|
|
input.selectionStart = input.selectionEnd = start + 1;
|
|
e.preventDefault(); // prevent form submission
|
|
} else {
|
|
// Send the message
|
|
const msg = input.value.trim();
|
|
if (msg) {
|
|
sendMessage(msg);
|
|
}
|
|
input.value = "";
|
|
e.preventDefault(); // prevent newline insertion
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
// --- Filters ---
|
|
function applyFilters(){
|
|
const typeVal = filterType.value.toLowerCase();
|
|
const searchVal = searchInput.value.toLowerCase();
|
|
Array.from(logDiv.children).forEach(entry=>{
|
|
const text = entry.innerText.toLowerCase();
|
|
entry.style.display = (!typeVal || entry.dataset.type===typeVal) && (!searchVal || text.includes(searchVal)) ? '' : 'none';
|
|
});
|
|
}
|
|
filterType.addEventListener('change', applyFilters);
|
|
searchInput.addEventListener('input', applyFilters);
|
|
|
|
// --- Markdown and escape ---
|
|
function escapeHTML(str){
|
|
const div=document.createElement('div');
|
|
div.textContent=str;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// --- Create log entry ---
|
|
// --- Create log entry ---
|
|
function createEntry(data) {
|
|
const isOwn = data.client === clientName;
|
|
const userColor = stringToColor(data.client);
|
|
|
|
// Trim trailing spaces/newlines
|
|
let msg = data.message.replace(/\s+$/g, '');
|
|
|
|
// Render Markdown with HTML allowed
|
|
let renderedMsg = md.render(msg);
|
|
|
|
// Auto-style GIFs and other images
|
|
// Wrap all <img> tags in a class with max dimensions
|
|
renderedMsg = renderedMsg.replace(/<img /g, '<img style="max-width:200px; max-height:150px; object-fit:contain; display:block; margin-top:5px; margin-bottom:5px;" ');
|
|
|
|
// Check the last log entry for grouping
|
|
const lastEntry = logDiv.lastElementChild;
|
|
if (
|
|
lastEntry &&
|
|
lastEntry.dataset.client === data.client &&
|
|
lastEntry.dataset.type === data.type
|
|
) {
|
|
const messageDiv = lastEntry.querySelector(".message");
|
|
messageDiv.innerHTML += "<br>" + renderedMsg;
|
|
return lastEntry;
|
|
}
|
|
|
|
// Otherwise, create a new entry
|
|
const entry = document.createElement("div");
|
|
entry.className = "log-entry " + (isOwn ? "own-message" : "other-message");
|
|
entry.dataset.type = data.type;
|
|
entry.dataset.client = data.client;
|
|
|
|
const usernameHTML = `<span class="client" style="color:${
|
|
isOwn ? "inherit" : userColor
|
|
}">${escapeHTML(data.client)}</span>`;
|
|
|
|
entry.innerHTML = `
|
|
<span class="toast-timestamp">${new Date(
|
|
data.timestamp
|
|
).toLocaleTimeString()}</span>
|
|
${usernameHTML}
|
|
<div class="message">${renderedMsg}</div>
|
|
`;
|
|
|
|
if (!isOwn) entry.style.backgroundColor = userColor + "33";
|
|
return entry;
|
|
}
|
|
|
|
</script>
|
|
</body>
|
|
</html>
|