- - -
- - - Rocket Ship - - - - - - - - - - {{ title }} app is running! - - - Rocket Ship Smoke - - - -
- - -

Resources

-

Here are some links to help you get started:

- -
- - - Learn Angular - - - - - CLI Documentation - - - - - - Angular Material - - - - - - Angular Blog - - - - - - Angular DevTools - - - + + + MeshCenter + +

Имя ноды чела который авторизовался (мб еще snnr rxxi шуе)

+
+ + +
+

{{u.name}}

+
+
+ +
+
- -

Next Steps

-

What do you want to do next with your app?

- - - -
- - - - - - - - - - - -
- - -
-
ng generate component xyz
-
ng add @angular/material
-
ng add @angular/pwa
-
ng add _____
-
ng test
-
ng build
-
- - - - - - - - - Gray Clouds Background - - - -
- - - - - - - - - - + diff --git a/ui/src/app/app.component.scss b/ui/src/app/app.component.scss index e69de29..cbdde49 100644 --- a/ui/src/app/app.component.scss +++ b/ui/src/app/app.component.scss @@ -0,0 +1,19 @@ +.container { + width: 100%; + height: 100% - 2.5%; +} + +.sidenav-content { + display: flex; + //height: 100%; + align-items: center; + justify-content: center; +} + +.sidenav { + padding: 20px; +} + +.spacer { + flex: 1 1 auto; +} diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts index e223dac..0741c53 100644 --- a/ui/src/app/app.component.ts +++ b/ui/src/app/app.component.ts @@ -6,5 +6,10 @@ import { Component } from '@angular/core'; styleUrls: ['./app.component.scss'] }) export class AppComponent { - title = 'ui'; + routes: {name: string, url: string}[] = [ + {name: "Команды бота", url:""}, + {name: "История сообщений", url:""}, + {name: "Статистика по нодам", url:""}, + {name: "Связь", url:""}, + ] } diff --git a/ui/src/app/app.module.ts b/ui/src/app/app.module.ts index 5c20967..81f9a69 100644 --- a/ui/src/app/app.module.ts +++ b/ui/src/app/app.module.ts @@ -4,6 +4,11 @@ import { BrowserModule } from '@angular/platform-browser'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import {MatToolbarModule} from "@angular/material/toolbar"; +import {MatIconModule} from "@angular/material/icon"; +import {MatSidenavModule} from "@angular/material/sidenav"; +import {MatButtonModule} from "@angular/material/button"; +import {HttpClientModule} from "@angular/common/http"; @NgModule({ declarations: [ @@ -12,7 +17,12 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; imports: [ BrowserModule, AppRoutingModule, - BrowserAnimationsModule + BrowserAnimationsModule, + MatToolbarModule, + MatIconModule, + MatSidenavModule, + MatButtonModule, + HttpClientModule ], providers: [], bootstrap: [AppComponent] diff --git a/webExtensions/extra/MessageDTO.py b/webExtensions/extra/MessageDTO.py new file mode 100644 index 0000000..f3e8b74 --- /dev/null +++ b/webExtensions/extra/MessageDTO.py @@ -0,0 +1,6 @@ +__slots__ = ["from", "to", "rx_snr", "hop_limit", "rx_rssi", "hop_start", "ts", "decoded_payload", "want_ack"] + +class MessageDTO: + def __init__(self, data): + for slot in __slots__: + setattr(self, slot, data.get(slot, None)) \ No newline at end of file diff --git a/webExtensions/extra/NodeDTO.py b/webExtensions/extra/NodeDTO.py new file mode 100644 index 0000000..8e490ce --- /dev/null +++ b/webExtensions/extra/NodeDTO.py @@ -0,0 +1,6 @@ +__slots__ = ["num", "snr", "hops_away", "ts", "long_name", "short_name"] + +class NodeDTO: + def __init__(self, data): + for slot in __slots__: + setattr(self, slot, data.get(slot, None)) \ No newline at end of file diff --git a/webExtensions/messageList.py b/webExtensions/messageList.py new file mode 100644 index 0000000..99290f5 --- /dev/null +++ b/webExtensions/messageList.py @@ -0,0 +1,24 @@ +from fastapi import FastAPI +from fastapi.responses import HTMLResponse +from fastapi import Query + +from pymongo.asynchronous.database import AsyncDatabase +from extra.MessageDTO import MessageDTO +from typing import List, Annotated +from pymongo import DESCENDING + +class WebExtension: + MESSAGE_PORTNUM = 1 + app: FastAPI + dbStore: AsyncDatabase + def __init__(self, core): + self.core = core + self.app = core.app + self.dbStore = core.dbStore + + @self.app.get(f"{self.core.context}/messages") + async def listOfMessages(limit: int = Query(10), offset: int = Query(0)): + collection = self.dbStore['packet'] + c = collection.find({"to": int(self.core.PUB_CH), "portnum":self.MESSAGE_PORTNUM}).sort("ts", DESCENDING).skip(offset).limit(limit) + l = await c.to_list() + return [MessageDTO(msg) for msg in l] \ No newline at end of file diff --git a/webExtensions/nodeList.py b/webExtensions/nodeList.py new file mode 100644 index 0000000..12211d5 --- /dev/null +++ b/webExtensions/nodeList.py @@ -0,0 +1,89 @@ +from fastapi import FastAPI +from fastapi.responses import HTMLResponse +from fastapi import Query + +from pymongo.asynchronous.database import AsyncDatabase +from extra.NodeDTO import NodeDTO +from typing import List, Annotated + +class WebExtension: + app: FastAPI + dbStore: AsyncDatabase + def __init__(self, core): + self.core = core + self.app = core.app + self.dbStore = core.dbStore + + @self.app.get(f"{self.core.context}/nodes/list") + async def listOfNodes(): + pipeline = [ #ai slooop + # Сортировка для каждого num по убыванию ts + {"$sort": {"num": 1, "ts": -1}}, + # Группировка по num и взятие первого (последнего) документа + {"$group": { + "_id": "$num", + "latest_doc": {"$first": "$$ROOT"} + }}, + # Замена корневого документа + {"$replaceRoot": {"newRoot": "$latest_doc"}}, + # Сортировка результата (опционально) + {"$sort": {"num": 1}} + ] + + collection = self.dbStore['node_info'] + c = await collection.aggregate(pipeline) + l = await c.to_list() + return [NodeDTO(node) for node in l] + + @self.app.get(f"{self.core.context}/nodes/direct") + async def listOfDirectNodes(): + pipeline = [ #ai slooop + {"$match": {"hops_away": 0}}, # Фильтруем по списку + # Сортировка для каждого num по убыванию ts + {"$sort": {"num": 1, "ts": -1}}, + # Группировка по num и взятие первого (последнего) документа + {"$group": { + "_id": "$num", + "latest_doc": {"$first": "$$ROOT"} + }}, + # Замена корневого документа + {"$replaceRoot": {"newRoot": "$latest_doc"}}, + # Сортировка результата (опционально) + {"$sort": {"num": 1}} + ] + + collection = self.dbStore['node_info'] + c = await collection.aggregate(pipeline) + l = await c.to_list() + return [NodeDTO(node) for node in l] + + @self.app.get(self.core.context + "/nodes/{num}") + async def oneNode(num: int): + collection = self.dbStore['node_info'] + c = await collection.find_one( + {"num":num}, + sort=[("ts", -1)] + ) + if c: + return NodeDTO(c) + else: + raise HTMLResponse(status_code=404) + + @self.app.get(self.core.context + "/nodes") + async def listOfSelectedNodes(nums: List[int] = Query(None)): + pipeline = [ + {"$match": {"num": {"$in": nums}}}, # Фильтруем по списку + {"$sort": {"num": 1, "ts": -1}}, # Сортируем по num и ts (по убыванию) + { + "$group": { + "_id": "$num", # Группируем по num + "latest_doc": {"$first": "$$ROOT"} # Берем первую (последнюю по ts) + } + }, + {"$replaceRoot": {"newRoot": "$latest_doc"}}, # Восстанавливаем структуру + {"$sort": {"num": 1}} # Сортируем результат по num + ] + collection = self.dbStore['node_info'] + c = await collection.aggregate(pipeline) + l = await c.to_list() + return [NodeDTO(node) for node in l] \ No newline at end of file