feat: the System tab is one bounded table, and the dial is the switch (4295)
CI and images / lint (push) Successful in 3s
CI and images / extension-version (push) Successful in 3s
CI and images / frontend-build (push) Successful in 20s
CI and images / backend-lint-and-test (push) Successful in 32s
CI and images / integration (push) Successful in 2m10s
CI and images / sign-extension (push) Successful in 3s
CI and images / build-agent (push) Successful in 6s
CI and images / build-web (push) Successful in 2m10s
CI and images / smoke-web (push) Successful in 52s
CI and images / promote (push) Skipped

Operator, 2026-09-23, on the screenshot: *"I feel that we can probably combine
the two sections into a single table and to format it in such a way that it
appears more bounded and less free-form or open. also there's nothing to
describe what 'auto' means or why their needs to be or should be on/off
toggles. almost all of it always needs to run there's only one optional piece
and it is killed by moving the 'cap' to zero."*

Three separate things, all correct.

## The four lanes were listed twice

The roster (milestone 365) said "ML tagging is running", and four hundred
pixels below it the lanes pane said "ML tagging · 1/1 busy". Two answers to
one question from two endpoints, free to disagree on screen. I moved the
second pane onto this tab yesterday and did not notice it duplicated the
first.

Now one row per part, with controls on the rows that have a lane and none on
the rows that do not. The join is on the QUEUE SET, because that is what
`service_roster` keys a celery part on — as a set, not as a string, so neither
side has to agree about order.

It lives in `utils/systemParts.js` rather than inline, and has a spec, because
its failure is SILENT and is the exact thing it exists to prevent: a lane that
stops matching its part does not throw, it grows a second row for the same
worker. The duplication, returning through the code that removed it.

## Bounded, not free-form

A real table — header, column rules, one bordered card — instead of dotted
rows floating on the page background with nothing saying where the list began
or what a column meant.

## The dial is the switch

There was an `On` switch per lane beside the slots dial. Of four lanes, three
must run for the application to work at all, so that switch offered a choice
that was never real — and for the one lane that IS optional, "off" and "zero
slots" were two ways of saying the same thing that could disagree with each
other.

So `enabled` is now DERIVED from the number: `set_lane` sets it from
`slots > 0` when the caller did not say. It stays on the API and in the model
— it is still the mechanism, and a drain-before-restart may still want a lane
holding its process with consumers cancelled without destroying the operator's
slot count to say so.

Two things fell out that a test now pins:

- The consumer command is sent on the CHANGE, not on the field being present.
  Otherwise every slots write re-sends a command that changes nothing —
  lesson #4183's churn, arriving through the new derivation.
- The model fetch fires on the off→on TRANSITION. It used to test `enabled is
  True`, the field having been sent. The UI no longer sends it, so the
  download that makes the ML lane usable would simply never have fired and the
  lane would have come on to consume a queue it had no model for.

## And Auto now says what it is

A legend under the table, in the operator's terms: what a slot is, that zero
turns a lane off, that three of the four are not optional, what `of N` means,
and that Auto lets a lane add slots by itself when its queue is backed up AND
every slot is busy — with why it is off by default, since it is the only thing
on the page that acts without being asked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR
This commit is contained in:
2026-09-23 11:26:27 -04:00
co-authored by Claude Opus 5
parent 274f7ffe21
commit abe16aa382
8 changed files with 761 additions and 470 deletions
+128
View File
@@ -0,0 +1,128 @@
import { describe, expect, it } from 'vitest'
import { mergeParts, queueKey } from '../src/utils/systemParts.js'
// The System tab's one table (operator 2026-09-23: "combine the two sections
// into a single table"). What is pinned here is the JOIN, because its failure
// is silent and is precisely the thing the merge exists to fix: a lane that
// stops matching its roster part does not throw — the table grows a SECOND row
// for the same worker, one with controls and one without.
const PART = {
key: 'celery:default,download,import,thumbnail',
kind: 'celery',
name: 'Worker',
state: 'ok',
detail: 'Worker is running',
// The roster SORTS a celery part's queues into its key.
queues: ['default', 'download', 'import', 'thumbnail'],
}
const LANE = {
name: 'worker',
display_name: 'Worker',
// The lane table lists them in the order the role reads them, which is NOT
// sorted. If the join ever compares these two lists directly rather than as
// sets, this fixture is what catches it.
queues: ['default', 'import', 'thumbnail', 'download'],
slots: 2,
slots_cap: 4,
autoscale: false,
live: { present: true, replicas: 1, pool: 2, active: 0 },
pending: 0,
}
const POSTGRES = {
key: 'postgres', kind: 'datastore', name: 'PostgreSQL',
state: 'ok', detail: 'answering', latency_ms: 2.5,
}
describe('queueKey', () => {
it('does not care what order either side lists its queues in', () => {
expect(queueKey(LANE.queues)).toBe(queueKey(PART.queues))
})
it('survives a part that has no queues at all', () => {
// A datastore, and also the stale `Worker ()` row a previous deployment
// left in the roster with an empty queue set. Neither must match a lane.
expect(queueKey(undefined)).toBe('')
expect(queueKey([])).toBe('')
})
})
describe('mergeParts', () => {
it('gives a worker ONE row, carrying its controls', () => {
const rows = mergeParts([PART, POSTGRES], [LANE])
expect(rows).toHaveLength(2)
const worker = rows.find((r) => r.name === 'Worker')
expect(worker.lane).toBe(LANE)
expect(rows.filter((r) => r.name === 'Worker')).toHaveLength(1)
})
it('leaves a datastore without a lane rather than guessing one', () => {
const pg = mergeParts([PART, POSTGRES], [LANE]).find((r) => r.key === 'postgres')
expect(pg.lane).toBeUndefined()
})
it('still lists a lane the roster has never seen', () => {
// Parts are learned as they appear; the lane table is known up front. The
// lane an operator most needs to find — an optional one, never started —
// is exactly the one with no roster entry.
const ml = {
...LANE, name: 'ml', display_name: 'ML tagging', queues: ['ml'],
slots: 0, optional: true,
live: { present: false, replicas: 0, pool: null, active: 0 },
}
const rows = mergeParts([POSTGRES], [ml])
const row = rows.find((r) => r.name === 'ML tagging')
expect(row.lane).toBe(ml)
expect(row.kindLabel).toBe('optional lane')
})
it('does not call a lane at zero slots broken', () => {
// The roster only knows a heartbeat age, so it goes on saying "is running"
// for a lane the operator deliberately dialled to nothing. The lane knows
// the difference; reporting the operator's own choice as a fault is how an
// indicator stops being read.
const off = { ...LANE, slots: 0 }
const row = mergeParts([PART], [off])[0]
expect(row.detail).toBe('off — no slots')
})
it('puts the broken thing first, whatever it is', () => {
const down = { ...POSTGRES, state: 'down', detail: 'not answering' }
const rows = mergeParts([PART, down], [LANE])
expect(rows[0].name).toBe('PostgreSQL')
})
it('otherwise puts the rows you can act on first', () => {
const rows = mergeParts([POSTGRES, PART], [LANE])
expect(rows.map((r) => r.name)).toEqual(['Worker', 'PostgreSQL'])
})
it('reports a wedged lane, and only through the reporter it was given', () => {
// `laneStuckFor` is passed in rather than imported, so this file does not
// re-test the store's rule — it tests that the merge asks.
const asked = []
const rows = mergeParts([PART], [LANE], (lane) => {
asked.push(lane.name)
return '40 minutes'
})
expect(asked).toEqual(['worker'])
expect(rows[0].stuckFor).toBe('40 minutes')
// The roster still owns a matched row's state — `stuckFor` is a note
// beside it, not a verdict that overrides the heartbeat.
expect(rows[0].state).toBe('ok')
})
it('handles an empty everything without inventing rows', () => {
expect(mergeParts([], [])).toEqual([])
expect(mergeParts(undefined, undefined)).toEqual([])
})
})