Files
RFB/rfb_logs.html
2026-10-11 14:48:04 +03:00

32 lines
21 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="ru">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>RFB · Состояние сервера</title>
<style>
:root{color-scheme:dark;--bg:#090f1b;--panel:#101a2b;--line:#243149;--muted:#91a2bd;--text:#eaf0fa;--teal:#4cdec2;--blue:#7caeff;--orange:#ffcc79;--red:#ff818e}*{box-sizing:border-box}body{margin:0;background:radial-gradient(ellipse at 90% 0,#14303a55,transparent 45%),var(--bg);font:14px/1.5 "Segoe UI",system-ui,sans-serif;color:var(--text)}main{max-width:1440px;margin:auto;padding:30px 36px 38px}header{display:flex;justify-content:space-between;gap:20px;align-items:center}.brand{font-size:12px;font-weight:700;letter-spacing:2.5px;color:var(--teal)}h1{font-size:28px;line-height:1.2;margin:9px 0 7px;letter-spacing:-.7px}h2{font-size:16px;margin:0}.muted,.sub{color:var(--muted)}.sub{font-size:12px}.buttons{display:flex;gap:8px;flex-wrap:wrap}button,a.button{border:1px solid var(--line);background:#142036;color:var(--text);border-radius:9px;padding:9px 13px;cursor:pointer;text-decoration:none;font:inherit;font-size:12px}button:hover,a.button:hover{border-color:var(--teal)}.pill{display:inline-flex;align-items:center;gap:7px;border:1px solid var(--line);border-radius:30px;padding:5px 10px;font-size:12px}.dot{width:7px;height:7px;border-radius:50%;background:currentColor}.ok{color:var(--teal)}.warn{color:var(--orange)}.bad{color:var(--red)}.meta{display:flex;gap:16px;flex-wrap:wrap;margin:19px 0;color:var(--muted);font-size:12px}.meta strong{color:var(--text);font-weight:500}.notice{border:1px solid #296359;border-radius:12px;background:#102e2d80;padding:18px 20px;display:flex;align-items:center;gap:16px;margin:18px 0}.notice.warn{border-color:#675135;background:#342c1d80}.notice.bad{border-color:#68323e;background:#391a2580}.notice svg{flex-shrink:0}.notice strong{display:block;font-size:16px;color:var(--text);margin-bottom:3px}.notice p{margin:0;font-size:12px;color:#bac8db}.cards{display:grid;grid-template-columns:repeat(6,1fr);gap:12px}.card,.panel{background:linear-gradient(150deg,#132035,#101929);border:1px solid var(--line);border-radius:12px}.card{padding:16px}.label{text-transform:uppercase;font-size:10px;letter-spacing:1.2px;color:var(--muted);font-weight:600}.value{font-size:26px;line-height:1.2;letter-spacing:-.7px;margin:10px 0 7px;font-variant-numeric:tabular-nums}.unit{font-size:12px;font-weight:400;color:var(--muted);margin-left:4px}.charts{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin:14px 0 22px}.panel{padding:19px 20px}.chart-header,.panel-header{display:flex;align-items:center;justify-content:space-between;gap:12px}.chart{width:100%;height:130px;display:block;margin-top:10px}.panel-header{margin-bottom:16px}.table-wrap{overflow:auto}table{border-collapse:collapse;width:100%;font-size:12px;white-space:nowrap}th{text-align:left;font-weight:500;color:var(--muted);padding:9px 12px;border-bottom:1px solid var(--line)}td{padding:11px 12px;border-bottom:1px solid #24314980;font-variant-numeric:tabular-nums}td:first-child{font-family:Consolas,monospace;color:#c6d7ed}tr:last-child td{border-bottom:0}td.message{white-space:normal;min-width:180px;max-width:350px;color:var(--muted)}.bottom{display:grid;grid-template-columns:minmax(0,2.4fr) minmax(230px,1fr);gap:14px;margin-top:16px}.filters{display:flex;gap:8px;flex-wrap:wrap}input,select{background:#0b1423;border:1px solid var(--line);color:var(--text);border-radius:7px;padding:7px 9px;font:inherit;font-size:12px}input{width:170px}.log{height:335px;overflow:auto;border-top:1px solid var(--line);margin-top:14px;padding-top:7px}.event{display:grid;grid-template-columns:67px 64px 1fr;gap:9px;padding:8px 0;border-bottom:1px solid #24314970;font:11px/1.65 Consolas,monospace}.event time{color:var(--muted)}.event .message{white-space:pre-wrap;overflow-wrap:anywhere}.side-item{border-bottom:1px solid var(--line);padding:13px 0}.side-item:last-child{border:0}.side-item strong{font-size:13px}.stats-line{display:flex;justify-content:space-between;gap:10px;font-size:12px;color:var(--muted);margin-top:7px}.stats-line b{color:var(--text);font-weight:500}.footer{display:flex;justify-content:space-between;gap:15px;margin-top:18px;font-size:11px;color:var(--muted)}.toast{margin-top:8px;font-size:12px;color:var(--orange)}.empty{padding:35px 0;text-align:center;color:var(--muted)}@media(max-width:1100px){.cards{grid-template-columns:repeat(3,1fr)}main{padding:24px}.bottom{grid-template-columns:2fr 1fr}}@media(max-width:760px){main{padding:18px 14px}header{align-items:flex-start;flex-direction:column}h1{font-size:24px}.cards{grid-template-columns:repeat(2,1fr)}.charts,.bottom{grid-template-columns:1fr}.meta{gap:8px 14px}.panel{padding:15px}.panel-header{align-items:flex-start;flex-wrap:wrap}.footer{flex-direction:column}.event{grid-template-columns:60px 50px 1fr;gap:6px}.value{font-size:23px}}
</style></head>
<body><main>
<header><div><div class="brand">RFB / OPERATIONS</div><h1>Состояние сервера <span id="host">—</span></h1><div class="sub" id="context">Подключение к локальной диагностике…</div></div><div class="buttons"><span class="pill ok" id="connection"><span class="dot"></span><span>Подключение</span></span><button id="pause">Пауза</button><button id="refresh">Обновить</button><a class="button" href="/status" target="_blank" rel="noopener">Статус матча ↗</a></div></header>
<div class="meta"><span>Версия <strong id="version">—</strong></span><span>Установлено <strong id="deployed">—</strong></span><span>Запуск <strong id="started">—</strong></span><span>В работе <strong id="uptime">—</strong></span></div>
<div class="notice warn" id="banner"><svg width="27" height="27" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><circle cx="12" cy="12" r="9"/><path d="M12 7v6m0 3v1"/></svg><div><strong id="headline">Получаем состояние</strong><p id="explanation">Эта страница читает локальные данные и не опрашивает API RFB.</p></div></div>
<section class="cards"><div class="card"><div class="label">Режим работы</div><div class="value" id="mode">—</div><div class="sub" id="game">Матч —</div></div><div class="card"><div class="label">API · за 60 секунд</div><div class="value"><span id="api-count">—</span><span class="unit" id="api-limit"></span></div><div class="sub" id="api-note">Общий бюджет машины</div></div><div class="card"><div class="label">Входящие · за 60 секунд</div><div class="value"><span id="http-count">—</span><span class="unit">запросов</span></div><div class="sub" id="http-note">От vMix и других клиентов</div></div><div class="card"><div class="label">CPU приложения</div><div class="value"><span id="cpu">—</span><span class="unit">%</span></div><div class="sub">От мощности одного ядра</div></div><div class="card"><div class="label">Память приложения</div><div class="value"><span id="ram">—</span><span class="unit">МБ</span></div><div class="sub" id="ram-note">Свободная RAM машины —</div></div><div class="card"><div class="label">Кэш ответов</div><div class="value"><span id="cache">—</span><span class="unit">%</span></div><div class="sub" id="cache-note">Повторные расчёты сэкономлены</div></div></section>
<section class="charts"><div class="panel"><div class="chart-header"><h2>Запросы к API</h2><span class="sub">за 60 с</span></div><svg class="chart" id="chart-api" viewBox="0 0 400 125" role="img" aria-label="График запросов к API"></svg></div><div class="panel"><div class="chart-header"><h2>Входящий поток</h2><span class="sub">за 60 с</span></div><svg class="chart" id="chart-http" viewBox="0 0 400 125" role="img" aria-label="График входящих запросов"></svg></div><div class="panel"><div class="chart-header"><h2>Нагрузка CPU</h2><span class="sub">%</span></div><svg class="chart" id="chart-cpu" viewBox="0 0 400 125" role="img" aria-label="График CPU"></svg></div></section>
<section class="panel"><div class="panel-header"><div><h2>Источники данных</h2><div class="sub">Состояние запроса и давность последних принятых данных</div></div><span class="pill" id="source-summary">—</span></div><div class="table-wrap"><table><thead><tr><th>Источник</th><th>Состояние</th><th>Давность</th><th>HTTP</th><th>Время ответа</th><th>Успех / ошибки</th><th>Комментарий</th></tr></thead><tbody id="sources"><tr><td colspan="7">Ожидание диагностики…</td></tr></tbody></table></div></section>
<section class="bottom"><div class="panel"><div class="panel-header"><div><h2>Журнал событий</h2><div class="sub">Последние 400 событий</div></div><div class="filters"><select id="level" aria-label="Уровень лога"><option value="ALL">Все уровни</option><option value="WARNING">Предупреждения</option><option value="ERROR">Ошибки</option><option value="INFO">Информация</option></select><input id="search" placeholder="Поиск по журналу" aria-label="Поиск"><button id="download">Скачать</button></div></div><div class="log" id="events"><div class="empty">События появятся после подключения</div></div></div><aside class="panel"><h2>Службы и ресурсы</h2><div class="side-item"><strong>Публикация JSON</strong><div class="sub" id="publication-state">Ожидает данных</div></div><div class="side-item"><strong>Telegram <span id="tg-state" class="muted">—</span></strong><div class="sub" id="tg-message">—</div><div class="sub" id="tg-time" style="margin-top:5px">—</div><div class="stats-line"><span>Доставлено / ошибки / очередь</span><b id="tg-counts">—</b></div><div style="margin-top:10px"><button id="telegram-test">Проверить доставку</button></div><div class="toast" id="tg-test-result"></div></div><div class="side-item"><strong>Excel / NAS</strong><div class="sub" id="excel-state">—</div><div class="sub" id="assets-state">—</div></div><div class="side-item"><strong>Запросы клиентов</strong><div class="stats-line"><span>Всего</span><b id="http-total">—</b></div><div class="stats-line"><span>Ответы 503 / 500</span><b id="http-errors">—</b></div><div class="sub" style="margin-top:7px">503 до прогрева означает, что данные ещё не готовы.</div></div><div class="side-item"><strong>Сборка</strong><div class="stats-line"><span>Собрана</span><b id="built">—</b></div><div class="stats-line"><span>Python / PID</span><b id="process">—</b></div><div class="sub" id="fingerprint" style="margin-top:7px">—</div></div></aside></section>
<div class="footer"><span id="updated">Обновление каждые 5 секунд · время МСК</span><span>Диагностика не создаёт запросов к API RFB · график за последние 15 минут</span></div>
</main><script>
'use strict';
const el=id=>document.getElementById(id), esc=x=>String(x??'—').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const number=x=>x==null?'—':Number(x).toLocaleString('ru-RU',{maximumFractionDigits:1});
const date=x=>x?new Date(typeof x==='number'?x*1000:x).toLocaleString('ru-RU',{timeZone:'Europe/Moscow',day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit',second:'2-digit'}):'не зафиксировано';
const age=x=>x==null?'—':x<60?`${Math.round(x)} с`:x<3600?`${Math.floor(x/60)} мин`:`${Math.floor(x/3600)} ч ${Math.floor(x%3600/60)} мин`;
const modes={waiting:'Ожидание',prestart:'Подготовка',live:'В эфире',finished:'Завершение',starting:'Запуск'};
const states={ok:['Готов','ok'],error:['Ошибка','bad'],stale:['Устарело','warn'],cached:['Из кэша','muted'],idle:['Ожидание','muted'],pending:['Ожидает загрузки','muted'],updating:['Обновляется','ok'],not_ready:['API ещё не готов','warn'],held:['Сохранены данные','warn']};
let paused=false,timer=null,loading=false,lastId=0,events=[],state=null;
function chart(id,history,key,color,limit=0){const points=history.map(x=>Number(x[key]??0));const max=Math.max(1,limit,...points)*1.15;let markup='';for(let i=0;i<3;i++){let y=12+i*41;markup+=`<line x1="30" y1="${y}" x2="395" y2="${y}" stroke="#26354d"/><text x="0" y="${y+4}" fill="#8195b0" font-size="9">${Math.round(max*(1-i/2))}</text>`;}if(limit){let y=94-limit/max*82;markup+=`<line x1="30" y1="${y}" x2="395" y2="${y}" stroke="#ffcc7970" stroke-dasharray="4 4"/>`;}if(points.length){let coords=points.map((v,i)=>`${30+i/Math.max(1,points.length-1)*365},${94-v/max*82}`);markup+=`<polygon points="30,94 ${coords.join(' ')} 395,94" fill="${color}" opacity=".08"/><polyline points="${coords.join(' ')}" fill="none" stroke="${color}" stroke-width="2" stroke-linejoin="round"/>`;markup+=`<text x="30" y="120" fill="#8195b0" font-size="9">${esc(date(history[0].ts).split(' ').pop())}</text><text x="395" y="120" text-anchor="end" fill="#8195b0" font-size="9">сейчас</text>`;}el(id).innerHTML=markup;}
function renderEvents(){const level=el('level').value,search=el('search').value.toLowerCase();const rows=events.filter(x=>(level==='ALL'||x.level===level||(level==='ERROR'&&x.level==='CRITICAL'))&&x.message.toLowerCase().includes(search)).slice(-200);const box=el('events'),atBottom=box.scrollTop+box.clientHeight>=box.scrollHeight-30;box.innerHTML=rows.length?rows.map(e=>`<div class="event"><time>${esc(date(e.ts).split(' ').pop())}</time><span class="${e.level==='ERROR'||e.level==='CRITICAL'?'bad':e.level==='WARNING'?'warn':'ok'}">${esc(e.level)}</span><span class="message">${esc(e.message)}</span></div>`).join(''):'<div class="empty">Подходящих событий нет</div>';if(atBottom)box.scrollTop=box.scrollHeight;}
function render(d){if(state&&(state.session_id!==d.session_id)){events=[];lastId=0;}state=d;el('host').textContent=d.hostname;el('context').textContent=`${d.league?.toUpperCase()??'RFB'} · ${d.team} · автономный сборщик`;el('version').textContent=d.release?.version??'—';el('deployed').textContent=d.deployment?.deployed_at?date(d.deployment.deployed_at):`обнаружено ${date(d.deployment?.first_seen_at)}`;el('started').textContent=date(d.started_at);el('uptime').textContent=age(d.uptime_seconds);const overall=d.overall??{severity:'warn',title:'Запуск диагностики',detail:'Ожидание первого измерения'};el('banner').className=`notice ${overall.severity}`;el('headline').textContent=overall.title;el('explanation').textContent=overall.detail;el('mode').textContent=modes[d.mode]??'Запуск';el('game').textContent=`Матч ${d.game_id??'не выбран'}`;el('api-count').textContent=number(d.api?.requests_last_minute);el('api-limit').textContent=`/ ${d.api?.limit_per_minute??'—'}`;let pause=Math.max(d.api?.pause_seconds??0,d.api?.network_pause_seconds??0);el('api-note').textContent=pause>0?`Общая пауза ${age(pause)}`:`Всего ${number(d.api?.total_requests)}`;el('http-count').textContent=number(d.http?.last_minute?.requests);el('http-note').textContent=`Ошибки за 60 с: ${number(d.http?.last_minute?.errors??0)}`;el('cpu').textContent=number(d.machine?.cpu_percent);el('ram').textContent=number(d.machine?.rss_mb);el('ram-note').textContent=`Свободно ${number(d.machine?.ram_available_mb)} / ${number(d.machine?.ram_total_mb)} МБ`;const hits=d.cache?.hits??0,misses=d.cache?.misses??0;el('cache').textContent=hits+misses?number(100*hits/(hits+misses)):'0';el('cache-note').textContent=`${number(hits)} ответов без пересчёта`;const hist=d.history??[];chart('chart-api',hist,'api','#4cdec2',d.api?.limit_per_minute??0);chart('chart-http',hist,'http','#7caeff');chart('chart-cpu',hist,'cpu','#c9a0ff',100);const sources=d.sources??[];el('source-summary').textContent=`${sources.filter(x=>x.active).length} активных / ${sources.length}`;el('sources').innerHTML=sources.map(r=>{const [label,color]=states[r.state]??['Неизвестно','muted'];let note=r.message||(r.state==='cached'?'Частый опрос выключен':r.state==='idle'?'Данные сейчас не требуются':'');if(r.retry_seconds>0)note+=` · повтор через ${age(r.retry_seconds)}`;return `<tr><td>${esc(r.name)}</td><td><span class="pill ${color}"><span class="dot"></span>${esc(label)}</span></td><td>${esc(age(r.age_seconds))}</td><td>${esc(r.http_status??'—')}</td><td>${r.duration_ms==null?'—':`${esc(number(r.duration_ms))} мс`}</td><td>${number(r.successes)} / ${number(r.errors)}</td><td class="message">${esc(note)}</td></tr>`;}).join('');const tg=d.telegram??{};const tgState=tg.state??(!tg.enabled?'disabled':tg.retry_seconds>0?'backoff':tg.last_success?'ready':'waiting');el('tg-state').textContent=({disabled:'выключен',backoff:'повтор с паузой',error:'ошибка доставки',ready:'доставка работает',waiting:'ожидает отправки'})[tgState]??'—';el('tg-state').className=tgState==='disabled'?'muted':tgState==='error'?'bad':tgState==='backoff'?'warn':'ok';el('tg-message').textContent=tg.message??'—';el('tg-time').textContent=`Доставка: ${date(tg.last_success)}${tg.last_error&&tgState!=='ready'?` · ошибка: ${date(tg.last_error)}`:''}`;el('tg-counts').textContent=`${number(tg.sent)} / ${number(tg.failed)} / ${number(tg.queue)}`;el('telegram-test').disabled=!tg.enabled;const pub=d.components?.publication;el('publication-state').textContent=pub?.message??'Ожидает данных';el('publication-state').className=pub?.ok===false?'sub warn':'sub';el('excel-state').textContent=`Excel: ${d.components?.excel?.message??'ожидает загрузки'}`;el('assets-state').textContent=`Иконки: ${d.components?.assets?.message??'ожидают загрузки'}`;el('http-total').textContent=number(d.http?.total);el('http-errors').textContent=`${number(d.http?.statuses?.['503']??0)} / ${number(d.http?.statuses?.['500']??0)}`;el('built').textContent=date(d.release?.built_at);el('process').textContent=`${d.python} / ${d.pid}`;el('fingerprint').textContent=`Код ${(d.deployment?.fingerprint??'—').slice(0,12)}`;for(const e of d.events??[]){if(e.id>lastId){events.push(e);lastId=e.id;}}events=events.slice(-400);renderEvents();el('connection').className='pill ok';el('connection').lastElementChild.textContent=paused?'Пауза':'На связи';el('updated').textContent=`Обновлено ${date(d.now)} · каждые 5 секунд · МСК`;}
async function refresh(){if(loading)return;loading=true;const controller=new AbortController(),timeout=setTimeout(()=>controller.abort(),4500);try{const response=await fetch(`/logs/data?after=${lastId}&session=${encodeURIComponent(state?.session_id??'')}`,{cache:'no-store',signal:controller.signal});if(!response.ok)throw new Error(`HTTP ${response.status}`);render(await response.json());}catch(e){el('connection').className='pill bad';el('connection').lastElementChild.textContent='Нет связи';el('banner').className='notice bad';el('headline').textContent='Не отвечает ваше приложение';el('explanation').textContent=`Не удалось обновить /logs/data (${e.name==='AbortError'?'тайм-аут':e.message}). На странице остаётся последнее полученное состояние.`;}finally{clearTimeout(timeout);loading=false;}}
async function tick(){clearTimeout(timer);if(!paused&&!document.hidden)await refresh();timer=setTimeout(tick,document.hidden?20000:5000);}
el('pause').onclick=()=>{paused=!paused;el('pause').textContent=paused?'Продолжить':'Пауза';el('connection').lastElementChild.textContent=paused?'Пауза':'На связи';if(!paused)tick();};el('refresh').onclick=()=>refresh();el('level').onchange=renderEvents;el('search').oninput=renderEvents;document.addEventListener('visibilitychange',()=>{if(!document.hidden&&!paused)tick();});el('download').onclick=()=>{const text=events.map(e=>`${e.ts} ${e.level} ${e.message}`).join('\n');const url=URL.createObjectURL(new Blob([text],{type:'text/plain;charset=utf-8'}));const link=document.createElement('a');link.href=url;link.download=`rfb-${state?.hostname??'server'}-${new Date().toISOString().slice(0,10)}.log`;link.click();setTimeout(()=>URL.revokeObjectURL(url),1000);};el('telegram-test').onclick=async()=>{el('telegram-test').disabled=true;try{const response=await fetch('/logs/telegram-test',{method:'POST'}),payload=await response.json();el('tg-test-result').textContent=payload.message??payload.detail??`HTTP ${response.status}`;}catch(e){el('tg-test-result').textContent='Не удалось отправить запрос';}finally{el('telegram-test').disabled=false;refresh();}};tick();
</script></body></html>