Freshness says which answer it is, and the self row keeps what it built

"Nothing was flagged as out of date" could not be answered from the
phone, because the card drew every way of not knowing as nothing at all:
a component this server had never built looked exactly like one whose
build was current. Freshness now names the state. behind, neverBuilt and
otherMode are measured and have something to do about them; uncommitted,
notBuiltHere, noCheckout and parked are the ways of not knowing, and each
says so in the row -- in the ordinary text colour, because "we could not
check" is a difference in kind from "there is something here", and a
colour cannot carry that.

Only the measured three make a card read as out of date, sit above the
"Up to date" heading, or light up Update. Saying it about something
nobody measured is a nag with nothing behind it.

And the bug the new words turned up on the first look: reconcile_self
rebuilds this server's own row from its declaration at every startup, and
built_from/built_mode were not carried across. The commit recorded by the
build that produced the new binary was forgotten by the process that
build started -- and restarting is how this server takes an update. Its
own card had no freshness ever again: never behind, never current, and
its Update with nothing to do.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Opus 5 committed 2026-09-02 21:47:21 -04:00
1 parent 07639ed14d
commit f359cd3edf
7 files changed
+290 -39

No files matched your search

@@ -84,6 +84,15 @@ data class GitStatus(
val root: String,
)
/**
* What a busy component's freshness is replaced with while it is being worked on.
*
* A word of the app's own rather than one of the server's, because it is not a measurement: it
* means "withheld until this run is over", and every reading of freshness treats it as saying
* nothing. The server's own words all describe something it actually looked at.
*/
const val FRESHNESS_WITHHELD = "withheld"
/**
* What a build button says for the components it covers.
*
@@ -180,11 +189,45 @@ data class ProjectComponent(
// for a server, which builds nothing this phone installs.
val apk: ComponentApk?,
) {
// Only "behind" is worth saying. "Current" is what a card already
// implies, and "unknown" said out loud would be on most rows most of
// the time, which is how a mark stops meaning anything.
val isBehind: Boolean
get() = freshness == "behind"
/**
* Whether there is something measured to act on: the build is behind the checkout, there is no
* build at all, or what is there was built in another mode.
*
* The one place that division is written down, because it decides three things that must not
* disagree -- whether the row says "out of date", whether the card sits above the "Up to date"
* heading, and whether Update can be pressed. Everything else is either current or a state
* nobody could measure, and calling those out of date is a nag with nothing behind it: Iris
* asked for exactly that on 2026-09-02, "it should not do that until things actually can be
* updated".
*/
val isStale: Boolean
get() = freshness in setOf("behind", "neverBuilt", "otherMode")
/**
* What the row says about this component's build, or null where it says nothing.
*
* The three ways of not knowing get words of their own rather than the silence they used to
* share with "current". That silence is what made "why isn't it flagging anything" impossible
* to answer from the phone: a component this server has never built looked exactly like one
* whose build was up to date, and the only difference was in a config file on the other
* machine. They are deliberately *not* coloured like "out of date" -- they are facts about what
* could not be measured, not a button to press.
*/
val freshnessNote: String?
get() =
when (freshness) {
"behind" -> "out of date"
"neverBuilt" -> "never built"
"otherMode" -> "built in another mode"
"uncommitted" -> "uncommitted changes"
"notBuiltHere" -> "not built here"
// "current", "parked", "noCheckout": nothing to say. The
// first is what a card already implies, and the other two
// are said by the branch line above -- a parked checkout
// shows "no branch", and a project outside git shows no
// branch line at all.
else -> null
}
val isServer: Boolean
get() = kind == "server"
@@ -2016,10 +2016,10 @@ private fun AppCard(
// was pressed and is read again only once the run is
// over. So "out of date" beside the bar that is making
// it current is not a measurement, it is the answer
// from before the press -- and unknown is what this
// card already says when it has no reading, so both
// readers of it (the row's own note, and the sibling
// warning) go quiet without either having to know why.
// from before the press -- so it is replaced by a word
// that says nothing, and both readers of it (the row's
// own note, and the sibling warning) go quiet without
// either having to know why.
// Deliberately the same pair of conditions that
// disables the Update button: what cannot be acted on
// is exactly what cannot be measured just now.
@@ -2028,7 +2028,7 @@ private fun AppCard(
.sortedBy { it.isServer }
.map { component ->
if (projectState.busy || componentStates[component.name].busy)
component.copy(freshness = "unknown")
component.copy(freshness = FRESHNESS_WITHHELD)
else component
}
components.forEach { component ->
@@ -3213,7 +3213,7 @@ private fun ComponentCard(
// Said in words, not by colour alone: "behind the
// checkout" is a difference in kind from "running", and a
// reader has no way to learn a colour that means it.
val behind = component.isBehind
val freshnessNote = component.freshnessNote
val status =
when {
!component.isServer -> null
@@ -3246,14 +3246,19 @@ private fun ComponentCard(
overflow = TextOverflow.Ellipsis,
)
}
if (behind) {
freshnessNote?.let {
Separator()
Text(
"out of date",
it,
style = MaterialTheme.typography.bodyMedium,
// The colour Pull & Build wears, because that is
// the button this is telling you to press.
color = ActionTone.Primary.color,
// Update's colour for the states with something
// to do, because that is the button this is
// telling you to press -- and the ordinary text
// colour for the ones that only say what could
// not be measured, which is not a call to act.
color =
if (component.isStale) ActionTone.Primary.color
else MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
@@ -4201,7 +4206,7 @@ private fun hasWorkWaiting(
!entry.built ||
entry.newCommits ||
entry.components.any { component ->
component.isBehind ||
component.isStale ||
component.apk?.let {
!isUpToDate(it, installedTimes[component.name], chosenVariants[component.name])
} == true