Search/AdvisoryAI and DAL conversion to EF finishes up. Preparation for microservices consolidation.
This commit is contained in:
@@ -427,7 +427,8 @@ export class UnifiedSearchClient {
|
||||
}
|
||||
|
||||
return value
|
||||
.replace(/<\/?mark>/gi, '')
|
||||
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, ' ')
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
@@ -531,6 +532,9 @@ export class UnifiedSearchClient {
|
||||
avgResultCount: 0,
|
||||
feedbackScore: 0,
|
||||
period,
|
||||
lowQualityResults: [],
|
||||
topQueries: [],
|
||||
trend: [],
|
||||
}),
|
||||
),
|
||||
);
|
||||
@@ -540,7 +544,7 @@ export class UnifiedSearchClient {
|
||||
// --- Search analytics types (Sprint 106 / G6) ---
|
||||
|
||||
export interface SearchAnalyticsEventDto {
|
||||
eventType: 'query' | 'click' | 'zero_result';
|
||||
eventType: 'query' | 'click' | 'zero_result' | 'synthesis';
|
||||
query: string;
|
||||
entityKey?: string;
|
||||
domain?: string;
|
||||
|
||||
@@ -171,9 +171,34 @@ export interface SearchQualityMetrics {
|
||||
avgResultCount: number;
|
||||
feedbackScore: number;
|
||||
period: string;
|
||||
lowQualityResults: SearchLowQualityResult[];
|
||||
topQueries: SearchTopQuery[];
|
||||
trend: SearchQualityTrendPoint[];
|
||||
}
|
||||
|
||||
export interface SearchQualityAlertUpdateRequest {
|
||||
status: 'acknowledged' | 'resolved';
|
||||
resolution?: string;
|
||||
}
|
||||
|
||||
export interface SearchLowQualityResult {
|
||||
entityKey: string;
|
||||
domain: string;
|
||||
negativeFeedbackCount: number;
|
||||
totalFeedback: number;
|
||||
negativeRate: number;
|
||||
}
|
||||
|
||||
export interface SearchTopQuery {
|
||||
query: string;
|
||||
totalSearches: number;
|
||||
avgResultCount: number;
|
||||
feedbackScore: number;
|
||||
}
|
||||
|
||||
export interface SearchQualityTrendPoint {
|
||||
day: string;
|
||||
totalSearches: number;
|
||||
zeroResultRate: number;
|
||||
feedbackScore: number;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,80 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { Injectable, inject, signal } from '@angular/core';
|
||||
import { NavigationEnd, Router } from '@angular/router';
|
||||
import { filter } from 'rxjs/operators';
|
||||
import type { UnifiedSearchDomain, UnifiedSearchFilter } from '../api/unified-search.models';
|
||||
|
||||
export interface ContextSuggestion {
|
||||
key: string;
|
||||
fallback: string;
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AmbientContextService {
|
||||
private readonly router = inject(Router);
|
||||
private readonly routeUrl = signal(this.router.url);
|
||||
|
||||
private readonly searchSuggestionSets = {
|
||||
findings: [
|
||||
{ key: 'ui.search.suggestion.findings.critical', fallback: 'critical findings' },
|
||||
{ key: 'ui.search.suggestion.findings.reachable', fallback: 'reachable vulnerabilities' },
|
||||
{ key: 'ui.search.suggestion.findings.unresolved', fallback: 'unresolved CVEs' },
|
||||
],
|
||||
policy: [
|
||||
{ key: 'ui.search.suggestion.policy.failing_gates', fallback: 'failing policy gates' },
|
||||
{ key: 'ui.search.suggestion.policy.production_deny', fallback: 'production deny rules' },
|
||||
{ key: 'ui.search.suggestion.policy.exceptions', fallback: 'policy exceptions' },
|
||||
],
|
||||
doctor: [
|
||||
{ key: 'ui.search.suggestion.doctor.database', fallback: 'database connectivity' },
|
||||
{ key: 'ui.search.suggestion.doctor.disk', fallback: 'disk space' },
|
||||
{ key: 'ui.search.suggestion.doctor.oidc', fallback: 'OIDC readiness' },
|
||||
],
|
||||
timeline: [
|
||||
{ key: 'ui.search.suggestion.timeline.failed_deployments', fallback: 'failed deployments' },
|
||||
{ key: 'ui.search.suggestion.timeline.recent_promotions', fallback: 'recent promotions' },
|
||||
{ key: 'ui.search.suggestion.timeline.release_history', fallback: 'release history' },
|
||||
],
|
||||
releases: [
|
||||
{ key: 'ui.search.suggestion.releases.pending_approvals', fallback: 'pending approvals' },
|
||||
{ key: 'ui.search.suggestion.releases.blocked_releases', fallback: 'blocked releases' },
|
||||
{ key: 'ui.search.suggestion.releases.environment_status', fallback: 'environment status' },
|
||||
],
|
||||
default: [
|
||||
{ key: 'ui.search.suggestion.default.deploy', fallback: 'How do I deploy?' },
|
||||
{ key: 'ui.search.suggestion.default.vex', fallback: 'What is a VEX statement?' },
|
||||
{ key: 'ui.search.suggestion.default.critical', fallback: 'Show critical findings' },
|
||||
],
|
||||
} as const satisfies Record<string, readonly ContextSuggestion[]>;
|
||||
|
||||
private readonly chatSuggestionSets = {
|
||||
vulnerability: [
|
||||
{ key: 'ui.chat.suggestion.vulnerability.exploitable', fallback: 'Is this exploitable in my environment?' },
|
||||
{ key: 'ui.chat.suggestion.vulnerability.remediation', fallback: 'What is the remediation?' },
|
||||
{ key: 'ui.chat.suggestion.vulnerability.evidence_chain', fallback: 'Show me the evidence chain' },
|
||||
{ key: 'ui.chat.suggestion.vulnerability.draft_vex', fallback: 'Draft a VEX statement' },
|
||||
],
|
||||
policy: [
|
||||
{ key: 'ui.chat.suggestion.policy.explain_rule', fallback: 'Explain this policy rule' },
|
||||
{ key: 'ui.chat.suggestion.policy.override_gate', fallback: 'What would happen if I override this gate?' },
|
||||
{ key: 'ui.chat.suggestion.policy.recent_violations', fallback: 'Show me recent policy violations' },
|
||||
{ key: 'ui.chat.suggestion.policy.add_exception', fallback: 'How do I add an exception?' },
|
||||
],
|
||||
default: [
|
||||
{ key: 'ui.chat.suggestion.default.what_can_do', fallback: 'What can Stella Ops do?' },
|
||||
{ key: 'ui.chat.suggestion.default.first_scan', fallback: 'How do I set up my first scan?' },
|
||||
{ key: 'ui.chat.suggestion.default.promotion_workflow', fallback: 'Explain the release promotion workflow' },
|
||||
{ key: 'ui.chat.suggestion.default.health_checks', fallback: 'What health checks should I run first?' },
|
||||
],
|
||||
} as const satisfies Record<string, readonly ContextSuggestion[]>;
|
||||
|
||||
constructor() {
|
||||
this.router.events
|
||||
.pipe(filter((event): event is NavigationEnd => event instanceof NavigationEnd))
|
||||
.subscribe((event) => this.routeUrl.set(event.urlAfterRedirects));
|
||||
}
|
||||
|
||||
currentDomain(): UnifiedSearchDomain | null {
|
||||
const url = this.router.url;
|
||||
const url = this.routeUrl();
|
||||
|
||||
if (url.startsWith('/security/triage') || url.startsWith('/security/findings')) {
|
||||
return 'findings';
|
||||
@@ -40,6 +107,46 @@ export class AmbientContextService {
|
||||
return null;
|
||||
}
|
||||
|
||||
getSearchSuggestions(): readonly ContextSuggestion[] {
|
||||
const url = this.routeUrl();
|
||||
|
||||
if (url.startsWith('/security/triage') || url.startsWith('/security/findings')) {
|
||||
return this.searchSuggestionSets.findings;
|
||||
}
|
||||
|
||||
if (url.startsWith('/ops/policy')) {
|
||||
return this.searchSuggestionSets.policy;
|
||||
}
|
||||
|
||||
if (url.startsWith('/ops/operations/doctor') || url.startsWith('/ops/operations/system-health')) {
|
||||
return this.searchSuggestionSets.doctor;
|
||||
}
|
||||
|
||||
if (url.startsWith('/ops/timeline') || url.startsWith('/audit')) {
|
||||
return this.searchSuggestionSets.timeline;
|
||||
}
|
||||
|
||||
if (url.startsWith('/releases') || url.startsWith('/mission-control')) {
|
||||
return this.searchSuggestionSets.releases;
|
||||
}
|
||||
|
||||
return this.searchSuggestionSets.default;
|
||||
}
|
||||
|
||||
getChatSuggestions(): readonly ContextSuggestion[] {
|
||||
const url = this.routeUrl();
|
||||
|
||||
if (url.match(/\/security\/(findings|triage)\/[^/]+/)) {
|
||||
return this.chatSuggestionSets.vulnerability;
|
||||
}
|
||||
|
||||
if (url.startsWith('/ops/policy')) {
|
||||
return this.chatSuggestionSets.policy;
|
||||
}
|
||||
|
||||
return this.chatSuggestionSets.default;
|
||||
}
|
||||
|
||||
buildContextFilter(): UnifiedSearchFilter {
|
||||
const domain = this.currentDomain();
|
||||
if (!domain) {
|
||||
|
||||
@@ -20,10 +20,11 @@ import {
|
||||
} from '@angular/core';
|
||||
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
import { ChatService } from './chat.service';
|
||||
import { ChatMessageComponent } from './chat-message.component';
|
||||
import { AmbientContextService } from '../../../core/services/ambient-context.service';
|
||||
import { I18nService } from '../../../core/i18n';
|
||||
import {
|
||||
Conversation,
|
||||
ConversationContext,
|
||||
@@ -552,7 +553,8 @@ export class ChatComponent implements OnInit, OnDestroy {
|
||||
@ViewChild('inputField') inputField!: ElementRef<HTMLTextAreaElement>;
|
||||
|
||||
private readonly chatService = inject(ChatService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly ambientContext = inject(AmbientContextService);
|
||||
private readonly i18n = inject(I18nService);
|
||||
private readonly destroy$ = new Subject<void>();
|
||||
private pendingInitialMessage: string | null = null;
|
||||
|
||||
@@ -571,51 +573,16 @@ export class ChatComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
|
||||
readonly inputPlaceholder = computed(() => {
|
||||
if (this.isStreaming()) return 'Waiting for response...';
|
||||
return 'Ask AdvisoryAI about this finding...';
|
||||
if (this.isStreaming()) {
|
||||
return this.i18n.tryT('ui.chat.input.waiting') ?? 'Waiting for response...';
|
||||
}
|
||||
return this.i18n.tryT('ui.chat.input.placeholder') ?? 'Ask AdvisoryAI about this finding...';
|
||||
});
|
||||
|
||||
readonly suggestions = computed(() => {
|
||||
const url = this.router.url;
|
||||
|
||||
// Vulnerability detail pages: keep original context-specific suggestions
|
||||
if (url.match(/\/security\/(findings|triage)\/[^/]+/)) {
|
||||
return [
|
||||
'Is this exploitable?',
|
||||
'What is the remediation?',
|
||||
'Show me the evidence',
|
||||
'Create a VEX statement',
|
||||
];
|
||||
}
|
||||
|
||||
// Policy pages
|
||||
if (url.startsWith('/ops/policy')) {
|
||||
return [
|
||||
'Explain the release promotion workflow',
|
||||
'What policy gates are failing?',
|
||||
'How do I create a policy exception?',
|
||||
'What health checks should I run first?',
|
||||
];
|
||||
}
|
||||
|
||||
// Doctor / health pages
|
||||
if (url.startsWith('/ops/operations/doctor')) {
|
||||
return [
|
||||
'What health checks should I run first?',
|
||||
'How do I troubleshoot database connectivity?',
|
||||
'Explain the system readiness checks',
|
||||
'What can Stella Ops do?',
|
||||
];
|
||||
}
|
||||
|
||||
// Default: general onboarding suggestions
|
||||
return [
|
||||
'What can Stella Ops do?',
|
||||
'How do I set up my first scan?',
|
||||
'Explain the release promotion workflow',
|
||||
'What health checks should I run first?',
|
||||
];
|
||||
});
|
||||
readonly suggestions = computed(() =>
|
||||
this.ambientContext
|
||||
.getChatSuggestions()
|
||||
.map((suggestion) => this.i18n.tryT(suggestion.key) ?? suggestion.fallback));
|
||||
|
||||
constructor() {
|
||||
// Auto-scroll on new content
|
||||
|
||||
@@ -13,6 +13,9 @@ import { takeUntil } from 'rxjs/operators';
|
||||
import { UnifiedSearchClient } from '../../../core/api/unified-search.client';
|
||||
import type {
|
||||
SearchQualityAlert,
|
||||
SearchQualityTrendPoint,
|
||||
SearchLowQualityResult,
|
||||
SearchTopQuery,
|
||||
SearchQualityMetrics,
|
||||
} from '../../../core/api/unified-search.models';
|
||||
|
||||
@@ -73,7 +76,7 @@ import type {
|
||||
<table class="sqd__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Query</th>
|
||||
<th>Query Hash</th>
|
||||
<th>Type</th>
|
||||
<th>Occurrences</th>
|
||||
<th>First Seen</th>
|
||||
@@ -134,6 +137,97 @@ import type {
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="sqd__section">
|
||||
<h2 class="sqd__section-title">Low-Quality Results</h2>
|
||||
@if (lowQualityResults().length === 0) {
|
||||
<div class="sqd__empty">No low-quality result signals yet.</div>
|
||||
} @else {
|
||||
<div class="sqd__table-wrapper">
|
||||
<table class="sqd__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Entity</th>
|
||||
<th>Domain</th>
|
||||
<th>Negative</th>
|
||||
<th>Total</th>
|
||||
<th>Negative Rate</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (row of lowQualityResults(); track row.entityKey + row.domain) {
|
||||
<tr>
|
||||
<td class="sqd__query-cell">{{ row.entityKey }}</td>
|
||||
<td>{{ row.domain }}</td>
|
||||
<td class="sqd__count-cell">{{ row.negativeFeedbackCount }}</td>
|
||||
<td class="sqd__count-cell">{{ row.totalFeedback }}</td>
|
||||
<td class="sqd__count-cell">{{ row.negativeRate }}%</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="sqd__section">
|
||||
<h2 class="sqd__section-title">Top Query Hashes</h2>
|
||||
@if (topQueries().length === 0) {
|
||||
<div class="sqd__empty">No query-hash history available for this period.</div>
|
||||
} @else {
|
||||
<div class="sqd__table-wrapper">
|
||||
<table class="sqd__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Query Hash</th>
|
||||
<th>Searches</th>
|
||||
<th>Avg Results</th>
|
||||
<th>Feedback Score</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (row of topQueries(); track row.query) {
|
||||
<tr>
|
||||
<td class="sqd__query-cell">{{ row.query }}</td>
|
||||
<td class="sqd__count-cell">{{ row.totalSearches }}</td>
|
||||
<td class="sqd__count-cell">{{ row.avgResultCount }}</td>
|
||||
<td class="sqd__count-cell">{{ row.feedbackScore }}%</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="sqd__section">
|
||||
<h2 class="sqd__section-title">30-Day Trend</h2>
|
||||
@if (trend().length === 0) {
|
||||
<div class="sqd__empty">No trend data available yet.</div>
|
||||
} @else {
|
||||
<div class="sqd__trend-card">
|
||||
<svg
|
||||
class="sqd__trend-svg"
|
||||
viewBox="0 0 1000 220"
|
||||
role="img"
|
||||
aria-label="Search quality trend chart"
|
||||
>
|
||||
<polyline class="sqd__trend-line sqd__trend-line--searches" [attr.points]="trendSearchPath()" />
|
||||
<polyline class="sqd__trend-line sqd__trend-line--zero" [attr.points]="trendZeroRatePath()" />
|
||||
<polyline class="sqd__trend-line sqd__trend-line--feedback" [attr.points]="trendFeedbackPath()" />
|
||||
</svg>
|
||||
<div class="sqd__trend-legend">
|
||||
<span class="sqd__legend-item"><span class="sqd__legend-swatch sqd__legend-swatch--searches"></span>Searches</span>
|
||||
<span class="sqd__legend-item"><span class="sqd__legend-swatch sqd__legend-swatch--zero"></span>Zero-Result %</span>
|
||||
<span class="sqd__legend-item"><span class="sqd__legend-swatch sqd__legend-swatch--feedback"></span>Feedback %</span>
|
||||
</div>
|
||||
<div class="sqd__trend-axis">
|
||||
<span>{{ formatDay(trend()[0]?.day) }}</span>
|
||||
<span>{{ formatDay(trend()[trend().length - 1]?.day) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
@@ -369,6 +463,81 @@ import type {
|
||||
color: #6b7280;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.sqd__trend-card {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.sqd__trend-svg {
|
||||
width: 100%;
|
||||
height: 220px;
|
||||
border-radius: 6px;
|
||||
background: linear-gradient(180deg, #f8fafc 0%, #ffffff 100%);
|
||||
}
|
||||
|
||||
.sqd__trend-line {
|
||||
fill: none;
|
||||
stroke-width: 3;
|
||||
stroke-linejoin: round;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.sqd__trend-line--searches {
|
||||
stroke: #1d4ed8;
|
||||
}
|
||||
|
||||
.sqd__trend-line--zero {
|
||||
stroke: #dc2626;
|
||||
}
|
||||
|
||||
.sqd__trend-line--feedback {
|
||||
stroke: #16a34a;
|
||||
}
|
||||
|
||||
.sqd__trend-legend {
|
||||
margin-top: 0.5rem;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
font-size: 0.75rem;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.sqd__legend-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.sqd__legend-swatch {
|
||||
width: 0.9rem;
|
||||
height: 0.3rem;
|
||||
border-radius: 999px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.sqd__legend-swatch--searches {
|
||||
background: #1d4ed8;
|
||||
}
|
||||
|
||||
.sqd__legend-swatch--zero {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
.sqd__legend-swatch--feedback {
|
||||
background: #16a34a;
|
||||
}
|
||||
|
||||
.sqd__trend-axis {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 0.4rem;
|
||||
font-size: 0.72rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
@@ -386,6 +555,25 @@ export class SearchQualityDashboardComponent implements OnInit, OnDestroy {
|
||||
readonly metrics = signal<SearchQualityMetrics | null>(null);
|
||||
readonly alerts = signal<SearchQualityAlert[]>([]);
|
||||
readonly isLoadingAlerts = signal(true);
|
||||
readonly lowQualityResults = computed<SearchLowQualityResult[]>(
|
||||
() => this.metrics()?.lowQualityResults ?? [],
|
||||
);
|
||||
readonly topQueries = computed<SearchTopQuery[]>(
|
||||
() => this.metrics()?.topQueries ?? [],
|
||||
);
|
||||
readonly trend = computed<SearchQualityTrendPoint[]>(
|
||||
() => this.metrics()?.trend ?? [],
|
||||
);
|
||||
readonly maxTrendSearchVolume = computed(() => {
|
||||
const values = this.trend().map((point) => point.totalSearches);
|
||||
return Math.max(1, ...values);
|
||||
});
|
||||
readonly trendSearchPath = computed(() =>
|
||||
this.buildTrendPath(this.trend(), (point) => point.totalSearches, this.maxTrendSearchVolume()));
|
||||
readonly trendZeroRatePath = computed(() =>
|
||||
this.buildTrendPath(this.trend(), (point) => point.zeroResultRate, 100));
|
||||
readonly trendFeedbackPath = computed(() =>
|
||||
this.buildTrendPath(this.trend(), (point) => point.feedbackScore, 100));
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadMetrics('7d');
|
||||
@@ -447,4 +635,42 @@ export class SearchQualityDashboardComponent implements OnInit, OnDestroy {
|
||||
return iso;
|
||||
}
|
||||
}
|
||||
|
||||
formatDay(day: string | undefined): string {
|
||||
if (!day) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
const d = new Date(day);
|
||||
return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
|
||||
}
|
||||
catch {
|
||||
return day;
|
||||
}
|
||||
}
|
||||
|
||||
private buildTrendPath(
|
||||
points: SearchQualityTrendPoint[],
|
||||
selector: (point: SearchQualityTrendPoint) => number,
|
||||
maxValue: number,
|
||||
): string {
|
||||
if (points.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const width = 1000;
|
||||
const height = 220;
|
||||
const safeMax = Math.max(1, maxValue);
|
||||
const stepX = points.length <= 1 ? 0 : width / (points.length - 1);
|
||||
|
||||
return points
|
||||
.map((point, index) => {
|
||||
const value = Math.max(0, Math.min(safeMax, selector(point)));
|
||||
const x = index * stepX;
|
||||
const y = height - (value / safeMax) * height;
|
||||
return `${x.toFixed(2)},${y.toFixed(2)}`;
|
||||
})
|
||||
.join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import { EntityCardComponent } from '../../shared/components/entity-card/entity-
|
||||
import { SynthesisPanelComponent } from '../../shared/components/synthesis-panel/synthesis-panel.component';
|
||||
import { AmbientContextService } from '../../core/services/ambient-context.service';
|
||||
import { SearchChatContextService } from '../../core/services/search-chat-context.service';
|
||||
import { I18nService } from '../../core/i18n';
|
||||
import { normalizeSearchActionRoute } from './search-route-matrix';
|
||||
|
||||
type SearchDomainFilter = 'all' | UnifiedSearchDomain;
|
||||
@@ -60,7 +61,7 @@ type SearchDomainFilter = 'all' | UnifiedSearchDomain;
|
||||
(focus)="onFocus()"
|
||||
(blur)="onBlur()"
|
||||
(keydown)="onKeydown($event)"
|
||||
aria-label="Global search"
|
||||
[attr.aria-label]="t('ui.search.input_aria_label', 'Global search')"
|
||||
aria-autocomplete="list"
|
||||
/>
|
||||
|
||||
@@ -69,20 +70,20 @@ type SearchDomainFilter = 'all' | UnifiedSearchDomain;
|
||||
|
||||
@if (showResults()) {
|
||||
<div class="search__results" id="search-results">
|
||||
@if (showDegradedModeBanner()) {
|
||||
<div class="search__degraded-banner" role="status" aria-live="polite">
|
||||
<span class="search__degraded-title">Fallback mode:</span>
|
||||
{{ degradedModeMessage() }}
|
||||
</div>
|
||||
}
|
||||
@if (showDegradedModeBanner()) {
|
||||
<div class="search__degraded-banner" role="status" aria-live="polite">
|
||||
<span class="search__degraded-title">{{ degradedModeLabel() }}</span>
|
||||
{{ degradedModeMessage() }}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (isLoading()) {
|
||||
<div class="search__loading">Searching...</div>
|
||||
<div class="search__loading">{{ t('ui.search.loading', 'Searching...') }}</div>
|
||||
} @else if (query().trim().length >= 1 && cards().length === 0) {
|
||||
<div class="search__empty">No results found</div>
|
||||
<div class="search__empty">{{ t('ui.search.no_results', 'No results found') }}</div>
|
||||
@if (searchResponse()?.suggestions?.length) {
|
||||
<div class="did-you-mean">
|
||||
<span class="did-you-mean__label">Did you mean:</span>
|
||||
<span class="did-you-mean__label">{{ t('ui.search.did_you_mean_label', 'Did you mean:') }}</span>
|
||||
@for (suggestion of searchResponse()!.suggestions!; track suggestion.text) {
|
||||
<button
|
||||
type="button"
|
||||
@@ -96,7 +97,7 @@ type SearchDomainFilter = 'all' | UnifiedSearchDomain;
|
||||
}
|
||||
@if (refinements().length > 0) {
|
||||
<div class="try-also-bar">
|
||||
<span class="try-also-bar__label">Try also:</span>
|
||||
<span class="try-also-bar__label">{{ t('ui.search.try_also_label', 'Try also:') }}</span>
|
||||
@for (r of refinements(); track r.text) {
|
||||
<button
|
||||
type="button"
|
||||
@@ -123,7 +124,7 @@ type SearchDomainFilter = 'all' | UnifiedSearchDomain;
|
||||
|
||||
@if (searchResponse()?.suggestions?.length) {
|
||||
<div class="did-you-mean">
|
||||
<span class="did-you-mean__label">Did you mean:</span>
|
||||
<span class="did-you-mean__label">{{ t('ui.search.did_you_mean_label', 'Did you mean:') }}</span>
|
||||
@for (suggestion of searchResponse()!.suggestions!; track suggestion.text) {
|
||||
<button
|
||||
type="button"
|
||||
@@ -137,7 +138,7 @@ type SearchDomainFilter = 'all' | UnifiedSearchDomain;
|
||||
}
|
||||
@if (refinements().length > 0) {
|
||||
<div class="try-also-bar">
|
||||
<span class="try-also-bar__label">Try also:</span>
|
||||
<span class="try-also-bar__label">{{ t('ui.search.try_also_label', 'Try also:') }}</span>
|
||||
@for (r of refinements(); track r.text) {
|
||||
<button
|
||||
type="button"
|
||||
@@ -173,8 +174,8 @@ type SearchDomainFilter = 'all' | UnifiedSearchDomain;
|
||||
@if (recentSearches().length > 0) {
|
||||
<div class="search__group">
|
||||
<div class="search__group-header">
|
||||
<div class="search__group-label">Recent</div>
|
||||
<button type="button" class="search__clear-history" (click)="clearSearchHistory()">Clear</button>
|
||||
<div class="search__group-label">{{ t('ui.search.recent_label', 'Recent') }}</div>
|
||||
<button type="button" class="search__clear-history" (click)="clearSearchHistory()">{{ t('ui.search.clear_history', 'Clear') }}</button>
|
||||
</div>
|
||||
@for (recent of recentSearches(); track recent; let i = $index) {
|
||||
<button
|
||||
@@ -195,7 +196,7 @@ type SearchDomainFilter = 'all' | UnifiedSearchDomain;
|
||||
}
|
||||
|
||||
<div class="search__suggestions">
|
||||
<div class="search__group-label">Suggested</div>
|
||||
<div class="search__group-label">{{ t('ui.search.suggested_label', 'Suggested') }}</div>
|
||||
<div class="search__suggestion-chips">
|
||||
@for (suggestion of contextualSuggestions(); track suggestion) {
|
||||
<button
|
||||
@@ -208,9 +209,9 @@ type SearchDomainFilter = 'all' | UnifiedSearchDomain;
|
||||
</div>
|
||||
|
||||
<div class="search__domain-guide">
|
||||
<div class="search__group-label">Search across your release control plane</div>
|
||||
<div class="search__group-label">{{ t('ui.search.empty_state_header', 'Search across your release control plane') }}</div>
|
||||
<div class="search__domain-grid">
|
||||
@for (domain of domainGuide; track domain.title) {
|
||||
@for (domain of domainGuide(); track domain.key) {
|
||||
<div class="search__domain-card">
|
||||
<div class="search__domain-title">
|
||||
<span class="search__domain-icon" aria-hidden="true">{{ domain.icon }}</span>
|
||||
@@ -233,13 +234,21 @@ type SearchDomainFilter = 'all' | UnifiedSearchDomain;
|
||||
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/>
|
||||
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>
|
||||
</svg>
|
||||
Getting Started
|
||||
{{ t('ui.search.quick_action.getting_started', 'Getting Started') }}
|
||||
</button>
|
||||
<button type="button" class="search__quick-link" (click)="navigateQuickAction('/ops/operations/doctor')">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>
|
||||
</svg>
|
||||
Run Health Check
|
||||
{{ t('ui.search.quick_action.run_health_check', 'Run Health Check') }}
|
||||
</button>
|
||||
<button type="button" class="search__quick-link" (click)="navigateQuickAction('/security/triage')">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 3h18v4H3z"/>
|
||||
<path d="M5 11h14"/>
|
||||
<path d="M5 16h10"/>
|
||||
</svg>
|
||||
{{ t('ui.search.quick_action.view_recent_scans', 'View Recent Scans') }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@@ -484,7 +493,7 @@ type SearchDomainFilter = 'all' | UnifiedSearchDomain;
|
||||
padding: 0.25rem 0.75rem;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
@media (max-width: 767px) {
|
||||
.search__domain-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -646,12 +655,14 @@ export class GlobalSearchComponent implements OnInit, OnDestroy {
|
||||
private readonly searchClient = inject(UnifiedSearchClient);
|
||||
private readonly ambientContext = inject(AmbientContextService);
|
||||
private readonly searchChatContext = inject(SearchChatContextService);
|
||||
private readonly i18n = inject(I18nService);
|
||||
private readonly destroy$ = new Subject<void>();
|
||||
private readonly searchTerms$ = new Subject<string>();
|
||||
private readonly recentSearchStorageKey = 'stella-recent-searches';
|
||||
private wasDegradedMode = false;
|
||||
private escapeCount = 0;
|
||||
private placeholderRotationHandle: ReturnType<typeof setInterval> | null = null;
|
||||
private blurHideHandle: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
@ViewChild('searchInput') searchInputRef!: ElementRef<HTMLInputElement>;
|
||||
|
||||
@@ -677,6 +688,8 @@ export class GlobalSearchComponent implements OnInit, OnDestroy {
|
||||
const mode = this.diagnosticsMode();
|
||||
return mode === 'legacy-fallback' || mode === 'fallback-empty';
|
||||
});
|
||||
readonly degradedModeLabel = computed(() =>
|
||||
this.i18n.tryT('ui.search.degraded.label') ?? 'Fallback mode:');
|
||||
readonly showDegradedModeBanner = computed(() =>
|
||||
!this.isLoading() &&
|
||||
this.query().trim().length >= 1 &&
|
||||
@@ -684,54 +697,121 @@ export class GlobalSearchComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
readonly degradedModeMessage = computed(() => {
|
||||
if (this.diagnosticsMode() === 'fallback-empty') {
|
||||
return 'Unified search is unavailable and legacy fallback returned no results. Try a broader query or retry.';
|
||||
return this.i18n.tryT('ui.search.degraded.empty') ??
|
||||
'Unified search is unavailable and legacy fallback returned no results. Try a broader query or retry.';
|
||||
}
|
||||
|
||||
return 'Showing legacy fallback results. Coverage and ranking may differ until unified search recovers.';
|
||||
return this.i18n.tryT('ui.search.degraded.results') ??
|
||||
'Showing legacy fallback results. Coverage and ranking may differ until unified search recovers.';
|
||||
});
|
||||
|
||||
readonly domainGuide: ReadonlyArray<{
|
||||
icon: string;
|
||||
title: string;
|
||||
description: string;
|
||||
example: string;
|
||||
}> = [
|
||||
{ icon: 'S', title: 'Security Findings', description: 'CVEs, vulnerabilities, and exposure data', example: 'CVE-2024-21626' },
|
||||
{ icon: 'V', title: 'VEX Statements', description: 'Vulnerability exploitability assessments', example: 'not_affected' },
|
||||
{ icon: 'P', title: 'Policy Rules', description: 'Release gate rules and enforcement', example: 'DENY-CRITICAL-PROD' },
|
||||
{ icon: 'D', title: 'Documentation', description: 'Guides, architecture, and runbooks', example: 'how to deploy' },
|
||||
{ icon: 'A', title: 'API Reference', description: 'OpenAPI endpoints and contracts', example: 'POST /api/v1/scanner/scans' },
|
||||
{ icon: 'H', title: 'Health Checks', description: 'System diagnostics and remediation', example: 'database connectivity' },
|
||||
{ icon: 'R', title: 'Release Workflows', description: 'Promotion status, rollout history, and gate decisions', example: 'failed promotion' },
|
||||
{ icon: 'C', title: 'Platform Catalog', description: 'Components, integrations, and environment inventory', example: 'registry integration' },
|
||||
];
|
||||
private readonly domainGuideCatalog = [
|
||||
{
|
||||
key: 'findings',
|
||||
icon: 'S',
|
||||
titleKey: 'ui.search.domain.findings.title',
|
||||
titleFallback: 'Security Findings',
|
||||
descriptionKey: 'ui.search.domain.findings.description',
|
||||
descriptionFallback: 'CVEs, vulnerabilities, and exposure data across your images.',
|
||||
exampleKey: 'ui.search.domain.findings.example',
|
||||
exampleFallback: 'CVE-2024-21626',
|
||||
},
|
||||
{
|
||||
key: 'vex',
|
||||
icon: 'V',
|
||||
titleKey: 'ui.search.domain.vex.title',
|
||||
titleFallback: 'VEX Statements',
|
||||
descriptionKey: 'ui.search.domain.vex.description',
|
||||
descriptionFallback: 'Exploitability statuses and vendor assertions for vulnerabilities.',
|
||||
exampleKey: 'ui.search.domain.vex.example',
|
||||
exampleFallback: 'not_affected',
|
||||
},
|
||||
{
|
||||
key: 'policy',
|
||||
icon: 'P',
|
||||
titleKey: 'ui.search.domain.policy.title',
|
||||
titleFallback: 'Policy Rules',
|
||||
descriptionKey: 'ui.search.domain.policy.description',
|
||||
descriptionFallback: 'Release gate rules, exceptions, and enforcement outcomes.',
|
||||
exampleKey: 'ui.search.domain.policy.example',
|
||||
exampleFallback: 'DENY-CRITICAL-PROD',
|
||||
},
|
||||
{
|
||||
key: 'docs',
|
||||
icon: 'D',
|
||||
titleKey: 'ui.search.domain.docs.title',
|
||||
titleFallback: 'Documentation',
|
||||
descriptionKey: 'ui.search.domain.docs.description',
|
||||
descriptionFallback: 'Guides, architecture references, and operator runbooks.',
|
||||
exampleKey: 'ui.search.domain.docs.example',
|
||||
exampleFallback: 'how to deploy',
|
||||
},
|
||||
{
|
||||
key: 'api',
|
||||
icon: 'A',
|
||||
titleKey: 'ui.search.domain.api.title',
|
||||
titleFallback: 'API Reference',
|
||||
descriptionKey: 'ui.search.domain.api.description',
|
||||
descriptionFallback: 'OpenAPI endpoints, request contracts, and service operations.',
|
||||
exampleKey: 'ui.search.domain.api.example',
|
||||
exampleFallback: 'POST /api/v1/scanner/scans',
|
||||
},
|
||||
{
|
||||
key: 'health',
|
||||
icon: 'H',
|
||||
titleKey: 'ui.search.domain.health.title',
|
||||
titleFallback: 'Health Checks',
|
||||
descriptionKey: 'ui.search.domain.health.description',
|
||||
descriptionFallback: 'Doctor checks, readiness diagnostics, and remediation guidance.',
|
||||
exampleKey: 'ui.search.domain.health.example',
|
||||
exampleFallback: 'database connectivity',
|
||||
},
|
||||
{
|
||||
key: 'operations',
|
||||
icon: 'O',
|
||||
titleKey: 'ui.search.domain.operations.title',
|
||||
titleFallback: 'Operations',
|
||||
descriptionKey: 'ui.search.domain.operations.description',
|
||||
descriptionFallback: 'Jobs, workflows, and operational controls across environments.',
|
||||
exampleKey: 'ui.search.domain.operations.example',
|
||||
exampleFallback: 'blocked releases',
|
||||
},
|
||||
{
|
||||
key: 'timeline',
|
||||
icon: 'T',
|
||||
titleKey: 'ui.search.domain.timeline.title',
|
||||
titleFallback: 'Timeline',
|
||||
descriptionKey: 'ui.search.domain.timeline.description',
|
||||
descriptionFallback: 'Promotion history, incidents, and causal event traces.',
|
||||
exampleKey: 'ui.search.domain.timeline.example',
|
||||
exampleFallback: 'release history',
|
||||
},
|
||||
] as const;
|
||||
|
||||
readonly contextualSuggestions = computed<string[]>(() => {
|
||||
const url = this.router.url;
|
||||
readonly domainGuide = computed(() =>
|
||||
this.domainGuideCatalog.map((domain) => ({
|
||||
key: domain.key,
|
||||
icon: domain.icon,
|
||||
title: this.t(domain.titleKey, domain.titleFallback),
|
||||
description: this.t(domain.descriptionKey, domain.descriptionFallback),
|
||||
example: this.t(domain.exampleKey, domain.exampleFallback),
|
||||
})));
|
||||
|
||||
if (url.startsWith('/security/triage') || url.startsWith('/security/findings')) {
|
||||
return ['critical findings', 'reachable vulnerabilities', 'unresolved CVEs'];
|
||||
}
|
||||
|
||||
if (url.startsWith('/ops/policy')) {
|
||||
return ['failing policy gates', 'production deny rules', 'policy exceptions'];
|
||||
}
|
||||
|
||||
if (url.startsWith('/ops/operations/doctor')) {
|
||||
return ['database connectivity', 'disk space', 'OIDC readiness'];
|
||||
}
|
||||
|
||||
return ['How do I deploy?', 'What is a VEX statement?', 'Show critical findings'];
|
||||
});
|
||||
readonly contextualSuggestions = computed<string[]>(() =>
|
||||
this.ambientContext
|
||||
.getSearchSuggestions()
|
||||
.map((suggestion) => this.i18n.tryT(suggestion.key) ?? suggestion.fallback));
|
||||
|
||||
readonly inputPlaceholder = computed(() => {
|
||||
const suggestions = this.contextualSuggestions();
|
||||
if (suggestions.length === 0) {
|
||||
return 'Search everything...';
|
||||
return this.t('ui.search.placeholder.default', 'Search everything...');
|
||||
}
|
||||
|
||||
const index = this.placeholderIndex() % suggestions.length;
|
||||
return `Try: ${suggestions[index]}`;
|
||||
return this.t('ui.search.placeholder.try', 'Try: {suggestion}', {
|
||||
suggestion: suggestions[index],
|
||||
});
|
||||
});
|
||||
|
||||
readonly cards = computed(() => this.searchResponse()?.cards ?? []);
|
||||
@@ -763,8 +843,10 @@ export class GlobalSearchComponent implements OnInit, OnDestroy {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.placeholderRotationHandle = setInterval(() => {
|
||||
this.placeholderIndex.update((current) => current + 1);
|
||||
}, 4500);
|
||||
if (!this.isFocused()) {
|
||||
this.placeholderIndex.update((current) => current + 1);
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
this.searchChatContext.chatToSearchRequested$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
@@ -844,6 +926,10 @@ export class GlobalSearchComponent implements OnInit, OnDestroy {
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
if (this.blurHideHandle) {
|
||||
clearTimeout(this.blurHideHandle);
|
||||
this.blurHideHandle = null;
|
||||
}
|
||||
if (this.placeholderRotationHandle) {
|
||||
clearInterval(this.placeholderRotationHandle);
|
||||
this.placeholderRotationHandle = null;
|
||||
@@ -851,6 +937,10 @@ export class GlobalSearchComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
onFocus(): void {
|
||||
if (this.blurHideHandle) {
|
||||
clearTimeout(this.blurHideHandle);
|
||||
this.blurHideHandle = null;
|
||||
}
|
||||
this.isFocused.set(true);
|
||||
this.escapeCount = 0;
|
||||
this.consumeChatToSearchContext();
|
||||
@@ -859,7 +949,19 @@ export class GlobalSearchComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
onBlur(): void {
|
||||
setTimeout(() => {
|
||||
if (this.blurHideHandle) {
|
||||
clearTimeout(this.blurHideHandle);
|
||||
}
|
||||
|
||||
this.blurHideHandle = setTimeout(() => {
|
||||
this.blurHideHandle = null;
|
||||
const input = this.searchInputRef?.nativeElement;
|
||||
const activeElement = typeof document !== 'undefined'
|
||||
? document.activeElement
|
||||
: null;
|
||||
if (input && activeElement === input) {
|
||||
return;
|
||||
}
|
||||
this.isFocused.set(false);
|
||||
this.escapeCount = 0;
|
||||
}, 200);
|
||||
@@ -1041,9 +1143,14 @@ export class GlobalSearchComponent implements OnInit, OnDestroy {
|
||||
this.selectedIndex.set(0);
|
||||
}
|
||||
|
||||
t(key: string, fallback: string, params?: Record<string, string | number>): string {
|
||||
return this.i18n.tryT(key, params) ?? fallback;
|
||||
}
|
||||
|
||||
getDomainFilterLabel(filter: SearchDomainFilter): string {
|
||||
if (filter === 'all') {
|
||||
return `All (${this.cards().length})`;
|
||||
const allLabel = this.t('ui.search.filter.all', 'All');
|
||||
return `${allLabel} (${this.cards().length})`;
|
||||
}
|
||||
|
||||
const count = this.cards().filter((c) => c.domain === filter).length;
|
||||
@@ -1089,6 +1196,7 @@ export class GlobalSearchComponent implements OnInit, OnDestroy {
|
||||
const primaryAction = selected.actions.find((a) => a.isPrimary) ?? selected.actions[0];
|
||||
if (primaryAction) {
|
||||
this.saveRecentSearch(this.query());
|
||||
this.emitClickAnalytics(selected);
|
||||
this.executeAction(primaryAction);
|
||||
this.closeResults();
|
||||
}
|
||||
@@ -1179,8 +1287,10 @@ export class GlobalSearchComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
const events: Array<{
|
||||
eventType: 'query' | 'zero_result';
|
||||
eventType: 'query' | 'zero_result' | 'synthesis';
|
||||
query: string;
|
||||
entityKey?: string;
|
||||
domain?: string;
|
||||
resultCount: number;
|
||||
durationMs?: number;
|
||||
}> = [{
|
||||
@@ -1199,6 +1309,17 @@ export class GlobalSearchComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
if (response.synthesis) {
|
||||
events.push({
|
||||
eventType: 'synthesis' as const,
|
||||
query: normalized,
|
||||
entityKey: '__synthesis__',
|
||||
domain: 'synthesis',
|
||||
resultCount: response.synthesis.sourceCount,
|
||||
durationMs: response.diagnostics?.durationMs,
|
||||
});
|
||||
}
|
||||
|
||||
this.searchClient.recordAnalytics(events);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,52 @@
|
||||
export interface SearchRouteMatrixEntry {
|
||||
domain: 'knowledge' | 'findings' | 'policy' | 'vex' | 'platform';
|
||||
sourceRoute: string;
|
||||
expectedRoute: string;
|
||||
}
|
||||
|
||||
// Acceptance matrix for FE-112 route normalization coverage.
|
||||
export const SEARCH_ACTION_ROUTE_MATRIX: ReadonlyArray<SearchRouteMatrixEntry> = [
|
||||
{
|
||||
domain: 'knowledge',
|
||||
sourceRoute: '/docs/modules/platform/architecture-overview.md#release-flow',
|
||||
expectedRoute: '/docs/modules/platform/architecture-overview.md#release-flow',
|
||||
},
|
||||
{
|
||||
domain: 'findings',
|
||||
sourceRoute: '/triage/findings/fnd-123',
|
||||
expectedRoute: '/security/findings/fnd-123',
|
||||
},
|
||||
{
|
||||
domain: 'policy',
|
||||
sourceRoute: '/policy/DENY-CRITICAL-PROD',
|
||||
expectedRoute: '/ops/policy?q=DENY-CRITICAL-PROD',
|
||||
},
|
||||
{
|
||||
domain: 'vex',
|
||||
sourceRoute: '/vex-hub/CVE-2024-21626',
|
||||
expectedRoute: '/security/advisories-vex?q=CVE-2024-21626',
|
||||
},
|
||||
{
|
||||
domain: 'platform',
|
||||
sourceRoute: '/scans/scan-42',
|
||||
expectedRoute: '/security/triage?q=scan-42',
|
||||
},
|
||||
];
|
||||
|
||||
const KNOWN_FRONTEND_ROUTE_ROOTS: ReadonlyArray<string> = [
|
||||
'/mission-control',
|
||||
'/releases',
|
||||
'/security',
|
||||
'/evidence',
|
||||
'/ops',
|
||||
'/setup',
|
||||
'/settings',
|
||||
'/welcome',
|
||||
'/docs',
|
||||
'/console',
|
||||
'/auth',
|
||||
];
|
||||
|
||||
export function normalizeSearchActionRoute(route: string): string {
|
||||
if (!route.startsWith('/')) {
|
||||
return route;
|
||||
@@ -7,7 +56,7 @@ export function normalizeSearchActionRoute(route: string): string {
|
||||
try {
|
||||
parsedUrl = new URL(route, 'https://stellaops.local');
|
||||
} catch {
|
||||
return route;
|
||||
return '/ops';
|
||||
}
|
||||
|
||||
const pathname = parsedUrl.pathname;
|
||||
@@ -27,8 +76,19 @@ export function normalizeSearchActionRoute(route: string): string {
|
||||
parsedUrl.pathname = '/ops/policy';
|
||||
parsedUrl.search = lookup ? `?q=${encodeURIComponent(lookup)}` : '';
|
||||
} else if (pathname.startsWith('/scans/')) {
|
||||
parsedUrl.pathname = `/security/scans/${pathname.substring('/scans/'.length)}`;
|
||||
const scanId = decodeURIComponent(pathname.substring('/scans/'.length));
|
||||
parsedUrl.pathname = '/security/triage';
|
||||
parsedUrl.search = scanId ? `?q=${encodeURIComponent(scanId)}` : '';
|
||||
}
|
||||
|
||||
if (!isKnownFrontendRoute(parsedUrl.pathname)) {
|
||||
return '/ops';
|
||||
}
|
||||
|
||||
return `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`;
|
||||
}
|
||||
|
||||
function isKnownFrontendRoute(pathname: string): boolean {
|
||||
return KNOWN_FRONTEND_ROUTE_ROOTS.some((root) =>
|
||||
pathname === root || pathname.startsWith(`${root}/`));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"_meta": { "locale": "bg-BG", "description": "Offline fallback bundle for StellaOps Console" },
|
||||
|
||||
"_meta": {
|
||||
"locale": "bg-BG",
|
||||
"description": "Offline fallback bundle for StellaOps Console"
|
||||
},
|
||||
"common.error.generic": "Something went wrong.",
|
||||
"common.error.not_found": "The requested resource was not found.",
|
||||
"common.error.unauthorized": "You do not have permission to perform this action.",
|
||||
@@ -9,12 +11,10 @@
|
||||
"common.error.timeout": "Request timed out.",
|
||||
"common.error.server_error": "An internal server error occurred. Please try again later.",
|
||||
"common.error.network": "Network error. Check your connection.",
|
||||
|
||||
"common.validation.required": "This field is required.",
|
||||
"common.validation.invalid": "Invalid value.",
|
||||
"common.validation.too_long": "Maximum {max} characters allowed.",
|
||||
"common.validation.too_short": "Minimum {min} characters required.",
|
||||
|
||||
"common.actions.save": "Save",
|
||||
"common.actions.cancel": "Cancel",
|
||||
"common.actions.delete": "Delete",
|
||||
@@ -26,7 +26,6 @@
|
||||
"common.actions.collapse": "Collapse",
|
||||
"common.actions.show_more": "Show more",
|
||||
"common.actions.show_less": "Show less",
|
||||
|
||||
"common.status.healthy": "Healthy",
|
||||
"common.status.degraded": "Degraded",
|
||||
"common.status.unavailable": "Unavailable",
|
||||
@@ -39,16 +38,13 @@
|
||||
"common.status.failed": "Failed",
|
||||
"common.status.canceled": "Canceled",
|
||||
"common.status.blocked": "Blocked",
|
||||
|
||||
"common.severity.critical": "Critical",
|
||||
"common.severity.high": "High",
|
||||
"common.severity.medium": "Medium",
|
||||
"common.severity.low": "Low",
|
||||
"common.severity.info": "Info",
|
||||
"common.severity.none": "None",
|
||||
|
||||
"common.time.just_now": "Just now",
|
||||
|
||||
"common.ui.loading": "Loading...",
|
||||
"common.ui.saving": "Saving...",
|
||||
"common.ui.deleting": "Deleting...",
|
||||
@@ -57,11 +53,9 @@
|
||||
"common.ui.offline": "You are offline.",
|
||||
"common.ui.reconnecting": "Reconnecting...",
|
||||
"common.ui.back_online": "Back online.",
|
||||
|
||||
"ui.loading.skeleton": "Loading...",
|
||||
"ui.loading.spinner": "Please wait...",
|
||||
"ui.loading.slow": "This is taking longer than expected...",
|
||||
|
||||
"ui.error.generic": "Something went wrong.",
|
||||
"ui.error.network": "Network error. Check your connection.",
|
||||
"ui.error.timeout": "Request timed out. Please try again.",
|
||||
@@ -70,19 +64,16 @@
|
||||
"ui.error.server_error": "Server error. Please try again later.",
|
||||
"ui.error.try_again": "Try again",
|
||||
"ui.error.go_back": "Go back",
|
||||
|
||||
"ui.offline.banner": "You're offline.",
|
||||
"ui.offline.description": "Some features may be unavailable.",
|
||||
"ui.offline.reconnecting": "Reconnecting...",
|
||||
"ui.offline.reconnected": "Back online.",
|
||||
|
||||
"ui.toast.success": "Success",
|
||||
"ui.toast.info": "Info",
|
||||
"ui.toast.warning": "Warning",
|
||||
"ui.toast.error": "Error",
|
||||
"ui.toast.dismiss": "Dismiss",
|
||||
"ui.toast.undo": "Undo",
|
||||
|
||||
"ui.actions.save": "Save",
|
||||
"ui.actions.saving": "Saving...",
|
||||
"ui.actions.saved": "Saved",
|
||||
@@ -111,7 +102,6 @@
|
||||
"ui.actions.sign_in": "Sign in",
|
||||
"ui.actions.back_to_list": "Back to list",
|
||||
"ui.actions.load_more": "Load more",
|
||||
|
||||
"ui.labels.all": "All",
|
||||
"ui.labels.title": "Title",
|
||||
"ui.labels.description": "Description",
|
||||
@@ -131,14 +121,12 @@
|
||||
"ui.labels.selected": "selected",
|
||||
"ui.labels.last_updated": "Last updated:",
|
||||
"ui.labels.expires": "Expires",
|
||||
|
||||
"ui.validation.required": "This field is required.",
|
||||
"ui.validation.invalid": "Invalid value.",
|
||||
"ui.validation.too_long": "Maximum {max} characters allowed.",
|
||||
"ui.validation.too_short": "Minimum {min} characters required.",
|
||||
"ui.validation.invalid_email": "Please enter a valid email address.",
|
||||
"ui.validation.invalid_url": "Please enter a valid URL.",
|
||||
|
||||
"ui.a11y.loading": "Content is loading.",
|
||||
"ui.a11y.loaded": "Content loaded.",
|
||||
"ui.a11y.error": "An error occurred.",
|
||||
@@ -148,10 +136,8 @@
|
||||
"ui.a11y.deselected": "Deselected",
|
||||
"ui.a11y.required": "Required field",
|
||||
"ui.a11y.optional": "Optional",
|
||||
|
||||
"ui.motion.reduced": "Animations reduced.",
|
||||
"ui.motion.enabled": "Animations enabled.",
|
||||
|
||||
"ui.auth.fresh_active": "Fresh auth: Active",
|
||||
"ui.auth.fresh_stale": "Fresh auth: Stale",
|
||||
"ui.locale.label": "Ezik",
|
||||
@@ -171,15 +157,14 @@
|
||||
"ui.settings.language.persisted": "Zapazeno za vashiya akaunt i preizpolzvano ot CLI.",
|
||||
"ui.settings.language.persisted_error": "Zapazeno lokalno, no sinkhronizatsiyata na akaunta se provali.",
|
||||
"ui.settings.language.sign_in_hint": "Vlezte v sistema, za da sinkhronizirate tazi nastroika s CLI.",
|
||||
|
||||
"ui.first_signal.label": "First signal",
|
||||
"ui.first_signal.run_prefix": "Run:",
|
||||
"ui.first_signal.live": "Live",
|
||||
"ui.first_signal.polling": "Polling",
|
||||
"ui.first_signal.range_prefix": "Range",
|
||||
"ui.first_signal.range_separator": "\u2013",
|
||||
"ui.first_signal.stage_separator": " \u00b7 ",
|
||||
"ui.first_signal.waiting": "Waiting for first signal\u2026",
|
||||
"ui.first_signal.range_separator": "–",
|
||||
"ui.first_signal.stage_separator": " · ",
|
||||
"ui.first_signal.waiting": "Waiting for first signal…",
|
||||
"ui.first_signal.not_available": "Signal not available yet.",
|
||||
"ui.first_signal.offline": "Offline. Last known signal may be stale.",
|
||||
"ui.first_signal.failed": "Failed to load signal.",
|
||||
@@ -202,20 +187,17 @@
|
||||
"ui.first_signal.stage.report": "Generating report",
|
||||
"ui.first_signal.stage.unknown": "Processing",
|
||||
"ui.first_signal.aria.card_label": "First signal status",
|
||||
|
||||
"ui.severity.critical": "Critical",
|
||||
"ui.severity.high": "High",
|
||||
"ui.severity.medium": "Medium",
|
||||
"ui.severity.low": "Low",
|
||||
"ui.severity.info": "Info",
|
||||
"ui.severity.none": "None",
|
||||
|
||||
"ui.release_orchestrator.title": "Release Orchestrator",
|
||||
"ui.release_orchestrator.subtitle": "Pipeline overview and release management",
|
||||
"ui.release_orchestrator.pipeline_runs": "Pipeline Runs",
|
||||
"ui.release_orchestrator.refresh_dashboard": "Refresh dashboard",
|
||||
|
||||
"ui.risk_dashboard.eyebrow": "Gateway \u00b7 Risk",
|
||||
"ui.risk_dashboard.eyebrow": "Gateway · Risk",
|
||||
"ui.risk_dashboard.title": "Risk Profiles",
|
||||
"ui.risk_dashboard.subtitle": "Tenant-scoped risk posture with deterministic ordering.",
|
||||
"ui.risk_dashboard.up_to_date": "Up to date",
|
||||
@@ -225,8 +207,7 @@
|
||||
"ui.risk_dashboard.risks_suffix": "risks.",
|
||||
"ui.risk_dashboard.error_unable_to_load": "Unable to load risk profiles.",
|
||||
"ui.risk_dashboard.no_risks_found": "No risks found for current filters.",
|
||||
"ui.risk_dashboard.loading_risks": "Loading risks\u2026",
|
||||
|
||||
"ui.risk_dashboard.loading_risks": "Loading risks…",
|
||||
"ui.findings.title": "Findings",
|
||||
"ui.findings.search_placeholder": "Search findings...",
|
||||
"ui.findings.clear_filters": "Clear Filters",
|
||||
@@ -242,7 +223,6 @@
|
||||
"ui.findings.select": "Select",
|
||||
"ui.findings.no_findings": "No findings to display.",
|
||||
"ui.findings.no_match": "No findings match the current filters.",
|
||||
|
||||
"ui.sources_dashboard.title": "Sources Dashboard",
|
||||
"ui.sources_dashboard.verifying": "Verifying...",
|
||||
"ui.sources_dashboard.verify_24h": "Verify last 24h",
|
||||
@@ -269,7 +249,6 @@
|
||||
"ui.sources_dashboard.data_from": "Data from",
|
||||
"ui.sources_dashboard.to": "to",
|
||||
"ui.sources_dashboard.hour_window": "h window",
|
||||
|
||||
"ui.timeline.title": "Timeline",
|
||||
"ui.timeline.event_timeline": "Event Timeline",
|
||||
"ui.timeline.refresh_timeline": "Refresh timeline",
|
||||
@@ -280,7 +259,6 @@
|
||||
"ui.timeline.load_more": "Load more events",
|
||||
"ui.timeline.event_details": "Event details",
|
||||
"ui.timeline.events": "events",
|
||||
|
||||
"ui.exception_center.title": "Exception Center",
|
||||
"ui.exception_center.list_view": "List view",
|
||||
"ui.exception_center.kanban_view": "Kanban view",
|
||||
@@ -298,7 +276,6 @@
|
||||
"ui.exception_center.no_exceptions": "No exceptions match the current filters",
|
||||
"ui.exception_center.column_empty": "No exceptions",
|
||||
"ui.exception_center.exceptions_suffix": "exceptions",
|
||||
|
||||
"ui.evidence_thread.back_to_list": "Back to list",
|
||||
"ui.evidence_thread.title_default": "Evidence Thread",
|
||||
"ui.evidence_thread.copy_digest": "Copy full digest",
|
||||
@@ -309,7 +286,6 @@
|
||||
"ui.evidence_thread.timeline_tab": "Timeline",
|
||||
"ui.evidence_thread.transcript_tab": "Transcript",
|
||||
"ui.evidence_thread.not_found": "No evidence thread found for this artifact.",
|
||||
|
||||
"ui.vulnerability_detail.eyebrow": "Vulnerability",
|
||||
"ui.vulnerability_detail.cvss": "CVSS",
|
||||
"ui.vulnerability_detail.impact_first": "Impact First",
|
||||
@@ -328,5 +304,76 @@
|
||||
"ui.vulnerability_detail.evidence_tree": "Evidence Tree and Citation Links",
|
||||
"ui.vulnerability_detail.evidence_explorer": "evidence explorer",
|
||||
"ui.vulnerability_detail.references": "References",
|
||||
"ui.vulnerability_detail.back_to_risk": "Back to Risk"
|
||||
"ui.vulnerability_detail.back_to_risk": "Back to Risk",
|
||||
"ui.search.input_aria_label": "Global search",
|
||||
"ui.search.loading": "Searching...",
|
||||
"ui.search.no_results": "No results found",
|
||||
"ui.search.did_you_mean_label": "Did you mean:",
|
||||
"ui.search.try_also_label": "Try also:",
|
||||
"ui.search.recent_label": "Recent",
|
||||
"ui.search.clear_history": "Clear",
|
||||
"ui.search.suggested_label": "Suggested",
|
||||
"ui.search.empty_state_header": "Search across your release control plane",
|
||||
"ui.search.quick_action.getting_started": "Getting Started",
|
||||
"ui.search.quick_action.run_health_check": "Run Health Check",
|
||||
"ui.search.quick_action.view_recent_scans": "View Recent Scans",
|
||||
"ui.search.filter.all": "All",
|
||||
"ui.search.placeholder.default": "Search everything...",
|
||||
"ui.search.placeholder.try": "Try: {suggestion}",
|
||||
"ui.search.domain.findings.title": "Security Findings",
|
||||
"ui.search.domain.findings.description": "CVEs, vulnerabilities, and exposure data across your images.",
|
||||
"ui.search.domain.findings.example": "CVE-2024-21626",
|
||||
"ui.search.domain.vex.title": "VEX Statements",
|
||||
"ui.search.domain.vex.description": "Exploitability statuses and vendor assertions for vulnerabilities.",
|
||||
"ui.search.domain.vex.example": "not_affected",
|
||||
"ui.search.domain.policy.title": "Policy Rules",
|
||||
"ui.search.domain.policy.description": "Release gate rules, exceptions, and enforcement outcomes.",
|
||||
"ui.search.domain.policy.example": "DENY-CRITICAL-PROD",
|
||||
"ui.search.domain.docs.title": "Documentation",
|
||||
"ui.search.domain.docs.description": "Guides, architecture references, and operator runbooks.",
|
||||
"ui.search.domain.docs.example": "how to deploy",
|
||||
"ui.search.domain.api.title": "API Reference",
|
||||
"ui.search.domain.api.description": "OpenAPI endpoints, request contracts, and service operations.",
|
||||
"ui.search.domain.api.example": "POST /api/v1/scanner/scans",
|
||||
"ui.search.domain.health.title": "Health Checks",
|
||||
"ui.search.domain.health.description": "Doctor checks, readiness diagnostics, and remediation guidance.",
|
||||
"ui.search.domain.health.example": "database connectivity",
|
||||
"ui.search.domain.operations.title": "Operations",
|
||||
"ui.search.domain.operations.description": "Jobs, workflows, and operational controls across environments.",
|
||||
"ui.search.domain.operations.example": "blocked releases",
|
||||
"ui.search.domain.timeline.title": "Timeline",
|
||||
"ui.search.domain.timeline.description": "Promotion history, incidents, and causal event traces.",
|
||||
"ui.search.domain.timeline.example": "release history",
|
||||
"ui.search.suggestion.findings.critical": "critical findings",
|
||||
"ui.search.suggestion.findings.reachable": "reachable vulnerabilities",
|
||||
"ui.search.suggestion.findings.unresolved": "unresolved CVEs",
|
||||
"ui.search.suggestion.policy.failing_gates": "failing policy gates",
|
||||
"ui.search.suggestion.policy.production_deny": "production deny rules",
|
||||
"ui.search.suggestion.policy.exceptions": "policy exceptions",
|
||||
"ui.search.suggestion.doctor.database": "database connectivity",
|
||||
"ui.search.suggestion.doctor.disk": "disk space",
|
||||
"ui.search.suggestion.doctor.oidc": "OIDC readiness",
|
||||
"ui.search.suggestion.timeline.failed_deployments": "failed deployments",
|
||||
"ui.search.suggestion.timeline.recent_promotions": "recent promotions",
|
||||
"ui.search.suggestion.timeline.release_history": "release history",
|
||||
"ui.search.suggestion.releases.pending_approvals": "pending approvals",
|
||||
"ui.search.suggestion.releases.blocked_releases": "blocked releases",
|
||||
"ui.search.suggestion.releases.environment_status": "environment status",
|
||||
"ui.search.suggestion.default.deploy": "How do I deploy?",
|
||||
"ui.search.suggestion.default.vex": "What is a VEX statement?",
|
||||
"ui.search.suggestion.default.critical": "Show critical findings",
|
||||
"ui.chat.input.waiting": "Waiting for response...",
|
||||
"ui.chat.input.placeholder": "Ask AdvisoryAI about this finding...",
|
||||
"ui.chat.suggestion.vulnerability.exploitable": "Is this exploitable in my environment?",
|
||||
"ui.chat.suggestion.vulnerability.remediation": "What is the remediation?",
|
||||
"ui.chat.suggestion.vulnerability.evidence_chain": "Show me the evidence chain",
|
||||
"ui.chat.suggestion.vulnerability.draft_vex": "Draft a VEX statement",
|
||||
"ui.chat.suggestion.policy.explain_rule": "Explain this policy rule",
|
||||
"ui.chat.suggestion.policy.override_gate": "What would happen if I override this gate?",
|
||||
"ui.chat.suggestion.policy.recent_violations": "Show me recent policy violations",
|
||||
"ui.chat.suggestion.policy.add_exception": "How do I add an exception?",
|
||||
"ui.chat.suggestion.default.what_can_do": "What can Stella Ops do?",
|
||||
"ui.chat.suggestion.default.first_scan": "How do I set up my first scan?",
|
||||
"ui.chat.suggestion.default.promotion_workflow": "Explain the release promotion workflow",
|
||||
"ui.chat.suggestion.default.health_checks": "What health checks should I run first?"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"_meta": { "locale": "de-DE", "description": "Offline fallback bundle for StellaOps Console" },
|
||||
|
||||
"_meta": {
|
||||
"locale": "de-DE",
|
||||
"description": "Offline fallback bundle for StellaOps Console"
|
||||
},
|
||||
"common.error.generic": "Etwas ist schiefgelaufen.",
|
||||
"common.error.not_found": "The requested resource was not found.",
|
||||
"common.error.unauthorized": "You do not have permission to perform this action.",
|
||||
@@ -9,12 +11,10 @@
|
||||
"common.error.timeout": "Request timed out.",
|
||||
"common.error.server_error": "An internal server error occurred. Please try again later.",
|
||||
"common.error.network": "Network error. Check your connection.",
|
||||
|
||||
"common.validation.required": "This field is required.",
|
||||
"common.validation.invalid": "Invalid value.",
|
||||
"common.validation.too_long": "Maximum {max} characters allowed.",
|
||||
"common.validation.too_short": "Minimum {min} characters required.",
|
||||
|
||||
"common.actions.save": "Speichern",
|
||||
"common.actions.cancel": "Abbrechen",
|
||||
"common.actions.delete": "Delete",
|
||||
@@ -26,7 +26,6 @@
|
||||
"common.actions.collapse": "Collapse",
|
||||
"common.actions.show_more": "Show more",
|
||||
"common.actions.show_less": "Show less",
|
||||
|
||||
"common.status.healthy": "Healthy",
|
||||
"common.status.degraded": "Degraded",
|
||||
"common.status.unavailable": "Unavailable",
|
||||
@@ -39,16 +38,13 @@
|
||||
"common.status.failed": "Failed",
|
||||
"common.status.canceled": "Canceled",
|
||||
"common.status.blocked": "Blocked",
|
||||
|
||||
"common.severity.critical": "Critical",
|
||||
"common.severity.high": "High",
|
||||
"common.severity.medium": "Medium",
|
||||
"common.severity.low": "Low",
|
||||
"common.severity.info": "Info",
|
||||
"common.severity.none": "None",
|
||||
|
||||
"common.time.just_now": "Just now",
|
||||
|
||||
"common.ui.loading": "Loading...",
|
||||
"common.ui.saving": "Saving...",
|
||||
"common.ui.deleting": "Deleting...",
|
||||
@@ -57,11 +53,9 @@
|
||||
"common.ui.offline": "You are offline.",
|
||||
"common.ui.reconnecting": "Reconnecting...",
|
||||
"common.ui.back_online": "Back online.",
|
||||
|
||||
"ui.loading.skeleton": "Loading...",
|
||||
"ui.loading.spinner": "Please wait...",
|
||||
"ui.loading.slow": "This is taking longer than expected...",
|
||||
|
||||
"ui.error.generic": "Something went wrong.",
|
||||
"ui.error.network": "Network error. Check your connection.",
|
||||
"ui.error.timeout": "Request timed out. Please try again.",
|
||||
@@ -70,19 +64,16 @@
|
||||
"ui.error.server_error": "Server error. Please try again later.",
|
||||
"ui.error.try_again": "Try again",
|
||||
"ui.error.go_back": "Go back",
|
||||
|
||||
"ui.offline.banner": "You're offline.",
|
||||
"ui.offline.description": "Some features may be unavailable.",
|
||||
"ui.offline.reconnecting": "Reconnecting...",
|
||||
"ui.offline.reconnected": "Back online.",
|
||||
|
||||
"ui.toast.success": "Success",
|
||||
"ui.toast.info": "Info",
|
||||
"ui.toast.warning": "Warning",
|
||||
"ui.toast.error": "Error",
|
||||
"ui.toast.dismiss": "Dismiss",
|
||||
"ui.toast.undo": "Undo",
|
||||
|
||||
"ui.actions.save": "Save",
|
||||
"ui.actions.saving": "Saving...",
|
||||
"ui.actions.saved": "Saved",
|
||||
@@ -111,7 +102,6 @@
|
||||
"ui.actions.sign_in": "Sign in",
|
||||
"ui.actions.back_to_list": "Back to list",
|
||||
"ui.actions.load_more": "Load more",
|
||||
|
||||
"ui.labels.all": "All",
|
||||
"ui.labels.title": "Title",
|
||||
"ui.labels.description": "Description",
|
||||
@@ -131,14 +121,12 @@
|
||||
"ui.labels.selected": "selected",
|
||||
"ui.labels.last_updated": "Last updated:",
|
||||
"ui.labels.expires": "Expires",
|
||||
|
||||
"ui.validation.required": "This field is required.",
|
||||
"ui.validation.invalid": "Invalid value.",
|
||||
"ui.validation.too_long": "Maximum {max} characters allowed.",
|
||||
"ui.validation.too_short": "Minimum {min} characters required.",
|
||||
"ui.validation.invalid_email": "Please enter a valid email address.",
|
||||
"ui.validation.invalid_url": "Please enter a valid URL.",
|
||||
|
||||
"ui.a11y.loading": "Content is loading.",
|
||||
"ui.a11y.loaded": "Content loaded.",
|
||||
"ui.a11y.error": "An error occurred.",
|
||||
@@ -148,10 +136,8 @@
|
||||
"ui.a11y.deselected": "Deselected",
|
||||
"ui.a11y.required": "Required field",
|
||||
"ui.a11y.optional": "Optional",
|
||||
|
||||
"ui.motion.reduced": "Animations reduced.",
|
||||
"ui.motion.enabled": "Animations enabled.",
|
||||
|
||||
"ui.auth.fresh_active": "Fresh auth: Active",
|
||||
"ui.auth.fresh_stale": "Fresh auth: Stale",
|
||||
"ui.locale.label": "Sprache",
|
||||
@@ -171,15 +157,14 @@
|
||||
"ui.settings.language.persisted": "Fuer Ihr Konto gespeichert und im CLI wiederverwendet.",
|
||||
"ui.settings.language.persisted_error": "Lokal gespeichert, aber Kontosynchronisierung fehlgeschlagen.",
|
||||
"ui.settings.language.sign_in_hint": "Melden Sie sich an, um diese Einstellung mit dem CLI zu synchronisieren.",
|
||||
|
||||
"ui.first_signal.label": "First signal",
|
||||
"ui.first_signal.run_prefix": "Run:",
|
||||
"ui.first_signal.live": "Live",
|
||||
"ui.first_signal.polling": "Polling",
|
||||
"ui.first_signal.range_prefix": "Range",
|
||||
"ui.first_signal.range_separator": "\u2013",
|
||||
"ui.first_signal.stage_separator": " \u00b7 ",
|
||||
"ui.first_signal.waiting": "Warten auf erstes Signal\u2026",
|
||||
"ui.first_signal.range_separator": "–",
|
||||
"ui.first_signal.stage_separator": " · ",
|
||||
"ui.first_signal.waiting": "Warten auf erstes Signal…",
|
||||
"ui.first_signal.not_available": "Signal not available yet.",
|
||||
"ui.first_signal.offline": "Offline. Last known signal may be stale.",
|
||||
"ui.first_signal.failed": "Signal konnte nicht geladen werden.",
|
||||
@@ -202,20 +187,17 @@
|
||||
"ui.first_signal.stage.report": "Generating report",
|
||||
"ui.first_signal.stage.unknown": "Processing",
|
||||
"ui.first_signal.aria.card_label": "First signal status",
|
||||
|
||||
"ui.severity.critical": "Critical",
|
||||
"ui.severity.high": "High",
|
||||
"ui.severity.medium": "Medium",
|
||||
"ui.severity.low": "Low",
|
||||
"ui.severity.info": "Info",
|
||||
"ui.severity.none": "None",
|
||||
|
||||
"ui.release_orchestrator.title": "Release Orchestrator",
|
||||
"ui.release_orchestrator.subtitle": "Pipeline overview and release management",
|
||||
"ui.release_orchestrator.pipeline_runs": "Pipeline Runs",
|
||||
"ui.release_orchestrator.refresh_dashboard": "Refresh dashboard",
|
||||
|
||||
"ui.risk_dashboard.eyebrow": "Gateway \u00b7 Risk",
|
||||
"ui.risk_dashboard.eyebrow": "Gateway · Risk",
|
||||
"ui.risk_dashboard.title": "Risk Profiles",
|
||||
"ui.risk_dashboard.subtitle": "Tenant-scoped risk posture with deterministic ordering.",
|
||||
"ui.risk_dashboard.up_to_date": "Up to date",
|
||||
@@ -225,8 +207,7 @@
|
||||
"ui.risk_dashboard.risks_suffix": "risks.",
|
||||
"ui.risk_dashboard.error_unable_to_load": "Unable to load risk profiles.",
|
||||
"ui.risk_dashboard.no_risks_found": "No risks found for current filters.",
|
||||
"ui.risk_dashboard.loading_risks": "Loading risks\u2026",
|
||||
|
||||
"ui.risk_dashboard.loading_risks": "Loading risks…",
|
||||
"ui.findings.title": "Findings",
|
||||
"ui.findings.search_placeholder": "Search findings...",
|
||||
"ui.findings.clear_filters": "Clear Filters",
|
||||
@@ -242,7 +223,6 @@
|
||||
"ui.findings.select": "Select",
|
||||
"ui.findings.no_findings": "No findings to display.",
|
||||
"ui.findings.no_match": "No findings match the current filters.",
|
||||
|
||||
"ui.sources_dashboard.title": "Sources Dashboard",
|
||||
"ui.sources_dashboard.verifying": "Verifying...",
|
||||
"ui.sources_dashboard.verify_24h": "Verify last 24h",
|
||||
@@ -269,7 +249,6 @@
|
||||
"ui.sources_dashboard.data_from": "Data from",
|
||||
"ui.sources_dashboard.to": "to",
|
||||
"ui.sources_dashboard.hour_window": "h window",
|
||||
|
||||
"ui.timeline.title": "Timeline",
|
||||
"ui.timeline.event_timeline": "Event Timeline",
|
||||
"ui.timeline.refresh_timeline": "Refresh timeline",
|
||||
@@ -280,7 +259,6 @@
|
||||
"ui.timeline.load_more": "Load more events",
|
||||
"ui.timeline.event_details": "Event details",
|
||||
"ui.timeline.events": "events",
|
||||
|
||||
"ui.exception_center.title": "Exception Center",
|
||||
"ui.exception_center.list_view": "List view",
|
||||
"ui.exception_center.kanban_view": "Kanban view",
|
||||
@@ -298,7 +276,6 @@
|
||||
"ui.exception_center.no_exceptions": "No exceptions match the current filters",
|
||||
"ui.exception_center.column_empty": "No exceptions",
|
||||
"ui.exception_center.exceptions_suffix": "exceptions",
|
||||
|
||||
"ui.evidence_thread.back_to_list": "Back to list",
|
||||
"ui.evidence_thread.title_default": "Evidence Thread",
|
||||
"ui.evidence_thread.copy_digest": "Copy full digest",
|
||||
@@ -309,7 +286,6 @@
|
||||
"ui.evidence_thread.timeline_tab": "Timeline",
|
||||
"ui.evidence_thread.transcript_tab": "Transcript",
|
||||
"ui.evidence_thread.not_found": "No evidence thread found for this artifact.",
|
||||
|
||||
"ui.vulnerability_detail.eyebrow": "Vulnerability",
|
||||
"ui.vulnerability_detail.cvss": "CVSS",
|
||||
"ui.vulnerability_detail.impact_first": "Impact First",
|
||||
@@ -328,5 +304,76 @@
|
||||
"ui.vulnerability_detail.evidence_tree": "Evidence Tree and Citation Links",
|
||||
"ui.vulnerability_detail.evidence_explorer": "evidence explorer",
|
||||
"ui.vulnerability_detail.references": "References",
|
||||
"ui.vulnerability_detail.back_to_risk": "Back to Risk"
|
||||
"ui.vulnerability_detail.back_to_risk": "Back to Risk",
|
||||
"ui.search.input_aria_label": "Global search",
|
||||
"ui.search.loading": "Searching...",
|
||||
"ui.search.no_results": "No results found",
|
||||
"ui.search.did_you_mean_label": "Did you mean:",
|
||||
"ui.search.try_also_label": "Try also:",
|
||||
"ui.search.recent_label": "Recent",
|
||||
"ui.search.clear_history": "Clear",
|
||||
"ui.search.suggested_label": "Suggested",
|
||||
"ui.search.empty_state_header": "Search across your release control plane",
|
||||
"ui.search.quick_action.getting_started": "Getting Started",
|
||||
"ui.search.quick_action.run_health_check": "Run Health Check",
|
||||
"ui.search.quick_action.view_recent_scans": "View Recent Scans",
|
||||
"ui.search.filter.all": "All",
|
||||
"ui.search.placeholder.default": "Search everything...",
|
||||
"ui.search.placeholder.try": "Try: {suggestion}",
|
||||
"ui.search.domain.findings.title": "Security Findings",
|
||||
"ui.search.domain.findings.description": "CVEs, vulnerabilities, and exposure data across your images.",
|
||||
"ui.search.domain.findings.example": "CVE-2024-21626",
|
||||
"ui.search.domain.vex.title": "VEX Statements",
|
||||
"ui.search.domain.vex.description": "Exploitability statuses and vendor assertions for vulnerabilities.",
|
||||
"ui.search.domain.vex.example": "not_affected",
|
||||
"ui.search.domain.policy.title": "Policy Rules",
|
||||
"ui.search.domain.policy.description": "Release gate rules, exceptions, and enforcement outcomes.",
|
||||
"ui.search.domain.policy.example": "DENY-CRITICAL-PROD",
|
||||
"ui.search.domain.docs.title": "Documentation",
|
||||
"ui.search.domain.docs.description": "Guides, architecture references, and operator runbooks.",
|
||||
"ui.search.domain.docs.example": "how to deploy",
|
||||
"ui.search.domain.api.title": "API Reference",
|
||||
"ui.search.domain.api.description": "OpenAPI endpoints, request contracts, and service operations.",
|
||||
"ui.search.domain.api.example": "POST /api/v1/scanner/scans",
|
||||
"ui.search.domain.health.title": "Health Checks",
|
||||
"ui.search.domain.health.description": "Doctor checks, readiness diagnostics, and remediation guidance.",
|
||||
"ui.search.domain.health.example": "database connectivity",
|
||||
"ui.search.domain.operations.title": "Operations",
|
||||
"ui.search.domain.operations.description": "Jobs, workflows, and operational controls across environments.",
|
||||
"ui.search.domain.operations.example": "blocked releases",
|
||||
"ui.search.domain.timeline.title": "Timeline",
|
||||
"ui.search.domain.timeline.description": "Promotion history, incidents, and causal event traces.",
|
||||
"ui.search.domain.timeline.example": "release history",
|
||||
"ui.search.suggestion.findings.critical": "critical findings",
|
||||
"ui.search.suggestion.findings.reachable": "reachable vulnerabilities",
|
||||
"ui.search.suggestion.findings.unresolved": "unresolved CVEs",
|
||||
"ui.search.suggestion.policy.failing_gates": "failing policy gates",
|
||||
"ui.search.suggestion.policy.production_deny": "production deny rules",
|
||||
"ui.search.suggestion.policy.exceptions": "policy exceptions",
|
||||
"ui.search.suggestion.doctor.database": "database connectivity",
|
||||
"ui.search.suggestion.doctor.disk": "disk space",
|
||||
"ui.search.suggestion.doctor.oidc": "OIDC readiness",
|
||||
"ui.search.suggestion.timeline.failed_deployments": "failed deployments",
|
||||
"ui.search.suggestion.timeline.recent_promotions": "recent promotions",
|
||||
"ui.search.suggestion.timeline.release_history": "release history",
|
||||
"ui.search.suggestion.releases.pending_approvals": "pending approvals",
|
||||
"ui.search.suggestion.releases.blocked_releases": "blocked releases",
|
||||
"ui.search.suggestion.releases.environment_status": "environment status",
|
||||
"ui.search.suggestion.default.deploy": "How do I deploy?",
|
||||
"ui.search.suggestion.default.vex": "What is a VEX statement?",
|
||||
"ui.search.suggestion.default.critical": "Show critical findings",
|
||||
"ui.chat.input.waiting": "Waiting for response...",
|
||||
"ui.chat.input.placeholder": "Ask AdvisoryAI about this finding...",
|
||||
"ui.chat.suggestion.vulnerability.exploitable": "Is this exploitable in my environment?",
|
||||
"ui.chat.suggestion.vulnerability.remediation": "What is the remediation?",
|
||||
"ui.chat.suggestion.vulnerability.evidence_chain": "Show me the evidence chain",
|
||||
"ui.chat.suggestion.vulnerability.draft_vex": "Draft a VEX statement",
|
||||
"ui.chat.suggestion.policy.explain_rule": "Explain this policy rule",
|
||||
"ui.chat.suggestion.policy.override_gate": "What would happen if I override this gate?",
|
||||
"ui.chat.suggestion.policy.recent_violations": "Show me recent policy violations",
|
||||
"ui.chat.suggestion.policy.add_exception": "How do I add an exception?",
|
||||
"ui.chat.suggestion.default.what_can_do": "What can Stella Ops do?",
|
||||
"ui.chat.suggestion.default.first_scan": "How do I set up my first scan?",
|
||||
"ui.chat.suggestion.default.promotion_workflow": "Explain the release promotion workflow",
|
||||
"ui.chat.suggestion.default.health_checks": "What health checks should I run first?"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"_meta": { "locale": "en-US", "description": "Offline fallback bundle for StellaOps Console" },
|
||||
|
||||
"_meta": {
|
||||
"locale": "en-US",
|
||||
"description": "Offline fallback bundle for StellaOps Console"
|
||||
},
|
||||
"common.error.generic": "Something went wrong.",
|
||||
"common.error.not_found": "The requested resource was not found.",
|
||||
"common.error.unauthorized": "You do not have permission to perform this action.",
|
||||
@@ -9,12 +11,10 @@
|
||||
"common.error.timeout": "Request timed out.",
|
||||
"common.error.server_error": "An internal server error occurred. Please try again later.",
|
||||
"common.error.network": "Network error. Check your connection.",
|
||||
|
||||
"common.validation.required": "This field is required.",
|
||||
"common.validation.invalid": "Invalid value.",
|
||||
"common.validation.too_long": "Maximum {max} characters allowed.",
|
||||
"common.validation.too_short": "Minimum {min} characters required.",
|
||||
|
||||
"common.actions.save": "Save",
|
||||
"common.actions.cancel": "Cancel",
|
||||
"common.actions.delete": "Delete",
|
||||
@@ -26,7 +26,6 @@
|
||||
"common.actions.collapse": "Collapse",
|
||||
"common.actions.show_more": "Show more",
|
||||
"common.actions.show_less": "Show less",
|
||||
|
||||
"common.status.healthy": "Healthy",
|
||||
"common.status.degraded": "Degraded",
|
||||
"common.status.unavailable": "Unavailable",
|
||||
@@ -39,16 +38,13 @@
|
||||
"common.status.failed": "Failed",
|
||||
"common.status.canceled": "Canceled",
|
||||
"common.status.blocked": "Blocked",
|
||||
|
||||
"common.severity.critical": "Critical",
|
||||
"common.severity.high": "High",
|
||||
"common.severity.medium": "Medium",
|
||||
"common.severity.low": "Low",
|
||||
"common.severity.info": "Info",
|
||||
"common.severity.none": "None",
|
||||
|
||||
"common.time.just_now": "Just now",
|
||||
|
||||
"common.ui.loading": "Loading...",
|
||||
"common.ui.saving": "Saving...",
|
||||
"common.ui.deleting": "Deleting...",
|
||||
@@ -57,11 +53,9 @@
|
||||
"common.ui.offline": "You are offline.",
|
||||
"common.ui.reconnecting": "Reconnecting...",
|
||||
"common.ui.back_online": "Back online.",
|
||||
|
||||
"ui.loading.skeleton": "Loading...",
|
||||
"ui.loading.spinner": "Please wait...",
|
||||
"ui.loading.slow": "This is taking longer than expected...",
|
||||
|
||||
"ui.error.generic": "Something went wrong.",
|
||||
"ui.error.network": "Network error. Check your connection.",
|
||||
"ui.error.timeout": "Request timed out. Please try again.",
|
||||
@@ -70,19 +64,16 @@
|
||||
"ui.error.server_error": "Server error. Please try again later.",
|
||||
"ui.error.try_again": "Try again",
|
||||
"ui.error.go_back": "Go back",
|
||||
|
||||
"ui.offline.banner": "You're offline.",
|
||||
"ui.offline.description": "Some features may be unavailable.",
|
||||
"ui.offline.reconnecting": "Reconnecting...",
|
||||
"ui.offline.reconnected": "Back online.",
|
||||
|
||||
"ui.toast.success": "Success",
|
||||
"ui.toast.info": "Info",
|
||||
"ui.toast.warning": "Warning",
|
||||
"ui.toast.error": "Error",
|
||||
"ui.toast.dismiss": "Dismiss",
|
||||
"ui.toast.undo": "Undo",
|
||||
|
||||
"ui.actions.save": "Save",
|
||||
"ui.actions.saving": "Saving...",
|
||||
"ui.actions.saved": "Saved",
|
||||
@@ -111,7 +102,6 @@
|
||||
"ui.actions.sign_in": "Sign in",
|
||||
"ui.actions.back_to_list": "Back to list",
|
||||
"ui.actions.load_more": "Load more",
|
||||
|
||||
"ui.labels.all": "All",
|
||||
"ui.labels.title": "Title",
|
||||
"ui.labels.description": "Description",
|
||||
@@ -131,14 +121,12 @@
|
||||
"ui.labels.selected": "selected",
|
||||
"ui.labels.last_updated": "Last updated:",
|
||||
"ui.labels.expires": "Expires",
|
||||
|
||||
"ui.validation.required": "This field is required.",
|
||||
"ui.validation.invalid": "Invalid value.",
|
||||
"ui.validation.too_long": "Maximum {max} characters allowed.",
|
||||
"ui.validation.too_short": "Minimum {min} characters required.",
|
||||
"ui.validation.invalid_email": "Please enter a valid email address.",
|
||||
"ui.validation.invalid_url": "Please enter a valid URL.",
|
||||
|
||||
"ui.a11y.loading": "Content is loading.",
|
||||
"ui.a11y.loaded": "Content loaded.",
|
||||
"ui.a11y.error": "An error occurred.",
|
||||
@@ -148,10 +136,8 @@
|
||||
"ui.a11y.deselected": "Deselected",
|
||||
"ui.a11y.required": "Required field",
|
||||
"ui.a11y.optional": "Optional",
|
||||
|
||||
"ui.motion.reduced": "Animations reduced.",
|
||||
"ui.motion.enabled": "Animations enabled.",
|
||||
|
||||
"ui.auth.fresh_active": "Fresh auth: Active",
|
||||
"ui.auth.fresh_stale": "Fresh auth: Stale",
|
||||
"ui.locale.label": "Locale",
|
||||
@@ -171,15 +157,14 @@
|
||||
"ui.settings.language.persisted": "Saved for your account and reused by CLI.",
|
||||
"ui.settings.language.persisted_error": "Saved locally, but account sync failed.",
|
||||
"ui.settings.language.sign_in_hint": "Sign in to sync this preference with CLI.",
|
||||
|
||||
"ui.first_signal.label": "First signal",
|
||||
"ui.first_signal.run_prefix": "Run:",
|
||||
"ui.first_signal.live": "Live",
|
||||
"ui.first_signal.polling": "Polling",
|
||||
"ui.first_signal.range_prefix": "Range",
|
||||
"ui.first_signal.range_separator": "\u2013",
|
||||
"ui.first_signal.stage_separator": " \u00b7 ",
|
||||
"ui.first_signal.waiting": "Waiting for first signal\u2026",
|
||||
"ui.first_signal.range_separator": "–",
|
||||
"ui.first_signal.stage_separator": " · ",
|
||||
"ui.first_signal.waiting": "Waiting for first signal…",
|
||||
"ui.first_signal.not_available": "Signal not available yet.",
|
||||
"ui.first_signal.offline": "Offline. Last known signal may be stale.",
|
||||
"ui.first_signal.failed": "Failed to load signal.",
|
||||
@@ -202,20 +187,17 @@
|
||||
"ui.first_signal.stage.report": "Generating report",
|
||||
"ui.first_signal.stage.unknown": "Processing",
|
||||
"ui.first_signal.aria.card_label": "First signal status",
|
||||
|
||||
"ui.severity.critical": "Critical",
|
||||
"ui.severity.high": "High",
|
||||
"ui.severity.medium": "Medium",
|
||||
"ui.severity.low": "Low",
|
||||
"ui.severity.info": "Info",
|
||||
"ui.severity.none": "None",
|
||||
|
||||
"ui.release_orchestrator.title": "Release Orchestrator",
|
||||
"ui.release_orchestrator.subtitle": "Pipeline overview and release management",
|
||||
"ui.release_orchestrator.pipeline_runs": "Pipeline Runs",
|
||||
"ui.release_orchestrator.refresh_dashboard": "Refresh dashboard",
|
||||
|
||||
"ui.risk_dashboard.eyebrow": "Gateway \u00b7 Risk",
|
||||
"ui.risk_dashboard.eyebrow": "Gateway · Risk",
|
||||
"ui.risk_dashboard.title": "Risk Profiles",
|
||||
"ui.risk_dashboard.subtitle": "Tenant-scoped risk posture with deterministic ordering.",
|
||||
"ui.risk_dashboard.up_to_date": "Up to date",
|
||||
@@ -225,8 +207,7 @@
|
||||
"ui.risk_dashboard.risks_suffix": "risks.",
|
||||
"ui.risk_dashboard.error_unable_to_load": "Unable to load risk profiles.",
|
||||
"ui.risk_dashboard.no_risks_found": "No risks found for current filters.",
|
||||
"ui.risk_dashboard.loading_risks": "Loading risks\u2026",
|
||||
|
||||
"ui.risk_dashboard.loading_risks": "Loading risks…",
|
||||
"ui.findings.title": "Findings",
|
||||
"ui.findings.search_placeholder": "Search findings...",
|
||||
"ui.findings.clear_filters": "Clear Filters",
|
||||
@@ -242,7 +223,6 @@
|
||||
"ui.findings.select": "Select",
|
||||
"ui.findings.no_findings": "No findings to display.",
|
||||
"ui.findings.no_match": "No findings match the current filters.",
|
||||
|
||||
"ui.sources_dashboard.title": "Sources Dashboard",
|
||||
"ui.sources_dashboard.verifying": "Verifying...",
|
||||
"ui.sources_dashboard.verify_24h": "Verify last 24h",
|
||||
@@ -269,7 +249,6 @@
|
||||
"ui.sources_dashboard.data_from": "Data from",
|
||||
"ui.sources_dashboard.to": "to",
|
||||
"ui.sources_dashboard.hour_window": "h window",
|
||||
|
||||
"ui.timeline.title": "Timeline",
|
||||
"ui.timeline.event_timeline": "Event Timeline",
|
||||
"ui.timeline.refresh_timeline": "Refresh timeline",
|
||||
@@ -280,7 +259,6 @@
|
||||
"ui.timeline.load_more": "Load more events",
|
||||
"ui.timeline.event_details": "Event details",
|
||||
"ui.timeline.events": "events",
|
||||
|
||||
"ui.exception_center.title": "Exception Center",
|
||||
"ui.exception_center.list_view": "List view",
|
||||
"ui.exception_center.kanban_view": "Kanban view",
|
||||
@@ -298,7 +276,6 @@
|
||||
"ui.exception_center.no_exceptions": "No exceptions match the current filters",
|
||||
"ui.exception_center.column_empty": "No exceptions",
|
||||
"ui.exception_center.exceptions_suffix": "exceptions",
|
||||
|
||||
"ui.evidence_thread.back_to_list": "Back to list",
|
||||
"ui.evidence_thread.title_default": "Evidence Thread",
|
||||
"ui.evidence_thread.copy_digest": "Copy full digest",
|
||||
@@ -309,7 +286,6 @@
|
||||
"ui.evidence_thread.timeline_tab": "Timeline",
|
||||
"ui.evidence_thread.transcript_tab": "Transcript",
|
||||
"ui.evidence_thread.not_found": "No evidence thread found for this artifact.",
|
||||
|
||||
"ui.vulnerability_detail.eyebrow": "Vulnerability",
|
||||
"ui.vulnerability_detail.cvss": "CVSS",
|
||||
"ui.vulnerability_detail.impact_first": "Impact First",
|
||||
@@ -328,5 +304,76 @@
|
||||
"ui.vulnerability_detail.evidence_tree": "Evidence Tree and Citation Links",
|
||||
"ui.vulnerability_detail.evidence_explorer": "evidence explorer",
|
||||
"ui.vulnerability_detail.references": "References",
|
||||
"ui.vulnerability_detail.back_to_risk": "Back to Risk"
|
||||
"ui.vulnerability_detail.back_to_risk": "Back to Risk",
|
||||
"ui.search.input_aria_label": "Global search",
|
||||
"ui.search.loading": "Searching...",
|
||||
"ui.search.no_results": "No results found",
|
||||
"ui.search.did_you_mean_label": "Did you mean:",
|
||||
"ui.search.try_also_label": "Try also:",
|
||||
"ui.search.recent_label": "Recent",
|
||||
"ui.search.clear_history": "Clear",
|
||||
"ui.search.suggested_label": "Suggested",
|
||||
"ui.search.empty_state_header": "Search across your release control plane",
|
||||
"ui.search.quick_action.getting_started": "Getting Started",
|
||||
"ui.search.quick_action.run_health_check": "Run Health Check",
|
||||
"ui.search.quick_action.view_recent_scans": "View Recent Scans",
|
||||
"ui.search.filter.all": "All",
|
||||
"ui.search.placeholder.default": "Search everything...",
|
||||
"ui.search.placeholder.try": "Try: {suggestion}",
|
||||
"ui.search.domain.findings.title": "Security Findings",
|
||||
"ui.search.domain.findings.description": "CVEs, vulnerabilities, and exposure data across your images.",
|
||||
"ui.search.domain.findings.example": "CVE-2024-21626",
|
||||
"ui.search.domain.vex.title": "VEX Statements",
|
||||
"ui.search.domain.vex.description": "Exploitability statuses and vendor assertions for vulnerabilities.",
|
||||
"ui.search.domain.vex.example": "not_affected",
|
||||
"ui.search.domain.policy.title": "Policy Rules",
|
||||
"ui.search.domain.policy.description": "Release gate rules, exceptions, and enforcement outcomes.",
|
||||
"ui.search.domain.policy.example": "DENY-CRITICAL-PROD",
|
||||
"ui.search.domain.docs.title": "Documentation",
|
||||
"ui.search.domain.docs.description": "Guides, architecture references, and operator runbooks.",
|
||||
"ui.search.domain.docs.example": "how to deploy",
|
||||
"ui.search.domain.api.title": "API Reference",
|
||||
"ui.search.domain.api.description": "OpenAPI endpoints, request contracts, and service operations.",
|
||||
"ui.search.domain.api.example": "POST /api/v1/scanner/scans",
|
||||
"ui.search.domain.health.title": "Health Checks",
|
||||
"ui.search.domain.health.description": "Doctor checks, readiness diagnostics, and remediation guidance.",
|
||||
"ui.search.domain.health.example": "database connectivity",
|
||||
"ui.search.domain.operations.title": "Operations",
|
||||
"ui.search.domain.operations.description": "Jobs, workflows, and operational controls across environments.",
|
||||
"ui.search.domain.operations.example": "blocked releases",
|
||||
"ui.search.domain.timeline.title": "Timeline",
|
||||
"ui.search.domain.timeline.description": "Promotion history, incidents, and causal event traces.",
|
||||
"ui.search.domain.timeline.example": "release history",
|
||||
"ui.search.suggestion.findings.critical": "critical findings",
|
||||
"ui.search.suggestion.findings.reachable": "reachable vulnerabilities",
|
||||
"ui.search.suggestion.findings.unresolved": "unresolved CVEs",
|
||||
"ui.search.suggestion.policy.failing_gates": "failing policy gates",
|
||||
"ui.search.suggestion.policy.production_deny": "production deny rules",
|
||||
"ui.search.suggestion.policy.exceptions": "policy exceptions",
|
||||
"ui.search.suggestion.doctor.database": "database connectivity",
|
||||
"ui.search.suggestion.doctor.disk": "disk space",
|
||||
"ui.search.suggestion.doctor.oidc": "OIDC readiness",
|
||||
"ui.search.suggestion.timeline.failed_deployments": "failed deployments",
|
||||
"ui.search.suggestion.timeline.recent_promotions": "recent promotions",
|
||||
"ui.search.suggestion.timeline.release_history": "release history",
|
||||
"ui.search.suggestion.releases.pending_approvals": "pending approvals",
|
||||
"ui.search.suggestion.releases.blocked_releases": "blocked releases",
|
||||
"ui.search.suggestion.releases.environment_status": "environment status",
|
||||
"ui.search.suggestion.default.deploy": "How do I deploy?",
|
||||
"ui.search.suggestion.default.vex": "What is a VEX statement?",
|
||||
"ui.search.suggestion.default.critical": "Show critical findings",
|
||||
"ui.chat.input.waiting": "Waiting for response...",
|
||||
"ui.chat.input.placeholder": "Ask AdvisoryAI about this finding...",
|
||||
"ui.chat.suggestion.vulnerability.exploitable": "Is this exploitable in my environment?",
|
||||
"ui.chat.suggestion.vulnerability.remediation": "What is the remediation?",
|
||||
"ui.chat.suggestion.vulnerability.evidence_chain": "Show me the evidence chain",
|
||||
"ui.chat.suggestion.vulnerability.draft_vex": "Draft a VEX statement",
|
||||
"ui.chat.suggestion.policy.explain_rule": "Explain this policy rule",
|
||||
"ui.chat.suggestion.policy.override_gate": "What would happen if I override this gate?",
|
||||
"ui.chat.suggestion.policy.recent_violations": "Show me recent policy violations",
|
||||
"ui.chat.suggestion.policy.add_exception": "How do I add an exception?",
|
||||
"ui.chat.suggestion.default.what_can_do": "What can Stella Ops do?",
|
||||
"ui.chat.suggestion.default.first_scan": "How do I set up my first scan?",
|
||||
"ui.chat.suggestion.default.promotion_workflow": "Explain the release promotion workflow",
|
||||
"ui.chat.suggestion.default.health_checks": "What health checks should I run first?"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"_meta": { "locale": "es-ES", "description": "Offline fallback bundle for StellaOps Console" },
|
||||
|
||||
"_meta": {
|
||||
"locale": "es-ES",
|
||||
"description": "Offline fallback bundle for StellaOps Console"
|
||||
},
|
||||
"common.error.generic": "Something went wrong.",
|
||||
"common.error.not_found": "The requested resource was not found.",
|
||||
"common.error.unauthorized": "You do not have permission to perform this action.",
|
||||
@@ -9,12 +11,10 @@
|
||||
"common.error.timeout": "Request timed out.",
|
||||
"common.error.server_error": "An internal server error occurred. Please try again later.",
|
||||
"common.error.network": "Network error. Check your connection.",
|
||||
|
||||
"common.validation.required": "This field is required.",
|
||||
"common.validation.invalid": "Invalid value.",
|
||||
"common.validation.too_long": "Maximum {max} characters allowed.",
|
||||
"common.validation.too_short": "Minimum {min} characters required.",
|
||||
|
||||
"common.actions.save": "Save",
|
||||
"common.actions.cancel": "Cancel",
|
||||
"common.actions.delete": "Delete",
|
||||
@@ -26,7 +26,6 @@
|
||||
"common.actions.collapse": "Collapse",
|
||||
"common.actions.show_more": "Show more",
|
||||
"common.actions.show_less": "Show less",
|
||||
|
||||
"common.status.healthy": "Healthy",
|
||||
"common.status.degraded": "Degraded",
|
||||
"common.status.unavailable": "Unavailable",
|
||||
@@ -39,16 +38,13 @@
|
||||
"common.status.failed": "Failed",
|
||||
"common.status.canceled": "Canceled",
|
||||
"common.status.blocked": "Blocked",
|
||||
|
||||
"common.severity.critical": "Critical",
|
||||
"common.severity.high": "High",
|
||||
"common.severity.medium": "Medium",
|
||||
"common.severity.low": "Low",
|
||||
"common.severity.info": "Info",
|
||||
"common.severity.none": "None",
|
||||
|
||||
"common.time.just_now": "Just now",
|
||||
|
||||
"common.ui.loading": "Loading...",
|
||||
"common.ui.saving": "Saving...",
|
||||
"common.ui.deleting": "Deleting...",
|
||||
@@ -57,11 +53,9 @@
|
||||
"common.ui.offline": "You are offline.",
|
||||
"common.ui.reconnecting": "Reconnecting...",
|
||||
"common.ui.back_online": "Back online.",
|
||||
|
||||
"ui.loading.skeleton": "Loading...",
|
||||
"ui.loading.spinner": "Please wait...",
|
||||
"ui.loading.slow": "This is taking longer than expected...",
|
||||
|
||||
"ui.error.generic": "Something went wrong.",
|
||||
"ui.error.network": "Network error. Check your connection.",
|
||||
"ui.error.timeout": "Request timed out. Please try again.",
|
||||
@@ -70,19 +64,16 @@
|
||||
"ui.error.server_error": "Server error. Please try again later.",
|
||||
"ui.error.try_again": "Try again",
|
||||
"ui.error.go_back": "Go back",
|
||||
|
||||
"ui.offline.banner": "You're offline.",
|
||||
"ui.offline.description": "Some features may be unavailable.",
|
||||
"ui.offline.reconnecting": "Reconnecting...",
|
||||
"ui.offline.reconnected": "Back online.",
|
||||
|
||||
"ui.toast.success": "Success",
|
||||
"ui.toast.info": "Info",
|
||||
"ui.toast.warning": "Warning",
|
||||
"ui.toast.error": "Error",
|
||||
"ui.toast.dismiss": "Dismiss",
|
||||
"ui.toast.undo": "Undo",
|
||||
|
||||
"ui.actions.save": "Save",
|
||||
"ui.actions.saving": "Saving...",
|
||||
"ui.actions.saved": "Saved",
|
||||
@@ -111,7 +102,6 @@
|
||||
"ui.actions.sign_in": "Sign in",
|
||||
"ui.actions.back_to_list": "Back to list",
|
||||
"ui.actions.load_more": "Load more",
|
||||
|
||||
"ui.labels.all": "All",
|
||||
"ui.labels.title": "Title",
|
||||
"ui.labels.description": "Description",
|
||||
@@ -131,14 +121,12 @@
|
||||
"ui.labels.selected": "selected",
|
||||
"ui.labels.last_updated": "Last updated:",
|
||||
"ui.labels.expires": "Expires",
|
||||
|
||||
"ui.validation.required": "This field is required.",
|
||||
"ui.validation.invalid": "Invalid value.",
|
||||
"ui.validation.too_long": "Maximum {max} characters allowed.",
|
||||
"ui.validation.too_short": "Minimum {min} characters required.",
|
||||
"ui.validation.invalid_email": "Please enter a valid email address.",
|
||||
"ui.validation.invalid_url": "Please enter a valid URL.",
|
||||
|
||||
"ui.a11y.loading": "Content is loading.",
|
||||
"ui.a11y.loaded": "Content loaded.",
|
||||
"ui.a11y.error": "An error occurred.",
|
||||
@@ -148,10 +136,8 @@
|
||||
"ui.a11y.deselected": "Deselected",
|
||||
"ui.a11y.required": "Required field",
|
||||
"ui.a11y.optional": "Optional",
|
||||
|
||||
"ui.motion.reduced": "Animations reduced.",
|
||||
"ui.motion.enabled": "Animations enabled.",
|
||||
|
||||
"ui.auth.fresh_active": "Fresh auth: Active",
|
||||
"ui.auth.fresh_stale": "Fresh auth: Stale",
|
||||
"ui.locale.label": "Idioma",
|
||||
@@ -171,15 +157,14 @@
|
||||
"ui.settings.language.persisted": "Guardado para tu cuenta y reutilizado por CLI.",
|
||||
"ui.settings.language.persisted_error": "Guardado localmente, pero fallo la sincronizacion de la cuenta.",
|
||||
"ui.settings.language.sign_in_hint": "Inicia sesion para sincronizar esta preferencia con CLI.",
|
||||
|
||||
"ui.first_signal.label": "First signal",
|
||||
"ui.first_signal.run_prefix": "Run:",
|
||||
"ui.first_signal.live": "Live",
|
||||
"ui.first_signal.polling": "Polling",
|
||||
"ui.first_signal.range_prefix": "Range",
|
||||
"ui.first_signal.range_separator": "\u2013",
|
||||
"ui.first_signal.stage_separator": " \u00b7 ",
|
||||
"ui.first_signal.waiting": "Waiting for first signal\u2026",
|
||||
"ui.first_signal.range_separator": "–",
|
||||
"ui.first_signal.stage_separator": " · ",
|
||||
"ui.first_signal.waiting": "Waiting for first signal…",
|
||||
"ui.first_signal.not_available": "Signal not available yet.",
|
||||
"ui.first_signal.offline": "Offline. Last known signal may be stale.",
|
||||
"ui.first_signal.failed": "Failed to load signal.",
|
||||
@@ -202,20 +187,17 @@
|
||||
"ui.first_signal.stage.report": "Generating report",
|
||||
"ui.first_signal.stage.unknown": "Processing",
|
||||
"ui.first_signal.aria.card_label": "First signal status",
|
||||
|
||||
"ui.severity.critical": "Critical",
|
||||
"ui.severity.high": "High",
|
||||
"ui.severity.medium": "Medium",
|
||||
"ui.severity.low": "Low",
|
||||
"ui.severity.info": "Info",
|
||||
"ui.severity.none": "None",
|
||||
|
||||
"ui.release_orchestrator.title": "Release Orchestrator",
|
||||
"ui.release_orchestrator.subtitle": "Pipeline overview and release management",
|
||||
"ui.release_orchestrator.pipeline_runs": "Pipeline Runs",
|
||||
"ui.release_orchestrator.refresh_dashboard": "Refresh dashboard",
|
||||
|
||||
"ui.risk_dashboard.eyebrow": "Gateway \u00b7 Risk",
|
||||
"ui.risk_dashboard.eyebrow": "Gateway · Risk",
|
||||
"ui.risk_dashboard.title": "Risk Profiles",
|
||||
"ui.risk_dashboard.subtitle": "Tenant-scoped risk posture with deterministic ordering.",
|
||||
"ui.risk_dashboard.up_to_date": "Up to date",
|
||||
@@ -225,8 +207,7 @@
|
||||
"ui.risk_dashboard.risks_suffix": "risks.",
|
||||
"ui.risk_dashboard.error_unable_to_load": "Unable to load risk profiles.",
|
||||
"ui.risk_dashboard.no_risks_found": "No risks found for current filters.",
|
||||
"ui.risk_dashboard.loading_risks": "Loading risks\u2026",
|
||||
|
||||
"ui.risk_dashboard.loading_risks": "Loading risks…",
|
||||
"ui.findings.title": "Findings",
|
||||
"ui.findings.search_placeholder": "Search findings...",
|
||||
"ui.findings.clear_filters": "Clear Filters",
|
||||
@@ -242,7 +223,6 @@
|
||||
"ui.findings.select": "Select",
|
||||
"ui.findings.no_findings": "No findings to display.",
|
||||
"ui.findings.no_match": "No findings match the current filters.",
|
||||
|
||||
"ui.sources_dashboard.title": "Sources Dashboard",
|
||||
"ui.sources_dashboard.verifying": "Verifying...",
|
||||
"ui.sources_dashboard.verify_24h": "Verify last 24h",
|
||||
@@ -269,7 +249,6 @@
|
||||
"ui.sources_dashboard.data_from": "Data from",
|
||||
"ui.sources_dashboard.to": "to",
|
||||
"ui.sources_dashboard.hour_window": "h window",
|
||||
|
||||
"ui.timeline.title": "Timeline",
|
||||
"ui.timeline.event_timeline": "Event Timeline",
|
||||
"ui.timeline.refresh_timeline": "Refresh timeline",
|
||||
@@ -280,7 +259,6 @@
|
||||
"ui.timeline.load_more": "Load more events",
|
||||
"ui.timeline.event_details": "Event details",
|
||||
"ui.timeline.events": "events",
|
||||
|
||||
"ui.exception_center.title": "Exception Center",
|
||||
"ui.exception_center.list_view": "List view",
|
||||
"ui.exception_center.kanban_view": "Kanban view",
|
||||
@@ -298,7 +276,6 @@
|
||||
"ui.exception_center.no_exceptions": "No exceptions match the current filters",
|
||||
"ui.exception_center.column_empty": "No exceptions",
|
||||
"ui.exception_center.exceptions_suffix": "exceptions",
|
||||
|
||||
"ui.evidence_thread.back_to_list": "Back to list",
|
||||
"ui.evidence_thread.title_default": "Evidence Thread",
|
||||
"ui.evidence_thread.copy_digest": "Copy full digest",
|
||||
@@ -309,7 +286,6 @@
|
||||
"ui.evidence_thread.timeline_tab": "Timeline",
|
||||
"ui.evidence_thread.transcript_tab": "Transcript",
|
||||
"ui.evidence_thread.not_found": "No evidence thread found for this artifact.",
|
||||
|
||||
"ui.vulnerability_detail.eyebrow": "Vulnerability",
|
||||
"ui.vulnerability_detail.cvss": "CVSS",
|
||||
"ui.vulnerability_detail.impact_first": "Impact First",
|
||||
@@ -328,5 +304,76 @@
|
||||
"ui.vulnerability_detail.evidence_tree": "Evidence Tree and Citation Links",
|
||||
"ui.vulnerability_detail.evidence_explorer": "evidence explorer",
|
||||
"ui.vulnerability_detail.references": "References",
|
||||
"ui.vulnerability_detail.back_to_risk": "Back to Risk"
|
||||
"ui.vulnerability_detail.back_to_risk": "Back to Risk",
|
||||
"ui.search.input_aria_label": "Global search",
|
||||
"ui.search.loading": "Searching...",
|
||||
"ui.search.no_results": "No results found",
|
||||
"ui.search.did_you_mean_label": "Did you mean:",
|
||||
"ui.search.try_also_label": "Try also:",
|
||||
"ui.search.recent_label": "Recent",
|
||||
"ui.search.clear_history": "Clear",
|
||||
"ui.search.suggested_label": "Suggested",
|
||||
"ui.search.empty_state_header": "Search across your release control plane",
|
||||
"ui.search.quick_action.getting_started": "Getting Started",
|
||||
"ui.search.quick_action.run_health_check": "Run Health Check",
|
||||
"ui.search.quick_action.view_recent_scans": "View Recent Scans",
|
||||
"ui.search.filter.all": "All",
|
||||
"ui.search.placeholder.default": "Search everything...",
|
||||
"ui.search.placeholder.try": "Try: {suggestion}",
|
||||
"ui.search.domain.findings.title": "Security Findings",
|
||||
"ui.search.domain.findings.description": "CVEs, vulnerabilities, and exposure data across your images.",
|
||||
"ui.search.domain.findings.example": "CVE-2024-21626",
|
||||
"ui.search.domain.vex.title": "VEX Statements",
|
||||
"ui.search.domain.vex.description": "Exploitability statuses and vendor assertions for vulnerabilities.",
|
||||
"ui.search.domain.vex.example": "not_affected",
|
||||
"ui.search.domain.policy.title": "Policy Rules",
|
||||
"ui.search.domain.policy.description": "Release gate rules, exceptions, and enforcement outcomes.",
|
||||
"ui.search.domain.policy.example": "DENY-CRITICAL-PROD",
|
||||
"ui.search.domain.docs.title": "Documentation",
|
||||
"ui.search.domain.docs.description": "Guides, architecture references, and operator runbooks.",
|
||||
"ui.search.domain.docs.example": "how to deploy",
|
||||
"ui.search.domain.api.title": "API Reference",
|
||||
"ui.search.domain.api.description": "OpenAPI endpoints, request contracts, and service operations.",
|
||||
"ui.search.domain.api.example": "POST /api/v1/scanner/scans",
|
||||
"ui.search.domain.health.title": "Health Checks",
|
||||
"ui.search.domain.health.description": "Doctor checks, readiness diagnostics, and remediation guidance.",
|
||||
"ui.search.domain.health.example": "database connectivity",
|
||||
"ui.search.domain.operations.title": "Operations",
|
||||
"ui.search.domain.operations.description": "Jobs, workflows, and operational controls across environments.",
|
||||
"ui.search.domain.operations.example": "blocked releases",
|
||||
"ui.search.domain.timeline.title": "Timeline",
|
||||
"ui.search.domain.timeline.description": "Promotion history, incidents, and causal event traces.",
|
||||
"ui.search.domain.timeline.example": "release history",
|
||||
"ui.search.suggestion.findings.critical": "critical findings",
|
||||
"ui.search.suggestion.findings.reachable": "reachable vulnerabilities",
|
||||
"ui.search.suggestion.findings.unresolved": "unresolved CVEs",
|
||||
"ui.search.suggestion.policy.failing_gates": "failing policy gates",
|
||||
"ui.search.suggestion.policy.production_deny": "production deny rules",
|
||||
"ui.search.suggestion.policy.exceptions": "policy exceptions",
|
||||
"ui.search.suggestion.doctor.database": "database connectivity",
|
||||
"ui.search.suggestion.doctor.disk": "disk space",
|
||||
"ui.search.suggestion.doctor.oidc": "OIDC readiness",
|
||||
"ui.search.suggestion.timeline.failed_deployments": "failed deployments",
|
||||
"ui.search.suggestion.timeline.recent_promotions": "recent promotions",
|
||||
"ui.search.suggestion.timeline.release_history": "release history",
|
||||
"ui.search.suggestion.releases.pending_approvals": "pending approvals",
|
||||
"ui.search.suggestion.releases.blocked_releases": "blocked releases",
|
||||
"ui.search.suggestion.releases.environment_status": "environment status",
|
||||
"ui.search.suggestion.default.deploy": "How do I deploy?",
|
||||
"ui.search.suggestion.default.vex": "What is a VEX statement?",
|
||||
"ui.search.suggestion.default.critical": "Show critical findings",
|
||||
"ui.chat.input.waiting": "Waiting for response...",
|
||||
"ui.chat.input.placeholder": "Ask AdvisoryAI about this finding...",
|
||||
"ui.chat.suggestion.vulnerability.exploitable": "Is this exploitable in my environment?",
|
||||
"ui.chat.suggestion.vulnerability.remediation": "What is the remediation?",
|
||||
"ui.chat.suggestion.vulnerability.evidence_chain": "Show me the evidence chain",
|
||||
"ui.chat.suggestion.vulnerability.draft_vex": "Draft a VEX statement",
|
||||
"ui.chat.suggestion.policy.explain_rule": "Explain this policy rule",
|
||||
"ui.chat.suggestion.policy.override_gate": "What would happen if I override this gate?",
|
||||
"ui.chat.suggestion.policy.recent_violations": "Show me recent policy violations",
|
||||
"ui.chat.suggestion.policy.add_exception": "How do I add an exception?",
|
||||
"ui.chat.suggestion.default.what_can_do": "What can Stella Ops do?",
|
||||
"ui.chat.suggestion.default.first_scan": "How do I set up my first scan?",
|
||||
"ui.chat.suggestion.default.promotion_workflow": "Explain the release promotion workflow",
|
||||
"ui.chat.suggestion.default.health_checks": "What health checks should I run first?"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"_meta": { "locale": "fr-FR", "description": "Offline fallback bundle for StellaOps Console" },
|
||||
|
||||
"_meta": {
|
||||
"locale": "fr-FR",
|
||||
"description": "Offline fallback bundle for StellaOps Console"
|
||||
},
|
||||
"common.error.generic": "Something went wrong.",
|
||||
"common.error.not_found": "The requested resource was not found.",
|
||||
"common.error.unauthorized": "You do not have permission to perform this action.",
|
||||
@@ -9,12 +11,10 @@
|
||||
"common.error.timeout": "Request timed out.",
|
||||
"common.error.server_error": "An internal server error occurred. Please try again later.",
|
||||
"common.error.network": "Network error. Check your connection.",
|
||||
|
||||
"common.validation.required": "This field is required.",
|
||||
"common.validation.invalid": "Invalid value.",
|
||||
"common.validation.too_long": "Maximum {max} characters allowed.",
|
||||
"common.validation.too_short": "Minimum {min} characters required.",
|
||||
|
||||
"common.actions.save": "Save",
|
||||
"common.actions.cancel": "Cancel",
|
||||
"common.actions.delete": "Delete",
|
||||
@@ -26,7 +26,6 @@
|
||||
"common.actions.collapse": "Collapse",
|
||||
"common.actions.show_more": "Show more",
|
||||
"common.actions.show_less": "Show less",
|
||||
|
||||
"common.status.healthy": "Healthy",
|
||||
"common.status.degraded": "Degraded",
|
||||
"common.status.unavailable": "Unavailable",
|
||||
@@ -39,16 +38,13 @@
|
||||
"common.status.failed": "Failed",
|
||||
"common.status.canceled": "Canceled",
|
||||
"common.status.blocked": "Blocked",
|
||||
|
||||
"common.severity.critical": "Critical",
|
||||
"common.severity.high": "High",
|
||||
"common.severity.medium": "Medium",
|
||||
"common.severity.low": "Low",
|
||||
"common.severity.info": "Info",
|
||||
"common.severity.none": "None",
|
||||
|
||||
"common.time.just_now": "Just now",
|
||||
|
||||
"common.ui.loading": "Loading...",
|
||||
"common.ui.saving": "Saving...",
|
||||
"common.ui.deleting": "Deleting...",
|
||||
@@ -57,11 +53,9 @@
|
||||
"common.ui.offline": "You are offline.",
|
||||
"common.ui.reconnecting": "Reconnecting...",
|
||||
"common.ui.back_online": "Back online.",
|
||||
|
||||
"ui.loading.skeleton": "Loading...",
|
||||
"ui.loading.spinner": "Please wait...",
|
||||
"ui.loading.slow": "This is taking longer than expected...",
|
||||
|
||||
"ui.error.generic": "Something went wrong.",
|
||||
"ui.error.network": "Network error. Check your connection.",
|
||||
"ui.error.timeout": "Request timed out. Please try again.",
|
||||
@@ -70,19 +64,16 @@
|
||||
"ui.error.server_error": "Server error. Please try again later.",
|
||||
"ui.error.try_again": "Try again",
|
||||
"ui.error.go_back": "Go back",
|
||||
|
||||
"ui.offline.banner": "You're offline.",
|
||||
"ui.offline.description": "Some features may be unavailable.",
|
||||
"ui.offline.reconnecting": "Reconnecting...",
|
||||
"ui.offline.reconnected": "Back online.",
|
||||
|
||||
"ui.toast.success": "Success",
|
||||
"ui.toast.info": "Info",
|
||||
"ui.toast.warning": "Warning",
|
||||
"ui.toast.error": "Error",
|
||||
"ui.toast.dismiss": "Dismiss",
|
||||
"ui.toast.undo": "Undo",
|
||||
|
||||
"ui.actions.save": "Save",
|
||||
"ui.actions.saving": "Saving...",
|
||||
"ui.actions.saved": "Saved",
|
||||
@@ -111,7 +102,6 @@
|
||||
"ui.actions.sign_in": "Sign in",
|
||||
"ui.actions.back_to_list": "Back to list",
|
||||
"ui.actions.load_more": "Load more",
|
||||
|
||||
"ui.labels.all": "All",
|
||||
"ui.labels.title": "Title",
|
||||
"ui.labels.description": "Description",
|
||||
@@ -131,14 +121,12 @@
|
||||
"ui.labels.selected": "selected",
|
||||
"ui.labels.last_updated": "Last updated:",
|
||||
"ui.labels.expires": "Expires",
|
||||
|
||||
"ui.validation.required": "This field is required.",
|
||||
"ui.validation.invalid": "Invalid value.",
|
||||
"ui.validation.too_long": "Maximum {max} characters allowed.",
|
||||
"ui.validation.too_short": "Minimum {min} characters required.",
|
||||
"ui.validation.invalid_email": "Please enter a valid email address.",
|
||||
"ui.validation.invalid_url": "Please enter a valid URL.",
|
||||
|
||||
"ui.a11y.loading": "Content is loading.",
|
||||
"ui.a11y.loaded": "Content loaded.",
|
||||
"ui.a11y.error": "An error occurred.",
|
||||
@@ -148,10 +136,8 @@
|
||||
"ui.a11y.deselected": "Deselected",
|
||||
"ui.a11y.required": "Required field",
|
||||
"ui.a11y.optional": "Optional",
|
||||
|
||||
"ui.motion.reduced": "Animations reduced.",
|
||||
"ui.motion.enabled": "Animations enabled.",
|
||||
|
||||
"ui.auth.fresh_active": "Fresh auth: Active",
|
||||
"ui.auth.fresh_stale": "Fresh auth: Stale",
|
||||
"ui.locale.label": "Langue",
|
||||
@@ -171,15 +157,14 @@
|
||||
"ui.settings.language.persisted": "Enregistre pour votre compte et reutilise par le CLI.",
|
||||
"ui.settings.language.persisted_error": "Enregistre localement, mais la synchronisation du compte a echoue.",
|
||||
"ui.settings.language.sign_in_hint": "Connectez-vous pour synchroniser cette preference avec le CLI.",
|
||||
|
||||
"ui.first_signal.label": "First signal",
|
||||
"ui.first_signal.run_prefix": "Run:",
|
||||
"ui.first_signal.live": "Live",
|
||||
"ui.first_signal.polling": "Polling",
|
||||
"ui.first_signal.range_prefix": "Range",
|
||||
"ui.first_signal.range_separator": "\u2013",
|
||||
"ui.first_signal.stage_separator": " \u00b7 ",
|
||||
"ui.first_signal.waiting": "Waiting for first signal\u2026",
|
||||
"ui.first_signal.range_separator": "–",
|
||||
"ui.first_signal.stage_separator": " · ",
|
||||
"ui.first_signal.waiting": "Waiting for first signal…",
|
||||
"ui.first_signal.not_available": "Signal not available yet.",
|
||||
"ui.first_signal.offline": "Offline. Last known signal may be stale.",
|
||||
"ui.first_signal.failed": "Failed to load signal.",
|
||||
@@ -202,20 +187,17 @@
|
||||
"ui.first_signal.stage.report": "Generating report",
|
||||
"ui.first_signal.stage.unknown": "Processing",
|
||||
"ui.first_signal.aria.card_label": "First signal status",
|
||||
|
||||
"ui.severity.critical": "Critical",
|
||||
"ui.severity.high": "High",
|
||||
"ui.severity.medium": "Medium",
|
||||
"ui.severity.low": "Low",
|
||||
"ui.severity.info": "Info",
|
||||
"ui.severity.none": "None",
|
||||
|
||||
"ui.release_orchestrator.title": "Release Orchestrator",
|
||||
"ui.release_orchestrator.subtitle": "Pipeline overview and release management",
|
||||
"ui.release_orchestrator.pipeline_runs": "Pipeline Runs",
|
||||
"ui.release_orchestrator.refresh_dashboard": "Refresh dashboard",
|
||||
|
||||
"ui.risk_dashboard.eyebrow": "Gateway \u00b7 Risk",
|
||||
"ui.risk_dashboard.eyebrow": "Gateway · Risk",
|
||||
"ui.risk_dashboard.title": "Risk Profiles",
|
||||
"ui.risk_dashboard.subtitle": "Tenant-scoped risk posture with deterministic ordering.",
|
||||
"ui.risk_dashboard.up_to_date": "Up to date",
|
||||
@@ -225,8 +207,7 @@
|
||||
"ui.risk_dashboard.risks_suffix": "risks.",
|
||||
"ui.risk_dashboard.error_unable_to_load": "Unable to load risk profiles.",
|
||||
"ui.risk_dashboard.no_risks_found": "No risks found for current filters.",
|
||||
"ui.risk_dashboard.loading_risks": "Loading risks\u2026",
|
||||
|
||||
"ui.risk_dashboard.loading_risks": "Loading risks…",
|
||||
"ui.findings.title": "Findings",
|
||||
"ui.findings.search_placeholder": "Search findings...",
|
||||
"ui.findings.clear_filters": "Clear Filters",
|
||||
@@ -242,7 +223,6 @@
|
||||
"ui.findings.select": "Select",
|
||||
"ui.findings.no_findings": "No findings to display.",
|
||||
"ui.findings.no_match": "No findings match the current filters.",
|
||||
|
||||
"ui.sources_dashboard.title": "Sources Dashboard",
|
||||
"ui.sources_dashboard.verifying": "Verifying...",
|
||||
"ui.sources_dashboard.verify_24h": "Verify last 24h",
|
||||
@@ -269,7 +249,6 @@
|
||||
"ui.sources_dashboard.data_from": "Data from",
|
||||
"ui.sources_dashboard.to": "to",
|
||||
"ui.sources_dashboard.hour_window": "h window",
|
||||
|
||||
"ui.timeline.title": "Timeline",
|
||||
"ui.timeline.event_timeline": "Event Timeline",
|
||||
"ui.timeline.refresh_timeline": "Refresh timeline",
|
||||
@@ -280,7 +259,6 @@
|
||||
"ui.timeline.load_more": "Load more events",
|
||||
"ui.timeline.event_details": "Event details",
|
||||
"ui.timeline.events": "events",
|
||||
|
||||
"ui.exception_center.title": "Exception Center",
|
||||
"ui.exception_center.list_view": "List view",
|
||||
"ui.exception_center.kanban_view": "Kanban view",
|
||||
@@ -298,7 +276,6 @@
|
||||
"ui.exception_center.no_exceptions": "No exceptions match the current filters",
|
||||
"ui.exception_center.column_empty": "No exceptions",
|
||||
"ui.exception_center.exceptions_suffix": "exceptions",
|
||||
|
||||
"ui.evidence_thread.back_to_list": "Back to list",
|
||||
"ui.evidence_thread.title_default": "Evidence Thread",
|
||||
"ui.evidence_thread.copy_digest": "Copy full digest",
|
||||
@@ -309,7 +286,6 @@
|
||||
"ui.evidence_thread.timeline_tab": "Timeline",
|
||||
"ui.evidence_thread.transcript_tab": "Transcript",
|
||||
"ui.evidence_thread.not_found": "No evidence thread found for this artifact.",
|
||||
|
||||
"ui.vulnerability_detail.eyebrow": "Vulnerability",
|
||||
"ui.vulnerability_detail.cvss": "CVSS",
|
||||
"ui.vulnerability_detail.impact_first": "Impact First",
|
||||
@@ -328,5 +304,76 @@
|
||||
"ui.vulnerability_detail.evidence_tree": "Evidence Tree and Citation Links",
|
||||
"ui.vulnerability_detail.evidence_explorer": "evidence explorer",
|
||||
"ui.vulnerability_detail.references": "References",
|
||||
"ui.vulnerability_detail.back_to_risk": "Back to Risk"
|
||||
"ui.vulnerability_detail.back_to_risk": "Back to Risk",
|
||||
"ui.search.input_aria_label": "Global search",
|
||||
"ui.search.loading": "Searching...",
|
||||
"ui.search.no_results": "No results found",
|
||||
"ui.search.did_you_mean_label": "Did you mean:",
|
||||
"ui.search.try_also_label": "Try also:",
|
||||
"ui.search.recent_label": "Recent",
|
||||
"ui.search.clear_history": "Clear",
|
||||
"ui.search.suggested_label": "Suggested",
|
||||
"ui.search.empty_state_header": "Search across your release control plane",
|
||||
"ui.search.quick_action.getting_started": "Getting Started",
|
||||
"ui.search.quick_action.run_health_check": "Run Health Check",
|
||||
"ui.search.quick_action.view_recent_scans": "View Recent Scans",
|
||||
"ui.search.filter.all": "All",
|
||||
"ui.search.placeholder.default": "Search everything...",
|
||||
"ui.search.placeholder.try": "Try: {suggestion}",
|
||||
"ui.search.domain.findings.title": "Security Findings",
|
||||
"ui.search.domain.findings.description": "CVEs, vulnerabilities, and exposure data across your images.",
|
||||
"ui.search.domain.findings.example": "CVE-2024-21626",
|
||||
"ui.search.domain.vex.title": "VEX Statements",
|
||||
"ui.search.domain.vex.description": "Exploitability statuses and vendor assertions for vulnerabilities.",
|
||||
"ui.search.domain.vex.example": "not_affected",
|
||||
"ui.search.domain.policy.title": "Policy Rules",
|
||||
"ui.search.domain.policy.description": "Release gate rules, exceptions, and enforcement outcomes.",
|
||||
"ui.search.domain.policy.example": "DENY-CRITICAL-PROD",
|
||||
"ui.search.domain.docs.title": "Documentation",
|
||||
"ui.search.domain.docs.description": "Guides, architecture references, and operator runbooks.",
|
||||
"ui.search.domain.docs.example": "how to deploy",
|
||||
"ui.search.domain.api.title": "API Reference",
|
||||
"ui.search.domain.api.description": "OpenAPI endpoints, request contracts, and service operations.",
|
||||
"ui.search.domain.api.example": "POST /api/v1/scanner/scans",
|
||||
"ui.search.domain.health.title": "Health Checks",
|
||||
"ui.search.domain.health.description": "Doctor checks, readiness diagnostics, and remediation guidance.",
|
||||
"ui.search.domain.health.example": "database connectivity",
|
||||
"ui.search.domain.operations.title": "Operations",
|
||||
"ui.search.domain.operations.description": "Jobs, workflows, and operational controls across environments.",
|
||||
"ui.search.domain.operations.example": "blocked releases",
|
||||
"ui.search.domain.timeline.title": "Timeline",
|
||||
"ui.search.domain.timeline.description": "Promotion history, incidents, and causal event traces.",
|
||||
"ui.search.domain.timeline.example": "release history",
|
||||
"ui.search.suggestion.findings.critical": "critical findings",
|
||||
"ui.search.suggestion.findings.reachable": "reachable vulnerabilities",
|
||||
"ui.search.suggestion.findings.unresolved": "unresolved CVEs",
|
||||
"ui.search.suggestion.policy.failing_gates": "failing policy gates",
|
||||
"ui.search.suggestion.policy.production_deny": "production deny rules",
|
||||
"ui.search.suggestion.policy.exceptions": "policy exceptions",
|
||||
"ui.search.suggestion.doctor.database": "database connectivity",
|
||||
"ui.search.suggestion.doctor.disk": "disk space",
|
||||
"ui.search.suggestion.doctor.oidc": "OIDC readiness",
|
||||
"ui.search.suggestion.timeline.failed_deployments": "failed deployments",
|
||||
"ui.search.suggestion.timeline.recent_promotions": "recent promotions",
|
||||
"ui.search.suggestion.timeline.release_history": "release history",
|
||||
"ui.search.suggestion.releases.pending_approvals": "pending approvals",
|
||||
"ui.search.suggestion.releases.blocked_releases": "blocked releases",
|
||||
"ui.search.suggestion.releases.environment_status": "environment status",
|
||||
"ui.search.suggestion.default.deploy": "How do I deploy?",
|
||||
"ui.search.suggestion.default.vex": "What is a VEX statement?",
|
||||
"ui.search.suggestion.default.critical": "Show critical findings",
|
||||
"ui.chat.input.waiting": "Waiting for response...",
|
||||
"ui.chat.input.placeholder": "Ask AdvisoryAI about this finding...",
|
||||
"ui.chat.suggestion.vulnerability.exploitable": "Is this exploitable in my environment?",
|
||||
"ui.chat.suggestion.vulnerability.remediation": "What is the remediation?",
|
||||
"ui.chat.suggestion.vulnerability.evidence_chain": "Show me the evidence chain",
|
||||
"ui.chat.suggestion.vulnerability.draft_vex": "Draft a VEX statement",
|
||||
"ui.chat.suggestion.policy.explain_rule": "Explain this policy rule",
|
||||
"ui.chat.suggestion.policy.override_gate": "What would happen if I override this gate?",
|
||||
"ui.chat.suggestion.policy.recent_violations": "Show me recent policy violations",
|
||||
"ui.chat.suggestion.policy.add_exception": "How do I add an exception?",
|
||||
"ui.chat.suggestion.default.what_can_do": "What can Stella Ops do?",
|
||||
"ui.chat.suggestion.default.first_scan": "How do I set up my first scan?",
|
||||
"ui.chat.suggestion.default.promotion_workflow": "Explain the release promotion workflow",
|
||||
"ui.chat.suggestion.default.health_checks": "What health checks should I run first?"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"_meta": { "locale": "ru-RU", "description": "Offline fallback bundle for StellaOps Console" },
|
||||
|
||||
"_meta": {
|
||||
"locale": "ru-RU",
|
||||
"description": "Offline fallback bundle for StellaOps Console"
|
||||
},
|
||||
"common.error.generic": "Something went wrong.",
|
||||
"common.error.not_found": "The requested resource was not found.",
|
||||
"common.error.unauthorized": "You do not have permission to perform this action.",
|
||||
@@ -9,12 +11,10 @@
|
||||
"common.error.timeout": "Request timed out.",
|
||||
"common.error.server_error": "An internal server error occurred. Please try again later.",
|
||||
"common.error.network": "Network error. Check your connection.",
|
||||
|
||||
"common.validation.required": "This field is required.",
|
||||
"common.validation.invalid": "Invalid value.",
|
||||
"common.validation.too_long": "Maximum {max} characters allowed.",
|
||||
"common.validation.too_short": "Minimum {min} characters required.",
|
||||
|
||||
"common.actions.save": "Save",
|
||||
"common.actions.cancel": "Cancel",
|
||||
"common.actions.delete": "Delete",
|
||||
@@ -26,7 +26,6 @@
|
||||
"common.actions.collapse": "Collapse",
|
||||
"common.actions.show_more": "Show more",
|
||||
"common.actions.show_less": "Show less",
|
||||
|
||||
"common.status.healthy": "Healthy",
|
||||
"common.status.degraded": "Degraded",
|
||||
"common.status.unavailable": "Unavailable",
|
||||
@@ -39,16 +38,13 @@
|
||||
"common.status.failed": "Failed",
|
||||
"common.status.canceled": "Canceled",
|
||||
"common.status.blocked": "Blocked",
|
||||
|
||||
"common.severity.critical": "Critical",
|
||||
"common.severity.high": "High",
|
||||
"common.severity.medium": "Medium",
|
||||
"common.severity.low": "Low",
|
||||
"common.severity.info": "Info",
|
||||
"common.severity.none": "None",
|
||||
|
||||
"common.time.just_now": "Just now",
|
||||
|
||||
"common.ui.loading": "Loading...",
|
||||
"common.ui.saving": "Saving...",
|
||||
"common.ui.deleting": "Deleting...",
|
||||
@@ -57,11 +53,9 @@
|
||||
"common.ui.offline": "You are offline.",
|
||||
"common.ui.reconnecting": "Reconnecting...",
|
||||
"common.ui.back_online": "Back online.",
|
||||
|
||||
"ui.loading.skeleton": "Loading...",
|
||||
"ui.loading.spinner": "Please wait...",
|
||||
"ui.loading.slow": "This is taking longer than expected...",
|
||||
|
||||
"ui.error.generic": "Something went wrong.",
|
||||
"ui.error.network": "Network error. Check your connection.",
|
||||
"ui.error.timeout": "Request timed out. Please try again.",
|
||||
@@ -70,19 +64,16 @@
|
||||
"ui.error.server_error": "Server error. Please try again later.",
|
||||
"ui.error.try_again": "Try again",
|
||||
"ui.error.go_back": "Go back",
|
||||
|
||||
"ui.offline.banner": "You're offline.",
|
||||
"ui.offline.description": "Some features may be unavailable.",
|
||||
"ui.offline.reconnecting": "Reconnecting...",
|
||||
"ui.offline.reconnected": "Back online.",
|
||||
|
||||
"ui.toast.success": "Success",
|
||||
"ui.toast.info": "Info",
|
||||
"ui.toast.warning": "Warning",
|
||||
"ui.toast.error": "Error",
|
||||
"ui.toast.dismiss": "Dismiss",
|
||||
"ui.toast.undo": "Undo",
|
||||
|
||||
"ui.actions.save": "Save",
|
||||
"ui.actions.saving": "Saving...",
|
||||
"ui.actions.saved": "Saved",
|
||||
@@ -111,7 +102,6 @@
|
||||
"ui.actions.sign_in": "Sign in",
|
||||
"ui.actions.back_to_list": "Back to list",
|
||||
"ui.actions.load_more": "Load more",
|
||||
|
||||
"ui.labels.all": "All",
|
||||
"ui.labels.title": "Title",
|
||||
"ui.labels.description": "Description",
|
||||
@@ -131,14 +121,12 @@
|
||||
"ui.labels.selected": "selected",
|
||||
"ui.labels.last_updated": "Last updated:",
|
||||
"ui.labels.expires": "Expires",
|
||||
|
||||
"ui.validation.required": "This field is required.",
|
||||
"ui.validation.invalid": "Invalid value.",
|
||||
"ui.validation.too_long": "Maximum {max} characters allowed.",
|
||||
"ui.validation.too_short": "Minimum {min} characters required.",
|
||||
"ui.validation.invalid_email": "Please enter a valid email address.",
|
||||
"ui.validation.invalid_url": "Please enter a valid URL.",
|
||||
|
||||
"ui.a11y.loading": "Content is loading.",
|
||||
"ui.a11y.loaded": "Content loaded.",
|
||||
"ui.a11y.error": "An error occurred.",
|
||||
@@ -148,10 +136,8 @@
|
||||
"ui.a11y.deselected": "Deselected",
|
||||
"ui.a11y.required": "Required field",
|
||||
"ui.a11y.optional": "Optional",
|
||||
|
||||
"ui.motion.reduced": "Animations reduced.",
|
||||
"ui.motion.enabled": "Animations enabled.",
|
||||
|
||||
"ui.auth.fresh_active": "Fresh auth: Active",
|
||||
"ui.auth.fresh_stale": "Fresh auth: Stale",
|
||||
"ui.locale.label": "Yazyk",
|
||||
@@ -171,15 +157,14 @@
|
||||
"ui.settings.language.persisted": "Sohraneno dlya vashego akkaunta i ispolzuetsya v CLI.",
|
||||
"ui.settings.language.persisted_error": "Lokalno sohraneno, no sinkhronizatsiya akkaunta ne udalas.",
|
||||
"ui.settings.language.sign_in_hint": "Vypolnite vkhod, chtoby sinkhronizirovat etu nastroiku s CLI.",
|
||||
|
||||
"ui.first_signal.label": "First signal",
|
||||
"ui.first_signal.run_prefix": "Run:",
|
||||
"ui.first_signal.live": "Live",
|
||||
"ui.first_signal.polling": "Polling",
|
||||
"ui.first_signal.range_prefix": "Range",
|
||||
"ui.first_signal.range_separator": "\u2013",
|
||||
"ui.first_signal.stage_separator": " \u00b7 ",
|
||||
"ui.first_signal.waiting": "Waiting for first signal\u2026",
|
||||
"ui.first_signal.range_separator": "–",
|
||||
"ui.first_signal.stage_separator": " · ",
|
||||
"ui.first_signal.waiting": "Waiting for first signal…",
|
||||
"ui.first_signal.not_available": "Signal not available yet.",
|
||||
"ui.first_signal.offline": "Offline. Last known signal may be stale.",
|
||||
"ui.first_signal.failed": "Failed to load signal.",
|
||||
@@ -202,20 +187,17 @@
|
||||
"ui.first_signal.stage.report": "Generating report",
|
||||
"ui.first_signal.stage.unknown": "Processing",
|
||||
"ui.first_signal.aria.card_label": "First signal status",
|
||||
|
||||
"ui.severity.critical": "Critical",
|
||||
"ui.severity.high": "High",
|
||||
"ui.severity.medium": "Medium",
|
||||
"ui.severity.low": "Low",
|
||||
"ui.severity.info": "Info",
|
||||
"ui.severity.none": "None",
|
||||
|
||||
"ui.release_orchestrator.title": "Release Orchestrator",
|
||||
"ui.release_orchestrator.subtitle": "Pipeline overview and release management",
|
||||
"ui.release_orchestrator.pipeline_runs": "Pipeline Runs",
|
||||
"ui.release_orchestrator.refresh_dashboard": "Refresh dashboard",
|
||||
|
||||
"ui.risk_dashboard.eyebrow": "Gateway \u00b7 Risk",
|
||||
"ui.risk_dashboard.eyebrow": "Gateway · Risk",
|
||||
"ui.risk_dashboard.title": "Risk Profiles",
|
||||
"ui.risk_dashboard.subtitle": "Tenant-scoped risk posture with deterministic ordering.",
|
||||
"ui.risk_dashboard.up_to_date": "Up to date",
|
||||
@@ -225,8 +207,7 @@
|
||||
"ui.risk_dashboard.risks_suffix": "risks.",
|
||||
"ui.risk_dashboard.error_unable_to_load": "Unable to load risk profiles.",
|
||||
"ui.risk_dashboard.no_risks_found": "No risks found for current filters.",
|
||||
"ui.risk_dashboard.loading_risks": "Loading risks\u2026",
|
||||
|
||||
"ui.risk_dashboard.loading_risks": "Loading risks…",
|
||||
"ui.findings.title": "Findings",
|
||||
"ui.findings.search_placeholder": "Search findings...",
|
||||
"ui.findings.clear_filters": "Clear Filters",
|
||||
@@ -242,7 +223,6 @@
|
||||
"ui.findings.select": "Select",
|
||||
"ui.findings.no_findings": "No findings to display.",
|
||||
"ui.findings.no_match": "No findings match the current filters.",
|
||||
|
||||
"ui.sources_dashboard.title": "Sources Dashboard",
|
||||
"ui.sources_dashboard.verifying": "Verifying...",
|
||||
"ui.sources_dashboard.verify_24h": "Verify last 24h",
|
||||
@@ -269,7 +249,6 @@
|
||||
"ui.sources_dashboard.data_from": "Data from",
|
||||
"ui.sources_dashboard.to": "to",
|
||||
"ui.sources_dashboard.hour_window": "h window",
|
||||
|
||||
"ui.timeline.title": "Timeline",
|
||||
"ui.timeline.event_timeline": "Event Timeline",
|
||||
"ui.timeline.refresh_timeline": "Refresh timeline",
|
||||
@@ -280,7 +259,6 @@
|
||||
"ui.timeline.load_more": "Load more events",
|
||||
"ui.timeline.event_details": "Event details",
|
||||
"ui.timeline.events": "events",
|
||||
|
||||
"ui.exception_center.title": "Exception Center",
|
||||
"ui.exception_center.list_view": "List view",
|
||||
"ui.exception_center.kanban_view": "Kanban view",
|
||||
@@ -298,7 +276,6 @@
|
||||
"ui.exception_center.no_exceptions": "No exceptions match the current filters",
|
||||
"ui.exception_center.column_empty": "No exceptions",
|
||||
"ui.exception_center.exceptions_suffix": "exceptions",
|
||||
|
||||
"ui.evidence_thread.back_to_list": "Back to list",
|
||||
"ui.evidence_thread.title_default": "Evidence Thread",
|
||||
"ui.evidence_thread.copy_digest": "Copy full digest",
|
||||
@@ -309,7 +286,6 @@
|
||||
"ui.evidence_thread.timeline_tab": "Timeline",
|
||||
"ui.evidence_thread.transcript_tab": "Transcript",
|
||||
"ui.evidence_thread.not_found": "No evidence thread found for this artifact.",
|
||||
|
||||
"ui.vulnerability_detail.eyebrow": "Vulnerability",
|
||||
"ui.vulnerability_detail.cvss": "CVSS",
|
||||
"ui.vulnerability_detail.impact_first": "Impact First",
|
||||
@@ -328,5 +304,76 @@
|
||||
"ui.vulnerability_detail.evidence_tree": "Evidence Tree and Citation Links",
|
||||
"ui.vulnerability_detail.evidence_explorer": "evidence explorer",
|
||||
"ui.vulnerability_detail.references": "References",
|
||||
"ui.vulnerability_detail.back_to_risk": "Back to Risk"
|
||||
"ui.vulnerability_detail.back_to_risk": "Back to Risk",
|
||||
"ui.search.input_aria_label": "Global search",
|
||||
"ui.search.loading": "Searching...",
|
||||
"ui.search.no_results": "No results found",
|
||||
"ui.search.did_you_mean_label": "Did you mean:",
|
||||
"ui.search.try_also_label": "Try also:",
|
||||
"ui.search.recent_label": "Recent",
|
||||
"ui.search.clear_history": "Clear",
|
||||
"ui.search.suggested_label": "Suggested",
|
||||
"ui.search.empty_state_header": "Search across your release control plane",
|
||||
"ui.search.quick_action.getting_started": "Getting Started",
|
||||
"ui.search.quick_action.run_health_check": "Run Health Check",
|
||||
"ui.search.quick_action.view_recent_scans": "View Recent Scans",
|
||||
"ui.search.filter.all": "All",
|
||||
"ui.search.placeholder.default": "Search everything...",
|
||||
"ui.search.placeholder.try": "Try: {suggestion}",
|
||||
"ui.search.domain.findings.title": "Security Findings",
|
||||
"ui.search.domain.findings.description": "CVEs, vulnerabilities, and exposure data across your images.",
|
||||
"ui.search.domain.findings.example": "CVE-2024-21626",
|
||||
"ui.search.domain.vex.title": "VEX Statements",
|
||||
"ui.search.domain.vex.description": "Exploitability statuses and vendor assertions for vulnerabilities.",
|
||||
"ui.search.domain.vex.example": "not_affected",
|
||||
"ui.search.domain.policy.title": "Policy Rules",
|
||||
"ui.search.domain.policy.description": "Release gate rules, exceptions, and enforcement outcomes.",
|
||||
"ui.search.domain.policy.example": "DENY-CRITICAL-PROD",
|
||||
"ui.search.domain.docs.title": "Documentation",
|
||||
"ui.search.domain.docs.description": "Guides, architecture references, and operator runbooks.",
|
||||
"ui.search.domain.docs.example": "how to deploy",
|
||||
"ui.search.domain.api.title": "API Reference",
|
||||
"ui.search.domain.api.description": "OpenAPI endpoints, request contracts, and service operations.",
|
||||
"ui.search.domain.api.example": "POST /api/v1/scanner/scans",
|
||||
"ui.search.domain.health.title": "Health Checks",
|
||||
"ui.search.domain.health.description": "Doctor checks, readiness diagnostics, and remediation guidance.",
|
||||
"ui.search.domain.health.example": "database connectivity",
|
||||
"ui.search.domain.operations.title": "Operations",
|
||||
"ui.search.domain.operations.description": "Jobs, workflows, and operational controls across environments.",
|
||||
"ui.search.domain.operations.example": "blocked releases",
|
||||
"ui.search.domain.timeline.title": "Timeline",
|
||||
"ui.search.domain.timeline.description": "Promotion history, incidents, and causal event traces.",
|
||||
"ui.search.domain.timeline.example": "release history",
|
||||
"ui.search.suggestion.findings.critical": "critical findings",
|
||||
"ui.search.suggestion.findings.reachable": "reachable vulnerabilities",
|
||||
"ui.search.suggestion.findings.unresolved": "unresolved CVEs",
|
||||
"ui.search.suggestion.policy.failing_gates": "failing policy gates",
|
||||
"ui.search.suggestion.policy.production_deny": "production deny rules",
|
||||
"ui.search.suggestion.policy.exceptions": "policy exceptions",
|
||||
"ui.search.suggestion.doctor.database": "database connectivity",
|
||||
"ui.search.suggestion.doctor.disk": "disk space",
|
||||
"ui.search.suggestion.doctor.oidc": "OIDC readiness",
|
||||
"ui.search.suggestion.timeline.failed_deployments": "failed deployments",
|
||||
"ui.search.suggestion.timeline.recent_promotions": "recent promotions",
|
||||
"ui.search.suggestion.timeline.release_history": "release history",
|
||||
"ui.search.suggestion.releases.pending_approvals": "pending approvals",
|
||||
"ui.search.suggestion.releases.blocked_releases": "blocked releases",
|
||||
"ui.search.suggestion.releases.environment_status": "environment status",
|
||||
"ui.search.suggestion.default.deploy": "How do I deploy?",
|
||||
"ui.search.suggestion.default.vex": "What is a VEX statement?",
|
||||
"ui.search.suggestion.default.critical": "Show critical findings",
|
||||
"ui.chat.input.waiting": "Waiting for response...",
|
||||
"ui.chat.input.placeholder": "Ask AdvisoryAI about this finding...",
|
||||
"ui.chat.suggestion.vulnerability.exploitable": "Is this exploitable in my environment?",
|
||||
"ui.chat.suggestion.vulnerability.remediation": "What is the remediation?",
|
||||
"ui.chat.suggestion.vulnerability.evidence_chain": "Show me the evidence chain",
|
||||
"ui.chat.suggestion.vulnerability.draft_vex": "Draft a VEX statement",
|
||||
"ui.chat.suggestion.policy.explain_rule": "Explain this policy rule",
|
||||
"ui.chat.suggestion.policy.override_gate": "What would happen if I override this gate?",
|
||||
"ui.chat.suggestion.policy.recent_violations": "Show me recent policy violations",
|
||||
"ui.chat.suggestion.policy.add_exception": "How do I add an exception?",
|
||||
"ui.chat.suggestion.default.what_can_do": "What can Stella Ops do?",
|
||||
"ui.chat.suggestion.default.first_scan": "How do I set up my first scan?",
|
||||
"ui.chat.suggestion.default.promotion_workflow": "Explain the release promotion workflow",
|
||||
"ui.chat.suggestion.default.health_checks": "What health checks should I run first?"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"_meta": { "locale": "uk-UA", "description": "Offline fallback bundle for StellaOps Console" },
|
||||
|
||||
"_meta": {
|
||||
"locale": "uk-UA",
|
||||
"description": "Offline fallback bundle for StellaOps Console"
|
||||
},
|
||||
"common.error.generic": "Something went wrong.",
|
||||
"common.error.not_found": "The requested resource was not found.",
|
||||
"common.error.unauthorized": "You do not have permission to perform this action.",
|
||||
@@ -9,12 +11,10 @@
|
||||
"common.error.timeout": "Request timed out.",
|
||||
"common.error.server_error": "An internal server error occurred. Please try again later.",
|
||||
"common.error.network": "Network error. Check your connection.",
|
||||
|
||||
"common.validation.required": "This field is required.",
|
||||
"common.validation.invalid": "Invalid value.",
|
||||
"common.validation.too_long": "Maximum {max} characters allowed.",
|
||||
"common.validation.too_short": "Minimum {min} characters required.",
|
||||
|
||||
"common.actions.save": "Save",
|
||||
"common.actions.cancel": "Cancel",
|
||||
"common.actions.delete": "Delete",
|
||||
@@ -26,7 +26,6 @@
|
||||
"common.actions.collapse": "Collapse",
|
||||
"common.actions.show_more": "Show more",
|
||||
"common.actions.show_less": "Show less",
|
||||
|
||||
"common.status.healthy": "Healthy",
|
||||
"common.status.degraded": "Degraded",
|
||||
"common.status.unavailable": "Unavailable",
|
||||
@@ -39,16 +38,13 @@
|
||||
"common.status.failed": "Failed",
|
||||
"common.status.canceled": "Canceled",
|
||||
"common.status.blocked": "Blocked",
|
||||
|
||||
"common.severity.critical": "Critical",
|
||||
"common.severity.high": "High",
|
||||
"common.severity.medium": "Medium",
|
||||
"common.severity.low": "Low",
|
||||
"common.severity.info": "Info",
|
||||
"common.severity.none": "None",
|
||||
|
||||
"common.time.just_now": "Just now",
|
||||
|
||||
"common.ui.loading": "Loading...",
|
||||
"common.ui.saving": "Saving...",
|
||||
"common.ui.deleting": "Deleting...",
|
||||
@@ -57,11 +53,9 @@
|
||||
"common.ui.offline": "You are offline.",
|
||||
"common.ui.reconnecting": "Reconnecting...",
|
||||
"common.ui.back_online": "Back online.",
|
||||
|
||||
"ui.loading.skeleton": "Loading...",
|
||||
"ui.loading.spinner": "Please wait...",
|
||||
"ui.loading.slow": "This is taking longer than expected...",
|
||||
|
||||
"ui.error.generic": "Something went wrong.",
|
||||
"ui.error.network": "Network error. Check your connection.",
|
||||
"ui.error.timeout": "Request timed out. Please try again.",
|
||||
@@ -70,19 +64,16 @@
|
||||
"ui.error.server_error": "Server error. Please try again later.",
|
||||
"ui.error.try_again": "Try again",
|
||||
"ui.error.go_back": "Go back",
|
||||
|
||||
"ui.offline.banner": "You're offline.",
|
||||
"ui.offline.description": "Some features may be unavailable.",
|
||||
"ui.offline.reconnecting": "Reconnecting...",
|
||||
"ui.offline.reconnected": "Back online.",
|
||||
|
||||
"ui.toast.success": "Success",
|
||||
"ui.toast.info": "Info",
|
||||
"ui.toast.warning": "Warning",
|
||||
"ui.toast.error": "Error",
|
||||
"ui.toast.dismiss": "Dismiss",
|
||||
"ui.toast.undo": "Undo",
|
||||
|
||||
"ui.actions.save": "Save",
|
||||
"ui.actions.saving": "Saving...",
|
||||
"ui.actions.saved": "Saved",
|
||||
@@ -111,7 +102,6 @@
|
||||
"ui.actions.sign_in": "Sign in",
|
||||
"ui.actions.back_to_list": "Back to list",
|
||||
"ui.actions.load_more": "Load more",
|
||||
|
||||
"ui.labels.all": "All",
|
||||
"ui.labels.title": "Title",
|
||||
"ui.labels.description": "Description",
|
||||
@@ -131,14 +121,12 @@
|
||||
"ui.labels.selected": "selected",
|
||||
"ui.labels.last_updated": "Last updated:",
|
||||
"ui.labels.expires": "Expires",
|
||||
|
||||
"ui.validation.required": "This field is required.",
|
||||
"ui.validation.invalid": "Invalid value.",
|
||||
"ui.validation.too_long": "Maximum {max} characters allowed.",
|
||||
"ui.validation.too_short": "Minimum {min} characters required.",
|
||||
"ui.validation.invalid_email": "Please enter a valid email address.",
|
||||
"ui.validation.invalid_url": "Please enter a valid URL.",
|
||||
|
||||
"ui.a11y.loading": "Content is loading.",
|
||||
"ui.a11y.loaded": "Content loaded.",
|
||||
"ui.a11y.error": "An error occurred.",
|
||||
@@ -148,10 +136,8 @@
|
||||
"ui.a11y.deselected": "Deselected",
|
||||
"ui.a11y.required": "Required field",
|
||||
"ui.a11y.optional": "Optional",
|
||||
|
||||
"ui.motion.reduced": "Animations reduced.",
|
||||
"ui.motion.enabled": "Animations enabled.",
|
||||
|
||||
"ui.auth.fresh_active": "Fresh auth: Active",
|
||||
"ui.auth.fresh_stale": "Fresh auth: Stale",
|
||||
"ui.locale.label": "Mova",
|
||||
@@ -171,15 +157,14 @@
|
||||
"ui.settings.language.persisted": "Zberezheno dlia vashoho oblikovoho zapysu ta povtorno vykorystovuietsia v CLI.",
|
||||
"ui.settings.language.persisted_error": "Lokalno zberezheno, ale synkhronizatsiia oblikovoho zapysu ne vdlasia.",
|
||||
"ui.settings.language.sign_in_hint": "Uvijdit, shchob synkhronizuvaty tsiu nalashtunku z CLI.",
|
||||
|
||||
"ui.first_signal.label": "First signal",
|
||||
"ui.first_signal.run_prefix": "Run:",
|
||||
"ui.first_signal.live": "Live",
|
||||
"ui.first_signal.polling": "Polling",
|
||||
"ui.first_signal.range_prefix": "Range",
|
||||
"ui.first_signal.range_separator": "\u2013",
|
||||
"ui.first_signal.stage_separator": " \u00b7 ",
|
||||
"ui.first_signal.waiting": "Waiting for first signal\u2026",
|
||||
"ui.first_signal.range_separator": "–",
|
||||
"ui.first_signal.stage_separator": " · ",
|
||||
"ui.first_signal.waiting": "Waiting for first signal…",
|
||||
"ui.first_signal.not_available": "Signal not available yet.",
|
||||
"ui.first_signal.offline": "Offline. Last known signal may be stale.",
|
||||
"ui.first_signal.failed": "Failed to load signal.",
|
||||
@@ -202,20 +187,17 @@
|
||||
"ui.first_signal.stage.report": "Generating report",
|
||||
"ui.first_signal.stage.unknown": "Processing",
|
||||
"ui.first_signal.aria.card_label": "First signal status",
|
||||
|
||||
"ui.severity.critical": "Critical",
|
||||
"ui.severity.high": "High",
|
||||
"ui.severity.medium": "Medium",
|
||||
"ui.severity.low": "Low",
|
||||
"ui.severity.info": "Info",
|
||||
"ui.severity.none": "None",
|
||||
|
||||
"ui.release_orchestrator.title": "Release Orchestrator",
|
||||
"ui.release_orchestrator.subtitle": "Pipeline overview and release management",
|
||||
"ui.release_orchestrator.pipeline_runs": "Pipeline Runs",
|
||||
"ui.release_orchestrator.refresh_dashboard": "Refresh dashboard",
|
||||
|
||||
"ui.risk_dashboard.eyebrow": "Gateway \u00b7 Risk",
|
||||
"ui.risk_dashboard.eyebrow": "Gateway · Risk",
|
||||
"ui.risk_dashboard.title": "Risk Profiles",
|
||||
"ui.risk_dashboard.subtitle": "Tenant-scoped risk posture with deterministic ordering.",
|
||||
"ui.risk_dashboard.up_to_date": "Up to date",
|
||||
@@ -225,8 +207,7 @@
|
||||
"ui.risk_dashboard.risks_suffix": "risks.",
|
||||
"ui.risk_dashboard.error_unable_to_load": "Unable to load risk profiles.",
|
||||
"ui.risk_dashboard.no_risks_found": "No risks found for current filters.",
|
||||
"ui.risk_dashboard.loading_risks": "Loading risks\u2026",
|
||||
|
||||
"ui.risk_dashboard.loading_risks": "Loading risks…",
|
||||
"ui.findings.title": "Findings",
|
||||
"ui.findings.search_placeholder": "Search findings...",
|
||||
"ui.findings.clear_filters": "Clear Filters",
|
||||
@@ -242,7 +223,6 @@
|
||||
"ui.findings.select": "Select",
|
||||
"ui.findings.no_findings": "No findings to display.",
|
||||
"ui.findings.no_match": "No findings match the current filters.",
|
||||
|
||||
"ui.sources_dashboard.title": "Sources Dashboard",
|
||||
"ui.sources_dashboard.verifying": "Verifying...",
|
||||
"ui.sources_dashboard.verify_24h": "Verify last 24h",
|
||||
@@ -269,7 +249,6 @@
|
||||
"ui.sources_dashboard.data_from": "Data from",
|
||||
"ui.sources_dashboard.to": "to",
|
||||
"ui.sources_dashboard.hour_window": "h window",
|
||||
|
||||
"ui.timeline.title": "Timeline",
|
||||
"ui.timeline.event_timeline": "Event Timeline",
|
||||
"ui.timeline.refresh_timeline": "Refresh timeline",
|
||||
@@ -280,7 +259,6 @@
|
||||
"ui.timeline.load_more": "Load more events",
|
||||
"ui.timeline.event_details": "Event details",
|
||||
"ui.timeline.events": "events",
|
||||
|
||||
"ui.exception_center.title": "Exception Center",
|
||||
"ui.exception_center.list_view": "List view",
|
||||
"ui.exception_center.kanban_view": "Kanban view",
|
||||
@@ -298,7 +276,6 @@
|
||||
"ui.exception_center.no_exceptions": "No exceptions match the current filters",
|
||||
"ui.exception_center.column_empty": "No exceptions",
|
||||
"ui.exception_center.exceptions_suffix": "exceptions",
|
||||
|
||||
"ui.evidence_thread.back_to_list": "Back to list",
|
||||
"ui.evidence_thread.title_default": "Evidence Thread",
|
||||
"ui.evidence_thread.copy_digest": "Copy full digest",
|
||||
@@ -309,7 +286,6 @@
|
||||
"ui.evidence_thread.timeline_tab": "Timeline",
|
||||
"ui.evidence_thread.transcript_tab": "Transcript",
|
||||
"ui.evidence_thread.not_found": "No evidence thread found for this artifact.",
|
||||
|
||||
"ui.vulnerability_detail.eyebrow": "Vulnerability",
|
||||
"ui.vulnerability_detail.cvss": "CVSS",
|
||||
"ui.vulnerability_detail.impact_first": "Impact First",
|
||||
@@ -328,5 +304,76 @@
|
||||
"ui.vulnerability_detail.evidence_tree": "Evidence Tree and Citation Links",
|
||||
"ui.vulnerability_detail.evidence_explorer": "evidence explorer",
|
||||
"ui.vulnerability_detail.references": "References",
|
||||
"ui.vulnerability_detail.back_to_risk": "Back to Risk"
|
||||
"ui.vulnerability_detail.back_to_risk": "Back to Risk",
|
||||
"ui.search.input_aria_label": "Global search",
|
||||
"ui.search.loading": "Searching...",
|
||||
"ui.search.no_results": "No results found",
|
||||
"ui.search.did_you_mean_label": "Did you mean:",
|
||||
"ui.search.try_also_label": "Try also:",
|
||||
"ui.search.recent_label": "Recent",
|
||||
"ui.search.clear_history": "Clear",
|
||||
"ui.search.suggested_label": "Suggested",
|
||||
"ui.search.empty_state_header": "Search across your release control plane",
|
||||
"ui.search.quick_action.getting_started": "Getting Started",
|
||||
"ui.search.quick_action.run_health_check": "Run Health Check",
|
||||
"ui.search.quick_action.view_recent_scans": "View Recent Scans",
|
||||
"ui.search.filter.all": "All",
|
||||
"ui.search.placeholder.default": "Search everything...",
|
||||
"ui.search.placeholder.try": "Try: {suggestion}",
|
||||
"ui.search.domain.findings.title": "Security Findings",
|
||||
"ui.search.domain.findings.description": "CVEs, vulnerabilities, and exposure data across your images.",
|
||||
"ui.search.domain.findings.example": "CVE-2024-21626",
|
||||
"ui.search.domain.vex.title": "VEX Statements",
|
||||
"ui.search.domain.vex.description": "Exploitability statuses and vendor assertions for vulnerabilities.",
|
||||
"ui.search.domain.vex.example": "not_affected",
|
||||
"ui.search.domain.policy.title": "Policy Rules",
|
||||
"ui.search.domain.policy.description": "Release gate rules, exceptions, and enforcement outcomes.",
|
||||
"ui.search.domain.policy.example": "DENY-CRITICAL-PROD",
|
||||
"ui.search.domain.docs.title": "Documentation",
|
||||
"ui.search.domain.docs.description": "Guides, architecture references, and operator runbooks.",
|
||||
"ui.search.domain.docs.example": "how to deploy",
|
||||
"ui.search.domain.api.title": "API Reference",
|
||||
"ui.search.domain.api.description": "OpenAPI endpoints, request contracts, and service operations.",
|
||||
"ui.search.domain.api.example": "POST /api/v1/scanner/scans",
|
||||
"ui.search.domain.health.title": "Health Checks",
|
||||
"ui.search.domain.health.description": "Doctor checks, readiness diagnostics, and remediation guidance.",
|
||||
"ui.search.domain.health.example": "database connectivity",
|
||||
"ui.search.domain.operations.title": "Operations",
|
||||
"ui.search.domain.operations.description": "Jobs, workflows, and operational controls across environments.",
|
||||
"ui.search.domain.operations.example": "blocked releases",
|
||||
"ui.search.domain.timeline.title": "Timeline",
|
||||
"ui.search.domain.timeline.description": "Promotion history, incidents, and causal event traces.",
|
||||
"ui.search.domain.timeline.example": "release history",
|
||||
"ui.search.suggestion.findings.critical": "critical findings",
|
||||
"ui.search.suggestion.findings.reachable": "reachable vulnerabilities",
|
||||
"ui.search.suggestion.findings.unresolved": "unresolved CVEs",
|
||||
"ui.search.suggestion.policy.failing_gates": "failing policy gates",
|
||||
"ui.search.suggestion.policy.production_deny": "production deny rules",
|
||||
"ui.search.suggestion.policy.exceptions": "policy exceptions",
|
||||
"ui.search.suggestion.doctor.database": "database connectivity",
|
||||
"ui.search.suggestion.doctor.disk": "disk space",
|
||||
"ui.search.suggestion.doctor.oidc": "OIDC readiness",
|
||||
"ui.search.suggestion.timeline.failed_deployments": "failed deployments",
|
||||
"ui.search.suggestion.timeline.recent_promotions": "recent promotions",
|
||||
"ui.search.suggestion.timeline.release_history": "release history",
|
||||
"ui.search.suggestion.releases.pending_approvals": "pending approvals",
|
||||
"ui.search.suggestion.releases.blocked_releases": "blocked releases",
|
||||
"ui.search.suggestion.releases.environment_status": "environment status",
|
||||
"ui.search.suggestion.default.deploy": "How do I deploy?",
|
||||
"ui.search.suggestion.default.vex": "What is a VEX statement?",
|
||||
"ui.search.suggestion.default.critical": "Show critical findings",
|
||||
"ui.chat.input.waiting": "Waiting for response...",
|
||||
"ui.chat.input.placeholder": "Ask AdvisoryAI about this finding...",
|
||||
"ui.chat.suggestion.vulnerability.exploitable": "Is this exploitable in my environment?",
|
||||
"ui.chat.suggestion.vulnerability.remediation": "What is the remediation?",
|
||||
"ui.chat.suggestion.vulnerability.evidence_chain": "Show me the evidence chain",
|
||||
"ui.chat.suggestion.vulnerability.draft_vex": "Draft a VEX statement",
|
||||
"ui.chat.suggestion.policy.explain_rule": "Explain this policy rule",
|
||||
"ui.chat.suggestion.policy.override_gate": "What would happen if I override this gate?",
|
||||
"ui.chat.suggestion.policy.recent_violations": "Show me recent policy violations",
|
||||
"ui.chat.suggestion.policy.add_exception": "How do I add an exception?",
|
||||
"ui.chat.suggestion.default.what_can_do": "What can Stella Ops do?",
|
||||
"ui.chat.suggestion.default.first_scan": "How do I set up my first scan?",
|
||||
"ui.chat.suggestion.default.promotion_workflow": "Explain the release promotion workflow",
|
||||
"ui.chat.suggestion.default.health_checks": "What health checks should I run first?"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"_meta": { "locale": "zh-CN", "description": "Offline fallback bundle for StellaOps Console" },
|
||||
|
||||
"_meta": {
|
||||
"locale": "zh-CN",
|
||||
"description": "Offline fallback bundle for StellaOps Console"
|
||||
},
|
||||
"common.error.generic": "Something went wrong.",
|
||||
"common.error.not_found": "The requested resource was not found.",
|
||||
"common.error.unauthorized": "You do not have permission to perform this action.",
|
||||
@@ -9,12 +11,10 @@
|
||||
"common.error.timeout": "Request timed out.",
|
||||
"common.error.server_error": "An internal server error occurred. Please try again later.",
|
||||
"common.error.network": "Network error. Check your connection.",
|
||||
|
||||
"common.validation.required": "This field is required.",
|
||||
"common.validation.invalid": "Invalid value.",
|
||||
"common.validation.too_long": "Maximum {max} characters allowed.",
|
||||
"common.validation.too_short": "Minimum {min} characters required.",
|
||||
|
||||
"common.actions.save": "Save",
|
||||
"common.actions.cancel": "Cancel",
|
||||
"common.actions.delete": "Delete",
|
||||
@@ -26,7 +26,6 @@
|
||||
"common.actions.collapse": "Collapse",
|
||||
"common.actions.show_more": "Show more",
|
||||
"common.actions.show_less": "Show less",
|
||||
|
||||
"common.status.healthy": "Healthy",
|
||||
"common.status.degraded": "Degraded",
|
||||
"common.status.unavailable": "Unavailable",
|
||||
@@ -39,16 +38,13 @@
|
||||
"common.status.failed": "Failed",
|
||||
"common.status.canceled": "Canceled",
|
||||
"common.status.blocked": "Blocked",
|
||||
|
||||
"common.severity.critical": "Critical",
|
||||
"common.severity.high": "High",
|
||||
"common.severity.medium": "Medium",
|
||||
"common.severity.low": "Low",
|
||||
"common.severity.info": "Info",
|
||||
"common.severity.none": "None",
|
||||
|
||||
"common.time.just_now": "Just now",
|
||||
|
||||
"common.ui.loading": "Loading...",
|
||||
"common.ui.saving": "Saving...",
|
||||
"common.ui.deleting": "Deleting...",
|
||||
@@ -57,11 +53,9 @@
|
||||
"common.ui.offline": "You are offline.",
|
||||
"common.ui.reconnecting": "Reconnecting...",
|
||||
"common.ui.back_online": "Back online.",
|
||||
|
||||
"ui.loading.skeleton": "Loading...",
|
||||
"ui.loading.spinner": "Please wait...",
|
||||
"ui.loading.slow": "This is taking longer than expected...",
|
||||
|
||||
"ui.error.generic": "Something went wrong.",
|
||||
"ui.error.network": "Network error. Check your connection.",
|
||||
"ui.error.timeout": "Request timed out. Please try again.",
|
||||
@@ -70,19 +64,16 @@
|
||||
"ui.error.server_error": "Server error. Please try again later.",
|
||||
"ui.error.try_again": "Try again",
|
||||
"ui.error.go_back": "Go back",
|
||||
|
||||
"ui.offline.banner": "You're offline.",
|
||||
"ui.offline.description": "Some features may be unavailable.",
|
||||
"ui.offline.reconnecting": "Reconnecting...",
|
||||
"ui.offline.reconnected": "Back online.",
|
||||
|
||||
"ui.toast.success": "Success",
|
||||
"ui.toast.info": "Info",
|
||||
"ui.toast.warning": "Warning",
|
||||
"ui.toast.error": "Error",
|
||||
"ui.toast.dismiss": "Dismiss",
|
||||
"ui.toast.undo": "Undo",
|
||||
|
||||
"ui.actions.save": "Save",
|
||||
"ui.actions.saving": "Saving...",
|
||||
"ui.actions.saved": "Saved",
|
||||
@@ -111,7 +102,6 @@
|
||||
"ui.actions.sign_in": "Sign in",
|
||||
"ui.actions.back_to_list": "Back to list",
|
||||
"ui.actions.load_more": "Load more",
|
||||
|
||||
"ui.labels.all": "All",
|
||||
"ui.labels.title": "Title",
|
||||
"ui.labels.description": "Description",
|
||||
@@ -131,14 +121,12 @@
|
||||
"ui.labels.selected": "selected",
|
||||
"ui.labels.last_updated": "Last updated:",
|
||||
"ui.labels.expires": "Expires",
|
||||
|
||||
"ui.validation.required": "This field is required.",
|
||||
"ui.validation.invalid": "Invalid value.",
|
||||
"ui.validation.too_long": "Maximum {max} characters allowed.",
|
||||
"ui.validation.too_short": "Minimum {min} characters required.",
|
||||
"ui.validation.invalid_email": "Please enter a valid email address.",
|
||||
"ui.validation.invalid_url": "Please enter a valid URL.",
|
||||
|
||||
"ui.a11y.loading": "Content is loading.",
|
||||
"ui.a11y.loaded": "Content loaded.",
|
||||
"ui.a11y.error": "An error occurred.",
|
||||
@@ -148,10 +136,8 @@
|
||||
"ui.a11y.deselected": "Deselected",
|
||||
"ui.a11y.required": "Required field",
|
||||
"ui.a11y.optional": "Optional",
|
||||
|
||||
"ui.motion.reduced": "Animations reduced.",
|
||||
"ui.motion.enabled": "Animations enabled.",
|
||||
|
||||
"ui.auth.fresh_active": "Fresh auth: Active",
|
||||
"ui.auth.fresh_stale": "Fresh auth: Stale",
|
||||
"ui.locale.label": "Language",
|
||||
@@ -171,15 +157,14 @@
|
||||
"ui.settings.language.persisted": "Yi baocun dao nin de zhanghu bing zai CLI zhong chongyong.",
|
||||
"ui.settings.language.persisted_error": "Yi ben di baocun, dan zhanghu tongbu shibai.",
|
||||
"ui.settings.language.sign_in_hint": "Qing denglu yi jiang ci pianhao tongbu dao CLI.",
|
||||
|
||||
"ui.first_signal.label": "First signal",
|
||||
"ui.first_signal.run_prefix": "Run:",
|
||||
"ui.first_signal.live": "Live",
|
||||
"ui.first_signal.polling": "Polling",
|
||||
"ui.first_signal.range_prefix": "Range",
|
||||
"ui.first_signal.range_separator": "\u2013",
|
||||
"ui.first_signal.stage_separator": " \u00b7 ",
|
||||
"ui.first_signal.waiting": "Waiting for first signal\u2026",
|
||||
"ui.first_signal.range_separator": "–",
|
||||
"ui.first_signal.stage_separator": " · ",
|
||||
"ui.first_signal.waiting": "Waiting for first signal…",
|
||||
"ui.first_signal.not_available": "Signal not available yet.",
|
||||
"ui.first_signal.offline": "Offline. Last known signal may be stale.",
|
||||
"ui.first_signal.failed": "Failed to load signal.",
|
||||
@@ -202,20 +187,17 @@
|
||||
"ui.first_signal.stage.report": "Generating report",
|
||||
"ui.first_signal.stage.unknown": "Processing",
|
||||
"ui.first_signal.aria.card_label": "First signal status",
|
||||
|
||||
"ui.severity.critical": "Critical",
|
||||
"ui.severity.high": "High",
|
||||
"ui.severity.medium": "Medium",
|
||||
"ui.severity.low": "Low",
|
||||
"ui.severity.info": "Info",
|
||||
"ui.severity.none": "None",
|
||||
|
||||
"ui.release_orchestrator.title": "Release Orchestrator",
|
||||
"ui.release_orchestrator.subtitle": "Pipeline overview and release management",
|
||||
"ui.release_orchestrator.pipeline_runs": "Pipeline Runs",
|
||||
"ui.release_orchestrator.refresh_dashboard": "Refresh dashboard",
|
||||
|
||||
"ui.risk_dashboard.eyebrow": "Gateway \u00b7 Risk",
|
||||
"ui.risk_dashboard.eyebrow": "Gateway · Risk",
|
||||
"ui.risk_dashboard.title": "Risk Profiles",
|
||||
"ui.risk_dashboard.subtitle": "Tenant-scoped risk posture with deterministic ordering.",
|
||||
"ui.risk_dashboard.up_to_date": "Up to date",
|
||||
@@ -225,8 +207,7 @@
|
||||
"ui.risk_dashboard.risks_suffix": "risks.",
|
||||
"ui.risk_dashboard.error_unable_to_load": "Unable to load risk profiles.",
|
||||
"ui.risk_dashboard.no_risks_found": "No risks found for current filters.",
|
||||
"ui.risk_dashboard.loading_risks": "Loading risks\u2026",
|
||||
|
||||
"ui.risk_dashboard.loading_risks": "Loading risks…",
|
||||
"ui.findings.title": "Findings",
|
||||
"ui.findings.search_placeholder": "Search findings...",
|
||||
"ui.findings.clear_filters": "Clear Filters",
|
||||
@@ -242,7 +223,6 @@
|
||||
"ui.findings.select": "Select",
|
||||
"ui.findings.no_findings": "No findings to display.",
|
||||
"ui.findings.no_match": "No findings match the current filters.",
|
||||
|
||||
"ui.sources_dashboard.title": "Sources Dashboard",
|
||||
"ui.sources_dashboard.verifying": "Verifying...",
|
||||
"ui.sources_dashboard.verify_24h": "Verify last 24h",
|
||||
@@ -269,7 +249,6 @@
|
||||
"ui.sources_dashboard.data_from": "Data from",
|
||||
"ui.sources_dashboard.to": "to",
|
||||
"ui.sources_dashboard.hour_window": "h window",
|
||||
|
||||
"ui.timeline.title": "Timeline",
|
||||
"ui.timeline.event_timeline": "Event Timeline",
|
||||
"ui.timeline.refresh_timeline": "Refresh timeline",
|
||||
@@ -280,7 +259,6 @@
|
||||
"ui.timeline.load_more": "Load more events",
|
||||
"ui.timeline.event_details": "Event details",
|
||||
"ui.timeline.events": "events",
|
||||
|
||||
"ui.exception_center.title": "Exception Center",
|
||||
"ui.exception_center.list_view": "List view",
|
||||
"ui.exception_center.kanban_view": "Kanban view",
|
||||
@@ -298,7 +276,6 @@
|
||||
"ui.exception_center.no_exceptions": "No exceptions match the current filters",
|
||||
"ui.exception_center.column_empty": "No exceptions",
|
||||
"ui.exception_center.exceptions_suffix": "exceptions",
|
||||
|
||||
"ui.evidence_thread.back_to_list": "Back to list",
|
||||
"ui.evidence_thread.title_default": "Evidence Thread",
|
||||
"ui.evidence_thread.copy_digest": "Copy full digest",
|
||||
@@ -309,7 +286,6 @@
|
||||
"ui.evidence_thread.timeline_tab": "Timeline",
|
||||
"ui.evidence_thread.transcript_tab": "Transcript",
|
||||
"ui.evidence_thread.not_found": "No evidence thread found for this artifact.",
|
||||
|
||||
"ui.vulnerability_detail.eyebrow": "Vulnerability",
|
||||
"ui.vulnerability_detail.cvss": "CVSS",
|
||||
"ui.vulnerability_detail.impact_first": "Impact First",
|
||||
@@ -328,5 +304,76 @@
|
||||
"ui.vulnerability_detail.evidence_tree": "Evidence Tree and Citation Links",
|
||||
"ui.vulnerability_detail.evidence_explorer": "evidence explorer",
|
||||
"ui.vulnerability_detail.references": "References",
|
||||
"ui.vulnerability_detail.back_to_risk": "Back to Risk"
|
||||
"ui.vulnerability_detail.back_to_risk": "Back to Risk",
|
||||
"ui.search.input_aria_label": "Global search",
|
||||
"ui.search.loading": "Searching...",
|
||||
"ui.search.no_results": "No results found",
|
||||
"ui.search.did_you_mean_label": "Did you mean:",
|
||||
"ui.search.try_also_label": "Try also:",
|
||||
"ui.search.recent_label": "Recent",
|
||||
"ui.search.clear_history": "Clear",
|
||||
"ui.search.suggested_label": "Suggested",
|
||||
"ui.search.empty_state_header": "Search across your release control plane",
|
||||
"ui.search.quick_action.getting_started": "Getting Started",
|
||||
"ui.search.quick_action.run_health_check": "Run Health Check",
|
||||
"ui.search.quick_action.view_recent_scans": "View Recent Scans",
|
||||
"ui.search.filter.all": "All",
|
||||
"ui.search.placeholder.default": "Search everything...",
|
||||
"ui.search.placeholder.try": "Try: {suggestion}",
|
||||
"ui.search.domain.findings.title": "Security Findings",
|
||||
"ui.search.domain.findings.description": "CVEs, vulnerabilities, and exposure data across your images.",
|
||||
"ui.search.domain.findings.example": "CVE-2024-21626",
|
||||
"ui.search.domain.vex.title": "VEX Statements",
|
||||
"ui.search.domain.vex.description": "Exploitability statuses and vendor assertions for vulnerabilities.",
|
||||
"ui.search.domain.vex.example": "not_affected",
|
||||
"ui.search.domain.policy.title": "Policy Rules",
|
||||
"ui.search.domain.policy.description": "Release gate rules, exceptions, and enforcement outcomes.",
|
||||
"ui.search.domain.policy.example": "DENY-CRITICAL-PROD",
|
||||
"ui.search.domain.docs.title": "Documentation",
|
||||
"ui.search.domain.docs.description": "Guides, architecture references, and operator runbooks.",
|
||||
"ui.search.domain.docs.example": "how to deploy",
|
||||
"ui.search.domain.api.title": "API Reference",
|
||||
"ui.search.domain.api.description": "OpenAPI endpoints, request contracts, and service operations.",
|
||||
"ui.search.domain.api.example": "POST /api/v1/scanner/scans",
|
||||
"ui.search.domain.health.title": "Health Checks",
|
||||
"ui.search.domain.health.description": "Doctor checks, readiness diagnostics, and remediation guidance.",
|
||||
"ui.search.domain.health.example": "database connectivity",
|
||||
"ui.search.domain.operations.title": "Operations",
|
||||
"ui.search.domain.operations.description": "Jobs, workflows, and operational controls across environments.",
|
||||
"ui.search.domain.operations.example": "blocked releases",
|
||||
"ui.search.domain.timeline.title": "Timeline",
|
||||
"ui.search.domain.timeline.description": "Promotion history, incidents, and causal event traces.",
|
||||
"ui.search.domain.timeline.example": "release history",
|
||||
"ui.search.suggestion.findings.critical": "critical findings",
|
||||
"ui.search.suggestion.findings.reachable": "reachable vulnerabilities",
|
||||
"ui.search.suggestion.findings.unresolved": "unresolved CVEs",
|
||||
"ui.search.suggestion.policy.failing_gates": "failing policy gates",
|
||||
"ui.search.suggestion.policy.production_deny": "production deny rules",
|
||||
"ui.search.suggestion.policy.exceptions": "policy exceptions",
|
||||
"ui.search.suggestion.doctor.database": "database connectivity",
|
||||
"ui.search.suggestion.doctor.disk": "disk space",
|
||||
"ui.search.suggestion.doctor.oidc": "OIDC readiness",
|
||||
"ui.search.suggestion.timeline.failed_deployments": "failed deployments",
|
||||
"ui.search.suggestion.timeline.recent_promotions": "recent promotions",
|
||||
"ui.search.suggestion.timeline.release_history": "release history",
|
||||
"ui.search.suggestion.releases.pending_approvals": "pending approvals",
|
||||
"ui.search.suggestion.releases.blocked_releases": "blocked releases",
|
||||
"ui.search.suggestion.releases.environment_status": "environment status",
|
||||
"ui.search.suggestion.default.deploy": "How do I deploy?",
|
||||
"ui.search.suggestion.default.vex": "What is a VEX statement?",
|
||||
"ui.search.suggestion.default.critical": "Show critical findings",
|
||||
"ui.chat.input.waiting": "Waiting for response...",
|
||||
"ui.chat.input.placeholder": "Ask AdvisoryAI about this finding...",
|
||||
"ui.chat.suggestion.vulnerability.exploitable": "Is this exploitable in my environment?",
|
||||
"ui.chat.suggestion.vulnerability.remediation": "What is the remediation?",
|
||||
"ui.chat.suggestion.vulnerability.evidence_chain": "Show me the evidence chain",
|
||||
"ui.chat.suggestion.vulnerability.draft_vex": "Draft a VEX statement",
|
||||
"ui.chat.suggestion.policy.explain_rule": "Explain this policy rule",
|
||||
"ui.chat.suggestion.policy.override_gate": "What would happen if I override this gate?",
|
||||
"ui.chat.suggestion.policy.recent_violations": "Show me recent policy violations",
|
||||
"ui.chat.suggestion.policy.add_exception": "How do I add an exception?",
|
||||
"ui.chat.suggestion.default.what_can_do": "What can Stella Ops do?",
|
||||
"ui.chat.suggestion.default.first_scan": "How do I set up my first scan?",
|
||||
"ui.chat.suggestion.default.promotion_workflow": "Explain the release promotion workflow",
|
||||
"ui.chat.suggestion.default.health_checks": "What health checks should I run first?"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"_meta": { "locale": "zh-TW", "description": "Offline fallback bundle for StellaOps Console" },
|
||||
|
||||
"_meta": {
|
||||
"locale": "zh-TW",
|
||||
"description": "Offline fallback bundle for StellaOps Console"
|
||||
},
|
||||
"common.error.generic": "Something went wrong.",
|
||||
"common.error.not_found": "The requested resource was not found.",
|
||||
"common.error.unauthorized": "You do not have permission to perform this action.",
|
||||
@@ -9,12 +11,10 @@
|
||||
"common.error.timeout": "Request timed out.",
|
||||
"common.error.server_error": "An internal server error occurred. Please try again later.",
|
||||
"common.error.network": "Network error. Check your connection.",
|
||||
|
||||
"common.validation.required": "This field is required.",
|
||||
"common.validation.invalid": "Invalid value.",
|
||||
"common.validation.too_long": "Maximum {max} characters allowed.",
|
||||
"common.validation.too_short": "Minimum {min} characters required.",
|
||||
|
||||
"common.actions.save": "Save",
|
||||
"common.actions.cancel": "Cancel",
|
||||
"common.actions.delete": "Delete",
|
||||
@@ -26,7 +26,6 @@
|
||||
"common.actions.collapse": "Collapse",
|
||||
"common.actions.show_more": "Show more",
|
||||
"common.actions.show_less": "Show less",
|
||||
|
||||
"common.status.healthy": "Healthy",
|
||||
"common.status.degraded": "Degraded",
|
||||
"common.status.unavailable": "Unavailable",
|
||||
@@ -39,16 +38,13 @@
|
||||
"common.status.failed": "Failed",
|
||||
"common.status.canceled": "Canceled",
|
||||
"common.status.blocked": "Blocked",
|
||||
|
||||
"common.severity.critical": "Critical",
|
||||
"common.severity.high": "High",
|
||||
"common.severity.medium": "Medium",
|
||||
"common.severity.low": "Low",
|
||||
"common.severity.info": "Info",
|
||||
"common.severity.none": "None",
|
||||
|
||||
"common.time.just_now": "Just now",
|
||||
|
||||
"common.ui.loading": "Loading...",
|
||||
"common.ui.saving": "Saving...",
|
||||
"common.ui.deleting": "Deleting...",
|
||||
@@ -57,11 +53,9 @@
|
||||
"common.ui.offline": "You are offline.",
|
||||
"common.ui.reconnecting": "Reconnecting...",
|
||||
"common.ui.back_online": "Back online.",
|
||||
|
||||
"ui.loading.skeleton": "Loading...",
|
||||
"ui.loading.spinner": "Please wait...",
|
||||
"ui.loading.slow": "This is taking longer than expected...",
|
||||
|
||||
"ui.error.generic": "Something went wrong.",
|
||||
"ui.error.network": "Network error. Check your connection.",
|
||||
"ui.error.timeout": "Request timed out. Please try again.",
|
||||
@@ -70,19 +64,16 @@
|
||||
"ui.error.server_error": "Server error. Please try again later.",
|
||||
"ui.error.try_again": "Try again",
|
||||
"ui.error.go_back": "Go back",
|
||||
|
||||
"ui.offline.banner": "You're offline.",
|
||||
"ui.offline.description": "Some features may be unavailable.",
|
||||
"ui.offline.reconnecting": "Reconnecting...",
|
||||
"ui.offline.reconnected": "Back online.",
|
||||
|
||||
"ui.toast.success": "Success",
|
||||
"ui.toast.info": "Info",
|
||||
"ui.toast.warning": "Warning",
|
||||
"ui.toast.error": "Error",
|
||||
"ui.toast.dismiss": "Dismiss",
|
||||
"ui.toast.undo": "Undo",
|
||||
|
||||
"ui.actions.save": "Save",
|
||||
"ui.actions.saving": "Saving...",
|
||||
"ui.actions.saved": "Saved",
|
||||
@@ -111,7 +102,6 @@
|
||||
"ui.actions.sign_in": "Sign in",
|
||||
"ui.actions.back_to_list": "Back to list",
|
||||
"ui.actions.load_more": "Load more",
|
||||
|
||||
"ui.labels.all": "All",
|
||||
"ui.labels.title": "Title",
|
||||
"ui.labels.description": "Description",
|
||||
@@ -131,14 +121,12 @@
|
||||
"ui.labels.selected": "selected",
|
||||
"ui.labels.last_updated": "Last updated:",
|
||||
"ui.labels.expires": "Expires",
|
||||
|
||||
"ui.validation.required": "This field is required.",
|
||||
"ui.validation.invalid": "Invalid value.",
|
||||
"ui.validation.too_long": "Maximum {max} characters allowed.",
|
||||
"ui.validation.too_short": "Minimum {min} characters required.",
|
||||
"ui.validation.invalid_email": "Please enter a valid email address.",
|
||||
"ui.validation.invalid_url": "Please enter a valid URL.",
|
||||
|
||||
"ui.a11y.loading": "Content is loading.",
|
||||
"ui.a11y.loaded": "Content loaded.",
|
||||
"ui.a11y.error": "An error occurred.",
|
||||
@@ -148,10 +136,8 @@
|
||||
"ui.a11y.deselected": "Deselected",
|
||||
"ui.a11y.required": "Required field",
|
||||
"ui.a11y.optional": "Optional",
|
||||
|
||||
"ui.motion.reduced": "Animations reduced.",
|
||||
"ui.motion.enabled": "Animations enabled.",
|
||||
|
||||
"ui.auth.fresh_active": "Fresh auth: Active",
|
||||
"ui.auth.fresh_stale": "Fresh auth: Stale",
|
||||
"ui.locale.label": "Language",
|
||||
@@ -171,15 +157,14 @@
|
||||
"ui.settings.language.persisted": "Yijing baocun dao zhanghu bing gong CLI chongyong.",
|
||||
"ui.settings.language.persisted_error": "Yijing benji baocun, dan zhanghu tongbu shibai.",
|
||||
"ui.settings.language.sign_in_hint": "Qing dengru yi jiang ci pianhao tongbu dao CLI.",
|
||||
|
||||
"ui.first_signal.label": "First signal",
|
||||
"ui.first_signal.run_prefix": "Run:",
|
||||
"ui.first_signal.live": "Live",
|
||||
"ui.first_signal.polling": "Polling",
|
||||
"ui.first_signal.range_prefix": "Range",
|
||||
"ui.first_signal.range_separator": "\u2013",
|
||||
"ui.first_signal.stage_separator": " \u00b7 ",
|
||||
"ui.first_signal.waiting": "Waiting for first signal\u2026",
|
||||
"ui.first_signal.range_separator": "–",
|
||||
"ui.first_signal.stage_separator": " · ",
|
||||
"ui.first_signal.waiting": "Waiting for first signal…",
|
||||
"ui.first_signal.not_available": "Signal not available yet.",
|
||||
"ui.first_signal.offline": "Offline. Last known signal may be stale.",
|
||||
"ui.first_signal.failed": "Failed to load signal.",
|
||||
@@ -202,20 +187,17 @@
|
||||
"ui.first_signal.stage.report": "Generating report",
|
||||
"ui.first_signal.stage.unknown": "Processing",
|
||||
"ui.first_signal.aria.card_label": "First signal status",
|
||||
|
||||
"ui.severity.critical": "Critical",
|
||||
"ui.severity.high": "High",
|
||||
"ui.severity.medium": "Medium",
|
||||
"ui.severity.low": "Low",
|
||||
"ui.severity.info": "Info",
|
||||
"ui.severity.none": "None",
|
||||
|
||||
"ui.release_orchestrator.title": "Release Orchestrator",
|
||||
"ui.release_orchestrator.subtitle": "Pipeline overview and release management",
|
||||
"ui.release_orchestrator.pipeline_runs": "Pipeline Runs",
|
||||
"ui.release_orchestrator.refresh_dashboard": "Refresh dashboard",
|
||||
|
||||
"ui.risk_dashboard.eyebrow": "Gateway \u00b7 Risk",
|
||||
"ui.risk_dashboard.eyebrow": "Gateway · Risk",
|
||||
"ui.risk_dashboard.title": "Risk Profiles",
|
||||
"ui.risk_dashboard.subtitle": "Tenant-scoped risk posture with deterministic ordering.",
|
||||
"ui.risk_dashboard.up_to_date": "Up to date",
|
||||
@@ -225,8 +207,7 @@
|
||||
"ui.risk_dashboard.risks_suffix": "risks.",
|
||||
"ui.risk_dashboard.error_unable_to_load": "Unable to load risk profiles.",
|
||||
"ui.risk_dashboard.no_risks_found": "No risks found for current filters.",
|
||||
"ui.risk_dashboard.loading_risks": "Loading risks\u2026",
|
||||
|
||||
"ui.risk_dashboard.loading_risks": "Loading risks…",
|
||||
"ui.findings.title": "Findings",
|
||||
"ui.findings.search_placeholder": "Search findings...",
|
||||
"ui.findings.clear_filters": "Clear Filters",
|
||||
@@ -242,7 +223,6 @@
|
||||
"ui.findings.select": "Select",
|
||||
"ui.findings.no_findings": "No findings to display.",
|
||||
"ui.findings.no_match": "No findings match the current filters.",
|
||||
|
||||
"ui.sources_dashboard.title": "Sources Dashboard",
|
||||
"ui.sources_dashboard.verifying": "Verifying...",
|
||||
"ui.sources_dashboard.verify_24h": "Verify last 24h",
|
||||
@@ -269,7 +249,6 @@
|
||||
"ui.sources_dashboard.data_from": "Data from",
|
||||
"ui.sources_dashboard.to": "to",
|
||||
"ui.sources_dashboard.hour_window": "h window",
|
||||
|
||||
"ui.timeline.title": "Timeline",
|
||||
"ui.timeline.event_timeline": "Event Timeline",
|
||||
"ui.timeline.refresh_timeline": "Refresh timeline",
|
||||
@@ -280,7 +259,6 @@
|
||||
"ui.timeline.load_more": "Load more events",
|
||||
"ui.timeline.event_details": "Event details",
|
||||
"ui.timeline.events": "events",
|
||||
|
||||
"ui.exception_center.title": "Exception Center",
|
||||
"ui.exception_center.list_view": "List view",
|
||||
"ui.exception_center.kanban_view": "Kanban view",
|
||||
@@ -298,7 +276,6 @@
|
||||
"ui.exception_center.no_exceptions": "No exceptions match the current filters",
|
||||
"ui.exception_center.column_empty": "No exceptions",
|
||||
"ui.exception_center.exceptions_suffix": "exceptions",
|
||||
|
||||
"ui.evidence_thread.back_to_list": "Back to list",
|
||||
"ui.evidence_thread.title_default": "Evidence Thread",
|
||||
"ui.evidence_thread.copy_digest": "Copy full digest",
|
||||
@@ -309,7 +286,6 @@
|
||||
"ui.evidence_thread.timeline_tab": "Timeline",
|
||||
"ui.evidence_thread.transcript_tab": "Transcript",
|
||||
"ui.evidence_thread.not_found": "No evidence thread found for this artifact.",
|
||||
|
||||
"ui.vulnerability_detail.eyebrow": "Vulnerability",
|
||||
"ui.vulnerability_detail.cvss": "CVSS",
|
||||
"ui.vulnerability_detail.impact_first": "Impact First",
|
||||
@@ -328,5 +304,76 @@
|
||||
"ui.vulnerability_detail.evidence_tree": "Evidence Tree and Citation Links",
|
||||
"ui.vulnerability_detail.evidence_explorer": "evidence explorer",
|
||||
"ui.vulnerability_detail.references": "References",
|
||||
"ui.vulnerability_detail.back_to_risk": "Back to Risk"
|
||||
"ui.vulnerability_detail.back_to_risk": "Back to Risk",
|
||||
"ui.search.input_aria_label": "Global search",
|
||||
"ui.search.loading": "Searching...",
|
||||
"ui.search.no_results": "No results found",
|
||||
"ui.search.did_you_mean_label": "Did you mean:",
|
||||
"ui.search.try_also_label": "Try also:",
|
||||
"ui.search.recent_label": "Recent",
|
||||
"ui.search.clear_history": "Clear",
|
||||
"ui.search.suggested_label": "Suggested",
|
||||
"ui.search.empty_state_header": "Search across your release control plane",
|
||||
"ui.search.quick_action.getting_started": "Getting Started",
|
||||
"ui.search.quick_action.run_health_check": "Run Health Check",
|
||||
"ui.search.quick_action.view_recent_scans": "View Recent Scans",
|
||||
"ui.search.filter.all": "All",
|
||||
"ui.search.placeholder.default": "Search everything...",
|
||||
"ui.search.placeholder.try": "Try: {suggestion}",
|
||||
"ui.search.domain.findings.title": "Security Findings",
|
||||
"ui.search.domain.findings.description": "CVEs, vulnerabilities, and exposure data across your images.",
|
||||
"ui.search.domain.findings.example": "CVE-2024-21626",
|
||||
"ui.search.domain.vex.title": "VEX Statements",
|
||||
"ui.search.domain.vex.description": "Exploitability statuses and vendor assertions for vulnerabilities.",
|
||||
"ui.search.domain.vex.example": "not_affected",
|
||||
"ui.search.domain.policy.title": "Policy Rules",
|
||||
"ui.search.domain.policy.description": "Release gate rules, exceptions, and enforcement outcomes.",
|
||||
"ui.search.domain.policy.example": "DENY-CRITICAL-PROD",
|
||||
"ui.search.domain.docs.title": "Documentation",
|
||||
"ui.search.domain.docs.description": "Guides, architecture references, and operator runbooks.",
|
||||
"ui.search.domain.docs.example": "how to deploy",
|
||||
"ui.search.domain.api.title": "API Reference",
|
||||
"ui.search.domain.api.description": "OpenAPI endpoints, request contracts, and service operations.",
|
||||
"ui.search.domain.api.example": "POST /api/v1/scanner/scans",
|
||||
"ui.search.domain.health.title": "Health Checks",
|
||||
"ui.search.domain.health.description": "Doctor checks, readiness diagnostics, and remediation guidance.",
|
||||
"ui.search.domain.health.example": "database connectivity",
|
||||
"ui.search.domain.operations.title": "Operations",
|
||||
"ui.search.domain.operations.description": "Jobs, workflows, and operational controls across environments.",
|
||||
"ui.search.domain.operations.example": "blocked releases",
|
||||
"ui.search.domain.timeline.title": "Timeline",
|
||||
"ui.search.domain.timeline.description": "Promotion history, incidents, and causal event traces.",
|
||||
"ui.search.domain.timeline.example": "release history",
|
||||
"ui.search.suggestion.findings.critical": "critical findings",
|
||||
"ui.search.suggestion.findings.reachable": "reachable vulnerabilities",
|
||||
"ui.search.suggestion.findings.unresolved": "unresolved CVEs",
|
||||
"ui.search.suggestion.policy.failing_gates": "failing policy gates",
|
||||
"ui.search.suggestion.policy.production_deny": "production deny rules",
|
||||
"ui.search.suggestion.policy.exceptions": "policy exceptions",
|
||||
"ui.search.suggestion.doctor.database": "database connectivity",
|
||||
"ui.search.suggestion.doctor.disk": "disk space",
|
||||
"ui.search.suggestion.doctor.oidc": "OIDC readiness",
|
||||
"ui.search.suggestion.timeline.failed_deployments": "failed deployments",
|
||||
"ui.search.suggestion.timeline.recent_promotions": "recent promotions",
|
||||
"ui.search.suggestion.timeline.release_history": "release history",
|
||||
"ui.search.suggestion.releases.pending_approvals": "pending approvals",
|
||||
"ui.search.suggestion.releases.blocked_releases": "blocked releases",
|
||||
"ui.search.suggestion.releases.environment_status": "environment status",
|
||||
"ui.search.suggestion.default.deploy": "How do I deploy?",
|
||||
"ui.search.suggestion.default.vex": "What is a VEX statement?",
|
||||
"ui.search.suggestion.default.critical": "Show critical findings",
|
||||
"ui.chat.input.waiting": "Waiting for response...",
|
||||
"ui.chat.input.placeholder": "Ask AdvisoryAI about this finding...",
|
||||
"ui.chat.suggestion.vulnerability.exploitable": "Is this exploitable in my environment?",
|
||||
"ui.chat.suggestion.vulnerability.remediation": "What is the remediation?",
|
||||
"ui.chat.suggestion.vulnerability.evidence_chain": "Show me the evidence chain",
|
||||
"ui.chat.suggestion.vulnerability.draft_vex": "Draft a VEX statement",
|
||||
"ui.chat.suggestion.policy.explain_rule": "Explain this policy rule",
|
||||
"ui.chat.suggestion.policy.override_gate": "What would happen if I override this gate?",
|
||||
"ui.chat.suggestion.policy.recent_violations": "Show me recent policy violations",
|
||||
"ui.chat.suggestion.policy.add_exception": "How do I add an exception?",
|
||||
"ui.chat.suggestion.default.what_can_do": "What can Stella Ops do?",
|
||||
"ui.chat.suggestion.default.first_scan": "How do I set up my first scan?",
|
||||
"ui.chat.suggestion.default.promotion_workflow": "Explain the release promotion workflow",
|
||||
"ui.chat.suggestion.default.health_checks": "What health checks should I run first?"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { NavigationEnd, Router } from '@angular/router';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
import { AmbientContextService } from '../../app/core/services/ambient-context.service';
|
||||
|
||||
describe('AmbientContextService', () => {
|
||||
let events: Subject<unknown>;
|
||||
let router: { url: string; events: Subject<unknown> };
|
||||
|
||||
beforeEach(() => {
|
||||
events = new Subject<unknown>();
|
||||
router = {
|
||||
url: '/security/triage',
|
||||
events,
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
AmbientContextService,
|
||||
{ provide: Router, useValue: router },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns findings domain and findings suggestions for triage routes', () => {
|
||||
const service = TestBed.inject(AmbientContextService);
|
||||
|
||||
expect(service.currentDomain()).toBe('findings');
|
||||
expect(service.getSearchSuggestions().map((item) => item.key)).toEqual([
|
||||
'ui.search.suggestion.findings.critical',
|
||||
'ui.search.suggestion.findings.reachable',
|
||||
'ui.search.suggestion.findings.unresolved',
|
||||
]);
|
||||
});
|
||||
|
||||
it('updates search and chat suggestion sets when route changes', () => {
|
||||
const service = TestBed.inject(AmbientContextService);
|
||||
|
||||
router.url = '/ops/policy';
|
||||
events.next(new NavigationEnd(1, '/ops/policy', '/ops/policy'));
|
||||
|
||||
expect(service.currentDomain()).toBe('policy');
|
||||
expect(service.getSearchSuggestions().map((item) => item.key)).toEqual([
|
||||
'ui.search.suggestion.policy.failing_gates',
|
||||
'ui.search.suggestion.policy.production_deny',
|
||||
'ui.search.suggestion.policy.exceptions',
|
||||
]);
|
||||
expect(service.getChatSuggestions().map((item) => item.key)).toEqual([
|
||||
'ui.chat.suggestion.policy.explain_rule',
|
||||
'ui.chat.suggestion.policy.override_gate',
|
||||
'ui.chat.suggestion.policy.recent_violations',
|
||||
'ui.chat.suggestion.policy.add_exception',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,9 +2,11 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { NavigationEnd, Router } from '@angular/router';
|
||||
import { Subject, of } from 'rxjs';
|
||||
|
||||
import type { EntityCard } from '../../app/core/api/unified-search.models';
|
||||
import { UnifiedSearchClient } from '../../app/core/api/unified-search.client';
|
||||
import { AmbientContextService } from '../../app/core/services/ambient-context.service';
|
||||
import { SearchChatContextService } from '../../app/core/services/search-chat-context.service';
|
||||
import { I18nService } from '../../app/core/i18n';
|
||||
import { GlobalSearchComponent } from '../../app/layout/global-search/global-search.component';
|
||||
|
||||
describe('GlobalSearchComponent', () => {
|
||||
@@ -63,6 +65,17 @@ describe('GlobalSearchComponent', () => {
|
||||
provide: AmbientContextService,
|
||||
useValue: {
|
||||
buildContextFilter: () => undefined,
|
||||
getSearchSuggestions: () => [
|
||||
{ key: 'ui.search.suggestion.default.deploy', fallback: 'How do I deploy?' },
|
||||
{ key: 'ui.search.suggestion.default.vex', fallback: 'What is a VEX statement?' },
|
||||
{ key: 'ui.search.suggestion.default.critical', fallback: 'Show critical findings' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
tryT: () => null,
|
||||
},
|
||||
},
|
||||
{ provide: SearchChatContextService, useValue: searchChatContext },
|
||||
@@ -84,6 +97,14 @@ describe('GlobalSearchComponent', () => {
|
||||
expect(input?.placeholder).toContain('Try:');
|
||||
});
|
||||
|
||||
it('renders eight domain cards in empty state guide', () => {
|
||||
component.onFocus();
|
||||
fixture.detectChanges();
|
||||
|
||||
const cards = fixture.nativeElement.querySelectorAll('.search__domain-card');
|
||||
expect(cards.length).toBe(8);
|
||||
});
|
||||
|
||||
it('queries unified search for one-character query terms', async () => {
|
||||
component.onFocus();
|
||||
component.onQueryChange('a');
|
||||
@@ -92,6 +113,43 @@ describe('GlobalSearchComponent', () => {
|
||||
expect(searchClient.search).toHaveBeenCalledWith('a', undefined);
|
||||
});
|
||||
|
||||
it('records synthesis analytics when synthesis is present in search response', async () => {
|
||||
searchClient.recordAnalytics.calls.reset();
|
||||
searchClient.search.and.returnValue(of({
|
||||
query: 'critical findings',
|
||||
topK: 10,
|
||||
cards: [createCard('findings', '/triage/findings/fnd-101')],
|
||||
synthesis: {
|
||||
summary: 'Synthesis summary',
|
||||
template: 'finding_overview',
|
||||
confidence: 'high',
|
||||
sourceCount: 1,
|
||||
domainsCovered: ['findings'],
|
||||
},
|
||||
diagnostics: {
|
||||
ftsMatches: 1,
|
||||
vectorMatches: 0,
|
||||
entityCardCount: 1,
|
||||
durationMs: 11,
|
||||
usedVector: false,
|
||||
mode: 'fts-only',
|
||||
},
|
||||
}));
|
||||
|
||||
component.onFocus();
|
||||
component.onQueryChange('critical findings');
|
||||
await waitForDebounce();
|
||||
|
||||
const events = searchClient.recordAnalytics.calls.mostRecent().args[0] as Array<Record<string, unknown>>;
|
||||
const synthesisEvent = events.find((event) => event['eventType'] === 'synthesis');
|
||||
expect(synthesisEvent).toEqual(jasmine.objectContaining({
|
||||
eventType: 'synthesis',
|
||||
entityKey: '__synthesis__',
|
||||
domain: 'synthesis',
|
||||
resultCount: 1,
|
||||
}));
|
||||
});
|
||||
|
||||
it('consumes chat-to-search context when navigation changes', () => {
|
||||
searchChatContext.consumeChatToSearch.and.returnValue({
|
||||
query: 'CVE-2024-21626',
|
||||
@@ -103,4 +161,135 @@ describe('GlobalSearchComponent', () => {
|
||||
|
||||
expect(component.query()).toBe('CVE-2024-21626');
|
||||
});
|
||||
|
||||
it('navigates to assistant host with openChat intent from Ask AI card action', () => {
|
||||
const card = createCard('findings', '/triage/findings/fnd-1');
|
||||
|
||||
component.onAskAiFromCard(card);
|
||||
|
||||
expect(searchChatContext.setSearchToChat).toHaveBeenCalled();
|
||||
expect(router.navigate).toHaveBeenCalledWith(
|
||||
['/security/triage'],
|
||||
jasmine.objectContaining({
|
||||
queryParams: jasmine.objectContaining({
|
||||
openChat: 'true',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('normalizes and navigates entity-card actions for all primary domains', () => {
|
||||
const cases: Array<{ domain: EntityCard['domain']; route: string; expected: string }> = [
|
||||
{
|
||||
domain: 'knowledge',
|
||||
route: '/docs/modules/platform/architecture-overview.md#release-flow',
|
||||
expected: '/docs/modules/platform/architecture-overview.md#release-flow',
|
||||
},
|
||||
{
|
||||
domain: 'findings',
|
||||
route: '/triage/findings/fnd-123',
|
||||
expected: '/security/findings/fnd-123',
|
||||
},
|
||||
{
|
||||
domain: 'policy',
|
||||
route: '/policy/DENY-CRITICAL-PROD',
|
||||
expected: '/ops/policy?q=DENY-CRITICAL-PROD',
|
||||
},
|
||||
{
|
||||
domain: 'vex',
|
||||
route: '/vex-hub/CVE-2024-21626',
|
||||
expected: '/security/advisories-vex?q=CVE-2024-21626',
|
||||
},
|
||||
{
|
||||
domain: 'platform',
|
||||
route: '/scans/scan-42',
|
||||
expected: '/security/triage?q=scan-42',
|
||||
},
|
||||
];
|
||||
|
||||
component.onFocus();
|
||||
component.onQueryChange('test');
|
||||
|
||||
for (const testCase of cases) {
|
||||
const card = createCard(testCase.domain, testCase.route);
|
||||
component.onCardAction(card, card.actions[0]);
|
||||
expect(router.navigateByUrl).toHaveBeenCalledWith(testCase.expected);
|
||||
}
|
||||
});
|
||||
|
||||
it('uses deterministic keyboard escape behavior for focus and close flow', () => {
|
||||
component.onFocus();
|
||||
component.onQueryChange('CVE-2024-21626');
|
||||
|
||||
component.onKeydown(new KeyboardEvent('keydown', { key: 'Escape' }));
|
||||
expect(component.query()).toBe('');
|
||||
expect(component.isFocused()).toBeTrue();
|
||||
|
||||
component.onKeydown(new KeyboardEvent('keydown', { key: 'Escape' }));
|
||||
expect(component.isFocused()).toBeFalse();
|
||||
});
|
||||
|
||||
it('emits click analytics when Ctrl+Enter runs selected primary action', () => {
|
||||
const card = createCard('findings', '/triage/findings/fnd-ctrl-enter');
|
||||
|
||||
component.onFocus();
|
||||
component.query.set('critical findings');
|
||||
component.searchResponse.set({
|
||||
query: 'critical findings',
|
||||
topK: 10,
|
||||
cards: [card],
|
||||
synthesis: null,
|
||||
diagnostics: {
|
||||
ftsMatches: 1,
|
||||
vectorMatches: 0,
|
||||
entityCardCount: 1,
|
||||
durationMs: 3,
|
||||
usedVector: false,
|
||||
mode: 'fts-only',
|
||||
},
|
||||
});
|
||||
searchClient.recordAnalytics.calls.reset();
|
||||
router.navigateByUrl.calls.reset();
|
||||
|
||||
component.onKeydown(new KeyboardEvent('keydown', { key: 'Enter', ctrlKey: true }));
|
||||
|
||||
expect(searchClient.recordAnalytics).toHaveBeenCalledWith([
|
||||
jasmine.objectContaining({
|
||||
eventType: 'click',
|
||||
entityKey: 'findings:sample',
|
||||
domain: 'findings',
|
||||
}),
|
||||
]);
|
||||
expect(router.navigateByUrl).toHaveBeenCalledWith('/security/findings/fnd-ctrl-enter');
|
||||
});
|
||||
|
||||
it('keeps focus when blur timer races with immediate refocus', async () => {
|
||||
component.onFocus();
|
||||
component.onBlur();
|
||||
component.onFocus();
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
expect(component.isFocused()).toBeTrue();
|
||||
});
|
||||
|
||||
function createCard(domain: EntityCard['domain'], route: string): EntityCard {
|
||||
return {
|
||||
entityKey: `${domain}:sample`,
|
||||
entityType: domain === 'policy' ? 'policy_rule' : domain === 'findings' ? 'finding' : domain === 'vex' ? 'vex_statement' : 'docs',
|
||||
domain,
|
||||
title: `${domain} sample`,
|
||||
snippet: `${domain} snippet`,
|
||||
score: 1,
|
||||
actions: [
|
||||
{
|
||||
label: 'Open',
|
||||
actionType: 'navigate',
|
||||
route,
|
||||
isPrimary: true,
|
||||
},
|
||||
],
|
||||
metadata: {},
|
||||
sources: [],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
import { normalizeSearchActionRoute } from '../../app/layout/global-search/search-route-matrix';
|
||||
import {
|
||||
normalizeSearchActionRoute,
|
||||
SEARCH_ACTION_ROUTE_MATRIX,
|
||||
} from '../../app/layout/global-search/search-route-matrix';
|
||||
|
||||
describe('normalizeSearchActionRoute', () => {
|
||||
it('covers at least one route per unified-search domain', () => {
|
||||
const domains = new Set(SEARCH_ACTION_ROUTE_MATRIX.map((entry) => entry.domain));
|
||||
expect(domains.has('knowledge')).toBeTrue();
|
||||
expect(domains.has('findings')).toBeTrue();
|
||||
expect(domains.has('policy')).toBeTrue();
|
||||
expect(domains.has('vex')).toBeTrue();
|
||||
expect(domains.has('platform')).toBeTrue();
|
||||
});
|
||||
|
||||
it('normalizes all matrix routes to expected frontend routes', () => {
|
||||
for (const entry of SEARCH_ACTION_ROUTE_MATRIX) {
|
||||
expect(normalizeSearchActionRoute(entry.sourceRoute)).toBe(entry.expectedRoute);
|
||||
}
|
||||
});
|
||||
|
||||
it('maps findings routes into security finding detail', () => {
|
||||
expect(normalizeSearchActionRoute('/triage/findings/abc-123')).toBe('/security/findings/abc-123');
|
||||
});
|
||||
@@ -18,10 +36,14 @@ describe('normalizeSearchActionRoute', () => {
|
||||
});
|
||||
|
||||
it('maps scan routes into security scans route', () => {
|
||||
expect(normalizeSearchActionRoute('/scans/scan-42')).toBe('/security/scans/scan-42');
|
||||
expect(normalizeSearchActionRoute('/scans/scan-42')).toBe('/security/triage?q=scan-42');
|
||||
});
|
||||
|
||||
it('preserves already valid app routes', () => {
|
||||
expect(normalizeSearchActionRoute('/docs/ops/runbook#overview')).toBe('/docs/ops/runbook#overview');
|
||||
});
|
||||
|
||||
it('falls back to /ops for unknown legacy routes', () => {
|
||||
expect(normalizeSearchActionRoute('/legacy/does-not-exist')).toBe('/ops');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import { SearchClient } from '../../app/core/api/search.client';
|
||||
import { UnifiedSearchClient } from '../../app/core/api/unified-search.client';
|
||||
|
||||
describe('UnifiedSearchClient', () => {
|
||||
let httpMock: HttpTestingController;
|
||||
let client: UnifiedSearchClient;
|
||||
|
||||
beforeEach(() => {
|
||||
const legacySearch = jasmine.createSpyObj('SearchClient', ['search']);
|
||||
legacySearch.search.and.returnValue(of({
|
||||
query: 'legacy',
|
||||
groups: [],
|
||||
totalCount: 0,
|
||||
durationMs: 0,
|
||||
}));
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
UnifiedSearchClient,
|
||||
{ provide: SearchClient, useValue: legacySearch },
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
});
|
||||
|
||||
httpMock = TestBed.inject(HttpTestingController);
|
||||
client = TestBed.inject(UnifiedSearchClient);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
httpMock.verify();
|
||||
});
|
||||
|
||||
it('sanitizes script/html tags from unified snippets', () => {
|
||||
let payload: any;
|
||||
|
||||
client.search('critical update').subscribe((response) => {
|
||||
payload = response;
|
||||
});
|
||||
|
||||
const request = httpMock.expectOne('/api/v1/search/query');
|
||||
expect(request.request.method).toBe('POST');
|
||||
|
||||
request.flush({
|
||||
query: 'critical update',
|
||||
topK: 10,
|
||||
cards: [
|
||||
{
|
||||
entityKey: 'docs:hardening',
|
||||
entityType: 'docs',
|
||||
domain: 'knowledge',
|
||||
title: 'Hardening Guide',
|
||||
snippet: "<mark>critical</mark><script>alert('x')</script> update available",
|
||||
score: 0.99,
|
||||
actions: [
|
||||
{
|
||||
label: 'Open',
|
||||
actionType: 'navigate',
|
||||
route: '/docs/modules/advisory-ai/knowledge-search.md',
|
||||
isPrimary: true,
|
||||
},
|
||||
],
|
||||
metadata: {},
|
||||
sources: ['knowledge'],
|
||||
},
|
||||
],
|
||||
synthesis: null,
|
||||
diagnostics: {
|
||||
ftsMatches: 1,
|
||||
vectorMatches: 0,
|
||||
entityCardCount: 1,
|
||||
durationMs: 7,
|
||||
usedVector: false,
|
||||
mode: 'fts-only',
|
||||
},
|
||||
});
|
||||
|
||||
expect(payload).toBeTruthy();
|
||||
expect(payload.cards.length).toBe(1);
|
||||
expect(payload.cards[0].snippet).toBe('critical update available');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,116 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ActivatedRoute, Router, convertToParamMap, type ParamMap } from '@angular/router';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
import { PlatformContextStore } from '../../app/core/context/platform-context.store';
|
||||
import { SearchChatContextService } from '../../app/core/services/search-chat-context.service';
|
||||
import type { EntityCard } from '../../app/core/api/unified-search.models';
|
||||
import { SecurityTriageChatHostComponent } from '../../app/features/security/security-triage-chat-host.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-security-findings-page',
|
||||
standalone: true,
|
||||
template: '',
|
||||
})
|
||||
class SecurityFindingsPageStubComponent {}
|
||||
|
||||
@Component({
|
||||
selector: 'stellaops-chat',
|
||||
standalone: true,
|
||||
template: '',
|
||||
})
|
||||
class ChatStubComponent {
|
||||
@Input() tenantId = 'default';
|
||||
@Input() initialUserMessage: string | null = null;
|
||||
@Output() close = new EventEmitter<void>();
|
||||
@Output() searchForMore = new EventEmitter<string>();
|
||||
}
|
||||
|
||||
describe('SecurityTriageChatHostComponent', () => {
|
||||
let fixture: ComponentFixture<SecurityTriageChatHostComponent>;
|
||||
let component: SecurityTriageChatHostComponent;
|
||||
let queryParamMap$: BehaviorSubject<ParamMap>;
|
||||
let router: { navigate: jasmine.Spy };
|
||||
let searchChatContext: SearchChatContextService;
|
||||
|
||||
beforeEach(async () => {
|
||||
queryParamMap$ = new BehaviorSubject<ParamMap>(convertToParamMap({}));
|
||||
router = {
|
||||
navigate: jasmine.createSpy('navigate').and.returnValue(Promise.resolve(true)),
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [SecurityTriageChatHostComponent],
|
||||
providers: [
|
||||
SearchChatContextService,
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
queryParamMap: queryParamMap$.asObservable(),
|
||||
},
|
||||
},
|
||||
{ provide: Router, useValue: router },
|
||||
{
|
||||
provide: PlatformContextStore,
|
||||
useValue: {
|
||||
tenantId: () => 'test-tenant',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
.overrideComponent(SecurityTriageChatHostComponent, {
|
||||
set: {
|
||||
imports: [SecurityFindingsPageStubComponent, ChatStubComponent],
|
||||
},
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(SecurityTriageChatHostComponent);
|
||||
component = fixture.componentInstance;
|
||||
searchChatContext = TestBed.inject(SearchChatContextService);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('opens assistant from openChat intent with CVE-grounded search context prompt', () => {
|
||||
const card: EntityCard = {
|
||||
entityKey: 'cve:CVE-2024-21626',
|
||||
entityType: 'finding',
|
||||
domain: 'findings',
|
||||
title: 'CVE-2024-21626',
|
||||
snippet: 'Container breakout',
|
||||
score: 0.99,
|
||||
severity: 'critical',
|
||||
actions: [],
|
||||
metadata: {},
|
||||
sources: ['findings'],
|
||||
};
|
||||
|
||||
searchChatContext.setSearchToChat({
|
||||
query: 'CVE-2024-21626',
|
||||
entityCards: [card],
|
||||
synthesis: null,
|
||||
});
|
||||
|
||||
queryParamMap$.next(convertToParamMap({
|
||||
openChat: 'true',
|
||||
q: 'CVE-2024-21626',
|
||||
}));
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.assistantOpen()).toBeTrue();
|
||||
expect(component.assistantInitialMessage()).toContain('CVE-2024-21626');
|
||||
expect(router.navigate).toHaveBeenCalledWith([], jasmine.objectContaining({
|
||||
queryParams: jasmine.objectContaining({ openChat: null }),
|
||||
}));
|
||||
});
|
||||
|
||||
it('closes assistant on chat search return signal when query is provided', () => {
|
||||
component.openAssistantPanel();
|
||||
expect(component.assistantOpen()).toBeTrue();
|
||||
|
||||
component.onChatSearchForMore('CVE-2024-21626');
|
||||
|
||||
expect(component.assistantOpen()).toBeFalse();
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,10 @@
|
||||
{
|
||||
"status": "passed",
|
||||
"failedTests": []
|
||||
"status": "failed",
|
||||
"failedTests": [
|
||||
"1afb01fbb618c47567d7-fd98c577baf3a66db4df",
|
||||
"1afb01fbb618c47567d7-b551a8493c84ef386f8a",
|
||||
"5116366078763408f6a4-3b86aa37005d1bc4cde8",
|
||||
"c1989d0c79e16f9a1fb6-8fc204434b4ce3c66739",
|
||||
"c1989d0c79e16f9a1fb6-f4d050a00e099a21c774"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
import { expect, test, type Page } from '@playwright/test';
|
||||
import {
|
||||
buildResponse,
|
||||
emptyResponse,
|
||||
mockSearchApiDynamic,
|
||||
setupAuthenticatedSession,
|
||||
setupBasicMocks,
|
||||
waitForEntityCards,
|
||||
waitForResults,
|
||||
} from './unified-search-fixtures';
|
||||
|
||||
const newcomerCard = {
|
||||
entityKey: 'cve:CVE-2024-21626',
|
||||
entityType: 'finding',
|
||||
domain: 'findings',
|
||||
title: 'CVE-2024-21626 in api-gateway',
|
||||
snippet: 'Reachable critical vulnerability detected in production workload.',
|
||||
score: 0.96,
|
||||
severity: 'critical',
|
||||
actions: [
|
||||
{
|
||||
label: 'Open finding',
|
||||
actionType: 'navigate',
|
||||
route: '/triage/findings/fnd-9001',
|
||||
isPrimary: true,
|
||||
},
|
||||
],
|
||||
sources: ['findings'],
|
||||
metadata: {},
|
||||
};
|
||||
|
||||
const healthySearchResponse = buildResponse(
|
||||
'critical findings',
|
||||
[newcomerCard],
|
||||
{
|
||||
summary: 'One critical finding matched. Ask AdvisoryAI for triage guidance.',
|
||||
template: 'finding_overview',
|
||||
confidence: 'high',
|
||||
sourceCount: 1,
|
||||
domainsCovered: ['findings'],
|
||||
},
|
||||
);
|
||||
|
||||
const degradedSearchResponse = {
|
||||
...buildResponse('critical findings', [newcomerCard]),
|
||||
diagnostics: {
|
||||
ftsMatches: 1,
|
||||
vectorMatches: 0,
|
||||
entityCardCount: 1,
|
||||
durationMs: 21,
|
||||
usedVector: false,
|
||||
mode: 'legacy-fallback',
|
||||
},
|
||||
};
|
||||
|
||||
const recoveredSearchResponse = {
|
||||
...buildResponse('CVE-2024-21626', [newcomerCard]),
|
||||
diagnostics: {
|
||||
ftsMatches: 4,
|
||||
vectorMatches: 2,
|
||||
entityCardCount: 1,
|
||||
durationMs: 37,
|
||||
usedVector: true,
|
||||
mode: 'hybrid',
|
||||
},
|
||||
};
|
||||
|
||||
test.describe('Assistant entry and search reliability', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await setupBasicMocks(page);
|
||||
await setupAuthenticatedSession(page);
|
||||
await mockChatEndpoints(page);
|
||||
});
|
||||
|
||||
test('healthy newcomer flow: search -> Ask AI -> search more -> action route', async ({ page }) => {
|
||||
await mockSearchApiDynamic(page, {
|
||||
'critical findings': healthySearchResponse,
|
||||
'cve-2024-21626': recoveredSearchResponse,
|
||||
}, recoveredSearchResponse);
|
||||
|
||||
await page.goto('/security/triage');
|
||||
await expect(page.locator('aside.sidebar')).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
const searchInput = page.locator('app-global-search input[type="text"]');
|
||||
await searchInput.focus();
|
||||
await expect(page.locator('.search__results')).toBeVisible();
|
||||
|
||||
const criticalSuggestion = page.locator('.search__suggestions .search__chip', {
|
||||
hasText: /critical findings/i,
|
||||
}).first();
|
||||
await expect(criticalSuggestion).toBeVisible();
|
||||
const suggestedQuery = (await criticalSuggestion.textContent())?.trim() || 'critical findings';
|
||||
await searchInput.fill(suggestedQuery);
|
||||
await expect(searchInput).toHaveValue(/critical findings/i);
|
||||
await waitForResults(page);
|
||||
await waitForEntityCards(page, 1);
|
||||
|
||||
await page.locator('.entity-card__action--ask-ai').first().click();
|
||||
const assistantDrawer = page.locator('.assistant-drawer');
|
||||
await expect(assistantDrawer).toBeVisible();
|
||||
await expect(assistantDrawer).toBeFocused();
|
||||
|
||||
const searchMoreButton = page.locator('.search-more-link');
|
||||
await expect(searchMoreButton).toBeVisible({ timeout: 10_000 });
|
||||
await searchMoreButton.click();
|
||||
|
||||
await expect(assistantDrawer).toBeHidden();
|
||||
await page.waitForResponse((response) => {
|
||||
if (!response.url().includes('/api/v1/search/query')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const body = response.request().postData() ?? '';
|
||||
return body.toLowerCase().includes('cve-2024-21626');
|
||||
});
|
||||
await expect(searchInput).toHaveValue(/CVE-2024-21626/i);
|
||||
await searchInput.focus();
|
||||
await expect(searchInput).toBeFocused();
|
||||
// Re-trigger the current query without Enter to avoid auto-select side effects.
|
||||
await searchInput.fill('CVE-2024-21626 ');
|
||||
await searchInput.press('Backspace');
|
||||
await waitForResults(page);
|
||||
await expect(page.locator('app-entity-card').first()).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
await page.locator('.entity-card__action--primary').first().click();
|
||||
await expect(page).toHaveURL(/\/security\/findings\/fnd-9001/i);
|
||||
});
|
||||
|
||||
test('degraded mode is visible and clears after recovery; focus order remains deterministic', async ({ page }) => {
|
||||
await mockSearchApiDynamic(page, {
|
||||
'critical findings': degradedSearchResponse,
|
||||
'cve-2024-21626': recoveredSearchResponse,
|
||||
}, recoveredSearchResponse);
|
||||
|
||||
await page.goto('/security/triage');
|
||||
await expect(page.locator('aside.sidebar')).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
const searchInput = page.locator('app-global-search input[type="text"]');
|
||||
await searchInput.focus();
|
||||
const criticalSuggestion = page.locator('.search__suggestions .search__chip', {
|
||||
hasText: /critical findings/i,
|
||||
}).first();
|
||||
await expect(criticalSuggestion).toBeVisible();
|
||||
const suggestedQuery = (await criticalSuggestion.textContent())?.trim() || 'critical findings';
|
||||
await searchInput.fill(suggestedQuery);
|
||||
await expect(searchInput).toHaveValue(/critical findings/i);
|
||||
await waitForResults(page);
|
||||
await waitForEntityCards(page, 1);
|
||||
|
||||
const degradedBanner = page.locator('.search__degraded-banner');
|
||||
await expect(degradedBanner).toBeVisible({ timeout: 10_000 });
|
||||
await expect(degradedBanner).toContainText(/fallback mode/i);
|
||||
|
||||
await searchInput.fill('CVE-2024-21626');
|
||||
await waitForResults(page);
|
||||
await waitForEntityCards(page, 1);
|
||||
await expect(degradedBanner).toBeHidden();
|
||||
|
||||
await page.locator('.entity-card__action--ask-ai').first().click();
|
||||
const assistantDrawer = page.locator('.assistant-drawer');
|
||||
await expect(assistantDrawer).toBeVisible();
|
||||
await expect(assistantDrawer).toBeFocused();
|
||||
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(assistantDrawer).toBeHidden();
|
||||
await expect(page.locator('.assistant-fab')).toBeFocused();
|
||||
});
|
||||
});
|
||||
|
||||
async function mockChatEndpoints(page: Page): Promise<void> {
|
||||
await page.route('**/api/v1/advisory-ai/conversations', async (route) => {
|
||||
if (route.request().method() !== 'POST') {
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify([]),
|
||||
});
|
||||
}
|
||||
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
conversationId: 'conv-newcomer-1',
|
||||
tenantId: 'test-tenant',
|
||||
userId: 'tester',
|
||||
context: {},
|
||||
turns: [],
|
||||
createdAt: '2026-02-25T00:00:00.000Z',
|
||||
updatedAt: '2026-02-25T00:00:00.000Z',
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.route('**/api/v1/advisory-ai/conversations/*/turns', async (route) => {
|
||||
if (route.request().method() !== 'POST') {
|
||||
return route.continue();
|
||||
}
|
||||
|
||||
const ssePayload = [
|
||||
'event: progress',
|
||||
'data: {"stage":"searching"}',
|
||||
'',
|
||||
'event: token',
|
||||
'data: {"content":"CVE-2024-21626 remains relevant for this finding. "}',
|
||||
'',
|
||||
'event: citation',
|
||||
'data: {"type":"finding","path":"CVE-2024-21626","verified":true}',
|
||||
'',
|
||||
'event: done',
|
||||
'data: {"turnId":"turn-newcomer-1","groundingScore":0.92}',
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'text/event-stream; charset=utf-8',
|
||||
'cache-control': 'no-cache',
|
||||
},
|
||||
body: ssePayload,
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user