("PagesCompleted")
.HasColumnType("integer")
.HasColumnName("pages_completed");
diff --git a/src/TextServices.Builder.Api/Services/Notifications/JobCompletionNotification.cs b/src/TextServices.Builder.Api/Services/Notifications/JobCompletionNotification.cs
index 45638c7..d4e59c9 100644
--- a/src/TextServices.Builder.Api/Services/Notifications/JobCompletionNotification.cs
+++ b/src/TextServices.Builder.Api/Services/Notifications/JobCompletionNotification.cs
@@ -8,4 +8,5 @@ public record JobCompletionNotification(
DateTimeOffset? Finished,
int TotalPages,
int TotalWordCount,
- string? Errors);
+ string? Errors,
+ int InvocationCount);
diff --git a/src/TextServices.Demo/wwwroot/builder.html b/src/TextServices.Demo/wwwroot/builder.html
index 33cf804..b73f002 100644
--- a/src/TextServices.Demo/wwwroot/builder.html
+++ b/src/TextServices.Demo/wwwroot/builder.html
@@ -60,6 +60,7 @@ Jobs
ID |
Source |
Status |
+ Invocations |
Progress |
Words |
Actions |
diff --git a/src/TextServices.Demo/wwwroot/js/builder.js b/src/TextServices.Demo/wwwroot/js/builder.js
index ab7c8bc..e182609 100644
--- a/src/TextServices.Demo/wwwroot/js/builder.js
+++ b/src/TextServices.Demo/wwwroot/js/builder.js
@@ -195,6 +195,7 @@ function renderTable(rows) {
${esc(id)} |
${esc(truncate(sourceUri, 50))} |
${esc(status)} |
+ ${job?.invocationCount ?? '—'} |
${renderProgress(job)} |
${job?.totalWordCount != null ? job.totalWordCount.toLocaleString() : '—'} |
|
diff --git a/src/TextServices.Search.Api/TextServices.Search.Api.csproj b/src/TextServices.Search.Api/TextServices.Search.Api.csproj
index 2fa1628..518c738 100644
--- a/src/TextServices.Search.Api/TextServices.Search.Api.csproj
+++ b/src/TextServices.Search.Api/TextServices.Search.Api.csproj
@@ -12,6 +12,7 @@
+
diff --git a/src/TextServices.Tests/BuilderApi/JobResponseTests.cs b/src/TextServices.Tests/BuilderApi/JobResponseTests.cs
index a4f3ff4..f4e8031 100644
--- a/src/TextServices.Tests/BuilderApi/JobResponseTests.cs
+++ b/src/TextServices.Tests/BuilderApi/JobResponseTests.cs
@@ -22,6 +22,28 @@ private static BuilderJob CompletedJob(
Status = JobStatus.Completed,
};
+ // -------------------------------------------------------------------------
+ // InvocationCount — reflects the job's current run count
+ // -------------------------------------------------------------------------
+
+ [Fact]
+ public void From_NewJob_InvocationCountIsOne()
+ {
+ var response = JobResponse.From(CompletedJob(), Options());
+ response.InvocationCount.ShouldBe(1);
+ }
+
+ [Fact]
+ public void From_ReprocessedJob_InvocationCountReflectsJobValue()
+ {
+ var job = CompletedJob();
+ job.InvocationCount = 3;
+
+ var response = JobResponse.From(job, Options());
+
+ response.InvocationCount.ShouldBe(3);
+ }
+
// -------------------------------------------------------------------------
// FulfilledServices field
// -------------------------------------------------------------------------
diff --git a/src/TextServices.Tests/BuilderApi/SnsJobNotifierTests.cs b/src/TextServices.Tests/BuilderApi/SnsJobNotifierTests.cs
index 18ce0e6..f8e0818 100644
--- a/src/TextServices.Tests/BuilderApi/SnsJobNotifierTests.cs
+++ b/src/TextServices.Tests/BuilderApi/SnsJobNotifierTests.cs
@@ -15,7 +15,7 @@ public sealed class SnsJobNotifierTests
private static JobCompletionNotification MakeNotification(
string jobId = "my/job",
JobStatus status = JobStatus.Completed) =>
- new(jobId, status, DateTimeOffset.UtcNow, 10, 500, null);
+ new(jobId, status, DateTimeOffset.UtcNow, 10, 500, null, 1);
// -------------------------------------------------------------------------
// No-op when TopicArn is absent
diff --git a/src/TextServices.Tests/BuilderApi/TextBuildJobTests.cs b/src/TextServices.Tests/BuilderApi/TextBuildJobTests.cs
index 6463f40..adb2dc7 100644
--- a/src/TextServices.Tests/BuilderApi/TextBuildJobTests.cs
+++ b/src/TextServices.Tests/BuilderApi/TextBuildJobTests.cs
@@ -598,6 +598,32 @@ public async Task ExecuteAsync_OnSuccess_NotifiesWithCompletedStatus()
notifier.Captured[0].Status.ShouldBe(JobStatus.Completed);
}
+ [Fact]
+ public async Task ExecuteAsync_OnSuccess_NotificationCarriesInvocationCount()
+ {
+ var pages = new List
+ {
+ new() { Id = "https://example.org/c/1", Width = 1000, Height = 1500,
+ TextUri = "https://example.org/alto/1.xml" },
+ };
+
+ var job = await CreateJob("test/notify-invocation-count",
+ sourceDataJson: JsonSerializer.Serialize(pages),
+ invocationCount: 2);
+
+ var altoFetcher = FakeAlto(new Dictionary
+ {
+ ["https://example.org/alto/1.xml"] = SampleAlto("hello world"),
+ });
+
+ var notifier = new CapturingJobNotifier();
+ var sut = MakeJob(altoFetcher: altoFetcher, jobNotifier: notifier);
+ await sut.ExecuteAsync(job.Id, FakeCancellationToken.Instance);
+
+ notifier.Captured.ShouldHaveSingleItem();
+ notifier.Captured[0].InvocationCount.ShouldBe(2);
+ }
+
[Fact]
public async Task ExecuteAsync_OnFailure_NotifiesWithFailedStatus()
{
@@ -622,7 +648,7 @@ public async Task ExecuteAsync_OnFailure_NotifiesWithFailedStatus()
private async Task CreateJob(string id,
string? sourceUri = null, string? sourceDataJson = null,
- JobServices services = JobServices.All)
+ JobServices services = JobServices.All, int invocationCount = 1)
{
var job = new BuilderJob
{
@@ -630,6 +656,7 @@ private async Task CreateJob(string id,
SourceUri = sourceUri,
SourceDataJson = sourceDataJson,
Services = (int)services,
+ InvocationCount = invocationCount,
};
_db.Jobs.Add(job);
await _db.SaveChangesAsync();