Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ScheduledTasks/ReplayParkedQueue/ReplayParkedQueue.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Get-ChildItem -Path $LogDirectory -Filter "ReplayParkedSubscriptions-*.log" -Fil
Remove-Item $_.FullName -Force
}
catch {
# Avoid failing the run due to cleanup issues
Write-Trace "WARN" "Failed to remove stale log file $($_.FullName): $($_.Exception.Message)"
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ public sealed class FileStatusReportService(
IDbContextFactory<MerchantFileProcessorDbContext> dbContextFactory,
IMerchantProcessingConfigurationState configurationState) : IFileStatusReportService
{
private const string TableStart = " <table>";
private const string TableEnd = " </table>";
private const string TableBodyStart = " <tbody>";
private const string TableBodyEnd = " </tbody>";

public async Task<FileStatusReport> GetReportAsync(CancellationToken cancellationToken)
{
await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Expand All @@ -87,18 +92,18 @@ public async Task<string> RenderHtmlAsync(CancellationToken cancellationToken)
html.AppendLine(" <h1>Merchant File Status</h1>");
html.AppendLine($" <p>Generated at {Encode(report.GeneratedUtc.ToString("u"))}. Auto-refreshes every 30 seconds.</p>");
html.AppendLine(" <h2>Merchants</h2>");
html.AppendLine(" <table>");
html.AppendLine(TableStart);
html.AppendLine(" <thead><tr><th>Merchant</th><th>Enabled</th><th>Successful Files</th><th>Failed Files</th><th>Last Processed (UTC)</th><th>Next Scheduled Send (UTC)</th><th>Details</th></tr></thead>");
html.AppendLine(" <tbody>");
html.AppendLine(TableBodyStart);

foreach (var merchant in report.MerchantSummaries)
{
html.AppendLine(
$" <tr><td>{Encode(merchant.MerchantName)}<br /><span class=\"mono\">{Encode(merchant.MerchantId)}</span></td><td>{(merchant.Enabled ? "Yes" : "No")}</td><td>{merchant.SuccessfulFilesSent}</td><td>{merchant.FailedFiles}</td><td>{Encode(merchant.LastProcessedUtc?.ToString("u") ?? "Never")}</td><td>{Encode(merchant.NextScheduledUtc?.ToString("u") ?? "Disabled")}</td><td><a href=\"/status/{Uri.EscapeDataString(merchant.MerchantId)}\">View details</a></td></tr>");
}

html.AppendLine(" </tbody>");
html.AppendLine(" </table>");
html.AppendLine(TableBodyEnd);
html.AppendLine(TableEnd);
AppendDocumentEnd(html);

return html.ToString();
Expand All @@ -119,18 +124,18 @@ public async Task<string> RenderHtmlAsync(CancellationToken cancellationToken)
html.AppendLine($" <p class=\"mono\">{Encode(report.Merchant.MerchantId)}</p>");
html.AppendLine($" <p>Generated at {Encode(report.GeneratedUtc.ToString("u"))}. Auto-refreshes every 30 seconds.</p>");
html.AppendLine(" <h2>Summary</h2>");
html.AppendLine(" <table>");
html.AppendLine(TableStart);
html.AppendLine(" <thead><tr><th>Enabled</th><th>Successful Files</th><th>Failed Files</th><th>Last Processed (UTC)</th><th>Next Scheduled Send (UTC)</th></tr></thead>");
html.AppendLine(" <tbody>");
html.AppendLine(TableBodyStart);
html.AppendLine(
$" <tr><td>{(report.Merchant.Enabled ? "Yes" : "No")}</td><td>{report.Merchant.SuccessfulFilesSent}</td><td>{report.Merchant.FailedFiles}</td><td>{Encode(report.Merchant.LastProcessedUtc?.ToString("u") ?? "Never")}</td><td>{Encode(report.Merchant.NextScheduledUtc?.ToString("u") ?? "Disabled")}</td></tr>");
html.AppendLine(" </tbody>");
html.AppendLine(" </table>");
html.AppendLine(TableBodyEnd);
html.AppendLine(TableEnd);

html.AppendLine(" <h2>Recent File Activity</h2>");
html.AppendLine(" <table>");
html.AppendLine(TableStart);
html.AppendLine(" <thead><tr><th>Processed (UTC)</th><th>Contract</th><th>Status</th><th>Profile</th><th>Details</th></tr></thead>");
html.AppendLine(" <tbody>");
html.AppendLine(TableBodyStart);

foreach (var file in report.RecentFiles)
{
Expand All @@ -143,8 +148,8 @@ public async Task<string> RenderHtmlAsync(CancellationToken cancellationToken)
html.AppendLine(" <tr><td colspan=\"5\">No file activity recorded yet.</td></tr>");
}

html.AppendLine(" </tbody>");
html.AppendLine(" </table>");
html.AppendLine(TableBodyEnd);
html.AppendLine(TableEnd);
AppendDocumentEnd(html);

return html.ToString();
Expand All @@ -163,22 +168,22 @@ public async Task<string> RenderHtmlAsync(CancellationToken cancellationToken)
html.AppendLine($" <p><a href=\"/status/{Uri.EscapeDataString(report.Merchant.MerchantId)}\">&larr; Back to merchant</a></p>");
html.AppendLine(" <h1>File Details</h1>");
html.AppendLine($" <p>Merchant {Encode(report.Merchant.MerchantName)}<br /><span class=\"mono\">{Encode(report.Merchant.MerchantId)}</span></p>");
html.AppendLine(" <table>");
html.AppendLine(TableStart);
html.AppendLine(" <thead><tr><th>Processed (UTC)</th><th>Contract</th><th>Upload Status</th><th>Processing</th><th>Profile</th><th>Format</th></tr></thead>");
html.AppendLine(" <tbody>");
html.AppendLine(TableBodyStart);
html.AppendLine(
$" <tr><td>{Encode(report.File.ProcessedUtc.ToString("u"))}</td><td>{Encode(report.File.ContractName)}<br /><span class=\"mono\">{Encode(report.File.ContractId)}</span></td><td class=\"status-{Encode(report.File.Status)}\">{Encode(report.File.Status)}</td><td>{Encode(report.File.ProcessingCompleted ? "Complete" : "Pending")}{(report.File.LastStatusCheckUtc.HasValue ? $"<br /><span class=\"mono\">Last checked {Encode(report.File.LastStatusCheckUtc.Value.ToString("u"))}</span>" : string.Empty)}</td><td>{Encode(report.File.FileProfileId)}</td><td>{Encode(report.File.Format)}</td></tr>");
html.AppendLine(" </tbody>");
html.AppendLine(" </table>");
html.AppendLine(TableBodyEnd);
html.AppendLine(TableEnd);

html.AppendLine(" <h2>File Data</h2>");
html.AppendLine(" <table>");
html.AppendLine(TableStart);
html.AppendLine(" <thead><tr><th>File Name</th><th>Records</th><th>Total Amount</th></tr></thead>");
html.AppendLine(" <tbody>");
html.AppendLine(TableBodyStart);
html.AppendLine(
$" <tr><td>{Encode(string.IsNullOrWhiteSpace(report.File.FileName) ? "(not generated)" : report.File.FileName)}</td><td>{report.File.RecordCount}</td><td>{report.File.TotalAmount:0.00}</td></tr>");
html.AppendLine(" </tbody>");
html.AppendLine(" </table>");
html.AppendLine(TableBodyEnd);
html.AppendLine(TableEnd);

html.AppendLine(" <h2>File Lines</h2>");
if (report.FileLines.Count == 0)
Expand All @@ -189,15 +194,15 @@ public async Task<string> RenderHtmlAsync(CancellationToken cancellationToken)
{
html.AppendLine(" <table>");
html.AppendLine(" <thead><tr><th>Line</th><th>Content</th><th>Status</th><th>Transaction</th><th>Rejection</th></tr></thead>");
html.AppendLine(" <tbody>");
html.AppendLine(TableBodyStart);

foreach (var line in report.FileLines)
{
html.AppendLine($" <tr><td>{line.LineNumber}</td><td class=\"mono\">{Encode(line.Content)}</td><td>{Encode(line.ProcessingStatus)}</td><td class=\"mono\">{Encode(line.TransactionId ?? string.Empty)}</td><td>{Encode(line.RejectionReason ?? string.Empty)}</td></tr>");
}

html.AppendLine(" </tbody>");
html.AppendLine(" </table>");
html.AppendLine(TableBodyEnd);
html.AppendLine(TableEnd);
}

html.AppendLine(" <h2>Error Details</h2>");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,15 @@ public sealed class OperationsDashboardService(
IConfiguration configuration) : IOperationsDashboardService
{
private const string SectionEnd = " </section>";
private const string SectionPanelStart = " <section class=\"panel\">";
private const string DivEnd = " </div>";
private const string TableStart = " <table>";
private const string TableEnd = " </table>";
private const string TableBodyStart = " <tbody>";
private const string TableBodyEnd = " </tbody>";
private const string TableRowStart = " <tr>";
private const string TableRowEnd = " </tr>";
private const string NeutralBadgeTone = "neutral";

public Task<OperationsDashboardModel> GetDashboardModelAsync(CancellationToken cancellationToken) =>
this.BuildDashboardModelAsync(cancellationToken);
Expand All @@ -102,17 +105,17 @@ public async Task<string> RenderDashboardHtmlAsync(CancellationToken cancellatio
html.AppendLine(" <div class=\"eyebrow\">Operations console</div>");
html.AppendLine(" <h1>Merchant File Processor</h1>");
html.AppendLine(" <p>Live configuration, scheduling, and processing visibility for the worker, using the same runtime settings that power the service.</p>");
html.AppendLine(" </div>");
html.AppendLine(DivEnd);
html.AppendLine(" <div class=\"hero-panel\">");
html.AppendLine($" <div class=\"hero-label\">Environment</div><div class=\"hero-value\">{Encode(model.EnvironmentName)}</div>");
html.AppendLine($" <div class=\"hero-label\">Generated</div><div class=\"hero-value mono\">{Encode(model.GeneratedUtc.ToString("u"))}</div>");
html.AppendLine($" <div class=\"hero-label\">Connection</div><div class=\"hero-value\">{Encode(model.ConnectionStringSummary)}</div>");
html.AppendLine(" </div>");
html.AppendLine(DivEnd);
html.AppendLine(SectionEnd);

AppendMetrics(html, model.Metrics);

html.AppendLine(" <section class=\"panel\">");
html.AppendLine(SectionPanelStart);
html.AppendLine(" <div class=\"section-title\">Merchant schedule</div>");
html.AppendLine(TableStart);
html.AppendLine(" <thead><tr><th>Merchant</th><th>Enabled</th><th>Estate</th><th>Run times (UTC)</th><th>Last merchant run</th><th>Next run</th><th>Success</th><th>Failed</th><th>Pending checks</th><th>Status</th></tr></thead>");
Expand All @@ -122,7 +125,7 @@ public async Task<string> RenderDashboardHtmlAsync(CancellationToken cancellatio
{
html.AppendLine(TableRowStart);
html.AppendLine($" <td><strong>{Encode(merchant.MerchantName)}</strong><br /><span class=\"mono muted\">{Encode(merchant.MerchantId)}</span></td>");
html.AppendLine($" <td>{RenderBadge(merchant.Enabled ? "Enabled" : "Disabled", merchant.Enabled ? "good" : "neutral")}</td>");
html.AppendLine($" <td>{RenderBadge(merchant.Enabled ? "Enabled" : "Disabled", merchant.Enabled ? "good" : NeutralBadgeTone)}</td>");
html.AppendLine($" <td class=\"mono\">{Encode(merchant.EstateId)}</td>");
html.AppendLine($" <td class=\"mono\">{Encode(merchant.RunTimesUtc)}</td>");
html.AppendLine($" <td class=\"mono\">{Encode(merchant.LastMerchantRunUtc?.ToString("u") ?? "Never")}</td>");
Expand All @@ -148,9 +151,9 @@ public async Task<string> RenderDashboardHtmlAsync(CancellationToken cancellatio
AppendContractsSection(html, model.Contracts);
html.AppendLine(SectionEnd);

html.AppendLine(" <section class=\"panel\">");
html.AppendLine(SectionPanelStart);
html.AppendLine(" <div class=\"section-title\">Recent merchant runs</div>");
html.AppendLine(" <table>");
html.AppendLine(TableStart);
html.AppendLine(" <thead><tr><th>Merchant</th><th>Scheduled</th><th>Completed</th><th>Status</th><th>Error</th></tr></thead>");
html.AppendLine(TableBodyStart);

Expand Down Expand Up @@ -191,12 +194,12 @@ public async Task<string> RenderConfigurationHtmlAsync(CancellationToken cancell
html.AppendLine(" <div class=\"eyebrow\">Configuration inventory</div>");
html.AppendLine(" <h1>Appsettings review</h1>");
html.AppendLine(" <p>This page mirrors the effective settings loaded at startup, so operators can validate what the worker is using without opening the deployment package.</p>");
html.AppendLine(" </div>");
html.AppendLine(DivEnd);
html.AppendLine(" <div class=\"hero-panel\">");
html.AppendLine($" <div class=\"hero-label\">Authentication</div><div class=\"hero-value\">{Encode(model.AuthenticationSummary)}</div>");
html.AppendLine($" <div class=\"hero-label\">File processing</div><div class=\"hero-value\">{Encode(model.FileProcessingSummary)}</div>");
html.AppendLine($" <div class=\"hero-label\">Polling</div><div class=\"hero-value\">{Encode(model.PollingSummary)}</div>");
html.AppendLine(" </div>");
html.AppendLine(DivEnd);
html.AppendLine(SectionEnd);

html.AppendLine(" <section class=\"grid-2\">");
Expand Down Expand Up @@ -229,7 +232,7 @@ public async Task<string> RenderConfigurationHtmlAsync(CancellationToken cancell
{
html.AppendLine(TableRowStart);
html.AppendLine($" <td><strong>{Encode(merchant.MerchantName)}</strong></td>");
html.AppendLine($" <td>{RenderBadge(merchant.Enabled ? "Enabled" : "Disabled", merchant.Enabled ? "good" : "neutral")}</td>");
html.AppendLine($" <td>{RenderBadge(merchant.Enabled ? "Enabled" : "Disabled", merchant.Enabled ? "good" : NeutralBadgeTone)}</td>");
html.AppendLine($" <td class=\"mono\">{Encode(merchant.EstateId)}</td>");
html.AppendLine($" <td class=\"mono\">{Encode(merchant.MerchantId)}</td>");
html.AppendLine($" <td class=\"mono\">{Encode(merchant.RunTimesUtc)}</td>");
Expand Down Expand Up @@ -262,7 +265,7 @@ public async Task<string> RenderRunsHtmlAsync(CancellationToken cancellationToke
html.AppendLine(" <div class=\"eyebrow\">Run history</div>");
html.AppendLine(" <h1>Merchant execution audit trail</h1>");
html.AppendLine(" <p>Each record shows the scheduled slot, completion time, and final outcome for a merchant processing cycle.</p>");
html.AppendLine(" </div>");
html.AppendLine(DivEnd);
html.AppendLine(SectionEnd);

html.AppendLine(" <section class=\"panel\">");
Expand Down Expand Up @@ -595,7 +598,7 @@ private static void AppendMetrics(StringBuilder html, IReadOnlyList<OperationsMe
html.AppendLine($" <div class=\"metric-label\">{Encode(metric.Label)}</div>");
html.AppendLine($" <div class=\"metric-value\">{Encode(metric.Value)}</div>");
html.AppendLine($" <div class=\"metric-detail\">{Encode(metric.Detail)}</div>");
html.AppendLine(" </div>");
html.AppendLine(DivEnd);
}
html.AppendLine(SectionEnd);
}
Expand Down Expand Up @@ -631,17 +634,17 @@ private static void AppendMerchantConfigPanel(StringBuilder html)

private static void AppendFileProfilesSection(StringBuilder html, IReadOnlyList<FileProfileRow> fileProfiles)
{
html.AppendLine(" <section class=\"panel\">");
html.AppendLine(SectionPanelStart);
html.AppendLine(" <div class=\"section-title\">File profiles</div>");
html.AppendLine(" <table>");
html.AppendLine(TableStart);
html.AppendLine(" <thead><tr><th>Profile</th><th>Format</th><th>Layout</th><th>Fields</th><th>Mapping</th></tr></thead>");
html.AppendLine(" <tbody>");
html.AppendLine(TableBodyStart);

foreach (var profile in fileProfiles)
{
html.AppendLine(" <tr>");
html.AppendLine($" <td><strong>{Encode(profile.FileProfileId)}</strong><br /><span class=\"mono muted\">{Encode(profile.FileProcessorFileProfileId)}</span></td>");
html.AppendLine($" <td>{RenderBadge(profile.Format, "neutral")}</td>");
html.AppendLine($" <td>{RenderBadge(profile.Format, NeutralBadgeTone)}</td>");
html.AppendLine($" <td>{Encode(profile.LayoutSummary)}<br /><span class=\"mono muted\">.{Encode(profile.FileExtension)}{(string.IsNullOrWhiteSpace(profile.ContentType) ? string.Empty : $" / {Encode(profile.ContentType)}")}</span></td>");
html.AppendLine($" <td>{profile.FieldCount} total<br /><span class=\"mono muted\">{profile.HeaderFieldCount} header, {profile.TrailerFieldCount} trailer</span></td>");
html.AppendLine($" <td class=\"mono smallwrap\">{Encode(profile.FieldMap)}</td>");
Expand All @@ -660,11 +663,11 @@ private static void AppendFileProfilesSection(StringBuilder html, IReadOnlyList<

private static void AppendContractsSection(StringBuilder html, IReadOnlyList<ContractMappingRow> contracts)
{
html.AppendLine(" <section class=\"panel\">");
html.AppendLine(SectionPanelStart);
html.AppendLine(" <div class=\"section-title\">Contract mappings</div>");
html.AppendLine(" <table>");
html.AppendLine(TableStart);
html.AppendLine(" <thead><tr><th>Contract</th><th>File profile</th></tr></thead>");
html.AppendLine(" <tbody>");
html.AppendLine(TableBodyStart);

foreach (var contract in contracts)
{
Expand Down Expand Up @@ -767,7 +770,7 @@ private static string RunTone(string status)
return "bad";
}

return "neutral";
return NeutralBadgeTone;
}

private static string Encode(string value) => WebUtility.HtmlEncode(value);
Expand Down
Loading
Loading