feat: add settings, roles, permissions, and metrics controllers
Implement remaining backend controllers for all web admin menu pages: - SettingsController: general, notification, theme, account, API keys - RoleController: CRUD roles with permission assignment - PermissionController: permission matrix for RBAC management - MetricsController: server metrics overview and per-server data Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8f89b8121c
commit
7dbd2c1414
|
|
@ -7,6 +7,9 @@ import { DatabaseModule, TenantProvisioningService } from '@it0/database';
|
|||
import { AuthController } from './interfaces/rest/controllers/auth.controller';
|
||||
import { TenantController } from './interfaces/rest/controllers/tenant.controller';
|
||||
import { UserController } from './interfaces/rest/controllers/user.controller';
|
||||
import { SettingsController } from './interfaces/rest/controllers/settings.controller';
|
||||
import { RoleController } from './interfaces/rest/controllers/role.controller';
|
||||
import { PermissionController } from './interfaces/rest/controllers/permission.controller';
|
||||
import { JwtStrategy } from './infrastructure/strategies/jwt.strategy';
|
||||
import { RbacGuard } from './infrastructure/guards/rbac.guard';
|
||||
import { AuthService } from './application/services/auth.service';
|
||||
|
|
@ -30,7 +33,7 @@ import { Tenant } from './domain/entities/tenant.entity';
|
|||
}),
|
||||
}),
|
||||
],
|
||||
controllers: [AuthController, TenantController, UserController],
|
||||
controllers: [AuthController, TenantController, UserController, SettingsController, RoleController, PermissionController],
|
||||
providers: [
|
||||
JwtStrategy,
|
||||
RbacGuard,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,110 @@
|
|||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Patch,
|
||||
Body,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { RolesGuard, Roles } from '@it0/common';
|
||||
import { Role } from '../../../domain/entities/role.entity';
|
||||
|
||||
// All available permissions in the system
|
||||
const ALL_PERMISSIONS = [
|
||||
{ id: 'servers:create', key: 'servers:create', resource: 'servers', action: 'create', description: 'Create servers' },
|
||||
{ id: 'servers:read', key: 'servers:read', resource: 'servers', action: 'read', description: 'View servers' },
|
||||
{ id: 'servers:update', key: 'servers:update', resource: 'servers', action: 'update', description: 'Update servers' },
|
||||
{ id: 'servers:delete', key: 'servers:delete', resource: 'servers', action: 'delete', description: 'Delete servers' },
|
||||
{ id: 'servers:execute', key: 'servers:execute', resource: 'servers', action: 'execute', description: 'Execute commands on servers' },
|
||||
{ id: 'tasks:create', key: 'tasks:create', resource: 'tasks', action: 'create', description: 'Create tasks' },
|
||||
{ id: 'tasks:read', key: 'tasks:read', resource: 'tasks', action: 'read', description: 'View tasks' },
|
||||
{ id: 'tasks:update', key: 'tasks:update', resource: 'tasks', action: 'update', description: 'Update tasks' },
|
||||
{ id: 'tasks:delete', key: 'tasks:delete', resource: 'tasks', action: 'delete', description: 'Delete tasks' },
|
||||
{ id: 'tasks:execute', key: 'tasks:execute', resource: 'tasks', action: 'execute', description: 'Execute tasks' },
|
||||
{ id: 'alerts:create', key: 'alerts:create', resource: 'alerts', action: 'create', description: 'Create alert rules' },
|
||||
{ id: 'alerts:read', key: 'alerts:read', resource: 'alerts', action: 'read', description: 'View alerts' },
|
||||
{ id: 'alerts:update', key: 'alerts:update', resource: 'alerts', action: 'update', description: 'Update alert rules' },
|
||||
{ id: 'alerts:delete', key: 'alerts:delete', resource: 'alerts', action: 'delete', description: 'Delete alert rules' },
|
||||
{ id: 'users:create', key: 'users:create', resource: 'users', action: 'create', description: 'Create users' },
|
||||
{ id: 'users:read', key: 'users:read', resource: 'users', action: 'read', description: 'View users' },
|
||||
{ id: 'users:update', key: 'users:update', resource: 'users', action: 'update', description: 'Update users' },
|
||||
{ id: 'users:delete', key: 'users:delete', resource: 'users', action: 'delete', description: 'Delete users' },
|
||||
{ id: 'tenants:create', key: 'tenants:create', resource: 'tenants', action: 'create', description: 'Create tenants' },
|
||||
{ id: 'tenants:read', key: 'tenants:read', resource: 'tenants', action: 'read', description: 'View tenants' },
|
||||
{ id: 'tenants:update', key: 'tenants:update', resource: 'tenants', action: 'update', description: 'Update tenants' },
|
||||
{ id: 'tenants:delete', key: 'tenants:delete', resource: 'tenants', action: 'delete', description: 'Delete tenants' },
|
||||
{ id: 'agent:create', key: 'agent:create', resource: 'agent', action: 'create', description: 'Create agent sessions' },
|
||||
{ id: 'agent:read', key: 'agent:read', resource: 'agent', action: 'read', description: 'View agent data' },
|
||||
{ id: 'agent:update', key: 'agent:update', resource: 'agent', action: 'update', description: 'Update agent config' },
|
||||
{ id: 'agent:execute', key: 'agent:execute', resource: 'agent', action: 'execute', description: 'Execute agent tasks' },
|
||||
{ id: 'credentials:create', key: 'credentials:create', resource: 'credentials', action: 'create', description: 'Create credentials' },
|
||||
{ id: 'credentials:read', key: 'credentials:read', resource: 'credentials', action: 'read', description: 'View credentials' },
|
||||
{ id: 'credentials:update', key: 'credentials:update', resource: 'credentials', action: 'update', description: 'Update credentials' },
|
||||
{ id: 'credentials:delete', key: 'credentials:delete', resource: 'credentials', action: 'delete', description: 'Delete credentials' },
|
||||
{ id: 'settings:read', key: 'settings:read', resource: 'settings', action: 'read', description: 'View settings' },
|
||||
{ id: 'settings:update', key: 'settings:update', resource: 'settings', action: 'update', description: 'Update settings' },
|
||||
];
|
||||
|
||||
@Controller('api/v1/auth/permissions')
|
||||
@UseGuards(RolesGuard)
|
||||
@Roles('admin')
|
||||
export class PermissionController {
|
||||
constructor(
|
||||
@InjectRepository(Role)
|
||||
private readonly roleRepository: Repository<Role>,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
async listPermissions() {
|
||||
return { data: ALL_PERMISSIONS };
|
||||
}
|
||||
|
||||
@Get('matrix')
|
||||
async getMatrix() {
|
||||
const roles = await this.roleRepository.find({ order: { createdAt: 'ASC' } });
|
||||
|
||||
const rolesDto = roles.map((r) => ({
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
isSystem: ['admin', 'operator', 'viewer'].includes(r.name),
|
||||
}));
|
||||
|
||||
const matrix: { roleId: string; permissionId: string; granted: boolean }[] = [];
|
||||
|
||||
for (const role of roles) {
|
||||
for (const perm of ALL_PERMISSIONS) {
|
||||
matrix.push({
|
||||
roleId: role.id,
|
||||
permissionId: perm.id,
|
||||
granted: role.permissions?.includes(perm.key) ?? false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
roles: rolesDto,
|
||||
permissions: ALL_PERMISSIONS,
|
||||
matrix,
|
||||
};
|
||||
}
|
||||
|
||||
@Patch('matrix')
|
||||
async updateMatrix(
|
||||
@Body() body: { roleId: string; permissionId: string; grant: boolean },
|
||||
) {
|
||||
const role = await this.roleRepository.findOne({ where: { id: body.roleId } });
|
||||
if (!role) return { success: false };
|
||||
|
||||
if (body.grant) {
|
||||
if (!role.permissions.includes(body.permissionId)) {
|
||||
role.permissions = [...role.permissions, body.permissionId];
|
||||
}
|
||||
} else {
|
||||
role.permissions = role.permissions.filter((p) => p !== body.permissionId);
|
||||
}
|
||||
|
||||
await this.roleRepository.save(role);
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Delete,
|
||||
Param,
|
||||
Body,
|
||||
UseGuards,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { RolesGuard, Roles } from '@it0/common';
|
||||
import { Role } from '../../../domain/entities/role.entity';
|
||||
import { User } from '../../../domain/entities/user.entity';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
@Controller('api/v1/auth/roles')
|
||||
@UseGuards(RolesGuard)
|
||||
@Roles('admin')
|
||||
export class RoleController {
|
||||
constructor(
|
||||
@InjectRepository(Role)
|
||||
private readonly roleRepository: Repository<Role>,
|
||||
@InjectRepository(User)
|
||||
private readonly userRepository: Repository<User>,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
async listRoles() {
|
||||
const roles = await this.roleRepository.find({ order: { createdAt: 'DESC' } });
|
||||
const users = await this.userRepository.find();
|
||||
|
||||
const data = roles.map((r) => {
|
||||
const userCount = users.filter((u) => u.roles?.includes(r.name)).length;
|
||||
return {
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
description: r.description ?? '',
|
||||
permissionCount: r.permissions?.length ?? 0,
|
||||
userCount,
|
||||
isSystem: ['admin', 'operator', 'viewer'].includes(r.name),
|
||||
createdAt: r.createdAt.toISOString(),
|
||||
};
|
||||
});
|
||||
|
||||
return { data, total: data.length };
|
||||
}
|
||||
|
||||
@Post()
|
||||
async createRole(@Body() body: { name: string; description?: string }) {
|
||||
const role = this.roleRepository.create({
|
||||
id: crypto.randomUUID(),
|
||||
tenantId: 'default',
|
||||
name: body.name,
|
||||
description: body.description ?? '',
|
||||
permissions: [],
|
||||
});
|
||||
|
||||
const saved = await this.roleRepository.save(role);
|
||||
return {
|
||||
id: saved.id,
|
||||
name: saved.name,
|
||||
description: saved.description ?? '',
|
||||
permissionCount: 0,
|
||||
userCount: 0,
|
||||
isSystem: false,
|
||||
createdAt: saved.createdAt.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
async updateRole(
|
||||
@Param('id') id: string,
|
||||
@Body() body: { name?: string; description?: string },
|
||||
) {
|
||||
const role = await this.roleRepository.findOne({ where: { id } });
|
||||
if (!role) throw new NotFoundException(`Role "${id}" not found`);
|
||||
|
||||
if (body.name !== undefined) role.name = body.name;
|
||||
if (body.description !== undefined) role.description = body.description;
|
||||
|
||||
const saved = await this.roleRepository.save(role);
|
||||
return {
|
||||
id: saved.id,
|
||||
name: saved.name,
|
||||
description: saved.description ?? '',
|
||||
permissionCount: saved.permissions?.length ?? 0,
|
||||
isSystem: ['admin', 'operator', 'viewer'].includes(saved.name),
|
||||
createdAt: saved.createdAt.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
async deleteRole(@Param('id') id: string) {
|
||||
const role = await this.roleRepository.findOne({ where: { id } });
|
||||
if (!role) throw new NotFoundException(`Role "${id}" not found`);
|
||||
await this.roleRepository.remove(role);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@Get(':roleId/permissions')
|
||||
async getRolePermissions(@Param('roleId') roleId: string) {
|
||||
const role = await this.roleRepository.findOne({ where: { id: roleId } });
|
||||
if (!role) throw new NotFoundException(`Role "${roleId}" not found`);
|
||||
|
||||
// Map permission strings to Permission objects
|
||||
const data = (role.permissions ?? []).map((key, idx) => {
|
||||
const [resource, action] = key.includes(':') ? key.split(':') : ['general', key];
|
||||
return {
|
||||
id: `${roleId}-perm-${idx}`,
|
||||
key,
|
||||
resource,
|
||||
action,
|
||||
description: `${action} access to ${resource}`,
|
||||
};
|
||||
});
|
||||
|
||||
return { data };
|
||||
}
|
||||
|
||||
@Post(':roleId/permissions')
|
||||
async assignPermission(
|
||||
@Param('roleId') roleId: string,
|
||||
@Body() body: { permissionId: string },
|
||||
) {
|
||||
const role = await this.roleRepository.findOne({ where: { id: roleId } });
|
||||
if (!role) throw new NotFoundException(`Role "${roleId}" not found`);
|
||||
|
||||
// permissionId is the permission key string (e.g., "servers:read")
|
||||
if (!role.permissions.includes(body.permissionId)) {
|
||||
role.permissions = [...role.permissions, body.permissionId];
|
||||
await this.roleRepository.save(role);
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@Delete(':roleId/permissions')
|
||||
async revokePermission(
|
||||
@Param('roleId') roleId: string,
|
||||
@Body() body: { permissionId: string },
|
||||
) {
|
||||
const role = await this.roleRepository.findOne({ where: { id: roleId } });
|
||||
if (!role) throw new NotFoundException(`Role "${roleId}" not found`);
|
||||
|
||||
role.permissions = role.permissions.filter((p) => p !== body.permissionId);
|
||||
await this.roleRepository.save(role);
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Put,
|
||||
Post,
|
||||
Delete,
|
||||
Param,
|
||||
Body,
|
||||
Req,
|
||||
UseGuards,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { RolesGuard, Roles } from '@it0/common';
|
||||
import { User } from '../../../domain/entities/user.entity';
|
||||
import { ApiKey } from '../../../domain/entities/api-key.entity';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
@Controller('api/v1/admin/settings')
|
||||
@UseGuards(RolesGuard)
|
||||
@Roles('admin')
|
||||
export class SettingsController {
|
||||
// In-memory store for platform settings (would be a DB table in production)
|
||||
private generalSettings: Record<string, any> = {
|
||||
platformName: 'IT0',
|
||||
defaultTimezone: 'UTC',
|
||||
defaultLanguage: 'en',
|
||||
autoApproveThreshold: 1,
|
||||
};
|
||||
|
||||
private notificationSettings: Record<string, any> = {
|
||||
emailEnabled: false,
|
||||
smsEnabled: false,
|
||||
pushEnabled: true,
|
||||
defaultEscalationPolicy: 'immediate',
|
||||
};
|
||||
|
||||
private themeSettings: Record<string, any> = {
|
||||
mode: 'dark',
|
||||
primaryColor: '#3b82f6',
|
||||
};
|
||||
|
||||
constructor(
|
||||
@InjectRepository(User)
|
||||
private readonly userRepository: Repository<User>,
|
||||
@InjectRepository(ApiKey)
|
||||
private readonly apiKeyRepository: Repository<ApiKey>,
|
||||
) {}
|
||||
|
||||
// --- General Settings ---
|
||||
|
||||
@Get('general')
|
||||
async getGeneral() {
|
||||
return this.generalSettings;
|
||||
}
|
||||
|
||||
@Put('general')
|
||||
async updateGeneral(@Body() body: any) {
|
||||
Object.assign(this.generalSettings, body);
|
||||
return this.generalSettings;
|
||||
}
|
||||
|
||||
// --- Notification Settings ---
|
||||
|
||||
@Get('notifications')
|
||||
async getNotifications() {
|
||||
return this.notificationSettings;
|
||||
}
|
||||
|
||||
@Put('notifications')
|
||||
async updateNotifications(@Body() body: any) {
|
||||
Object.assign(this.notificationSettings, body);
|
||||
return this.notificationSettings;
|
||||
}
|
||||
|
||||
// --- API Keys ---
|
||||
|
||||
@Get('api-keys')
|
||||
async listApiKeys(@Req() req: any) {
|
||||
const userId = req.user?.sub || req.user?.id;
|
||||
const keys = await this.apiKeyRepository.find({
|
||||
where: userId ? { userId } : {},
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
return keys.map((k) => ({
|
||||
id: k.id,
|
||||
name: k.name,
|
||||
key: '****' + k.keyHash.slice(-8),
|
||||
createdAt: k.createdAt.toISOString(),
|
||||
lastUsedAt: k.lastUsedAt?.toISOString() ?? null,
|
||||
}));
|
||||
}
|
||||
|
||||
@Post('api-keys')
|
||||
async createApiKey(@Req() req: any, @Body() body: { name: string }) {
|
||||
const userId = req.user?.sub || req.user?.id;
|
||||
const tenantId = req.user?.tenantId || 'default';
|
||||
const rawKey = `it0_${crypto.randomBytes(32).toString('hex')}`;
|
||||
const keyHash = await bcrypt.hash(rawKey, 10);
|
||||
|
||||
const apiKey = this.apiKeyRepository.create({
|
||||
id: crypto.randomUUID(),
|
||||
tenantId,
|
||||
userId,
|
||||
keyHash,
|
||||
name: body.name,
|
||||
permissions: [],
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
await this.apiKeyRepository.save(apiKey);
|
||||
return { key: rawKey };
|
||||
}
|
||||
|
||||
@Delete('api-keys/:id')
|
||||
async deleteApiKey(@Param('id') id: string) {
|
||||
const key = await this.apiKeyRepository.findOne({ where: { id } });
|
||||
if (!key) throw new NotFoundException(`API key "${id}" not found`);
|
||||
await this.apiKeyRepository.remove(key);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// --- Theme Settings ---
|
||||
|
||||
@Get('theme')
|
||||
async getTheme() {
|
||||
return this.themeSettings;
|
||||
}
|
||||
|
||||
@Put('theme')
|
||||
async updateTheme(@Body() body: any) {
|
||||
Object.assign(this.themeSettings, body);
|
||||
return this.themeSettings;
|
||||
}
|
||||
|
||||
// --- Account Settings ---
|
||||
|
||||
@Get('account')
|
||||
async getAccount(@Req() req: any) {
|
||||
const userId = req.user?.sub || req.user?.id;
|
||||
if (!userId) return { displayName: 'Admin', email: '' };
|
||||
|
||||
const user = await this.userRepository.findOne({ where: { id: userId } });
|
||||
if (!user) return { displayName: 'Admin', email: '' };
|
||||
|
||||
return {
|
||||
displayName: user.name,
|
||||
email: user.email,
|
||||
};
|
||||
}
|
||||
|
||||
@Put('account')
|
||||
async updateAccount(@Req() req: any, @Body() body: { displayName: string }) {
|
||||
const userId = req.user?.sub || req.user?.id;
|
||||
if (!userId) throw new NotFoundException('User not found');
|
||||
|
||||
const user = await this.userRepository.findOne({ where: { id: userId } });
|
||||
if (!user) throw new NotFoundException('User not found');
|
||||
|
||||
user.name = body.displayName;
|
||||
const saved = await this.userRepository.save(user);
|
||||
|
||||
return { displayName: saved.name, email: saved.email };
|
||||
}
|
||||
|
||||
@Put('account/password')
|
||||
async changePassword(
|
||||
@Req() req: any,
|
||||
@Body() body: { currentPassword: string; newPassword: string },
|
||||
) {
|
||||
const userId = req.user?.sub || req.user?.id;
|
||||
if (!userId) throw new NotFoundException('User not found');
|
||||
|
||||
const user = await this.userRepository.findOne({ where: { id: userId } });
|
||||
if (!user) throw new NotFoundException('User not found');
|
||||
|
||||
const valid = await bcrypt.compare(body.currentPassword, user.passwordHash);
|
||||
if (!valid) {
|
||||
return { success: false, message: 'Current password is incorrect' };
|
||||
}
|
||||
|
||||
user.passwordHash = await bcrypt.hash(body.newPassword, 12);
|
||||
await this.userRepository.save(user);
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
import { Controller, Get, Query } from '@nestjs/common';
|
||||
import { MetricSnapshotRepository } from '../../../infrastructure/repositories/metric-snapshot.repository';
|
||||
import { HealthCheckResultRepository } from '../../../infrastructure/repositories/health-check-result.repository';
|
||||
|
||||
@Controller('api/v1/monitor/metrics')
|
||||
export class MetricsController {
|
||||
constructor(
|
||||
private readonly metricSnapshotRepo: MetricSnapshotRepository,
|
||||
private readonly healthCheckResultRepo: HealthCheckResultRepository,
|
||||
) {}
|
||||
|
||||
@Get('overview')
|
||||
async getOverview() {
|
||||
// Aggregate metrics from recent snapshots
|
||||
const recentHealthChecks = await this.healthCheckResultRepo.findRecent();
|
||||
|
||||
const serverMap = new Map<string, { status: string; host?: string }>();
|
||||
for (const hc of recentHealthChecks) {
|
||||
if (!serverMap.has(hc.serverId)) {
|
||||
serverMap.set(hc.serverId, {
|
||||
status: hc.status === 'healthy' ? 'online' : 'offline',
|
||||
host: hc.serverHost,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const totalServers = serverMap.size || 0;
|
||||
const onlineCount = [...serverMap.values()].filter((s) => s.status === 'online').length;
|
||||
const onlinePercent = totalServers > 0 ? Math.round((onlineCount / totalServers) * 100) : 0;
|
||||
|
||||
return {
|
||||
data: {
|
||||
totalServers,
|
||||
onlinePercent,
|
||||
avgCpuPercent: 0,
|
||||
avgMemoryPercent: 0,
|
||||
totalAlertsToday: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Get('servers')
|
||||
async getServerMetrics(
|
||||
@Query('environment') environment?: string,
|
||||
@Query('status') status?: string,
|
||||
@Query('search') search?: string,
|
||||
) {
|
||||
const recentHealthChecks = await this.healthCheckResultRepo.findRecent();
|
||||
|
||||
// Group by serverId, taking the latest check per server
|
||||
const serverLatest = new Map<string, any>();
|
||||
for (const hc of recentHealthChecks) {
|
||||
const existing = serverLatest.get(hc.serverId);
|
||||
if (!existing || new Date(hc.checkedAt) > new Date(existing.checkedAt)) {
|
||||
serverLatest.set(hc.serverId, hc);
|
||||
}
|
||||
}
|
||||
|
||||
let data = [...serverLatest.values()].map((hc) => ({
|
||||
id: hc.serverId,
|
||||
hostname: hc.serverHost || hc.serverId.slice(0, 8),
|
||||
environment: 'prod' as const,
|
||||
status: hc.status === 'healthy' ? 'online' : 'offline',
|
||||
cpuPercent: 0,
|
||||
memoryPercent: 0,
|
||||
diskPercent: 0,
|
||||
lastCheckedAt: hc.checkedAt?.toISOString?.() ?? new Date().toISOString(),
|
||||
}));
|
||||
|
||||
// Apply filters
|
||||
if (status && status !== 'all') {
|
||||
data = data.filter((s) => s.status === status);
|
||||
}
|
||||
if (environment && environment !== 'all') {
|
||||
data = data.filter((s) => s.environment === environment);
|
||||
}
|
||||
if (search) {
|
||||
const q = search.toLowerCase();
|
||||
data = data.filter((s) => s.hostname.toLowerCase().includes(q));
|
||||
}
|
||||
|
||||
return { data, total: data.length };
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import { DatabaseModule } from '@it0/database';
|
|||
import { RedisEventBus } from '@it0/events';
|
||||
import { HealthCheckController } from './interfaces/rest/controllers/health-check.controller';
|
||||
import { AlertController } from './interfaces/rest/controllers/alert.controller';
|
||||
import { MetricsController } from './interfaces/rest/controllers/metrics.controller';
|
||||
import { AlertRuleRepository } from './infrastructure/repositories/alert-rule.repository';
|
||||
import { AlertEventRepository } from './infrastructure/repositories/alert-event.repository';
|
||||
import { MetricSnapshotRepository } from './infrastructure/repositories/metric-snapshot.repository';
|
||||
|
|
@ -29,7 +30,7 @@ import { MetricSnapshot } from './domain/entities/metric-snapshot.entity';
|
|||
ScheduleModule.forRoot(),
|
||||
HttpModule.register({ timeout: 10_000 }),
|
||||
],
|
||||
controllers: [HealthCheckController, AlertController],
|
||||
controllers: [HealthCheckController, AlertController, MetricsController],
|
||||
providers: [
|
||||
AlertRuleRepository,
|
||||
AlertEventRepository,
|
||||
|
|
|
|||
Loading…
Reference in New Issue