# Fleet directory: row fields and status model

> What ok, warn, bad and offline mean in the fleet directory, which telemetry populates each row field, how missing values render, and which bulk actions are live.

The fleet directory is the robotics entry screen in the ClutchCall
console. It renders one row per registered robot, a four-tile KPI strip,
and an optional map view. Two procedures back the screen:

| Data | Procedure | Refresh |
| ---- | --------- | ------- |
| KPI strip | `fleetRobotics.kpiStrip({ orgId })` | every 30 s, plus manual refresh |
| Robot rows | `fleetRobotics.robotsList({ orgId, page, perPage })` | every 30 s, plus manual refresh |
| Row tags | resource tags for kind `dim_robot` | on mount and after a tag edit |

Rows are rendered as the BFF returns them. The screen does not synthesise
robots, and it does not hide a robot that has stopped reporting telemetry —
a registered robot with no live session still occupies a row.

## The status model: ok, warn, bad, offline

Every row carries a `status` of `ok`, `warn`, `bad`, or `offline`. The
status is computed by the BFF and delivered on the row; the console only
renders it (as the status dot, as the map pin colour, and as a filter
value).

| Status | Meaning in the directory | Counted as online? | Counted as errored? |
| ------ | ------------------------ | ------------------ | ------------------- |
| `ok` | Robot is reporting and nothing is flagged. | Yes | No |
| `warn` | Robot is reporting, with a degraded condition. | Yes | No |
| `bad` | Robot is reporting an error condition. | Yes | Yes |
| `offline` | Robot is registered but not reporting. | No | No |

Two consequences of that table are worth internalising:

- **`bad` is still online.** A robot in error is connected and talking,
  which is why the "Robots online" tile can read `12 / 12` while the hint
  underneath says `3 errored`.
- **`offline` is the only status excluded from the online count.** The
  console's fallback online count is `ok + warn + bad`, and its fallback
  errored count is the number of `bad` rows. These fallbacks are used only
  when `kpiStrip` does not return `online_robots` / `errored_robots`.

The Status filter chip cycles through `Any → ok → warn → bad → offline`.
It is an exact match on the row's `status`, not a severity threshold, so
filtering to `warn` does not include `bad`.

## Where each row field comes from

| Column | Row field | Rendering |
| ------ | --------- | --------- |
| Robot | `name`, `id`, `model` | Name on the first line; `id · model` in mono underneath. |
| Site | `site` | Chip. Also populates the Site filter's option list. |
| Last seen | `lastSeen` | The literal value `live` renders as a blinking green dot and the word `live`. Any other value is printed verbatim in mono. |
| Status | `status` | Status dot. See the status model above. |
| Battery | `battery` | Percentage bar. |
| Task | `task` | Mono text — the task identifier the robot reports. |
| fps | `fps` | Mono, coloured: green above 10, amber above 0, grey at 0. Shown to one decimal. |
| RTT | `rtt` | Mono milliseconds, coloured: normal below 100 ms, amber below 400 ms, red at or above 400 ms. |
| Alerts | `alerts` | A red chip with the count, or a grey `0`. |
| Tags | tag service | Editable inline. Robots inherit tags from their fleet and, failing that, their site, so most tags shown here were never typed on the robot itself. |

`pose.x` / `pose.y` are not shown in the list. They are used only by the
map view, which drops a pin per robot with the row's `status` as the pin
colour and the robot `id` as the label.

Clicking a row navigates to that robot's detail screen at `/robot/<id>`.

## Missing values and the degraded-telemetry flag

Telemetry fields can be absent while the robot record still exists. The
screen distinguishes "unknown" from a real zero rather than collapsing
both to `0`.

**Per row:**

- `fps` and `rtt` render as an em dash (`—`) when the field is `null` or
  absent. A reported `0` fps renders as `0.0` in grey — that is a robot
  publishing nothing on its video track, which is a different condition
  from no telemetry at all.
- A row with no `pose.x` / `pose.y` is filtered out of the map view. It
  still appears in the list. The map's "N robots" hint counts the rows
  passing your filters, not the number of pins drawn, so a fleet with
  partial pose data shows fewer pins than that number.
- The Battery filter reads a missing `battery` as `0`. A robot with no
  battery reading therefore survives the `< 20%` filter and is excluded by
  `≥ 20%` and `≥ 50%`.

**Fleet-wide:** when `kpiStrip` returns `source: "degraded"`, there is no
live session to aggregate. The console then blanks the three
session-derived tiles rather than showing zeros:

| Tile | Normal | When `source` is `degraded` |
| ---- | ------ | --------------------------- |
| Robots online | count `/ total`, hint `N errored` | `—`, hint `no live session · telemetry idle` |
| Aggregate fps | Hz, hint `across tracks` | `—`, hint `no live session · telemetry idle` |
| Fleet avg RTT | ms, hint `P50 over 30s`, amber at or above 100 ms | `—`, hint `no live session · telemetry idle` |
| Active alerts | alert count, hint `N red` | unchanged — still rendered |

Total robots and active alerts are not session-derived, so they keep
their values under the degraded flag. If every tile but "Robots" is
dashed out, the fleet is registered but nothing is streaming; that is not
an error in the console.

## Filters, search, and pagination

`robotsList` is paginated server-side at 50 rows per page. Search and the
filter chips are then applied **in the browser, to the rows of the current
page**. The counter in the filter bar reads `X of Y`, where `Y` is the row
count delivered for that page.

- **Search** matches a case-insensitive substring against `id`, `name`,
  `model`, `task`, and `site` joined together.
- **Site** and **Model** chips are built from the distinct non-empty
  values present in the loaded rows, so their option lists reflect the
  current page.
- **Status** cycles the four statuses. **Battery** cycles
  `Any → ≥ 20% → ≥ 50% → < 20%`.
- **Tag filter** narrows further, and reports how many rows matched out
  of the rows that survived the other filters.

Changing search or any chip resets to page 1.

**Export** writes the currently filtered rows to `fleet-robots.csv` with
the columns `id`, `name`, `model`, `site`, `status`, `battery`, `task`,
`fps`, `rtt`, `alerts`, `last_seen`. It exports what you can see — the
filtered current page — not the whole fleet. The button is disabled when
no rows match.

## Bulk actions and their current status

Select rows with the checkboxes to raise the bulk bar. Only one action is
wired up in the current build:

| Action | Status | Behaviour |
| ------ | ------ | --------- |
| Send to dock | Disabled | Not implemented. |
| **Emergency stop** | **Implemented** | Confirms, then publishes a zero-velocity command to `robot/<id>/ctl` for each selected robot. |
| Open group teleop | Disabled | Not implemented. |
| Export selected | Disabled | Not implemented. Use the header **Export** button, which exports the filtered rows. |

Emergency stop asks for confirmation first, naming the number of robots
and the topic it will publish to. It then attempts each robot
independently and reports the ones it could not reach in an alert listing
their ids. A failure for one robot does not abort the others, and the
list is not rolled back — treat the reported ids as robots that are still
moving and re-issue the stop for them.

## How a registered robot becomes live

A row exists because the robot is registered, not because it is
connected. The lifecycle you see in this screen is:

1. **Registered.** Add the robot with **Add robot**, or let it register
   with the relay. Until `robotsList` returns any rows, the table shows
   `Robots appear here once they register with the relay — add one to
   begin.` (If rows exist but your filters exclude all of them, you get
   the different message `No robots match the current filters — clear
   them to see the whole fleet.`)
2. **Registered, not reporting.** The row shows identity fields — `name`,
   `id`, `model`, `site`, tags — with `status: offline`, a non-`live`
   `lastSeen`, and dashes for `fps` and `rtt`. With no session anywhere in
   the fleet, `kpiStrip` reports `source: "degraded"` and the online, fps,
   and RTT tiles dash out.
3. **Live.** Once the robot publishes telemetry, `lastSeen` becomes the
   literal `live` and renders as the blinking indicator, `status` moves to
   `ok`, `warn`, or `bad`, and `battery`, `task`, `fps`, `rtt`, `alerts`,
   and `pose` populate. The robot now counts toward the online tile and,
   with a pose, appears as a pin on the map.

Because both queries poll every 30 s, a robot that has just come up can
take until the next poll to flip from `offline` to `live`. Use the
refresh button in the filter bar to refetch both the rows and the KPI
strip immediately; the timestamp beside it shows when the row data last
arrived.

## Related

- [Telemetry](/platform/telemetry) — the metric, trace, and CDR streams behind these numbers
- [Authentication](/concepts/authentication) — API keys and relay tokens used by control-plane calls
