From a67ea17ad2750111992232fba8e7f7f0f0498755 Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Wed, 15 Jul 2026 11:12:47 +0400 Subject: [PATCH] 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 --- docs/ADMIN.md | 22 +++++ docs/BACKEND.md | 8 ++ docs/SPRINT-PLAN.md | 5 +- src/app/app.routes.ts | 4 + .../facade/admin-dashboard.facade.ts | 1 + .../dashboard/models/admin-dashboard.model.ts | 2 + .../facade/admin-monitoring.facade.ts | 27 +++++++ .../models/admin-monitoring.model.ts | 32 ++++++++ .../admin-monitoring-page.component.html | 81 +++++++++++++++++++ .../admin-monitoring-page.component.scss | 9 +++ .../pages/admin-monitoring-page.component.ts | 28 +++++++ .../admin-monitoring-local.gateway.ts | 81 +++++++++++++++++++ 12 files changed, 298 insertions(+), 2 deletions(-) create mode 100644 src/app/features/admin/monitoring/facade/admin-monitoring.facade.ts create mode 100644 src/app/features/admin/monitoring/models/admin-monitoring.model.ts create mode 100644 src/app/features/admin/monitoring/pages/admin-monitoring-page.component.html create mode 100644 src/app/features/admin/monitoring/pages/admin-monitoring-page.component.scss create mode 100644 src/app/features/admin/monitoring/pages/admin-monitoring-page.component.ts create mode 100644 src/app/features/admin/monitoring/services/admin-monitoring-local.gateway.ts diff --git a/docs/ADMIN.md b/docs/ADMIN.md index c25c1aa..749ca7c 100644 --- a/docs/ADMIN.md +++ b/docs/ADMIN.md @@ -369,6 +369,28 @@ routes at all, this is a net-new admin section. - Wired into `AdminDashboardFacade`'s Quick Actions list (`dashboard.actionUsers` -> `/​: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 - **Dashboard metrics endpoint.** Categories/Products counts are computed diff --git a/docs/BACKEND.md b/docs/BACKEND.md index 464be20..6189976 100644 --- a/docs/BACKEND.md +++ b/docs/BACKEND.md @@ -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. +## 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 ### Production 502/504 Bad Gateway on refresh / back-navigation diff --git a/docs/SPRINT-PLAN.md b/docs/SPRINT-PLAN.md index 84ce4cb..421a1cd 100644 --- a/docs/SPRINT-PLAN.md +++ b/docs/SPRINT-PLAN.md @@ -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 - Commit: `feat(admin): users and permissions` -## Sprint 26 — Monitoring -- [ ] Audit/security/login logs views (local), API/error/warning feed, queue/webhook placeholders, health page +## Sprint 26 — Monitoring ✅ done +- [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` ## Sprint 27 — Analytics diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 8b286eb..14113ca 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -112,6 +112,10 @@ const coreRoutes: Routes = [ path: 'users', 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' } ] }, diff --git a/src/app/features/admin/dashboard/facade/admin-dashboard.facade.ts b/src/app/features/admin/dashboard/facade/admin-dashboard.facade.ts index b38d9da..2acd828 100644 --- a/src/app/features/admin/dashboard/facade/admin-dashboard.facade.ts +++ b/src/app/features/admin/dashboard/facade/admin-dashboard.facade.ts @@ -19,6 +19,7 @@ const QUICK_ACTIONS: AdminDashboardQuickAction[] = [ { id: 'orders', labelKey: 'dashboard.actionOrders', route: ['backoffice', 'orders'] }, { id: 'media-library', labelKey: 'dashboard.actionMediaLibrary', route: ['backoffice', 'media'] }, { id: 'users', labelKey: 'dashboard.actionUsers', route: ['backoffice', 'users'] }, + { id: 'monitoring', labelKey: 'dashboard.actionMonitoring', route: ['backoffice', 'monitoring'] }, { id: 'preview-marketplace', labelKey: 'dashboard.actionPreviewMarketplace', route: [''] }, ]; diff --git a/src/app/features/admin/dashboard/models/admin-dashboard.model.ts b/src/app/features/admin/dashboard/models/admin-dashboard.model.ts index b7c8013..c9f4797 100644 --- a/src/app/features/admin/dashboard/models/admin-dashboard.model.ts +++ b/src/app/features/admin/dashboard/models/admin-dashboard.model.ts @@ -18,6 +18,8 @@ export type AdminDashboardQuickActionId = | 'transactions' | 'orders' | 'media-library' + | 'users' + | 'monitoring' | 'preview-marketplace'; export interface AdminDashboardQuickAction { diff --git a/src/app/features/admin/monitoring/facade/admin-monitoring.facade.ts b/src/app/features/admin/monitoring/facade/admin-monitoring.facade.ts new file mode 100644 index 0000000..b7c1acb --- /dev/null +++ b/src/app/features/admin/monitoring/facade/admin-monitoring.facade.ts @@ -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({ category: 'all', search: '' }); + readonly events = signal([]); + readonly queues = signal([]); + readonly webhooks = signal([]); + 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): void { + this.filters.update(current => ({ ...current, ...patch })); + this.gateway.loadEvents(this.filters()).pipe(take(1)).subscribe(events => this.events.set(events)); + } +} diff --git a/src/app/features/admin/monitoring/models/admin-monitoring.model.ts b/src/app/features/admin/monitoring/models/admin-monitoring.model.ts new file mode 100644 index 0000000..ce6dfe8 --- /dev/null +++ b/src/app/features/admin/monitoring/models/admin-monitoring.model.ts @@ -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; +} diff --git a/src/app/features/admin/monitoring/pages/admin-monitoring-page.component.html b/src/app/features/admin/monitoring/pages/admin-monitoring-page.component.html new file mode 100644 index 0000000..46ae0c5 --- /dev/null +++ b/src/app/features/admin/monitoring/pages/admin-monitoring-page.component.html @@ -0,0 +1,81 @@ +
+
+

{{ 'adminMonitoring.health' | translate }}

+
+ @for (check of dashboardFacade.healthChecks(); track check.code) { + {{ check.labelKey | translate }} + } +
+
+ +
+

{{ 'adminMonitoring.queues' | translate }}

+
+ @for (queue of facade.queues(); track queue.name) { +
+ {{ queue.name }} + {{ 'adminMonitoring.depth' | translate }}: {{ queue.depth }} + {{ ('adminMonitoring.queueStatus.' + queue.status) | translate }} +
+ } +
+
+ +
+

{{ 'adminMonitoring.webhooks' | translate }}

+ + + + {{ 'adminMonitoring.endpoint' | translate }} + {{ 'adminMonitoring.event' | translate }} + {{ 'backoffice.status' | translate }} + {{ 'adminOrders.createdAt' | translate }} + + + + @for (delivery of facade.webhooks(); track delivery.id) { + + {{ delivery.endpoint }} + {{ delivery.event }} + {{ ('adminMonitoring.webhookStatus.' + delivery.status) | translate }} + {{ delivery.timestamp | date:'short' }} + + } + + +
+ +
+

{{ 'adminMonitoring.events' | translate }}

+
+ + +
+ + + + {{ 'adminMonitoring.category' | translate }} + {{ 'adminMonitoring.level' | translate }} + {{ 'adminMonitoring.message' | translate }} + {{ 'adminUsers.name' | translate }} + {{ 'adminOrders.createdAt' | translate }} + + + + @for (event of facade.events(); track event.id) { + + {{ ('adminMonitoring.category.' + event.category) | translate }} + {{ event.level }} + {{ event.message }} + {{ event.actor }} + {{ event.timestamp | date:'short' }} + + } + + +
+
diff --git a/src/app/features/admin/monitoring/pages/admin-monitoring-page.component.scss b/src/app/features/admin/monitoring/pages/admin-monitoring-page.component.scss new file mode 100644 index 0000000..81fc390 --- /dev/null +++ b/src/app/features/admin/monitoring/pages/admin-monitoring-page.component.scss @@ -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; } } diff --git a/src/app/features/admin/monitoring/pages/admin-monitoring-page.component.ts b/src/app/features/admin/monitoring/pages/admin-monitoring-page.component.ts new file mode 100644 index 0000000..a21b6d8 --- /dev/null +++ b/src/app/features/admin/monitoring/pages/admin-monitoring-page.component.ts @@ -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(); + } +} diff --git a/src/app/features/admin/monitoring/services/admin-monitoring-local.gateway.ts b/src/app/features/admin/monitoring/services/admin-monitoring-local.gateway.ts new file mode 100644 index 0000000..1b31f5e --- /dev/null +++ b/src/app/features/admin/monitoring/services/admin-monitoring-local.gateway.ts @@ -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 { + 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 { + 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 { + 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}`; + } + } +}