ca58c18802
- eventlog() now stores {ts, host, level, service, message} dicts instead of strings
- WebSocket sends/broadcasts filter event log messages by the user's managed hosts
- live.html renders structured log entries with level-coloured spans
- Swiss railway clock minute hand now holds until second hand reaches 12, then steps
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
287 lines
8.7 KiB
HTML
287 lines
8.7 KiB
HTML
<head>
|
|
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<link rel="stylesheet" href="/static/style.css" type="text/css" />
|
|
<link rel="icon" href="/static/images/favicon.ico" sizes="32x32" />
|
|
<title>{{ title }}</title>
|
|
{% if extra_scripts %}<script src="{{ extra_scripts }}"></script>{% endif %}
|
|
<style>
|
|
/* ── Reset / shared baseline ── */
|
|
*, *::before, *::after { box-sizing: border-box; }
|
|
html {
|
|
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
|
font-size: 14px;
|
|
}
|
|
body {
|
|
margin: 0;
|
|
padding: 10px;
|
|
padding-top: 60px;
|
|
background: #f5f5f5;
|
|
}
|
|
h1 { font-size: 1.5em; color: #333; margin: 0 0 5px; }
|
|
h2 { font-size: 1.1em; color: #333; margin: 0 0 8px; }
|
|
p { margin: 0; }
|
|
|
|
/* Navigation bar — shared across all pages */
|
|
.nav {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
z-index: 200;
|
|
background: #fff;
|
|
padding: 6px 12px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,.1);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
}
|
|
.nav-links { display: flex; align-items: center; flex-wrap: wrap; gap: 4px; }
|
|
.nav a {
|
|
margin-right: 20px;
|
|
text-decoration: none;
|
|
color: #0066cc;
|
|
font-weight: 500;
|
|
font-size: 0.9em;
|
|
}
|
|
.nav a:hover { text-decoration: underline; }
|
|
.nav a.active { color: #333; font-weight: bold; }
|
|
.nav-user {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
text-decoration: none;
|
|
color: #333;
|
|
font-size: 0.9em;
|
|
font-weight: 500;
|
|
padding: 4px 8px;
|
|
border-radius: 20px;
|
|
transition: background 0.15s;
|
|
}
|
|
.nav-user:hover { background: #f0f4ff; text-decoration: none; }
|
|
.nav-username {
|
|
max-width: 0;
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
opacity: 0;
|
|
transition: max-width 0.2s ease, opacity 0.2s ease;
|
|
}
|
|
.nav-user:hover .nav-username {
|
|
max-width: 160px;
|
|
opacity: 1;
|
|
}
|
|
.nav-avatar {
|
|
width: 28px; height: 28px;
|
|
border-radius: 50%;
|
|
object-fit: cover;
|
|
flex-shrink: 0;
|
|
}
|
|
.nav-initials {
|
|
width: 28px; height: 28px;
|
|
border-radius: 50%;
|
|
background: #0066cc;
|
|
color: #fff;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 0.75em;
|
|
font-weight: 700;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* ── Mobile nav: hamburger toggle ── */
|
|
.nav-hamburger {
|
|
display: none;
|
|
flex-direction: column;
|
|
justify-content: space-between;
|
|
width: 26px; height: 20px;
|
|
cursor: pointer;
|
|
flex-shrink: 0;
|
|
background: none;
|
|
border: none;
|
|
padding: 0;
|
|
}
|
|
.nav-hamburger span {
|
|
display: block;
|
|
height: 3px;
|
|
background: #555;
|
|
border-radius: 2px;
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
.nav-hamburger { display: flex; }
|
|
.nav-links {
|
|
display: none;
|
|
width: 100%;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
padding-top: 8px;
|
|
border-top: 1px solid #eee;
|
|
order: 3;
|
|
}
|
|
.nav-links.nav-open { display: flex; }
|
|
.nav-links a { margin-right: 0; padding: 6px 0; font-size: 1em; }
|
|
}
|
|
|
|
/* Swiss railway clock — nav */
|
|
.nav-pie {
|
|
flex-shrink: 0;
|
|
line-height: 0;
|
|
margin-left: auto;
|
|
padding: 4px 4px 4px 0;
|
|
}
|
|
#alert-pie { display: block; cursor: default; }
|
|
.nav-clock {
|
|
flex-shrink: 0;
|
|
line-height: 0;
|
|
padding: 4px 4px 4px 0;
|
|
cursor: pointer;
|
|
}
|
|
#swiss-clock { display: block; }
|
|
|
|
/* Swiss railway clock — full-page overlay */
|
|
#clock-overlay {
|
|
display: none;
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 9999;
|
|
background: #1a1a1a;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
}
|
|
#clock-overlay.visible { display: flex; }
|
|
#swiss-clock-overlay { display: block; }
|
|
</style>
|
|
<script>
|
|
/* ── Swiss Federal Railway (SBB) clock ── */
|
|
|
|
/* Draw one frame of the clock onto any canvas element. */
|
|
function drawSwissClock(canvas) {
|
|
var SIZE = canvas.width;
|
|
var R = SIZE / 2;
|
|
var ctx = canvas.getContext('2d');
|
|
var now = new Date();
|
|
var h = now.getHours() % 12;
|
|
var m = now.getMinutes();
|
|
var s = now.getSeconds();
|
|
var ms = now.getMilliseconds();
|
|
|
|
/* Seconds hand idles ~1.5 s at 12 before advancing (SBB behaviour) */
|
|
var sFrac = s + ms / 1000;
|
|
var sAngle = sFrac >= 58.5 ? 0 : (sFrac / 58.5) * Math.PI * 2;
|
|
|
|
ctx.clearRect(0, 0, SIZE, SIZE);
|
|
|
|
/* face */
|
|
ctx.beginPath();
|
|
ctx.arc(R, R, R - 1, 0, Math.PI * 2);
|
|
ctx.fillStyle = '#fff';
|
|
ctx.fill();
|
|
ctx.strokeStyle = '#333';
|
|
ctx.lineWidth = SIZE * 0.018;
|
|
ctx.stroke();
|
|
|
|
/* tick marks */
|
|
for (var i = 0; i < 60; i++) {
|
|
var a = (i / 60) * Math.PI * 2 - Math.PI / 2;
|
|
var isHour = (i % 5 === 0);
|
|
ctx.beginPath();
|
|
ctx.moveTo(R + Math.cos(a) * (isHour ? R * 0.72 : R * 0.88),
|
|
R + Math.sin(a) * (isHour ? R * 0.72 : R * 0.88));
|
|
ctx.lineTo(R + Math.cos(a) * R * 0.94,
|
|
R + Math.sin(a) * R * 0.94);
|
|
ctx.strokeStyle = '#222';
|
|
ctx.lineWidth = isHour ? SIZE * 0.027 : SIZE * 0.011;
|
|
ctx.lineCap = 'butt';
|
|
ctx.stroke();
|
|
}
|
|
|
|
/* hands */
|
|
function hand(angle, tip, tail, width, color) {
|
|
ctx.save();
|
|
ctx.translate(R, R);
|
|
ctx.rotate(angle);
|
|
ctx.beginPath();
|
|
ctx.moveTo(tail, 0);
|
|
ctx.lineTo(tip, 0);
|
|
ctx.strokeStyle = color;
|
|
ctx.lineWidth = width;
|
|
ctx.lineCap = 'square';
|
|
ctx.stroke();
|
|
ctx.restore();
|
|
}
|
|
|
|
hand((sFrac >= 58.5 ? m + 1 : m) / 60 * Math.PI * 2 - Math.PI / 2,
|
|
R * 0.88, -R * 0.12, SIZE * 0.027, '#222'); /* minute */
|
|
hand((h + m / 60) / 12 * Math.PI * 2 - Math.PI / 2,
|
|
R * 0.58, -R * 0.12, SIZE * 0.039, '#222'); /* hour */
|
|
hand(sAngle - Math.PI / 2, R * 0.78, -R * 0.22,
|
|
SIZE * 0.013, '#e00'); /* second tail+tip */
|
|
|
|
/* round dot at tip of second hand */
|
|
var dotR = SIZE * 0.028;
|
|
ctx.save();
|
|
ctx.translate(R, R);
|
|
ctx.rotate(sAngle - Math.PI / 2);
|
|
ctx.beginPath();
|
|
ctx.arc(R * 0.78, 0, dotR, 0, Math.PI * 2);
|
|
ctx.fillStyle = '#e00';
|
|
ctx.fill();
|
|
ctx.restore();
|
|
|
|
/* centre cap */
|
|
ctx.beginPath();
|
|
ctx.arc(R, R, R * 0.04, 0, Math.PI * 2);
|
|
ctx.fillStyle = '#222';
|
|
ctx.fill();
|
|
}
|
|
|
|
/* Resize the overlay canvas to fit the viewport, keeping it square. */
|
|
function resizeOverlayClock() {
|
|
var oc = document.getElementById('swiss-clock-overlay');
|
|
if (!oc) return;
|
|
var size = Math.min(window.innerWidth, window.innerHeight) * 0.88;
|
|
size = Math.floor(size);
|
|
oc.width = size;
|
|
oc.height = size;
|
|
}
|
|
|
|
/* Main tick — redraws both nav clock and (if visible) overlay clock. */
|
|
function clockTick() {
|
|
var nav = document.getElementById('swiss-clock');
|
|
if (nav) drawSwissClock(nav);
|
|
var overlay = document.getElementById('clock-overlay');
|
|
if (overlay && overlay.classList.contains('visible')) {
|
|
var oc = document.getElementById('swiss-clock-overlay');
|
|
if (oc) drawSwissClock(oc);
|
|
}
|
|
var delay = 100 - (Date.now() % 100);
|
|
setTimeout(clockTick, delay);
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
/* Start the shared tick loop */
|
|
clockTick();
|
|
|
|
/* Overlay toggle — clicking the nav clock opens it */
|
|
var navClock = document.querySelector('.nav-clock');
|
|
var overlay = document.getElementById('clock-overlay');
|
|
if (navClock && overlay) {
|
|
navClock.addEventListener('click', function() {
|
|
resizeOverlayClock();
|
|
overlay.classList.add('visible');
|
|
});
|
|
overlay.addEventListener('click', function() {
|
|
overlay.classList.remove('visible');
|
|
});
|
|
window.addEventListener('resize', function() {
|
|
if (overlay.classList.contains('visible')) resizeOverlayClock();
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
<script src="static/sorttable.js"></script>
|
|
</head> |