feat(update): surface backend update receipt in action popup (#958) (#964)

Adopt GET /api/hermes/update/receipt and append its summary to the
end of the hermes-update action progress popup after it finishes.
Additive only: existing checkHermesUpdate + connection-retry polling
is untouched. 404 (old backend) is handled gracefully.

- add UpdateReceiptResponse data model matching backend schema
- add getUpdateReceipt() to HermesApiService
- ActionProgressController gains trailingLines + pushTrailingLines
- ActionProgressDialog renders trailingLines after the log
- SystemViewModel fetches receipt on action finish and formats it
- extract pure formatUpdateReceiptLines for mockk-free testing
This commit is contained in:
M57 authored and GitHub committed 2026-08-24 01:35:59 +03:00
1 parent 5fd732be9b
commit ece0288ca8
8 files changed
+342 -1

No files matched your search

@@ -110,7 +110,7 @@ class SystemViewModel(application: Application) :
val actionProgress =
ActionProgressController(
scope = viewModelScope,
onFinished = { loadAll() },
onFinished = { status -> onActionFinished(status) },
)
private var actionPollingJob: Job? = null
@@ -223,6 +223,38 @@ class SystemViewModel(application: Application) :
}
}
/**
* Fired when [actionProgress] finishes tracking a backend action. For the
* `hermes-update` action we additionally fetch the durable update receipt
* (issue #958) and append its summary to the end of the progress popup,
* then refresh the screen so the version row reflects the new build. Other
* actions just refresh. A 404 / missing receipt is silently skipped — the
* popup already shows success from the action status.
*/
private fun onActionFinished(status: ActionStatusResponse?) {
if (status?.name == "hermes-update") {
viewModelScope.launch {
val receipt = fetchUpdateReceiptLines()
if (receipt.isNotEmpty()) actionProgress.pushTrailingLines(receipt)
}
}
loadAll()
}
/**
* Pull `GET /api/hermes/update/receipt` and render a compact summary block
* for the popup. Returns an empty list when the endpoint is unavailable
* (old backend, no receipt yet, or any error) so the caller appends nothing.
*/
private suspend fun fetchUpdateReceiptLines(): List<String> {
val result =
withContext(Dispatchers.IO) {
safeApiCall { ApiClient.hermesApi.getUpdateReceipt() }
}
val data = (result as? NetworkResult.Success)?.data ?: return emptyList()
return formatUpdateReceiptLines(data)
}
// ── Gateway actions ────────────────────────────────────────────────
fun startGateway() {
@@ -0,0 +1,46 @@
package com.m57.hermescontrol.ui.system
import com.m57.hermescontrol.data.model.UpdateReceipt
import com.m57.hermescontrol.data.model.UpdateReceiptResponse
import com.m57.hermescontrol.data.model.UpdateReceiptSummary
/**
* Render a compact, popup-friendly summary of an update receipt (issue #958).
*
* Returns an empty list when there is nothing to show (no receipt, old
* backend, or any malformed payload) so callers can skip appending entirely.
*
* Pure + [internal] so it is unit-testable without mocking the API layer.
*/
internal fun formatUpdateReceiptLines(receipt: UpdateReceiptResponse?): List<String> {
if (receipt == null) return emptyList()
val summary: UpdateReceiptSummary? = receipt.summary
val full: UpdateReceipt? = receipt.receipt
if (summary == null && full == null) return emptyList()
val lines = mutableListOf<String>()
lines.add("Update receipt")
val outcome = summary?.outcome ?: full?.outcome
if (outcome != null) lines.add("Outcome: $outcome")
val pre = full?.preUpdate
val post = full?.postUpdate
if (pre != null || post != null) {
val from = pre?.version ?: pre?.sha?.take(8) ?: "?"
val to = post?.version ?: post?.sha?.take(8) ?: "?"
lines.add("Version: $from → $to")
} else if (summary?.postVersion != null) {
lines.add("Version: → ${summary.postVersion}")
}
val fleet = full?.fleet
if (!fleet.isNullOrEmpty()) {
val states = fleet.map { "${it.profile ?: "?"}: ${it.state ?: "?"}" }
lines.add("Fleet: ${states.joinToString(", ")}")
} else if (summary != null && !summary.fleetStates.isNullOrEmpty()) {
lines.add("Fleet: ${summary.fleetStates.joinToString(", ")}")
}
return lines
}