From 5cb1de1c4d3fcccafb68a332fda36ec3b649f24e Mon Sep 17 00:00:00 2001 From: Taylor Addison Date: Sat, 5 Sep 2026 14:35:05 -0400 Subject: [PATCH 1/2] Job pills go back to words alone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A design pass proposed giving every job state a pill icon, and the icons are drawn and sitting in icons.tsx: one circle per state, differing only inside, so a column of them reads down cleanly. They are not wired up. ui.tsx already carried the opposite rule with a better argument behind it — device states are read across a whole shelf at a glance, job status is read one row at a time — and a pill that is already a coloured word does not get more legible for gaining a picture. Reverting the lookup and leaving the drawings behind the comment, so the next person to want them finds both the icons and the reason they were not used. Device pills keep their icons, unchanged. Co-Authored-By: Claude Opus 5 --- collector/dash/src/ui.tsx | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/collector/dash/src/ui.tsx b/collector/dash/src/ui.tsx index 3fb8c95..3bc88b1 100644 --- a/collector/dash/src/ui.tsx +++ b/collector/dash/src/ui.tsx @@ -76,21 +76,16 @@ export function Stat({ label, value, tone }: { label: string; value: ComponentCh ); } -/* Every state gets an icon — device states and job states alike. - Device states are read across a whole shelf at a glance; job states are read - down a long Jobs table, where a column of identical circles differing only - inside is faster to scan than five words of similar length. The word stays - in every pill, so the icon is never the only thing carrying the meaning. */ -const PILL_ICON: Partial> = { - online: "online", - stale: "stale", - offline: "offline", - queued: "queued", - claimed: "claimed", - done: "done", - failed: "failed", - cancelled: "cancelled", -}; +/* Device states get an icon; job states do not. Online/stale/offline are + read across a whole shelf at a glance, job status is read one row at a time. + + icons.tsx does carry a matching set for the job states (queued, claimed, + done, failed, cancelled) — one circle differing only inside, drawn for a + design pass that proposed putting them here. They are deliberately not wired + up: the rule above is the older and the better-argued one, and a pill that + is already a coloured word does not get more legible for gaining a picture. + Add them here if a long Jobs table ever proves otherwise. */ +const PILL_ICON: Partial> = { online: "online", stale: "stale", offline: "offline" }; export const Pill = ({ kind, children }: { kind: string; children?: ComponentChildren }) => { const icon = PILL_ICON[kind]; From aebd2225f860d88e6cca9a916b3506dfcaa56ef0 Mon Sep 17 00:00:00 2001 From: Taylor Addison Date: Sat, 5 Sep 2026 14:35:31 -0400 Subject: [PATCH 2/2] The Android runner looks like the rest of the fleet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A phone on the shelf and the collector in a browser are one product, and until now the Android runner was a title, two bare EditTexts, two buttons and a status line on a default AppCompat background. This gives it the dashboard's screen. The palette, theme and most shape drawables landed in 4ee7ab8 by accident — a commit about waiting jobs swept up work in progress. This is the half that makes them do anything: - activity_main is rebuilt as the dashboard's vocabulary at phone scale: a status hero over the mark's own pulse, the collector and device id as rows in a card rather than boxed inputs, three telemetry tiles, a job card that appears only when there is a job to name, and 48dp controls. - PulseTraceView draws the mark's trace left to right while the agent is in contact, and rests as a complete flat drawing when it is not. It honours ANIMATOR_DURATION_SCALE, so a device set to no animations gets none. - MainActivity binds the service's new phase and telemetry flows. The battery and thermal tiles read locally before the first beacon, because deciding whether to enrol a phone on 8% is a pre-start question, and read from the beacon afterwards, so they show what the collector was actually sent. - The launcher icon becomes adaptive: flat ink background, the phone and pulse inside the 66dp safe circle so no mask clips it, and a monochrome layer for themed icons. The status-bar icon becomes a stroke on the 24 grid; filled, it collapsed into a grey lozenge at notification size. No verdict badge on a finished job: the engines post their own final rows and this process never sees the outcome, so a green pill here would be a guess. The status TextView keeps its id and its text, so the Maestro flows that read the agent's state off this screen keep working. Verified by build only. assembleDebug links every layout and drawable reference, but three emulators wedged under this machine's load and the screen has not been looked at on a device. Co-Authored-By: Claude Opus 5 --- .../app/src/main/AndroidManifest.xml | 5 +- .../com/taylab/fleetrunner/MainActivity.kt | 245 ++++++++- .../taylab/fleetrunner/ui/PulseTraceView.kt | 157 ++++++ .../app/src/main/res/drawable/dot.xml | 4 + .../app/src/main/res/drawable/ic_glyph.xml | 14 + .../app/src/main/res/layout/activity_main.xml | 486 ++++++++++++++++-- .../app/src/main/res/mipmap/ic_launcher.xml | 16 + .../src/main/res/mipmap/ic_launcher_round.xml | 16 + .../app/src/main/res/values/strings.xml | 12 + 9 files changed, 918 insertions(+), 37 deletions(-) create mode 100644 runner-android/app/src/main/java/com/taylab/fleetrunner/ui/PulseTraceView.kt create mode 100644 runner-android/app/src/main/res/drawable/dot.xml create mode 100644 runner-android/app/src/main/res/drawable/ic_glyph.xml create mode 100644 runner-android/app/src/main/res/mipmap/ic_launcher.xml create mode 100644 runner-android/app/src/main/res/mipmap/ic_launcher_round.xml diff --git a/runner-android/app/src/main/AndroidManifest.xml b/runner-android/app/src/main/AndroidManifest.xml index 515f4ca..81456b9 100644 --- a/runner-android/app/src/main/AndroidManifest.xml +++ b/runner-android/app/src/main/AndroidManifest.xml @@ -18,8 +18,9 @@ diff --git a/runner-android/app/src/main/java/com/taylab/fleetrunner/MainActivity.kt b/runner-android/app/src/main/java/com/taylab/fleetrunner/MainActivity.kt index 2c0a02b..7240462 100644 --- a/runner-android/app/src/main/java/com/taylab/fleetrunner/MainActivity.kt +++ b/runner-android/app/src/main/java/com/taylab/fleetrunner/MainActivity.kt @@ -4,13 +4,20 @@ import android.Manifest import android.os.Build import android.os.Bundle import android.provider.Settings +import android.view.View import android.widget.Button import android.widget.EditText import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope +import com.taylab.fleetrunner.telemetry.Telemetry +import com.taylab.fleetrunner.ui.PulseTraceView +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch // Loopback by default, so a fresh clone works over `adb reverse` and an @@ -23,6 +30,26 @@ class MainActivity : AppCompatActivity() { private val prefs by lazy { getSharedPreferences("fleet", MODE_PRIVATE) } + private lateinit var pulse: PulseTraceView + private lateinit var headline: TextView + private lateinit var subtitle: TextView + private lateinit var beaconAge: TextView + private lateinit var liveDot: View + private lateinit var liveLabel: TextView + private lateinit var batteryValue: TextView + private lateinit var batteryMeter: View + private lateinit var thermalValue: TextView + private lateinit var thermalSteps: List + private lateinit var networkValue: TextView + private lateinit var jobCard: View + private lateinit var jobLabel: TextView + private lateinit var jobPill: TextView + private lateinit var jobWorkload: TextView + private lateinit var jobId: TextView + private lateinit var jobElapsed: TextView + private lateinit var urlField: EditText + private lateinit var idField: EditText + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) @@ -45,9 +72,7 @@ class MainActivity : AppCompatActivity() { } } - val urlField = findViewById(R.id.collector_url) - val idField = findViewById(R.id.device_id) - val statusView = findViewById(R.id.status) + bindViews() // `--es base_url http://host:port` lets enrollment point a device at a // collector without rebuilding the app — the whole shelf can be moved @@ -64,9 +89,7 @@ class MainActivity : AppCompatActivity() { } findViewById