Ajout redirection des logs sur websocket et page /logs/view

This commit is contained in:
Arnaud Nelissen
2025-09-30 19:05:40 +02:00
parent 3ff1b73cd2
commit 539b6f262e
7 changed files with 633 additions and 9 deletions

464
logs-view/index.html Normal file
View File

@@ -0,0 +1,464 @@
<!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>