From dadb6f3bcb16ef94cb9515ea27b0c2c62ce46dfb Mon Sep 17 00:00:00 2001 From: DmitriyA Date: Tue, 10 Jun 2025 09:28:20 -0400 Subject: [PATCH 1/3] sidebar menu improvement --- src/menu/menu.controller.ts | 48 +++++++++++++++++++++++++++---------- src/menu/menu.service.ts | 35 +++++++++++++++++++++++---- 2 files changed, 66 insertions(+), 17 deletions(-) diff --git a/src/menu/menu.controller.ts b/src/menu/menu.controller.ts index 1f8f017..40093c4 100644 --- a/src/menu/menu.controller.ts +++ b/src/menu/menu.controller.ts @@ -1,14 +1,41 @@ -import { Controller, Get, Param, Post, Body, Put } from '@nestjs/common'; +import { Controller, Get, Post, Put, Body, Param, Headers, HttpException, HttpStatus } from '@nestjs/common'; import { MenuService } from './menu.service'; import { MenuItem } from './menu.interface'; +import { Response } from 'express'; @Controller('menu') export class MenuController { constructor(private readonly menuService: MenuService) { } - @Get() - async getMenu(): Promise { - return this.menuService.getFullMenu(); + @Get('full') + async getFullMenu(@Headers('if-modified-since') ifModifiedSince?: string) { + try { + const result = await this.menuService.getFullMenuWithCache(ifModifiedSince); + + if (!result.fresh && ifModifiedSince) { + throw new HttpException('Not Modified', HttpStatus.NOT_MODIFIED); + } + + return result.menu; + } catch (error) { + if (error.status === HttpStatus.NOT_MODIFIED) { + throw error; + } + throw new HttpException( + error.message || 'Failed to load menu', + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + + @Get('check-updates') + async checkUpdates(@Headers('if-modified-since') ifModifiedSince: string) { + if (!ifModifiedSince) { + throw new HttpException('If-Modified-Since header is required', HttpStatus.BAD_REQUEST); + } + + const hasUpdates = await this.menuService.checkForUpdates(ifModifiedSince); + return { hasUpdates }; } @Post('save') @@ -17,21 +44,18 @@ export class MenuController { 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); + await this.menuService.saveOverrides(data.overrides); + return { status: 'success' }; } @Put(':id') async updateMenuItem( @Param('id') id: string, @Body() update: Partial - ): Promise { - return this.menuService.updateMenuItem(id, update); + ) { + const updatedItem = await this.menuService.updateMenuItem(id, update); + return updatedItem; } } \ No newline at end of file diff --git a/src/menu/menu.service.ts b/src/menu/menu.service.ts index b830d8d..2e0dc20 100644 --- a/src/menu/menu.service.ts +++ b/src/menu/menu.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; import { PrometheusService } from '../prometheus.service'; import { MenuItem } from './menu.interface'; import * as fs from 'fs/promises'; @@ -7,6 +7,10 @@ import { RangeService } from './range.service'; @Injectable() export class MenuService { + private menuCache: MenuItem | null = null; + private lastModified: Date | null = null; + private cacheInitialized = false; + constructor( private readonly prometheusService: PrometheusService, private readonly rangeService: RangeService @@ -15,16 +19,33 @@ export class MenuService { private readonly menuOverridesPath = path.join(process.cwd(), 'data', 'menu.json'); async saveMenuToFile(): Promise { - const menu = await this.getFullMenu(); + const { menu } = await this.getFullMenuWithCache(); await fs.mkdir(path.dirname(this.menuOverridesPath), { recursive: true }); await fs.writeFile(this.menuOverridesPath, JSON.stringify(menu, null, 2), 'utf-8'); } - async getFullMenu(): Promise { + async getFullMenuWithCache(ifModifiedSince?: string): Promise<{ menu: MenuItem; fresh: boolean }> { + if (this.menuCache && this.lastModified && (!ifModifiedSince || new Date(ifModifiedSince) >= this.lastModified)) { + return { menu: this.menuCache, fresh: false }; + } + const dynamicItemsPromise = this.generateDynamicItems(); const baseMenu = await this.injectDynamicItems(this.getStaticStructure(), dynamicItemsPromise); const overrides = await this.loadOverrides(); - return this.applyOverrides(baseMenu, overrides); + const freshMenu = this.applyOverrides(baseMenu, overrides); + + this.menuCache = freshMenu; + this.lastModified = new Date(); + this.cacheInitialized = true; + + return { menu: freshMenu, fresh: true }; + } + + async checkForUpdates(ifModifiedSince: string): Promise { + if (!this.cacheInitialized) { + await this.getFullMenuWithCache(); + } + return !this.lastModified || new Date(ifModifiedSince) < this.lastModified; } private applyOverrides(menu: MenuItem, overrides: Partial[]): MenuItem { @@ -262,17 +283,21 @@ export class MenuService { } async updateMenuItem(id: string, update: Partial): Promise { - const fullMenu = await this.getFullMenu(); + const { menu: fullMenu } = await this.getFullMenuWithCache(); const item = this.findMenuItem(fullMenu, id); if (!item) throw new Error('Menu item not found'); Object.assign(item, update); + // Инвалидируем кэш после изменения + this.menuCache = null; return item; } async saveOverrides(overrides: Partial[]): Promise { await fs.writeFile(this.menuOverridesPath, JSON.stringify({ overrides }, null, 2), 'utf-8'); + // Инвалидируем кэш после изменения оверрайдов + this.menuCache = null; } private findMenuItem(menu: MenuItem, id: string): MenuItem | null { From 4074d453849979f55cd2e63673c39dd612023572 Mon Sep 17 00:00:00 2001 From: DmitriyA Date: Wed, 11 Jun 2025 07:31:58 -0400 Subject: [PATCH 2/3] added ranges editor --- src/menu/menu.controller.ts | 1 - src/menu/menu.module.ts | 3 ++- src/menu/menu.service.ts | 9 +++++++-- src/menu/range.controller.ts | 37 ++++++++++++++++++++++++++++++++++++ src/menu/range.service.ts | 16 ++++++++++++++++ 5 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 src/menu/range.controller.ts diff --git a/src/menu/menu.controller.ts b/src/menu/menu.controller.ts index 40093c4..499ee08 100644 --- a/src/menu/menu.controller.ts +++ b/src/menu/menu.controller.ts @@ -1,7 +1,6 @@ import { Controller, Get, Post, Put, Body, Param, Headers, HttpException, HttpStatus } from '@nestjs/common'; import { MenuService } from './menu.service'; import { MenuItem } from './menu.interface'; -import { Response } from 'express'; @Controller('menu') export class MenuController { diff --git a/src/menu/menu.module.ts b/src/menu/menu.module.ts index 6eccc8d..926b955 100644 --- a/src/menu/menu.module.ts +++ b/src/menu/menu.module.ts @@ -4,10 +4,11 @@ import { HttpModule } from '@nestjs/axios'; import { MenuService } from './menu.service'; import { PrometheusModule } from '../prometheus.module'; import { RangeService } from './range.service'; +import { RangeController } from './range.controller'; @Module({ imports: [PrometheusModule, HttpModule], - controllers: [MenuController], + controllers: [MenuController, RangeController], providers: [MenuService, RangeService] }) export class MenuModule { } \ No newline at end of file diff --git a/src/menu/menu.service.ts b/src/menu/menu.service.ts index 2e0dc20..583a843 100644 --- a/src/menu/menu.service.ts +++ b/src/menu/menu.service.ts @@ -289,17 +289,22 @@ export class MenuService { if (!item) throw new Error('Menu item not found'); Object.assign(item, update); - // Инвалидируем кэш после изменения + this.menuCache = null; return item; } async saveOverrides(overrides: Partial[]): Promise { await fs.writeFile(this.menuOverridesPath, JSON.stringify({ overrides }, null, 2), 'utf-8'); - // Инвалидируем кэш после изменения оверрайдов + this.menuCache = null; } + invalidateCache(): void { + this.menuCache = null; + this.lastModified = new Date(); + } + private findMenuItem(menu: MenuItem, id: string): MenuItem | null { if (menu.id === id) return menu; diff --git a/src/menu/range.controller.ts b/src/menu/range.controller.ts new file mode 100644 index 0000000..92c5657 --- /dev/null +++ b/src/menu/range.controller.ts @@ -0,0 +1,37 @@ +import { Controller, Post, Get, Body, HttpException, HttpStatus } from '@nestjs/common'; +import { RangeService } from './range.service'; +import { MenuService } from './menu.service'; + +@Controller('ranges') +export class RangeController { + constructor( + private readonly rangeService: RangeService, + private readonly menuService: MenuService + ) { } + + @Get('list') + async getRanges() { + try { + return await this.rangeService.getRanges(); + } catch (error) { + throw new HttpException('Failed to fetch ranges', HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Post('update') + async updateRanges( + @Body() data: Array<{ name: string; ranges: { min: number; max: number; status: number }[] }> + ) { + if (!Array.isArray(data)) { + throw new HttpException('Invalid data format', HttpStatus.BAD_REQUEST); + } + + try { + const result = await this.rangeService.updateRanges(data); + this.menuService.invalidateCache(); + return result; + } catch (error) { + throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR); + } + } +} diff --git a/src/menu/range.service.ts b/src/menu/range.service.ts index 7a0aa52..166c123 100644 --- a/src/menu/range.service.ts +++ b/src/menu/range.service.ts @@ -51,4 +51,20 @@ export class RangeService { return {}; } } + + async updateRanges(data: Array<{ name: string; ranges: { min: number; max: number; status: number }[] }>) { + try { + const response = await firstValueFrom( + this.httpService.post('http://192.168.2.39:9999/api/ranges/9999', data, { + headers: { 'Content-Type': 'application/json' }, + }) + ); + return response.data; + } catch (error) { + console.error('Failed to update ranges:', error); + throw new Error('Failed to update ranges'); + } + } + + } \ No newline at end of file From 57cf65b9a6226b8758d19cede092ecc3a41d1bb0 Mon Sep 17 00:00:00 2001 From: DmitriyA Date: Wed, 11 Jun 2025 08:14:25 -0400 Subject: [PATCH 3/3] added env --- .env | 3 +++ src/menu/range.service.ts | 18 +++++++++++++----- src/prometheus.service.ts | 2 +- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/.env b/.env index 0322715..d94718a 100644 --- a/.env +++ b/.env @@ -24,3 +24,6 @@ # COOKIE_SECURE=false # COOKIE_SAME_SITE=lax +# Для меню +#RANGES_API_URL=http://192.168.2.39:9999 +#RANGES_API_ENDPOINT=/api/ranges/9999 diff --git a/src/menu/range.service.ts b/src/menu/range.service.ts index 166c123..73dd0d4 100644 --- a/src/menu/range.service.ts +++ b/src/menu/range.service.ts @@ -1,17 +1,27 @@ import { Injectable } from '@nestjs/common'; import { HttpService } from '@nestjs/axios'; import { firstValueFrom } from 'rxjs'; +import { ConfigService } from '@nestjs/config'; @Injectable() export class RangeService { - constructor(private readonly httpService: HttpService) { } + private readonly rangesApiUrl: string; + private readonly rangesApiEndpoint: string; + + constructor( + private readonly httpService: HttpService, + private readonly configService: ConfigService + ) { + this.rangesApiUrl = this.configService.get('RANGES_API_URL', 'localhost:3000'); + this.rangesApiEndpoint = this.configService.get('RANGES_API_ENDPOINT', 'localhost:3000'); + } async getRanges(): Promise>> { try { const response = await firstValueFrom( this.httpService.request({ method: 'OPTIONS', - url: 'http://192.168.2.39:9999/api/ranges/9999', + url: `${this.rangesApiUrl}${this.rangesApiEndpoint}`, headers: { 'Accept': 'application/json' } @@ -55,7 +65,7 @@ export class RangeService { async updateRanges(data: Array<{ name: string; ranges: { min: number; max: number; status: number }[] }>) { try { const response = await firstValueFrom( - this.httpService.post('http://192.168.2.39:9999/api/ranges/9999', data, { + this.httpService.post(`${this.rangesApiUrl}${this.rangesApiEndpoint}`, data, { headers: { 'Content-Type': 'application/json' }, }) ); @@ -65,6 +75,4 @@ export class RangeService { throw new Error('Failed to update ranges'); } } - - } \ No newline at end of file diff --git a/src/prometheus.service.ts b/src/prometheus.service.ts index ac76079..eb685a1 100644 --- a/src/prometheus.service.ts +++ b/src/prometheus.service.ts @@ -124,7 +124,7 @@ export class PrometheusService { async fetchMetricsRange(metric: string, start: number, end: number, step: number, filters: Record = {}): Promise { const query = this.buildFilteredQuery(metric, { ...filters, - instance: '192.168.2.34:9050' // Явно фильтруем по нужному instance + instance: '192.168.2.34:9050' }); try { const response = await lastValueFrom(