Add audit logging, processing stages, delete functionality, and bug fixes
- Add audit logging system for tracking CV uploads, processing, deletion, report views, and PDF exports for billing/reference purposes - Add processing stage display on dashboard instead of generic "Processing" - Add delete button for CV checks on dashboard - Fix duplicate primary key error in CompanyCache (race condition) - Fix DbContext concurrency in Dashboard (concurrent delete/load operations) - Fix ProcessCVCheckJob to handle deleted records gracefully - Fix duplicate flags in verification report by deduplicating on Title+Description - Remove internal cache notes from verification results - Add EF migrations for ProcessingStage and AuditLog table Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
@inject ILogger<Dashboard> Logger
|
||||
@inject IJSRuntime JSRuntime
|
||||
@inject TrueCV.Web.Services.PdfReportService PdfReportService
|
||||
@inject IAuditService AuditService
|
||||
|
||||
<PageTitle>Dashboard - TrueCV</PageTitle>
|
||||
|
||||
@@ -179,7 +180,7 @@
|
||||
case "Processing":
|
||||
<span class="badge bg-primary">
|
||||
<span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true" style="width: 0.7rem; height: 0.7rem;"></span>
|
||||
Processing
|
||||
@(check.ProcessingStage ?? "Processing")
|
||||
</span>
|
||||
break;
|
||||
case "Pending":
|
||||
@@ -206,24 +207,32 @@
|
||||
}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
@if (check.Status == "Completed")
|
||||
{
|
||||
<a href="/report/@check.Id" class="btn btn-sm btn-outline-primary" @onclick:stopPropagation="true">
|
||||
View Report
|
||||
</a>
|
||||
}
|
||||
else if (check.Status is "Pending" or "Processing")
|
||||
{
|
||||
<button class="btn btn-sm btn-outline-secondary" disabled>
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
<div class="btn-group" role="group">
|
||||
@if (check.Status == "Completed")
|
||||
{
|
||||
<a href="/report/@check.Id" class="btn btn-sm btn-outline-primary" @onclick:stopPropagation="true">
|
||||
View Report
|
||||
</a>
|
||||
}
|
||||
else if (check.Status is "Pending" or "Processing")
|
||||
{
|
||||
<button class="btn btn-sm btn-outline-secondary" disabled>
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a href="/check" class="btn btn-sm btn-outline-warning" @onclick:stopPropagation="true">
|
||||
Retry
|
||||
</a>
|
||||
}
|
||||
<button class="btn btn-sm btn-outline-danger" @onclick="() => DeleteCheck(check.Id)" @onclick:stopPropagation="true" title="Delete">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
|
||||
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0z"/>
|
||||
<path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4zM2.5 3h11V2h-11z"/>
|
||||
</svg>
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a href="/check" class="btn btn-sm btn-outline-warning">
|
||||
Retry
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@@ -252,7 +261,9 @@
|
||||
private string? _errorMessage;
|
||||
private Guid _userId;
|
||||
private System.Threading.Timer? _pollingTimer;
|
||||
private bool _isPolling;
|
||||
private volatile bool _isPolling;
|
||||
private volatile bool _disposed;
|
||||
private volatile bool _isOperationInProgress;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
@@ -262,20 +273,31 @@
|
||||
|
||||
private void StartPollingIfNeeded()
|
||||
{
|
||||
if (HasProcessingChecks() && !_isPolling)
|
||||
if (HasProcessingChecks() && !_isPolling && !_disposed)
|
||||
{
|
||||
_isPolling = true;
|
||||
_pollingTimer = new System.Threading.Timer(async _ =>
|
||||
{
|
||||
await InvokeAsync(async () =>
|
||||
if (_disposed) return;
|
||||
|
||||
try
|
||||
{
|
||||
await LoadChecks();
|
||||
if (!HasProcessingChecks())
|
||||
await InvokeAsync(async () =>
|
||||
{
|
||||
StopPolling();
|
||||
}
|
||||
StateHasChanged();
|
||||
});
|
||||
if (_disposed) return;
|
||||
|
||||
await LoadChecks();
|
||||
if (!HasProcessingChecks())
|
||||
{
|
||||
StopPolling();
|
||||
}
|
||||
StateHasChanged();
|
||||
});
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// Component was disposed, ignore
|
||||
}
|
||||
}, null, TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(3));
|
||||
}
|
||||
}
|
||||
@@ -298,11 +320,15 @@
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_disposed = true;
|
||||
StopPolling();
|
||||
}
|
||||
|
||||
private async Task LoadChecks()
|
||||
{
|
||||
if (_isOperationInProgress) return;
|
||||
|
||||
_isOperationInProgress = true;
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
|
||||
@@ -317,7 +343,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
_checks = await CVCheckService.GetUserChecksAsync(_userId);
|
||||
_checks = await CVCheckService.GetUserChecksAsync(_userId) ?? [];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -327,6 +353,7 @@
|
||||
finally
|
||||
{
|
||||
_isLoading = false;
|
||||
_isOperationInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -400,6 +427,8 @@
|
||||
var base64 = Convert.ToBase64String(pdfBytes);
|
||||
var fileName = "TrueCV_Report_" + DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".pdf";
|
||||
await JSRuntime.InvokeVoidAsync("downloadFile", fileName, base64, "application/pdf");
|
||||
|
||||
await AuditService.LogAsync(_userId, AuditActions.ReportExported, null, null, $"Exported {reportDataList.Count} reports to PDF");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -420,4 +449,29 @@
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task DeleteCheck(Guid checkId)
|
||||
{
|
||||
if (_isOperationInProgress) return;
|
||||
|
||||
_isOperationInProgress = true;
|
||||
try
|
||||
{
|
||||
var success = await CVCheckService.DeleteCheckAsync(checkId, _userId);
|
||||
if (success)
|
||||
{
|
||||
_checks.RemoveAll(c => c.Id == checkId);
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error deleting CV check {CheckId}", checkId);
|
||||
_errorMessage = "Failed to delete the CV check. Please try again.";
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isOperationInProgress = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user