Author SHA1 Message Date
cedricandClaude Opus 5.5 7ad10b6969 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:15 +02:00
claude Bot 1a21656546 Merge pull request 'Préréglages de tags couleur pour les logs HTTP/HTTPS' (#10) from feat/filtres-http into main 2026-10-03 15:17:58 +02:00
cedricandClaude Opus 5.5 be58284916 Ready-made color tag presets for HTTP/HTTPS access logs
A '+ Preset…' menu in Settings > Filters adds groups of tags for status
codes, methods, probes, bots and TLS/proxy errors (nginx, Apache, Traefik,
Caddy, HAProxy). A regex group named hl colors only that part of a match,
and tags get an optional label shown in place of the pattern.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-03 15:16:44 +02:00
9 changed files with 267 additions and 26 deletions

No files matched your search

+18 -8
View File
@@ -228,17 +228,27 @@ couleur, est mémorisé par navigateur.
Tags par défaut (pastel) : `warning` (orange), `error` (rouge), `ok` (vert). Les tags par Tags par défaut (pastel) : `warning` (orange), `error` (rouge), `ok` (vert). Les tags par
défaut qui utilisent encore les couleurs des versions précédentes passent automatiquement aux défaut qui utilisent encore les couleurs des versions précédentes passent automatiquement aux
couleurs pastel. couleurs pastel.
Le menu *+ Préréglage…* ajoute des tags tout faits pour les logs d'accès HTTP/HTTPS (nginx et
Apache common/combined, Traefik CLF et JSON, Caddy JSON, HAProxy httplog) : codes de statut
(2xx vert, 3xx bleu, 4xx orange, 5xx rouge), méthodes, sondes et attaques (`wp-login.php`,
`/.env`, `../`…), robots et scripts, erreurs TLS et proxy. Les tags déjà présents ne sont pas
ajoutés en double, et les tags ajoutés se modifient comme les autres. Dans une expression
régulière, un groupe nommé `hl` (`(?<hl>…)`) ne colore que cette partie de la correspondance :
les préréglages s'en servent pour colorer le code de statut ou la méthode, pas le texte autour.
- **Interface** - **Interface**
- *Thème* : Système (suit la préférence de l'ordinateur ou du téléphone), Clair ou Sombre. Le - *Thème* : Système (suit la préférence de l'ordinateur ou du téléphone), Clair ou Sombre. Le
bouton soleil/lune de l'en-tête bascule entre clair et sombre. bouton soleil/lune de l'en-tête bascule entre clair et sombre.
- *Affichage des logs* : taille du texte (très petite, petite, moyenne, grande) et police : - *Affichage des logs* : taille du texte (très petite, petite, moyenne, grande), densité
la police monospace du système, ou l'une des 12 polices libres conçues pour le texte dense (normale, ou compacte pour afficher environ 50 % de lignes en plus à l'écran) et police :
(JetBrains Mono, Fira Code, Source Code Pro, IBM Plex Mono, Cascadia Code, Roboto Mono, la police monospace du système, [Iosevka](https://github.com/be5invis/Iosevka) (étroite,
Ubuntu Mono, Inconsolata, Red Hat Mono, Noto Sans Mono, Victor Mono, DM Mono). Elles sont donc plus de texte par ligne ; servie par LogStream lui-même, fonctionne hors ligne), ou
chargées par le navigateur depuis [Bunny Fonts](https://fonts.bunny.net), un service l'une des 12 polices libres conçues pour le texte dense (JetBrains Mono, Fira Code, Source
européen de polices respectueux de la vie privée ; sans accès à internet, la police du Code Pro, IBM Plex Mono, Cascadia Code, Roboto Mono, Ubuntu Mono, Inconsolata, Red Hat Mono,
système est utilisée. Les ligatures sont désactivées pour que `->` ou `!=` s'affichent tels Noto Sans Mono, Victor Mono, DM Mono). Ces 12 polices sont chargées par le navigateur depuis
quels. [Bunny Fonts](https://fonts.bunny.net), un service européen de polices respectueux de la vie
privée ; sans accès à internet, la police du système est utilisée. Les ligatures sont
désactivées pour que `->` ou `!=` s'affichent tels quels. Le réglage le plus dense est Très
petite + Compacte + Iosevka.
- **Données** : « Supprimer tous les logs » efface définitivement tous les logs stockés (il faut - **Données** : « Supprimer tous les logs » efface définitivement tous les logs stockés (il faut
taper `PURGE` pour confirmer). Les tags et les paramètres sont conservés. VictoriaLogs doit taper `PURGE` pour confirmer). Les tags et les paramètres sont conservés. VictoriaLogs doit
être lancé avec `-delete.enable` (déjà présent dans `docker-compose.yml`) ; mettez être lancé avec `-delete.enable` (déjà présent dans `docker-compose.yml`) ; mettez
+16 -6
View File
@@ -207,15 +207,25 @@ remembered per browser.
(`logstream-data` volume), so they are shared by every browser. Default tags (pastel): (`logstream-data` volume), so they are shared by every browser. Default tags (pastel):
`warning` (orange), `error` (red), `ok` (green). Default tags still using the colors of `warning` (orange), `error` (red), `ok` (green). Default tags still using the colors of
earlier versions are switched to the pastel ones automatically. earlier versions are switched to the pastel ones automatically.
The *+ Preset…* menu adds ready-made tags for HTTP/HTTPS access logs (nginx and Apache
common/combined, Traefik CLF and JSON, Caddy JSON, HAProxy httplog): status codes (2xx green,
3xx blue, 4xx orange, 5xx red), methods, probes and attacks (`wp-login.php`, `/.env`,
`../`…), bots and scripts, TLS and proxy errors. Tags already in the list are skipped, and the
added tags can be edited like any other. In a regular expression, a group named `hl`
(`(?<hl>…)`) colors only that part of the match: the presets use it to color the status code
or the method, not the text around it.
- **Interface** - **Interface**
- *Theme*: System (follows the computer/phone preference), Light or Dark. The sun/moon - *Theme*: System (follows the computer/phone preference), Light or Dark. The sun/moon
button in the header switches between light and dark. button in the header switches between light and dark.
- *Log display*: font size (tiny, small, medium, large) and font: the system monospace - *Log display*: font size (tiny, small, medium, large), density (normal, or compact to
font, or one of 12 free fonts made for dense text (JetBrains Mono, Fira Code, Source fit about 50% more lines on screen) and font: the system monospace font,
Code Pro, IBM Plex Mono, Cascadia Code, Roboto Mono, Ubuntu Mono, Inconsolata, Red Hat [Iosevka](https://github.com/be5invis/Iosevka) (narrow, so more text fits on each line;
Mono, Noto Sans Mono, Victor Mono, DM Mono). They are loaded by the browser from served by LogStream itself, works offline), or one of 12 free fonts made for dense text
[Bunny Fonts](https://fonts.bunny.net), a privacy-friendly European font service; without (JetBrains Mono, Fira Code, Source Code Pro, IBM Plex Mono, Cascadia Code, Roboto Mono,
internet access, the system font is used. Ligatures are disabled so `->` or `!=` show as typed. Ubuntu Mono, Inconsolata, Red Hat Mono, Noto Sans Mono, Victor Mono, DM Mono). These 12 are
loaded by the browser from [Bunny Fonts](https://fonts.bunny.net), a privacy-friendly
European font service; without internet access, the system font is used. Ligatures are
disabled so `->` or `!=` show as typed. The densest setting is Tiny + Compact + Iosevka.
- **Data**: "Delete all logs" permanently erases every stored log (you must type - **Data**: "Delete all logs" permanently erases every stored log (you must type
`PURGE` to confirm). Tags and settings are kept. VictoriaLogs needs `-delete.enable` `PURGE` to confirm). Tags and settings are kept. VictoriaLogs needs `-delete.enable`
(already set in `docker-compose.yml`); set `ALLOW_PURGE=false` to disable the feature. (already set in `docker-compose.yml`); set `ALLOW_PURGE=false` to disable the feature.
+2
View File
@@ -17,6 +17,7 @@ import (
type Tag struct { type Tag struct {
ID string `json:"id"` ID string `json:"id"`
Pattern string `json:"pattern"` Pattern string `json:"pattern"`
Label string `json:"label,omitempty"` // shown instead of the pattern (presets)
Color string `json:"color"` Color string `json:"color"`
WholeWord bool `json:"wholeWord"` WholeWord bool `json:"wholeWord"`
CaseSensitive bool `json:"caseSensitive"` CaseSensitive bool `json:"caseSensitive"`
@@ -72,6 +73,7 @@ var (
func (t *Tag) validate() error { func (t *Tag) validate() error {
t.Pattern = strings.TrimSpace(t.Pattern) t.Pattern = strings.TrimSpace(t.Pattern)
t.Label = strings.TrimSpace(t.Label)
if t.Pattern == "" { if t.Pattern == "" {
return &codedError{code: "pattern_required", msg: "the keyword is required"} return &codedError{code: "pattern_required", msg: "the keyword is required"}
} }
+93 -10
View File
@@ -48,6 +48,12 @@ const I18N = {
newTag: 'new', newTag: 'new',
confirmDelete: (p) => `Delete tag "${p}"?`, confirmDelete: (p) => `Delete tag "${p}"?`,
confirmReset: 'Replace all tags with the defaults (warning, error, ok)?', confirmReset: 'Replace all tags with the defaults (warning, error, ok)?',
presetAria: 'Add a preset', presetPick: '+ Preset…',
preset_http_status: 'HTTP status codes', preset_http_methods: 'HTTP methods',
preset_http_probes: 'Probes and attacks', preset_http_bots: 'Bots and scripts',
preset_http_errors: 'TLS/HTTPS and proxy errors',
pl_probes: 'probes / attacks', pl_bots: 'bots / scripts', pl_tls: 'TLS errors', pl_proxy: 'proxy errors',
presetAdded: (n) => (n ? `${n} tag(s) added` : 'These tags are already in the list'),
tagsLoadErr: 'Tags: ', tagsLoadErr: 'Tags: ',
err_pattern_required: 'The keyword is required', err_pattern_required: 'The keyword is required',
err_invalid_color: 'Invalid color (expected #rrggbb)', err_invalid_color: 'Invalid color (expected #rrggbb)',
@@ -113,8 +119,9 @@ const I18N = {
themeHelp: 'System follows the light/dark preference of your computer or phone.', themeHelp: 'System follows the light/dark preference of your computer or phone.',
logDisplay: 'Log display', fontSize: 'Font size', logDisplay: 'Log display', fontSize: 'Font size',
sizeTiny: 'Tiny', sizeSmall: 'Small', sizeMedium: 'Medium', sizeLarge: 'Large', sizeTiny: 'Tiny', sizeSmall: 'Small', sizeMedium: 'Medium', sizeLarge: 'Large',
fontLabel: 'Font', fontSystem: 'System monospace (no download)', density: 'Density', densityNormal: 'Normal', densityCompact: 'Compact',
fontHelp: 'Free fonts (SIL Open Font License) loaded by your browser from Bunny Fonts, a privacy-friendly European font service. Without internet access, the system font is used.', fontLabel: 'Font', fontSystem: 'System monospace (no download)', fontBuiltin: 'built in, narrow',
fontHelp: 'Free fonts (SIL Open Font License). Iosevka is served by LogStream itself and works offline; it is narrow, so more text fits on each line. The others are loaded by your browser from Bunny Fonts, a privacy-friendly European font service; without internet access, the system font is used.',
previewMsgs: ['error: link down on eth1, carrier lost', 'warning: disk /dev/sda temperature 58°C', 'backup finished: ok (12.4 GB in 00:14:32)'], previewMsgs: ['error: link down on eth1, carrier lost', 'warning: disk /dev/sda temperature 58°C', 'backup finished: ok (12.4 GB in 00:14:32)'],
dangerZone: 'Danger zone', dangerZone: 'Danger zone',
purgeHelp: 'Permanently delete every stored log to start from scratch. Color tags and settings are kept.', purgeHelp: 'Permanently delete every stored log to start from scratch. Color tags and settings are kept.',
@@ -192,6 +199,12 @@ const I18N = {
newTag: 'nouveau', newTag: 'nouveau',
confirmDelete: (p) => `Supprimer le tag « ${p} » ?`, confirmDelete: (p) => `Supprimer le tag « ${p} » ?`,
confirmReset: 'Remplacer tous les tags par les tags par défaut (warning, error, ok) ?', confirmReset: 'Remplacer tous les tags par les tags par défaut (warning, error, ok) ?',
presetAria: 'Ajouter un préréglage', presetPick: '+ Préréglage…',
preset_http_status: 'Codes HTTP', preset_http_methods: 'Méthodes HTTP',
preset_http_probes: 'Sondes et attaques', preset_http_bots: 'Robots et scripts',
preset_http_errors: 'Erreurs TLS/HTTPS et proxy',
pl_probes: 'sondes / attaques', pl_bots: 'robots / scripts', pl_tls: 'erreurs TLS', pl_proxy: 'erreurs proxy',
presetAdded: (n) => (n ? `${n} tag(s) ajouté(s)` : 'Ces tags sont déjà dans la liste'),
tagsLoadErr: 'Tags : ', tagsLoadErr: 'Tags : ',
err_pattern_required: 'Le mot-clé est obligatoire', err_pattern_required: 'Le mot-clé est obligatoire',
err_invalid_color: 'Couleur invalide (format #rrggbb attendu)', err_invalid_color: 'Couleur invalide (format #rrggbb attendu)',
@@ -257,8 +270,9 @@ const I18N = {
themeHelp: 'Système suit la préférence clair/sombre de votre ordinateur ou de votre téléphone.', themeHelp: 'Système suit la préférence clair/sombre de votre ordinateur ou de votre téléphone.',
logDisplay: 'Affichage des logs', fontSize: 'Taille du texte', logDisplay: 'Affichage des logs', fontSize: 'Taille du texte',
sizeTiny: 'Très petite', sizeSmall: 'Petite', sizeMedium: 'Moyenne', sizeLarge: 'Grande', sizeTiny: 'Très petite', sizeSmall: 'Petite', sizeMedium: 'Moyenne', sizeLarge: 'Grande',
fontLabel: 'Police', fontSystem: 'Monospace du système (aucun téléchargement)', density: 'Densité', densityNormal: 'Normale', densityCompact: 'Compacte',
fontHelp: 'Polices libres (licence SIL Open Font) chargées par votre navigateur depuis Bunny Fonts, un service européen respectueux de la vie privée. Sans accès à internet, la police du système est utilisée.', fontLabel: 'Police', fontSystem: 'Monospace du système (aucun téléchargement)', fontBuiltin: 'intégrée, étroite',
fontHelp: 'Polices libres (licence SIL Open Font). Iosevka est servie par LogStream lui-même et fonctionne hors ligne ; elle est étroite, donc chaque ligne affiche plus de texte. Les autres sont chargées par votre navigateur depuis Bunny Fonts, un service européen respectueux de la vie privée ; sans accès à internet, la police du système est utilisée.',
previewMsgs: ['error: link down on eth1, carrier lost', 'warning: disk /dev/sda temperature 58°C', 'backup finished: ok (12.4 GB in 00:14:32)'], previewMsgs: ['error: link down on eth1, carrier lost', 'warning: disk /dev/sda temperature 58°C', 'backup finished: ok (12.4 GB in 00:14:32)'],
dangerZone: 'Zone de danger', dangerZone: 'Zone de danger',
purgeHelp: 'Supprime définitivement tous les logs stockés pour repartir de zéro. Les tags de couleur et les réglages sont conservés.', purgeHelp: 'Supprime définitivement tous les logs stockés pour repartir de zéro. Les tags de couleur et les réglages sont conservés.',
@@ -519,9 +533,11 @@ $('#themeSwitch').addEventListener('click', (ev) => {
/* ================= Log font and size ================= */ /* ================= Log font and size ================= */
// Free monospace fonts, served by Bunny Fonts (https://fonts.bunny.net). // Free monospace fonts, served by Bunny Fonts (https://fonts.bunny.net), except the
// built-in ones (web/fonts, declared in style.css), which also work offline.
const LOG_FONTS = [ const LOG_FONTS = [
{ id: 'system' }, { id: 'system' },
{ id: 'iosevka', family: 'Iosevka', builtin: true },
{ id: 'jetbrains-mono', family: 'JetBrains Mono' }, { id: 'jetbrains-mono', family: 'JetBrains Mono' },
{ id: 'fira-code', family: 'Fira Code' }, { id: 'fira-code', family: 'Fira Code' },
{ id: 'source-code-pro', family: 'Source Code Pro' }, { id: 'source-code-pro', family: 'Source Code Pro' },
@@ -536,7 +552,8 @@ const LOG_FONTS = [
{ id: 'dm-mono', family: 'DM Mono', weights: '400,500' }, { id: 'dm-mono', family: 'DM Mono', weights: '400,500' },
]; ];
const LOG_SIZES = { tiny: '10.5px', small: '11.5px', medium: '12.5px', large: '14px' }; const LOG_SIZES = { tiny: '10.5px', small: '11.5px', medium: '12.5px', large: '14px' };
const ui = { font: 'system', size: 'medium' }; const LOG_DENSITIES = ['normal', 'compact'];
const ui = { font: 'system', size: 'medium', density: 'normal' };
function applyLogFont(id) { function applyLogFont(id) {
const f = LOG_FONTS.find((x) => x.id === id) || LOG_FONTS[0]; const f = LOG_FONTS.find((x) => x.id === id) || LOG_FONTS[0];
@@ -546,6 +563,10 @@ function applyLogFont(id) {
root.removeProperty('--log-font'); root.removeProperty('--log-font');
return; return;
} }
if (f.builtin) {
root.setProperty('--log-font', `'${f.family}', var(--mono)`);
return;
}
const href = `https://fonts.bunny.net/css?family=${f.id}:${f.weights || '400,700'}&display=swap`; const href = `https://fonts.bunny.net/css?family=${f.id}:${f.weights || '400,700'}&display=swap`;
let link = document.getElementById('logFontCss'); let link = document.getElementById('logFontCss');
if (!link) { if (!link) {
@@ -561,13 +582,22 @@ function applyLogSize(id) {
document.documentElement.style.setProperty('--log-size', LOG_SIZES[ui.size]); document.documentElement.style.setProperty('--log-size', LOG_SIZES[ui.size]);
} }
function applyLogDensity(id) {
ui.density = LOG_DENSITIES.includes(id) ? id : 'normal';
document.documentElement.dataset.density = ui.density;
}
function renderInterface() { function renderInterface() {
renderThemeSwitch(); renderThemeSwitch();
for (const b of document.querySelectorAll('#sizeSwitch [data-size]')) { for (const b of document.querySelectorAll('#sizeSwitch [data-size]')) {
b.setAttribute('aria-checked', String(b.dataset.size === ui.size)); b.setAttribute('aria-checked', String(b.dataset.size === ui.size));
} }
for (const b of document.querySelectorAll('#densitySwitch [data-density]')) {
b.setAttribute('aria-checked', String(b.dataset.density === ui.density));
}
const sel = $('#fontSelect'); const sel = $('#fontSelect');
sel.innerHTML = LOG_FONTS.map((f) => `<option value="${f.id}">${esc(f.family || t('fontSystem'))}</option>`).join(''); const fontName = (f) => (!f.family ? t('fontSystem') : f.builtin ? `${f.family} (${t('fontBuiltin')})` : f.family);
sel.innerHTML = LOG_FONTS.map((f) => `<option value="${f.id}">${esc(fontName(f))}</option>`).join('');
sel.value = ui.font; sel.value = ui.font;
renderFontPreview(); renderFontPreview();
renderHistoSettings(); renderHistoSettings();
@@ -591,6 +621,13 @@ $('#sizeSwitch').addEventListener('click', (ev) => {
store.set('logSize', ui.size); store.set('logSize', ui.size);
renderInterface(); renderInterface();
}); });
$('#densitySwitch').addEventListener('click', (ev) => {
const b = ev.target.closest('[data-density]');
if (!b) return;
applyLogDensity(b.dataset.density);
store.set('logDensity', ui.density);
renderInterface();
});
$('#fontSelect').addEventListener('change', (ev) => { $('#fontSelect').addEventListener('change', (ev) => {
applyLogFont(ev.target.value); applyLogFont(ev.target.value);
store.set('logFont', ui.font); store.set('logFont', ui.font);
@@ -681,7 +718,10 @@ function compileMatchers() {
let src = tag.regex ? tag.pattern : escapeRe(tag.pattern); let src = tag.regex ? tag.pattern : escapeRe(tag.pattern);
if (tag.wholeWord) src = `(?<![\\p{L}\\p{N}_])(?:${src})(?![\\p{L}\\p{N}_])`; if (tag.wholeWord) src = `(?<![\\p{L}\\p{N}_])(?:${src})(?![\\p{L}\\p{N}_])`;
try { try {
out.push({ re: new RegExp(src, 'gu' + (tag.caseSensitive ? '' : 'i')), html: `<mark class="tag" style="${tagStyle(tag.color)}">` }); // A regex may name a group "hl" to color only that part of the match.
const hl = tag.regex && /\(\?P?<hl>/.test(tag.pattern);
if (hl) src = src.replace(/\(\?P<hl>/g, '(?<hl>');
out.push({ re: new RegExp(src, 'gu' + (hl ? 'd' : '') + (tag.caseSensitive ? '' : 'i')), html: `<mark class="tag" style="${tagStyle(tag.color)}">` });
} catch (e) { } catch (e) {
console.warn('Tag skipped (invalid pattern):', tag.pattern, e.message); console.warn('Tag skipped (invalid pattern):', tag.pattern, e.message);
} }
@@ -706,6 +746,8 @@ function highlight(text) {
let x; let x;
while ((x = m.re.exec(text)) !== null) { while ((x = m.re.exec(text)) !== null) {
if (x[0] === '') { m.re.lastIndex++; continue; } if (x[0] === '') { m.re.lastIndex++; continue; }
const g = x.indices?.groups?.hl;
if (g) { if (g[1] > g[0]) spans.push({ s: g[0], e: g[1], prio, html: m.html }); continue; }
spans.push({ s: x.index, e: x.index + x[0].length, prio, html: m.html }); spans.push({ s: x.index, e: x.index + x[0].length, prio, html: m.html });
} }
}); });
@@ -1978,6 +2020,28 @@ $('#purgeBtn').addEventListener('click', async () => {
const PALETTE = ['#6366f1', '#0ea5e9', '#14b8a6', '#a855f7', '#ec4899', '#eab308', '#64748b', '#f97316']; const PALETTE = ['#6366f1', '#0ea5e9', '#14b8a6', '#a855f7', '#ec4899', '#eab308', '#64748b', '#f97316'];
// Ready-made tags for HTTP/HTTPS access logs. The patterns are valid in both
// JavaScript and Go (RE2) and cover nginx/Apache (common, combined), Traefik
// (CLF, JSON), Caddy (JSON) and HAProxy (httplog). For status codes and
// methods only the "hl" group is colored, not the context around it.
const HTTP_STATUS_CTX = '(?:" |"(?:status|DownstreamStatus|OriginStatus|status_code)": ?|(?:-1|\\d+)/(?:-1|\\d+)/(?:-1|\\d+)/(?:-1|\\d+)/\\+?\\d+ )';
const httpStatus = (d, color) => ({ label: `HTTP ${d}xx`, pattern: `${HTTP_STATUS_CTX}(?<hl>${d}\\d\\d)\\b`, color });
const httpMethod = (m, color) => ({ label: m.replace(/\|/g, '/'), pattern: `"(?<hl>${m})[ "]`, color, caseSensitive: true });
const PRESETS = {
http_status: () => [httpStatus(2, '#86efac'), httpStatus(3, '#93c5fd'), httpStatus(4, '#fdba74'), httpStatus(5, '#f87171')],
http_methods: () => [httpMethod('GET|HEAD|OPTIONS', '#cbd5e1'), httpMethod('POST|PUT|PATCH', '#c4b5fd'), httpMethod('DELETE', '#f9a8d4')],
http_probes: () => [{ label: t('pl_probes'), color: '#fda4af',
pattern: '(?:wp-login\\.php|xmlrpc\\.php|wp-admin|phpmyadmin|/\\.env|/\\.git|/\\.aws|/cgi-bin/|\\.\\./|%2e%2e|/etc/passwd|<script|union(?:\\s|%20|\\+)+select)' }],
http_bots: () => [{ label: t('pl_bots'), color: '#fde68a',
pattern: '\\b(?:[a-z]*bot|crawler|spider|curl|wget|python-requests|Go-http-client|zgrab|masscan|nmap|sqlmap|nikto)\\b' }],
http_errors: () => [
{ label: t('pl_tls'), color: '#f0abfc',
pattern: '(?:TLS handshake error|SSL_do_handshake\\(\\) failed|SSL handshake|SSL routines|certificate (?:has )?expired|certificate verify failed|bad certificate|unknown certificate|x509:)' },
{ label: t('pl_proxy'), color: '#fdba74',
pattern: '(?:upstream timed out|upstream prematurely closed|no live upstreams|connect\\(\\) failed|connection refused|bad gateway|gateway time-?out|service unavailable)' },
],
};
async function loadTags() { async function loadTags() {
try { state.tags = await api('/api/tags'); } catch (e) { toast(t('tagsLoadErr') + e.message); } try { state.tags = await api('/api/tags'); } catch (e) { toast(t('tagsLoadErr') + e.message); }
compileMatchers(); compileMatchers();
@@ -1989,7 +2053,7 @@ function tagRowHTML(tag) {
return `<div class="tag-row${tag.enabled ? '' : ' off'}" data-id="${esc(tag.id)}"> return `<div class="tag-row${tag.enabled ? '' : ' off'}" data-id="${esc(tag.id)}">
<input type="color" value="${esc(tag.color)}" data-f="color" aria-label="${esc(t('colorAria'))}"> <input type="color" value="${esc(tag.color)}" data-f="color" aria-label="${esc(t('colorAria'))}">
<input type="text" value="${esc(tag.pattern)}" data-f="pattern" placeholder="${esc(t('keyword'))}" spellcheck="false" aria-label="${esc(t('keyword'))}"> <input type="text" value="${esc(tag.pattern)}" data-f="pattern" placeholder="${esc(t('keyword'))}" spellcheck="false" aria-label="${esc(t('keyword'))}">
<span class="preview"><mark class="tag" style="${tagStyle(tag.color)}">${esc(tag.pattern || t('preview'))}</mark></span> <span class="preview" title="${esc(tag.pattern)}"><mark class="tag" style="${tagStyle(tag.color)}">${esc(tag.label || tag.pattern || t('preview'))}</mark></span>
<div class="opts"> <div class="opts">
${opt('wholeWord', t('optWhole'), t('optWholeTitle'))} ${opt('wholeWord', t('optWhole'), t('optWholeTitle'))}
${opt('caseSensitive', 'Aa', t('optCaseTitle'))} ${opt('caseSensitive', 'Aa', t('optCaseTitle'))}
@@ -2032,7 +2096,7 @@ $('#tagList').addEventListener('input', (ev) => {
tag[field] = ev.target.type === 'checkbox' ? ev.target.checked : ev.target.value; tag[field] = ev.target.type === 'checkbox' ? ev.target.checked : ev.target.value;
rowEl.classList.toggle('off', !tag.enabled); rowEl.classList.toggle('off', !tag.enabled);
const mark = rowEl.querySelector('.preview mark'); const mark = rowEl.querySelector('.preview mark');
mark.textContent = tag.pattern || t('preview'); mark.textContent = tag.label || tag.pattern || t('preview');
mark.setAttribute('style', tagStyle(tag.color)); mark.setAttribute('style', tagStyle(tag.color));
scheduleSave(tag, rowEl); scheduleSave(tag, rowEl);
applyTags(); applyTags();
@@ -2065,6 +2129,24 @@ $('#addTag').addEventListener('click', async () => {
} catch (e) { toast(e.message); } } catch (e) { toast(e.message); }
}); });
// Adds a preset group, skipping tags whose pattern is already in the list.
$('#presetTags').addEventListener('change', async (ev) => {
const make = PRESETS[ev.target.value];
ev.target.value = '';
if (!make) return;
let added = 0;
try {
for (const p of make()) {
if (state.tags.some((x) => x.pattern === p.pattern)) continue;
state.tags.push(await api('/api/tags', { method: 'POST', body: { regex: true, enabled: true, ...p } }));
added++;
}
} catch (e) { toast(e.message); }
renderTagList();
applyTags();
toast(t('presetAdded', added));
});
$('#resetTags').addEventListener('click', async () => { $('#resetTags').addEventListener('click', async () => {
if (!confirm(t('confirmReset'))) return; if (!confirm(t('confirmReset'))) return;
try { try {
@@ -2098,6 +2180,7 @@ $('#settingsDlg').addEventListener('click', (ev) => { if (ev.target === ev.curre
} }
applyLogFont(store.get('logFont', 'system')); applyLogFont(store.get('logFont', 'system'));
applyLogSize(store.get('logSize', 'medium')); applyLogSize(store.get('logSize', 'medium'));
applyLogDensity(store.get('logDensity', 'normal'));
applyLang(); applyLang();
$('#range').value = store.get('range', '1h'); $('#range').value = store.get('range', '1h');
if (!$('#range').value) $('#range').value = '1h'; if (!$('#range').value) $('#range').value = '1h';
+110
View File
@@ -0,0 +1,110 @@
Copyright (c) 2015-2023, Renzhi Li (aka. Belleve Invis, belleve@typeof.net)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
--------------------------
SIL Open Font License v1.1
====================================================
Preamble
----------
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
Definitions
-------------
`"Font Software"` refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
`"Reserved Font Name"` refers to any names specified as such after the
copyright statement(s).
`"Original Version"` refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
`"Modified Version"` refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
`"Author"` refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
Permission & Conditions
------------------------
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1. Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2. Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3. No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4. The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5. The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
Termination
-----------
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
Binary file not shown.
+15
View File
@@ -188,7 +188,17 @@
<p class="muted small" data-i18n="tagsHelp"></p> <p class="muted small" data-i18n="tagsHelp"></p>
<div id="tagList" class="tag-list"></div> <div id="tagList" class="tag-list"></div>
<footer> <footer>
<span class="tag-add">
<button id="addTag" class="btn primary" type="button" data-i18n="addTag">+ Add tag</button> <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> <button id="resetTags" class="btn ghost" type="button" data-i18n="resetTags">Restore default tags</button>
</footer> </footer>
</section> </section>
@@ -271,6 +281,11 @@
<button type="button" role="radio" data-size="medium" data-i18n="sizeMedium">Medium</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> <button type="button" role="radio" data-size="large" data-i18n="sizeLarge">Large</button>
</div> </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> <label for="fontSelect" data-i18n="fontLabel">Font</label>
<select id="fontSelect" class="field"></select> <select id="fontSelect" class="field"></select>
</div> </div>
+13 -2
View File
@@ -1,3 +1,8 @@
/* Iosevka (SIL OFL, web/fonts/OFL-Iosevka.txt): a narrow monospace font served by LogStream
itself, so it also works offline. Latin subset only; other scripts fall back to --mono. */
@font-face { font-family: "Iosevka"; src: url("fonts/iosevka-400.woff2") format("woff2"); font-weight: 400 500; font-display: swap; }
@font-face { font-family: "Iosevka"; src: url("fonts/iosevka-700.woff2") format("woff2"); font-weight: 600 800; font-display: swap; }
/* ---------- Theme: everything goes through these variables ---------- */ /* ---------- Theme: everything goes through these variables ---------- */
:root { :root {
--bg: #f6f7f9; --bg: #f6f7f9;
@@ -341,6 +346,9 @@ body.col-resizing, body.col-resizing * { cursor: col-resize !important; user-sel
cursor: pointer; cursor: pointer;
} }
.row:last-child { border-bottom: 0; } .row:last-child { border-bottom: 0; }
/* Settings > Interface > Density: compact fits about 50% more lines on screen */
:root[data-density="compact"] .row { padding-top: 1px; padding-bottom: 1px; line-height: 1.25; }
:root[data-density="compact"] .row .sev { padding-top: 0; padding-bottom: 0; line-height: 1.3; }
.row:hover { background: var(--row-hover); } .row:hover { background: var(--row-hover); }
.row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; } .row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.row.new { animation: flash 1.2s ease-out; } .row.new { animation: flash 1.2s ease-out; }
@@ -578,6 +586,8 @@ input.switch:disabled { cursor: not-allowed; }
input.switch:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } input.switch:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.set-section footer { display: flex; flex-wrap: wrap; gap: 8px; justify-content: space-between; margin-top: 14px; } .set-section footer { display: flex; flex-wrap: wrap; gap: 8px; justify-content: space-between; margin-top: 14px; }
.tag-add { display: flex; flex-wrap: wrap; gap: 8px; }
.tag-add select.field { width: auto; }
.seg { display: inline-flex; padding: 3px; gap: 3px; background: var(--panel-2); border: 1px solid var(--border); border-radius: 10px; } .seg { display: inline-flex; padding: 3px; gap: 3px; background: var(--panel-2); border: 1px solid var(--border); border-radius: 10px; }
.seg button { .seg button {
@@ -663,8 +673,9 @@ input.switch:focus-visible { outline: 2px solid var(--accent); outline-offset: 2
} }
.set-nav button span { max-width: 100%; overflow: hidden; text-overflow: ellipsis; } .set-nav button span { max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
.set-panels { padding: 0 16px 18px; } .set-panels { padding: 0 16px 18px; }
#sizeSwitch { display: flex; } #sizeSwitch, #densitySwitch { display: flex; }
#sizeSwitch button { flex: 1; padding: 0 4px; white-space: nowrap; font-size: 12px; } #sizeSwitch button, #densitySwitch button { flex: 1; padding: 0 4px; white-space: nowrap; font-size: 12px; }
:root[data-density="compact"] .row { padding-top: 3px; padding-bottom: 3px; }
.field-grid select { margin-bottom: 6px; } .field-grid select { margin-bottom: 6px; }
.status { padding: 6px 16px; } .status { padding: 6px 16px; }
.tag-row { grid-template-columns: 38px 1fr 36px; } .tag-row { grid-template-columns: 38px 1fr 36px; }