247 lines
12 KiB
HTML
247 lines
12 KiB
HTML
<!doctype html>
|
|
<html lang="id">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Midtrans Middleware — Log Viewer</title>
|
|
<style>
|
|
:root {
|
|
--bg: #0a0b0e; --panel: #1c1f28; --border: #3a4050; --text: #f5f7fa; --dim: #aab2c5;
|
|
--accent: #6ab5ff; --debug: #9aa3b8; --debug-fg: #0a0b0e; --info: #3b82f6; --info-fg: #ffffff;
|
|
--warn: #f5a524; --warn-fg: #1a1200; --error: #ef4444; --error-fg: #ffffff;
|
|
--pill-bg: #2b3040; --pill-border: #454c60; --pill-hover: #394158;
|
|
}
|
|
@media (prefers-color-scheme: light) {
|
|
:root { --bg: #eef0f4; --panel: #ffffff; --border: #c7ccd6; --text: #10131a; --dim: #454b58;
|
|
--pill-bg: #e6e9f0; --pill-border: #c2c8d4; --pill-hover: #d7dbe6;
|
|
--debug-fg: #ffffff; --info-fg: #ffffff; --warn-fg: #1a1200; --error-fg: #ffffff; }
|
|
}
|
|
* { box-sizing: border-box; }
|
|
body { margin: 0; background: var(--bg); color: var(--text); font-family: -apple-system, Segoe UI, Roboto, sans-serif; }
|
|
header { position: sticky; top: 0; z-index: 5; background: var(--panel); border-bottom: 1px solid var(--border);
|
|
padding: 10px 14px; display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
|
|
header h1 { font-size: 14px; margin: 0 12px 0 0; color: var(--dim); font-weight: 700; white-space: nowrap; letter-spacing: .04em; }
|
|
select, input[type=text], button { background: var(--pill-bg); color: var(--text); border: 1px solid var(--pill-border);
|
|
border-radius: 6px; padding: 6px 10px; font-size: 13px; font-family: inherit; }
|
|
input[type=text] { min-width: 220px; flex: 1 1 220px; }
|
|
input[type=text]::placeholder { color: var(--dim); }
|
|
button { cursor: pointer; font-weight: 600; }
|
|
button:hover { background: var(--pill-hover); }
|
|
.levels { display: flex; gap: 5px; }
|
|
.lvl-chip { padding: 5px 10px; border-radius: 6px; border: 1.5px solid var(--border); cursor: pointer; font-size: 12px;
|
|
font-weight: 500; opacity: .4; user-select: none; background: transparent; }
|
|
.lvl-chip.on { opacity: 1; }
|
|
.lvl-chip[data-lvl=DEBUG].on { color: var(--debug-fg); background: var(--debug); border-color: var(--debug); }
|
|
.lvl-chip[data-lvl=INFO].on { color: var(--info-fg); background: var(--info); border-color: var(--info); }
|
|
.lvl-chip[data-lvl=WARN].on { color: var(--warn-fg); background: var(--warn); border-color: var(--warn); }
|
|
.lvl-chip[data-lvl=ERROR].on { color: var(--error-fg); background: var(--error); border-color: var(--error); }
|
|
.lvl-chip[data-lvl=DEBUG]:not(.on) { color: var(--debug); }
|
|
.lvl-chip[data-lvl=INFO]:not(.on) { color: var(--info); }
|
|
.lvl-chip[data-lvl=WARN]:not(.on) { color: var(--warn); }
|
|
.lvl-chip[data-lvl=ERROR]:not(.on) { color: var(--error); }
|
|
#stats { color: var(--dim); font-size: 12px; white-space: nowrap; font-weight: 600; }
|
|
#trace-bar { display: none; align-items: center; gap: 8px; padding: 8px 14px; background: var(--pill-bg);
|
|
border-bottom: 1px solid var(--border); font-size: 12px; }
|
|
#trace-bar.on { display: flex; }
|
|
#trace-bar b { color: var(--accent); }
|
|
main { padding: 8px 14px 40px; max-width: 100%; }
|
|
.row { display: flex; gap: 10px; padding: 6px 8px; border-radius: 6px; font-size: 12.5px; font-family: ui-monospace, SFMono-Regular, Consolas, Menlo, monospace;
|
|
align-items: flex-start; border-left: 4px solid transparent; }
|
|
.row:hover { background: var(--panel); }
|
|
.row.ERROR { border-left-color: var(--error); background: color-mix(in srgb, var(--error) 10%, transparent); }
|
|
.row.WARN { border-left-color: var(--warn); background: color-mix(in srgb, var(--warn) 8%, transparent); }
|
|
.row.INFO { border-left-color: transparent; }
|
|
.row.DEBUG { opacity: .7; }
|
|
.ts { color: var(--dim); white-space: nowrap; flex: 0 0 auto; font-weight: 600; }
|
|
.badge { flex: 0 0 auto; font-weight: 600; padding: 2px 7px; border-radius: 4px; font-size: 11px; letter-spacing: .03em; }
|
|
.badge.DEBUG { color: var(--debug-fg); background: var(--debug); }
|
|
.badge.INFO { color: var(--info-fg); background: var(--info); }
|
|
.badge.WARN { color: var(--warn-fg); background: var(--warn); }
|
|
.badge.ERROR { color: var(--error-fg); background: var(--error); }
|
|
.msg { flex: 0 0 auto; font-weight: 700; white-space: nowrap; color: var(--text); }
|
|
.meta { flex: 1 1 auto; color: var(--dim); display: flex; flex-wrap: wrap; gap: 5px; min-width: 0; }
|
|
.pill { background: var(--pill-bg); border: 1px solid var(--pill-border); border-radius: 4px; padding: 1px 7px; cursor: pointer; white-space: nowrap; color: var(--text); }
|
|
.pill:hover { background: var(--pill-hover); border-color: var(--accent); }
|
|
.pill.hit { outline: 2px solid var(--accent); border-color: var(--accent); color: var(--accent); font-weight: 700; }
|
|
#empty { color: var(--dim); text-align: center; padding: 40px; font-size: 13px; }
|
|
#jump { position: fixed; right: 20px; bottom: 20px; border-radius: 20px; padding: 8px 14px; box-shadow: 0 2px 10px rgba(0,0,0,.3);
|
|
background: var(--accent); color: #06121f; border: none; font-weight: 800; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<h1>LOG VIEWER</h1>
|
|
<select id="fileSelect"></select>
|
|
<div class="levels" id="levels">
|
|
<span class="lvl-chip on" data-lvl="DEBUG">DEBUG</span>
|
|
<span class="lvl-chip on" data-lvl="INFO">INFO</span>
|
|
<span class="lvl-chip on" data-lvl="WARN">WARN</span>
|
|
<span class="lvl-chip on" data-lvl="ERROR">ERROR</span>
|
|
</div>
|
|
<input type="text" id="search" placeholder="Cari teks, order_id, request id...">
|
|
<label style="font-size:12px;color:var(--dim);display:flex;align-items:center;gap:4px;">
|
|
<input type="checkbox" id="autoRefresh"> auto-refresh 5s
|
|
</label>
|
|
<button id="refreshBtn">Refresh</button>
|
|
<span id="stats"></span>
|
|
</header>
|
|
<div id="trace-bar">
|
|
Trace aktif untuk: <b id="traceVal"></b>
|
|
<button id="clearTrace">Hapus filter</button>
|
|
</div>
|
|
<main id="rows"><div id="empty">Memuat...</div></main>
|
|
<button id="jump" style="display:none;">↓ Terbaru</button>
|
|
|
|
<script>
|
|
(function () {
|
|
var state = { file: null, entries: [], levels: new Set(['DEBUG','INFO','WARN','ERROR']), search: '' };
|
|
var params = new URLSearchParams(location.search);
|
|
state.file = params.get('file') || '';
|
|
|
|
function parseLine(raw) {
|
|
var m = raw.match(/^\[([^\]]+)\]\s\[(\w+)\s*\]\s(.*)$/);
|
|
if (!m) return { ts: '', level: 'INFO', msg: raw, metaRaw: '', meta: null, raw: raw };
|
|
var ts = m[1], level = m[2].trim(), rest = m[3];
|
|
var sepIdx = rest.indexOf(' | ');
|
|
var msg = sepIdx === -1 ? rest : rest.slice(0, sepIdx);
|
|
var metaRaw = sepIdx === -1 ? '' : rest.slice(sepIdx + 3);
|
|
var meta = null;
|
|
if (metaRaw) { try { meta = JSON.parse(metaRaw); } catch (e) { meta = null; } }
|
|
return { ts: ts, level: level, msg: msg, metaRaw: metaRaw, meta: meta, raw: raw };
|
|
}
|
|
|
|
function fmtTime(ts) {
|
|
if (!ts) return '';
|
|
var i = ts.indexOf('T');
|
|
return i === -1 ? ts : ts.slice(i + 1, ts.length - 6);
|
|
}
|
|
|
|
async function loadFileList() {
|
|
var res = await fetch('/api/logs/files', { headers: { Accept: 'application/json' } });
|
|
if (!res.ok) return;
|
|
var data = await res.json();
|
|
var sel = document.getElementById('fileSelect');
|
|
sel.innerHTML = '';
|
|
data.files.forEach(function (f) {
|
|
var opt = document.createElement('option');
|
|
opt.value = f.filename; opt.textContent = f.filename + ' (' + (f.size/1024).toFixed(1) + ' KB)';
|
|
sel.appendChild(opt);
|
|
});
|
|
if (!state.file && data.files.length) state.file = data.files[0].filename;
|
|
sel.value = state.file;
|
|
}
|
|
|
|
async function loadFile() {
|
|
document.getElementById('rows').innerHTML = '<div id="empty">Memuat...</div>';
|
|
var res = await fetch('/api/logs/files/' + encodeURIComponent(state.file), { headers: { Accept: 'application/json' } });
|
|
if (!res.ok) {
|
|
document.getElementById('rows').innerHTML = '<div id="empty">Gagal memuat file (' + res.status + ')</div>';
|
|
return;
|
|
}
|
|
var data = await res.json();
|
|
state.entries = data.content.map(parseLine);
|
|
var url = new URL(location.href); url.searchParams.set('file', state.file);
|
|
history.replaceState(null, '', url);
|
|
render();
|
|
}
|
|
|
|
function matches(e) {
|
|
if (!state.levels.has(e.level)) return false;
|
|
if (!state.search) return true;
|
|
return e.raw.toLowerCase().indexOf(state.search.toLowerCase()) !== -1;
|
|
}
|
|
|
|
function metaHtml(e) {
|
|
if (!e.meta || typeof e.meta !== 'object') {
|
|
return e.metaRaw ? '<span class="pill">' + escapeHtml(e.metaRaw) + '</span>' : '';
|
|
}
|
|
return Object.keys(e.meta).map(function (k) {
|
|
var v = e.meta[k];
|
|
var vs = typeof v === 'object' ? JSON.stringify(v) : String(v);
|
|
var isHit = state.search && vs.toLowerCase() === state.search.toLowerCase();
|
|
return '<span class="pill' + (isHit ? ' hit' : '') + '" data-val="' + escapeAttr(vs) + '">' +
|
|
escapeHtml(k) + ': ' + escapeHtml(vs) + '</span>';
|
|
}).join('');
|
|
}
|
|
|
|
function escapeHtml(s) { return s.replace(/[&<>]/g, function (c) { return { '&': '&', '<': '<', '>': '>' }[c]; }); }
|
|
function escapeAttr(s) { return escapeHtml(s).replace(/"/g, '"'); }
|
|
|
|
function render() {
|
|
var filtered = state.entries.filter(matches);
|
|
var container = document.getElementById('rows');
|
|
if (!filtered.length) {
|
|
container.innerHTML = '<div id="empty">Tidak ada log yang cocok dengan filter.</div>';
|
|
} else {
|
|
var html = filtered.map(function (e) {
|
|
return '<div class="row ' + e.level + '">' +
|
|
'<span class="ts" title="' + escapeAttr(e.ts) + '">' + escapeHtml(fmtTime(e.ts)) + '</span>' +
|
|
'<span class="badge ' + e.level + '">' + e.level + '</span>' +
|
|
'<span class="msg">' + escapeHtml(e.msg) + '</span>' +
|
|
'<span class="meta">' + metaHtml(e) + '</span>' +
|
|
'</div>';
|
|
}).join('');
|
|
container.innerHTML = html;
|
|
}
|
|
var counts = { DEBUG: 0, INFO: 0, WARN: 0, ERROR: 0 };
|
|
state.entries.forEach(function (e) { if (counts[e.level] !== undefined) counts[e.level]++; });
|
|
document.getElementById('stats').textContent =
|
|
filtered.length + ' / ' + state.entries.length + ' baris — ' +
|
|
'E:' + counts.ERROR + ' W:' + counts.WARN + ' I:' + counts.INFO + ' D:' + counts.DEBUG;
|
|
}
|
|
|
|
document.getElementById('rows').addEventListener('click', function (ev) {
|
|
var pill = ev.target.closest('.pill');
|
|
if (!pill || !pill.dataset.val) return;
|
|
setSearch(pill.dataset.val);
|
|
});
|
|
|
|
function setSearch(v) {
|
|
state.search = v;
|
|
document.getElementById('search').value = v;
|
|
document.getElementById('trace-bar').classList.toggle('on', !!v);
|
|
document.getElementById('traceVal').textContent = v;
|
|
render();
|
|
}
|
|
|
|
document.getElementById('clearTrace').addEventListener('click', function () { setSearch(''); });
|
|
document.getElementById('search').addEventListener('input', function (ev) { setSearch(ev.target.value); });
|
|
|
|
document.getElementById('levels').addEventListener('click', function (ev) {
|
|
var chip = ev.target.closest('.lvl-chip');
|
|
if (!chip) return;
|
|
var lvl = chip.dataset.lvl;
|
|
if (state.levels.has(lvl)) { state.levels.delete(lvl); chip.classList.remove('on'); }
|
|
else { state.levels.add(lvl); chip.classList.add('on'); }
|
|
render();
|
|
});
|
|
|
|
document.getElementById('fileSelect').addEventListener('change', function (ev) {
|
|
state.file = ev.target.value;
|
|
loadFile();
|
|
});
|
|
|
|
document.getElementById('refreshBtn').addEventListener('click', loadFile);
|
|
|
|
var timer = null;
|
|
document.getElementById('autoRefresh').addEventListener('change', function (ev) {
|
|
if (timer) { clearInterval(timer); timer = null; }
|
|
if (ev.target.checked) timer = setInterval(loadFile, 5000);
|
|
});
|
|
|
|
var jumpBtn = document.getElementById('jump');
|
|
window.addEventListener('scroll', function () {
|
|
jumpBtn.style.display = (document.body.scrollHeight - window.scrollY - window.innerHeight > 400) ? 'block' : 'none';
|
|
});
|
|
jumpBtn.addEventListener('click', function () { window.scrollTo(0, document.body.scrollHeight); });
|
|
|
|
(async function init() {
|
|
await loadFileList();
|
|
await loadFile();
|
|
window.scrollTo(0, document.body.scrollHeight);
|
|
})();
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|