diff --git a/app/src/main/kotlin/com/shonar/desktop/Screens.kt b/app/src/main/kotlin/com/shonar/desktop/Screens.kt index d70ebd4..b50449d 100644 --- a/app/src/main/kotlin/com/shonar/desktop/Screens.kt +++ b/app/src/main/kotlin/com/shonar/desktop/Screens.kt @@ -709,7 +709,8 @@ fun DetailScreen(state: DesktopState) { "Summarizing… ${it.progress}%" it.status == "running" -> "Summarizing… waiting for model" + DesktopState.waitSuffix(it.startedAt, System.currentTimeMillis()) - else -> "Summary queued" + else -> "Summary queued" + + DesktopState.waitSuffix(it.createdAt, System.currentTimeMillis()) } Text(label, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.primary) @@ -885,7 +886,9 @@ fun DetailScreen(state: DesktopState) { } Text(buildString { append(when { - sStatus == "queued" -> "Queued…" + sStatus == "queued" -> "Queued…" + + DesktopState.waitSuffix(sJob?.createdAt, + System.currentTimeMillis()) sProg != null && sProg > 0 -> "Summarizing… $sProg%" else -> "Summarizing… waiting for model" + DesktopState.waitSuffix(sJob?.startedAt, diff --git a/app/src/test/kotlin/com/shonar/desktop/JobProgressTest.kt b/app/src/test/kotlin/com/shonar/desktop/JobProgressTest.kt index a79a87d..06bfb8e 100644 --- a/app/src/test/kotlin/com/shonar/desktop/JobProgressTest.kt +++ b/app/src/test/kotlin/com/shonar/desktop/JobProgressTest.kt @@ -81,4 +81,23 @@ class JobProgressTest { assertEquals(" · 1:02", DesktopState.waitSuffix( t.minusSeconds(62).toString() + "Z", now)) } + + @Test fun `parseJobs surfaces created_at for queue wait`() { + val raw = """[{"job_type":"summarize","status":"queued","attempt":0, + "max_attempts":3,"created_at":"2026-09-16T15:50:12.556516"}]""" + val jobs = com.shonar.recording.parseJobs(raw) + assertEquals(1, jobs.size) + assertEquals("2026-09-16T15:50:12.556516", jobs[0].createdAt) + assertNull(jobs[0].startedAt) + } + + @Test fun `queued wait suffix counts from created_at`() { + val t0 = System.currentTimeMillis() / 1000 * 1000 + val iso = java.time.LocalDateTime.ofInstant( + java.time.Instant.ofEpochMilli(t0 - 124_000), java.time.ZoneOffset.UTC) + .withNano(0).toString() + // Same formatter the running-wait label uses: queued time is + // visible time too, so it must count from created_at. + assertEquals(" · 2:04", DesktopState.waitSuffix(iso, t0)) + } } diff --git a/backend/shonar/api/schemas_recordings.py b/backend/shonar/api/schemas_recordings.py index d1ca5f9..71327cf 100644 --- a/backend/shonar/api/schemas_recordings.py +++ b/backend/shonar/api/schemas_recordings.py @@ -129,6 +129,8 @@ class ProcessingJobOut(ORMModel): progress: int | None started_at: datetime | None finished_at: datetime | None + # Queue wait is visible time too: the UI counts it for queued jobs. + created_at: datetime | None = None class TranscriptUpdate(BaseModel): diff --git a/shared/com/shonar/recording/AiContent.kt b/shared/com/shonar/recording/AiContent.kt index 84b99fe..8041d83 100644 --- a/shared/com/shonar/recording/AiContent.kt +++ b/shared/com/shonar/recording/AiContent.kt @@ -59,6 +59,9 @@ data class JobInfo( val progress: Int? = null, /** ISO-8601 UTC when the job started running (null if unknown). */ val startedAt: String? = null, + /** ISO-8601 UTC when the job was queued (null if unknown). The wait + * label counts from this while the engine holds the job queued. */ + val createdAt: String? = null, ) /** Summary content keys, in display order. */ @@ -135,6 +138,7 @@ fun parseJobs(raw: String): List = runCatching { stage = o.optString("stage", null).takeUnless { it.isNullOrBlank() }, progress = if (o.has("progress") && !o.isNull("progress")) o.optInt("progress") else null, startedAt = o.optString("started_at", null).takeUnless { it.isNullOrBlank() }, + createdAt = o.optString("created_at", null).takeUnless { it.isNullOrBlank() }, ) } }.getOrDefault(emptyList())