Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be58284916 | ||
|
|
7b139e8931 | ||
|
|
19ed16ac12 |
No files matched your search
@@ -228,6 +228,13 @@ 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.
|
||||||
|
|||||||
@@ -207,6 +207,13 @@ 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.
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ func (o *OIDC) handleCallback(w http.ResponseWriter, r *http.Request) {
|
|||||||
var ls loginState
|
var ls loginState
|
||||||
c, err := r.Cookie(loginCookie + state)
|
c, err := r.Cookie(loginCookie + state)
|
||||||
if state == "" || err != nil || !verifyCookie(o.key, c.Value, &ls) || time.Now().Unix() > ls.Exp {
|
if state == "" || err != nil || !verifyCookie(o.key, c.Value, &ls) || time.Now().Unix() > ls.Exp {
|
||||||
http.Error(w, "login expired or started in another browser: open logstream again", http.StatusBadRequest)
|
http.Error(w, "login expired or started in another browser: open LogStream again", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.SetCookie(w, &http.Cookie{Name: loginCookie + state, Path: "/", MaxAge: -1, HttpOnly: true, Secure: o.secure})
|
http.SetCookie(w, &http.Cookie{Name: loginCookie + state, Path: "/", MaxAge: -1, HttpOnly: true, Secure: o.secure})
|
||||||
@@ -260,7 +260,7 @@ func (o *OIDC) handleCallback(w http.ResponseWriter, r *http.Request) {
|
|||||||
user, err := o.exchange(r, q.Get("code"), ls)
|
user, err := o.exchange(r, q.Get("code"), ls)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("oidc: login failed: %v", err)
|
log.Printf("oidc: login failed: %v", err)
|
||||||
http.Error(w, "login failed, see the logstream logs", http.StatusForbidden)
|
http.Error(w, "login failed, see the LogStream logs", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("oidc: %s logged in", user)
|
log.Printf("oidc: %s logged in", user)
|
||||||
|
|||||||
@@ -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"}
|
||||||
}
|
}
|
||||||
|
|||||||
+66
-9
@@ -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)',
|
||||||
@@ -71,7 +77,7 @@ const I18N = {
|
|||||||
srcHost: 'Host system',
|
srcHost: 'Host system',
|
||||||
hostTitle: 'Host system logs',
|
hostTitle: 'Host system logs',
|
||||||
hostEnabled: 'Collect the system logs of this machine',
|
hostEnabled: 'Collect the system logs of this machine',
|
||||||
hostOff: 'Off: the system logs of the machine hosting Logstream are not collected.',
|
hostOff: 'Off: the system logs of the machine hosting LogStream are not collected.',
|
||||||
hostWaiting: 'Starting…',
|
hostWaiting: 'Starting…',
|
||||||
hostJournal: ({ n, r }) => `Reading the systemd journal (${n} files): ${r} entries since startup.`,
|
hostJournal: ({ n, r }) => `Reading the systemd journal (${n} files): ${r} entries since startup.`,
|
||||||
hostFiles: ({ n, r }) => `Following ${n} files of /var/log: ${r} lines since startup.`,
|
hostFiles: ({ n, r }) => `Following ${n} files of /var/log: ${r} lines since startup.`,
|
||||||
@@ -98,8 +104,8 @@ const I18N = {
|
|||||||
dockerLegend: 'Colored: followed · grey: not followed · dashed: stopped · the color identifies the compose project. Click a label to switch it.',
|
dockerLegend: 'Colored: followed · grey: not followed · dashed: stopped · the color identifies the compose project. Click a label to switch it.',
|
||||||
dockerNoMatch: 'No container',
|
dockerNoMatch: 'No container',
|
||||||
stFollowing: 'followed', stSelected: 'followed when running', stIgnored: 'not followed', stStopped: 'stopped',
|
stFollowing: 'followed', stSelected: 'followed when running', stIgnored: 'not followed', stStopped: 'stopped',
|
||||||
stLocked: 'excluded', stLockedTitle: 'Logstream itself, or label logstream.exclude=true',
|
stLocked: 'excluded', stLockedTitle: 'LogStream itself, or label logstream.exclude=true',
|
||||||
dockerHelp: 'Logs are read through docker-socket-proxy, a read-only gateway: Logstream can list containers and read their logs, nothing else. Choices apply per compose service (or container name), so they survive container re-creations.',
|
dockerHelp: 'Logs are read through docker-socket-proxy, a read-only gateway: LogStream can list containers and read their logs, nothing else. Choices apply per compose service (or container name), so they survive container re-creations.',
|
||||||
fContainer: 'container', fContainerId: 'container ID', fImage: 'image', fProject: 'compose project',
|
fContainer: 'container', fContainerId: 'container ID', fImage: 'image', fProject: 'compose project',
|
||||||
fService: 'compose service', fStream: 'stream', fSourceType: 'source',
|
fService: 'compose service', fStream: 'stream', fSourceType: 'source',
|
||||||
fUnit: 'systemd unit', fLogFile: 'log file',
|
fUnit: 'systemd unit', fLogFile: 'log file',
|
||||||
@@ -192,6 +198,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)',
|
||||||
@@ -215,7 +227,7 @@ const I18N = {
|
|||||||
srcHost: 'Système hôte',
|
srcHost: 'Système hôte',
|
||||||
hostTitle: 'Logs système de l\'hôte',
|
hostTitle: 'Logs système de l\'hôte',
|
||||||
hostEnabled: 'Collecter les logs système de cette machine',
|
hostEnabled: 'Collecter les logs système de cette machine',
|
||||||
hostOff: 'Désactivé : les logs système de la machine qui héberge Logstream ne sont pas collectés.',
|
hostOff: 'Désactivé : les logs système de la machine qui héberge LogStream ne sont pas collectés.',
|
||||||
hostWaiting: 'Démarrage…',
|
hostWaiting: 'Démarrage…',
|
||||||
hostJournal: ({ n, r }) => `Lecture du journal systemd (${n} fichiers) : ${r} entrées depuis le démarrage.`,
|
hostJournal: ({ n, r }) => `Lecture du journal systemd (${n} fichiers) : ${r} entrées depuis le démarrage.`,
|
||||||
hostFiles: ({ n, r }) => `Suivi de ${n} fichiers de /var/log : ${r} lignes depuis le démarrage.`,
|
hostFiles: ({ n, r }) => `Suivi de ${n} fichiers de /var/log : ${r} lignes depuis le démarrage.`,
|
||||||
@@ -242,8 +254,8 @@ const I18N = {
|
|||||||
dockerLegend: 'En couleur : suivis · en gris : non suivis · pointillés : arrêtés · la couleur identifie le projet compose. Cliquez sur une étiquette pour la basculer.',
|
dockerLegend: 'En couleur : suivis · en gris : non suivis · pointillés : arrêtés · la couleur identifie le projet compose. Cliquez sur une étiquette pour la basculer.',
|
||||||
dockerNoMatch: 'Aucun conteneur',
|
dockerNoMatch: 'Aucun conteneur',
|
||||||
stFollowing: 'suivi', stSelected: 'suivi dès qu\'il tourne', stIgnored: 'non suivi', stStopped: 'arrêté',
|
stFollowing: 'suivi', stSelected: 'suivi dès qu\'il tourne', stIgnored: 'non suivi', stStopped: 'arrêté',
|
||||||
stLocked: 'exclu', stLockedTitle: 'Logstream lui-même, ou étiquette logstream.exclude=true',
|
stLocked: 'exclu', stLockedTitle: 'LogStream lui-même, ou étiquette logstream.exclude=true',
|
||||||
dockerHelp: 'Les logs sont lus via docker-socket-proxy, une passerelle en lecture seule : Logstream peut lister les conteneurs et lire leurs logs, rien d\'autre. Les choix s\'appliquent par service compose (ou nom de conteneur), ils survivent donc à la recréation des conteneurs.',
|
dockerHelp: 'Les logs sont lus via docker-socket-proxy, une passerelle en lecture seule : LogStream peut lister les conteneurs et lire leurs logs, rien d\'autre. Les choix s\'appliquent par service compose (ou nom de conteneur), ils survivent donc à la recréation des conteneurs.',
|
||||||
fContainer: 'conteneur', fContainerId: 'ID du conteneur', fImage: 'image', fProject: 'projet compose',
|
fContainer: 'conteneur', fContainerId: 'ID du conteneur', fImage: 'image', fProject: 'projet compose',
|
||||||
fService: 'service compose', fStream: 'flux', fSourceType: 'source',
|
fService: 'service compose', fStream: 'flux', fSourceType: 'source',
|
||||||
fUnit: 'unité systemd', fLogFile: 'fichier de log',
|
fUnit: 'unité systemd', fLogFile: 'fichier de log',
|
||||||
@@ -681,7 +693,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 +721,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 +1995,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 +2028,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 +2071,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 +2104,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 {
|
||||||
|
|||||||
+13
-3
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Logstream</title>
|
<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="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">
|
<link rel="stylesheet" href="style.css">
|
||||||
<script>
|
<script>
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
<div class="brand">
|
<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>
|
<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>
|
<span>LogStream</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="search">
|
<div class="search">
|
||||||
@@ -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>
|
||||||
<button id="addTag" class="btn primary" type="button" data-i18n="addTag">+ Add tag</button>
|
<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>
|
<button id="resetTags" class="btn ghost" type="button" data-i18n="resetTags">Restore default tags</button>
|
||||||
</footer>
|
</footer>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
+2
-2
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Logstream</title>
|
<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="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">
|
<link rel="stylesheet" href="style.css">
|
||||||
<script>
|
<script>
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
<img id="logo" class="login-logo" src="auth/logo" alt="" hidden>
|
<img id="logo" class="login-logo" src="auth/logo" alt="" hidden>
|
||||||
<div class="brand">
|
<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>
|
<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>
|
<span>LogStream</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="muted" data-i18n="intro">Sign in to view the logs.</p>
|
<p class="muted" data-i18n="intro">Sign in to view the logs.</p>
|
||||||
|
|
||||||
|
|||||||
@@ -578,6 +578,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 {
|
||||||
|
|||||||
Reference in new issue
Block a user