Fix journey cluster defects + UX improvements across 7 clusters
P0 fixes (clean-start + route contracts): - VexHub: fix migration 002 table name + add repair migration 003 - Gateway: add /console/admin and /api/v1/unknowns routes - IDP: add platform.idp.admin scope to OAuth client + web config - Risk: fix URL construction from authority to gateway base - Unknowns: fix client path from /api/v1/scanner/unknowns to /api/v1/unknowns P1 fixes (trust + shell integrity): - Audit: fix module name normalization, add Authority audit source - Stage: add persistence across web store, API contracts, DB migration 059 - Posture: add per-source error tracking + degradation banner P2 fixes (adoption + workflow clarity): - Rename Triage to Findings in navigation + breadcrumbs - Command palette: show quick actions for plain text queries, fix scan routes - Scan: add local-mode limitation messaging + queue hints - Release: add post-seal promotion CTA with pre-filled release ID - Welcome: rewrite around operator adoption model (Get Started + What Stella Replaces) UX improvements: - Status rail: convert to icon-only with color state + tooltips - Event Stream Monitor: new page at /ops/operations/event-stream - Sidebar: collapse Operations by default - User menu: embed theme switcher (Day/Night/System), remove standalone toggle - Settings: add Profile section with email editing + PUT /api/v1/platform/preferences/email endpoint - Docs viewer: replace custom parser with ngx-markdown (marked) for proper table/code/blockquote rendering Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -36,14 +36,16 @@ public sealed class HttpUnifiedAuditEventProvider : IUnifiedAuditEventProvider
|
||||
using var timeoutSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
timeoutSource.CancelAfter(timeout);
|
||||
|
||||
var getAuthority = GetAuthorityEventsAsync(endpointOptions, timeoutSource.Token);
|
||||
var getJobEngine = GetJobEngineEventsAsync(endpointOptions, timeoutSource.Token);
|
||||
var getPolicy = GetPolicyEventsAsync(endpointOptions, timeoutSource.Token);
|
||||
var getEvidence = GetEvidenceLockerEventsAsync(endpointOptions, timeoutSource.Token);
|
||||
var getNotify = GetNotifyEventsAsync(endpointOptions, timeoutSource.Token);
|
||||
|
||||
await Task.WhenAll(getJobEngine, getPolicy, getEvidence, getNotify).ConfigureAwait(false);
|
||||
await Task.WhenAll(getAuthority, getJobEngine, getPolicy, getEvidence, getNotify).ConfigureAwait(false);
|
||||
|
||||
return getJobEngine.Result
|
||||
return getAuthority.Result
|
||||
.Concat(getJobEngine.Result)
|
||||
.Concat(getPolicy.Result)
|
||||
.Concat(getEvidence.Result)
|
||||
.Concat(getNotify.Result)
|
||||
@@ -54,6 +56,87 @@ public sealed class HttpUnifiedAuditEventProvider : IUnifiedAuditEventProvider
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyList<UnifiedAuditEvent>> GetAuthorityEventsAsync(
|
||||
UnifiedAuditModuleEndpointsOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var uri = BuildUri(
|
||||
options.AuthorityBaseUrl,
|
||||
"/console/admin/audit",
|
||||
new Dictionary<string, string?> { ["limit"] = options.FetchLimitPerModule.ToString(CultureInfo.InvariantCulture) });
|
||||
|
||||
if (uri is null)
|
||||
{
|
||||
return Array.Empty<UnifiedAuditEvent>();
|
||||
}
|
||||
|
||||
using var document = await GetJsonDocumentAsync(uri, cancellationToken).ConfigureAwait(false);
|
||||
if (document is null)
|
||||
{
|
||||
return Array.Empty<UnifiedAuditEvent>();
|
||||
}
|
||||
|
||||
if (!TryGetPropertyIgnoreCase(document.RootElement, "events", out var entries) ||
|
||||
entries.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return Array.Empty<UnifiedAuditEvent>();
|
||||
}
|
||||
|
||||
var events = new List<UnifiedAuditEvent>();
|
||||
foreach (var entry in entries.EnumerateArray())
|
||||
{
|
||||
var id = GetString(entry, "id") ?? GetString(entry, "eventId");
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var eventType = GetString(entry, "eventType") ?? GetString(entry, "type");
|
||||
var description = GetString(entry, "description") ?? GetString(entry, "summary") ?? eventType ?? "Authority audit event";
|
||||
var action = UnifiedAuditValueMapper.NormalizeAction(eventType, description);
|
||||
|
||||
var actorId = GetString(entry, "actorId") ?? GetString(entry, "actor") ?? "authority-system";
|
||||
var actorType = UnifiedAuditValueMapper.NormalizeActorType(GetString(entry, "actorType"));
|
||||
var severity = UnifiedAuditValueMapper.NormalizeSeverity(
|
||||
GetString(entry, "severity"),
|
||||
action,
|
||||
description);
|
||||
|
||||
events.Add(new UnifiedAuditEvent
|
||||
{
|
||||
Id = id,
|
||||
Timestamp = UnifiedAuditValueMapper.ParseTimestampOrDefault(
|
||||
GetString(entry, "occurredAt") ?? GetString(entry, "timestamp"), TimestampFallback),
|
||||
Module = "authority",
|
||||
Action = action,
|
||||
Severity = severity,
|
||||
Actor = new UnifiedAuditActor
|
||||
{
|
||||
Id = actorId,
|
||||
Name = GetString(entry, "actorName") ?? actorId,
|
||||
Type = actorType,
|
||||
IpAddress = GetString(entry, "actorIp") ?? GetString(entry, "ipAddress"),
|
||||
UserAgent = GetString(entry, "userAgent")
|
||||
},
|
||||
Resource = new UnifiedAuditResource
|
||||
{
|
||||
Type = GetString(entry, "resourceType") ?? "authority_resource",
|
||||
Id = GetString(entry, "resourceId") ?? id
|
||||
},
|
||||
Description = description,
|
||||
Details = new Dictionary<string, object?>(StringComparerOrdinal)
|
||||
{
|
||||
["eventType"] = eventType
|
||||
},
|
||||
CorrelationId = GetString(entry, "correlationId"),
|
||||
TenantId = GetString(entry, "tenantId"),
|
||||
Tags = ["authority", action]
|
||||
});
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyList<UnifiedAuditEvent>> GetJobEngineEventsAsync(
|
||||
UnifiedAuditModuleEndpointsOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -277,7 +360,7 @@ public sealed class HttpUnifiedAuditEventProvider : IUnifiedAuditEventProvider
|
||||
{
|
||||
Id = id,
|
||||
Timestamp = UnifiedAuditValueMapper.ParseTimestampOrDefault(GetString(entry, "occurredAt"), TimestampFallback),
|
||||
Module = "sbom",
|
||||
Module = "evidencelocker",
|
||||
Action = action,
|
||||
Severity = UnifiedAuditValueMapper.NormalizeSeverity(GetString(entry, "severity"), action, eventType),
|
||||
Actor = new UnifiedAuditActor
|
||||
@@ -297,7 +380,7 @@ public sealed class HttpUnifiedAuditEventProvider : IUnifiedAuditEventProvider
|
||||
["eventType"] = eventType,
|
||||
["subject"] = subject
|
||||
},
|
||||
Tags = ["sbom", action]
|
||||
Tags = ["evidencelocker", action]
|
||||
});
|
||||
}
|
||||
|
||||
@@ -345,7 +428,7 @@ public sealed class HttpUnifiedAuditEventProvider : IUnifiedAuditEventProvider
|
||||
{
|
||||
Id = id,
|
||||
Timestamp = UnifiedAuditValueMapper.ParseTimestampOrDefault(GetString(entry, "createdAt"), TimestampFallback),
|
||||
Module = "integrations",
|
||||
Module = "notify",
|
||||
Action = action,
|
||||
Severity = UnifiedAuditValueMapper.NormalizeSeverity(GetString(entry, "severity"), action, description),
|
||||
Actor = new UnifiedAuditActor
|
||||
@@ -363,7 +446,7 @@ public sealed class HttpUnifiedAuditEventProvider : IUnifiedAuditEventProvider
|
||||
Details = details,
|
||||
CorrelationId = GetString(entry, "correlationId"),
|
||||
TenantId = GetString(entry, "tenantId"),
|
||||
Tags = ["integrations", action]
|
||||
Tags = ["notify", action]
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ public static class UnifiedAuditCatalog
|
||||
"scanner",
|
||||
"attestor",
|
||||
"sbom",
|
||||
"evidencelocker",
|
||||
"notify",
|
||||
"scheduler"
|
||||
];
|
||||
|
||||
@@ -159,13 +161,7 @@ public static class UnifiedAuditValueMapper
|
||||
|
||||
public static string NormalizeModule(string rawModule)
|
||||
{
|
||||
var source = rawModule.Trim().ToLowerInvariant();
|
||||
return source switch
|
||||
{
|
||||
"evidencelocker" => "sbom",
|
||||
"notify" => "integrations",
|
||||
_ => UnifiedAuditCatalog.Modules.Contains(source, StringComparer.Ordinal) ? source : "integrations"
|
||||
};
|
||||
return rawModule.Trim().ToLowerInvariant();
|
||||
}
|
||||
|
||||
public static DateTimeOffset ParseTimestampOrDefault(string? rawTimestamp, DateTimeOffset defaultValue)
|
||||
@@ -355,6 +351,7 @@ public sealed record UnifiedAuditQuery
|
||||
|
||||
public sealed record UnifiedAuditModuleEndpointsOptions
|
||||
{
|
||||
public string AuthorityBaseUrl { get; set; } = "http://authority.stella-ops.local";
|
||||
public string JobEngineBaseUrl { get; set; } = "http://jobengine.stella-ops.local";
|
||||
public string PolicyBaseUrl { get; set; } = "http://policy-gateway.stella-ops.local";
|
||||
public string EvidenceLockerBaseUrl { get; set; } = "http://evidencelocker.stella-ops.local";
|
||||
|
||||
@@ -18,6 +18,10 @@ builder.Services.AddSingleton(TimeProvider.System);
|
||||
|
||||
builder.Services.Configure<UnifiedAuditModuleEndpointsOptions>(options =>
|
||||
{
|
||||
options.AuthorityBaseUrl = builder.Configuration["UnifiedAudit:Sources:Authority"]
|
||||
?? builder.Configuration["STELLAOPS_AUTHORITY_URL"]
|
||||
?? options.AuthorityBaseUrl;
|
||||
|
||||
options.JobEngineBaseUrl = builder.Configuration["UnifiedAudit:Sources:JobEngine"]
|
||||
?? builder.Configuration["STELLAOPS_JOBENGINE_URL"]
|
||||
?? options.JobEngineBaseUrl;
|
||||
|
||||
Reference in New Issue
Block a user