diff --git a/src/menu/menu-overrides.json b/src/menu/menu-overrides.json new file mode 100644 index 0000000..e69de29 diff --git a/src/menu/menu.controller.ts b/src/menu/menu.controller.ts index c08b10b..1f8f017 100644 --- a/src/menu/menu.controller.ts +++ b/src/menu/menu.controller.ts @@ -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 { return this.menuService.getFullMenu(); } + @Post('save') + async saveMenu() { + await this.menuService.saveMenuToFile(); + return { status: 'saved' }; + } + + @Get('full') + async getFullMenu(): Promise { + return this.menuService.getFullMenu(); + } + + @Post('overrides') + async saveOverrides(@Body() data: { overrides: Partial[] }) { + return this.menuService.saveOverrides(data.overrides); + } + @Put(':id') async updateMenuItem( @Param('id') id: string, diff --git a/src/menu/menu.service.ts b/src/menu/menu.service.ts index 62f1047..502b295 100644 --- a/src/menu/menu.service.ts +++ b/src/menu/menu.service.ts @@ -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 { + 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 { 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 { + 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[]> { + 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[]): Promise { + 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; diff --git a/src/metrics.gateway.ts b/src/metrics.gateway.ts index dc78ab7..684d5f3 100644 --- a/src/metrics.gateway.ts +++ b/src/metrics.gateway.ts @@ -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 { + // Создаём уникальный ключ на основе метрики и фильтров + 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; + } + ) { + 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); } } }; diff --git a/tsconfig.json b/tsconfig.json index 4731bbf..9d2e02e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,6 +16,9 @@ "forceConsistentCasingInFileNames": true, "noImplicitAny": false, "strictBindCallApply": false, - "noFallthroughCasesInSwitch": false + "noFallthroughCasesInSwitch": false, + "types": [ + "node" + ] } -} +} \ No newline at end of file