Show usage per machine, and say why a machine has none
The server now reports limits per machine, so the screen has to as well: one card per machine that offers a paid service, named by the machine first, because these are one account's numbers and which account is decided by which box ran the session. It also has to say which of four things happened, and the reason for splitting them shows up here rather than in the data. A machine nobody has logged in on is working exactly as somebody set it up, so it reads as a plain statement in ordinary text -- marking it would be the interface nagging about a decision already made, and would dilute the marks that do mean something. Only "couldn't reach it" and "the endpoint refused" are coloured as faults, and they say different things because they need different things done. The old screen drew all three in the error colour. No machine offering a paid service is not an error either: it says so instead of drawing nothing. The app also stopped parsing: `available` no longer exists and `getBoolean` on a missing key throws, so this had to land with the server change rather than after it. An older backend sending no `state` is read as "failed" rather than "ok", since an empty card drawn as healthy is the worse failure. Looked at running, against five machines: local reporting notLoggedIn with the backend's HOME emptied, loopback-over-ssh returning real windows beside it, an unreachable host showing ssh's own message in red, and a machine with no Claude provider correctly absent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VETa8afmpWaYezLCqJhDB8
This commit is contained in:
1 parent
6166b1f626
commit
206319045d
4 files changed
+116
-24
No files matched your search
@@ -418,9 +418,21 @@ data class UsageWindow(
|
||||
|
||||
data class UsageSnapshot(
|
||||
val provider: String,
|
||||
val available: Boolean,
|
||||
/** Stable id of the machine these numbers belong to. */
|
||||
val setup: String,
|
||||
/** That machine's current label. */
|
||||
val setupName: String,
|
||||
/**
|
||||
* What came back: "ok", "notLoggedIn", "unreachable" or "failed".
|
||||
*
|
||||
* Four rather than a flag, because the screen has to treat them differently. "notLoggedIn" is a
|
||||
* machine somebody chose not to put an account on -- a fact, not a fault -- while the other two
|
||||
* are faults worth chasing. Collapsing them made a healthy setup read as broken.
|
||||
*/
|
||||
val state: String,
|
||||
/** Why, for the two states that are faults. Absent otherwise. */
|
||||
val detail: String?,
|
||||
val windows: List<UsageWindow>,
|
||||
val error: String?,
|
||||
)
|
||||
|
||||
/** The backend caches; refreshing more often than its poll interval just re-reads the cache. */
|
||||
@@ -429,8 +441,12 @@ fun fetchUsage(settings: ServerSettings): List<UsageSnapshot> =
|
||||
connection.jsonObjects { snapshot ->
|
||||
UsageSnapshot(
|
||||
provider = snapshot.getString("provider"),
|
||||
available = snapshot.getBoolean("available"),
|
||||
error = snapshot.optString("error").ifEmpty { null },
|
||||
setup = snapshot.optString("setup"),
|
||||
setupName = snapshot.optString("setupName"),
|
||||
// Unknown to an older backend, and unknown is not "fine": defaulting to "ok"
|
||||
// would draw an empty card as a healthy one.
|
||||
state = snapshot.optString("state").ifEmpty { "failed" },
|
||||
detail = snapshot.optString("detail").ifEmpty { null },
|
||||
windows =
|
||||
snapshot.getJSONArray("windows").mapObjects { window ->
|
||||
UsageWindow(
|
||||
|
||||
@@ -53,10 +53,11 @@ fun UsageScreen(settings: ServerSettings, onBack: () -> Unit) {
|
||||
Column(Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(16.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||
// Deliberately not subtitled with the provider this was opened from. These numbers
|
||||
// are the *account's*, reported by whichever paid service answered -- naming the
|
||||
// session's provider here made an echo session's screen read "echo" above a card
|
||||
// reading "claude", which is a claim about echo that nothing measured. Each card
|
||||
// names the service it came from, which is the true scope.
|
||||
// belong to an account on a particular machine, reported by whichever paid service
|
||||
// answered there -- naming the session's provider here made an echo session's
|
||||
// screen read "echo" above a card reading "claude", which is a claim about echo
|
||||
// that nothing measured. Each card names the machine and the service it came from,
|
||||
// which is the true scope.
|
||||
Text(
|
||||
"Usage",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
@@ -71,30 +72,79 @@ fun UsageScreen(settings: ServerSettings, onBack: () -> Unit) {
|
||||
is LoadState.Loading -> CircularProgressIndicator()
|
||||
is LoadState.Error -> Text(current.message, color = MaterialTheme.colorScheme.error)
|
||||
is LoadState.Loaded ->
|
||||
current.value.forEach { snapshot ->
|
||||
Card(Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(16.dp)) {
|
||||
Text(snapshot.provider, style = MaterialTheme.typography.titleMedium)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
if (!snapshot.available) {
|
||||
if (current.value.isEmpty()) {
|
||||
// Not an error and not a blank screen: no machine offers a paid service,
|
||||
// so there is genuinely nothing to report and saying so is the answer.
|
||||
Text(
|
||||
"No machine here runs anything with usage limits.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
} else {
|
||||
current.value.forEach { snapshot ->
|
||||
Card(Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(16.dp)) {
|
||||
// The machine first: these are one account's numbers, and
|
||||
// which account is decided by which machine ran the session.
|
||||
Text(
|
||||
snapshot.error ?: "Unavailable",
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
snapshot.setupName.ifEmpty { snapshot.setup },
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
}
|
||||
snapshot.windows.forEach { window ->
|
||||
WindowBar(window)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Text(
|
||||
snapshot.provider,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
SnapshotState(snapshot)
|
||||
snapshot.windows.forEach { window ->
|
||||
WindowBar(window)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(12.dp))
|
||||
}
|
||||
Spacer(Modifier.height(12.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Anything other than numbers: why this machine has none.
|
||||
*
|
||||
* The distinction the old single message could not draw. A machine nobody has logged in on is
|
||||
* working exactly as somebody set it up, so it reads as a plain statement -- marking it would be
|
||||
* the interface nagging about a decision already made, and would dilute the marks that do mean
|
||||
* something. Only the two faults are coloured as faults.
|
||||
*/
|
||||
@Composable
|
||||
private fun SnapshotState(snapshot: UsageSnapshot) {
|
||||
when (snapshot.state) {
|
||||
"ok" -> {}
|
||||
"notLoggedIn" ->
|
||||
Text(
|
||||
"No Claude account on this machine.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
// Reached but refused, versus never reached at all: different things to go and do,
|
||||
// so they say different things rather than sharing one "unavailable".
|
||||
"failed" ->
|
||||
Text(
|
||||
snapshot.detail ?: "Couldn't read the limits from this machine.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = failedColor,
|
||||
)
|
||||
else ->
|
||||
Text(
|
||||
snapshot.detail ?: "Couldn't reach this machine.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = failedColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WindowBar(window: UsageWindow) {
|
||||
val color =
|
||||
|
||||
Reference in new issue
Block a user