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

@@ -0,0 +1,81 @@
package com.m57.hermescontrol.data.model
import kotlinx.serialization.Serializable
/**
* Durable record of the most recent `hermes update` run, returned by
* `GET /api/hermes/update/receipt` (hermes-agent #91277). This is the
* authoritative outcome — it survives gateway restarts and log rotation,
* unlike heuristic connection-retry polling.
*
* All fields are nullable: the backend may emit a partial receipt, and the
* response is decoded with `ignoreUnknownKeys = true`, so future additions
* won't break parsing.
*
* The same data is also attached (flattened) to
* `GET /api/actions/hermes-update/status` under a `receipt` key.
*/
@Serializable
data class UpdateReceiptResponse(
val receipt: UpdateReceipt? = null,
val summary: UpdateReceiptSummary? = null,
)
@Serializable
data class UpdateReceipt(
val schema: Int? = null,
val startedAt: String? = null,
val finishedAt: String? = null,
val argv: List<String>? = null,
val pid: Int? = null,
/** `success` | `partial` | `running`. `running` means the update is still mid-flight. */
val outcome: String? = null,
val preUpdate: UpdateReceiptVersion? = null,
val postUpdate: UpdateReceiptVersion? = null,
val steps: List<UpdateReceiptStep>? = null,
val skips: List<String>? = null,
val gatewayRestart: UpdateReceiptGatewayRestart? = null,
val fleet: List<UpdateReceiptFleetEntry>? = null,
)
@Serializable
data class UpdateReceiptVersion(
val sha: String? = null,
val version: String? = null,
)
@Serializable
data class UpdateReceiptStep(
val name: String? = null,
val ok: Boolean? = null,
val detail: String? = null,
val at: String? = null,
)
@Serializable
data class UpdateReceiptGatewayRestart(
val at: String? = null,
val ok: Boolean? = null,
)
@Serializable
data class UpdateReceiptFleetEntry(
val profile: String? = null,
val pid: Int? = null,
val codeSha: String? = null,
val state: String? = null,
)
/**
* Flattened highlights of [UpdateReceipt], returned alongside it by the
* endpoint. Mirrors the backend's `summary` object so the popup can show the
* essentials without walking the full receipt.
*/
@Serializable
data class UpdateReceiptSummary(
val outcome: String? = null,
val preSha: String? = null,
val postSha: String? = null,
val postVersion: String? = null,
val fleetStates: List<String>? = null,
)