feat: Add PathViewer and RiskDriftCard components with templates and styles

- Implemented PathViewerComponent for visualizing reachability call paths.
- Added RiskDriftCardComponent to display reachability drift results.
- Created corresponding HTML templates and SCSS styles for both components.
- Introduced test fixtures for reachability analysis in JSON format.
- Enhanced user interaction with collapsible and expandable features in PathViewer.
- Included risk trend visualization and summary metrics in RiskDriftCard.
This commit is contained in:
master
2025-12-18 18:35:30 +02:00
parent 811f35cba7
commit 0dc71e760a
70 changed files with 8904 additions and 163 deletions

View File

@@ -67,6 +67,7 @@ public sealed class EpssIngestOptions
public sealed class EpssIngestJob : BackgroundService
{
private readonly IEpssRepository _repository;
private readonly IEpssRawRepository? _rawRepository;
private readonly EpssOnlineSource _onlineSource;
private readonly EpssBundleSource _bundleSource;
private readonly EpssCsvStreamParser _parser;
@@ -82,9 +83,11 @@ public sealed class EpssIngestJob : BackgroundService
EpssCsvStreamParser parser,
IOptions<EpssIngestOptions> options,
TimeProvider timeProvider,
ILogger<EpssIngestJob> logger)
ILogger<EpssIngestJob> logger,
IEpssRawRepository? rawRepository = null)
{
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
_rawRepository = rawRepository; // Optional - raw storage for replay capability
_onlineSource = onlineSource ?? throw new ArgumentNullException(nameof(onlineSource));
_bundleSource = bundleSource ?? throw new ArgumentNullException(nameof(bundleSource));
_parser = parser ?? throw new ArgumentNullException(nameof(parser));
@@ -186,6 +189,18 @@ public sealed class EpssIngestJob : BackgroundService
session,
cancellationToken).ConfigureAwait(false);
// Store raw payload for replay capability (Sprint: SPRINT_3413_0001_0001, Task: R2)
if (_rawRepository is not null)
{
await StoreRawPayloadAsync(
importRun.ImportRunId,
sourceFile.SourceUri,
modelDate,
session,
fileContent.Length,
cancellationToken).ConfigureAwait(false);
}
// Mark success
await _repository.MarkImportSucceededAsync(
importRun.ImportRunId,
@@ -279,4 +294,69 @@ public sealed class EpssIngestJob : BackgroundService
var hash = System.Security.Cryptography.SHA256.HashData(content);
return Convert.ToHexString(hash).ToLowerInvariant();
}
/// <summary>
/// Stores raw EPSS payload for deterministic replay capability.
/// Sprint: SPRINT_3413_0001_0001, Task: R2
/// </summary>
private async Task StoreRawPayloadAsync(
Guid importRunId,
string sourceUri,
DateOnly modelDate,
EpssParsedSession session,
long compressedSize,
CancellationToken cancellationToken)
{
if (_rawRepository is null)
{
return;
}
try
{
// Convert parsed rows to JSON array for raw storage
var payload = System.Text.Json.JsonSerializer.Serialize(
session.Rows.Select(r => new
{
cve = r.CveId,
epss = r.Score,
percentile = r.Percentile
}),
new System.Text.Json.JsonSerializerOptions { WriteIndented = false });
var payloadBytes = System.Text.Encoding.UTF8.GetBytes(payload);
var payloadSha256 = System.Security.Cryptography.SHA256.HashData(payloadBytes);
var raw = new EpssRaw
{
SourceUri = sourceUri,
AsOfDate = modelDate,
Payload = payload,
PayloadSha256 = payloadSha256,
HeaderComment = session.HeaderComment,
ModelVersion = session.ModelVersionTag,
PublishedDate = session.PublishedDate,
RowCount = session.RowCount,
CompressedSize = compressedSize,
DecompressedSize = payloadBytes.LongLength,
ImportRunId = importRunId
};
await _rawRepository.CreateAsync(raw, cancellationToken).ConfigureAwait(false);
_logger.LogDebug(
"Stored raw EPSS payload: modelDate={ModelDate}, rows={RowCount}, size={Size}",
modelDate,
session.RowCount,
payloadBytes.Length);
}
catch (Exception ex)
{
// Log but don't fail ingestion if raw storage fails
_logger.LogWarning(
ex,
"Failed to store raw EPSS payload for {ModelDate}; ingestion will continue",
modelDate);
}
}
}