From 541e98fa4995aa8940cb970ddbb002b306d0c2bb Mon Sep 17 00:00:00 2001 From: abrusco Date: Mon, 6 Jul 2026 10:00:59 -0300 Subject: [PATCH 1/7] feat: polish standalone UI and bump to v0.9.3 --- docs/app.js | 53 ++- docs/index.html | 227 +++++------ docs/styles.css | 988 ++++++++++++++++++++++++++++++---------------- docs/version.json | 2 +- 4 files changed, 781 insertions(+), 489 deletions(-) diff --git a/docs/app.js b/docs/app.js index 2eff11b..d335901 100644 --- a/docs/app.js +++ b/docs/app.js @@ -175,6 +175,8 @@ const elements = { selectedFile: document.querySelector('#selectedFile'), selectedTitle: document.querySelector('#selectedTitle'), metaObject: document.querySelector('#metaObject'), + nodeName: document.querySelector('#nodeName'), + roleName: document.querySelector('#roleName'), organization: document.querySelector('#organization'), date: document.querySelector('#date'), duration: document.querySelector('#duration'), @@ -719,13 +721,16 @@ function highlightSql(sql) { 'NVL', 'TO_DATE', 'TRUNC', 'REGEXP_SUBSTR', 'SYSDATE', 'COUNT', 'SUM', 'MIN', 'MAX', 'AVG', 'SUBSTR', 'DECODE', 'COALESCE', 'TO_CHAR', 'TO_NUMBER' ]); - const tokenPattern = /(--[^\n]*|'(?:''|[^'])*'|\b\d+(?:\.\d+)?\b|\b[A-Za-z_][A-Za-z0-9_$#]*\b|<>|<=|>=|:=|[=+\-*/(),.;])/g; + const tokenPattern = /(--[^\n]*|"(?:""|[^"])*"|'(?:''|[^'])*'|\b\d+(?:\.\d+)?\b|\b[A-Za-z_][A-Za-z0-9_$#]*\b|<>|<=|>=|:=|[=+\-*/(),.;])/g; return String(sql || '').replace(tokenPattern, (token) => { const escaped = escapeHtml(token); if (token.startsWith('--')) { return `${escaped}`; } + if (token.startsWith('"')) { + return `${escaped}`; + } if (token.startsWith("'")) { return `${escaped}`; } @@ -924,6 +929,8 @@ function renderSelectedEntry() { elements.selectedFile.textContent = 'Nenhum item selecionado'; elements.selectedTitle.textContent = 'Selecione uma query'; elements.metaObject.textContent = '-'; + elements.nodeName.textContent = '-'; + elements.roleName.textContent = '-'; elements.organization.textContent = '-'; elements.date.textContent = '-'; elements.duration.textContent = '-'; @@ -939,6 +946,8 @@ function renderSelectedEntry() { elements.selectedFile.textContent = `${entry.fileName} | linha ${entry.lineNumber}`; elements.selectedTitle.textContent = entryDisplayTitle(entry); elements.metaObject.textContent = entry.meta4Object || '-'; + elements.nodeName.textContent = entry.node || '-'; + elements.roleName.textContent = entry.connection?.role || '-'; elements.organization.textContent = entry.connection?.organization || '-'; elements.date.textContent = formatDate(entry.connection?.date); elements.duration.textContent = entry.durationMs == null ? '-' : `${entry.durationMs} ms`; @@ -946,7 +955,8 @@ function renderSelectedEntry() { const sqlText = currentSqlText(entry); elements.sqlOutput.innerHTML = sqlText ? highlightSql(sqlText) : 'Sem SQL capturado.'; updateFormatSqlButton(); - elements.rawOutput.textContent = (entry.rawLines || []).join('\n'); + const rawText = (entry.rawLines || []).join('\n'); + elements.rawOutput.innerHTML = rawText ? highlightSql(rawText) : ''; renderTable(entry); } @@ -1228,28 +1238,37 @@ if (elements.formatSqlButton) { }); } -elements.copyButton.addEventListener('click', async () => { - const entry = selectedEntry(); - if (!entry?.sql) { - return; - } - await navigator.clipboard.writeText(currentSqlText(entry)); - const original = elements.copyButton.textContent; - elements.copyButton.textContent = 'Copiado'; - window.setTimeout(() => { - elements.copyButton.textContent = original; - }, 1200); -}); +if (elements.copyButton) { + elements.copyButton.addEventListener('click', async () => { + const entry = selectedEntry(); + if (!entry?.sql) { + return; + } + await navigator.clipboard.writeText(currentSqlText(entry)); + const original = elements.copyButton.textContent; + elements.copyButton.textContent = 'Copiado'; + window.setTimeout(() => { + elements.copyButton.textContent = original; + }, 1200); + }); +} -elements.refreshButton.addEventListener('click', reloadLoadedFiles); +if (elements.refreshButton) { + elements.refreshButton.addEventListener('click', reloadLoadedFiles); +} async function loadAppVersion() { + const appVersionElement = document.querySelector('#appVersion'); + if (!appVersionElement) { + return; + } + try { const response = await fetch('version.json', { cache: 'no-store' }); if (response.ok) { const data = await response.json(); const version = data.version || '0.0.0'; - document.querySelector('#appVersion').textContent = `v${version}`; + appVersionElement.textContent = `v${version}`; return; } } catch (e) { @@ -1258,7 +1277,7 @@ async function loadAppVersion() { const meta = document.querySelector('meta[name="app-version"]'); const v = (meta && meta.getAttribute('content')) || '0.0.0'; - document.querySelector('#appVersion').textContent = `v${v}`; + appVersionElement.textContent = `v${v}`; } initializeTheme(); diff --git a/docs/index.html b/docs/index.html index 76377a0..e8d4d97 100644 --- a/docs/index.html +++ b/docs/index.html @@ -3,151 +3,134 @@ - + M4 Log Visualizer - + +
+
+

Nenhum item selecionado

+

Selecione uma query

+
+ Objeto- + Node- + Role- + Data- + Tempo- + + +
+
+ +
+ +
+
+

SQL Oracle

+
+ + +
+
+
Aguardando seleção...
+
+ +
+
+

Resultado retornado no log

+ +
+
+
+
+
+ +
+
+

Trecho original

+
+

+          
+ +
+
+ + + + + + - - - - - - - - - - - - diff --git a/docs/styles.css b/docs/styles.css index 4c889ae..7cc3c50 100644 --- a/docs/styles.css +++ b/docs/styles.css @@ -1,144 +1,228 @@ -:root { +/* ═══════════════════════════════════════════════════════ + M4 Log Visualizer — Carbon Split design + ═══════════════════════════════════════════════════════ */ + +/* ── Design tokens ──────────────────────────────────── */ +:root { color-scheme: light; - --bg: #f4f7f8; - --panel: #ffffff; - --panel-soft: #eef4f5; - --sidebar: #ffffff; - --control: #ffffff; - --control-hover: #f8fbfb; - --table-head: #f8fbfb; - --raw-bg: #fbfcfc; - --raw-text: #3b4649; - --text: #172022; - --muted: #647276; - --line: #d9e2e5; - --accent: #176b72; - --accent-strong: #0d4f55; - --orange: #b84f18; - --shadow: 0 18px 45px rgba(20, 45, 50, 0.10); - font-family: "Segoe UI", Arial, sans-serif; + --bg: #f6f8fa; + --panel: #ffffff; + --panel-soft: #f0f3f6; + --sidebar: #f6f8fa; + --control: #ffffff; + --control-hover: #f0f3f6; + --table-head: #f6f8fa; + --raw-bg: #f8f9fa; + --raw-text: #3b4649; + --text: #1f2328; + --muted: #656d76; + --line: #d0d7de; + --accent: #0969da; + --accent-strong: #0550ae; + --orange: #bc4c00; + --shadow: 0 1px 3px rgba(31,35,40,.12), 0 4px 12px rgba(31,35,40,.06); + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif; } [data-theme="dark"] { color-scheme: dark; - --bg: #0f1416; - --panel: #171d20; - --panel-soft: #1f2b2e; - --sidebar: #12181b; - --control: #151c1f; - --control-hover: #1b2528; - --table-head: #1c2528; - --raw-bg: #111719; - --raw-text: #d2dcdf; - --text: #edf3f4; - --muted: #9aabad; - --line: #2c3a3e; - --accent: #3aaeb7; - --accent-strong: #75d3da; - --orange: #d56f2d; - --shadow: 0 18px 45px rgba(0, 0, 0, 0.36); -} -* { - box-sizing: border-box; -} - -body { - height: 100vh; - margin: 0; - display: grid; - grid-template-columns: minmax(320px, 420px) 1fr; - background: var(--bg); - color: var(--text); - overflow: hidden; -} - -button, -input { - font: inherit; -} - + --bg: #0d1117; + --panel: #161b22; + --panel-soft: #1c2128; + --sidebar: #0d1117; + --control: #21262d; + --control-hover: #30363d; + --table-head: #1c2128; + --raw-bg: #0d1117; + --raw-text: #c9d1d9; + --text: #e6edf3; + --muted: #8b949e; + --line: #30363d; + --accent: #58a6ff; + --accent-strong: #79c0ff; + --orange: #e3b341; + --shadow: 0 1px 3px rgba(0,0,0,.4), 0 4px 16px rgba(0,0,0,.3); +} + +/* ── Reset ──────────────────────────────────────────── */ +*, *::before, *::after { box-sizing: border-box; } +body { margin: 0; } +h1, h2, h3, h4, p { margin: 0; } +ul { margin: 0; padding: 0; list-style: none; } +button, input { font: inherit; } + +/* ── Global button base ─────────────────────────────── */ button { - min-height: 38px; + min-height: 36px; border: 1px solid var(--line); border-radius: 6px; background: var(--control); color: var(--text); cursor: pointer; + transition: border-color .12s, background .12s; } +button:hover { border-color: var(--accent); } -button:hover { - border-color: var(--accent); +/* ── Layout ─────────────────────────────────────────── */ +body { + height: 100vh; + display: grid; + grid-template-rows: 52px 1fr; + background: var(--bg); + color: var(--text); + overflow: hidden; } -.sidebar { - height: 100vh; +.app-body { + display: grid; + grid-template-columns: minmax(300px, 380px) 1fr; min-height: 0; - border-right: 1px solid var(--line); - background: var(--sidebar); - display: flex; - flex-direction: column; overflow: hidden; } -.brand { +/* ── Top bar ────────────────────────────────────────── */ +.topbar { display: flex; - gap: 12px; align-items: center; - padding: 18px; - border-bottom: 1px solid var(--line); + padding: 0 10px; + gap: 0; + background: #24292f; + border-bottom: 1px solid #30363d; + overflow: hidden; } -.brand-title-row { +.topbar-brand { display: flex; - align-items: baseline; - gap: 10px; -} - -.app-version { - font-size: 13px; - color: var(--muted); - background: var(--panel-soft); - padding: 4px 8px; - border-radius: 999px; - border: 1px solid var(--line); + align-items: center; + gap: 9px; + flex-shrink: 0; + padding-right: 16px; + margin-right: 4px; + border-right: 1px solid rgba(255,255,255,.12); } .brand-mark { - width: 44px; - height: 44px; + width: 30px; + height: 30px; display: grid; place-items: center; - border-radius: 8px; + border-radius: 6px; background: var(--accent); color: #fff; font-weight: 800; + font-size: 12px; + flex-shrink: 0; } -h1, -h2, -h3, -p { - margin: 0; +.brand-name { + font-size: 14px; + font-weight: 600; + color: #e6edf3; + white-space: nowrap; } -h1 { - font-size: 20px; +.app-version { + font-size: 11px; + color: #8b949e; + background: rgba(255,255,255,.07); + padding: 2px 7px; + border-radius: 999px; + border: 1px solid rgba(255,255,255,.12); + white-space: nowrap; } -.brand p, -.eyebrow, -.summary, -.details-grid span, -.panel-header span { - color: var(--muted); +.topbar-title { + flex: 1; + min-width: 0; + padding: 0 14px; + overflow: hidden; +} + +.topbar-title .eyebrow { + font-size: 11px; + color: #8b949e; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.topbar-title h2 { + font-size: 14px; + font-weight: 600; + color: #e6edf3; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-top: 2px; +} + +.topbar-actions { + display: flex; + align-items: center; + gap: 8px; + margin-left: auto; + flex-shrink: 0; + padding-left: 14px; + border-left: 1px solid rgba(255,255,255,.12); +} + +/* Override generic button for topbar */ +.topbar-actions button { + height: 30px; + min-height: 30px; + padding: 0 10px; + border: 1px solid rgba(255,255,255,.18); + border-radius: 6px; + background: rgba(255,255,255,.06); + color: #e6edf3; font-size: 12px; } +.topbar-actions button:hover { + background: rgba(255,255,255,.14); + border-color: rgba(255,255,255,.3); +} + +.topbar-actions .icon-button { + width: 32px; + min-width: 32px; + padding: 0; + display: inline-grid; + place-items: center; + font-size: 18px; +} + +.theme-icon { + width: 22px; + height: 22px; + display: inline-grid; + place-items: center; + font-size: 18px; + line-height: 1; +} + +/* ── Sidebar ────────────────────────────────────────── */ +.sidebar { + height: 100%; + min-height: 0; + border-right: 1px solid var(--line); + background: var(--sidebar); + display: flex; + flex-direction: column; + overflow: hidden; +} + +.sidebar-top { + flex-shrink: 0; + padding: 10px 10px 0; + display: flex; + flex-direction: column; + gap: 7px; +} .file-loader { - margin: 14px 18px 0; - padding: 12px; + padding: 9px 10px; border: 1px dashed var(--line); - border-radius: 8px; + border-radius: 7px; background: var(--control-hover); } @@ -149,10 +233,11 @@ h1 { .file-loader label { display: block; - margin-bottom: 8px; - font-size: 12px; + margin-bottom: 6px; + font-size: 11px; font-weight: 700; text-transform: uppercase; + letter-spacing: .05em; color: var(--muted); } @@ -161,50 +246,81 @@ h1 { font-size: 12px; } -.file-loader p { - margin-top: 8px; +.file-loader input[type="file"] { + display: block; + padding: 5px 0; color: var(--muted); - font-size: 12px; - line-height: 1.35; } -.file-loader strong { +.file-loader input[type="file"]::file-selector-button { + margin-right: 10px; + height: 30px; + padding: 0 12px; + border: 1px solid var(--line); + border-radius: 6px; + background: linear-gradient(180deg, var(--control) 0%, var(--control-hover) 100%); color: var(--text); + cursor: pointer; + transition: border-color .12s, background .12s, transform .12s; } -.search-box { - padding: 16px 18px 12px; + +.file-loader input[type="file"]::file-selector-button:hover { + border-color: var(--accent); + background: var(--panel-soft); } -.search-box label { - display: block; - margin-bottom: 8px; - font-size: 12px; - font-weight: 700; - text-transform: uppercase; +.file-loader input[type="file"]::file-selector-button:active { + transform: translateY(1px); +} + +.file-loader p { + margin-top: 5px; color: var(--muted); + font-size: 11px; + font-family: Consolas, "Courier New", monospace; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .search-box input { width: 100%; - height: 42px; + height: 34px; border: 1px solid var(--line); border-radius: 6px; - padding: 0 12px; + padding: 0 10px; outline: none; background: var(--control); color: var(--text); + font-size: 13px; } - +.search-box input::placeholder { color: var(--muted); } .search-box input:focus { border-color: var(--accent); - box-shadow: 0 0 0 3px rgba(23, 107, 114, 0.14); + box-shadow: 0 0 0 3px rgba(9, 105, 218, .18); +} +[data-theme="dark"] .search-box input:focus { + box-shadow: 0 0 0 3px rgba(88, 166, 255, .18); } .filters { display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)) 40px; - gap: 8px; - padding: 0 18px 14px; + grid-template-columns: repeat(3, minmax(72px, 1fr)) 34px; + gap: 5px; +} + +.filters button { + height: 30px; + min-height: 30px; + padding: 0; + font-size: 12px; + border: 1px solid var(--line); + border-radius: 5px; + background: var(--control); + color: var(--text); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .filter.active { @@ -213,13 +329,10 @@ h1 { color: #fff; } - .sort-button { - min-width: 40px; - width: 40px; - padding: 0; - font-size: 18px; - line-height: 1; + width: 34px; + min-width: 34px; + font-size: 16px; } .sort-button.active { @@ -227,158 +340,140 @@ h1 { border-color: var(--accent); color: #fff; } + .system-filter { - margin: 0 18px 12px; display: flex; align-items: center; - gap: 8px; + gap: 7px; color: var(--muted); - font-size: 13px; + font-size: 12px; cursor: pointer; + user-select: none; + padding-bottom: 2px; } .system-filter input { - width: 16px; - height: 16px; + width: 14px; + height: 14px; accent-color: var(--accent); + cursor: pointer; } + .summary { - padding: 0 18px 12px; + color: var(--muted); + font-size: 11px; + line-height: 1.4; + padding-bottom: 8px; + border-bottom: 1px solid var(--line); } .entry-list { flex: 1; min-height: 0; overflow: auto; - padding: 0 10px 18px; + overflow-x: hidden; + padding: 6px 6px 16px; } .entry-item { width: 100%; - min-height: 82px; - margin-bottom: 8px; - padding: 10px; + margin-bottom: 2px; + padding: 7px 9px; display: block; text-align: left; - border-radius: 8px; + border-radius: 6px; + border: 1px solid transparent; + background: transparent; + cursor: pointer; + min-height: unset; + overflow: hidden; +} + +.entry-item:hover { + background: var(--control-hover); + border-color: var(--line); } .entry-item.active { - border-color: var(--accent); background: var(--panel-soft); + border-color: var(--accent); } .entry-title { display: flex; justify-content: space-between; - gap: 10px; - font-weight: 700; + align-items: baseline; + gap: 8px; + font-weight: 600; + font-size: 13px; + line-height: 1.3; +} + +.entry-title > span:last-child { + font-size: 11px; + font-weight: 400; + color: var(--muted); + white-space: nowrap; + flex-shrink: 0; } .entry-meta, .entry-sql { display: block; - margin-top: 6px; + margin-top: 3px; color: var(--muted); - font-size: 12px; - line-height: 1.35; + font-size: 11px; + line-height: 1.3; } .entry-sql { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + font-family: Consolas, "Courier New", monospace; } +/* ── Viewer ─────────────────────────────────────────── */ .viewer { - height: 100vh; + height: 100%; min-width: 0; - padding: 22px; - overflow: auto; -} - -.toolbar { - position: sticky; - top: -22px; - z-index: 5; - background: var(--bg); - padding-top: 22px; display: flex; - justify-content: space-between; - align-items: flex-start; - gap: 16px; - margin-bottom: 16px; -} - -.toolbar h2 { - margin-top: 3px; - font-size: 26px; + flex-direction: column; + overflow: hidden; + background: var(--bg); } -.actions { - display: flex; - gap: 8px; - flex-wrap: wrap; - justify-content: flex-end; +.viewer-meta { + flex-shrink: 0; + padding: 10px 14px 0; + border-bottom: 1px solid var(--line); + background: var(--panel); } - -.icon-button { - width: 40px; - min-width: 40px; - padding: 0; - display: inline-grid; - place-items: center; +.viewer-meta-file { + font-size: 13px; + color: var(--muted); + margin-bottom: 4px; } -.theme-icon { - width: 22px; - height: 22px; - display: inline-grid; - place-items: center; - color: var(--text); +.viewer-meta-title { font-size: 20px; - line-height: 1; -} -.actions button { - padding: 0 14px; -} - -#themeButton { - padding: 0; -} - -#copyButton { - background: var(--orange); - border-color: var(--orange); - color: #fff; -} - -.details-grid { - display: grid; - grid-template-columns: repeat(5, minmax(120px, 1fr)); - gap: 10px; - margin-bottom: 16px; -} - -.details-grid div { - min-width: 0; - padding: 12px; - background: var(--panel); - border: 1px solid var(--line); - border-radius: 8px; + line-height: 1.1; + font-weight: 700; + letter-spacing: .01em; + margin-bottom: 8px; } -.details-grid strong { - display: block; - margin-top: 4px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; +.viewer-scroll { + flex: 1; + min-height: 0; + overflow: auto; + padding: 14px; } +/* ── Panels ─────────────────────────────────────────── */ .panel { - margin-bottom: 16px; + margin-bottom: 10px; background: var(--panel); border: 1px solid var(--line); border-radius: 8px; @@ -387,23 +482,73 @@ h1 { } .panel-header { - min-height: 48px; - padding: 12px 14px; + min-height: 42px; + padding: 7px 12px; display: flex; justify-content: space-between; align-items: center; gap: 12px; border-bottom: 1px solid var(--line); + background: var(--panel-soft); } +.panel h3 { + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: .06em; + color: var(--muted); +} .panel-action { - min-height: 32px; - padding: 0 10px; + height: 28px; + min-height: 28px; + padding: 0 11px; font-size: 12px; + border: 1px solid var(--line); + border-radius: 5px; + background: var(--control); + color: var(--text); } -.panel h3 { - font-size: 15px; +.panel-action:hover { border-color: var(--accent); } + +#formatSqlButton { + background: #0f766e; + border-color: #0f766e; + color: #fff; + font-weight: 600; + box-shadow: 0 1px 0 rgba(31, 35, 40, .08); +} + +#formatSqlButton:hover { + background: #0b5f59; + border-color: #0b5f59; +} + +#formatSqlButton:focus-visible { + outline: none; + box-shadow: 0 0 0 3px rgba(9, 105, 218, .25); +} + +[data-theme="dark"] #formatSqlButton:focus-visible { + box-shadow: 0 0 0 3px rgba(88, 166, 255, .25); +} + +[data-theme="dark"] #formatSqlButton { + background: #1b9a8b; + border-color: #1b9a8b; + color: #f0fffb; +} + +[data-theme="dark"] #formatSqlButton:hover { + background: #147e72; + border-color: #147e72; +} + +.panel-actions { + display: flex; + align-items: center; + gap: 8px; } pre { @@ -412,125 +557,320 @@ pre { overflow: auto; white-space: pre-wrap; word-break: break-word; - font-family: Consolas, "Courier New", monospace; + font-family: Consolas, "Cascadia Code", "Courier New", monospace; font-size: 13px; - line-height: 1.5; + line-height: 1.6; + color: var(--text); } .sql-panel pre { - max-height: 310px; - white-space: pre-wrap; + max-height: min(56vh, 520px); overflow-wrap: anywhere; - word-break: break-word; } +/* ── SQL syntax highlighting ────────────────────────── */ +.sql-keyword { color: #cf222e; font-weight: 700; } +.sql-function { color: #8250df; font-weight: 600; } +.sql-string { color: #0a3069; } +.sql-identifier { color: #953800; font-weight: 600; } +.sql-number { color: #0550ae; } +.sql-comment { color: #57606a; font-style: italic; } +.sql-operator { color: #6e7781; font-weight: 600; } + +[data-theme="dark"] .sql-keyword { color: #ff7b72; font-weight: 700; } +[data-theme="dark"] .sql-function { color: #d2a8ff; font-weight: 600; } +[data-theme="dark"] .sql-string { color: #a5d6ff; } +[data-theme="dark"] .sql-identifier { color: #ffa657; font-weight: 600; } +[data-theme="dark"] .sql-number { color: #79c0ff; } +[data-theme="dark"] .sql-comment { color: #8b949e; font-style: italic; } +[data-theme="dark"] .sql-operator { color: #8b949e; font-weight: 600; } + +/* ── Raw output ─────────────────────────────────────── */ +.raw-panel pre { + max-height: 240px; + color: var(--raw-text); + background: var(--raw-bg); + font-size: 12px; +} -.sql-keyword { - color: #0b5cad; - font-weight: 700; +/* ── Result table ───────────────────────────────────── */ +.table-wrap { width: 100%; overflow: auto; } + +table { width: 100%; border-collapse: collapse; font-size: 12px; } + +th, td { + padding: 7px 12px; + border-bottom: 1px solid var(--line); + text-align: left; + vertical-align: top; +} + +tbody tr:nth-child(even) { + background: color-mix(in srgb, var(--panel-soft) 65%, transparent); } -.sql-function { - color: #7b3fb2; +th { + position: sticky; + top: 0; + background: var(--table-head); + color: var(--muted); + font-size: 11px; font-weight: 700; + text-transform: uppercase; + letter-spacing: .04em; } -.sql-string { - color: #b3261e; +td { font-family: Consolas, "Courier New", monospace; } + +.empty { padding: 16px; color: var(--muted); font-size: 13px; } + +/* ── Status bar ─────────────────────────────────────── */ +.status-bar { + flex-shrink: 0; + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 6px; + padding: 0 0 8px; } -.sql-number { - color: #8a4b00; +.status-item { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; + border: 1px solid var(--line); + border-radius: 8px; + background: var(--panel-soft); + padding: 6px 10px; + min-height: 50px; } -.sql-comment { - color: #617176; - font-style: italic; +.status-label { + font-size: 9px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: .05em; + color: var(--muted); } -.sql-operator { - color: #0b6f64; +.status-item strong { + display: block; + width: 100%; font-weight: 700; + font-size: 13px; + line-height: 1.05; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-variant-numeric: tabular-nums; } -[data-theme="dark"] .sql-keyword { - color: #7db7ff; +.status-item-primary { + background: #f4f8ff; + border-color: #b8cceb; + box-shadow: inset 0 0 0 1px rgba(9, 105, 218, .06); } -[data-theme="dark"] .sql-function { - color: #d6a7ff; +.status-item-primary strong { + font-size: 14px; } -[data-theme="dark"] .sql-string { - color: #ff9f8f; +.status-item-compact { + padding-right: 8px; } -[data-theme="dark"] .sql-number { - color: #f0c36a; +.status-item-compact strong { + font-size: 12px; } -[data-theme="dark"] .sql-comment { - color: #7f9296; +[data-theme="dark"] .status-item-primary { + background: #1b2535; + border-color: #2f4f75; + box-shadow: inset 0 0 0 1px rgba(88, 166, 255, .18); } -[data-theme="dark"] .sql-operator { - color: #58d8c9; +.status-hidden { + display: none; } -.raw-panel pre { - max-height: 260px; - color: var(--raw-text); - background: var(--raw-bg); + +/* ── Settings drawer ────────────────────────────────── */ +.settings-panel { + position: fixed; + top: 0; + right: 0; + width: 400px; + max-width: 100%; + height: 100vh; + z-index: 80; + background: var(--panel); + border-left: 1px solid var(--line); + box-shadow: -8px 0 32px rgba(0,0,0,.22); + display: flex; + flex-direction: column; + transform: translateX(0); + transition: transform .2s ease, visibility .2s; + visibility: visible; } -.table-wrap { - width: 100%; +.settings-panel.hidden { + transform: translateX(100%); + visibility: hidden; + pointer-events: none; +} + +.settings-panel .panel-header { + flex-shrink: 0; +} + +.settings-content { + flex: 1; + min-height: 0; overflow: auto; + padding: 14px; + display: flex; + flex-direction: column; + gap: 12px; } -table { - width: 100%; - border-collapse: collapse; +.settings-content > label { + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: .05em; + color: var(--muted); +} + +.add-row { + display: flex; + gap: 8px; +} + +.add-row input { + flex: 1; + height: 34px; + padding: 0 10px; + border: 1px solid var(--line); + border-radius: 6px; + background: var(--control); + color: var(--text); font-size: 13px; } +.add-row input:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 3px rgba(9, 105, 218, .18); +} +[data-theme="dark"] .add-row input:focus { + box-shadow: 0 0 0 3px rgba(88, 166, 255, .18); +} -th, -td { - padding: 10px 12px; - border-bottom: 1px solid var(--line); - text-align: left; - vertical-align: top; +.add-row button { + height: 34px; + min-height: 34px; + padding: 0 12px; } -th { - position: sticky; - top: 0; - background: var(--table-head); - color: var(--muted); - font-size: 12px; +.ignored-list-wrap { + flex: 1; + min-height: 100px; + max-height: 360px; + overflow: auto; + border: 1px solid var(--line); + border-radius: 6px; + background: var(--panel-soft); + padding: 8px; +} + +.ignored-list-wrap h4 { + font-size: 11px; + font-weight: 700; text-transform: uppercase; + letter-spacing: .04em; + color: var(--muted); + margin: 0 0 8px; + padding-bottom: 6px; + border-bottom: 1px solid var(--line); } -td { +.ignored-list { + display: flex; + flex-direction: column; + gap: 4px; +} + +.ignored-list li { + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; + padding: 5px 8px; + border-radius: 5px; + background: var(--control); + border: 1px solid var(--line); + font-size: 12px; font-family: Consolas, "Courier New", monospace; } -.empty { - padding: 20px; +.ignored-list .remove-ignored { + min-width: 26px; + height: 26px; + min-height: 26px; + padding: 0 5px; + font-size: 12px; + border: 1px solid var(--line); + border-radius: 4px; + background: var(--control); color: var(--muted); + flex-shrink: 0; +} +.ignored-list .remove-ignored:hover { + border-color: #cf222e; + color: #cf222e; + background: var(--control); +} + +.settings-actions { + display: flex; + gap: 8px; + justify-content: flex-end; +} + +.settings-actions button { + height: 34px; + min-height: 34px; + padding: 0 14px; + font-size: 13px; } +/* ── Backdrop ───────────────────────────────────────── */ +#settingsBackdrop { + position: fixed; + inset: 0; + background: rgba(0,0,0,.4); + z-index: 70; +} +#settingsBackdrop.hidden { display: none; } + +/* ── Responsive ─────────────────────────────────────── */ @media (max-width: 980px) { body { - grid-template-columns: 1fr; + grid-template-rows: 52px auto; height: auto; min-height: 100vh; overflow: auto; } + .app-body { + grid-template-columns: 1fr; + overflow: visible; + height: auto; + } + .sidebar { height: auto; - min-height: auto; - border-right: 0; + overflow: visible; + border-right: none; border-bottom: 1px solid var(--line); } @@ -539,76 +879,26 @@ td { overflow: visible; } - .toolbar { - position: static; - padding-top: 0; - } - - .entry-list { - max-height: 320px; + .viewer-scroll { + overflow: visible; + height: auto; + flex: unset; } - .details-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } + .entry-list { max-height: 360px; } + .viewer-meta-title { font-size: 20px; } + .status-bar { grid-template-columns: repeat(3, minmax(0, 1fr)); } } -@media (max-width: 560px) { - .toolbar, - .panel-header { - display: block; - } - - .actions { - margin-top: 12px; - justify-content: flex-start; - } - - .filters, - .details-grid { - grid-template-columns: 1fr; - } - - .viewer { - padding: 14px; - } +@media (max-width: 600px) { + .brand-name { display: none; } + .filters { grid-template-columns: 1fr 1fr 34px; } + .viewer-scroll { padding: 10px; } + .viewer-meta { padding: 8px 10px 0; } + .viewer-meta-file { font-size: 12px; } + .viewer-meta-title { font-size: 17px; margin-bottom: 8px; } + .status-item { min-height: 48px; padding: 6px 9px; } + .status-label { font-size: 10px; } + .status-item strong { font-size: 13px; } + .status-item-primary strong { font-size: 14px; } } - -/* Settings panel */ -.settings-panel.hidden { display: none; } -.settings-panel { - position: fixed; - right: 24px; - top: 80px; - width: 420px; - max-width: calc(100% - 48px); - z-index: 80; - box-shadow: 0 20px 50px rgba(0,0,0,0.35); -} -.settings-panel .settings-content { padding: 14px; display: flex; flex-direction: column; gap: 12px; } -.settings-panel .add-row { display: flex; gap: 8px; } -.settings-panel .add-row input { flex: 1; padding: 8px; border: 1px solid var(--line); border-radius: 6px; background: var(--control); color: var(--text); } -.settings-panel .ignored-list-wrap { max-height: 260px; overflow: auto; border: 1px solid var(--line); background: var(--panel); padding: 10px; border-radius: 6px; } -.ignored-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; } -.ignored-list li { display: flex; justify-content: space-between; gap: 8px; align-items: center; padding: 8px; border-radius: 6px; background: var(--panel-soft); border: 1px solid var(--line); } -.ignored-list button { min-width: 32px; padding: 6px 8px; } -.settings-actions { display: flex; gap: 8px; justify-content: flex-end; } - -/* Backdrop used by settings panel */ -#settingsBackdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.36); z-index: 70; } -#settingsBackdrop.hidden { display: none; } - - - - - - - - - - - - - - - diff --git a/docs/version.json b/docs/version.json index f58a78c..5a0f4ab 100644 --- a/docs/version.json +++ b/docs/version.json @@ -1,3 +1,3 @@ { - "version": "0.9.2" + "version": "0.9.3" } From bb2ccb1be2e337b114e93508bf3fc5258dd23807 Mon Sep 17 00:00:00 2001 From: abrusco Date: Mon, 6 Jul 2026 11:27:28 -0300 Subject: [PATCH 2/7] feat: melhorar ignorar objeto e atualizar versao 0.9.4 --- docs/app.js | 118 ++++++++++++++++++++++++++++++++++++++++++++-- docs/index.html | 10 +++- docs/styles.css | 59 +++++++++++++++++++++++ docs/version.json | 2 +- public/app.js | 12 ++++- 5 files changed, 194 insertions(+), 7 deletions(-) diff --git a/docs/app.js b/docs/app.js index d335901..0697c45 100644 --- a/docs/app.js +++ b/docs/app.js @@ -112,13 +112,79 @@ function saveIgnoredObjects() { } function addIgnoredObject(name) { + const options = arguments[1] || {}; + const deferSelectionMs = Number(options.deferSelectionMs) || 0; const n = String(name || '').trim().toUpperCase(); if (!n) return; if (userIgnoredSet.has(n)) return; userIgnoredSet.add(n); saveIgnoredObjects(); renderIgnoredList(); - render(); + + const refreshSelectionAndRender = () => { + const visible = filteredEntries(); + if (!visible.some((entry) => entry.id === state.selectedId)) { + state.selectedId = visible[0]?.id || null; + } + render(); + }; + + if (state.pendingIgnoreSelectionTimer) { + window.clearTimeout(state.pendingIgnoreSelectionTimer); + state.pendingIgnoreSelectionTimer = null; + } + + if (deferSelectionMs > 0) { + // Atualiza a lista imediatamente, mas preserva a selecao atual por um instante + // para tornar o feedback visual de "ignorar" perceptivel ao usuario. + render(); + state.pendingIgnoreSelectionTimer = window.setTimeout(() => { + state.pendingIgnoreSelectionTimer = null; + refreshSelectionAndRender(); + }, deferSelectionMs); + return; + } + + refreshSelectionAndRender(); +} + +function showIgnoreObjectFeedback(type = 'added') { + const objectBox = elements.metaObject?.closest('.status-item'); + if (objectBox) { + objectBox.classList.remove('status-item-feedback-added', 'status-item-feedback-existing'); + const feedbackClass = type === 'existing' ? 'status-item-feedback-existing' : 'status-item-feedback-added'; + objectBox.classList.add(feedbackClass); + window.setTimeout(() => { + objectBox.classList.remove(feedbackClass); + }, 700); + } + + if (!elements.ignoreObjectLink) { + return; + } + + const originalText = elements.ignoreObjectLink.textContent || 'ignorar'; + const originalTitle = elements.ignoreObjectLink.title || ''; + if (type === 'existing') { + elements.ignoreObjectLink.textContent = 'ja ignorado'; + elements.ignoreObjectLink.title = 'Este objeto ja esta na lista de ignorados'; + } else { + elements.ignoreObjectLink.textContent = 'ignorado'; + elements.ignoreObjectLink.title = 'Objeto adicionado a lista de ignorados'; + } + + window.setTimeout(() => { + const entry = selectedEntry(); + const objectName = (entry?.meta4Object || '').trim().toUpperCase(); + if (!objectName) { + elements.ignoreObjectLink.textContent = originalText; + elements.ignoreObjectLink.title = originalTitle; + return; + } + const isIgnored = userIgnoredSet.has(objectName); + elements.ignoreObjectLink.textContent = isIgnored ? 'ignorado' : 'ignorar'; + elements.ignoreObjectLink.title = isIgnored ? 'Este objeto ja esta na lista de ignorados' : 'Adicionar objeto atual a lista de ignorados'; + }, 1200); } function removeIgnoredObject(name) { @@ -155,6 +221,7 @@ const state = { files: [], loadedFiles: [], source: 'default', + pendingIgnoreSelectionTimer: null, selectedId: null, filterFile: 'all', search: '', @@ -175,6 +242,7 @@ const elements = { selectedFile: document.querySelector('#selectedFile'), selectedTitle: document.querySelector('#selectedTitle'), metaObject: document.querySelector('#metaObject'), + ignoreObjectLink: document.querySelector('#ignoreObjectLink'), nodeName: document.querySelector('#nodeName'), roleName: document.querySelector('#roleName'), organization: document.querySelector('#organization'), @@ -355,6 +423,16 @@ function looksLikeResultRow(line) { return /^("?[^"]+"?|-?[0-9]|NULL\b|\b)/i.test(trimmed); } +function normalizeOracleDateLiterals(sql) { + return String(sql || '') + .replace(/\{\s*d\s*'(\d{4})-(\d{2})-(\d{2})'\s*\}/gi, (_match, year, month, day) => { + return `to_date('${day}/${month}/${year}', 'DD/MM/YYYY')`; + }) + .replace(/\{\s*ts\s*'(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})'\s*\}/gi, (_match, year, month, day, hour, minute, second) => { + return `to_date('${year}-${month}-${day} ${hour}:${minute}:${second}', 'YYYY-MM-DD HH24:MI:SS')`; + }); +} + function normalizeSql(sql) { return sql.replace(/\s+/g, ' ').trim(); } @@ -508,7 +586,7 @@ function parseLogContent(content, fileName) { if (!current) { return; } - current.sql = current.sql.trim(); + current.sql = normalizeOracleDateLiterals(current.sql.trim()); current.normalizedSql = normalizeSql(current.sql); entries.push(current); current = null; @@ -929,6 +1007,11 @@ function renderSelectedEntry() { elements.selectedFile.textContent = 'Nenhum item selecionado'; elements.selectedTitle.textContent = 'Selecione uma query'; elements.metaObject.textContent = '-'; + if (elements.ignoreObjectLink) { + elements.ignoreObjectLink.classList.remove('is-visible', 'is-disabled'); + elements.ignoreObjectLink.removeAttribute('aria-disabled'); + elements.ignoreObjectLink.dataset.object = ''; + } elements.nodeName.textContent = '-'; elements.roleName.textContent = '-'; elements.organization.textContent = '-'; @@ -945,7 +1028,19 @@ function renderSelectedEntry() { elements.selectedFile.textContent = `${entry.fileName} | linha ${entry.lineNumber}`; elements.selectedTitle.textContent = entryDisplayTitle(entry); - elements.metaObject.textContent = entry.meta4Object || '-'; + const objectName = entry.meta4Object || '-'; + const normalizedObjectName = String(objectName).trim().toUpperCase(); + const isIgnoredObject = !!normalizedObjectName && normalizedObjectName !== '-' && userIgnoredSet.has(normalizedObjectName); + elements.metaObject.textContent = objectName; + if (elements.ignoreObjectLink) { + const shouldShowLink = !!normalizedObjectName && normalizedObjectName !== '-'; + elements.ignoreObjectLink.dataset.object = shouldShowLink ? objectName : ''; + elements.ignoreObjectLink.classList.toggle('is-visible', shouldShowLink); + elements.ignoreObjectLink.classList.toggle('is-disabled', isIgnoredObject); + elements.ignoreObjectLink.setAttribute('aria-disabled', isIgnoredObject ? 'true' : 'false'); + elements.ignoreObjectLink.textContent = isIgnoredObject ? 'ignorado' : 'ignorar'; + elements.ignoreObjectLink.title = isIgnoredObject ? 'Este objeto ja esta na lista de ignorados' : 'Adicionar objeto atual a lista de ignorados'; + } elements.nodeName.textContent = entry.node || '-'; elements.roleName.textContent = entry.connection?.role || '-'; elements.organization.textContent = entry.connection?.organization || '-'; @@ -1194,6 +1289,23 @@ if (elements.addIgnoredButton) { }); } +if (elements.ignoreObjectLink) { + elements.ignoreObjectLink.addEventListener('click', (event) => { + event.preventDefault(); + const objectName = elements.ignoreObjectLink.dataset.object || selectedEntry()?.meta4Object || ''; + if (!objectName) { + return; + } + const alreadyIgnored = userIgnoredSet.has(String(objectName).trim().toUpperCase()); + if (alreadyIgnored) { + showIgnoreObjectFeedback('existing'); + return; + } + addIgnoredObject(objectName, { deferSelectionMs: 1000 }); + showIgnoreObjectFeedback('added'); + }); +} + if (elements.ignoredList) { elements.ignoredList.addEventListener('click', (ev) => { const btn = ev.target.closest('.remove-ignored'); diff --git a/docs/index.html b/docs/index.html index e8d4d97..83b4088 100644 --- a/docs/index.html +++ b/docs/index.html @@ -3,7 +3,7 @@ - + M4 Log Visualizer @@ -61,7 +61,13 @@

Nenhum item selecionado

Selecione uma query

- Objeto- + + Objeto + + - + ignorar + + Node- Role- Data- diff --git a/docs/styles.css b/docs/styles.css index 7cc3c50..7bdffca 100644 --- a/docs/styles.css +++ b/docs/styles.css @@ -670,12 +670,71 @@ td { font-family: Consolas, "Courier New", monospace; } background: #f4f8ff; border-color: #b8cceb; box-shadow: inset 0 0 0 1px rgba(9, 105, 218, .06); + transition: box-shadow .18s ease, border-color .18s ease, background-color .18s ease; } .status-item-primary strong { font-size: 14px; } +.status-object-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + width: 100%; +} + +.status-object-row strong { + flex: 1; + min-width: 0; +} + +.status-ignore-link { + display: none; + flex: 0 0 auto; + color: var(--accent); + text-decoration: underline; + text-underline-offset: 2px; + font-size: 12px; + line-height: 1; +} + +.status-ignore-link.is-visible { + display: inline; +} + +.status-ignore-link.is-disabled { + color: var(--muted); + cursor: default; + pointer-events: none; + text-decoration: none; +} + +.status-item-feedback-added { + border-color: #3ca66b; + box-shadow: inset 0 0 0 1px rgba(60, 166, 107, .25), 0 0 0 3px rgba(60, 166, 107, .12); + background: #eefbf3; +} + +.status-item-feedback-existing { + border-color: #ca7b1b; + box-shadow: inset 0 0 0 1px rgba(202, 123, 27, .2), 0 0 0 3px rgba(202, 123, 27, .1); + background: #fff7ea; +} + +[data-theme="dark"] .status-item-feedback-added { + border-color: #3ca66b; + box-shadow: inset 0 0 0 1px rgba(60, 166, 107, .35), 0 0 0 3px rgba(60, 166, 107, .15); + background: #173226; +} + +[data-theme="dark"] .status-item-feedback-existing { + border-color: #ca7b1b; + box-shadow: inset 0 0 0 1px rgba(202, 123, 27, .3), 0 0 0 3px rgba(202, 123, 27, .12); + background: #352712; +} + .status-item-compact { padding-right: 8px; } diff --git a/docs/version.json b/docs/version.json index 5a0f4ab..324ec66 100644 --- a/docs/version.json +++ b/docs/version.json @@ -1,3 +1,3 @@ { - "version": "0.9.3" + "version": "0.9.4" } diff --git a/public/app.js b/public/app.js index 44ce191..dbe47f4 100644 --- a/public/app.js +++ b/public/app.js @@ -131,6 +131,16 @@ function looksLikeResultRow(line) { return /^("?[^"]+"?|-?[0-9]|NULL\b|\b)/i.test(trimmed); } +function normalizeOracleDateLiterals(sql) { + return String(sql || '') + .replace(/\{\s*d\s*'(\d{4})-(\d{2})-(\d{2})'\s*\}/gi, (_match, year, month, day) => { + return `to_date('${day}/${month}/${year}', 'DD/MM/YYYY')`; + }) + .replace(/\{\s*ts\s*'(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})'\s*\}/gi, (_match, year, month, day, hour, minute, second) => { + return `to_date('${year}-${month}-${day} ${hour}:${minute}:${second}', 'YYYY-MM-DD HH24:MI:SS')`; + }); +} + function normalizeSql(sql) { return sql.replace(/\s+/g, ' ').trim(); } @@ -174,7 +184,7 @@ function parseLogContent(content, fileName) { if (!current) { return; } - current.sql = current.sql.trim(); + current.sql = normalizeOracleDateLiterals(current.sql.trim()); current.normalizedSql = normalizeSql(current.sql); entries.push(current); current = null; From 893f30a6b608e49079e0eb85fab996b9f511218f Mon Sep 17 00:00:00 2001 From: abrusco Date: Mon, 6 Jul 2026 12:01:04 -0300 Subject: [PATCH 3/7] chore: remover elementos Node.js, manter apenas versao standalone em docs --- public/index.html => docs/index.html.bak | 56 ++- public/styles.css => docs/styles.css.bak | 174 ++++++- package.json | 17 - public/app.js | 560 ----------------------- server.js | 320 ------------- 5 files changed, 219 insertions(+), 908 deletions(-) rename public/index.html => docs/index.html.bak (56%) rename public/styles.css => docs/styles.css.bak (64%) delete mode 100644 package.json delete mode 100644 public/app.js delete mode 100644 server.js diff --git a/public/index.html b/docs/index.html.bak similarity index 56% rename from public/index.html rename to docs/index.html.bak index 33629c1..76377a0 100644 --- a/public/index.html +++ b/docs/index.html.bak @@ -3,6 +3,7 @@ + M4 Log Visualizer @@ -11,15 +12,18 @@
M4
-

Log Visualizer

-

Selecione ou arraste os logs Meta4

+
+

Log Visualizer

+ v0.0.0 +
+

Pasta padrao: C:\ProgramData\meta4\M4Temp\m4ldb

-

Selecione ou arraste ldbinsp0_1.txt e ldbinsp0_2.txt.

+

Pasta padrao: C:\ProgramData\meta4\M4Temp\m4ldb.

+ + + +
@@ -44,6 +56,8 @@

Log Visualizer

Selecione uma query

+ +
@@ -75,6 +89,7 @@

Selecione uma query

SQL Oracle

+
Aguardando selecao...
@@ -95,9 +110,44 @@

Trecho original


       
+
+      
+      
     
 
     
   
 
 
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/styles.css b/docs/styles.css.bak
similarity index 64%
rename from public/styles.css
rename to docs/styles.css.bak
index 5532496..4c889ae 100644
--- a/public/styles.css
+++ b/docs/styles.css.bak
@@ -3,6 +3,12 @@
   --bg: #f4f7f8;
   --panel: #ffffff;
   --panel-soft: #eef4f5;
+  --sidebar: #ffffff;
+  --control: #ffffff;
+  --control-hover: #f8fbfb;
+  --table-head: #f8fbfb;
+  --raw-bg: #fbfcfc;
+  --raw-text: #3b4649;
   --text: #172022;
   --muted: #647276;
   --line: #d9e2e5;
@@ -13,6 +19,25 @@
   font-family: "Segoe UI", Arial, sans-serif;
 }
 
+[data-theme="dark"] {
+  color-scheme: dark;
+  --bg: #0f1416;
+  --panel: #171d20;
+  --panel-soft: #1f2b2e;
+  --sidebar: #12181b;
+  --control: #151c1f;
+  --control-hover: #1b2528;
+  --table-head: #1c2528;
+  --raw-bg: #111719;
+  --raw-text: #d2dcdf;
+  --text: #edf3f4;
+  --muted: #9aabad;
+  --line: #2c3a3e;
+  --accent: #3aaeb7;
+  --accent-strong: #75d3da;
+  --orange: #d56f2d;
+  --shadow: 0 18px 45px rgba(0, 0, 0, 0.36);
+}
 * {
   box-sizing: border-box;
 }
@@ -36,7 +61,7 @@ button {
   min-height: 38px;
   border: 1px solid var(--line);
   border-radius: 6px;
-  background: #fff;
+  background: var(--control);
   color: var(--text);
   cursor: pointer;
 }
@@ -49,7 +74,7 @@ button:hover {
   height: 100vh;
   min-height: 0;
   border-right: 1px solid var(--line);
-  background: #ffffff;
+  background: var(--sidebar);
   display: flex;
   flex-direction: column;
   overflow: hidden;
@@ -63,6 +88,21 @@ button:hover {
   border-bottom: 1px solid var(--line);
 }
 
+.brand-title-row {
+  display: flex;
+  align-items: baseline;
+  gap: 10px;
+}
+
+.app-version {
+  font-size: 13px;
+  color: var(--muted);
+  background: var(--panel-soft);
+  padding: 4px 8px;
+  border-radius: 999px;
+  border: 1px solid var(--line);
+}
+
 .brand-mark {
   width: 44px;
   height: 44px;
@@ -99,7 +139,7 @@ h1 {
   padding: 12px;
   border: 1px dashed var(--line);
   border-radius: 8px;
-  background: #f8fbfb;
+  background: var(--control-hover);
 }
 
 .file-loader.dragging {
@@ -151,6 +191,8 @@ h1 {
   border-radius: 6px;
   padding: 0 12px;
   outline: none;
+  background: var(--control);
+  color: var(--text);
 }
 
 .search-box input:focus {
@@ -160,7 +202,7 @@ h1 {
 
 .filters {
   display: grid;
-  grid-template-columns: repeat(3, 1fr);
+  grid-template-columns: repeat(3, minmax(0, 1fr)) 40px;
   gap: 8px;
   padding: 0 18px 14px;
 }
@@ -171,6 +213,35 @@ h1 {
   color: #fff;
 }
 
+
+.sort-button {
+  min-width: 40px;
+  width: 40px;
+  padding: 0;
+  font-size: 18px;
+  line-height: 1;
+}
+
+.sort-button.active {
+  background: var(--accent);
+  border-color: var(--accent);
+  color: #fff;
+}
+.system-filter {
+  margin: 0 18px 12px;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  color: var(--muted);
+  font-size: 13px;
+  cursor: pointer;
+}
+
+.system-filter input {
+  width: 16px;
+  height: 16px;
+  accent-color: var(--accent);
+}
 .summary {
   padding: 0 18px 12px;
 }
@@ -251,10 +322,32 @@ h1 {
   justify-content: flex-end;
 }
 
+
+.icon-button {
+  width: 40px;
+  min-width: 40px;
+  padding: 0;
+  display: inline-grid;
+  place-items: center;
+}
+
+.theme-icon {
+  width: 22px;
+  height: 22px;
+  display: inline-grid;
+  place-items: center;
+  color: var(--text);
+  font-size: 20px;
+  line-height: 1;
+}
 .actions button {
   padding: 0 14px;
 }
 
+#themeButton {
+  padding: 0;
+}
+
 #copyButton {
   background: var(--orange);
   border-color: var(--orange);
@@ -303,6 +396,12 @@ h1 {
   border-bottom: 1px solid var(--line);
 }
 
+
+.panel-action {
+  min-height: 32px;
+  padding: 0 10px;
+  font-size: 12px;
+}
 .panel h3 {
   font-size: 15px;
 }
@@ -327,7 +426,7 @@ pre {
 
 
 .sql-keyword {
-  color: #044f9e;
+  color: #0b5cad;
   font-weight: 700;
 }
 
@@ -353,10 +452,34 @@ pre {
   color: #0b6f64;
   font-weight: 700;
 }
+
+[data-theme="dark"] .sql-keyword {
+  color: #7db7ff;
+}
+
+[data-theme="dark"] .sql-function {
+  color: #d6a7ff;
+}
+
+[data-theme="dark"] .sql-string {
+  color: #ff9f8f;
+}
+
+[data-theme="dark"] .sql-number {
+  color: #f0c36a;
+}
+
+[data-theme="dark"] .sql-comment {
+  color: #7f9296;
+}
+
+[data-theme="dark"] .sql-operator {
+  color: #58d8c9;
+}
 .raw-panel pre {
   max-height: 260px;
-  color: #3b4649;
-  background: #fbfcfc;
+  color: var(--raw-text);
+  background: var(--raw-bg);
 }
 
 .table-wrap {
@@ -381,7 +504,7 @@ td {
 th {
   position: sticky;
   top: 0;
-  background: #f8fbfb;
+  background: var(--table-head);
   color: var(--muted);
   font-size: 12px;
   text-transform: uppercase;
@@ -451,6 +574,41 @@ td {
   }
 }
 
+/* Settings panel */
+.settings-panel.hidden { display: none; }
+.settings-panel {
+  position: fixed;
+  right: 24px;
+  top: 80px;
+  width: 420px;
+  max-width: calc(100% - 48px);
+  z-index: 80;
+  box-shadow: 0 20px 50px rgba(0,0,0,0.35);
+}
+.settings-panel .settings-content { padding: 14px; display: flex; flex-direction: column; gap: 12px; }
+.settings-panel .add-row { display: flex; gap: 8px; }
+.settings-panel .add-row input { flex: 1; padding: 8px; border: 1px solid var(--line); border-radius: 6px; background: var(--control); color: var(--text); }
+.settings-panel .ignored-list-wrap { max-height: 260px; overflow: auto; border: 1px solid var(--line); background: var(--panel); padding: 10px; border-radius: 6px; }
+.ignored-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
+.ignored-list li { display: flex; justify-content: space-between; gap: 8px; align-items: center; padding: 8px; border-radius: 6px; background: var(--panel-soft); border: 1px solid var(--line); }
+.ignored-list button { min-width: 32px; padding: 6px 8px; }
+.settings-actions { display: flex; gap: 8px; justify-content: flex-end; }
+
+/* Backdrop used by settings panel */
+#settingsBackdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.36); z-index: 70; }
+#settingsBackdrop.hidden { display: none; }
+
+
+
+
+
+
+
+
+
+
+
+
 
 
 
diff --git a/package.json b/package.json
deleted file mode 100644
index 0efcf1d..0000000
--- a/package.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
-  "name": "m4-log-visualizer",
-  "version": "1.0.0",
-  "description": "Visualizador local de logs Meta4 com extracao de SQL e resultados.",
-  "main": "server.js",
-  "scripts": {
-    "start": "node server.js"
-  },
-  "keywords": [
-    "meta4",
-    "logs",
-    "sql",
-    "oracle"
-  ],
-  "author": "",
-  "license": "MIT"
-}
diff --git a/public/app.js b/public/app.js
deleted file mode 100644
index dbe47f4..0000000
--- a/public/app.js
+++ /dev/null
@@ -1,560 +0,0 @@
-const state = {
-  entries: [],
-  files: [],
-  loadedFiles: [],
-  selectedId: null,
-  filterFile: 'all',
-  search: ''
-};
-
-const elements = {
-  logDir: document.querySelector('#logDir'),
-  summary: document.querySelector('#summary'),
-  entryList: document.querySelector('#entryList'),
-  searchInput: document.querySelector('#searchInput'),
-  fileInput: document.querySelector('#fileInput'),
-  dropZone: document.querySelector('#dropZone'),
-  selectedFile: document.querySelector('#selectedFile'),
-  selectedTitle: document.querySelector('#selectedTitle'),
-  metaObject: document.querySelector('#metaObject'),
-  organization: document.querySelector('#organization'),
-  date: document.querySelector('#date'),
-  duration: document.querySelector('#duration'),
-  rowCount: document.querySelector('#rowCount'),
-  sqlOutput: document.querySelector('#sqlOutput'),
-  rawOutput: document.querySelector('#rawOutput'),
-  resultTable: document.querySelector('#resultTable'),
-  tableHint: document.querySelector('#tableHint'),
-  copyButton: document.querySelector('#copyButton'),
-  refreshButton: document.querySelector('#refreshButton')
-};
-
-function escapeHtml(value) {
-  return String(value ?? '')
-    .replaceAll('&', '&')
-    .replaceAll('<', '<')
-    .replaceAll('>', '>')
-    .replaceAll('"', '"')
-    .replaceAll("'", ''');
-}
-
-function decodeBuffer(buffer) {
-  const utf8 = new TextDecoder('utf-8', { fatal: false }).decode(buffer);
-  const replacementCount = (utf8.match(/\uFFFD/g) || []).length;
-  if (replacementCount > 0) {
-    return new TextDecoder('windows-1252').decode(buffer);
-  }
-  return utf8;
-}
-
-function parseHeader(line) {
-  const objectMatch = line.match(/Meta4Object\s*=\s*([^\.\r\n]+)/i);
-  const nodeMatch = line.match(/Node\s*=\s*([^\.\r\n]+)/i);
-  const recordSetMatch = line.match(/RecordSet\s*=\s*([^\.\r\n]+)/i);
-  return {
-    meta4Object: objectMatch ? objectMatch[1].trim() : '',
-    node: nodeMatch ? nodeMatch[1].trim() : '',
-    recordSet: recordSetMatch ? recordSetMatch[1].trim() : ''
-  };
-}
-
-function parseConnection(line) {
-  const roleMatch = line.match(/Role\s*<([^>]+)>/i);
-  const rsmMatch = line.match(/RSM\s*<([^>]+)>/i);
-  const organizationMatch = line.match(/Organization\s*<([^>]+)>/i);
-  const dateMatch = line.match(/Date\s*=\s*([^\.\r\n]+)/i);
-  const tickMatch = line.match(/Tick\s*=\s*([^\.\r\n]+)/i);
-  return {
-    role: roleMatch ? roleMatch[1].trim() : '',
-    rsm: rsmMatch ? rsmMatch[1].trim() : '',
-    organization: organizationMatch ? organizationMatch[1].trim() : '',
-    date: dateMatch ? dateMatch[1].trim() : '',
-    tick: tickMatch ? tickMatch[1].trim() : ''
-  };
-}
-
-function parseTime(line) {
-  const timeMatch = line.match(/Time\s*=\s*([0-9]+)\s*\(ms\)/i);
-  const threadMatch = line.match(/Thread ID\s*=\s*([0-9]+)/i);
-  return {
-    durationMs: timeMatch ? Number(timeMatch[1]) : null,
-    threadId: threadMatch ? threadMatch[1] : ''
-  };
-}
-
-function splitResultRow(line) {
-  const values = [];
-  let current = '';
-  let inQuotes = false;
-
-  for (let i = 0; i < line.length; i += 1) {
-    const char = line[i];
-    const next = line[i + 1];
-
-    if (char === '"') {
-      if (inQuotes && next === '"') {
-        current += '"';
-        i += 1;
-      } else {
-        inQuotes = !inQuotes;
-      }
-      continue;
-    }
-
-    if (char === ';' && !inQuotes) {
-      const trimmed = current.trim();
-      if (trimmed.length > 0) {
-        values.push(trimmed);
-      }
-      current = '';
-      continue;
-    }
-
-    current += char;
-  }
-
-  const trimmed = current.trim();
-  if (trimmed.length > 0) {
-    values.push(trimmed);
-  }
-  return values;
-}
-
-function looksLikeResultRow(line) {
-  const trimmed = line.trim();
-  if (!trimmed.endsWith(';') || !trimmed.includes(';')) {
-    return false;
-  }
-  if (/^(Execute|DB Connection|Calculating|Number of records)/i.test(trimmed)) {
-    return false;
-  }
-  return /^("?[^"]+"?|-?[0-9]|NULL\b|\b)/i.test(trimmed);
-}
-
-function normalizeOracleDateLiterals(sql) {
-  return String(sql || '')
-    .replace(/\{\s*d\s*'(\d{4})-(\d{2})-(\d{2})'\s*\}/gi, (_match, year, month, day) => {
-      return `to_date('${day}/${month}/${year}', 'DD/MM/YYYY')`;
-    })
-    .replace(/\{\s*ts\s*'(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})'\s*\}/gi, (_match, year, month, day, hour, minute, second) => {
-      return `to_date('${year}-${month}-${day} ${hour}:${minute}:${second}', 'YYYY-MM-DD HH24:MI:SS')`;
-    });
-}
-
-function normalizeSql(sql) {
-  return sql.replace(/\s+/g, ' ').trim();
-}
-
-function createEntry(fileName, index, headerLine) {
-  return {
-    id: `${fileName}-${index}`,
-    fileName,
-    lineNumber: index + 1,
-    ...parseHeader(headerLine),
-    connection: {},
-    sql: '',
-    normalizedSql: '',
-    rows: [],
-    recordCount: null,
-    durationMs: null,
-    threadId: '',
-    rawLines: [headerLine]
-  };
-}
-
-function cloneEntryForStatement(entry, index, statementLine) {
-  return {
-    ...createEntry(entry.fileName, index, entry.rawLines[0]),
-    id: `${entry.fileName}-${index}-${entry.rawLines.length}`,
-    meta4Object: entry.meta4Object,
-    node: entry.node,
-    recordSet: entry.recordSet,
-    connection: { ...entry.connection },
-    rawLines: [entry.rawLines[0], statementLine]
-  };
-}
-
-function parseLogContent(content, fileName) {
-  const lines = content.split(/\r?\n/);
-  const entries = [];
-  let current = null;
-  let readingSql = false;
-
-  function finishCurrent() {
-    if (!current) {
-      return;
-    }
-    current.sql = normalizeOracleDateLiterals(current.sql.trim());
-    current.normalizedSql = normalizeSql(current.sql);
-    entries.push(current);
-    current = null;
-    readingSql = false;
-  }
-
-  lines.forEach((line, index) => {
-    if (/^\s*Execute Real Stmt\./i.test(line)) {
-      finishCurrent();
-      current = createEntry(fileName, index, line.trim());
-      return;
-    }
-
-    if (!current) {
-      return;
-    }
-
-    current.rawLines.push(line);
-    const trimmed = line.trim();
-
-    if (/^DB Connection/i.test(trimmed)) {
-      current.connection = parseConnection(trimmed);
-      return;
-    }
-
-    if (/^Execute Stmt\s*=/i.test(trimmed)) {
-      if (current.sql.trim().length > 0 || current.rows.length > 0) {
-        const previous = current;
-        finishCurrent();
-        current = cloneEntryForStatement(previous, index, line);
-      }
-      current.sql += trimmed.replace(/^Execute Stmt\s*=\s*/i, '');
-      readingSql = true;
-      return;
-    }
-
-    if (/^Number of records\s*:/i.test(trimmed)) {
-      const count = trimmed.match(/Number of records\s*:\s*([0-9]+)/i);
-      current.recordCount = count ? Number(count[1]) : current.rows.length;
-      readingSql = false;
-      return;
-    }
-
-    if (/^Calculating Time\./i.test(trimmed)) {
-      Object.assign(current, parseTime(trimmed));
-      readingSql = false;
-      return;
-    }
-
-    if (looksLikeResultRow(trimmed)) {
-      current.rows.push(splitResultRow(trimmed));
-      readingSql = false;
-      return;
-    }
-
-    if (readingSql && trimmed.length > 0) {
-      current.sql += ` ${trimmed}`;
-    }
-  });
-
-  finishCurrent();
-  return entries;
-}
-
-function highlightSql(sql) {
-  const keywords = new Set([
-    'SELECT', 'FROM', 'WHERE', 'AND', 'OR', 'ORDER', 'BY', 'GROUP', 'HAVING',
-    'INSERT', 'UPDATE', 'DELETE', 'VALUES', 'SET', 'INTO', 'JOIN', 'INNER',
-    'LEFT', 'RIGHT', 'FULL', 'OUTER', 'ON', 'UNION', 'ALL', 'DISTINCT', 'AS',
-    'CASE', 'WHEN', 'THEN', 'ELSE', 'END', 'BEGIN', 'NULL', 'IS', 'NOT', 'IN',
-    'EXISTS', 'LIKE', 'BETWEEN', 'ASC', 'DESC', 'CREATE', 'ALTER', 'DROP'
-  ]);
-  const functions = new Set([
-    'NVL', 'TO_DATE', 'TRUNC', 'REGEXP_SUBSTR', 'SYSDATE', 'COUNT', 'SUM',
-    'MIN', 'MAX', 'AVG', 'SUBSTR', 'DECODE', 'COALESCE', 'TO_CHAR', 'TO_NUMBER'
-  ]);
-  const tokenPattern = /(--[^\n]*|'(?:''|[^'])*'|\b\d+(?:\.\d+)?\b|\b[A-Za-z_][A-Za-z0-9_$#]*\b|<>|<=|>=|:=|[=+\-*/(),.;])/g;
-
-  return String(sql || '').replace(tokenPattern, (token) => {
-    const escaped = escapeHtml(token);
-    if (token.startsWith('--')) {
-      return `${escaped}`;
-    }
-    if (token.startsWith("'")) {
-      return `${escaped}`;
-    }
-    if (/^\d/.test(token)) {
-      return `${escaped}`;
-    }
-
-    const upper = token.toUpperCase();
-    if (keywords.has(upper)) {
-      return `${escaped}`;
-    }
-    if (functions.has(upper)) {
-      return `${escaped}`;
-    }
-    if (/^(<>|<=|>=|:=|[=+\-*/(),.;])$/.test(token)) {
-      return `${escaped}`;
-    }
-    return escaped;
-  });
-}
-
-function formatDate(value) {
-  if (!value) {
-    return '-';
-  }
-  return value;
-}
-
-function selectedEntry() {
-  return state.entries.find((entry) => entry.id === state.selectedId) || null;
-}
-
-function entrySearchText(entry) {
-  return [
-    entry.fileName,
-    entry.meta4Object,
-    entry.node,
-    entry.recordSet,
-    entry.connection?.organization,
-    entry.connection?.date,
-    entry.sql,
-    entry.rows.flat().join(' ')
-  ].join(' ').toLowerCase();
-}
-
-function entryDisplayTitle(entry) {
-  const object = entry.meta4Object || 'SQL sem objeto';
-  const node = entry.node || '';
-
-  if (!node || node === object) {
-    return object;
-  }
-
-  return `${object} :: ${node}`;
-}
-
-function entryDisplaySubtitle(entry) {
-  const parts = [
-    entry.fileName,
-    `linha ${entry.lineNumber}`,
-    entry.connection?.date || 'sem data',
-    `${entry.rows.length} linha(s)`
-  ];
-
-  return parts.join(' | ');
-}
-
-function filteredEntries() {
-  const search = state.search.trim().toLowerCase();
-  return state.entries.filter((entry) => {
-    const fileMatches = state.filterFile === 'all' || entry.fileName === state.filterFile;
-    const searchMatches = !search || entrySearchText(entry).includes(search);
-    return fileMatches && searchMatches;
-  });
-}
-
-function renderSummary(entries) {
-  const existing = state.files.filter((file) => file.exists);
-  const missing = state.files.filter((file) => !file.exists);
-  const rowTotal = state.entries.reduce((sum, entry) => sum + entry.rows.length, 0);
-
-  if (state.files.length === 0) {
-    elements.summary.textContent = 'Nenhum arquivo carregado.';
-    return;
-  }
-
-  elements.summary.textContent = `${entries.length} de ${state.entries.length} SQLs visiveis, ${rowTotal} linhas de resultado. ${existing.length} arquivo(s) lido(s), ${missing.length} ausente(s).`;
-}
-
-function renderEntryList() {
-  const entries = filteredEntries();
-  renderSummary(entries);
-
-  if (state.files.length === 0) {
-    elements.entryList.innerHTML = '
Carregue os arquivos de log para iniciar.
'; - return; - } - - if (entries.length === 0) { - elements.entryList.innerHTML = '
Nenhuma query encontrada.
'; - return; - } - - elements.entryList.innerHTML = entries.map((entry) => { - const title = entryDisplayTitle(entry); - const subtitle = entryDisplaySubtitle(entry); - return ` - - `; - }).join(''); -} - -function renderTable(entry) { - if (!entry || entry.rows.length === 0) { - elements.resultTable.innerHTML = 'Nenhum resultado retornado neste trecho.'; - elements.tableHint.textContent = ''; - return; - } - - const columnCount = Math.max(...entry.rows.map((row) => row.length)); - const header = Array.from({ length: columnCount }, (_, index) => `Col ${index + 1}`).join(''); - const body = entry.rows.map((row) => { - const cells = Array.from({ length: columnCount }, (_, index) => `${escapeHtml(row[index] ?? '')}`).join(''); - return `${cells}`; - }).join(''); - - elements.tableHint.textContent = `${entry.rows.length} linha(s), ${columnCount} coluna(s)`; - elements.resultTable.innerHTML = `${header}${body}`; -} - -function renderSelectedEntry() { - const entry = selectedEntry(); - - if (!entry) { - elements.selectedFile.textContent = 'Nenhum item selecionado'; - elements.selectedTitle.textContent = 'Selecione uma query'; - elements.metaObject.textContent = '-'; - elements.organization.textContent = '-'; - elements.date.textContent = '-'; - elements.duration.textContent = '-'; - elements.rowCount.textContent = '-'; - elements.sqlOutput.textContent = state.files.length === 0 ? 'Carregue ldbinsp0_1.txt e/ou ldbinsp0_2.txt.' : 'Aguardando selecao...'; - elements.rawOutput.textContent = ''; - elements.resultTable.innerHTML = 'Selecione uma query na lista lateral.'; - elements.tableHint.textContent = ''; - return; - } - - elements.selectedFile.textContent = `${entry.fileName} | linha ${entry.lineNumber}`; - elements.selectedTitle.textContent = entryDisplayTitle(entry); - elements.metaObject.textContent = entry.meta4Object || '-'; - elements.organization.textContent = entry.connection?.organization || '-'; - elements.date.textContent = formatDate(entry.connection?.date); - elements.duration.textContent = entry.durationMs == null ? '-' : `${entry.durationMs} ms`; - elements.rowCount.textContent = String(entry.recordCount ?? entry.rows.length); - elements.sqlOutput.innerHTML = entry.sql ? highlightSql(entry.sql) : 'Sem SQL capturado.'; - elements.rawOutput.textContent = (entry.rawLines || []).join('\n'); - renderTable(entry); -} - -function render() { - renderEntryList(); - renderSelectedEntry(); -} - -async function readLogFile(file) { - const buffer = await file.arrayBuffer(); - return { - fileName: file.name, - exists: true, - size: file.size, - modifiedAt: new Date(file.lastModified).toISOString(), - content: decodeBuffer(buffer) - }; -} - -async function loadFiles(fileList, keepSelection = false) { - const files = Array.from(fileList || []).filter((file) => file.name.toLowerCase().endsWith('.txt')); - if (files.length === 0) { - elements.summary.textContent = 'Selecione ao menos um arquivo .txt.'; - return; - } - - state.loadedFiles = files; - elements.summary.textContent = 'Lendo arquivos...'; - - const parsedFiles = await Promise.all(files.map(readLogFile)); - const entries = parsedFiles.flatMap((file) => parseLogContent(file.content, file.fileName)); - - state.files = parsedFiles.map(({ content, ...file }) => file); - state.entries = entries; - elements.logDir.textContent = `${state.files.length} arquivo(s) carregado(s) no navegador`; - - if (!keepSelection || !state.entries.some((entry) => entry.id === state.selectedId)) { - state.selectedId = state.entries[0]?.id || null; - } - - render(); -} - -function reloadLoadedFiles() { - if (state.loadedFiles.length === 0) { - elements.fileInput.click(); - return; - } - loadFiles(state.loadedFiles, true).catch((error) => { - elements.summary.textContent = error.message; - }); -} - -elements.entryList.addEventListener('click', (event) => { - const button = event.target.closest('[data-id]'); - if (!button) { - return; - } - state.selectedId = button.dataset.id; - render(); -}); - -document.querySelectorAll('.filter').forEach((button) => { - button.addEventListener('click', () => { - document.querySelectorAll('.filter').forEach((item) => item.classList.remove('active')); - button.classList.add('active'); - state.filterFile = button.dataset.file; - const visible = filteredEntries(); - if (!visible.some((entry) => entry.id === state.selectedId)) { - state.selectedId = visible[0]?.id || null; - } - render(); - }); -}); - -elements.searchInput.addEventListener('input', () => { - state.search = elements.searchInput.value; - const visible = filteredEntries(); - if (!visible.some((entry) => entry.id === state.selectedId)) { - state.selectedId = visible[0]?.id || null; - } - render(); -}); - -elements.fileInput.addEventListener('change', () => { - loadFiles(elements.fileInput.files).catch((error) => { - elements.summary.textContent = error.message; - }); -}); - -['dragenter', 'dragover'].forEach((eventName) => { - elements.dropZone.addEventListener(eventName, (event) => { - event.preventDefault(); - elements.dropZone.classList.add('dragging'); - }); -}); - -['dragleave', 'drop'].forEach((eventName) => { - elements.dropZone.addEventListener(eventName, (event) => { - event.preventDefault(); - elements.dropZone.classList.remove('dragging'); - }); -}); - -elements.dropZone.addEventListener('drop', (event) => { - loadFiles(event.dataTransfer.files).catch((error) => { - elements.summary.textContent = error.message; - }); -}); - -elements.copyButton.addEventListener('click', async () => { - const entry = selectedEntry(); - if (!entry?.sql) { - return; - } - await navigator.clipboard.writeText(entry.sql); - const original = elements.copyButton.textContent; - elements.copyButton.textContent = 'Copiado'; - window.setTimeout(() => { - elements.copyButton.textContent = original; - }, 1200); -}); - -elements.refreshButton.addEventListener('click', reloadLoadedFiles); - -render(); diff --git a/server.js b/server.js deleted file mode 100644 index 3929e1a..0000000 --- a/server.js +++ /dev/null @@ -1,320 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const http = require('http'); -const { URL } = require('url'); - -const PORT = Number(process.env.PORT || 3000); -const LOG_DIR = process.env.M4_LOG_DIR || 'C:\\ProgramData\\meta4\\M4Temp\\m4ldb'; -const LOG_FILES = ['ldbinsp0_1.txt', 'ldbinsp0_2.txt']; -const PUBLIC_DIR = path.join(__dirname, 'public'); - -function decodeBuffer(buffer) { - const utf8 = buffer.toString('utf8'); - const replacementCount = (utf8.match(/\uFFFD/g) || []).length; - if (replacementCount > 0) { - return buffer.toString('latin1'); - } - return utf8; -} - -function getLogPath(fileName) { - if (!LOG_FILES.includes(fileName)) { - return null; - } - return path.join(LOG_DIR, fileName); -} - -function readLogFile(fileName) { - const filePath = getLogPath(fileName); - if (!filePath || !fs.existsSync(filePath)) { - return { fileName, filePath, exists: false, content: '' }; - } - - const stat = fs.statSync(filePath); - const content = decodeBuffer(fs.readFileSync(filePath)); - return { - fileName, - filePath, - exists: true, - size: stat.size, - modifiedAt: stat.mtime.toISOString(), - content - }; -} - -function parseHeader(line) { - const objectMatch = line.match(/Meta4Object\s*=\s*([^.\r\n]+)/i); - const nodeMatch = line.match(/Node\s*=\s*([^.\r\n]+)/i); - const recordSetMatch = line.match(/RecordSet\s*=\s*([^.\r\n]+)/i); - return { - meta4Object: objectMatch ? objectMatch[1].trim() : '', - node: nodeMatch ? nodeMatch[1].trim() : '', - recordSet: recordSetMatch ? recordSetMatch[1].trim() : '' - }; -} - -function parseConnection(line) { - const roleMatch = line.match(/Role\s*<([^>]+)>/i); - const rsmMatch = line.match(/RSM\s*<([^>]+)>/i); - const organizationMatch = line.match(/Organization\s*<([^>]+)>/i); - const dateMatch = line.match(/Date\s*=\s*([^.\r\n]+)/i); - const tickMatch = line.match(/Tick\s*=\s*([^.\r\n]+)/i); - return { - role: roleMatch ? roleMatch[1].trim() : '', - rsm: rsmMatch ? rsmMatch[1].trim() : '', - organization: organizationMatch ? organizationMatch[1].trim() : '', - date: dateMatch ? dateMatch[1].trim() : '', - tick: tickMatch ? tickMatch[1].trim() : '' - }; -} - -function parseTime(line) { - const timeMatch = line.match(/Time\s*=\s*([0-9]+)\s*\(ms\)/i); - const threadMatch = line.match(/Thread ID\s*=\s*([0-9]+)/i); - return { - durationMs: timeMatch ? Number(timeMatch[1]) : null, - threadId: threadMatch ? threadMatch[1] : '' - }; -} - -function splitResultRow(line) { - const values = []; - let current = ''; - let inQuotes = false; - - for (let i = 0; i < line.length; i += 1) { - const char = line[i]; - const next = line[i + 1]; - - if (char === '"') { - if (inQuotes && next === '"') { - current += '"'; - i += 1; - } else { - inQuotes = !inQuotes; - } - continue; - } - - if (char === ';' && !inQuotes) { - const trimmed = current.trim(); - if (trimmed.length > 0) { - values.push(trimmed); - } - current = ''; - continue; - } - - current += char; - } - - const trimmed = current.trim(); - if (trimmed.length > 0) { - values.push(trimmed); - } - return values; -} - -function looksLikeResultRow(line) { - const trimmed = line.trim(); - if (!trimmed.endsWith(';') || !trimmed.includes(';')) { - return false; - } - if (/^(Execute|DB Connection|Calculating|Number of records)/i.test(trimmed)) { - return false; - } - return /^("?[^"]+"?|-?[0-9]|NULL\b)/i.test(trimmed); -} - -function normalizeSql(sql) { - return sql.replace(/\s+/g, ' ').trim(); -} - -function createEntry(fileName, index, headerLine) { - return { - id: `${fileName}-${index}`, - fileName, - lineNumber: index + 1, - ...parseHeader(headerLine), - connection: {}, - sql: '', - normalizedSql: '', - rows: [], - recordCount: null, - durationMs: null, - threadId: '', - rawLines: [headerLine] - }; -} - -function cloneEntryForStatement(entry, index, statementLine) { - return { - ...createEntry(entry.fileName, index, entry.rawLines[0]), - id: `${entry.fileName}-${index}-${entry.rawLines.length}`, - meta4Object: entry.meta4Object, - node: entry.node, - recordSet: entry.recordSet, - connection: { ...entry.connection }, - rawLines: [entry.rawLines[0], statementLine] - }; -} - -function parseLogContent(content, fileName) { - const lines = content.split(/\r?\n/); - const entries = []; - let current = null; - let readingSql = false; - - function finishCurrent() { - if (!current) { - return; - } - current.sql = current.sql.trim(); - current.normalizedSql = normalizeSql(current.sql); - entries.push(current); - current = null; - readingSql = false; - } - - lines.forEach((line, index) => { - if (/^\s*Execute Real Stmt\./i.test(line)) { - finishCurrent(); - current = createEntry(fileName, index, line.trim()); - return; - } - - if (!current) { - return; - } - - current.rawLines.push(line); - const trimmed = line.trim(); - - if (/^DB Connection/i.test(trimmed)) { - current.connection = parseConnection(trimmed); - return; - } - - if (/^Execute Stmt\s*=/i.test(trimmed)) { - if (current.sql.trim().length > 0 || current.rows.length > 0) { - const previous = current; - finishCurrent(); - current = cloneEntryForStatement(previous, index, line); - } - current.sql += trimmed.replace(/^Execute Stmt\s*=\s*/i, ''); - readingSql = true; - return; - } - - if (/^Number of records\s*:/i.test(trimmed)) { - const count = trimmed.match(/Number of records\s*:\s*([0-9]+)/i); - current.recordCount = count ? Number(count[1]) : current.rows.length; - readingSql = false; - return; - } - - if (/^Calculating Time\./i.test(trimmed)) { - Object.assign(current, parseTime(trimmed)); - readingSql = false; - return; - } - - if (looksLikeResultRow(trimmed)) { - current.rows.push(splitResultRow(trimmed)); - readingSql = false; - return; - } - - if (readingSql && trimmed.length > 0) { - current.sql += ` ${trimmed}`; - } - }); - - finishCurrent(); - return entries; -} - -function getParsedLogs() { - const files = LOG_FILES.map(readLogFile); - const entries = files.flatMap((file) => ( - file.exists ? parseLogContent(file.content, file.fileName) : [] - )); - - return { - logDir: LOG_DIR, - files: files.map(({ content, ...file }) => file), - entries - }; -} - -function sendJson(response, statusCode, payload) { - const body = JSON.stringify(payload, null, 2); - response.writeHead(statusCode, { - 'Content-Type': 'application/json; charset=utf-8', - 'Cache-Control': 'no-store' - }); - response.end(body); -} - -function sendStatic(response, requestPath) { - const safePath = requestPath === '/' ? '/index.html' : requestPath; - const filePath = path.normalize(path.join(PUBLIC_DIR, safePath)); - - if (!filePath.startsWith(PUBLIC_DIR)) { - response.writeHead(403); - response.end('Forbidden'); - return; - } - - if (!fs.existsSync(filePath) || fs.statSync(filePath).isDirectory()) { - response.writeHead(404); - response.end('Not found'); - return; - } - - const ext = path.extname(filePath).toLowerCase(); - const contentType = { - '.html': 'text/html; charset=utf-8', - '.css': 'text/css; charset=utf-8', - '.js': 'application/javascript; charset=utf-8', - '.json': 'application/json; charset=utf-8' - }[ext] || 'application/octet-stream'; - - response.writeHead(200, { 'Content-Type': contentType }); - fs.createReadStream(filePath).pipe(response); -} - -const server = http.createServer((request, response) => { - try { - const url = new URL(request.url, `http://${request.headers.host}`); - - if (url.pathname === '/api/logs') { - sendJson(response, 200, getParsedLogs()); - return; - } - - if (url.pathname === '/api/raw') { - const fileName = url.searchParams.get('file') || LOG_FILES[0]; - const file = readLogFile(fileName); - if (!file.exists) { - sendJson(response, 404, file); - return; - } - sendJson(response, 200, file); - return; - } - - sendStatic(response, decodeURIComponent(url.pathname)); - } catch (error) { - sendJson(response, 500, { - error: error.message, - stack: process.env.NODE_ENV === 'development' ? error.stack : undefined - }); - } -}); - -server.listen(PORT, () => { - console.log(`M4 Log Visualizer running at http://localhost:${PORT}`); - console.log(`Reading logs from ${LOG_DIR}`); -}); - From 5d099de393f292e7604ffa91aeba23e7e1b4d909 Mon Sep 17 00:00:00 2001 From: abrusco Date: Mon, 6 Jul 2026 14:05:12 -0300 Subject: [PATCH 4/7] perf: otimizar busca em logs grandes --- docs/app.js | 49 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/docs/app.js b/docs/app.js index 0697c45..dc9d572 100644 --- a/docs/app.js +++ b/docs/app.js @@ -559,10 +559,24 @@ function createEntry(fileName, index, headerLine) { recordCount: null, durationMs: null, threadId: '', + searchText: '', rawLines: [headerLine] }; } +function buildEntrySearchText(entry) { + return [ + entry.fileName, + entry.meta4Object, + entry.node, + entry.recordSet, + entry.connection?.organization, + entry.connection?.date, + entry.sql, + entry.rows.flat().join(' ') + ].join(' ').toLowerCase(); +} + function cloneEntryForStatement(entry, index, statementLine) { return { ...createEntry(entry.fileName, index, entry.rawLines[0]), @@ -576,6 +590,10 @@ function cloneEntryForStatement(entry, index, statementLine) { }; } +function isSqlTerminationLine(trimmed) { + return /^(No Data Found\b|Num Items\b|Num Registers\b|Calculating Time\.|APISQL Statement in TI\b|#-+|#\s*[A-Za-z_][A-Za-z0-9_\s#]*#)/i.test(trimmed); +} + function parseLogContent(content, fileName) { const lines = content.split(/\r?\n/); const entries = []; @@ -588,6 +606,7 @@ function parseLogContent(content, fileName) { } current.sql = normalizeOracleDateLiterals(current.sql.trim()); current.normalizedSql = normalizeSql(current.sql); + current.searchText = buildEntrySearchText(current); entries.push(current); current = null; readingSql = false; @@ -636,6 +655,11 @@ function parseLogContent(content, fileName) { return; } + if (readingSql && isSqlTerminationLine(trimmed)) { + finishCurrent(); + return; + } + if (looksLikeResultRow(trimmed)) { const row = splitResultRow(trimmed); if (current.rows.length === 0 && looksLikeHeaderRow(row)) { @@ -849,16 +873,7 @@ function selectedEntry() { } function entrySearchText(entry) { - return [ - entry.fileName, - entry.meta4Object, - entry.node, - entry.recordSet, - entry.connection?.organization, - entry.connection?.date, - entry.sql, - entry.rows.flat().join(' ') - ].join(' ').toLowerCase(); + return entry.searchText || buildEntrySearchText(entry); } function entryDisplayTitle(entry) { @@ -1203,11 +1218,17 @@ elements.onlyRealStmt.addEventListener('change', () => { elements.searchInput.addEventListener('input', () => { state.search = elements.searchInput.value; - const visible = filteredEntries(); - if (!visible.some((entry) => entry.id === state.selectedId)) { - state.selectedId = visible[0]?.id || null; + if (state.searchRenderTimer) { + window.clearTimeout(state.searchRenderTimer); } - render(); + state.searchRenderTimer = window.setTimeout(() => { + state.searchRenderTimer = null; + const visible = filteredEntries(); + if (!visible.some((entry) => entry.id === state.selectedId)) { + state.selectedId = visible[0]?.id || null; + } + render(); + }, 140); }); elements.fileInput.addEventListener('change', () => { From 3e59cc86edff8896efe8c05e2390a99bdd583308 Mon Sep 17 00:00:00 2001 From: abrusco Date: Mon, 6 Jul 2026 15:01:50 -0300 Subject: [PATCH 5/7] chore(release): bump version to 0.9.5 --- docs/index.html | 2 +- docs/version.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.html b/docs/index.html index 83b4088..6cc1736 100644 --- a/docs/index.html +++ b/docs/index.html @@ -3,7 +3,7 @@ - + M4 Log Visualizer diff --git a/docs/version.json b/docs/version.json index 324ec66..d0024c3 100644 --- a/docs/version.json +++ b/docs/version.json @@ -1,3 +1,3 @@ { - "version": "0.9.4" + "version": "0.9.5" } From 42bc6528fc30217066b57d657156b3007f6a8275 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Aug 2026 10:24:00 +0000 Subject: [PATCH 6/7] feat(seo): add meta tags, robots.txt, sitemap.xml; remove .bak files Co-authored-by: abrusco <8458577+abrusco@users.noreply.github.com> --- docs/index.html | 36 ++- docs/index.html.bak | 153 ----------- docs/robots.txt | 4 + docs/sitemap.xml | 8 + docs/styles.css.bak | 614 -------------------------------------------- 5 files changed, 47 insertions(+), 768 deletions(-) delete mode 100644 docs/index.html.bak create mode 100644 docs/robots.txt create mode 100644 docs/sitemap.xml delete mode 100644 docs/styles.css.bak diff --git a/docs/index.html b/docs/index.html index 6cc1736..3945ecd 100644 --- a/docs/index.html +++ b/docs/index.html @@ -4,7 +4,41 @@ - M4 Log Visualizer + + + M4 Log Visualizer – Visualizador de Logs Meta4, PeopleNet e Cegid + + + + + + + + + + + + + + + + + + + + diff --git a/docs/index.html.bak b/docs/index.html.bak deleted file mode 100644 index 76377a0..0000000 --- a/docs/index.html.bak +++ /dev/null @@ -1,153 +0,0 @@ - - - - - - - M4 Log Visualizer - - - - - -
-
-
-

Nenhum item selecionado

-

Selecione uma query

-
-
- - - - -
-
- -
-
- Objeto - - -
-
- Organizacao - - -
-
- Data - - -
-
- Tempo - - -
-
- Linhas - - -
-
- -
-
-

SQL Oracle

- -
-
Aguardando selecao...
-
- -
-
-

Resultado retornado no log

- -
-
-
-
-
- -
-
-

Trecho original

-
-

-      
- - - -
- - - - - - - - - - - - - - - - diff --git a/docs/robots.txt b/docs/robots.txt new file mode 100644 index 0000000..92774de --- /dev/null +++ b/docs/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://abrusco.github.io/M4LogVisualizer/sitemap.xml diff --git a/docs/sitemap.xml b/docs/sitemap.xml new file mode 100644 index 0000000..ea131ac --- /dev/null +++ b/docs/sitemap.xml @@ -0,0 +1,8 @@ + + + + https://abrusco.github.io/M4LogVisualizer/ + monthly + 1.0 + + diff --git a/docs/styles.css.bak b/docs/styles.css.bak deleted file mode 100644 index 4c889ae..0000000 --- a/docs/styles.css.bak +++ /dev/null @@ -1,614 +0,0 @@ -:root { - color-scheme: light; - --bg: #f4f7f8; - --panel: #ffffff; - --panel-soft: #eef4f5; - --sidebar: #ffffff; - --control: #ffffff; - --control-hover: #f8fbfb; - --table-head: #f8fbfb; - --raw-bg: #fbfcfc; - --raw-text: #3b4649; - --text: #172022; - --muted: #647276; - --line: #d9e2e5; - --accent: #176b72; - --accent-strong: #0d4f55; - --orange: #b84f18; - --shadow: 0 18px 45px rgba(20, 45, 50, 0.10); - font-family: "Segoe UI", Arial, sans-serif; -} - -[data-theme="dark"] { - color-scheme: dark; - --bg: #0f1416; - --panel: #171d20; - --panel-soft: #1f2b2e; - --sidebar: #12181b; - --control: #151c1f; - --control-hover: #1b2528; - --table-head: #1c2528; - --raw-bg: #111719; - --raw-text: #d2dcdf; - --text: #edf3f4; - --muted: #9aabad; - --line: #2c3a3e; - --accent: #3aaeb7; - --accent-strong: #75d3da; - --orange: #d56f2d; - --shadow: 0 18px 45px rgba(0, 0, 0, 0.36); -} -* { - box-sizing: border-box; -} - -body { - height: 100vh; - margin: 0; - display: grid; - grid-template-columns: minmax(320px, 420px) 1fr; - background: var(--bg); - color: var(--text); - overflow: hidden; -} - -button, -input { - font: inherit; -} - -button { - min-height: 38px; - border: 1px solid var(--line); - border-radius: 6px; - background: var(--control); - color: var(--text); - cursor: pointer; -} - -button:hover { - border-color: var(--accent); -} - -.sidebar { - height: 100vh; - min-height: 0; - border-right: 1px solid var(--line); - background: var(--sidebar); - display: flex; - flex-direction: column; - overflow: hidden; -} - -.brand { - display: flex; - gap: 12px; - align-items: center; - padding: 18px; - border-bottom: 1px solid var(--line); -} - -.brand-title-row { - display: flex; - align-items: baseline; - gap: 10px; -} - -.app-version { - font-size: 13px; - color: var(--muted); - background: var(--panel-soft); - padding: 4px 8px; - border-radius: 999px; - border: 1px solid var(--line); -} - -.brand-mark { - width: 44px; - height: 44px; - display: grid; - place-items: center; - border-radius: 8px; - background: var(--accent); - color: #fff; - font-weight: 800; -} - -h1, -h2, -h3, -p { - margin: 0; -} - -h1 { - font-size: 20px; -} - -.brand p, -.eyebrow, -.summary, -.details-grid span, -.panel-header span { - color: var(--muted); - font-size: 12px; -} - -.file-loader { - margin: 14px 18px 0; - padding: 12px; - border: 1px dashed var(--line); - border-radius: 8px; - background: var(--control-hover); -} - -.file-loader.dragging { - border-color: var(--accent); - background: var(--panel-soft); -} - -.file-loader label { - display: block; - margin-bottom: 8px; - font-size: 12px; - font-weight: 700; - text-transform: uppercase; - color: var(--muted); -} - -.file-loader input { - width: 100%; - font-size: 12px; -} - -.file-loader p { - margin-top: 8px; - color: var(--muted); - font-size: 12px; - line-height: 1.35; -} - -.file-loader strong { - color: var(--text); -} -.search-box { - padding: 16px 18px 12px; -} - -.search-box label { - display: block; - margin-bottom: 8px; - font-size: 12px; - font-weight: 700; - text-transform: uppercase; - color: var(--muted); -} - -.search-box input { - width: 100%; - height: 42px; - border: 1px solid var(--line); - border-radius: 6px; - padding: 0 12px; - outline: none; - background: var(--control); - color: var(--text); -} - -.search-box input:focus { - border-color: var(--accent); - box-shadow: 0 0 0 3px rgba(23, 107, 114, 0.14); -} - -.filters { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)) 40px; - gap: 8px; - padding: 0 18px 14px; -} - -.filter.active { - background: var(--accent); - border-color: var(--accent); - color: #fff; -} - - -.sort-button { - min-width: 40px; - width: 40px; - padding: 0; - font-size: 18px; - line-height: 1; -} - -.sort-button.active { - background: var(--accent); - border-color: var(--accent); - color: #fff; -} -.system-filter { - margin: 0 18px 12px; - display: flex; - align-items: center; - gap: 8px; - color: var(--muted); - font-size: 13px; - cursor: pointer; -} - -.system-filter input { - width: 16px; - height: 16px; - accent-color: var(--accent); -} -.summary { - padding: 0 18px 12px; -} - -.entry-list { - flex: 1; - min-height: 0; - overflow: auto; - padding: 0 10px 18px; -} - -.entry-item { - width: 100%; - min-height: 82px; - margin-bottom: 8px; - padding: 10px; - display: block; - text-align: left; - border-radius: 8px; -} - -.entry-item.active { - border-color: var(--accent); - background: var(--panel-soft); -} - -.entry-title { - display: flex; - justify-content: space-between; - gap: 10px; - font-weight: 700; -} - -.entry-meta, -.entry-sql { - display: block; - margin-top: 6px; - color: var(--muted); - font-size: 12px; - line-height: 1.35; -} - -.entry-sql { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.viewer { - height: 100vh; - min-width: 0; - padding: 22px; - overflow: auto; -} - -.toolbar { - position: sticky; - top: -22px; - z-index: 5; - background: var(--bg); - padding-top: 22px; - display: flex; - justify-content: space-between; - align-items: flex-start; - gap: 16px; - margin-bottom: 16px; -} - -.toolbar h2 { - margin-top: 3px; - font-size: 26px; -} - -.actions { - display: flex; - gap: 8px; - flex-wrap: wrap; - justify-content: flex-end; -} - - -.icon-button { - width: 40px; - min-width: 40px; - padding: 0; - display: inline-grid; - place-items: center; -} - -.theme-icon { - width: 22px; - height: 22px; - display: inline-grid; - place-items: center; - color: var(--text); - font-size: 20px; - line-height: 1; -} -.actions button { - padding: 0 14px; -} - -#themeButton { - padding: 0; -} - -#copyButton { - background: var(--orange); - border-color: var(--orange); - color: #fff; -} - -.details-grid { - display: grid; - grid-template-columns: repeat(5, minmax(120px, 1fr)); - gap: 10px; - margin-bottom: 16px; -} - -.details-grid div { - min-width: 0; - padding: 12px; - background: var(--panel); - border: 1px solid var(--line); - border-radius: 8px; -} - -.details-grid strong { - display: block; - margin-top: 4px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.panel { - margin-bottom: 16px; - background: var(--panel); - border: 1px solid var(--line); - border-radius: 8px; - box-shadow: var(--shadow); - overflow: hidden; -} - -.panel-header { - min-height: 48px; - padding: 12px 14px; - display: flex; - justify-content: space-between; - align-items: center; - gap: 12px; - border-bottom: 1px solid var(--line); -} - - -.panel-action { - min-height: 32px; - padding: 0 10px; - font-size: 12px; -} -.panel h3 { - font-size: 15px; -} - -pre { - margin: 0; - padding: 14px; - overflow: auto; - white-space: pre-wrap; - word-break: break-word; - font-family: Consolas, "Courier New", monospace; - font-size: 13px; - line-height: 1.5; -} - -.sql-panel pre { - max-height: 310px; - white-space: pre-wrap; - overflow-wrap: anywhere; - word-break: break-word; -} - - -.sql-keyword { - color: #0b5cad; - font-weight: 700; -} - -.sql-function { - color: #7b3fb2; - font-weight: 700; -} - -.sql-string { - color: #b3261e; -} - -.sql-number { - color: #8a4b00; -} - -.sql-comment { - color: #617176; - font-style: italic; -} - -.sql-operator { - color: #0b6f64; - font-weight: 700; -} - -[data-theme="dark"] .sql-keyword { - color: #7db7ff; -} - -[data-theme="dark"] .sql-function { - color: #d6a7ff; -} - -[data-theme="dark"] .sql-string { - color: #ff9f8f; -} - -[data-theme="dark"] .sql-number { - color: #f0c36a; -} - -[data-theme="dark"] .sql-comment { - color: #7f9296; -} - -[data-theme="dark"] .sql-operator { - color: #58d8c9; -} -.raw-panel pre { - max-height: 260px; - color: var(--raw-text); - background: var(--raw-bg); -} - -.table-wrap { - width: 100%; - overflow: auto; -} - -table { - width: 100%; - border-collapse: collapse; - font-size: 13px; -} - -th, -td { - padding: 10px 12px; - border-bottom: 1px solid var(--line); - text-align: left; - vertical-align: top; -} - -th { - position: sticky; - top: 0; - background: var(--table-head); - color: var(--muted); - font-size: 12px; - text-transform: uppercase; -} - -td { - font-family: Consolas, "Courier New", monospace; -} - -.empty { - padding: 20px; - color: var(--muted); -} - -@media (max-width: 980px) { - body { - grid-template-columns: 1fr; - height: auto; - min-height: 100vh; - overflow: auto; - } - - .sidebar { - height: auto; - min-height: auto; - border-right: 0; - border-bottom: 1px solid var(--line); - } - - .viewer { - height: auto; - overflow: visible; - } - - .toolbar { - position: static; - padding-top: 0; - } - - .entry-list { - max-height: 320px; - } - - .details-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } -} - -@media (max-width: 560px) { - .toolbar, - .panel-header { - display: block; - } - - .actions { - margin-top: 12px; - justify-content: flex-start; - } - - .filters, - .details-grid { - grid-template-columns: 1fr; - } - - .viewer { - padding: 14px; - } -} - -/* Settings panel */ -.settings-panel.hidden { display: none; } -.settings-panel { - position: fixed; - right: 24px; - top: 80px; - width: 420px; - max-width: calc(100% - 48px); - z-index: 80; - box-shadow: 0 20px 50px rgba(0,0,0,0.35); -} -.settings-panel .settings-content { padding: 14px; display: flex; flex-direction: column; gap: 12px; } -.settings-panel .add-row { display: flex; gap: 8px; } -.settings-panel .add-row input { flex: 1; padding: 8px; border: 1px solid var(--line); border-radius: 6px; background: var(--control); color: var(--text); } -.settings-panel .ignored-list-wrap { max-height: 260px; overflow: auto; border: 1px solid var(--line); background: var(--panel); padding: 10px; border-radius: 6px; } -.ignored-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; } -.ignored-list li { display: flex; justify-content: space-between; gap: 8px; align-items: center; padding: 8px; border-radius: 6px; background: var(--panel-soft); border: 1px solid var(--line); } -.ignored-list button { min-width: 32px; padding: 6px 8px; } -.settings-actions { display: flex; gap: 8px; justify-content: flex-end; } - -/* Backdrop used by settings panel */ -#settingsBackdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.36); z-index: 70; } -#settingsBackdrop.hidden { display: none; } - - - - - - - - - - - - - - - From 0173b361c929d932000817bc7017d8ea01e06147 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Aug 2026 10:41:54 +0000 Subject: [PATCH 7/7] chore: bump version to 0.9.6 Co-authored-by: abrusco <8458577+abrusco@users.noreply.github.com> --- docs/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.html b/docs/index.html index 227d124..cfa0c6d 100644 --- a/docs/index.html +++ b/docs/index.html @@ -3,7 +3,7 @@ - + M4 Log Visualizer – Visualizador de Logs Meta4, PeopleNet e Cegid @@ -47,7 +47,7 @@
M4 Log Visualizer - v0.0.0 + v0.9.6