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:
master
2026-03-17 15:10:36 +02:00
parent 4b7d3587ca
commit b851aa8300
50 changed files with 2163 additions and 551 deletions

View File

@@ -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]
});
}

View File

@@ -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";

View File

@@ -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;