Withhold a component's freshness while it is being built

Nothing re-reads the manifest during a run, so the "out of date" note
beside a running progress bar is the answer from before the button was
pressed -- shown for the length of a build, next to the work that is
making it wrong.

The card now reads a busy component's freshness as unknown, in the one
place the list of components is built for the cards, so the row's note
and the sibling-mismatch warning both go quiet. Same condition as the
one that disables the Update button.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Opus 5 committed 2026-09-01 10:09:29 -04:00
1 parent aa8e2b97a5
commit c35f84a373
2 files changed
+116 -81

No files matched your search

+15
View File
@@ -494,6 +494,21 @@ mutable at runtime from the phone.
the Retry that acts on it, in the one place that reports that the Retry that acts on it, in the one place that reports that
component's failures whether they came from a build, a download or a component's failures whether they came from a build, a download or a
service action. service action.
- **A card being worked on has no freshness, rather than the one from
before the press.** Nothing re-reads the manifest during a run -- the
entry was fetched before the button was pressed and read again only
once the run is over -- so "out of date" drawn beside the bar that is
making it current is last minute's answer wearing this minute's
clothes. `UpdaterScreen` therefore maps a busy component's `freshness`
to `unknown` in the one place the component list is built for the
cards, which is what makes both readers of it -- the row's own note and
`MismatchedPairNote` -- go quiet without either having to know why. The
condition is deliberately the same pair that disables the Update button
(`projectState.busy || componentState.busy`): what cannot be acted on
is exactly what cannot be measured just now. It comes back the instant
the run ends, still saying "out of date" if the build failed, because
by then the entry has been read again -- withheld is not the same as
cleared.
- **A project's own `.dev-updater.ron` is a request, never an - **A project's own `.dev-updater.ron` is a request, never an
instruction.** It only runs once accepted from the phone, which copies instruction.** It only runs once accepted from the phone, which copies
it into `config.ron`; `AppEntry::pending_declaration` is the whole gate. it into `config.ron`; `AppEntry::pending_declaration` is the whole gate.
@@ -1580,87 +1580,107 @@ private fun AppCard(
// order is the *build* order -- dev-updater builds its // order is the *build* order -- dev-updater builds its
// server before its APK on purpose -- and that stays as // server before its APK on purpose -- and that stays as
// it is; this sort is stable, so anything else keeps it. // it is; this sort is stable, so anything else keeps it.
entry.components //
.sortedBy { it.isServer } // Freshness is withheld from a component being worked
.forEach { component -> // on, because it is the one thing on the card that the
val installed = installedTimes[component.name] // work is *about* and nothing re-reads it while the
val installedSize = installedSizes[component.name] // work runs: the entry was fetched before the button
val chosenVariantPath = chosenVariants[component.name] // was pressed and is read again only once the run is
// This component's own, so nothing below can // over. So "out of date" beside the bar that is making
// reach for a sibling's by accident. // it current is not a measurement, it is the answer
val componentState = componentStates[component.name] // from before the press -- and unknown is what this
val upToDate = // card already says when it has no reading, so both
component.apk?.let { // readers of it (the row's own note, and the sibling
isUpToDate(it, installed, chosenVariantPath) // warning) go quiet without either having to know why.
} == true // Deliberately the same pair of conditions that
ComponentCard( // disables the Update button: what cannot be acted on
entryKey = entry.key, // is exactly what cannot be measured just now.
component = component, val components =
packageName = component.apk?.packageName, entry.components
// What the download would cost, and what it .sortedBy { it.isServer }
// replaces. An APK's size sits where a .map { component ->
// server's state does: the one thing worth if (projectState.busy || componentStates[component.name].busy)
// knowing about it besides its name. component.copy(freshness = "unknown")
sizeText = else component
component.apk }
?.takeIf { it.built } components.forEach { component ->
?.let { apk -> val installed = installedTimes[component.name]
when { val installedSize = installedSizes[component.name]
!upToDate && installedSize != null -> val chosenVariantPath = chosenVariants[component.name]
"${formatSize(installedSize)} \u2192 " + // This component's own, so nothing below can
formatSize(apk.size) // reach for a sibling's by accident.
else -> formatSize(apk.size) val componentState = componentStates[component.name]
} val upToDate =
}, component.apk?.let { isUpToDate(it, installed, chosenVariantPath) } ==
chosenVariantPath = chosenVariantPath, true
onSelectVariant = { onSelectVariant(component.name, it) }, ComponentCard(
// This app reaches the server through this server. entryKey = entry.key,
// Stopping or uninstalling it is the one action component = component,
// here that cannot be undone from the phone. packageName = component.apk?.packageName,
isOwnServer = entry.builtIn, // What the download would cost, and what it
build = // replaces. An APK's size sits where a
componentBuild( // server's state does: the one thing worth
projectState, // knowing about it besides its name.
componentState, sizeText =
component.name, component.apk
), ?.takeIf { it.built }
// Being worked on right now, which the ?.let { apk ->
// component's own slice of the build says when {
// directly rather than being inferred from a !upToDate && installedSize != null ->
// project-wide phase name. "${formatSize(installedSize)} \u2192 " +
working = formatSize(apk.size)
componentBuild(projectState, componentState, component.name) else -> formatSize(apk.size)
?.running == true, }
busy = serviceBusy == component.name, },
state = componentState, chosenVariantPath = chosenVariantPath,
onAction = { action, purge -> onSelectVariant = { onSelectVariant(component.name, it) },
onServiceAction(component.name, action, purge) // This app reaches the server through this server.
}, // Stopping or uninstalling it is the one action
controls = { // here that cannot be undone from the phone.
if (!component.isServer) { isOwnServer = entry.builtIn,
// Said before the button, because it build =
// qualifies what pressing it gets you. componentBuild(
MismatchedPairNote( projectState,
self = component, componentState,
others = entry.components, component.name,
) ),
// The button that asked for it, then // Being worked on right now, which the
// how far along it is: a bar reports on // component's own slice of the build says
// the control above it. // directly rather than being inferred from a
UpdateButton( // project-wide phase name.
built = component.apk?.built == true, working =
needsBuild = entry.needsBuild, componentBuild(projectState, componentState, component.name)
installed = installed != null, ?.running == true,
upToDate = upToDate, busy = serviceBusy == component.name,
state = componentState, state = componentState,
projectState = projectState, onAction = { action, purge ->
onUpdate = { onUpdate(entry, component.name) }, onServiceAction(component.name, action, purge)
) },
ApkProgress(componentState) controls = {
} if (!component.isServer) {
}, // Said before the button, because it
) // qualifies what pressing it gets you.
} MismatchedPairNote(
self = component,
others = components,
)
// The button that asked for it, then
// how far along it is: a bar reports on
// the control above it.
UpdateButton(
built = component.apk?.built == true,
needsBuild = entry.needsBuild,
installed = installed != null,
upToDate = upToDate,
state = componentState,
projectState = projectState,
onUpdate = { onUpdate(entry, component.name) },
)
ApkProgress(componentState)
}
},
)
}
} }
} }
// What there is to read before pressing anything, directly // What there is to read before pressing anything, directly