From 03b6c1f2892eef2e5557944b88a15f8dea4f2d49 Mon Sep 17 00:00:00 2001 From: Maxim Date: Thu, 15 Oct 2026 20:41:07 +0300 Subject: [PATCH] Add event filters to traffic dialogs and mirror city database in Gitea --- README.md | 13 +++++++++++++ assets/traffic.html | 9 +++++---- traffic_city.go | 20 ++++++++++++++++++-- 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 3974783..7a0fef7 100644 --- a/README.md +++ b/README.md @@ -665,3 +665,16 @@ IP посетителей не отправляются во внешние се в истории и списке IP; местоположение приблизительное и может относиться к VPN или провайдеру. Для части IP город неизвестен. База устанавливается отдельно от релиза, автоматически каждый месяц не обновляется. + +## Фильтр истории и зеркало GeoIP (v0.25.1) + +В окнах стран и событий есть локальный переключатель «Все события / HTTP / +Срабатывания / Решения CrowdSec». Он меняет тип событий только внутри окна, +сохраняя страну, домен, остальные фильтры и снимок времени; пагинация начинается +с первой страницы. Сводные карточки над историей сохраняют исходные итоги. + +DB-IP City Lite загружается сначала из вложения релиза v0.25.1 в Gitea, +при ошибке — с официального сервера DB-IP. Для собственного адреса Gitea можно +задать `GITEA_URL` в окружении сервиса Dashboard. База хранится отдельно от Git-истории +и бинарника приложения; контрольная сумма проверяется независимо от источника. +Официальный файл: https://download.db-ip.com/free/dbip-city-lite-2026-10.mmdb.gz diff --git a/assets/traffic.html b/assets/traffic.html index bd0254d..957e669 100644 --- a/assets/traffic.html +++ b/assets/traffic.html @@ -26,7 +26,7 @@ html[data-theme="light"] .metric i{background:#e2f2e7}html[data-theme="light"] . html[data-theme="light"] .section-label,html[data-theme="light"] .event .path,html[data-theme="light"] dialog p{color:var(--text)}html[data-theme="light"] .event .meta{color:var(--muted)}html[data-theme="light"] .event{border-color:var(--line)}html[data-theme="light"] .event:hover{background:#eaf1f4} html[data-theme="light"] dialog,html[data-theme="light"] .tooltip,html[data-theme="light"] #networkSummary{background:#fff;color:var(--text);border-color:var(--line);box-shadow:0 12px 40px #4e64752b}html[data-theme="light"] dialog::backdrop{background:#1e2d3759}html[data-theme="light"] .monitoring-tag{color:var(--green)} @media(max-width:760px){header nav{gap:5px}#themeToggle{padding:5px}.map-controls{bottom:3px;right:3px}.map-controls button{padding:4px 8px}} -.history-pager{display:flex;align-items:center;justify-content:space-between;gap:12px;position:sticky;bottom:-22px;background:var(--panel);padding:12px 0}dialog{width:min(760px,calc(100vw - 30px))} +#historyFilters{display:flex;flex-wrap:wrap;gap:6px;margin:16px 0}#historyFilters[hidden]{display:none}#historyFilters button[aria-pressed="true"]{border-color:var(--blue);background:var(--panel);box-shadow:inset 0 -2px currentColor}.history-pager{display:flex;align-items:center;justify-content:space-between;gap:12px;position:sticky;bottom:-22px;background:var(--panel);padding:12px 0}dialog{width:min(760px,calc(100vw - 30px))}
PROXMOX / LIVE OBSERVATORY

Traffic space.

@@ -119,10 +119,11 @@ mapCanvas.onpointerup=e=>{const moved=drag?.moved; pointers.delete(e.pointerId); mapCanvas.onpointercancel=e=>{pointers.clear();drag=null;hideCountryTip()};mapCanvas.onpointerleave=()=>{hideCountryTip();redrawMap()}; window.addEventListener('resize',clampMap); document.querySelectorAll('[data-metric]').forEach(b=>b.onclick=()=>{if(!view)return;const key=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]);const explanation={unique:'Уникальный IP не равен человеку. Учитываются IP HTTP-запросов.',requests:'Количество HTTP-запросов. Ошибки HTTP сами по себе не доказывают атаку.',alerts:'Срабатывания сценариев CrowdSec. Одно срабатывание может объединять несколько запросов.',decisions:'Решения CrowdSec, например ban или captcha. Это не число отброшенных пакетов и не список активных банов.'}[key];showDetails(title,filterNote()+'
'+title+''+num(view[field])+'

'+explanation+'

По странам

'+rows.map(c=>'').join('')+historyShell());startHistory({type,group:key==='unique'?'ip':''})}); -el('detailsBody').onclick=e=>{const b=e.target.closest('[data-country]');if(b)openCountry(b.dataset.country);const h=e.target.closest('[data-history-page]');if(h&&!historyBusy)loadHistory(Number(h.dataset.historyPage))}; +el('detailsBody').onclick=e=>{const b=e.target.closest('[data-country]');if(b)openCountry(b.dataset.country);const f=e.target.closest('[data-history-kind]');if(f&&historyQuery){historyVersion++;historyBusy=false;historyQuery.kind=f.dataset.historyKind;historyQuery.type='';renderHistoryFilters();loadHistory(0)}const h=e.target.closest('[data-history-page]');if(h&&!historyBusy)loadHistory(Number(h.dataset.historyPage))}; let historyQuery=null,historyVersion=0,historyBusy=false; -function historyShell(){return '

Загружаем историю…

'} -function startHistory(extra){historyVersion++;historyBusy=false;historyQuery={domain:el('domain').value,kind,external:el('external').checked?'1':'0',hideMonitoring:el('hideMonitoring').checked?'1':'0',snapshot:String(view.now),maxSequence:String(view.maxSequence),...extra};loadHistory(0)} +function historyShell(){return '

Загружаем историю…

'} +function startHistory(extra){historyVersion++;historyBusy=false;historyQuery={domain:el('domain').value,kind,external:el('external').checked?'1':'0',hideMonitoring:el('hideMonitoring').checked?'1':'0',snapshot:String(view.now),maxSequence:String(view.maxSequence),...extra};renderHistoryFilters();loadHistory(0)} +function renderHistoryFilters(){const box=el('historyFilters');if(!box)return;box.hidden=historyQuery.group==='ip';const selected=historyQuery.type||historyQuery.kind||'';box.innerHTML=[['','Все события'],['request','HTTP'],['alert','Срабатывания'],['decision','Решения CrowdSec']].map(([value,label])=>'').join('')} async function loadHistory(page){const version=historyVersion,query={...historyQuery,page:String(page)};historyBusy=true;try{const r=await fetch('/api/traffic/history?'+new URLSearchParams(query));if(!r.ok)throw Error('Не удалось загрузить историю: HTTP '+r.status);const d=await r.json();if(version!==historyVersion||!el('details').open||!el('historyPanel'))return;historyQuery.snapshot=String(d.snapshot);historyQuery.maxSequence=String(d.maxSequence);const pages=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);let body=query.group==='ip'?(d.ips.length?'':'

IP не найдены.

'):eventList(d.events);el('historyPanel').innerHTML='

'+(query.group==='ip'?'Все уникальные HTTP IP':'История событий')+'

'+body+'
'+(page+1)+' / '+pages+'
'}catch(e){if(version===historyVersion&&el('historyPanel'))el('historyPanel').innerHTML='

'+safe(e.message)+'

'}finally{if(version===historyVersion)historyBusy=false}} el('details').addEventListener('close',()=>{historyVersion++;historyBusy=false;historyQuery=null}); applyTrafficTheme(document.documentElement.dataset.theme||'dark');clampMap(); diff --git a/traffic_city.go b/traffic_city.go index 5d37dad..64806eb 100644 --- a/traffic_city.go +++ b/traffic_city.go @@ -101,9 +101,25 @@ func trafficCityRoutes(mux *http.ServeMux) { writeJSON(w, status, nil) }) } -func installTrafficCity() error { return installTrafficCityFrom(cityDownloadURL, cityDatabaseSHA1) } +func cityMirrorURL() string { + base := strings.TrimRight(os.Getenv("GITEA_URL"), "/") + if base == "" { + base = "https://git.myown.center" + } + return base + "/maxim/ProxmoxDash/releases/download/v0.25.1/dbip-city-lite-2026-10.mmdb.gz" +} +func installTrafficCity() error { + if err := installTrafficCityFrom(cityMirrorURL(), cityDatabaseSHA1); err == nil { + return nil + } + return installTrafficCityFrom(cityDownloadURL, cityDatabaseSHA1) +} func installTrafficCityFrom(source, expectedHash string) error { - client := &http.Client{Timeout: 15 * time.Minute} + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.ResponseHeaderTimeout = 20 * time.Second + transport.DialContext = (&net.Dialer{Timeout: 10 * time.Second, KeepAlive: 30 * time.Second}).DialContext + defer transport.CloseIdleConnections() + client := &http.Client{Timeout: 15 * time.Minute, Transport: transport} response, err := client.Get(source) if err != nil { return err