feat(diagnostics): add health engine
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { languageGuard } from './guards/language.guard';
|
||||
import { environment } from '../environments/environment';
|
||||
|
||||
// Core routes (same across all brands)
|
||||
const coreRoutes: Routes = [
|
||||
@@ -67,6 +68,10 @@ const cmsContentRoutes: Routes = [];
|
||||
|
||||
// All routes sit under a :lang prefix (e.g. /ru/cart, /en/product/5)
|
||||
export const routes: Routes = [
|
||||
...(environment.production ? [] : [{
|
||||
path: '__diagnostics',
|
||||
loadComponent: () => import('./features/diagnostics/components/diagnostics-page.component').then(m => m.DiagnosticsPageComponent)
|
||||
}]),
|
||||
{
|
||||
path: ':lang',
|
||||
canActivate: [languageGuard],
|
||||
|
||||
@@ -10,7 +10,16 @@ export interface UnknownWidgetDiagnostic {
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class RuntimeDiagnosticsService {
|
||||
private readonly unknownWidgetEvents: UnknownWidgetDiagnostic[] = [];
|
||||
|
||||
logUnknownWidget(event: UnknownWidgetDiagnostic): void {
|
||||
void event;
|
||||
this.unknownWidgetEvents.unshift(event);
|
||||
if (this.unknownWidgetEvents.length > 100) {
|
||||
this.unknownWidgetEvents.length = 100;
|
||||
}
|
||||
}
|
||||
|
||||
getUnknownWidgetEvents(): UnknownWidgetDiagnostic[] {
|
||||
return [...this.unknownWidgetEvents];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
@if (!devMode) {
|
||||
<main class="diagnostics-page">
|
||||
<section class="diagnostics-card">
|
||||
<h1>{{ 'diagnostics.disabledTitle' | translate }}</h1>
|
||||
<p>{{ 'diagnostics.disabledDescription' | translate }}</p>
|
||||
</section>
|
||||
</main>
|
||||
} @else {
|
||||
<main class="diagnostics-page">
|
||||
@if (report$ | async; as report) {
|
||||
<section class="diagnostics-hero diagnostics-card">
|
||||
<div>
|
||||
<h1>{{ 'diagnostics.title' | translate }}</h1>
|
||||
<p>{{ 'diagnostics.generatedAt' | translate:{ value: report.generatedAt } }}</p>
|
||||
</div>
|
||||
<div class="health-score">
|
||||
<strong>{{ report.summary.score }}%</strong>
|
||||
<span>{{ 'diagnostics.healthScore' | translate }}</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="diagnostics-grid">
|
||||
<article class="diagnostics-card summary-card">
|
||||
<h2>{{ 'diagnostics.summary' | translate }}</h2>
|
||||
<ul>
|
||||
<li>{{ 'diagnostics.passedChecks' | translate:{ count: report.summary.passedChecks } }}</li>
|
||||
<li>{{ 'diagnostics.warnings' | translate:{ count: report.summary.warningCount } }}</li>
|
||||
<li>{{ 'diagnostics.errors' | translate:{ count: report.summary.errorCount } }}</li>
|
||||
<li>{{ 'diagnostics.critical' | translate:{ count: report.summary.criticalCount } }}</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article class="diagnostics-card">
|
||||
<h2>{{ 'diagnostics.healthChecks' | translate }}</h2>
|
||||
<ul class="check-list">
|
||||
@for (check of report.checks; track check.key) {
|
||||
<li [class.failed]="!check.passed">
|
||||
<strong>{{ ('diagnostics.checks.' + check.key) | translate }}</strong>
|
||||
<span>{{ check.passed ? ('diagnostics.ok' | translate) : ('diagnostics.failed' | translate) }}</span>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="diagnostics-card">
|
||||
<h2>{{ 'diagnostics.results' | translate }}</h2>
|
||||
@if (report.entries.length === 0) {
|
||||
<p>{{ 'diagnostics.noIssues' | translate }}</p>
|
||||
} @else {
|
||||
<div class="entries">
|
||||
@for (entry of report.entries; track entry.code + entry.affectedComponent + entry.description) {
|
||||
<article class="entry" [attr.data-severity]="entry.severity">
|
||||
<div class="entry-head">
|
||||
<span class="severity">{{ ('diagnostics.severity.' + entry.severity) | translate }}</span>
|
||||
<code>{{ entry.code }}</code>
|
||||
</div>
|
||||
<h3>{{ tDiagnostic(entry.title) }}</h3>
|
||||
<p>{{ tDiagnostic(entry.description) }}</p>
|
||||
<p><strong>{{ 'diagnostics.affectedComponent' | translate }}:</strong> {{ entry.affectedComponent }}</p>
|
||||
<p><strong>{{ 'diagnostics.suggestedResolution' | translate }}:</strong> {{ tDiagnostic(entry.suggestedResolution) }}</p>
|
||||
</article>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
</main>
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
.diagnostics-page {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 24px;
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.diagnostics-card {
|
||||
background: #fff;
|
||||
border: 1px solid #d9e2e1;
|
||||
border-radius: 16px;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.diagnostics-hero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.health-score {
|
||||
display: grid;
|
||||
justify-items: end;
|
||||
}
|
||||
|
||||
.health-score strong {
|
||||
font-size: 2rem;
|
||||
color: #1e3c38;
|
||||
}
|
||||
|
||||
.diagnostics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.summary-card ul,
|
||||
.check-list {
|
||||
margin: 0;
|
||||
padding-left: 18px;
|
||||
}
|
||||
|
||||
.check-list li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.check-list li.failed {
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.entries {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.entry {
|
||||
border: 1px solid #d9e2e1;
|
||||
border-radius: 12px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.entry[data-severity='critical'] {
|
||||
border-color: #991b1b;
|
||||
}
|
||||
|
||||
.entry[data-severity='error'] {
|
||||
border-color: #c2410c;
|
||||
}
|
||||
|
||||
.entry[data-severity='warning'] {
|
||||
border-color: #ca8a04;
|
||||
}
|
||||
|
||||
.entry-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.severity {
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.diagnostics-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.diagnostics-hero {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.health-score {
|
||||
justify-items: start;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
||||
import { TranslatePipe } from '../../../i18n/translate.pipe';
|
||||
import { TranslateService } from '../../../i18n/translate.service';
|
||||
import { DiagnosticsFacade } from '../facade/diagnostics.facade';
|
||||
|
||||
@Component({
|
||||
selector: 'app-diagnostics-page',
|
||||
standalone: true,
|
||||
imports: [AsyncPipe, TranslatePipe],
|
||||
templateUrl: './diagnostics-page.component.html',
|
||||
styleUrls: ['./diagnostics-page.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class DiagnosticsPageComponent {
|
||||
private readonly diagnosticsFacade = inject(DiagnosticsFacade);
|
||||
private readonly translate = inject(TranslateService);
|
||||
|
||||
readonly report$ = this.diagnosticsFacade.loadReport();
|
||||
readonly devMode = this.diagnosticsFacade.devMode;
|
||||
|
||||
tDiagnostic(value: string): string {
|
||||
const [key, param] = value.split(':');
|
||||
return key.startsWith('diagnostics.')
|
||||
? this.translate.t(key, param ? { value: param } : undefined)
|
||||
: value;
|
||||
}
|
||||
}
|
||||
105
src/app/features/diagnostics/facade/diagnostics.facade.ts
Normal file
105
src/app/features/diagnostics/facade/diagnostics.facade.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { Injectable, computed, inject } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { forkJoin, map, of } from 'rxjs';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { ConfigService } from '../../../core/config/config.service';
|
||||
import { TenantResolverService } from '../../../core/config/tenant-resolver.service';
|
||||
import { RuntimeProviderStrategyService } from '../../../core/providers/runtime-provider-strategy.service';
|
||||
import { PlatformRuntimeStateService } from '../../../core/runtime/platform-runtime-state.service';
|
||||
import { RuntimeDiagnosticsService } from '../../../core/runtime/runtime-diagnostics.service';
|
||||
import { TranslateService } from '../../../i18n/translate.service';
|
||||
import { WidgetManifestService } from '../../../widgets/registry/widget-manifest.service';
|
||||
import { WidgetRegistryService } from '../../../widgets/registry/widget-registry.service';
|
||||
import { DiagnosticsReport } from '../models/diagnostics.model';
|
||||
import { DiagnosticsLoggerService } from '../services/diagnostics-logger.service';
|
||||
import { BootstrapDiagnosticsValidator } from '../validators/bootstrap-diagnostics.validator';
|
||||
import { calculateHealthScore } from '../validators/diagnostics-health-score.util';
|
||||
import { RuntimeDiagnosticsValidator } from '../validators/runtime-diagnostics.validator';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class DiagnosticsFacade {
|
||||
private readonly configService = inject(ConfigService);
|
||||
private readonly tenantResolver = inject(TenantResolverService);
|
||||
private readonly runtimeState = inject(PlatformRuntimeStateService);
|
||||
private readonly runtimeDiagnostics = inject(RuntimeDiagnosticsService);
|
||||
private readonly translate = inject(TranslateService);
|
||||
private readonly widgetManifest = inject(WidgetManifestService);
|
||||
private readonly widgetRegistry = inject(WidgetRegistryService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly strategy = inject(RuntimeProviderStrategyService);
|
||||
private readonly logger = inject(DiagnosticsLoggerService);
|
||||
private readonly document = inject(DOCUMENT);
|
||||
|
||||
readonly devMode = !environment.production;
|
||||
readonly logs = this.logger.logs;
|
||||
readonly availableRoutes = computed(() => this.collectAvailableRoutes());
|
||||
|
||||
loadReport() {
|
||||
const bootstrapValidator = new BootstrapDiagnosticsValidator();
|
||||
const runtimeValidator = new RuntimeDiagnosticsValidator(
|
||||
this.runtimeState,
|
||||
this.runtimeDiagnostics,
|
||||
this.strategy,
|
||||
this.router,
|
||||
this.document
|
||||
);
|
||||
|
||||
return forkJoin({
|
||||
bootstrap: this.configService.loadBootstrap().pipe(map(value => value), map(value => value)),
|
||||
manifest: this.widgetManifest.getManifest().pipe(map(value => value)),
|
||||
}).pipe(
|
||||
map(({ bootstrap, manifest }) => {
|
||||
const entries = [
|
||||
...bootstrapValidator.validate(bootstrap, manifest, this.collectAvailableRoutes(), this.translate),
|
||||
...runtimeValidator.validate(bootstrap, manifest),
|
||||
];
|
||||
|
||||
for (const entry of entries) {
|
||||
this.logger.log(entry);
|
||||
}
|
||||
|
||||
const checks = [
|
||||
{ key: 'bootstrapLoaded', passed: !!bootstrap, severity: 'critical' as const },
|
||||
{ key: 'tenantResolved', passed: this.tenantResolver.getTenantKey().trim().length > 0, severity: 'error' as const },
|
||||
{ key: 'runtimeInitialized', passed: this.runtimeState.initialized(), severity: 'critical' as const },
|
||||
{ key: 'themeLoaded', passed: !!bootstrap?.theme?.themeId && this.document.documentElement.getAttribute('data-theme-id')?.length! > 0, severity: 'warning' as const },
|
||||
{ key: 'widgetManifestLoaded', passed: (manifest?.widgets?.length ?? 0) > 0, severity: 'warning' as const },
|
||||
{ key: 'sectionEngineInitialized', passed: true, severity: 'info' as const },
|
||||
{ key: 'configurationEngineInitialized', passed: !!this.configService.getBootstrapSnapshot(), severity: 'error' as const },
|
||||
{ key: 'translationResourcesAvailable', passed: this.translate.t('app.pageTitle') !== 'app.pageTitle', severity: 'warning' as const },
|
||||
{ key: 'requiredAssetsAvailable', passed: Array.from(this.document.images).every(image => !image.complete || image.naturalWidth > 0), severity: 'warning' as const },
|
||||
];
|
||||
|
||||
const summary = {
|
||||
score: calculateHealthScore(entries),
|
||||
passedChecks: checks.filter(check => check.passed).length,
|
||||
infoCount: entries.filter(entry => entry.severity === 'info').length,
|
||||
warningCount: entries.filter(entry => entry.severity === 'warning').length,
|
||||
errorCount: entries.filter(entry => entry.severity === 'error').length,
|
||||
criticalCount: entries.filter(entry => entry.severity === 'critical').length,
|
||||
};
|
||||
|
||||
return {
|
||||
generatedAt: new Date().toISOString(),
|
||||
summary,
|
||||
checks,
|
||||
entries,
|
||||
} satisfies DiagnosticsReport;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private collectAvailableRoutes(): string[] {
|
||||
const visit = (routes: any[], prefix = ''): string[] => routes.flatMap(route => {
|
||||
const current = route.path ? `${prefix}/${route.path}`.replace(/\/+/g, '/').replace(/\/$/, '') || '/' : prefix || '/';
|
||||
const children = route.children ? visit(route.children, current === '/' ? '' : current) : [];
|
||||
return [current || '/', ...children];
|
||||
});
|
||||
|
||||
return visit(this.router.config)
|
||||
.map(route => route.startsWith('/:lang') ? route.slice(6) || '/' : route)
|
||||
.map(route => route === '' ? '/' : route)
|
||||
.filter((route, index, source) => source.indexOf(route) === index);
|
||||
}
|
||||
}
|
||||
26
src/app/features/diagnostics/models/diagnostics.model.ts
Normal file
26
src/app/features/diagnostics/models/diagnostics.model.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export type DiagnosticSeverity = 'info' | 'warning' | 'error' | 'critical';
|
||||
|
||||
export interface DiagnosticEntry {
|
||||
code: string;
|
||||
severity: DiagnosticSeverity;
|
||||
title: string;
|
||||
description: string;
|
||||
affectedComponent: string;
|
||||
suggestedResolution: string;
|
||||
}
|
||||
|
||||
export interface DiagnosticsHealthSummary {
|
||||
score: number;
|
||||
passedChecks: number;
|
||||
infoCount: number;
|
||||
warningCount: number;
|
||||
errorCount: number;
|
||||
criticalCount: number;
|
||||
}
|
||||
|
||||
export interface DiagnosticsReport {
|
||||
generatedAt: string;
|
||||
summary: DiagnosticsHealthSummary;
|
||||
checks: Array<{ key: string; passed: boolean; severity: DiagnosticSeverity }>;
|
||||
entries: DiagnosticEntry[];
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Injectable, signal } from '@angular/core';
|
||||
import { DiagnosticEntry } from '../models/diagnostics.model';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class DiagnosticsLoggerService {
|
||||
private readonly logsState = signal<DiagnosticEntry[]>([]);
|
||||
|
||||
readonly logs = this.logsState.asReadonly();
|
||||
|
||||
log(entry: DiagnosticEntry): void {
|
||||
this.logsState.update(current => [entry, ...current].slice(0, 300));
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.logsState.set([]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
import { TranslateService } from '../../../i18n/translate.service';
|
||||
import { BootstrapConfig } from '../../../shared/models/config';
|
||||
import { WidgetManifestFile } from '../../../widgets/contracts/widget-manifest.contract';
|
||||
import { DiagnosticEntry } from '../models/diagnostics.model';
|
||||
|
||||
const KNOWN_LAYOUTS = new Set(['default', 'sidebar-left', 'carousel-home', 'minimal']);
|
||||
|
||||
export class BootstrapDiagnosticsValidator {
|
||||
validate(
|
||||
bootstrap: BootstrapConfig | null,
|
||||
manifest: WidgetManifestFile | null,
|
||||
availableRoutes: string[],
|
||||
translate: TranslateService
|
||||
): DiagnosticEntry[] {
|
||||
const entries: DiagnosticEntry[] = [];
|
||||
|
||||
if (!bootstrap) {
|
||||
entries.push({
|
||||
code: 'BOOTSTRAP_MISSING',
|
||||
severity: 'critical',
|
||||
title: 'diagnostics.bootstrapMissingTitle',
|
||||
description: 'diagnostics.bootstrapMissingDescription',
|
||||
affectedComponent: 'ConfigService',
|
||||
suggestedResolution: 'diagnostics.bootstrapMissingResolution'
|
||||
});
|
||||
return entries;
|
||||
}
|
||||
|
||||
this.validateRequiredProperties(bootstrap, entries);
|
||||
this.validateUnknownWidgetTypes(bootstrap, manifest, entries);
|
||||
this.validateDuplicateIds(bootstrap, entries);
|
||||
this.validateLayouts(bootstrap, entries);
|
||||
this.validateFeatureFlags(bootstrap, entries);
|
||||
this.validatePages(bootstrap, entries);
|
||||
this.validateNavigationTargets(bootstrap, entries, availableRoutes);
|
||||
this.validateMissingTranslations(bootstrap, entries, translate);
|
||||
this.validateMissingMedia(bootstrap, entries);
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private validateRequiredProperties(bootstrap: BootstrapConfig, entries: DiagnosticEntry[]): void {
|
||||
const requiredPaths: Array<[string, unknown]> = [
|
||||
['schemaVersion', bootstrap.schemaVersion],
|
||||
['tenant.id', bootstrap.tenant?.id],
|
||||
['tenant.slug', bootstrap.tenant?.slug],
|
||||
['theme.themeId', bootstrap.theme?.themeId],
|
||||
['branding.logoUrl', bootstrap.branding?.logoUrl],
|
||||
['navigation.header', bootstrap.navigation?.header],
|
||||
['pages', bootstrap.pages],
|
||||
];
|
||||
|
||||
for (const [path, value] of requiredPaths) {
|
||||
const missing = value == null || (Array.isArray(value) && value.length === 0) || (typeof value === 'string' && value.trim().length === 0);
|
||||
if (missing) {
|
||||
entries.push({
|
||||
code: 'BOOTSTRAP_REQUIRED_PROPERTY_MISSING',
|
||||
severity: 'error',
|
||||
title: 'diagnostics.requiredPropertyMissingTitle',
|
||||
description: `diagnostics.requiredPropertyMissingDescription:${path}`,
|
||||
affectedComponent: 'BootstrapConfig',
|
||||
suggestedResolution: 'diagnostics.requiredPropertyMissingResolution'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private validateUnknownWidgetTypes(bootstrap: BootstrapConfig, manifest: WidgetManifestFile | null, entries: DiagnosticEntry[]): void {
|
||||
const known = new Set((manifest?.widgets ?? []).map(widget => widget.type));
|
||||
for (const page of bootstrap.pages ?? []) {
|
||||
for (const section of page.sections ?? []) {
|
||||
for (const widget of section.widgets ?? []) {
|
||||
if (known.size > 0 && !known.has(widget.type)) {
|
||||
entries.push({
|
||||
code: 'BOOTSTRAP_UNKNOWN_WIDGET_TYPE',
|
||||
severity: 'error',
|
||||
title: 'diagnostics.unknownWidgetTypeTitle',
|
||||
description: `diagnostics.unknownWidgetTypeDescription:${widget.type}`,
|
||||
affectedComponent: `page:${page.key}/section:${section.id}`,
|
||||
suggestedResolution: 'diagnostics.unknownWidgetTypeResolution'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private validateDuplicateIds(bootstrap: BootstrapConfig, entries: DiagnosticEntry[]): void {
|
||||
const pageIds = new Set<string>();
|
||||
|
||||
for (const page of bootstrap.pages ?? []) {
|
||||
if (pageIds.has(page.id)) {
|
||||
entries.push({
|
||||
code: 'BOOTSTRAP_DUPLICATE_PAGE_ID',
|
||||
severity: 'error',
|
||||
title: 'diagnostics.duplicateIdTitle',
|
||||
description: `diagnostics.duplicatePageIdDescription:${page.id}`,
|
||||
affectedComponent: `page:${page.key}`,
|
||||
suggestedResolution: 'diagnostics.duplicateIdResolution'
|
||||
});
|
||||
}
|
||||
pageIds.add(page.id);
|
||||
|
||||
const sectionIds = new Set<string>();
|
||||
for (const section of page.sections ?? []) {
|
||||
if (sectionIds.has(section.id)) {
|
||||
entries.push({
|
||||
code: 'BOOTSTRAP_DUPLICATE_SECTION_ID',
|
||||
severity: 'error',
|
||||
title: 'diagnostics.duplicateIdTitle',
|
||||
description: `diagnostics.duplicateSectionIdDescription:${section.id}`,
|
||||
affectedComponent: `page:${page.key}`,
|
||||
suggestedResolution: 'diagnostics.duplicateIdResolution'
|
||||
});
|
||||
}
|
||||
sectionIds.add(section.id);
|
||||
|
||||
const widgetIds = new Set<string>();
|
||||
for (const widget of section.widgets ?? []) {
|
||||
if (widgetIds.has(widget.id)) {
|
||||
entries.push({
|
||||
code: 'BOOTSTRAP_DUPLICATE_WIDGET_ID',
|
||||
severity: 'error',
|
||||
title: 'diagnostics.duplicateIdTitle',
|
||||
description: `diagnostics.duplicateWidgetIdDescription:${widget.id}`,
|
||||
affectedComponent: `page:${page.key}/section:${section.id}`,
|
||||
suggestedResolution: 'diagnostics.duplicateIdResolution'
|
||||
});
|
||||
}
|
||||
widgetIds.add(widget.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private validateLayouts(bootstrap: BootstrapConfig, entries: DiagnosticEntry[]): void {
|
||||
for (const page of bootstrap.pages ?? []) {
|
||||
const layout = typeof page.layout === 'string' ? page.layout : page.layout?.type;
|
||||
if (!layout || !KNOWN_LAYOUTS.has(layout)) {
|
||||
entries.push({
|
||||
code: 'BOOTSTRAP_UNKNOWN_LAYOUT',
|
||||
severity: 'warning',
|
||||
title: 'diagnostics.unknownLayoutTitle',
|
||||
description: `diagnostics.unknownLayoutDescription:${layout ?? 'undefined'}`,
|
||||
affectedComponent: `page:${page.key}`,
|
||||
suggestedResolution: 'diagnostics.unknownLayoutResolution'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private validateFeatureFlags(bootstrap: BootstrapConfig, entries: DiagnosticEntry[]): void {
|
||||
const featureFlags = bootstrap.featureFlags ?? {};
|
||||
for (const [key, value] of Object.entries(featureFlags)) {
|
||||
if (typeof value !== 'boolean') {
|
||||
entries.push({
|
||||
code: 'BOOTSTRAP_INVALID_FEATURE_FLAG',
|
||||
severity: 'error',
|
||||
title: 'diagnostics.invalidFeatureFlagTitle',
|
||||
description: `diagnostics.invalidFeatureFlagDescription:${key}`,
|
||||
affectedComponent: 'featureFlags',
|
||||
suggestedResolution: 'diagnostics.invalidFeatureFlagResolution'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private validatePages(bootstrap: BootstrapConfig, entries: DiagnosticEntry[]): void {
|
||||
for (const page of bootstrap.pages ?? []) {
|
||||
const broken = !page.key || !page.title || !page.route?.path || !Array.isArray(page.sections);
|
||||
if (broken) {
|
||||
entries.push({
|
||||
code: 'BOOTSTRAP_BROKEN_PAGE_DEFINITION',
|
||||
severity: 'error',
|
||||
title: 'diagnostics.brokenPageDefinitionTitle',
|
||||
description: `diagnostics.brokenPageDefinitionDescription:${page.id}`,
|
||||
affectedComponent: `page:${page.id}`,
|
||||
suggestedResolution: 'diagnostics.brokenPageDefinitionResolution'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private validateNavigationTargets(bootstrap: BootstrapConfig, entries: DiagnosticEntry[], availableRoutes: string[]): void {
|
||||
const allowed = new Set(availableRoutes);
|
||||
|
||||
const validateItem = (route: string | undefined, location: string) => {
|
||||
if (!route) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalized = route.startsWith('/') ? route : `/${route}`;
|
||||
if (!allowed.has(normalized)) {
|
||||
entries.push({
|
||||
code: 'BOOTSTRAP_INVALID_NAVIGATION_TARGET',
|
||||
severity: 'warning',
|
||||
title: 'diagnostics.invalidNavigationTargetTitle',
|
||||
description: `diagnostics.invalidNavigationTargetDescription:${normalized}`,
|
||||
affectedComponent: location,
|
||||
suggestedResolution: 'diagnostics.invalidNavigationTargetResolution'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
for (const item of bootstrap.navigation?.header ?? []) {
|
||||
validateItem(item.route, `navigation.header:${item.id}`);
|
||||
for (const child of item.children ?? []) {
|
||||
validateItem(child.route, `navigation.header:${item.id}/${child.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const item of bootstrap.navigation?.footer ?? []) {
|
||||
if ('items' in item) {
|
||||
for (const child of item.items ?? []) {
|
||||
validateItem(child.route, `navigation.footer.group`);
|
||||
}
|
||||
} else {
|
||||
validateItem(item.route, `navigation.footer:${item.id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private validateMissingTranslations(bootstrap: BootstrapConfig, entries: DiagnosticEntry[], translate: TranslateService): void {
|
||||
for (const item of bootstrap.navigation?.header ?? []) {
|
||||
if (item.labelKey && translate.t(item.labelKey) === item.labelKey) {
|
||||
entries.push({
|
||||
code: 'BOOTSTRAP_MISSING_TRANSLATION',
|
||||
severity: 'warning',
|
||||
title: 'diagnostics.missingTranslationTitle',
|
||||
description: `diagnostics.missingTranslationDescription:${item.labelKey}`,
|
||||
affectedComponent: `navigation.header:${item.id}`,
|
||||
suggestedResolution: 'diagnostics.missingTranslationResolution'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private validateMissingMedia(bootstrap: BootstrapConfig, entries: DiagnosticEntry[]): void {
|
||||
if (!bootstrap.branding?.logoUrl) {
|
||||
entries.push({
|
||||
code: 'BOOTSTRAP_MISSING_MEDIA',
|
||||
severity: 'warning',
|
||||
title: 'diagnostics.missingMediaTitle',
|
||||
description: 'diagnostics.missingMediaDescription:branding.logoUrl',
|
||||
affectedComponent: 'branding',
|
||||
suggestedResolution: 'diagnostics.missingMediaResolution'
|
||||
});
|
||||
}
|
||||
|
||||
if (!bootstrap.branding?.faviconUrl) {
|
||||
entries.push({
|
||||
code: 'BOOTSTRAP_MISSING_MEDIA',
|
||||
severity: 'warning',
|
||||
title: 'diagnostics.missingMediaTitle',
|
||||
description: 'diagnostics.missingMediaDescription:branding.faviconUrl',
|
||||
affectedComponent: 'branding',
|
||||
suggestedResolution: 'diagnostics.missingMediaResolution'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { DiagnosticEntry } from '../models/diagnostics.model';
|
||||
|
||||
export function calculateHealthScore(entries: DiagnosticEntry[]): number {
|
||||
const penalties = entries.reduce((acc, entry) => {
|
||||
if (entry.severity === 'critical') return acc + 15;
|
||||
if (entry.severity === 'error') return acc + 8;
|
||||
if (entry.severity === 'warning') return acc + 3;
|
||||
return acc;
|
||||
}, 0);
|
||||
|
||||
return Math.max(0, 100 - penalties);
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { Inject } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { RuntimeProviderStrategyService } from '../../../core/providers/runtime-provider-strategy.service';
|
||||
import { PlatformRuntimeStateService } from '../../../core/runtime/platform-runtime-state.service';
|
||||
import { RuntimeDiagnosticsService } from '../../../core/runtime/runtime-diagnostics.service';
|
||||
import { BootstrapConfig } from '../../../shared/models/config';
|
||||
import { WidgetManifestFile } from '../../../widgets/contracts/widget-manifest.contract';
|
||||
import { DiagnosticEntry } from '../models/diagnostics.model';
|
||||
|
||||
export class RuntimeDiagnosticsValidator {
|
||||
constructor(
|
||||
private readonly runtimeState: PlatformRuntimeStateService,
|
||||
private readonly runtimeDiagnostics: RuntimeDiagnosticsService,
|
||||
private readonly strategy: RuntimeProviderStrategyService,
|
||||
private readonly router: Router,
|
||||
@Inject(DOCUMENT) private readonly document: Document
|
||||
) {}
|
||||
|
||||
validate(bootstrap: BootstrapConfig | null, manifest: WidgetManifestFile | null): DiagnosticEntry[] {
|
||||
const entries: DiagnosticEntry[] = [];
|
||||
|
||||
if (!this.runtimeState.initialized()) {
|
||||
entries.push({
|
||||
code: 'RUNTIME_NOT_INITIALIZED',
|
||||
severity: 'critical',
|
||||
title: 'diagnostics.runtimeNotInitializedTitle',
|
||||
description: 'diagnostics.runtimeNotInitializedDescription',
|
||||
affectedComponent: 'PlatformRuntimeService',
|
||||
suggestedResolution: 'diagnostics.runtimeNotInitializedResolution'
|
||||
});
|
||||
}
|
||||
|
||||
this.validateConfigFallbackUsage(entries);
|
||||
this.validateDatasourceCoverage(bootstrap, manifest, entries);
|
||||
this.validateWidgetFailures(entries);
|
||||
this.validateBrokenRoutes(entries);
|
||||
this.validateImageFailures(entries);
|
||||
this.validateMissingOptionalData(bootstrap, entries);
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private validateConfigFallbackUsage(entries: DiagnosticEntry[]): void {
|
||||
const mode = this.strategy.getBootstrapProviderMode();
|
||||
if (mode === 'mock' && !environment.production) {
|
||||
entries.push({
|
||||
code: 'RUNTIME_FALLBACK_USAGE',
|
||||
severity: 'info',
|
||||
title: 'diagnostics.configFallbackUsageTitle',
|
||||
description: 'diagnostics.configFallbackUsageDescription',
|
||||
affectedComponent: 'RuntimeProviderStrategyService',
|
||||
suggestedResolution: 'diagnostics.configFallbackUsageResolution'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private validateDatasourceCoverage(bootstrap: BootstrapConfig | null, manifest: WidgetManifestFile | null, entries: DiagnosticEntry[]): void {
|
||||
if (!bootstrap || !manifest) {
|
||||
return;
|
||||
}
|
||||
|
||||
const manifestByType = new Map(manifest.widgets.map(widget => [widget.type, widget]));
|
||||
|
||||
for (const page of bootstrap.pages ?? []) {
|
||||
for (const section of page.sections ?? []) {
|
||||
for (const widget of section.widgets ?? []) {
|
||||
const definition = manifestByType.get(widget.type);
|
||||
if (!definition) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const requiresDataSource = definition.supportedDataSources.length > 0;
|
||||
const hasDataSource = typeof widget.props?.['source'] === 'string' || typeof definition.defaultSettings?.['source'] === 'string';
|
||||
|
||||
if (requiresDataSource && !hasDataSource) {
|
||||
entries.push({
|
||||
code: 'RUNTIME_MISSING_DATASOURCE',
|
||||
severity: 'warning',
|
||||
title: 'diagnostics.missingDatasourceTitle',
|
||||
description: `diagnostics.missingDatasourceDescription:${widget.id}`,
|
||||
affectedComponent: `page:${page.key}/section:${section.id}`,
|
||||
suggestedResolution: 'diagnostics.missingDatasourceResolution'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private validateWidgetFailures(entries: DiagnosticEntry[]): void {
|
||||
for (const event of this.runtimeDiagnostics.getUnknownWidgetEvents()) {
|
||||
entries.push({
|
||||
code: 'RUNTIME_WIDGET_RENDER_FAILURE',
|
||||
severity: 'error',
|
||||
title: 'diagnostics.widgetRenderFailureTitle',
|
||||
description: `diagnostics.widgetRenderFailureDescription:${event.widget}`,
|
||||
affectedComponent: `page:${event.page}/section:${event.section}`,
|
||||
suggestedResolution: 'diagnostics.widgetRenderFailureResolution'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private validateBrokenRoutes(entries: DiagnosticEntry[]): void {
|
||||
const routePaths = this.router.config
|
||||
.flatMap(route => route.children?.length ? route.children : [route])
|
||||
.map(route => route.path)
|
||||
.filter((path): path is string => typeof path === 'string');
|
||||
|
||||
if (!routePaths.includes('search')) {
|
||||
entries.push({
|
||||
code: 'RUNTIME_BROKEN_ROUTE',
|
||||
severity: 'error',
|
||||
title: 'diagnostics.brokenRouteTitle',
|
||||
description: 'diagnostics.brokenRouteDescription:/search',
|
||||
affectedComponent: 'Router',
|
||||
suggestedResolution: 'diagnostics.brokenRouteResolution'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private validateImageFailures(entries: DiagnosticEntry[]): void {
|
||||
const failedImages = Array.from(this.document.images).filter(image => image.complete && image.naturalWidth === 0);
|
||||
for (const image of failedImages.slice(0, 10)) {
|
||||
entries.push({
|
||||
code: 'RUNTIME_IMAGE_LOAD_FAILED',
|
||||
severity: 'warning',
|
||||
title: 'diagnostics.imageLoadFailedTitle',
|
||||
description: `diagnostics.imageLoadFailedDescription:${image.currentSrc || image.src}`,
|
||||
affectedComponent: 'DOM',
|
||||
suggestedResolution: 'diagnostics.imageLoadFailedResolution'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private validateMissingOptionalData(bootstrap: BootstrapConfig | null, entries: DiagnosticEntry[]): void {
|
||||
if (!bootstrap) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!bootstrap.branding?.supportEmail) {
|
||||
entries.push({
|
||||
code: 'RUNTIME_OPTIONAL_DATA_MISSING',
|
||||
severity: 'info',
|
||||
title: 'diagnostics.optionalDataMissingTitle',
|
||||
description: 'diagnostics.optionalDataMissingDescription:branding.supportEmail',
|
||||
affectedComponent: 'branding',
|
||||
suggestedResolution: 'diagnostics.optionalDataMissingResolution'
|
||||
});
|
||||
}
|
||||
|
||||
if (!bootstrap.branding?.logoCompactUrl) {
|
||||
entries.push({
|
||||
code: 'RUNTIME_OPTIONAL_DATA_MISSING',
|
||||
severity: 'info',
|
||||
title: 'diagnostics.optionalDataMissingTitle',
|
||||
description: 'diagnostics.optionalDataMissingDescription:branding.logoCompactUrl',
|
||||
affectedComponent: 'branding',
|
||||
suggestedResolution: 'diagnostics.optionalDataMissingResolution'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -468,4 +468,93 @@ export const en: Translations = {
|
||||
compareSize: 'Size',
|
||||
compareUnknown: 'unknown',
|
||||
},
|
||||
diagnostics: {
|
||||
title: 'Marketplace Diagnostics',
|
||||
disabledTitle: 'Diagnostics unavailable',
|
||||
disabledDescription: 'Diagnostics page is available only in development mode.',
|
||||
generatedAt: 'Generated at {{value}}',
|
||||
healthScore: 'Health score',
|
||||
summary: 'Summary',
|
||||
passedChecks: '{{count}} passed checks',
|
||||
warnings: '{{count}} warnings',
|
||||
errors: '{{count}} errors',
|
||||
critical: '{{count}} critical',
|
||||
healthChecks: 'Health checks',
|
||||
results: 'Detailed results',
|
||||
noIssues: 'No diagnostics issues found.',
|
||||
ok: 'OK',
|
||||
failed: 'Failed',
|
||||
affectedComponent: 'Affected component',
|
||||
suggestedResolution: 'Suggested resolution',
|
||||
severity: {
|
||||
info: 'Info',
|
||||
warning: 'Warning',
|
||||
error: 'Error',
|
||||
critical: 'Critical',
|
||||
},
|
||||
checks: {
|
||||
bootstrapLoaded: 'Bootstrap loaded',
|
||||
tenantResolved: 'Tenant resolved',
|
||||
runtimeInitialized: 'Runtime initialized',
|
||||
themeLoaded: 'Theme loaded',
|
||||
widgetManifestLoaded: 'Widget manifest loaded',
|
||||
sectionEngineInitialized: 'Section engine reachable',
|
||||
configurationEngineInitialized: 'Configuration engine initialized',
|
||||
translationResourcesAvailable: 'Translations available',
|
||||
requiredAssetsAvailable: 'Required assets healthy',
|
||||
},
|
||||
bootstrapMissingTitle: 'Bootstrap missing',
|
||||
bootstrapMissingDescription: 'Bootstrap configuration could not be loaded.',
|
||||
bootstrapMissingResolution: 'Verify bootstrap provider response and local mock availability.',
|
||||
requiredPropertyMissingTitle: 'Required property missing',
|
||||
requiredPropertyMissingDescription: 'Required bootstrap property is missing.',
|
||||
requiredPropertyMissingResolution: 'Add missing property to bootstrap payload.',
|
||||
unknownWidgetTypeTitle: 'Unknown widget type',
|
||||
unknownWidgetTypeDescription: 'Widget type is not present in widget manifest.',
|
||||
unknownWidgetTypeResolution: 'Register widget in manifest or remove invalid widget config.',
|
||||
duplicateIdTitle: 'Duplicate identifier',
|
||||
duplicatePageIdDescription: 'Duplicate page id detected.',
|
||||
duplicateSectionIdDescription: 'Duplicate section id detected.',
|
||||
duplicateWidgetIdDescription: 'Duplicate widget id detected.',
|
||||
duplicateIdResolution: 'Ensure ids are unique within bootstrap config.',
|
||||
unknownLayoutTitle: 'Unknown layout',
|
||||
unknownLayoutDescription: 'Page layout value is not recognized.',
|
||||
unknownLayoutResolution: 'Use supported layout or extend diagnostics allow-list.',
|
||||
invalidFeatureFlagTitle: 'Invalid feature flag',
|
||||
invalidFeatureFlagDescription: 'Feature flag value must be boolean.',
|
||||
invalidFeatureFlagResolution: 'Normalize feature flag values to true or false.',
|
||||
brokenPageDefinitionTitle: 'Broken page definition',
|
||||
brokenPageDefinitionDescription: 'Page definition is incomplete.',
|
||||
brokenPageDefinitionResolution: 'Add missing route, title, key, or sections.',
|
||||
invalidNavigationTargetTitle: 'Invalid navigation target',
|
||||
invalidNavigationTargetDescription: 'Navigation route does not match known routes.',
|
||||
invalidNavigationTargetResolution: 'Update route target or add matching route surface.',
|
||||
missingTranslationTitle: 'Missing translation',
|
||||
missingTranslationDescription: 'Translation key resolves to itself.',
|
||||
missingTranslationResolution: 'Add missing key to translation resources.',
|
||||
missingMediaTitle: 'Missing media',
|
||||
missingMediaDescription: 'Expected branding media is missing.',
|
||||
missingMediaResolution: 'Provide valid media URLs in bootstrap branding config.',
|
||||
runtimeNotInitializedTitle: 'Runtime not initialized',
|
||||
runtimeNotInitializedDescription: 'Platform runtime state has not been marked initialized.',
|
||||
runtimeNotInitializedResolution: 'Verify PlatformRuntimeService initialize sequence.',
|
||||
configFallbackUsageTitle: 'Configuration fallback in use',
|
||||
configFallbackUsageDescription: 'Development runtime is using fallback or mock configuration.',
|
||||
configFallbackUsageResolution: 'Switch provider mode if real bootstrap validation is needed.',
|
||||
missingDatasourceTitle: 'Missing datasource',
|
||||
missingDatasourceDescription: 'Widget supports datasource selection but none was declared.',
|
||||
missingDatasourceResolution: 'Define source in widget props or manifest default settings.',
|
||||
widgetRenderFailureTitle: 'Widget render failure',
|
||||
widgetRenderFailureDescription: 'Widget fell back to unknown widget renderer.',
|
||||
widgetRenderFailureResolution: 'Register widget component or fix manifest component key.',
|
||||
brokenRouteTitle: 'Broken route expectation',
|
||||
brokenRouteDescription: 'Expected route surface is missing.',
|
||||
brokenRouteResolution: 'Restore route registration or update diagnostics expectation.',
|
||||
imageLoadFailedTitle: 'Image load failed',
|
||||
imageLoadFailedDescription: 'Rendered image failed to load.',
|
||||
imageLoadFailedResolution: 'Verify asset path, CORS, and media availability.',
|
||||
optionalDataMissingTitle: 'Optional data missing',
|
||||
optionalDataMissingDescription: 'Optional configuration data is absent.',
|
||||
optionalDataMissingResolution: 'Add optional value if UX depends on it.',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -468,4 +468,88 @@ export const hy: Translations = {
|
||||
compareSize: 'Չափ',
|
||||
compareUnknown: 'անհայտ',
|
||||
},
|
||||
diagnostics: {
|
||||
title: 'Մարքեթփլեյսի ախտորոշում',
|
||||
disabledTitle: 'Ախտորոշումը հասանելի չէ',
|
||||
disabledDescription: 'Ախտորոշման էջը հասանելի է միայն development ռեժիմում։',
|
||||
generatedAt: 'Ստեղծվել է {{value}}',
|
||||
healthScore: 'Առողջության գնահատական',
|
||||
summary: 'Ամփոփում',
|
||||
passedChecks: '{{count}} հաջող ստուգում',
|
||||
warnings: '{{count}} նախազգուշացում',
|
||||
errors: '{{count}} սխալ',
|
||||
critical: '{{count}} կրիտիկական',
|
||||
healthChecks: 'Առողջության ստուգումներ',
|
||||
results: 'Մանրամասն արդյունքներ',
|
||||
noIssues: 'Ախտորոշման խնդիրներ չեն գտնվել։',
|
||||
ok: 'Լավ',
|
||||
failed: 'Խափանվեց',
|
||||
affectedComponent: 'Վնասված բաղադրիչ',
|
||||
suggestedResolution: 'Առաջարկվող լուծում',
|
||||
severity: { info: 'Տեղեկ.', warning: 'Զգուշ.', error: 'Սխալ', critical: 'Կրիտիկական' },
|
||||
checks: {
|
||||
bootstrapLoaded: 'Bootstrap բեռնված է',
|
||||
tenantResolved: 'Tenant որոշված է',
|
||||
runtimeInitialized: 'Runtime-ը մեկնարկված է',
|
||||
themeLoaded: 'Թեման բեռնված է',
|
||||
widgetManifestLoaded: 'Widget manifest-ը բեռնված է',
|
||||
sectionEngineInitialized: 'Section Engine-ը հասանելի է',
|
||||
configurationEngineInitialized: 'Configuration Engine-ը մեկնարկված է',
|
||||
translationResourcesAvailable: 'Թարգմանությունները հասանելի են',
|
||||
requiredAssetsAvailable: 'Պարտադիր assets-ը հասանելի են',
|
||||
},
|
||||
bootstrapMissingTitle: 'Bootstrap-ը բացակայում է',
|
||||
bootstrapMissingDescription: 'Bootstrap կարգավորումը չի բեռնվել։',
|
||||
bootstrapMissingResolution: 'Ստուգեք bootstrap provider-ը և local mock-ը։',
|
||||
requiredPropertyMissingTitle: 'Պարտադիր հատկությունը բացակայում է',
|
||||
requiredPropertyMissingDescription: 'Bootstrap-ում պարտադիր հատկություն չկա։',
|
||||
requiredPropertyMissingResolution: 'Ավելացրեք հատկությունը bootstrap payload-ում։',
|
||||
unknownWidgetTypeTitle: 'Անհայտ widget type',
|
||||
unknownWidgetTypeDescription: 'Widget type-ը չկա widget manifest-ում։',
|
||||
unknownWidgetTypeResolution: 'Ավելացրեք widget-ը manifest-ում կամ հեռացրեք սխալ config-ը։',
|
||||
duplicateIdTitle: 'Կրկնվող ID',
|
||||
duplicatePageIdDescription: 'Գտնվել է կրկնվող page id։',
|
||||
duplicateSectionIdDescription: 'Գտնվել է կրկնվող section id։',
|
||||
duplicateWidgetIdDescription: 'Գտնվել է կրկնվող widget id։',
|
||||
duplicateIdResolution: 'Bootstrap config-ում ID-ները դարձրեք եզակի։',
|
||||
unknownLayoutTitle: 'Անհայտ layout',
|
||||
unknownLayoutDescription: 'Էջի layout արժեքը ճանաչված չէ։',
|
||||
unknownLayoutResolution: 'Օգտագործեք աջակցվող layout կամ ընդլայնեք allow-list-ը։',
|
||||
invalidFeatureFlagTitle: 'Սխալ feature flag',
|
||||
invalidFeatureFlagDescription: 'Feature flag արժեքը պետք է boolean լինի։',
|
||||
invalidFeatureFlagResolution: 'Նորմալացրեք արժեքը true կամ false։',
|
||||
brokenPageDefinitionTitle: 'Կոտրված page definition',
|
||||
brokenPageDefinitionDescription: 'Էջի նկարագրությունը թերի է։',
|
||||
brokenPageDefinitionResolution: 'Ավելացրեք route, title, key կամ sections։',
|
||||
invalidNavigationTargetTitle: 'Սխալ navigation target',
|
||||
invalidNavigationTargetDescription: 'Navigation route-ը չի համընկնում հայտնի route-ների հետ։',
|
||||
invalidNavigationTargetResolution: 'Թարմացրեք route target-ը կամ ավելացրեք route։',
|
||||
missingTranslationTitle: 'Թարգմանությունը բացակայում է',
|
||||
missingTranslationDescription: 'Թարգմանության key-ը չի թարգմանվում։',
|
||||
missingTranslationResolution: 'Ավելացրեք key-ը translation resources-ում։',
|
||||
missingMediaTitle: 'Media-ն բացակայում է',
|
||||
missingMediaDescription: 'Սպասվող branding media-ն բացակայում է։',
|
||||
missingMediaResolution: 'Տրամադրեք ճիշտ media URL-ներ branding config-ում։',
|
||||
runtimeNotInitializedTitle: 'Runtime-ը մեկնարկված չէ',
|
||||
runtimeNotInitializedDescription: 'Platform runtime state-ը initialized չէ։',
|
||||
runtimeNotInitializedResolution: 'Ստուգեք PlatformRuntimeService initialize շղթան։',
|
||||
configFallbackUsageTitle: 'Օգտագործվում է fallback config',
|
||||
configFallbackUsageDescription: 'Development runtime-ը օգտագործում է fallback կամ mock config։',
|
||||
configFallbackUsageResolution: 'Փոխեք provider mode-ը, եթե պետք է իրական bootstrap ստուգում։',
|
||||
missingDatasourceTitle: 'Datasource-ը բացակայում է',
|
||||
missingDatasourceDescription: 'Widget-ը աջակցում է datasource, բայց այն նշված չէ։',
|
||||
missingDatasourceResolution: 'Նշեք source widget props-ում կամ manifest default settings-ում։',
|
||||
widgetRenderFailureTitle: 'Widget render failure',
|
||||
widgetRenderFailureDescription: 'Widget-ը անցել է unknown widget renderer-ի։',
|
||||
widgetRenderFailureResolution: 'Գրանցեք widget component-ը կամ ուղղեք manifest component key-ը։',
|
||||
brokenRouteTitle: 'Կոտրված route սպասում',
|
||||
brokenRouteDescription: 'Սպասվող route-ը բացակայում է։',
|
||||
brokenRouteResolution: 'Վերականգնեք route registration-ը կամ թարմացրեք diagnostics կանոնը։',
|
||||
imageLoadFailedTitle: 'Նկարի բեռնումը ձախողվեց',
|
||||
imageLoadFailedDescription: 'Արտապատկերվող նկարը չի բեռնվել։',
|
||||
imageLoadFailedResolution: 'Ստուգեք asset path-ը, CORS-ը և media հասանելիությունը։',
|
||||
optionalDataMissingTitle: 'Optional data բացակայում է',
|
||||
optionalDataMissingDescription: 'Optional config արժեքը բացակայում է։',
|
||||
optionalDataMissingResolution: 'Ավելացրեք արժեքը, եթե UX-ը կախված է դրանից։',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -468,4 +468,88 @@ export const ru: Translations = {
|
||||
compareSize: 'Размер',
|
||||
compareUnknown: 'неизвестно',
|
||||
},
|
||||
diagnostics: {
|
||||
title: 'Диагностика маркетплейса',
|
||||
disabledTitle: 'Диагностика недоступна',
|
||||
disabledDescription: 'Страница диагностики доступна только в режиме разработки.',
|
||||
generatedAt: 'Сформировано: {{value}}',
|
||||
healthScore: 'Оценка состояния',
|
||||
summary: 'Сводка',
|
||||
passedChecks: '{{count}} успешных проверок',
|
||||
warnings: '{{count}} предупреждений',
|
||||
errors: '{{count}} ошибок',
|
||||
critical: '{{count}} критических',
|
||||
healthChecks: 'Проверки состояния',
|
||||
results: 'Подробные результаты',
|
||||
noIssues: 'Проблем диагностики не найдено.',
|
||||
ok: 'OK',
|
||||
failed: 'Ошибка',
|
||||
affectedComponent: 'Затронутый компонент',
|
||||
suggestedResolution: 'Рекомендация',
|
||||
severity: { info: 'Инфо', warning: 'Предупреждение', error: 'Ошибка', critical: 'Критично' },
|
||||
checks: {
|
||||
bootstrapLoaded: 'Bootstrap загружен',
|
||||
tenantResolved: 'Tenant определен',
|
||||
runtimeInitialized: 'Runtime инициализирован',
|
||||
themeLoaded: 'Тема загружена',
|
||||
widgetManifestLoaded: 'Манифест виджетов загружен',
|
||||
sectionEngineInitialized: 'Section Engine доступен',
|
||||
configurationEngineInitialized: 'Configuration Engine инициализирован',
|
||||
translationResourcesAvailable: 'Переводы доступны',
|
||||
requiredAssetsAvailable: 'Обязательные assets доступны',
|
||||
},
|
||||
bootstrapMissingTitle: 'Bootstrap отсутствует',
|
||||
bootstrapMissingDescription: 'Конфигурация bootstrap не загружена.',
|
||||
bootstrapMissingResolution: 'Проверьте provider bootstrap и локальный mock.',
|
||||
requiredPropertyMissingTitle: 'Отсутствует обязательное свойство',
|
||||
requiredPropertyMissingDescription: 'В bootstrap отсутствует обязательное свойство.',
|
||||
requiredPropertyMissingResolution: 'Добавьте свойство в bootstrap payload.',
|
||||
unknownWidgetTypeTitle: 'Неизвестный тип виджета',
|
||||
unknownWidgetTypeDescription: 'Тип виджета отсутствует в widget manifest.',
|
||||
unknownWidgetTypeResolution: 'Добавьте виджет в manifest или удалите неверную конфигурацию.',
|
||||
duplicateIdTitle: 'Дублирующийся идентификатор',
|
||||
duplicatePageIdDescription: 'Обнаружен дублирующийся id страницы.',
|
||||
duplicateSectionIdDescription: 'Обнаружен дублирующийся id секции.',
|
||||
duplicateWidgetIdDescription: 'Обнаружен дублирующийся id виджета.',
|
||||
duplicateIdResolution: 'Сделайте id уникальными в bootstrap конфигурации.',
|
||||
unknownLayoutTitle: 'Неизвестный layout',
|
||||
unknownLayoutDescription: 'Значение layout страницы не распознано.',
|
||||
unknownLayoutResolution: 'Используйте поддерживаемый layout или расширьте allow-list.',
|
||||
invalidFeatureFlagTitle: 'Некорректный feature flag',
|
||||
invalidFeatureFlagDescription: 'Значение feature flag должно быть boolean.',
|
||||
invalidFeatureFlagResolution: 'Нормализуйте значение к true или false.',
|
||||
brokenPageDefinitionTitle: 'Некорректное описание страницы',
|
||||
brokenPageDefinitionDescription: 'Описание страницы неполное.',
|
||||
brokenPageDefinitionResolution: 'Добавьте route, title, key или sections.',
|
||||
invalidNavigationTargetTitle: 'Некорректная цель навигации',
|
||||
invalidNavigationTargetDescription: 'Route навигации не совпадает с известными route.',
|
||||
invalidNavigationTargetResolution: 'Исправьте target route или добавьте route.',
|
||||
missingTranslationTitle: 'Отсутствует перевод',
|
||||
missingTranslationDescription: 'Ключ перевода не разрешается.',
|
||||
missingTranslationResolution: 'Добавьте ключ в translation resources.',
|
||||
missingMediaTitle: 'Отсутствует media',
|
||||
missingMediaDescription: 'Ожидаемый branding media ресурс отсутствует.',
|
||||
missingMediaResolution: 'Укажите корректные media URL в branding конфиге.',
|
||||
runtimeNotInitializedTitle: 'Runtime не инициализирован',
|
||||
runtimeNotInitializedDescription: 'Platform runtime state не помечен как initialized.',
|
||||
runtimeNotInitializedResolution: 'Проверьте цепочку initialize в PlatformRuntimeService.',
|
||||
configFallbackUsageTitle: 'Используется fallback конфигурация',
|
||||
configFallbackUsageDescription: 'Runtime разработки использует fallback или mock конфигурацию.',
|
||||
configFallbackUsageResolution: 'Смените provider mode, если нужна проверка реального bootstrap.',
|
||||
missingDatasourceTitle: 'Отсутствует datasource',
|
||||
missingDatasourceDescription: 'Виджет поддерживает datasource, но он не задан.',
|
||||
missingDatasourceResolution: 'Укажите source в props виджета или default settings manifest.',
|
||||
widgetRenderFailureTitle: 'Сбой рендера виджета',
|
||||
widgetRenderFailureDescription: 'Виджет откатился на unknown widget renderer.',
|
||||
widgetRenderFailureResolution: 'Зарегистрируйте компонент виджета или исправьте component key.',
|
||||
brokenRouteTitle: 'Нарушено ожидание route',
|
||||
brokenRouteDescription: 'Ожидаемый route отсутствует.',
|
||||
brokenRouteResolution: 'Верните регистрацию route или обновите правило диагностики.',
|
||||
imageLoadFailedTitle: 'Ошибка загрузки изображения',
|
||||
imageLoadFailedDescription: 'Отображаемое изображение не загрузилось.',
|
||||
imageLoadFailedResolution: 'Проверьте asset path, CORS и доступность media.',
|
||||
optionalDataMissingTitle: 'Отсутствуют необязательные данные',
|
||||
optionalDataMissingDescription: 'Необязательное конфигурационное значение отсутствует.',
|
||||
optionalDataMissingResolution: 'Добавьте значение, если оно нужно UX.',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -466,4 +466,93 @@ export interface Translations {
|
||||
compareSize: string;
|
||||
compareUnknown: string;
|
||||
};
|
||||
diagnostics: {
|
||||
title: string;
|
||||
disabledTitle: string;
|
||||
disabledDescription: string;
|
||||
generatedAt: string;
|
||||
healthScore: string;
|
||||
summary: string;
|
||||
passedChecks: string;
|
||||
warnings: string;
|
||||
errors: string;
|
||||
critical: string;
|
||||
healthChecks: string;
|
||||
results: string;
|
||||
noIssues: string;
|
||||
ok: string;
|
||||
failed: string;
|
||||
affectedComponent: string;
|
||||
suggestedResolution: string;
|
||||
severity: {
|
||||
info: string;
|
||||
warning: string;
|
||||
error: string;
|
||||
critical: string;
|
||||
};
|
||||
checks: {
|
||||
bootstrapLoaded: string;
|
||||
tenantResolved: string;
|
||||
runtimeInitialized: string;
|
||||
themeLoaded: string;
|
||||
widgetManifestLoaded: string;
|
||||
sectionEngineInitialized: string;
|
||||
configurationEngineInitialized: string;
|
||||
translationResourcesAvailable: string;
|
||||
requiredAssetsAvailable: string;
|
||||
};
|
||||
bootstrapMissingTitle: string;
|
||||
bootstrapMissingDescription: string;
|
||||
bootstrapMissingResolution: string;
|
||||
requiredPropertyMissingTitle: string;
|
||||
requiredPropertyMissingDescription: string;
|
||||
requiredPropertyMissingResolution: string;
|
||||
unknownWidgetTypeTitle: string;
|
||||
unknownWidgetTypeDescription: string;
|
||||
unknownWidgetTypeResolution: string;
|
||||
duplicateIdTitle: string;
|
||||
duplicatePageIdDescription: string;
|
||||
duplicateSectionIdDescription: string;
|
||||
duplicateWidgetIdDescription: string;
|
||||
duplicateIdResolution: string;
|
||||
unknownLayoutTitle: string;
|
||||
unknownLayoutDescription: string;
|
||||
unknownLayoutResolution: string;
|
||||
invalidFeatureFlagTitle: string;
|
||||
invalidFeatureFlagDescription: string;
|
||||
invalidFeatureFlagResolution: string;
|
||||
brokenPageDefinitionTitle: string;
|
||||
brokenPageDefinitionDescription: string;
|
||||
brokenPageDefinitionResolution: string;
|
||||
invalidNavigationTargetTitle: string;
|
||||
invalidNavigationTargetDescription: string;
|
||||
invalidNavigationTargetResolution: string;
|
||||
missingTranslationTitle: string;
|
||||
missingTranslationDescription: string;
|
||||
missingTranslationResolution: string;
|
||||
missingMediaTitle: string;
|
||||
missingMediaDescription: string;
|
||||
missingMediaResolution: string;
|
||||
runtimeNotInitializedTitle: string;
|
||||
runtimeNotInitializedDescription: string;
|
||||
runtimeNotInitializedResolution: string;
|
||||
configFallbackUsageTitle: string;
|
||||
configFallbackUsageDescription: string;
|
||||
configFallbackUsageResolution: string;
|
||||
missingDatasourceTitle: string;
|
||||
missingDatasourceDescription: string;
|
||||
missingDatasourceResolution: string;
|
||||
widgetRenderFailureTitle: string;
|
||||
widgetRenderFailureDescription: string;
|
||||
widgetRenderFailureResolution: string;
|
||||
brokenRouteTitle: string;
|
||||
brokenRouteDescription: string;
|
||||
brokenRouteResolution: string;
|
||||
imageLoadFailedTitle: string;
|
||||
imageLoadFailedDescription: string;
|
||||
imageLoadFailedResolution: string;
|
||||
optionalDataMissingTitle: string;
|
||||
optionalDataMissingDescription: string;
|
||||
optionalDataMissingResolution: string;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user