<header><div><spanclass="brand-mark"aria-hidden="true">◎</span><div><divclass="eyebrow">PROXMOX / LIVE OBSERVATORY</div><h1>Traffic space<spanstyle="color:var(--green)">.</span></h1></div></div><nav><spanid="live"class="live stale">Подключение…</span><spanclass="now"id="clock"></span><ahref="/">↖ Dashboard</a><buttonid="themeToggle"class="icon-button"aria-label="Переключить тему">☀</button><buttonid="fullscreen"class="icon-button"aria-label="Полный экран">⛶</button></nav></header>
<divclass="toolbar"><divclass="segments"><buttonclass="active"data-kind="">Весь трафик</button><buttondata-kind="request">HTTP</button><buttondata-kind="alert">Срабатывания</button><buttondata-kind="decision">Решения</button></div><selectid="domain"aria-label="Фильтр по домену"><optionvalue="">Все домены</option></select><aid="networkLink"href="/network"style="font-size:10px;white-space:nowrap"hidden>TCP / UDP ↗</a><labelid="externalControl"><inputtype="checkbox"id="external"> Только внешние IP</label><labelid="monitoringControl"><inputtype="checkbox"id="hideMonitoring"> Скрыть мониторинг</label><labelid="networkOverlayControl"hidden><inputtype="checkbox"id="networkOverlay"checked> Сеть на карте</label><buttonid="pause">Пауза</button></div>
<sectionclass="metrics"aria-label="Статистика за последние 24 часа"><buttonclass="metric"data-metric="unique"><div><small>Уникальные IP · 24 ч</small><bid="unique">—</b></div><iaria-hidden="true">⌁</i></button><buttonclass="metric request"data-metric="requests"><div><small>HTTP-запросы · 24 ч</small><bid="requests">—</b></div><iaria-hidden="true">↗</i></button><buttonclass="metric alert"data-metric="alerts"><div><small>Срабатывания · 24 ч</small><bid="alerts">—</b></div><iaria-hidden="true">ϟ</i></button><buttonclass="metric decision"data-metric="decisions"><div><small>Решения CrowdSec · 24 ч</small><bid="decisions">—</b></div><iaria-hidden="true">◇</i></button></section>
<sectionclass="dashboard"><divclass="panel map-panel"><divclass="map-heading"><spanclass="section-label">Глобальная активность</span><buttonclass="source-status"id="sourceDetails"aria-label="Состояние источника"><spanid="sourceName">Источник…</span><spanid="status"class="status-box"role="status">Подключение</span><span>ⓘ</span></button></div><divclass="map-stage"><buttonid="networkSummary"hiddenaria-label="Сетевая активность на карте"></button><divclass="map-controls"><buttonid="zoomOut"aria-label="Отдалить карту">−</button><outputid="zoomLevel">100%</output><buttonid="zoomIn"aria-label="Приблизить карту">+</button><buttonid="zoomReset"aria-label="Сбросить масштаб карты">↺</button></div><canvastabindex="0"id="world"role="img"aria-label="Карта стран источников; данные доступны также в статистике и ленте событий"></canvas></div><divclass="map-foot"><divclass="legend"><span><iclass="dot"></i>HTTP</span><span><iclass="dot alert"></i>Срабатывание</span><span><iclass="dot decision"></i>Решение</span></div><spanid="unmapped">Без геолокации: —</span></div></div>
<sectionclass="dashboard"><divclass="panel map-panel"><divclass="map-heading"><spanclass="section-label">Глобальная активность</span><buttonclass="source-status"id="sourceDetails"aria-label="Состояние источника"><spanid="sourceName">Источник…</span><spanid="status"class="status-box"role="status">Подключение</span><span>ⓘ</span></button></div><divclass="map-stage"><buttonid="networkSummary"hiddenaria-label="Сетевая активность на карте"></button><divclass="map-controls"><buttonid="zoomOut"aria-label="Отдалить карту">−</button><outputid="zoomLevel">100%</output><buttonid="zoomIn"aria-label="Приблизить карту">+</button><buttonid="zoomReset"aria-label="Сбросить масштаб карты">↺</button></div><canvastabindex="0"id="world"role="img"aria-label="Карта стран источников; данные доступны также в статистике и ленте событий"></canvas></div><divclass="map-foot"><divclass="legend"><span><iclass="dot"></i>HTTP</span><span><iclass="dot alert"></i>Срабатывание</span><span><iclass="dot decision"></i>Решение</span></div><spanid="cityLayerNote"title="Города по приблизительным координатам GeoIP"></span><spanid="unmapped">Без геолокации: —</span></div></div>
<asideclass="panel feed-panel"><divclass="feed-heading"><h2>Поток событий</h2><spanclass="note"id="feedMode">LIVE / 100</span></div><divclass="feed"id="feed"><divclass="empty">Ожидание первого события…</div></div><divclass="pager"><buttonid="feedPrev"aria-label="Предыдущая страница событий">←</button><spanid="feedPage">—</span><buttonid="feedNext"aria-label="Следующая страница событий">→</button><buttonid="latest">К новым ↗</button></div></aside></section>
<sectionclass="bottom"><divclass="panel chart-panel"><divclass="bottom-head"><h2>Ритм запросов <spanclass="note">/ 60 мин</span></h2><spanclass="note"><bid="qps"style="color:var(--blue)">—</b> req/s</span></div><canvasclass="chart"id="chart"role="img"aria-label="HTTP-запросы в минуту за последний час; req/s — среднее за последние 60 секунд"></canvas></div><divclass="panel"><divclass="bottom-head"><h2>География</h2><spanclass="note">ТОП 7 / события</span></div><divid="countries"></div></div><divclass="panel"><divclass="bottom-head"><h2>Источники</h2><spanclass="note">ТОП 7 / IP</span></div><divid="ips"></div></div></section>
<footer><divclass="credits"><span><ahref="https://db-ip.com"target="_blank"rel="noopener">IP Geolocation by DB-IP</a> · <spanid="geoDate"></span></span><aclass="license-credit"href="https://creativecommons.org/licenses/by/4.0/">CC BY 4.0</a><aclass="natural-credit"href="https://www.naturalearthdata.com/">Natural Earth</a></div><buttonclass="footer-help"id="about">Как читать данные ⓘ</button><ahref="/#traffic-settings">Настройки ↗</a></footer>
@@ -52,9 +52,11 @@ function networkVisible(){return !!view?.network&&el('networkOverlay').checked&&
ctx.globalAlpha=1;ctx.strokeStyle=colors.decision;ctx.lineWidth=1.5/mapZoom;ctx.beginPath();ctx.arc(...end,7/mapZoom,0,Math.PI*2);ctx.stroke();ctx.fillStyle=isLight()?'#554496':'#e7dcff';ctx.beginPath();ctx.arc(...end,3/mapZoom,0,Math.PI*2);ctx.fill();ctx.font=(11/mapZoom)+'px system-ui';ctx.textAlign='center';ctx.fillText(destination?'NPM · '+destination.name:'NPM · геопозиция не задана',end[0],end[1]+22/mapZoom);
}
ctx.restore();
@@ -70,7 +72,7 @@ function render(data){view=data;el('networkLink').hidden=!data.settings.networkE
constunmapped=data.countries.filter(c=>!countryMap.has(c.name)).reduce((n,c)=>n+c.count,0);el('unmapped').textContent='Локальные / без геолокации: '+num(unmapped);
functionrenderNetworkSummary(){el('networkSummary').hidden=!networkVisible();if(!networkVisible())return;constn=view.network,fresh=networkFresh();el('networkSummary').innerHTML=(fresh?'Сеть · ':'Сеть · нет свежего снимка · ')+['tcp','udp','icmp'].map(p=>'<span style="color:'+colors[p]+'">'+p.toUpperCase()+' '+num(n[p])+'</span>').join(' · ')+(n.error||n.limited?' · ⚠':'')+' ⓘ';el('networkSummary').title='Текущие потоки conntrack. Фильтры домена и HTTP/CrowdSec на них не влияют.'}
el('networkSummary').onclick=()=>{constn=view.network;showDetails('Сетевая активность на карте','<p>Зелёный — TCP, розовый — UDP, жёлтый — ICMP/ICMPv6. Кольца показывают страны текущих потоков, импульс означает получение нового снимка (~5 с), а не новый запрос или пакет.</p><p>Входящий поток идёт к NPM, исходящий — от NPM. Если оба конца публичные, направление определяется по инициатору; это не трассировка маршрута.</p><p>HTTP-фильтры домена и типа события относятся только к HTTP/CrowdSec. «Только внешние IP» и «Скрыть мониторинг» применяются также к сетевому слою. Для сети используются только правила без домена/пути.</p><p>Снимок: '+(n.at?newDate(n.at*1000).toLocaleString('ru-RU'):'ещё не получен')+'. Локальные / без страны: '+num(n.unmapped)+'. Скрыто потоков мониторинга: '+num(n.monitoringHidden)+'.</p><p>'+safe(n.error||(n.limited?'Достигнут лимит 5 000 потоков':'При устаревании снимка сетевые кольца и импульсы скрываются.'))+'</p><p><a href="/network">Открыть таблицу TCP/UDP/ICMP ↗</a></p>')};
el('about').onclick=()=>showDetails('Как читать данные','<p>Голубой — HTTP-запросы, оранжевый — срабатывания CrowdSec, сиреневый — решения о ban или captcha.</p><p>Решение о бане не подтверждает конкретный отброшенный запрос. HTTP 401/403/502 сами по себе не означают блокировку CrowdSec.</p><p>Счётчики за 24 часа следуют выбранным фильтрам. Уникальный IP не равен человеку. Req/s — среднее за 60 секунд; график показывает запросы в минуту за последний час.</p><p>Точки на карте обозначают страны, а не точное местоположение людей. Локальные адреса остаются в ленте без страны. «Скрыть мониторинг» исключает из отображения HTTP-проверки по правилам в настройках; события CrowdSec сохраняются на экране. Исходная история не удаляется. В ленте показаны последние 100 событий; полная сохранённая история за 24 часа доступна в карточках и окнах стран; нажмите строку, чтобы увидеть детали целиком.</p>');
el('about').onclick=()=>showDetails('Как читать данные','<p>Голубой — HTTP-запросы, оранжевый — срабатывания CrowdSec, сиреневый — решения о ban или captcha.</p><p>Решение о бане не подтверждает конкретный отброшенный запрос. HTTP 401/403/502 сами по себе не означают блокировку CrowdSec.</p><p>Счётчики за 24 часа следуют выбранным фильтрам. Уникальный IP не равен человеку. Req/s — среднее за 60 секунд; график показывает запросы в минуту за последний час.</p><p>При масштабе от 200% и установленной базе городов появляются города с HTTP/CrowdSec-активностью. Геолокация приблизительная. Сетевые потоки и события без города остаются на уровне стран. Показываются до 500 наиболее активных городов; остальные события учитываются в точках стран. Локальные адреса остаются в ленте без страны. «Скрыть мониторинг» исключает из отображения HTTP-проверки по правилам в настройках; события CrowdSec сохраняются на экране. Исходная история не удаляется. В ленте показаны последние 100 событий; полная сохранённая история за 24 часа доступна в карточках и окнах стран; нажмите строку, чтобы увидеть детали целиком.</p>');
functionopenCity(c){showDetails(c.city,filterNote()+'<p>'+safe([countryName(c.country),c.region].filter(Boolean).join(' · '))+'</p>'+statGrid(c)+'<p class="modal-note">Приблизительное местоположение IP по DB-IP. Это может быть город провайдера или VPN, а не местоположение посетителя.</p><p>Последнее событие: '+newDate(c.lastSeen*1000).toLocaleString('ru-RU')+'</p><button class="country-choice" data-country="'+safe(c.country)+'">Вся история страны: '+safe(countryName(c.country))+'</button>')}
document.querySelectorAll('[data-metric]').forEach(b=>b.onclick=()=>{if(!view)return;constkey=b.dataset.metric,title={unique:'Уникальные HTTP IP',requests:'HTTP-запросы',alerts:'Срабатывания CrowdSec',decisions:'Решения CrowdSec'}[key],field=key==='unique'?'uniqueIps':key,type={unique:'request',requests:'request',alerts:'alert',decisions:'decision'}[key],rows=(view.countryDetails||[]).filter(c=>c[field]>0).sort((a,b)=>b[field]-a[field]);constexplanation={unique:'Уникальный IP не равен человеку. Учитываются IP HTTP-запросов.',requests:'Количество HTTP-запросов. Ошибки HTTP сами по себе не доказывают атаку.',alerts:'Срабатывания сценариев CrowdSec. Одно срабатывание может объединять несколько запросов.',decisions:'Решения CrowdSec, например ban или captcha. Это не число отброшенных пакетов и не список активных банов.'}[key];showDetails(title,filterNote()+'<div class="detail-stat"><small>'+title+'</small><b>'+num(view[field])+'</b></div><p>'+explanation+'</p><h3>По странам</h3>'+rows.map(c=>'<button class="country-choice" data-country="'+safe(c.country)+'">'+safe(countryName(c.country))+' · '+num(c[field])+'</button>').join('')+historyShell());startHistory({type,group:key==='unique'?'ip':''})});
functionhistoryShell(){return'<div id="historyFilters" role="group" aria-label="Тип событий в истории"></div><section id="historyPanel"><p>Загружаем историю…</p></section>'}
asyncfunctionloadHistory(page){constversion=historyVersion,query={...historyQuery,page:String(page)};historyBusy=true;try{constr=awaitfetch('/api/traffic/history?'+newURLSearchParams(query));if(!r.ok)throwError('Не удалось загрузить историю: HTTP '+r.status);constd=awaitr.json();if(version!==historyVersion||!el('details').open||!el('historyPanel'))return;historyQuery.snapshot=String(d.snapshot);historyQuery.maxSequence=String(d.maxSequence);constpages=Math.max(1,Math.ceil(d.total/d.pageSize)),start=d.total?page*d.pageSize+1:0,end=Math.min(d.total,(page+1)*d.pageSize);letbody=query.group==='ip'?(d.ips.length?'<ul class="detail-list">'+d.ips.map(i=>'<li><b>'+safe(i.ip)+'</b> · '+num(i.requests)+' HTTP-запросов<small>'+safe([countryName(i.country),i.region,i.city].filter(Boolean).join(' · '))+' · последнее '+newDate(i.lastSeen*1000).toLocaleString('ru-RU')+'</small></li>').join('')+'</ul>':'<p>IP не найдены.</p>'):eventList(d.events);el('historyPanel').innerHTML='<h3>'+(query.group==='ip'?'Все уникальные HTTP IP':'История событий')+'</h3><p class="modal-note">'+num(start)+'–'+num(end)+' из '+num(d.total)+' · последние 24 часа на момент открытия. '+(d.cityReady?'Город/регион приблизительны; отсутствие названия означает, что база их не определила.':'Для городов установите базу GeoIP в настройках карты.')+'</p>'+body+'<div class="history-pager"><button data-history-page="'+(page-1)+'" '+(page===0?'disabled':'')+'>← Назад</button><span>'+(page+1)+' / '+pages+'</span><button data-history-page="'+(page+1)+'" '+(page+1>=pages?'disabled':'')+'>Далее →</button></div>'}catch(e){if(version===historyVersion&&el('historyPanel'))el('historyPanel').innerHTML='<p>'+safe(e.message)+'</p><button data-history-page="'+page+'">Повторить</button>'}finally{if(version===historyVersion)historyBusy=false}}
r,err:=s.db.Query(`SELECT CAST((ts/60)*60 AS TEXT),COUNT(*) FROM traffic_events`+where+` AND kind='request' AND ts>=? GROUP BY ts/60 ORDER BY ts/60`,append(args,now-3600)...)
rows,err:=s.db.Query(`SELECT ip,country,SUM(kind='request'),SUM(kind='alert'),SUM(kind='decision'),MAX(ts) FROM traffic_events`+where+` GROUP BY ip,country`,args...)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.