Files
logstream/web/index.html
cedricandClaude Opus 5.5 9ea1371696 Compact density and built-in Iosevka font for denser log display
Settings > Interface > Log display gets a Density switch (Normal/Compact)
that tightens row padding and line height, and the font list gets Iosevka,
a narrow SIL OFL monospace font served from web/fonts (Latin subset,
13 KB per weight) so it works offline, unlike the Bunny Fonts ones.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-03 15:39:33 +02:00

356 lines
20 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>LogStream</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%236366f1'/%3E%3Cpath d='M8 10h16M8 16h11M8 22h14' stroke='white' stroke-width='3' stroke-linecap='round'/%3E%3C/svg%3E">
<link rel="stylesheet" href="style.css">
<script>
// Apply the saved theme and language before first paint to avoid a flash.
try {
var t = localStorage.getItem('logstream.theme');
if (t === 'light' || t === 'dark') document.documentElement.dataset.theme = t;
var l = localStorage.getItem('logstream.lang');
if (l) document.documentElement.lang = l;
} catch (e) {}
</script>
</head>
<body>
<header class="topbar">
<div class="brand">
<svg viewBox="0 0 32 32" aria-hidden="true"><rect width="32" height="32" rx="8"/><path d="M8 10h16M8 16h11M8 22h14"/></svg>
<span>LogStream</span>
</div>
<div class="search">
<svg class="ico" viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
<input id="q" type="search" autocomplete="off" spellcheck="false" data-i18n-aria="searchAria">
<button id="mode" class="chip" type="button" data-i18n-title="modeTitle">Simple</button>
</div>
<div class="actions">
<button id="live" class="btn live" type="button">
<span class="dot"></span><span class="lbl" data-i18n="live">Live</span>
</button>
<button id="settingsBtn" class="icon-btn" type="button" data-i18n-title="settings" data-i18n-aria="settings">
<!-- Gear icon (Lucide "settings", ISC license) -->
<svg viewBox="0 0 24 24"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg>
</button>
<button id="themeBtn" class="icon-btn" type="button" data-i18n-title="theme" data-i18n-aria="theme">
<svg class="sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
<svg class="moon" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>
</button>
<a id="logoutBtn" class="icon-btn" href="/auth/logout" hidden data-i18n-title="logout" data-i18n-aria="logout">
<!-- Log out icon (Lucide "log-out", ISC license) -->
<svg viewBox="0 0 24 24"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><path d="m16 17 5-5-5-5"/><path d="M21 12H9"/></svg>
</a>
</div>
<div class="progress" aria-hidden="true"></div>
</header>
<section class="filters">
<select id="range" data-i18n-aria="rangeAria">
<option value="5m" data-i18n="r5m">5 min</option>
<option value="15m" data-i18n="r15m">15 min</option>
<option value="1h" data-i18n="r1h" selected>1 hour</option>
<option value="6h" data-i18n="r6h">6 hours</option>
<option value="24h" data-i18n="r24h">24 hours</option>
<option value="7d" data-i18n="r7d">7 days</option>
<option value="30d" data-i18n="r30d">30 days</option>
<option value="all" data-i18n="rAll">All</option>
</select>
<select id="severity" data-i18n-aria="severityAria">
<option value="" data-i18n="sevAll">All severities</option>
<option value="2" data-i18n="sev2">Critical and above</option>
<option value="3" data-i18n="sev3">Errors and above</option>
<option value="4" data-i18n="sev4">Warnings and above</option>
<option value="5" data-i18n="sev5">Notice and above</option>
<option value="6" data-i18n="sev6">Info and above</option>
</select>
<select id="host" data-i18n-aria="hostAria"><option value="" data-i18n="allHosts">All hosts</option></select>
<select id="app" data-i18n-aria="appAria"><option value="" data-i18n="allApps">All apps</option></select>
<select id="source" data-i18n-aria="sourceAria">
<option value="" data-i18n="srcAll">All sources</option>
<option value="syslog">Syslog</option>
<option value="docker">Docker</option>
<option value="host" data-i18n="srcHost">Host system</option>
</select>
<span class="spacer"></span>
<span id="count" class="muted"></span>
<div class="export">
<button id="exportBtn" class="btn tool" type="button" aria-haspopup="menu" aria-expanded="false">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v12M7 10l5 5 5-5M4 17v3h16v-3"/></svg>
<span class="lbl" data-i18n="export">Export</span>
</button>
<div id="exportMenu" class="menu" role="menu" hidden>
<button type="button" role="menuitem" data-export="csv">
<strong>CSV</strong><small data-i18n="exportCsvHint">Comma separated, UTF-8</small>
</button>
<button type="button" role="menuitem" data-export="excel">
<strong data-i18n="exportExcel">CSV for Excel</strong><small data-i18n="exportExcelHint">Semicolon separated, for Excel in French</small>
</button>
<p id="exportNote" class="menu-note"></p>
</div>
</div>
</section>
<section id="histo" class="histo" data-i18n-aria="histoAria" hidden></section>
<div id="error" class="error-banner" hidden></div>
<button id="newPill" class="pill" type="button" hidden></button>
<div id="table" class="table" hidden>
<div id="listHead" class="list-head" role="presentation">
<span data-col="rcv"><span class="lbl" data-i18n="colRcv">Received</span><i class="grip" data-i18n-title="colGrip"></i></span>
<span data-col="mt"><span class="lbl" data-i18n="colMt">Message time</span><i class="grip" data-i18n-title="colGrip"></i></span>
<span data-col="sev"><span class="lbl" data-i18n="colSev">Severity</span><i class="grip" data-i18n-title="colGrip"></i></span>
<span data-col="host"><span class="lbl" data-i18n="colHost">Host</span><i class="grip" data-i18n-title="colGrip"></i></span>
<span data-col="app"><span class="lbl" data-i18n="colApp">App</span><i class="grip" data-i18n-title="colGrip"></i></span>
<span data-col="codes"><span class="lbl" data-i18n="colCodes" data-i18n-title="codesTitle">Filters</span></span>
<span data-col="msg"><span class="lbl" data-i18n="colMsg">Message</span></span>
</div>
<main id="list" class="list"></main>
</div>
<div id="empty" class="empty" hidden>
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 6h16M4 12h10M4 18h7"/></svg>
<p><strong data-i18n="emptyTitle">No matching logs.</strong></p>
<p class="muted" data-i18n="emptyHint">Widen the time range or send syslog messages to port 514 (UDP or TCP).</p>
</div>
<footer class="status">
<span id="conn" class="conn"></span>
<span id="stats" class="muted"></span>
<button id="toTop" class="icon-btn to-top" type="button" data-i18n-title="toTop" data-i18n-aria="toTop" hidden>
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 19V5M5 12l7-7 7 7"/></svg>
</button>
</footer>
<dialog id="settingsDlg" class="settings" aria-labelledby="settingsTitle">
<div class="dlg">
<header>
<h2 id="settingsTitle" data-i18n="settings">Settings</h2>
<button id="closeSettings" class="icon-btn" type="button" data-i18n-aria="close">
<svg viewBox="0 0 24 24"><path d="M6 6l12 12M18 6 6 18"/></svg>
</button>
</header>
<div class="set-body">
<nav id="setNav" class="set-nav" role="tablist" aria-orientation="vertical">
<button type="button" role="tab" data-tab="locale">
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18"/></svg>
<span data-i18n="tabLocale">Localization</span>
</button>
<button type="button" role="tab" data-tab="filters">
<svg viewBox="0 0 24 24"><path d="M3 12V4a1 1 0 0 1 1-1h8l9 9-9 9z"/><circle cx="8" cy="8" r="1.5"/></svg>
<span data-i18n="tabFilters">Filters</span>
</button>
<button type="button" role="tab" data-tab="sources">
<svg viewBox="0 0 24 24"><path d="M21 8 12 3 3 8v8l9 5 9-5z"/><path d="m3 8 9 5 9-5M12 13v8"/></svg>
<span data-i18n="tabSources">Sources</span>
</button>
<button type="button" role="tab" data-tab="interface">
<svg viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="12" rx="2"/><path d="M8 20h8M12 16v4"/></svg>
<span data-i18n="tabInterface">Interface</span>
</button>
<button type="button" role="tab" data-tab="data">
<svg viewBox="0 0 24 24"><ellipse cx="12" cy="5" rx="8" ry="3"/><path d="M4 5v14c0 1.7 3.6 3 8 3s8-1.3 8-3V5M4 12c0 1.7 3.6 3 8 3s8-1.3 8-3"/></svg>
<span data-i18n="tabData">Data</span>
</button>
</nav>
<div class="set-panels">
<!-- Localization -->
<div class="set-panel" role="tabpanel" data-panel="locale">
<section class="set-section">
<h3 data-i18n="language">Language</h3>
<div id="langSwitch" class="seg" role="radiogroup" data-i18n-aria="language">
<button type="button" role="radio" data-lang="en">English</button>
<button type="button" role="radio" data-lang="fr">Français</button>
</div>
</section>
<section class="set-section">
<h3 data-i18n="dateTime">Date &amp; time</h3>
<div class="field-grid">
<label for="tzSelect" data-i18n="tzLabel">Time zone</label>
<select id="tzSelect" class="field"></select>
<label for="fmtSelect" data-i18n="fmtLabel">Reception time format</label>
<select id="fmtSelect" class="field"></select>
</div>
<p id="timePreview" class="time-preview"></p>
<p class="muted small" data-i18n="timeHelp"></p>
</section>
</div>
<!-- Filters -->
<div class="set-panel" role="tabpanel" data-panel="filters" hidden>
<section class="set-section">
<h3 data-i18n="tagsTitle">Color tags</h3>
<p class="muted small" data-i18n="tagsHelp"></p>
<div id="tagList" class="tag-list"></div>
<footer>
<span class="tag-add">
<button id="addTag" class="btn primary" type="button" data-i18n="addTag">+ Add tag</button>
<select id="presetTags" class="field" aria-label="Presets" data-i18n-aria="presetAria">
<option value="" data-i18n="presetPick">+ Preset…</option>
<option value="http_status" data-i18n="preset_http_status">HTTP status codes</option>
<option value="http_methods" data-i18n="preset_http_methods">HTTP methods</option>
<option value="http_probes" data-i18n="preset_http_probes">Probes and attacks</option>
<option value="http_bots" data-i18n="preset_http_bots">Bots and scripts</option>
<option value="http_errors" data-i18n="preset_http_errors">TLS/HTTPS and proxy errors</option>
</select>
</span>
<button id="resetTags" class="btn ghost" type="button" data-i18n="resetTags">Restore default tags</button>
</footer>
</section>
</div>
<!-- Sources -->
<div class="set-panel" role="tabpanel" data-panel="sources" hidden>
<section class="set-section">
<h3>Syslog</h3>
<p id="syslogStatus" class="docker-status"></p>
<label class="switch-row">
<input id="syslogEnabled" type="checkbox" class="switch">
<span data-i18n="syslogEnabled">Receive syslog messages</span>
</label>
<div class="sl-grid">
<span class="lbl" data-i18n="syslogProtocols">Protocols</span>
<div id="syslogProtos" class="chip-field sl-protos">
<button type="button" class="chip" data-proto="udp" style="--h:205">UDP</button>
<button type="button" class="chip" data-proto="tcp" style="--h:265">TCP</button>
</div>
<span class="lbl" data-i18n="syslogPort">Port</span>
<div class="sl-port"><code id="syslogPortVal">514</code><span id="syslogPortHelp" class="muted small"></span></div>
</div>
</section>
<section class="set-section">
<h3 data-i18n="hostTitle">Host system logs</h3>
<p id="hostStatus" class="docker-status"></p>
<label class="switch-row">
<input id="hostEnabled" type="checkbox" class="switch">
<span data-i18n="hostEnabled">Collect the system logs of this machine</span>
</label>
<p class="muted small" data-i18n="hostHelp"></p>
</section>
<section class="set-section">
<h3 data-i18n="dockerTitle">Docker containers</h3>
<p id="dockerStatus" class="docker-status"></p>
<div id="dockerBody" hidden>
<div class="dk-opts">
<label class="switch-row">
<input id="dockerDefault" type="checkbox" class="switch">
<span data-i18n="dockerDefault">Follow new containers automatically</span>
</label>
<label class="switch-row">
<input id="dockerStopped" type="checkbox" class="switch">
<span id="dockerStoppedLbl">Show stopped containers</span>
</label>
</div>
<div class="dk-toolbar">
<input id="dockerFilter" type="search" class="dk-filter" autocomplete="off" spellcheck="false" data-i18n-ph="dockerFilter" data-i18n-aria="dockerFilter">
<button id="dockerAll" class="link-btn" type="button" data-i18n="dockerAll">Enable all</button>
<button id="dockerNone" class="link-btn" type="button" data-i18n="dockerNone">Disable all</button>
</div>
<div id="dockerList" class="chip-field"></div>
<p class="dk-legend" data-i18n="dockerLegend"></p>
</div>
<p class="muted small" data-i18n="dockerHelp"></p>
</section>
</div>
<!-- Interface -->
<div class="set-panel" role="tabpanel" data-panel="interface" hidden>
<section class="set-section">
<h3 data-i18n="themeTitle">Theme</h3>
<div id="themeSwitch" class="seg" role="radiogroup" data-i18n-aria="themeTitle">
<button type="button" role="radio" data-theme-opt="system" data-i18n="themeSystem">System</button>
<button type="button" role="radio" data-theme-opt="light" data-i18n="themeLight">Light</button>
<button type="button" role="radio" data-theme-opt="dark" data-i18n="themeDark">Dark</button>
</div>
<p class="muted small hint" data-i18n="themeHelp"></p>
</section>
<section class="set-section">
<h3 data-i18n="logDisplay">Log display</h3>
<div class="field-grid">
<span class="lbl" data-i18n="fontSize">Font size</span>
<div id="sizeSwitch" class="seg" role="radiogroup" data-i18n-aria="fontSize">
<button type="button" role="radio" data-size="tiny" data-i18n="sizeTiny">Tiny</button>
<button type="button" role="radio" data-size="small" data-i18n="sizeSmall">Small</button>
<button type="button" role="radio" data-size="medium" data-i18n="sizeMedium">Medium</button>
<button type="button" role="radio" data-size="large" data-i18n="sizeLarge">Large</button>
</div>
<span class="lbl" data-i18n="density">Density</span>
<div id="densitySwitch" class="seg" role="radiogroup" data-i18n-aria="density">
<button type="button" role="radio" data-density="normal" data-i18n="densityNormal">Normal</button>
<button type="button" role="radio" data-density="compact" data-i18n="densityCompact">Compact</button>
</div>
<label for="fontSelect" data-i18n="fontLabel">Font</label>
<select id="fontSelect" class="field"></select>
</div>
<div id="fontPreview" class="list preview-list" aria-hidden="true"></div>
<p class="muted small" data-i18n="fontHelp"></p>
<div class="cols-row">
<button id="resetCols" class="btn ghost" type="button" data-i18n="resetCols">Reset column widths</button>
<span class="muted small" data-i18n="colsHelp"></span>
</div>
</section>
<section class="set-section">
<h3 data-i18n="histoTitle">Timeline</h3>
<div class="field-grid">
<span class="lbl" data-i18n="hScale">Scale</span>
<div class="seg" role="radiogroup" data-histo="scale" data-i18n-aria="hScale">
<button type="button" role="radio" data-v="linear" data-i18n="hLinear">Linear</button>
<button type="button" role="radio" data-v="sqrt">√</button>
<button type="button" role="radio" data-v="log">Log</button>
</div>
<span class="lbl" data-i18n="hHeight">Height</span>
<div class="seg" role="radiogroup" data-histo="height" data-i18n-aria="hHeight">
<button type="button" role="radio" data-v="s">S</button>
<button type="button" role="radio" data-v="m">M</button>
<button type="button" role="radio" data-v="l">L</button>
</div>
<span class="lbl" data-i18n="hColor">Color</span>
<div class="seg" role="radiogroup" data-histo="color" data-i18n-aria="hColor">
<button type="button" role="radio" data-v="severity" data-i18n="hColSeverity">Severity</button>
<button type="button" role="radio" data-v="intensity" data-i18n="hColIntensity">Intensity</button>
<button type="button" role="radio" data-v="none" data-i18n="hColNone">None</button>
</div>
<span class="lbl" data-i18n="hRender">Display</span>
<div class="seg" role="radiogroup" data-histo="render" data-i18n-aria="hRender">
<button type="button" role="radio" data-v="bars" data-i18n="hBars">Bars</button>
<button type="button" role="radio" data-v="area" data-i18n="hArea">Area</button>
</div>
<label for="histoStep" data-i18n="hStep">Division</label>
<select id="histoStep" class="field" data-histo="step"></select>
<label for="histoRefresh" data-i18n="hRefresh">Refresh</label>
<select id="histoRefresh" class="field" data-histo="refresh"></select>
</div>
<p class="muted small" data-i18n="histoHelp"></p>
</section>
</div>
<!-- Data -->
<div class="set-panel" role="tabpanel" data-panel="data" hidden>
<section class="set-section danger">
<h3 data-i18n="dangerZone">Danger zone</h3>
<p class="muted small" data-i18n="purgeHelp"></p>
<div class="purge-row">
<button id="purgeBtn" class="btn danger" type="button" data-i18n="purgeBtn">Delete all logs…</button>
<span id="purgeStatus" class="purge-status muted" hidden></span>
</div>
</section>
</div>
</div>
</div>
</div>
</dialog>
<div id="toast" class="toast" hidden></div>
<script src="app.js"></script>
</body>
</html>