websocket update
test-org/trust-module-backend/pipeline/pr-main Build started... Details
test-org/trust-module-backend/pipeline/pr-rc Build queued... Details

pull/17/head
DmitriyA 2025-05-27 22:13:46 -04:00
parent 319e2cdd69
commit 4e1cd72f59
5 changed files with 108 additions and 17 deletions

View File

View File

@ -4,13 +4,29 @@ import { MenuItem } from './menu.interface';
@Controller('menu')
export class MenuController {
constructor(private readonly menuService: MenuService) {}
constructor(private readonly menuService: MenuService) { }
@Get()
async getMenu(): Promise<MenuItem> {
return this.menuService.getFullMenu();
}
@Post('save')
async saveMenu() {
await this.menuService.saveMenuToFile();
return { status: 'saved' };
}
@Get('full')
async getFullMenu(): Promise<MenuItem> {
return this.menuService.getFullMenu();
}
@Post('overrides')
async saveOverrides(@Body() data: { overrides: Partial<MenuItem>[] }) {
return this.menuService.saveOverrides(data.overrides);
}
@Put(':id')
async updateMenuItem(
@Param('id') id: string,

View File

@ -1,14 +1,53 @@
import { Injectable } from '@nestjs/common';
import { PrometheusService } from '../prometheus.service';
import { MenuItem } from './menu.interface';
import * as fs from 'fs/promises';
import * as path from 'path';
@Injectable()
export class MenuService {
constructor(private readonly prometheusService: PrometheusService) {}
constructor(private readonly prometheusService: PrometheusService) { }
private readonly menuOverridesPath = path.join(process.cwd(), 'data', 'menu.json');
async saveMenuToFile(): Promise<void> {
const menu = await this.getFullMenu();
await fs.mkdir(path.dirname(this.menuOverridesPath), { recursive: true });
await fs.writeFile(this.menuOverridesPath, JSON.stringify(menu, null, 2), 'utf-8');
}
async getFullMenu(): Promise<MenuItem> {
const dynamicItems = await this.generateDynamicItems();
return this.injectDynamicItems(this.getStaticStructure(), dynamicItems);
const baseMenu = this.injectDynamicItems(this.getStaticStructure(), dynamicItems);
const overrides = await this.loadOverrides();
return this.applyOverrides(baseMenu, overrides);
}
private applyOverrides(menu: MenuItem, overrides: Partial<MenuItem>[]): MenuItem {
const overrideMap = new Map(overrides.map(o => [o.id, o]));
const apply = (item: MenuItem): MenuItem => {
const override = overrideMap.get(item.id);
const updated = override ? { ...item, ...override } : item;
if (updated.items) {
updated.items = updated.items.map(apply);
}
return updated;
};
return apply(menu);
}
private async loadOverrides(): Promise<Partial<MenuItem>[]> {
try {
const content = await fs.readFile(this.menuOverridesPath, 'utf-8');
const parsed = JSON.parse(content);
return parsed.overrides || [];
} catch (e) {
return []; // если файл не существует
}
}
private getStaticStructure(): MenuItem {
@ -81,8 +120,17 @@ export class MenuService {
device.startsWith('lo') ||
device.startsWith('/run');
const devices = this.extractUniqueEntities(allSeries, 'device')
.filter(device => !isGarbageDevice(device));
const isGarbageInstance = (instance: string) =>
instance.includes('192.168.2.34:9049');
const filteredSeries = allSeries.filter(({ labels }) => {
const device = labels.device;
const instance = labels.instance;
return (!device || !isGarbageDevice(device)) &&
(!instance || !isGarbageInstance(instance));
});
const devices = this.extractUniqueEntities(filteredSeries, 'device');
return devices.map(device => ({
id: `device_${device}`,
@ -172,6 +220,10 @@ export class MenuService {
return item;
}
async saveOverrides(overrides: Partial<MenuItem>[]): Promise<void> {
await fs.writeFile(this.menuOverridesPath, JSON.stringify({ overrides }, null, 2), 'utf-8');
}
private findMenuItem(menu: MenuItem, id: string): MenuItem | null {
if (menu.id === id) return menu;

View File

@ -116,34 +116,54 @@ export class MetricsGateway implements OnGatewayInit, OnGatewayConnection, OnGat
}
}
@SubscribeMessage('subscribe-metric')
async handleSubscribeMetric(client: Socket, payload: { metric: string; interval?: number }) {
const { metric, interval = 5000 } = payload;
private getSubscriptionKey(metric: string, filters: Record<string, string>): string {
// Создаём уникальный ключ на основе метрики и фильтров
const filterKeys = Object.keys(filters).sort();
const filterString = filterKeys.map(k => `${k}=${filters[k]}`).join('&');
return `${metric}${filterString ? `?${filterString}` : ''}`;
}
if (!this.metricSubscriptions.has(metric)) {
@SubscribeMessage('subscribe-metric')
async handleSubscribeMetric(
client: Socket,
payload: {
metric: string;
interval?: number;
filters?: Record<string, string>;
}
) {
const { metric, interval = 5000, filters = {} } = payload;
const subscriptionKey = this.getSubscriptionKey(metric, filters);
if (!this.metricSubscriptions.has(subscriptionKey)) {
const stopUpdates = await this.sendPeriodicUpdates(
metric,
interval,
(data) => {
this.server.emit('metrics-data', { metric, data });
}
// Отправляем только подписчикам этой конкретной метрики с фильтрами
this.server.emit('metrics-data', {
metric: subscriptionKey,
data
});
},
filters
);
this.metricSubscriptions.set(metric, {
this.metricSubscriptions.set(subscriptionKey, {
stopUpdates,
clients: new Set([client.id])
});
} else {
this.metricSubscriptions.get(metric)?.clients.add(client.id);
this.metricSubscriptions.get(subscriptionKey)?.clients.add(client.id);
}
const unsubscribe = () => {
const subscription = this.metricSubscriptions.get(metric);
const subscription = this.metricSubscriptions.get(subscriptionKey);
if (subscription) {
subscription.clients.delete(client.id);
if (subscription.clients.size === 0) {
subscription.stopUpdates();
this.metricSubscriptions.delete(metric);
this.metricSubscriptions.delete(subscriptionKey);
}
}
};

View File

@ -16,6 +16,9 @@
"forceConsistentCasingInFileNames": true,
"noImplicitAny": false,
"strictBindCallApply": false,
"noFallthroughCasesInSwitch": false
"noFallthroughCasesInSwitch": false,
"types": [
"node"
]
}
}