diff --git a/CHANGELOG.md b/CHANGELOG.md index ae59b89..8b6c31e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,9 +22,11 @@ Countries and networks database Simpler pages - Top-N is one table: conversations (client, server, service, country) by - default; click a column heading to group by it, a number heading to - sort. The row of eleven tabs is gone; the less common groupings are in - **Group by**. + default; **Group by** switches the grouping. Every column sorts, and + the number columns (traffic, packets, average packet size, flows) rank + all traffic in the range rather than re-ordering the rows shown: the + smallest average packet size finds scanners and floods. The row of + eleven tabs is gone. - "Who grew" is gone from the overview (web and terminal UI). - Flow paths: labels, bars and flows can all be clicked; before, only the thin bars could. diff --git a/README.md b/README.md index 1e289c8..7a3881b 100644 --- a/README.md +++ b/README.md @@ -623,7 +623,7 @@ Pages: | Page | What it answers | |---|---| | Overview | How much traffic now and compared with last week, by application; top clients and services | -| Top-N | One table of the top 66: by default conversations (client, server, service, country). Click a blue column heading to group by it, a number heading to sort; **Group by** offers applications, networks, segments, devices, encapsulation and VLAN | +| Top-N | One table of the top 66: by default conversations (client, server, service, country). Every column heading sorts; number columns (traffic, packets, average packet size, flows) rank all traffic in the range, so the smallest average packet size finds scanners and floods. **Group by** switches to applications, networks, segments, devices, encapsulation and VLAN | | Flow paths | Which segment talks to which application in which country | | Geo & networks | Traffic by country and by network (AS) | | Threat intel | Hosts that talked to addresses on your threat lists, and how much they sent | diff --git a/docs/README.ar.md b/docs/README.ar.md index 2c7524e..e6b9951 100644 --- a/docs/README.ar.md +++ b/docs/README.ar.md @@ -647,7 +647,7 @@ curl -L https://www.spamhaus.org/drop/drop.txt -o /threats/spamh | الصفحة | ما الذي تجيب عنه | |---|---| | نظرة عامة | حجم الحركة الآن ومقارنةً بالأسبوع الماضي حسب التطبيق؛ أبرز العملاء والخدمات | -| Top-N | جدول واحد لأعلى 66: افتراضيًا المحادثات (العميل والخادم والخدمة والدولة). انقر عنوان عمود أزرق للتجميع حسبه، أو عنوان عمود رقمي للفرز؛ ويتيح **التجميع حسب** التطبيقات والشبكات والمقاطع والأجهزة وأنواع التغليف وشبكات VLAN | +| Top-N | جدول واحد لأعلى 66: افتراضيًا المحادثات (العميل والخادم والخدمة والدولة). كل عنوان عمود يفرز؛ والأعمدة الرقمية (حركة المرور، الحزم، متوسط الحزمة، التدفقات) تعيد اختيار أعلى 66 من كل حركة المرور في الفترة، لذا يكشف أصغر متوسط حزمة عمليات المسح والإغراق. ويبدّل **التجميع حسب** إلى التطبيقات والشبكات والمقاطع والأجهزة وأنواع التغليف وشبكات VLAN | | مسارات الحركة | أي مقطع يتواصل مع أي تطبيق في أي دولة | | الجغرافيا والشبكات | الحركة حسب الدولة وحسب الشبكة (AS) | | معلومات التهديدات | المضيفات التي تواصلت مع عناوين في قوائم تهديداتك، وكم أرسلت | diff --git a/docs/README.bn.md b/docs/README.bn.md index 1f6f964..b2d680a 100644 --- a/docs/README.bn.md +++ b/docs/README.bn.md @@ -654,7 +654,7 @@ application, দেশ, ডিভাইস — ক্লিক করা যা | পেজ | কোন প্রশ্নের উত্তর দেয় | |---|---| | সারসংক্ষেপ | এখন কত ট্রাফিক আর গত সপ্তাহের তুলনায় কত, application অনুযায়ী; শীর্ষ client ও service | -| Top-N | শীর্ষ 66-এর একটি table: ডিফল্টভাবে conversation (client, server, service, দেশ)। কোনো নীল column heading-এ ক্লিক করলে সেটি অনুযায়ী group হয়, সংখ্যার heading-এ ক্লিক করলে sort হয়; **গ্রুপের ভিত্তি**-তে application, নেটওয়ার্ক, segment, ডিভাইস, encapsulation ও VLAN পাওয়া যায় | +| Top-N | শীর্ষ 66-এর একটি table: ডিফল্টভাবে conversation (client, server, service, দেশ)। যেকোনো column heading-এ sort হয়; সংখ্যার column (ট্রাফিক, প্যাকেট, গড় প্যাকেট, flow) সময়সীমার সব ট্রাফিক থেকে শীর্ষ 66 আবার বাছে, তাই সবচেয়ে ছোট গড় প্যাকেট দিয়ে scan ও flood ধরা পড়ে। **গ্রুপের ভিত্তি**-তে application, নেটওয়ার্ক, segment, ডিভাইস, encapsulation ও VLAN-এ যাওয়া যায় | | ট্রাফিকের পথ | কোন segment কোন দেশের কোন application-এর সাথে কথা বলে | | ভূগোল ও নেটওয়ার্ক | দেশ ও নেটওয়ার্ক (AS) অনুযায়ী ট্রাফিক | | হুমকির তথ্য | যেসব host আপনার threat list-এর address-এর সাথে কথা বলেছে, এবং কতটা পাঠিয়েছে | diff --git a/docs/README.es.md b/docs/README.es.md index 4dd3d49..d2c1ad8 100644 --- a/docs/README.es.md +++ b/docs/README.es.md @@ -674,7 +674,7 @@ Páginas: | Página | Qué responde | |---|---| | Resumen | Cuánto tráfico hay ahora y frente a la semana pasada, por aplicación; principales clientes y servicios | -| Top-N | Una sola tabla de los 66 primeros: por defecto, conversaciones (cliente, servidor, servicio, país). Haga clic en un encabezado de columna azul para agrupar por él, o en un encabezado numérico para ordenar; **Agrupar por** ofrece aplicaciones, redes, segmentos, equipos, encapsulación y VLAN | +| Top-N | Una sola tabla de los 66 primeros: por defecto, conversaciones (cliente, servidor, servicio, país). Cualquier encabezado ordena; las columnas numéricas (tráfico, paquetes, paquete medio, flujos) clasifican todo el tráfico del periodo, así el menor paquete medio revela escaneos e inundaciones. **Agrupar por** cambia a aplicaciones, redes, segmentos, equipos, encapsulación y VLAN | | Rutas de tráfico | Qué segmento habla con qué aplicación en qué país | | Geografía y redes | Tráfico por país y por red (AS) | | Inteligencia de amenazas | Hosts que hablaron con direcciones de sus listas de amenazas y cuánto enviaron | diff --git a/docs/README.fr.md b/docs/README.fr.md index 8d29282..ea5292c 100644 --- a/docs/README.fr.md +++ b/docs/README.fr.md @@ -691,7 +691,7 @@ Pages : | Page | À quoi elle répond | |---|---| | Vue d'ensemble | Combien de trafic maintenant et par rapport à la semaine dernière, par application ; principaux clients et services | -| Top-N | Un seul tableau du top 66 : par défaut les conversations (client, serveur, service, pays). Cliquez sur un en-tête de colonne bleu pour regrouper par cette colonne, sur un en-tête numérique pour trier ; **Regrouper par** propose applications, réseaux, segments, équipements, encapsulation et VLAN | +| Top-N | Un seul tableau du top 66 : par défaut les conversations (client, serveur, service, pays). Chaque en-tête trie ; les colonnes numériques (trafic, paquets, paquet moyen, flux) classent tout le trafic de la période, si bien que le plus petit paquet moyen révèle scans et inondations. **Regrouper par** passe aux applications, réseaux, segments, équipements, encapsulation et VLAN | | Chemins du trafic | Quel segment parle à quelle application dans quel pays | | Géographie et réseaux | Trafic par pays et par réseau (AS) | | Menaces | Hôtes ayant communiqué avec des adresses de vos listes de menaces, et volume envoyé | diff --git a/docs/README.hi.md b/docs/README.hi.md index cdce3e6..72f3b02 100644 --- a/docs/README.hi.md +++ b/docs/README.hi.md @@ -657,7 +657,7 @@ application, देश, डिवाइस — पर क्लिक किय | पेज | किस सवाल का जवाब देता है | |---|---| | सारांश | अभी कितना ट्रैफ़िक है और पिछले हफ़्ते की तुलना में कितना, application के हिसाब से; top clients और services | -| Top-N | top 66 की एक table: डिफ़ॉल्ट रूप से conversations (client, server, service, देश)। किसी नीले column heading पर क्लिक करके उसके हिसाब से group करें, किसी संख्या वाले heading पर क्लिक करके sort करें; **इसके अनुसार समूह** में applications, नेटवर्क, segments, डिवाइस, encapsulation और VLAN मिलते हैं | +| Top-N | top 66 की एक table: डिफ़ॉल्ट रूप से conversations (client, server, service, देश)। हर column heading से sort होता है; संख्या वाले columns (ट्रैफ़िक, पैकेट, औसत पैकेट, flows) समय-सीमा के पूरे ट्रैफ़िक से top 66 फिर से चुनते हैं, इसलिए सबसे छोटे औसत पैकेट से scan और flood पकड़े जाते हैं। **इसके अनुसार समूह** से applications, नेटवर्क, segments, डिवाइस, encapsulation और VLAN पर जाएँ | | ट्रैफ़िक के रास्ते | कौन-सा segment किस देश में किस application से बात करता है | | भूगोल और नेटवर्क | देश के हिसाब से और नेटवर्क (AS) के हिसाब से ट्रैफ़िक | | ख़तरे की जानकारी | वे hosts जिन्होंने आपकी threat lists के addresses से बात की, और उन्होंने कितना भेजा | diff --git a/docs/README.id.md b/docs/README.id.md index 27e813e..1b2352d 100644 --- a/docs/README.id.md +++ b/docs/README.id.md @@ -670,7 +670,7 @@ Halaman: | Halaman | Pertanyaan yang dijawab | |---|---| | Ringkasan | Berapa trafik sekarang dan dibanding minggu lalu, per aplikasi; klien dan layanan teratas | -| Top-N | Satu tabel 66 teratas: secara default percakapan (klien, server, layanan, negara). Klik judul kolom biru untuk mengelompokkan menurut kolom itu, judul kolom angka untuk mengurutkan; **Kelompokkan menurut** menawarkan aplikasi, jaringan, segmen, perangkat, enkapsulasi, dan VLAN | +| Top-N | Satu tabel 66 teratas: secara default percakapan (klien, server, layanan, negara). Setiap judul kolom mengurutkan; kolom angka (lalu lintas, paket, rata-rata paket, flow) memilih ulang 66 teratas dari semua lalu lintas dalam rentang, sehingga rata-rata paket terkecil menemukan pemindaian dan banjir. **Kelompokkan menurut** beralih ke aplikasi, jaringan, segmen, perangkat, enkapsulasi, dan VLAN | | Jalur trafik | Segmen mana berbicara dengan aplikasi apa di negara mana | | Geografi & jaringan | Trafik per negara dan per jaringan (AS) | | Intel ancaman | Host yang berkomunikasi dengan alamat di daftar ancaman Anda, dan berapa banyak yang mereka kirim | diff --git a/docs/README.ja.md b/docs/README.ja.md index 3ce6970..862decd 100644 --- a/docs/README.ja.md +++ b/docs/README.ja.md @@ -547,7 +547,7 @@ curl -L https://www.spamhaus.org/drop/drop.txt -o /threats/spamh | ページ | 分かること | |---|---| | 概要 | 現在のトラフィック量と先週との比較(アプリケーション別)、上位のクライアントとサービス | -| Top-N | 上位 66 件を 1 つの表で表示。既定は会話(クライアント、サーバー、サービス、国)。青い列見出しをクリックするとその列でグループ化、数値の列見出しをクリックすると並べ替えます。**グループ化** ではアプリケーション、ネットワーク、セグメント、機器、カプセル化、VLAN も選べます | +| Top-N | 上位 66 件を 1 つの表で表示。既定は会話(クライアント、サーバー、サービス、国)。どの列見出しでも並べ替えられます。数値の列(トラフィック、パケット数、平均パケット長、フロー数)は期間内の全トラフィックから上位 66 件を取り直すので、平均パケット長の小さい順でスキャンやフラッドが見つかります。**グループ化** でアプリケーション、ネットワーク、セグメント、機器、カプセル化、VLAN に切り替えられます | | フローの流れ | どのセグメントがどの国のどのアプリケーションと通信しているか | | 地域とネットワーク | 国別・ネットワーク(AS)別のトラフィック | | 脅威インテル | 脅威リスト上のアドレスと通信したホストと、その送信量 | diff --git a/docs/README.ko.md b/docs/README.ko.md index 941be27..bc3b664 100644 --- a/docs/README.ko.md +++ b/docs/README.ko.md @@ -634,7 +634,7 @@ curl -L https://www.spamhaus.org/drop/drop.txt -o /threats/spamh | 페이지 | 알 수 있는 것 | |---|---| | 개요 | 현재 트래픽 양과 지난주 대비 변화(애플리케이션별), 상위 클라이언트와 서비스 | -| Top-N | 상위 66개를 담은 하나의 표: 기본은 대화(클라이언트, 서버, 서비스, 국가). 파란 열 제목을 클릭하면 그 열로 그룹화하고, 숫자 열 제목을 클릭하면 정렬합니다. **그룹 기준**에서 애플리케이션, 네트워크, 세그먼트, 장비, 캡슐화, VLAN을 고를 수 있습니다 | +| Top-N | 상위 66개를 담은 하나의 표: 기본은 대화(클라이언트, 서버, 서비스, 국가). 모든 열 제목으로 정렬할 수 있고, 숫자 열(트래픽, 패킷, 평균 패킷 크기, 플로)은 기간 내 전체 트래픽에서 상위 66개를 다시 뽑으므로 평균 패킷 크기가 작은 순으로 스캔과 플러드를 찾을 수 있습니다. **그룹 기준**에서 애플리케이션, 네트워크, 세그먼트, 장비, 캡슐화, VLAN으로 바꿀 수 있습니다 | | 트래픽 경로 | 어느 세그먼트가 어느 국가의 어느 애플리케이션과 통신하는지 | | 지역 및 네트워크 | 국가별, 네트워크(AS)별 트래픽 | | 위협 정보 | 위협 목록에 있는 주소와 통신한 호스트와 그 전송량 | diff --git a/docs/README.pt.md b/docs/README.pt.md index eea61ab..869be95 100644 --- a/docs/README.pt.md +++ b/docs/README.pt.md @@ -673,7 +673,7 @@ Páginas: | Página | O que responde | |---|---| | Visão geral | Quanto tráfego há agora e em comparação com a semana passada, por aplicação; principais clientes e serviços | -| Top-N | Uma única tabela dos 66 maiores: por padrão, conversas (cliente, servidor, serviço, país). Clique em um cabeçalho de coluna azul para agrupar por ele, em um cabeçalho numérico para ordenar; **Agrupar por** oferece aplicações, redes, segmentos, equipamentos, encapsulamento e VLAN | +| Top-N | Uma única tabela dos 66 maiores: por padrão, conversas (cliente, servidor, serviço, país). Qualquer cabeçalho ordena; as colunas numéricas (tráfego, pacotes, pacote médio, fluxos) classificam todo o tráfego do período, então o menor pacote médio revela varreduras e inundações. **Agrupar por** muda para aplicações, redes, segmentos, equipamentos, encapsulamento e VLAN | | Caminhos do tráfego | Qual segmento fala com qual aplicação em qual país | | Geografia e redes | Tráfego por país e por rede (AS) | | Ameaças | Hosts que se comunicaram com endereços das suas listas de ameaças, e quanto enviaram | diff --git a/docs/README.ru.md b/docs/README.ru.md index b3cb308..4ad0f87 100644 --- a/docs/README.ru.md +++ b/docs/README.ru.md @@ -667,7 +667,7 @@ curl -L https://www.spamhaus.org/drop/drop.txt -o /threats/spamh | Страница | На какой вопрос отвечает | |---|---| | Обзор | Сколько трафика сейчас и по сравнению с прошлой неделей, по приложениям; основные клиенты и сервисы | -| Top-N | Одна таблица топ 66: по умолчанию сеансы (клиент, сервер, сервис, страна). Щелчок по синему заголовку столбца группирует по нему, по числовому — сортирует; **Группировать по** предлагает приложения, сети, сегменты, устройства, инкапсуляцию и VLAN | +| Top-N | Одна таблица топ 66: по умолчанию сеансы (клиент, сервер, сервис, страна). Любой заголовок сортирует; числовые столбцы (трафик, пакеты, средний пакет, потоки) заново выбирают топ 66 из всего трафика за период, поэтому наименьший средний пакет находит сканирования и флуд. **Группировать по** переключает на приложения, сети, сегменты, устройства, инкапсуляцию и VLAN | | Пути трафика | Какой сегмент обращается к какому приложению в какой стране | | География и сети | Трафик по странам и по сетям (AS) | | Угрозы | Хосты, обращавшиеся к адресам из ваших списков угроз, и сколько они отправили | diff --git a/docs/README.ur.md b/docs/README.ur.md index 0780926..421120b 100644 --- a/docs/README.ur.md +++ b/docs/README.ur.md @@ -657,7 +657,7 @@ application، ملک، ڈیوائس — پر کلک کیا جا سکتا ہے: | صفحہ | کس سوال کا جواب دیتا ہے | |---|---| | جائزہ | ابھی کتنی ٹریفک ہے اور پچھلے ہفتے کے مقابلے میں کتنی، application کے حساب سے؛ سرفہرست clients اور services | -| ٹاپ N | سرفہرست 66 کی ایک table: ڈیفالٹ طور پر conversations (client، server، service، ملک)۔ کسی نیلے column heading پر کلک کر کے اس کے حساب سے group کریں، کسی عددی heading پر کلک کر کے sort کریں؛ **گروپ بندی بلحاظ** میں applications، نیٹ ورکس، segments، ڈیوائسز، encapsulation اور VLAN ملتے ہیں | +| ٹاپ N | سرفہرست 66 کی ایک table: ڈیفالٹ طور پر conversations (client، server، service، ملک)۔ ہر column heading سے sort ہوتا ہے؛ عددی columns (ٹریفک، پیکٹ، اوسط پیکٹ، flows) وقت کی حد کے پورے ٹریفک سے سرفہرست 66 دوبارہ چنتے ہیں، اس لیے سب سے چھوٹے اوسط پیکٹ سے scan اور flood پکڑے جاتے ہیں۔ **گروپ بندی بلحاظ** سے applications، نیٹ ورکس، segments، ڈیوائسز، encapsulation اور VLAN پر جائیں | | ٹریفک کے راستے | کون سا segment کس ملک میں کس application سے بات کرتا ہے | | مقامات اور نیٹ ورک | ملک کے حساب سے اور نیٹ ورک (AS) کے حساب سے ٹریفک | | خطرے کی معلومات | وہ hosts جنہوں نے آپ کی threat lists کے addresses سے بات کی، اور کتنا بھیجا | diff --git a/docs/README.zh.md b/docs/README.zh.md index 9ceabec..f3434e3 100644 --- a/docs/README.zh.md +++ b/docs/README.zh.md @@ -547,7 +547,7 @@ curl -L https://www.spamhaus.org/drop/drop.txt -o /threats/spamh | 页面 | 回答什么问题 | |---|---| | 概览 | 当前流量多大、与上周相比如何,按应用拆分;主要客户端和服务 | -| Top-N | 一张前 66 名的表:默认是会话(客户端、服务器、服务、国家)。点击蓝色列标题按该列分组,点击数字列标题排序;**分组方式** 还提供应用、网络、网段、设备、封装和 VLAN | +| Top-N | 一张前 66 名的表:默认是会话(客户端、服务器、服务、国家)。每个列标题都能排序;数字列(流量、包数、平均包长、流数)按时间范围内的全部流量重新取前 66 名,按平均包长从小到大排能找出扫描和泛洪。**分组方式** 可切换到应用、网络、网段、设备、封装和 VLAN | | 流向 | 哪个网段在访问哪个国家的哪个应用 | | 地理与运营商 | 按国家和按网络(AS)统计的流量 | | 威胁情报 | 与威胁情报列表中地址有通信的主机,以及它们发送了多少流量 | diff --git a/docs/images/topn.png b/docs/images/topn.png index a563004..c8077d5 100644 Binary files a/docs/images/topn.png and b/docs/images/topn.png differ diff --git a/internal/api/handlers.go b/internal/api/handlers.go index e811897..a9d5c2d 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -220,8 +220,11 @@ func (s *Server) topn(w http.ResponseWriter, r *http.Request) { fail(w, err) return } - dim := r.URL.Query().Get("dim") - rows, err := s.Store.TopN(q, dim, limitParam(r, 66)) + dim, by := r.URL.Query().Get("dim"), r.URL.Query().Get("by") + if by == "" { + by = "wire" + } + rows, err := s.Store.TopNBy(q, dim, limitParam(r, 66), by, r.URL.Query().Get("asc") == "1") if err != nil { fail(w, err) return diff --git a/internal/pipeline/pipeline_test.go b/internal/pipeline/pipeline_test.go index a8d0e3c..d057252 100644 --- a/internal/pipeline/pipeline_test.go +++ b/internal/pipeline/pipeline_test.go @@ -166,6 +166,49 @@ func TestTopNIPv6Groups(t *testing.T) { } } +// Ranking by another measure picks a different top, in both the detail +// query and the rollup. +func TestTopNBy(t *testing.T) { + p, st := setup(t) + now := time.Now().UTC().Truncate(time.Hour) + exp := netip.MustParseAddr("10.0.0.1") + srv := netip.MustParseAddr("198.51.100.9") + big, small := netip.MustParseAddr("10.1.1.1"), netip.MustParseAddr("10.1.1.2") + var recs []flow.Record + for i := 0; i < 8*60; i++ { + ts := now.Add(-8*time.Hour + time.Duration(i)*time.Minute) + // big: one flow of 100 full-size packets; small: 5 flows of 100 tiny packets + recs = append(recs, flow.Record{Start: ts, End: ts, Src: big, Dst: srv, SrcPort: 50000, DstPort: 443, Proto: 6, + Bytes: 100 * 1500, Packets: 100, Mult: 1, SamplingKnown: true, Exporter: exp, Source: flow.SrcIPFIX}) + for f := 0; f < 5; f++ { + recs = append(recs, flow.Record{Start: ts, End: ts, Src: small, Dst: srv, SrcPort: uint16(40000 + f), DstPort: 80, Proto: 6, + Bytes: 100 * 40, Packets: 100, Mult: 1, SamplingKnown: true, Exporter: exp, Source: flow.SrcIPFIX}) + } + } + p.Ingest(recs) + p.FlushRows() + p.FlushRollups() + for _, span := range []time.Duration{time.Hour, 8 * time.Hour} { // detail, rollup + q := store.Query{From: now.Add(-span), To: now} + for _, tc := range []struct { + by string + asc bool + want string + }{{"wire", false, "10.1.1.1"}, {"pkts", false, "10.1.1.2"}, {"flows", false, "10.1.1.2"}, {"avg", true, "10.1.1.2"}, {"avg", false, "10.1.1.1"}, {"wire", true, "10.1.1.2"}} { + top, err := st.TopNBy(q, "client", 1, tc.by, tc.asc) + if err != nil { + t.Fatal(err) + } + if len(top) != 1 || top[0].Key != tc.want { + t.Errorf("%v by %s asc=%v: %+v want %s", span, tc.by, tc.asc, top, tc.want) + } + } + } + if _, err := st.TopNBy(store.Query{From: now.Add(-time.Hour), To: now}, "client", 1, "nope"); err == nil { + t.Error("unknown ranking accepted") + } +} + // Detail older than the retention is removed from disk and queries. func TestRetention(t *testing.T) { st, err := store.Open(store.Options{Dir: t.TempDir(), RawDays: 1}) diff --git a/internal/store/query.go b/internal/store/query.go index a4f4cd5..432cb44 100644 --- a/internal/store/query.go +++ b/internal/store/query.go @@ -277,6 +277,34 @@ func keyPartFor(expr string, i int) keyPart { // TopN returns the largest groups by wire bytes. func (s *Store) TopN(q Query, dim string, limit int) ([]TopRow, error) { + return s.TopNBy(q, dim, limit, "wire") +} + +// topOrder maps a ranking measure to the aggregate it orders by, for the +// detail query and the rollup query. +var topOrder = map[string][2]string{ + "wire": {"x_w", "sum(wire)"}, + "bytes": {"x_b", "sum(bytes)"}, + "pkts": {"x_p", "sum(pkts)"}, + "flows": {"x_f", "sum(flows)"}, + "peers": {"x_peers", "sum(wire)"}, // rollups hold no peer counts + // average packet size; smallest first is the interesting end (scans, + // floods), so callers ask for it ascending + "avg": {"x_w / greatest(x_p, 1)", "sum(wire) / greatest(sum(pkts), 1)"}, +} + +// TopNBy returns the groups ranked by one measure: wire, bytes, pkts, +// flows, peers or avg (average wire bytes per packet). asc ranks the +// smallest first. +func (s *Store) TopNBy(q Query, dim string, limit int, by string, asc ...bool) ([]TopRow, error) { + ord, ok := topOrder[by] + if !ok { + return nil, fmt.Errorf("unknown ranking %q", by) + } + dir := "DESC" + if len(asc) > 0 && asc[0] { + dir = "ASC" + } d, ok := dims[dim] if !ok { return nil, fmt.Errorf("unknown dimension %q", dim) @@ -285,7 +313,7 @@ func (s *Store) TopN(q Query, dim string, limit int) ([]TopRow, error) { limit = 66 } if q.usesRollup() && d.rollup != "" { - return s.topRollup(q, d.rollup, limit) + return s.topRollup(q, d.rollup, limit, ord[1]+" "+dir) } where, args, err := q.where() if err != nil { @@ -330,8 +358,8 @@ func (s *Store) TopN(q Query, dim string, limit int) ([]TopRow, error) { src := s.Source(q.From, q.To) aggs := fmt.Sprintf(`%s AS x_extra, sum(bytes) AS x_b, sum(wire) AS x_w, sum(pkts) AS x_p, sum(flows) AS x_f, %s AS x_peers, %s AS x_int`, extra, peers, intc) part := func(sel, grp []string, c string) string { - return fmt.Sprintf(`(SELECT %s, %s FROM %s WHERE %s GROUP BY %s ORDER BY x_w DESC LIMIT %d)`, - strings.Join(sel, ", "), aggs, src, c, strings.Join(grp, ", "), limit) + return fmt.Sprintf(`(SELECT %s, %s FROM %s WHERE %s GROUP BY %s ORDER BY %s %s, x_w DESC LIMIT %d)`, + strings.Join(sel, ", "), aggs, src, c, strings.Join(grp, ", "), ord[0], dir, limit) } body := part(inner, groups, cond) qargs := args @@ -340,8 +368,8 @@ func (s *Store) TopN(q Query, dim string, limit int) ([]TopRow, error) { body = part(inner, groups, "("+cond+") AND "+v4) + " UNION ALL " + part(inner6, groups6, "("+cond+") AND NOT ("+v4+")") qargs = append(append([]any{}, args...), args...) } - sqlq := fmt.Sprintf(`SELECT %s, x_extra, x_b, x_w, x_p, x_f, x_peers, x_int FROM (%s) ORDER BY x_w DESC LIMIT %d`, - strings.Join(outer, ", "), body, limit) + sqlq := fmt.Sprintf(`SELECT %s, x_extra, x_b, x_w, x_p, x_f, x_peers, x_int FROM (%s) ORDER BY %s %s, x_w DESC LIMIT %d`, + strings.Join(outer, ", "), body, ord[0], dir, limit) rows, err := s.DB.Query(sqlq, qargs...) if err != nil { return nil, err @@ -360,16 +388,16 @@ func (s *Store) TopN(q Query, dim string, limit int) ([]TopRow, error) { return out, rows.Err() } -func (s *Store) topRollup(q Query, rollup string, limit int) ([]TopRow, error) { +func (s *Store) topRollup(q Query, rollup string, limit int, order string) ([]TopRow, error) { var sqlq string args := []any{q.From.UTC(), q.To.UTC()} if strings.HasPrefix(rollup, "host:") { role, _ := strconv.Atoi(rollup[5:]) sqlq = fmt.Sprintf(`SELECT ip, sum(bytes), sum(wire), sum(pkts), sum(flows), bool_or(internal) FROM r_host - WHERE ts >= ? AND ts < ? AND role = %d GROUP BY 1 ORDER BY sum(wire) DESC LIMIT %d`, role, limit) + WHERE ts >= ? AND ts < ? AND role = %d GROUP BY 1 ORDER BY %s LIMIT %d`, role, order, limit) } else { sqlq = fmt.Sprintf(`SELECT val, sum(bytes), sum(wire), sum(pkts), sum(flows), false FROM r_dim - WHERE ts >= ? AND ts < ? AND dim = ? GROUP BY 1 ORDER BY sum(wire) DESC LIMIT %d`, limit) + WHERE ts >= ? AND ts < ? AND dim = ? GROUP BY 1 ORDER BY %s LIMIT %d`, order, limit) args = append(args, rollup) } rows, err := s.DB.Query(sqlq, args...) diff --git a/internal/web/static/app.css b/internal/web/static/app.css index c52a6c4..7e9dc5f 100644 --- a/internal/web/static/app.css +++ b/internal/web/static/app.css @@ -172,7 +172,6 @@ th.sk{cursor:pointer;user-select:none} th.sk:hover{color:var(--accent-ink)} th.sk[aria-sort]{color:var(--ink)} th.sk .ar{font-size:10px;margin-inline-start:3px;opacity:.7} -th.gk{cursor:pointer;color:var(--accent-ink);text-decoration:underline dotted;text-underline-offset:3px} .dimsel{border:1px solid var(--line-2);border-radius:6px;padding:4px 8px;background:var(--surface);font-size:13px} .ph .tools{display:flex;gap:10px;align-items:center;margin-inline-start:auto;font-size:12.5px;color:var(--ink-3)} /* sankey: labels and flows are clickable too */ diff --git a/internal/web/static/app.js b/internal/web/static/app.js index 51d9421..3597310 100644 --- a/internal/web/static/app.js +++ b/internal/web/static/app.js @@ -428,21 +428,27 @@ views.overview = async (el) => { const DIMS = ['conv', 'client', 'server', 'app', 'port', 'country', 'asn', 'segment', 'exporter', 'encap', 'vlan']; // Top-N is one table. It starts with conversations (client, server, -// service, country); clicking one of those column headings groups the -// table by it, clicking a number heading sorts. The select switches to the -// less common groupings. -const GROUP_OF = {'col.client': 'client', 'col.server': 'server', 'col.service': 'port', 'col.port': 'port', 'col.country': 'country', 'col.app': 'app', 'col.asn': 'asn', 'col.segment': 'segment', 'col.device': 'exporter'}; +// service, country); the select switches the grouping. Every column heading +// sorts: number columns rank on the server, so "by packets" is the real top +// by packets and not the top by traffic re-ordered; text columns sort the +// rows shown. +const NUMK = ['wire', 'pkts', 'avg', 'flows', 'peers']; +const TEXT_OF = {client: ['key'], server: ['key', 'extra'], conv: ['key', 'key2', 'key3', 'extra'], app: ['key'], port: ['key', 'extra'], + country: ['key', ''], asn: ['key', 'extra'], segment: ['key'], exporter: ['key'], encap: ['key'], vlan: ['key']}; views.topn = el => { if (!DIMS.includes(state.dim)) state.dim = 'conv'; return topTable(el, state.dim); }; async function topTable(el, dim) { - const d = await api('topn', {dim, limit: 66}); + const sk = state.sort.k, asc = state.sort.asc, num = NUMK.includes(sk); + const d = await api('topn', num ? {dim, limit: 66, by: sk, asc: asc ? 1 : 0} : {dim, limit: 66}); let rows = d.rows || []; const max = Math.max(1, ...rows.map(r => r.wire)), total = rows.reduce((s, r) => s + r.wire, 0); - const sk = state.sort.k, dirm = state.sort.asc ? 1 : -1; - if (sk !== 'wire') rows.sort((a, b) => dirm * ((a[sk] || 0) - (b[sk] || 0)) || b.wire - a.wire); - else if (state.sort.asc) rows.reverse(); + if (!num) { + const f = TEXT_OF[dim][+sk.slice(1)] || 'key'; + rows.sort((a, b) => (asc ? 1 : -1) * String(a[f] || '').localeCompare(String(b[f] || ''), undefined, {numeric: true})); + } + const avg = r => r.pkts ? r.wire / r.pkts : 0; const cell = r => { switch (dim) { case 'client': return [ipCell(r.key)]; @@ -461,28 +467,27 @@ async function topTable(el, dim) { }; const heads = {client: ['col.client'], server: ['col.server', 'col.country'], conv: ['col.client', 'col.server', 'col.service', 'col.country'], app: ['col.app'], port: ['col.port', 'col.app'], country: ['col.country', ''], asn: ['col.asn', 'col.org'], segment: ['col.segment'], exporter: ['col.device'], encap: ['col.encap'], vlan: ['col.vlan']}[dim]; - const showPeers = ['client', 'server'].includes(dim) && rows.some(r => r.peers); - const groupTh = h => { - const g = GROUP_OF[h]; - if (!h) return ''; - if (g && g !== dim) return `${t(h)}`; - return `${t(h)}`; - }; + // peer counts exist only for clients and servers over the detail data + const showPeers = ['client', 'server'].includes(dim) && (rows.some(r => r.peers) || sk === 'peers'); const sortTh = (k, label, cls = 'num') => { - const on = sk === k, ar = on ? (state.sort.asc ? '▲' : '▼') : ''; - return `${label}${ar}`; + if (!label) return ''; + const on = sk === k, ar = on ? (asc ? '▲' : '▼') : ''; + return `${label}${ar}`; }; const slow = dim === 'conv' && spanMs() > 216e5 ? ` · ${esc(t('conv.slow'))}` : ''; el.innerHTML = `

${t('topn.title', {n: 66})}

${esc(t('topn.hint'))}${slow}
- ${heads.map(groupTh).join('')}${sortTh('wire', t('col.traffic'))}${showPeers ? sortTh('peers', t('col.peers')) : ''}${sortTh('flows', t('col.flows'))} - ${rows.map((r, i) => `${cell(r).map(c => ``).join('')}${showPeers ? `` : ''}`).join('') || ``} +
%${t('col.share')}
${i + 1}${c}${fmtBytes(r.wire)}${nf(r.wire / Math.max(1, total) * 100, 1)}${bar(r.wire, max)}${nf(r.peers)}${nf(r.flows)}
${t('empty.nodata')}
${heads.map((h, i) => sortTh('t' + i, h && t(h), '')).join('')}${sortTh('wire', t('col.traffic'))}${sortTh('pkts', t('col.pkts'))}${sortTh('avg', t('col.avg'))}${showPeers ? sortTh('peers', t('col.peers')) : ''}${sortTh('flows', t('col.flows'))} + ${rows.map((r, i) => `${cell(r).map(c => ``).join('')}${showPeers ? `` : ''}`).join('') || ``}
%${t('col.share')}
${i + 1}${c}${fmtBytes(r.wire)}${nf(r.wire / Math.max(1, total) * 100, 1)}${bar(r.wire, max)}${nf(r.pkts)}${nf(avg(r))} B${nf(r.peers)}${nf(r.flows)}
${t('empty.nodata')}
`; $('#dimSel').onchange = e => { state.dim = e.target.value; state.sort = {k: 'wire', asc: false}; render(); }; - el.querySelectorAll('th[data-g]').forEach(h => h.onclick = () => { state.dim = h.dataset.g; state.sort = {k: 'wire', asc: false}; render(); }); el.querySelectorAll('th[data-s]').forEach(h => h.onclick = () => { - const k = h.dataset.s; state.sort = {k, asc: state.sort.k === k ? !state.sort.asc : false}; render(); + const k = h.dataset.s; + // a new column starts at its interesting end: largest first, except the + // average packet size (small packets: scans, floods) and text (A to Z) + state.sort = state.sort.k === k ? {k, asc: !state.sort.asc} : {k, asc: k === 'avg' || !NUMK.includes(k)}; + render(); }); } diff --git a/internal/web/static/i18n/ar.json b/internal/web/static/i18n/ar.json index 0800938..06d1ba4 100644 --- a/internal/web/static/i18n/ar.json +++ b/internal/web/static/i18n/ar.json @@ -94,6 +94,8 @@ "col.vs_prev": "مقارنةً بالفترة السابقة", "col.peers": "الأطراف", "col.flows": "التدفقات", +"col.pkts": "الحزم", +"col.avg": "متوسط الحزمة", "col.country": "الدولة", "col.asn": "AS", "col.org": "الشبكة", @@ -229,8 +231,7 @@ "pop.named": "تم الحفظ؛ يظهر الاسم الآن في كل مكان", "pop.actions": "إجراءات", "topn.group": "التجميع حسب", -"topn.hint": "انقر عنوان عمود أزرق للتجميع حسبه، أو عنوانًا رقميًا للفرز", -"topn.by_col": "جمّع الجدول حسب هذا العمود", +"topn.hint": "انقر على أي عنوان عمود للفرز. أعمدة الأرقام ترتّب كل حركة المرور في الفترة، وليس الصفوف المعروضة فقط.", "nav.detail": "التفاصيل", "det.peers": "المضيفون الذين يتواصل معهم", "det.clients": "المضيفون الذين يستخدمونه", diff --git a/internal/web/static/i18n/bn.json b/internal/web/static/i18n/bn.json index 025f782..751dba0 100644 --- a/internal/web/static/i18n/bn.json +++ b/internal/web/static/i18n/bn.json @@ -94,6 +94,8 @@ "col.vs_prev": "আগের সময়ের তুলনায়", "col.peers": "সহযোগী", "col.flows": "ফ্লো", +"col.pkts": "প্যাকেট", +"col.avg": "গড় প্যাকেট", "col.country": "দেশ", "col.asn": "AS", "col.org": "নেটওয়ার্ক", @@ -229,8 +231,7 @@ "pop.named": "সংরক্ষিত হয়েছে; নামটি এখন সব জায়গায় দেখাবে", "pop.actions": "অ্যাকশন", "topn.group": "গ্রুপের ভিত্তি", -"topn.hint": "নীল কলাম শিরোনামে ক্লিক করলে সেই কলাম অনুযায়ী গ্রুপ হবে, সংখ্যার শিরোনামে ক্লিক করলে সাজানো হবে", -"topn.by_col": "টেবিলটি এই কলাম অনুযায়ী গ্রুপ করুন", +"topn.hint": "সাজাতে যেকোনো কলামের শিরোনামে ক্লিক করুন। সংখ্যার কলামগুলো সময়সীমার সব ট্রাফিক থেকে র‍্যাঙ্ক করে, শুধু দেখানো সারিগুলো নয়।", "nav.detail": "বিস্তারিত", "det.peers": "যেসব হোস্টের সঙ্গে এটি কথা বলে", "det.clients": "যেসব হোস্ট এটি ব্যবহার করে", diff --git a/internal/web/static/i18n/en.json b/internal/web/static/i18n/en.json index 917c376..1939348 100644 --- a/internal/web/static/i18n/en.json +++ b/internal/web/static/i18n/en.json @@ -94,6 +94,8 @@ "col.vs_prev": "vs previous", "col.peers": "Peers", "col.flows": "Flows", +"col.pkts": "Packets", +"col.avg": "Avg packet", "col.country": "Country", "col.asn": "AS", "col.org": "Network", @@ -229,8 +231,7 @@ "pop.named": "Saved; the name now shows everywhere", "pop.actions": "Actions", "topn.group": "Group by", -"topn.hint": "Click a blue column heading to group by it, a number heading to sort", -"topn.by_col": "Group the table by this column", +"topn.hint": "Click any column heading to sort. Number columns rank all traffic in the time range, not just the rows shown.", "nav.detail": "Details", "det.peers": "Hosts it talks to", "det.clients": "Hosts using it", diff --git a/internal/web/static/i18n/es.json b/internal/web/static/i18n/es.json index 219c04d..7f3aa55 100644 --- a/internal/web/static/i18n/es.json +++ b/internal/web/static/i18n/es.json @@ -94,6 +94,8 @@ "col.vs_prev": "vs. periodo anterior", "col.peers": "Interlocutores", "col.flows": "Flujos", +"col.pkts": "Paquetes", +"col.avg": "Paquete medio", "col.country": "País", "col.asn": "AS", "col.org": "Red", @@ -229,8 +231,7 @@ "pop.named": "Guardado; el nombre ya aparece en todas partes", "pop.actions": "Acciones", "topn.group": "Agrupar por", -"topn.hint": "Haga clic en un encabezado de columna azul para agrupar por ella, o en uno numérico para ordenar", -"topn.by_col": "Agrupar la tabla por esta columna", +"topn.hint": "Haga clic en cualquier encabezado para ordenar. Las columnas numéricas clasifican todo el tráfico del periodo, no solo las filas mostradas.", "nav.detail": "Detalles", "det.peers": "Equipos con los que habla", "det.clients": "Equipos que lo usan", diff --git a/internal/web/static/i18n/fr.json b/internal/web/static/i18n/fr.json index fcf32de..9dca33d 100644 --- a/internal/web/static/i18n/fr.json +++ b/internal/web/static/i18n/fr.json @@ -94,6 +94,8 @@ "col.vs_prev": "vs période précédente", "col.peers": "Correspondants", "col.flows": "Flux", +"col.pkts": "Paquets", +"col.avg": "Paquet moyen", "col.country": "Pays", "col.asn": "AS", "col.org": "Réseau", @@ -229,8 +231,7 @@ "pop.named": "Enregistré ; le nom apparaît désormais partout", "pop.actions": "Actions", "topn.group": "Regrouper par", -"topn.hint": "Cliquez sur un en-tête de colonne bleu pour regrouper par cette colonne, sur un en-tête numérique pour trier", -"topn.by_col": "Regrouper le tableau par cette colonne", +"topn.hint": "Cliquez sur un en-tête de colonne pour trier. Les colonnes numériques classent tout le trafic de la période, pas seulement les lignes affichées.", "nav.detail": "Détails", "det.peers": "Hôtes avec lesquels il communique", "det.clients": "Hôtes qui l'utilisent", diff --git a/internal/web/static/i18n/hi.json b/internal/web/static/i18n/hi.json index 985f512..ffaf087 100644 --- a/internal/web/static/i18n/hi.json +++ b/internal/web/static/i18n/hi.json @@ -94,6 +94,8 @@ "col.vs_prev": "पिछली अवधि से", "col.peers": "साथी", "col.flows": "फ़्लो", +"col.pkts": "पैकेट", +"col.avg": "औसत पैकेट", "col.country": "देश", "col.asn": "AS", "col.org": "नेटवर्क", @@ -229,8 +231,7 @@ "pop.named": "सहेज लिया गया; यह नाम अब हर जगह दिखेगा", "pop.actions": "कार्रवाइयाँ", "topn.group": "इसके अनुसार समूह", -"topn.hint": "नीले कॉलम शीर्षक पर क्लिक करने से उसके अनुसार समूह बनेगा, संख्या वाले शीर्षक पर क्लिक करने से क्रम बदलेगा", -"topn.by_col": "तालिका को इस कॉलम के अनुसार समूहित करें", +"topn.hint": "क्रमबद्ध करने के लिए किसी भी कॉलम शीर्षक पर क्लिक करें। संख्या वाले कॉलम समय-सीमा के पूरे ट्रैफ़िक को रैंक करते हैं, केवल दिखाई गई पंक्तियों को नहीं।", "nav.detail": "विवरण", "det.peers": "जिन होस्ट से यह बात करता है", "det.clients": "जो होस्ट इसका उपयोग करते हैं", diff --git a/internal/web/static/i18n/id.json b/internal/web/static/i18n/id.json index 57eeb75..7597fe1 100644 --- a/internal/web/static/i18n/id.json +++ b/internal/web/static/i18n/id.json @@ -94,6 +94,8 @@ "col.vs_prev": "vs periode sebelumnya", "col.peers": "Lawan bicara", "col.flows": "Flow", +"col.pkts": "Paket", +"col.avg": "Rata-rata paket", "col.country": "Negara", "col.asn": "AS", "col.org": "Jaringan", @@ -229,8 +231,7 @@ "pop.named": "Tersimpan; nama ini kini tampil di semua tempat", "pop.actions": "Tindakan", "topn.group": "Kelompokkan menurut", -"topn.hint": "Klik judul kolom biru untuk mengelompokkan menurut kolom itu, judul kolom angka untuk mengurutkan", -"topn.by_col": "Kelompokkan tabel menurut kolom ini", +"topn.hint": "Klik judul kolom mana pun untuk mengurutkan. Kolom angka memeringkat semua lalu lintas dalam rentang waktu, bukan hanya baris yang tampil.", "nav.detail": "Detail", "det.peers": "Host yang diajak berkomunikasi", "det.clients": "Host yang menggunakannya", diff --git a/internal/web/static/i18n/ja.json b/internal/web/static/i18n/ja.json index daa2277..810234e 100644 --- a/internal/web/static/i18n/ja.json +++ b/internal/web/static/i18n/ja.json @@ -94,6 +94,8 @@ "col.vs_prev": "前期間比", "col.peers": "相手数", "col.flows": "フロー数", +"col.pkts": "パケット数", +"col.avg": "平均パケット長", "col.country": "国", "col.asn": "AS", "col.org": "ネットワーク", @@ -229,8 +231,7 @@ "pop.named": "保存しました。この名前がすべての画面に表示されます", "pop.actions": "操作", "topn.group": "グループ化", -"topn.hint": "青い列見出しをクリックするとその列でグループ化、数値の列見出しをクリックすると並べ替えます", -"topn.by_col": "この列で表をグループ化", +"topn.hint": "列見出しをクリックすると並べ替えます。数値の列は表示中の行だけでなく、期間内の全トラフィックから順位を取り直します。", "nav.detail": "詳細", "det.peers": "通信相手のホスト", "det.clients": "利用しているホスト", diff --git a/internal/web/static/i18n/ko.json b/internal/web/static/i18n/ko.json index 479ec38..18e1308 100644 --- a/internal/web/static/i18n/ko.json +++ b/internal/web/static/i18n/ko.json @@ -94,6 +94,8 @@ "col.vs_prev": "이전 대비", "col.peers": "상대 수", "col.flows": "플로 수", +"col.pkts": "패킷", +"col.avg": "평균 패킷 크기", "col.country": "국가", "col.asn": "AS", "col.org": "네트워크", @@ -229,8 +231,7 @@ "pop.named": "저장했습니다. 이제 모든 화면에 이 이름이 표시됩니다", "pop.actions": "작업", "topn.group": "그룹 기준", -"topn.hint": "파란색 열 제목을 클릭하면 그 열로 그룹화하고, 숫자 열 제목을 클릭하면 정렬합니다", -"topn.by_col": "이 열로 표 그룹화", +"topn.hint": "열 제목을 클릭하면 정렬됩니다. 숫자 열은 표시된 행만이 아니라 기간 내 전체 트래픽에서 순위를 다시 매깁니다.", "nav.detail": "상세", "det.peers": "통신하는 호스트", "det.clients": "이를 사용하는 호스트", diff --git a/internal/web/static/i18n/pt.json b/internal/web/static/i18n/pt.json index 687ed04..ea9c52a 100644 --- a/internal/web/static/i18n/pt.json +++ b/internal/web/static/i18n/pt.json @@ -94,6 +94,8 @@ "col.vs_prev": "vs período anterior", "col.peers": "Interlocutores", "col.flows": "Fluxos", +"col.pkts": "Pacotes", +"col.avg": "Pacote médio", "col.country": "País", "col.asn": "AS", "col.org": "Rede", @@ -229,8 +231,7 @@ "pop.named": "Salvo; o nome agora aparece em todo lugar", "pop.actions": "Ações", "topn.group": "Agrupar por", -"topn.hint": "Clique em um título de coluna azul para agrupar por ela, ou em um título numérico para ordenar", -"topn.by_col": "Agrupar a tabela por esta coluna", +"topn.hint": "Clique em qualquer cabeçalho para ordenar. As colunas numéricas classificam todo o tráfego do período, não só as linhas exibidas.", "nav.detail": "Detalhes", "det.peers": "Hosts com quem conversa", "det.clients": "Hosts que o usam", diff --git a/internal/web/static/i18n/ru.json b/internal/web/static/i18n/ru.json index 70cbf7f..dd142a2 100644 --- a/internal/web/static/i18n/ru.json +++ b/internal/web/static/i18n/ru.json @@ -94,6 +94,8 @@ "col.vs_prev": "к прошлому периоду", "col.peers": "Собеседники", "col.flows": "Потоки", +"col.pkts": "Пакеты", +"col.avg": "Средний пакет", "col.country": "Страна", "col.asn": "AS", "col.org": "Сеть", @@ -229,8 +231,7 @@ "pop.named": "Сохранено; теперь это название показывается везде", "pop.actions": "Действия", "topn.group": "Группировать по", -"topn.hint": "Щёлкните синий заголовок столбца, чтобы сгруппировать по нему, или числовой — чтобы отсортировать", -"topn.by_col": "Сгруппировать таблицу по этому столбцу", +"topn.hint": "Нажмите на заголовок любого столбца для сортировки. Числовые столбцы ранжируют весь трафик за период, а не только показанные строки.", "nav.detail": "Подробности", "det.peers": "С какими узлами общается", "det.clients": "Узлы, которые его используют", diff --git a/internal/web/static/i18n/ur.json b/internal/web/static/i18n/ur.json index c7088d4..9f20913 100644 --- a/internal/web/static/i18n/ur.json +++ b/internal/web/static/i18n/ur.json @@ -94,6 +94,8 @@ "col.vs_prev": "پچھلی مدت سے", "col.peers": "ہم منصب", "col.flows": "فلو", +"col.pkts": "پیکٹ", +"col.avg": "اوسط پیکٹ", "col.country": "ملک", "col.asn": "AS", "col.org": "نیٹ ورک", @@ -229,8 +231,7 @@ "pop.named": "محفوظ ہو گیا؛ یہ نام اب ہر جگہ دکھائی دے گا", "pop.actions": "کارروائیاں", "topn.group": "گروپ بندی بلحاظ", -"topn.hint": "کسی نیلے کالم عنوان پر کلک کریں تو اس کے لحاظ سے گروپ ہو گا، عددی عنوان پر کلک کریں تو ترتیب بدلے گی", -"topn.by_col": "جدول کو اس کالم کے لحاظ سے گروپ کریں", +"topn.hint": "ترتیب دینے کے لیے کسی بھی کالم کے عنوان پر کلک کریں۔ عددی کالم وقت کی حد کے تمام ٹریفک کی درجہ بندی کرتے ہیں، صرف دکھائی گئی قطاروں کی نہیں۔", "nav.detail": "تفصیل", "det.peers": "جن ہوسٹس سے یہ بات کرتا ہے", "det.clients": "جو ہوسٹس اسے استعمال کرتے ہیں", diff --git a/internal/web/static/i18n/zh.json b/internal/web/static/i18n/zh.json index aff098c..f949916 100644 --- a/internal/web/static/i18n/zh.json +++ b/internal/web/static/i18n/zh.json @@ -94,6 +94,8 @@ "col.vs_prev": "比上一时段", "col.peers": "对端数", "col.flows": "流数", +"col.pkts": "包数", +"col.avg": "平均包长", "col.country": "国家", "col.asn": "AS", "col.org": "运营商", @@ -229,8 +231,7 @@ "pop.named": "已保存,各处都会显示这个名称", "pop.actions": "操作", "topn.group": "分组方式", -"topn.hint": "点击蓝色列标题按该列分组,点击数字列标题排序", -"topn.by_col": "按此列对表格分组", +"topn.hint": "点击任意列标题排序。数字列按时间范围内的全部流量重新取前几名,不只是重排当前显示的行。", "nav.detail": "详情", "det.peers": "与它通信的主机", "det.clients": "使用它的主机",