Browse Source

packet stats

main
gsd 4 months ago
parent
commit
32de8f9d73
  1. 3
      dbService.py
  2. 19
      ui/package-lock.json
  3. 3
      ui/package.json
  4. 6
      ui/src/app/app-routing.module.ts
  5. 3
      ui/src/app/app.component.ts
  6. 11
      ui/src/app/app.module.ts
  7. 19
      ui/src/app/components/messages/MessageHistory.component.ts
  8. 75
      ui/src/app/components/nodes/DirectNodes.component.ts
  9. 127
      ui/src/app/components/nodes/nodes-list.component.ts
  10. 5
      ui/src/app/components/nodes/nodes.styles.scss
  11. 164
      ui/src/app/components/packet/NetworkStatus.component.ts
  12. 17
      ui/src/app/entities/PacketGroup.ts
  13. 62
      ui/src/app/utils/PortNums.ts
  14. 20
      ui/src/app/utils/Utils.ts
  15. 24
      ui/src/styles.scss

3
dbService.py

@ -137,15 +137,12 @@ class PacketDbService:
if packetsSumNode:
groupPipe["$group"]["_id"] = "$from"
print(packetsPerNode, packetsSumNode)
pipeline.append(groupPipe)
print(pipeline)
###print(pipeline)
collection = self.dbStore['packet']
c = await collection.aggregate(pipeline)
l = await c.to_list()
print(l[0])
return [PacketGroup(p, packetsPerNode, packetsSumNode) for p in l]
class DbService(NodeDbService, PacketDbService):

19
ui/package-lock.json

@ -18,6 +18,7 @@
"@angular/platform-browser": "^14.2.0",
"@angular/platform-browser-dynamic": "^14.2.0",
"@angular/router": "^14.2.0",
"chart.js": "^4.5.1",
"rxjs": "~7.5.0",
"tslib": "^2.3.0",
"zone.js": "~0.11.4"
@ -2971,6 +2972,12 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@kurkle/color": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
"license": "MIT"
},
"node_modules/@leichtgewicht/ip-codec": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
@ -4501,6 +4508,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/chart.js": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
"license": "MIT",
"dependencies": {
"@kurkle/color": "^0.3.0"
},
"engines": {
"pnpm": ">=8"
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",

3
ui/package.json

@ -20,6 +20,7 @@
"@angular/platform-browser": "^14.2.0",
"@angular/platform-browser-dynamic": "^14.2.0",
"@angular/router": "^14.2.0",
"chart.js": "^4.5.1",
"rxjs": "~7.5.0",
"tslib": "^2.3.0",
"zone.js": "~0.11.4"
@ -37,4 +38,4 @@
"karma-jasmine-html-reporter": "~2.0.0",
"typescript": "~4.7.2"
}
}
}

6
ui/src/app/app-routing.module.ts

@ -1,12 +1,14 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import {DirectNodesComponent} from "./components/nodes/DirectNodes.component";
import {NodesListComponent} from "./components/nodes/nodes-list.component";
import {BotCommandsComponent} from "./components/botCommands/BotCommands.component";
import {MessageHistoryComponent} from "./components/messages/MessageHistory.component";
import {NetworkStatusComponent} from "./components/packet/NetworkStatus.component";
const routes: Routes = [
{path: "nodes/direct", component: DirectNodesComponent},
{path: "nodes/:type", component: NodesListComponent},
{path: "messages", component: MessageHistoryComponent},
{path: "network/status", component: NetworkStatusComponent},
{path: "", component: BotCommandsComponent}
];

3
ui/src/app/app.component.ts

@ -22,9 +22,10 @@ export class AppComponent implements OnInit {
userNode!: NodeDTO
routes: {name: string, url: string}[] = [
//{name: "Команды бота", url:""},
{name: "Статистика сети", url:"network/status"},
{name: "История сообщений", url:"messages"},
{name: "Прямые ноды", url:"nodes/direct"},
{name: "Все ноды", url:"nodes/list"},
//{name: "Все ноды", url:""},
]

11
ui/src/app/app.module.ts

@ -11,7 +11,7 @@ import {MatButtonModule} from "@angular/material/button";
import {HttpClientModule, HTTP_INTERCEPTORS} from "@angular/common/http";
import {BotCommandsComponent} from "./components/botCommands/BotCommands.component";
import {MessageHistoryComponent} from "./components/messages/MessageHistory.component";
import {DirectNodesComponent, NodeDtoSortPipe} from "./components/nodes/DirectNodes.component";
import {NodesListComponent, NodeDtoSearchPipe, NodeDtoSortPipe} from "./components/nodes/nodes-list.component";
import {AllNodesComponent} from "./components/nodes/AllNodes.component";
import {AuthDialog, AuthInterceptor} from "./auth/AuthInterceptor";
import {MatDialogModule} from "@angular/material/dialog";
@ -23,15 +23,17 @@ import {MatSnackBarModule} from "@angular/material/snack-bar";
import {MatDividerModule} from "@angular/material/divider";
import {MatCardModule} from "@angular/material/card";
import {MatSelectModule} from "@angular/material/select";
import {NetworkStatusComponent} from "./components/packet/NetworkStatus.component";
@NgModule({
declarations: [
AppComponent,
BotCommandsComponent,
MessageHistoryComponent,
DirectNodesComponent,
NodesListComponent,
AllNodesComponent,
AuthDialog
AuthDialog,
NetworkStatusComponent
],
imports: [
BrowserModule,
@ -52,7 +54,8 @@ import {MatSelectModule} from "@angular/material/select";
MatDividerModule,
MatCardModule,
NodeDtoSortPipe,
MatSelectModule
MatSelectModule,
NodeDtoSearchPipe
],
providers: [{
provide: HTTP_INTERCEPTORS,

19
ui/src/app/components/messages/MessageHistory.component.ts

@ -4,6 +4,7 @@ import {MessageDTO} from "../../entities/MessageDTO";
import {NodeDTO} from "../../entities/NodeDTO";
import {KeyValueMap} from "../../entities/KeyValueMap";
import {Subscription} from "rxjs";
import {numToColor} from "../../utils/Utils";
@Component({
selector: "app-message-history",
@ -21,6 +22,7 @@ import {Subscription} from "rxjs";
})
export class MessageHistoryComponent implements OnInit, OnDestroy {
constructor(private http: HttpClient) {}
numToColor = numToColor;
loading: boolean = false;
canLoadMoreMessage: boolean = true;
@ -101,21 +103,4 @@ export class MessageHistoryComponent implements OnInit, OnDestroy {
)
})
}
numToColor(num:number) {
// Приводим к беззнаковому 32-битному и перемешиваем биты
let n = num >>> 0;
// Мультипликативная хеш-функция с простыми числами
n = (n * 2654435761) >>> 0; // константа из Knuth
n ^= (n >> 16);
n = (n * 0x85EBCA6B) >>> 0;
n ^= (n >> 13);
n = (n * 0xC2B2AE35) >>> 0;
n ^= (n >> 16);
const r = (n >> 16) & 0xFF;
const g = (n >> 8) & 0xFF;
const b = n & 0xFF;
return `rgba(${r}, ${g}, ${b}, 0.2)`
}
}

75
ui/src/app/components/nodes/DirectNodes.component.ts

@ -1,75 +0,0 @@
import {Component, OnInit} from "@angular/core";
import {HttpClient} from "@angular/common/http";
import {NodeDTO} from "../../entities/NodeDTO";
//todo abs this
@Component({
selector: 'app-direct-nodes',
styleUrls: ['nodes.styles.scss'],
template: `
<!--<p *ngFor="let node of nodes">{{node.long_name}}</p>-->
<div style="width: 80%; padding: 0 10%; padding-top: 8px">
<div style="display: flex; justify-content: start">
<mat-form-field appearance="fill" style="width: 250px">
<mat-label>Сортировать по ...</mat-label>
<mat-select [(ngModel)]="sort" >
<mat-option *ngFor="let s of sortVars" [value]="s">
{{s.name}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="card-wrapper">
<mat-card *ngFor="let node of nodes | NodeDtoSort: sort.type">
<mat-card-header>
<mat-card-title>{{node.long_name}}</mat-card-title>
<mat-card-subtitle>{{node.short_name}} ({{node.num}})</mat-card-subtitle>
</mat-card-header>
<!--<mat-card-content>
</mat-card-content>-->
<mat-card-actions>
<button mat-button *ngIf="node.hops_away > 0">Прыжков: {{node.hops_away}}</button>
<button mat-button>SNR: {{node.snr}}</button>
<button mat-button>{{node.ts * 1000 | date:"HH:mm dd.MM.yyyy"}}</button>
</mat-card-actions>
</mat-card>
</div>
</div>
`
})
export class DirectNodesComponent implements OnInit {
constructor(private http: HttpClient) {
}
nodes: NodeDTO[] = [];
sortVars:{name: string, type: string}[] = [
{name: "Последнему пингу", type: "ts"},
{name: "SNR", type: "snr"},
//{name: "Имени", type: "long_name"}
]
sort: {name: string, type: string} = this.sortVars[0]
ngOnInit(): void {
this.http.get(`api/nodes/direct`).subscribe(
(res) => this.nodes = res as NodeDTO[]
)
}
}
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'NodeDtoSort',
standalone: true
})
export class NodeDtoSortPipe implements PipeTransform {
transform(values: NodeDTO[], field: string = ""): NodeDTO[] {
if (field == "") return values;
return values.sort((n1,n2) =>
{
// @ts-ignore
return n2[field] - n1[field]
});
}
}

127
ui/src/app/components/nodes/nodes-list.component.ts

@ -0,0 +1,127 @@
import {Component, OnInit} from "@angular/core";
import {HttpClient} from "@angular/common/http";
import {NodeDTO} from "../../entities/NodeDTO";
import {numToColor} from "../../utils/Utils";
//todo abs this
@Component({
selector: 'app-direct-nodes',
styleUrls: ['nodes.styles.scss'],
template: `
<!--<p *ngFor="let node of nodes">{{node.long_name}}</p>-->
<div style="width: 80%; padding: 0 10%; padding-top: 8px">
<div style="display: flex; justify-content: start">
<mat-form-field appearance="fill" style="width: 250px">
<mat-label>Сортировать по ...</mat-label>
<mat-select [(ngModel)]="sort" >
<mat-option *ngFor="let s of sortVars" [value]="s">
{{s.name}}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="fill" style="width: 250px; padding-left: 8px">
<mat-label>Искать по ...</mat-label>
<mat-select [(ngModel)]="search" >
<mat-option *ngFor="let s of searchVars" [value]="s">
{{s.name}}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field style="width: 250px; padding-left: 8px; padding-top: 14px">
<mat-label>Искать по ...</mat-label>
<input matInput [(ngModel)]="searchContent">
</mat-form-field>
</div>
<div class="card-wrapper-350">
<mat-card
*ngFor="let node of nodes | NodeDtoSort: sort.type | NodeDtoSearch: search.type: searchContent"
[style]="{'background-color':numToColor(node.num)}">
<mat-card-header>
<mat-card-title>{{node.long_name}}</mat-card-title>
<mat-card-subtitle>{{node.short_name}} ({{node.num}})</mat-card-subtitle>
</mat-card-header>
<!--<mat-card-content>
</mat-card-content>-->
<mat-card-actions>
<button mat-button *ngIf="node.hops_away > 0">Прыжков: {{node.hops_away}}</button>
<button mat-button>SNR: {{node.snr}}</button>
<button mat-button>{{node.ts * 1000 | date:"HH:mm dd.MM.yyyy"}}</button>
</mat-card-actions>
</mat-card>
</div>
</div>
`
})
export class NodesListComponent implements OnInit {
numToColor = numToColor
constructor(private http: HttpClient,
private route: ActivatedRoute) {
}
nodes: NodeDTO[] = [];
sortVars:{name: string, type: string}[] = [
{name: "Последнему пингу", type: "ts"},
{name: "SNR", type: "snr"},
//{name: "Имени", type: "long_name"}
]
sort: {name: string, type: string} = this.sortVars[0]
searchVars: {name: string, type: string}[] = [
{name: "Длинному имени", type: "long_name"},
{name: "Короткому имени", type: "short_name"},
{name: "Хопов больше чем", type: "hops_away"}
]
search: {name: string, type: string} = this.searchVars[0]
searchContent: string = "";
ngOnInit(): void {
this.route.params.subscribe(
(params) => {
this.http.get(`api/nodes/${params['type']}`).subscribe(
(res) => this.nodes = res as NodeDTO[]
)
}
)
}
}
import { Pipe, PipeTransform } from '@angular/core';
import {ActivatedRoute} from "@angular/router";
@Pipe({
name: 'NodeDtoSort',
standalone: true
})
export class NodeDtoSortPipe implements PipeTransform {
transform(values: NodeDTO[], field: string = ""): NodeDTO[] {
if (field == "") return values;
return values.sort((n1,n2) =>
{
// @ts-ignore
return n2[field] - n1[field]
});
}
}
@Pipe({
name: 'NodeDtoSearch',
standalone: true
})
export class NodeDtoSearchPipe implements PipeTransform {
transform(values: NodeDTO[], field: string = "", search: any = ""): NodeDTO[] {
if (field == "" || search == "") return values;
return values.filter((node) => {
switch (field) {
case "long_name":
return node.long_name && node.long_name.toLowerCase().indexOf(search.toLowerCase()) != -1;
case "short_name":
return node.short_name && node.short_name.toLowerCase().indexOf(search.toLowerCase()) != -1;
case "hops_away":
return node.hops_away != null && node.hops_away > Number.parseInt(search);
default: return true;
}
})
}
}

5
ui/src/app/components/nodes/nodes.styles.scss

@ -1,5 +0,0 @@
.card-wrapper {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 24px;
}

164
ui/src/app/components/packet/NetworkStatus.component.ts

@ -0,0 +1,164 @@
import {Component, OnInit} from "@angular/core";
import {HttpClient} from "@angular/common/http";
import {PacketGroup} from "../../entities/PacketGroup";
import {Chart} from "chart.js/auto";
import {numToColor} from "../../utils/Utils";
import {KeyValueMap} from "../../entities/KeyValueMap";
import {NodeDTO} from "../../entities/NodeDTO";
@Component({
selector: "app-network-status",
template: `
<div style="width: 80%; padding: 0 10%;">
<div *ngFor="let type of graphs" class="card-wrapper-450" style="padding-top: 12px">
<mat-card *ngFor="let p1 of type.cards">
<mat-card-content>
<canvas [id]="p1.canvasId">{{p1.chart}}</canvas>
</mat-card-content>
</mat-card>
</div>
</div>
`
})
export class NetworkStatusComponent implements OnInit {
DAY = 86400;
WEEK = this.DAY * 7;
MONTH = this.DAY * 30;
nodesNames: KeyValueMap<NodeDTO> = {}
//общая статичтика по perPortNum за день\неделю\месяц / ?=
//top кто насрал пакетами всего за день\неделю\месяц / ?=packetsSumNode=true
graphs:any[] = [
{
header: "Cтатистика пакетов в сети",
type: 'perPortNum',
cards:[{
chart: Chart,
canvasId: "DayPerPB",
params: "?=",
before: new Date().getTime()/1000,
after: (new Date().getTime()/1000) - this.DAY,
config: this.generateConfigTChart("Cтатистика по пакетам в сети за день")
},{
chart: Chart,
canvasId: "WeekPerPB",
params: "?=",
before: new Date().getTime()/1000,
after: (new Date().getTime()/1000) - this.WEEK,
config: this.generateConfigTChart("Cтатистика по пакетам в сети за неделю")
},{
chart: Chart,
canvasId: "MonthPerPB",
params: "?=",
before: new Date().getTime()/1000,
after: (new Date().getTime()/1000) - this.MONTH,
config: this.generateConfigTChart("Cтатистика по пакетам в сети за месяц")
}]
},
{
header: "Общая статистика пакетов в сети",
type: 'perSumNode',
cards:[{
chart: Chart,
canvasId: "DaySumPB",
params: "?packetsSumNode=true",
before: new Date().getTime()/1000,
after: (new Date().getTime()/1000) - this.DAY,
config: this.generateConfigTChart("Количество пакетов от пользоватей за день", false)
},{
chart: Chart,
canvasId: "WeekSumPB",
params: "?packetsSumNode=true",
before: new Date().getTime()/1000,
after: (new Date().getTime()/1000) - this.WEEK,
config: this.generateConfigTChart("Количество пакетов от пользоватей за неделю", false)
},{
chart: Chart,
canvasId: "MonthSumPB",
params: "?packetsSumNode=true",
before: new Date().getTime()/1000,
after: (new Date().getTime()/1000) - this.MONTH,
config: this.generateConfigTChart("Количество пакетов от пользоватей за месяц", false)
}]
}
];
constructor(private http: HttpClient) {}
generateConfigTChart(name: string, legend: boolean = true) {
return {
type: 'pie',
data: {},
options: {
responsive: true,
plugins: {
legend: {
position: 'bottom',
display: legend
},
title: {
display: true,
text: name
}
}
}
}
}
ngOnInit(): void {
this.http.get(`api/nodes/list`).subscribe(
(obj) => {
(obj as NodeDTO[]).forEach(
(node) => {
this.nodesNames[`${node.num}`] = node;
}
)
}
).add(
() => {
this.graphs.forEach(
(graph) => {
graph.cards.forEach((settings:any) => {
this.http.get(`api/packet/stats${settings.params}&before=${settings.before}&after=${settings.after}`)
.subscribe((data) => {
settings.config.data = {
labels: [],
datasets: [
{
label: settings.canvasId,
data: [],
backgroundColor: []
}
]
};
(data as Object[]).map((obj) => PacketGroup.fromDto(obj)).sort((p1, p2) => p2.count - p1.count).forEach(
(d) => {
switch (graph.type) {
case "perPortNum": {
settings.config.data.labels.push(`${d.portnumName} - ${d.count}`)
settings.config.data.datasets[0].data.push(d.count)
settings.config.data.datasets[0].backgroundColor.push(numToColor(d.portnum, 0))
break;
}
case "perSumNode": {
const name = `${d.from}` in this.nodesNames ? this.nodesNames[`${d.from}`].long_name : `${d.from}`
settings.config.data.labels.push(`${name} - ${d.count}`)
settings.config.data.datasets[0].data.push(d.count)
settings.config.data.datasets[0].backgroundColor.push(numToColor(d.from, 0))
break;
}
}
}
)
//if (settings.chart)
// settings.chart.destroy()
settings.chart = new Chart(settings.canvasId, settings.config)
})
})
}
)
}
)
}
}

17
ui/src/app/entities/PacketGroup.ts

@ -0,0 +1,17 @@
import {PortNums} from "../utils/PortNums";
export class PacketGroup {
count!: number;
portnum!: number;
portnumName!:string;
from!: number;
type!: string;
static fromDto(data:any):PacketGroup {
const p = new PacketGroup();
Object.assign(p, data);
// @ts-ignore
p.portnumName = p.portnum in PortNums ? PortNums[p.portnum] : "UNKNOWN OR MISSED"
return p;
}
}

62
ui/src/app/utils/PortNums.ts

@ -1,32 +1,32 @@
export class PortNums {
0: "UNKNOWN_APP";
1: "TEXT_MESSAGE_APP";
2: "REMOTE_HARDWARE_APP";
3: "POSITION_APP";
4: "NODEINFO_APP";
5: "ROUTING_APP";
6: "ADMIN_APP";
7: "TEXT_MESSAGE_COMPRESSED_APP";
8: "WAYPOINT_APP";
9: "AUDIO_APP";
10: "DETECTION_SENSOR_APP";
11: "ALERT_APP";
32: "REPLY_APP";
33: "IP_TUNNEL_APP";
34: "PAXCOUNTER_APP";
64: "SERIAL_APP";
65: "STORE_FORWARD_APP";
66: "RANGE_TEST_APP";
67: "TELEMETRY_APP";
68: "ZPS_APP";
69: "SIMULATOR_APP";
70: "TRACEROUTE_APP";
71: "NEIGHBORINFO_APP";
72: "ATAK_PLUGIN";
73: "MAP_REPORT_APP";
74: "POWERSTRESS_APP";
76: "RETICULUM_TUNNEL_APP";
256: "PRIVATE_APP";
257: "ATAK_FORWARDER";
511: "MAX";
export let PortNums = {
0: "UNKNOWN_APP",
1: "TEXT_MESSAGE_APP",
2: "REMOTE_HARDWARE_APP",
3: "POSITION_APP",
4: "NODEINFO_APP",
5: "ROUTING_APP",
6: "ADMIN_APP",
7: "TEXT_MESSAGE_COMPRESSED_APP",
8: "WAYPOINT_APP",
9: "AUDIO_APP",
10: "DETECTION_SENSOR_APP",
11: "ALERT_APP",
32: "REPLY_APP",
33: "IP_TUNNEL_APP",
34: "PAXCOUNTER_APP",
64: "SERIAL_APP",
65: "STORE_FORWARD_APP",
66: "RANGE_TEST_APP",
67: "TELEMETRY_APP",
68: "ZPS_APP",
69: "SIMULATOR_APP",
70: "TRACEROUTE_APP",
71: "NEIGHBORINFO_APP",
72: "ATAK_PLUGIN",
73: "MAP_REPORT_APP",
74: "POWERSTRESS_APP",
76: "RETICULUM_TUNNEL_APP",
256: "PRIVATE_APP",
257: "ATAK_FORWARDER",
511: "MAX",
}

20
ui/src/app/utils/Utils.ts

@ -0,0 +1,20 @@
export function numToColor(num:number, alpha = 0.2) {
// Приводим к беззнаковому 32-битному и перемешиваем биты
let n = num >>> 0;
// Мультипликативная хеш-функция с простыми числами
n = (n * 2654435761) >>> 0; // константа из Knuth
n ^= (n >> 16);
n = (n * 0x85EBCA6B) >>> 0;
n ^= (n >> 13);
n = (n * 0xC2B2AE35) >>> 0;
n ^= (n >> 16);
const r = (n >> 16) & 0xFF;
const g = (n >> 8) & 0xFF;
const b = n & 0xFF;
if (alpha == 0)
return `rgb(${r}, ${g}, ${b})`
else
return `rgba(${r}, ${g}, ${b}, ${alpha})`
}

24
ui/src/styles.scss

@ -2,3 +2,27 @@
html, body { height: 100%; }
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
.card-wrapper {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 24px;
}
.card-wrapper-250 {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 24px;
}
.card-wrapper-350 {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 24px;
}
.card-wrapper-450 {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(450px, 1fr));
gap: 24px;
}

Loading…
Cancel
Save