first commit

This commit is contained in:
alexandrump
2026-02-09 01:02:53 +01:00
commit 82f3464565
90 changed files with 4788 additions and 0 deletions

View File

@@ -0,0 +1,104 @@
import { Controller, Get, Query } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CatalogService } from '../catalog/catalog.service';
import { AlertsService } from '../alerts/alerts.service';
import { ClassificationTagEntity } from '../entities/classification-tag.entity';
@Controller('dashboard')
export class DashboardController {
constructor(
private readonly catalog: CatalogService,
private readonly alerts: AlertsService,
@InjectRepository(ClassificationTagEntity)
private readonly tags: Repository<ClassificationTagEntity>,
) {}
@Get('summary')
async summary(@Query('days') days?: string) {
const daysCount = this.clampDays(days ? Number(days) : 14);
const items = await this.catalog.list();
const changes = await this.catalog.listChanges(20);
const since = this.startOfDayOffset(daysCount - 1);
const trendChanges = await this.catalog.listChangesSince(since);
const profiles = await this.alerts.listProfiles(process.env.DEFAULT_ALERT_USER || '00000000-0000-0000-0000-000000000001');
const tags = await this.tags.find();
const perPublisher: Record<string, number> = {};
const perFormat: Record<string, number> = {};
const perTopic: Record<string, number> = {};
const perTerritory: Record<string, number> = {};
const trend = this.buildTrend(daysCount);
for (const item of items) {
if (item.publisher) perPublisher[item.publisher] = (perPublisher[item.publisher] || 0) + 1;
if (item.format) {
for (const format of String(item.format).split(',')) {
const value = format.trim();
if (!value) continue;
perFormat[value] = (perFormat[value] || 0) + 1;
}
}
}
for (const tag of tags) {
if (tag.type === 'topic') perTopic[tag.value] = (perTopic[tag.value] || 0) + 1;
if (tag.type === 'territory') perTerritory[tag.value] = (perTerritory[tag.value] || 0) + 1;
}
for (const change of trendChanges) {
const key = this.formatDateKey(change.createdAt);
const bucket = trend[key];
if (!bucket) continue;
if (change.eventType === 'created') bucket.created += 1;
else bucket.updated += 1;
bucket.total += 1;
}
return {
totals: {
items: items.length,
changes: changes.length,
alertProfiles: profiles.length,
},
latestChanges: changes.slice(0, 8),
trend: Object.values(trend),
topPublishers: this.toSorted(perPublisher).slice(0, 6),
topFormats: this.toSorted(perFormat).slice(0, 6),
topTopics: this.toSorted(perTopic).slice(0, 6),
topTerritories: this.toSorted(perTerritory).slice(0, 6),
};
}
private toSorted(input: Record<string, number>) {
return Object.entries(input)
.map(([label, value]) => ({ label, value }))
.sort((a, b) => b.value - a.value);
}
private clampDays(value: number) {
if (!value || Number.isNaN(value)) return 14;
return Math.min(Math.max(value, 7), 60);
}
private startOfDayOffset(offsetDays: number) {
const date = new Date();
date.setHours(0, 0, 0, 0);
date.setDate(date.getDate() - offsetDays);
return date;
}
private formatDateKey(value: Date) {
return new Date(value).toISOString().slice(0, 10);
}
private buildTrend(days: number) {
const out: Record<string, { date: string; created: number; updated: number; total: number }> = {};
for (let i = days - 1; i >= 0; i -= 1) {
const d = this.startOfDayOffset(i);
const key = this.formatDateKey(d);
out[key] = { date: key, created: 0, updated: 0, total: 0 };
}
return out;
}
}

View File

@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { DashboardController } from './dashboard.controller';
import { CatalogModule } from '../catalog/catalog.module';
import { AlertsModule } from '../alerts/alerts.module';
import { ClassificationTagEntity } from '../entities/classification-tag.entity';
@Module({
imports: [CatalogModule, AlertsModule, TypeOrmModule.forFeature([ClassificationTagEntity])],
controllers: [DashboardController],
})
export class DashboardModule {}