feat(admin): monitoring center
Sprint 26. New features/admin/monitoring/ module + net-new /:lang/backoffice/monitoring route + Dashboard Quick Action. - Health section reuses AdminDashboardFacade.healthChecks directly (real data, unchanged since Sprint 19) instead of duplicating the logic - unified AdminMonitoringEvent feed covering audit/security/login/ failed-login/api/error/warning, category filter + search, 40 seeded synthetic entries (no logging backend exists anywhere in this system) - mock queue depth/status cards, mock webhook delivery log - intentionally kept separate from Sprint 24's per-transaction audit and Sprint 25's per-user audit - different scopes, no consolidation attempted Also fixed a real type error: AdminDashboardQuickActionId's union was missing 'users' and 'monitoring' (added when wiring those Quick Actions), caught by ng build's template type-checking even though plain tsc --noEmit passed - a reminder that ng build is the authoritative check here. docs/ADMIN.md + docs/BACKEND.md (new item 15) updated. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -369,6 +369,28 @@ routes at all, this is a net-new admin section.
|
|||||||
- Wired into `AdminDashboardFacade`'s Quick Actions list (`dashboard.actionUsers`
|
- Wired into `AdminDashboardFacade`'s Quick Actions list (`dashboard.actionUsers`
|
||||||
-> `/:lang/backoffice/users`).
|
-> `/:lang/backoffice/users`).
|
||||||
|
|
||||||
|
## Sprint 26 - Monitoring (mock/local, health reuses real data)
|
||||||
|
|
||||||
|
`features/admin/monitoring/`, single page at `/:lang/backoffice/monitoring`
|
||||||
|
(new Dashboard Quick Action).
|
||||||
|
|
||||||
|
- **Health**: reuses `AdminDashboardFacade.healthChecks` directly (the same
|
||||||
|
real, non-mocked bootstrap-validation checks from Sprint 19's dashboard)
|
||||||
|
instead of duplicating the logic - this is the one section on this page
|
||||||
|
backed by real data.
|
||||||
|
- **Audit / security / login / failed-login / API / error / warning
|
||||||
|
events**: one unified `AdminMonitoringEvent` feed (`category` + `level`
|
||||||
|
discriminators) with category filter + search, seeded with 40
|
||||||
|
deterministic synthetic entries by `AdminMonitoringLocalGateway` - no
|
||||||
|
logging backend exists anywhere in this system, so there is nothing real
|
||||||
|
to read from.
|
||||||
|
- **Queue monitoring**: 3 mock named queues with depth + status.
|
||||||
|
- **Webhook monitoring**: mock delivery log (endpoint/event/status/time).
|
||||||
|
- This is deliberately a separate, system-wide log from the two
|
||||||
|
narrower-scoped audit trails added earlier: Sprint 24's per-transaction
|
||||||
|
audit and Sprint 25's per-user audit. No consolidation attempted - they
|
||||||
|
track different things.
|
||||||
|
|
||||||
## Known gaps / backend needs
|
## Known gaps / backend needs
|
||||||
|
|
||||||
- **Dashboard metrics endpoint.** Categories/Products counts are computed
|
- **Dashboard metrics endpoint.** Categories/Products counts are computed
|
||||||
|
|||||||
@@ -160,6 +160,14 @@ Plus, if authenticated history/wishlist/compare/saved-searches sync is wanted: `
|
|||||||
|
|
||||||
**Frontend files:** implement `AdminUsersApiGateway` against `AdminUsersGateway` (`services/admin-users-gateway.interface.ts`) and rebind via an injection token.
|
**Frontend files:** implement `AdminUsersApiGateway` against `AdminUsersGateway` (`services/admin-users-gateway.interface.ts`) and rebind via an injection token.
|
||||||
|
|
||||||
|
## 15. Monitoring (Sprint 26, mock/local except Health)
|
||||||
|
|
||||||
|
**Current behavior:** `features/admin/monitoring/` — Health section reads real data (`AdminDashboardFacade.healthChecks`, unchanged from Sprint 19). Everything else (audit/security/login/failed-login/API/error/warning event feed, queue depths, webhook deliveries) is synthetic, seeded once in `AdminMonitoringLocalGateway` — no logging, queue, or webhook infrastructure exists anywhere in this system.
|
||||||
|
|
||||||
|
**Needed:** real structured logging with a query API (by category/level/actor/time range), real queue introspection (whatever job runner ships), and real webhook delivery tracking once webhooks exist as a feature at all.
|
||||||
|
|
||||||
|
**Frontend files:** implement `AdminMonitoringApiGateway`-equivalent methods against a to-be-defined `AdminMonitoringGateway` interface (`AdminMonitoringLocalGateway` currently has no interface extracted — add one when a real implementation is built, mirroring the pattern used everywhere else in `admin/*`).
|
||||||
|
|
||||||
## Known reliability issues
|
## Known reliability issues
|
||||||
|
|
||||||
### Production 502/504 Bad Gateway on refresh / back-navigation
|
### Production 502/504 Bad Gateway on refresh / back-navigation
|
||||||
|
|||||||
@@ -59,8 +59,9 @@ Notify user: **from Sprint 20 (Categories) once product↔category link + admin
|
|||||||
- [x] `docs/ADMIN.md` (new Sprint 25 section), `docs/BACKEND.md` item 14 added
|
- [x] `docs/ADMIN.md` (new Sprint 25 section), `docs/BACKEND.md` item 14 added
|
||||||
- Commit: `feat(admin): users and permissions`
|
- Commit: `feat(admin): users and permissions`
|
||||||
|
|
||||||
## Sprint 26 — Monitoring
|
## Sprint 26 — Monitoring ✅ done
|
||||||
- [ ] Audit/security/login logs views (local), API/error/warning feed, queue/webhook placeholders, health page
|
- [x] Audit/security/login/failed-login logs (unified event feed, category filter), API/error/warning feed, queue/webhook mock views, health page (reuses real Sprint 19 healthChecks)
|
||||||
|
- [x] `docs/ADMIN.md` (new Sprint 26 section), `docs/BACKEND.md` item 15 added
|
||||||
- Commit: `feat(admin): monitoring center`
|
- Commit: `feat(admin): monitoring center`
|
||||||
|
|
||||||
## Sprint 27 — Analytics
|
## Sprint 27 — Analytics
|
||||||
|
|||||||
@@ -112,6 +112,10 @@ const coreRoutes: Routes = [
|
|||||||
path: 'users',
|
path: 'users',
|
||||||
loadComponent: () => import('./features/admin/users/pages/admin-users-page.component').then(m => m.AdminUsersPageComponent)
|
loadComponent: () => import('./features/admin/users/pages/admin-users-page.component').then(m => m.AdminUsersPageComponent)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'monitoring',
|
||||||
|
loadComponent: () => import('./features/admin/monitoring/pages/admin-monitoring-page.component').then(m => m.AdminMonitoringPageComponent)
|
||||||
|
},
|
||||||
{ path: '**', redirectTo: 'dashboard' }
|
{ path: '**', redirectTo: 'dashboard' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const QUICK_ACTIONS: AdminDashboardQuickAction[] = [
|
|||||||
{ id: 'orders', labelKey: 'dashboard.actionOrders', route: ['backoffice', 'orders'] },
|
{ id: 'orders', labelKey: 'dashboard.actionOrders', route: ['backoffice', 'orders'] },
|
||||||
{ id: 'media-library', labelKey: 'dashboard.actionMediaLibrary', route: ['backoffice', 'media'] },
|
{ id: 'media-library', labelKey: 'dashboard.actionMediaLibrary', route: ['backoffice', 'media'] },
|
||||||
{ id: 'users', labelKey: 'dashboard.actionUsers', route: ['backoffice', 'users'] },
|
{ id: 'users', labelKey: 'dashboard.actionUsers', route: ['backoffice', 'users'] },
|
||||||
|
{ id: 'monitoring', labelKey: 'dashboard.actionMonitoring', route: ['backoffice', 'monitoring'] },
|
||||||
{ id: 'preview-marketplace', labelKey: 'dashboard.actionPreviewMarketplace', route: [''] },
|
{ id: 'preview-marketplace', labelKey: 'dashboard.actionPreviewMarketplace', route: [''] },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ export type AdminDashboardQuickActionId =
|
|||||||
| 'transactions'
|
| 'transactions'
|
||||||
| 'orders'
|
| 'orders'
|
||||||
| 'media-library'
|
| 'media-library'
|
||||||
|
| 'users'
|
||||||
|
| 'monitoring'
|
||||||
| 'preview-marketplace';
|
| 'preview-marketplace';
|
||||||
|
|
||||||
export interface AdminDashboardQuickAction {
|
export interface AdminDashboardQuickAction {
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Injectable, inject, signal } from '@angular/core';
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
|
import { AdminMonitoringEvent, AdminMonitoringEventFilters, AdminQueue, AdminWebhookDelivery } from '../models/admin-monitoring.model';
|
||||||
|
import { AdminMonitoringLocalGateway } from '../services/admin-monitoring-local.gateway';
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class AdminMonitoringFacade {
|
||||||
|
private readonly gateway = inject(AdminMonitoringLocalGateway);
|
||||||
|
|
||||||
|
readonly filters = signal<AdminMonitoringEventFilters>({ category: 'all', search: '' });
|
||||||
|
readonly events = signal<AdminMonitoringEvent[]>([]);
|
||||||
|
readonly queues = signal<AdminQueue[]>([]);
|
||||||
|
readonly webhooks = signal<AdminWebhookDelivery[]>([]);
|
||||||
|
readonly loading = signal(false);
|
||||||
|
|
||||||
|
loadAll(): void {
|
||||||
|
this.loading.set(true);
|
||||||
|
this.gateway.loadEvents(this.filters()).pipe(take(1)).subscribe(events => { this.events.set(events); this.loading.set(false); });
|
||||||
|
this.gateway.loadQueues().pipe(take(1)).subscribe(queues => this.queues.set(queues));
|
||||||
|
this.gateway.loadWebhooks().pipe(take(1)).subscribe(webhooks => this.webhooks.set(webhooks));
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFilters(patch: Partial<AdminMonitoringEventFilters>): void {
|
||||||
|
this.filters.update(current => ({ ...current, ...patch }));
|
||||||
|
this.gateway.loadEvents(this.filters()).pipe(take(1)).subscribe(events => this.events.set(events));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
export type AdminMonitoringCategory = 'audit' | 'security' | 'login' | 'failed_login' | 'api' | 'error' | 'warning';
|
||||||
|
export type AdminMonitoringLevel = 'info' | 'warning' | 'error';
|
||||||
|
export type AdminQueueStatus = 'healthy' | 'degraded' | 'down';
|
||||||
|
export type AdminWebhookStatus = 'delivered' | 'failed' | 'pending';
|
||||||
|
|
||||||
|
export interface AdminMonitoringEvent {
|
||||||
|
id: string;
|
||||||
|
category: AdminMonitoringCategory;
|
||||||
|
level: AdminMonitoringLevel;
|
||||||
|
message: string;
|
||||||
|
actor: string;
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminMonitoringEventFilters {
|
||||||
|
category: 'all' | AdminMonitoringCategory;
|
||||||
|
search: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminQueue {
|
||||||
|
name: string;
|
||||||
|
depth: number;
|
||||||
|
status: AdminQueueStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminWebhookDelivery {
|
||||||
|
id: string;
|
||||||
|
endpoint: string;
|
||||||
|
event: string;
|
||||||
|
status: AdminWebhookStatus;
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<section class="admin-monitoring-page">
|
||||||
|
<div class="card">
|
||||||
|
<h2>{{ 'adminMonitoring.health' | translate }}</h2>
|
||||||
|
<div class="health-grid">
|
||||||
|
@for (check of dashboardFacade.healthChecks(); track check.code) {
|
||||||
|
<app-badge [variant]="check.healthy ? 'success' : 'danger'">{{ check.labelKey | translate }}</app-badge>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>{{ 'adminMonitoring.queues' | translate }}</h2>
|
||||||
|
<div class="queue-grid">
|
||||||
|
@for (queue of facade.queues(); track queue.name) {
|
||||||
|
<div class="queue-item">
|
||||||
|
<strong>{{ queue.name }}</strong>
|
||||||
|
<span>{{ 'adminMonitoring.depth' | translate }}: {{ queue.depth }}</span>
|
||||||
|
<app-badge [variant]="queue.status === 'healthy' ? 'success' : queue.status === 'degraded' ? 'warning' : 'danger'">{{ ('adminMonitoring.queueStatus.' + queue.status) | translate }}</app-badge>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>{{ 'adminMonitoring.webhooks' | translate }}</h2>
|
||||||
|
<app-table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ 'adminMonitoring.endpoint' | translate }}</th>
|
||||||
|
<th>{{ 'adminMonitoring.event' | translate }}</th>
|
||||||
|
<th>{{ 'backoffice.status' | translate }}</th>
|
||||||
|
<th>{{ 'adminOrders.createdAt' | translate }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (delivery of facade.webhooks(); track delivery.id) {
|
||||||
|
<tr>
|
||||||
|
<td>{{ delivery.endpoint }}</td>
|
||||||
|
<td>{{ delivery.event }}</td>
|
||||||
|
<td><app-badge [variant]="delivery.status === 'delivered' ? 'success' : delivery.status === 'failed' ? 'danger' : 'neutral'">{{ ('adminMonitoring.webhookStatus.' + delivery.status) | translate }}</app-badge></td>
|
||||||
|
<td>{{ delivery.timestamp | date:'short' }}</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</app-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>{{ 'adminMonitoring.events' | translate }}</h2>
|
||||||
|
<div class="filters">
|
||||||
|
<app-input type="search" [ngModel]="facade.filters().search" (ngModelChange)="facade.updateFilters({ search: $event })" [placeholder]="'adminOrders.search' | translate" />
|
||||||
|
<select [ngModel]="facade.filters().category" (ngModelChange)="facade.updateFilters({ category: $event })">
|
||||||
|
@for (category of categories; track category) {
|
||||||
|
<option [value]="category">{{ ('adminMonitoring.category.' + category) | translate }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<app-table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ 'adminMonitoring.category' | translate }}</th>
|
||||||
|
<th>{{ 'adminMonitoring.level' | translate }}</th>
|
||||||
|
<th>{{ 'adminMonitoring.message' | translate }}</th>
|
||||||
|
<th>{{ 'adminUsers.name' | translate }}</th>
|
||||||
|
<th>{{ 'adminOrders.createdAt' | translate }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (event of facade.events(); track event.id) {
|
||||||
|
<tr>
|
||||||
|
<td>{{ ('adminMonitoring.category.' + event.category) | translate }}</td>
|
||||||
|
<td><app-badge [variant]="event.level === 'error' ? 'danger' : event.level === 'warning' ? 'warning' : 'neutral'">{{ event.level }}</app-badge></td>
|
||||||
|
<td>{{ event.message }}</td>
|
||||||
|
<td>{{ event.actor }}</td>
|
||||||
|
<td>{{ event.timestamp | date:'short' }}</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</app-table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
.admin-monitoring-page { display: grid; gap: 16px; padding: 16px; max-width: 1100px; margin: 0 auto; }
|
||||||
|
.card { display: grid; gap: 12px; padding: 16px; border: 1px solid var(--border-color, #d3dad9); border-radius: 16px; background: #fff; }
|
||||||
|
.card h2 { margin: 0; font-size: 1.1rem; }
|
||||||
|
.health-grid { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||||
|
.queue-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
|
||||||
|
.queue-item { border: 1px solid var(--border-color, #d3dad9); border-radius: 10px; padding: 10px; display: grid; gap: 4px; }
|
||||||
|
.filters { display: flex; flex-wrap: wrap; gap: 10px; }
|
||||||
|
select { min-height: 40px; padding: 0 10px; border: 1px solid var(--border-color, #d3dad9); border-radius: 10px; }
|
||||||
|
@media (max-width: 700px) { .queue-grid { grid-template-columns: 1fr; } .filters { flex-direction: column; align-items: stretch; } }
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { AdminMonitoringFacade } from '../facade/admin-monitoring.facade';
|
||||||
|
import { AdminDashboardFacade } from '../../dashboard/facade/admin-dashboard.facade';
|
||||||
|
import { TranslatePipe } from '../../../../i18n/translate.pipe';
|
||||||
|
import { InputComponent } from '../../../../shared/ui/input/input.component';
|
||||||
|
import { BadgeComponent } from '../../../../shared/ui/badge/badge.component';
|
||||||
|
import { TableComponent } from '../../../../shared/ui/table/table.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-admin-monitoring-page',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, FormsModule, TranslatePipe, InputComponent, BadgeComponent, TableComponent],
|
||||||
|
templateUrl: './admin-monitoring-page.component.html',
|
||||||
|
styleUrls: ['./admin-monitoring-page.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class AdminMonitoringPageComponent {
|
||||||
|
readonly facade = inject(AdminMonitoringFacade);
|
||||||
|
readonly dashboardFacade = inject(AdminDashboardFacade);
|
||||||
|
|
||||||
|
readonly categories = ['all', 'audit', 'security', 'login', 'failed_login', 'api', 'error', 'warning'] as const;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.facade.loadAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Observable, of } from 'rxjs';
|
||||||
|
import { delay } from 'rxjs/operators';
|
||||||
|
import {
|
||||||
|
AdminMonitoringCategory,
|
||||||
|
AdminMonitoringEvent,
|
||||||
|
AdminMonitoringEventFilters,
|
||||||
|
AdminMonitoringLevel,
|
||||||
|
AdminQueue,
|
||||||
|
AdminWebhookDelivery,
|
||||||
|
} from '../models/admin-monitoring.model';
|
||||||
|
|
||||||
|
const CATEGORIES: AdminMonitoringCategory[] = ['audit', 'security', 'login', 'failed_login', 'api', 'error', 'warning'];
|
||||||
|
const ACTORS = ['karen@dexar.market', 'anna@dexar.market', 'system', 'unknown'];
|
||||||
|
const EVENT_COUNT = 40;
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class AdminMonitoringLocalGateway {
|
||||||
|
private events: AdminMonitoringEvent[] | null = null;
|
||||||
|
|
||||||
|
loadEvents(filters: AdminMonitoringEventFilters): Observable<AdminMonitoringEvent[]> {
|
||||||
|
const all = this.ensureEvents();
|
||||||
|
const filtered = all
|
||||||
|
.filter(event => filters.category === 'all' || event.category === filters.category)
|
||||||
|
.filter(event => !filters.search || `${event.message} ${event.actor}`.toLowerCase().includes(filters.search.toLowerCase()))
|
||||||
|
.sort((left, right) => right.timestamp.localeCompare(left.timestamp));
|
||||||
|
return of(filtered).pipe(delay(50));
|
||||||
|
}
|
||||||
|
|
||||||
|
loadQueues(): Observable<AdminQueue[]> {
|
||||||
|
const queues: AdminQueue[] = [
|
||||||
|
{ name: 'order-notifications', depth: 3, status: 'healthy' },
|
||||||
|
{ name: 'media-processing', depth: 0, status: 'healthy' },
|
||||||
|
{ name: 'webhook-delivery', depth: 12, status: 'degraded' },
|
||||||
|
];
|
||||||
|
return of(queues).pipe(delay(50));
|
||||||
|
}
|
||||||
|
|
||||||
|
loadWebhooks(): Observable<AdminWebhookDelivery[]> {
|
||||||
|
return of(Array.from({ length: 8 }, (_, index) => ({
|
||||||
|
id: `webhook-${index + 1}`,
|
||||||
|
endpoint: index % 2 === 0 ? 'https://partner.example.com/orders' : 'https://partner.example.com/inventory',
|
||||||
|
event: index % 2 === 0 ? 'order.created' : 'inventory.updated',
|
||||||
|
status: index % 5 === 0 ? 'failed' : index % 4 === 0 ? 'pending' : 'delivered',
|
||||||
|
timestamp: new Date(Date.now() - index * 3 * 60 * 60 * 1000).toISOString(),
|
||||||
|
} as AdminWebhookDelivery))).pipe(delay(50));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ensureEvents(): AdminMonitoringEvent[] {
|
||||||
|
if (!this.events) {
|
||||||
|
this.events = Array.from({ length: EVENT_COUNT }, (_, index) => this.seedEvent(index));
|
||||||
|
}
|
||||||
|
return this.events;
|
||||||
|
}
|
||||||
|
|
||||||
|
private seedEvent(index: number): AdminMonitoringEvent {
|
||||||
|
const category = CATEGORIES[index % CATEGORIES.length];
|
||||||
|
const level: AdminMonitoringLevel = category === 'error' || category === 'failed_login' ? 'error' : category === 'warning' ? 'warning' : 'info';
|
||||||
|
return {
|
||||||
|
id: `event-${index + 1}`,
|
||||||
|
category,
|
||||||
|
level,
|
||||||
|
message: this.messageFor(category, index),
|
||||||
|
actor: ACTORS[index % ACTORS.length],
|
||||||
|
timestamp: new Date(Date.now() - index * 45 * 60 * 1000).toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private messageFor(category: AdminMonitoringCategory, index: number): string {
|
||||||
|
switch (category) {
|
||||||
|
case 'audit': return `Admin action recorded (#${index})`;
|
||||||
|
case 'security': return `Security policy check passed (#${index})`;
|
||||||
|
case 'login': return `Successful admin login (#${index})`;
|
||||||
|
case 'failed_login': return `Failed login attempt (#${index})`;
|
||||||
|
case 'api': return `GET /api/products responded 200 in ${80 + index}ms`;
|
||||||
|
case 'error': return `Unhandled exception in checkout flow (#${index})`;
|
||||||
|
case 'warning': return `Slow query detected (#${index})`;
|
||||||
|
default: return `Event #${index}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user