Files
back-hokm/internal/telescope/page.go
T
2026-07-05 08:42:42 +03:30

123 lines
5.2 KiB
Go

package telescope
// pageHTML یک صفحه‌ی مستقل (بدونِ وابستگیِ خارجی) که هر ۲ ثانیه داده را می‌گیرد.
const pageHTML = `<!doctype html>
<html lang="fa" dir="rtl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>تلسکوپ — بازرسِ درخواست‌ها</title>
<style>
:root { color-scheme: dark; }
* { box-sizing: border-box; }
body { margin:0; font-family: -apple-system, "Segoe UI", Tahoma, sans-serif;
background:#0E2347; color:#E8EEF7; }
header { padding:14px 20px; background:#17345C; border-bottom:1px solid #1E5FA8;
display:flex; align-items:center; gap:14px; position:sticky; top:0; }
header h1 { font-size:17px; margin:0; font-weight:700; }
header .dot { width:9px; height:9px; border-radius:50%; background:#4ade80; }
header .meta { margin-inline-start:auto; font-size:12px; opacity:.7; }
header label { font-size:12px; opacity:.85; display:flex; align-items:center; gap:6px; }
.filters { padding:10px 20px; display:flex; gap:8px; flex-wrap:wrap; }
.filters input, .filters select {
background:#0b1c39; color:#E8EEF7; border:1px solid #1E5FA8;
border-radius:8px; padding:7px 10px; font-size:13px; }
.filters input { flex:1; min-width:160px; }
table { width:100%; border-collapse:collapse; font-size:13px; }
th, td { padding:9px 12px; text-align:right; border-bottom:1px solid #16305a; white-space:nowrap; }
th { position:sticky; top:52px; background:#122c52; font-weight:600; z-index:1; }
tr:hover { background:#132b50; }
.m { font-weight:700; font-size:11px; padding:2px 7px; border-radius:6px; }
.GET{background:#134e4a;color:#5eead4} .POST{background:#1e3a8a;color:#93c5fd}
.PUT{background:#713f12;color:#fcd34d} .DELETE{background:#7f1d1d;color:#fca5a5}
.st { font-weight:700; }
.s2{color:#4ade80} .s3{color:#60a5fa} .s4{color:#fbbf24} .s5{color:#f87171}
.path { font-family: ui-monospace, monospace; }
.body { display:none; }
tr.open .body { display:table-row; }
.body td { background:#0b1c39; white-space:pre-wrap; font-family: ui-monospace, monospace;
font-size:12px; color:#fca5a5; direction:ltr; text-align:left; }
.slow { color:#fbbf24; }
.dim { opacity:.55; }
.empty { padding:40px; text-align:center; opacity:.6; }
</style>
</head>
<body>
<header>
<span class="dot"></span>
<h1>تلسکوپ — بازرسِ درخواست‌ها</h1>
<label><input type="checkbox" id="live" checked> زنده</label>
<span class="meta" id="meta">…</span>
</header>
<div class="filters">
<input id="q" placeholder="جستجو در مسیر…">
<select id="mf">
<option value="">همه متدها</option>
<option>GET</option><option>POST</option><option>PUT</option><option>DELETE</option>
</select>
<select id="sf">
<option value="">همه وضعیت‌ها</option>
<option value="2">2xx</option><option value="3">3xx</option>
<option value="4">4xx</option><option value="5">5xx</option>
</select>
</div>
<table>
<thead><tr>
<th>زمان</th><th>متد</th><th>مسیر</th><th>وضعیت</th><th>مدت</th><th>حجم</th><th>IP</th>
</tr></thead>
<tbody id="rows"></tbody>
</table>
<div class="empty" id="empty" style="display:none">درخواستی ثبت نشده است</div>
<script>
let data = [];
const $ = s => document.querySelector(s);
function fmtTime(t){ const d = new Date(t); return d.toLocaleTimeString('fa-IR'); }
function stClass(s){ return 's' + Math.floor(s/100); }
function render(){
const q = $('#q').value.trim().toLowerCase();
const mf = $('#mf').value, sf = $('#sf').value;
const rows = data.filter(e =>
(!q || (e.path||'').toLowerCase().includes(q)) &&
(!mf || e.method === mf) &&
(!sf || Math.floor((e.status||0)/100) == sf));
const tb = $('#rows'); tb.innerHTML = '';
$('#empty').style.display = rows.length ? 'none' : 'block';
for(const e of rows){
const tr = document.createElement('tr');
const slow = e.duration_ms > 500 ? ' slow' : '';
tr.innerHTML =
'<td class="dim">'+fmtTime(e.time)+'</td>'+
'<td><span class="m '+e.method+'">'+e.method+'</span></td>'+
'<td class="path">'+e.path+'</td>'+
'<td class="st '+stClass(e.status)+'">'+(e.status||'-')+'</td>'+
'<td class="'+slow.trim()+'">'+e.duration_ms+'ms</td>'+
'<td class="dim">'+e.bytes+'</td>'+
'<td class="dim">'+(e.ip||'')+'</td>';
if(e.body){
tr.style.cursor='pointer';
tr.onclick = () => { const b = tr.nextSibling; b.style.display = b.style.display==='table-row'?'none':'table-row'; };
tb.appendChild(tr);
const b = document.createElement('tr'); b.className='body';
b.innerHTML = '<td colspan="7">'+e.body.replace(/</g,'&lt;')+'</td>';
b.style.display='none';
tb.appendChild(b);
} else {
tb.appendChild(tr);
}
}
$('#meta').textContent = rows.length + ' / ' + data.length + ' درخواست';
}
async function load(){
try{
const r = await fetch('/admin/telescope/data');
data = await r.json();
render();
}catch(e){ $('#meta').textContent = 'خطا در دریافت'; }
}
['q','mf','sf'].forEach(id => $('#'+id).addEventListener('input', render));
load();
setInterval(() => { if($('#live').checked) load(); }, 2000);
</script>
</body>
</html>`