Filter the queue by check, and say when a check cannot answer

TWO CONTROLS, ONE BOARD

Severity answers "how bad". It could not answer "what kind", and on a fleet
where eight endpoints go quiet at once, being able to set those aside and look
at the four crashes is the difference between a board and a list.

Chips per contributing check, coloured by its severity, composing with the
severity rollup above them. Both narrow what is already loaded rather than
refetching, so no combination can disagree with the counts. Picking a severity
that excludes the active chip clears the chip: two controls lit above an empty
queue, neither obviously wrong, is worse than losing the narrower one.

A chip counts every row, not the rows the queue had room for. A chip reading 5
above a "3 more" line would be its own small lie.

"LOW SUPPLIES 0" WAS NOT TRUE

Asked what happened to toner replacement, and the answer was worse than the
layout question that prompted it.

_get_low_supplies_data returned the same empty result for three different
situations: Zabbix not configured, Zabbix unreachable, and every cartridge
full. The dashboard strip therefore displayed "Low Supplies 0 / Critical 0"
while the monitoring was dead - and this instance is exactly that case, with
zabbix_enabled true against a URL nothing answers on.

So the strip removed in the previous commit was not information. It was
reassurance, and it read identically whether the printers were fine or nobody
could see them. That is the failure this codebase already names elsewhere: an
empty card and a broken card must not look the same.

The endpoint now distinguishes the three, and returns an envelope rather than a
bare list so a card can say WHY it has no rows. cardVisible shows such a card
whatever its empty rule, because "I cannot tell" is not "nothing to report".

It renders as a notice line, not a queue row: there is no asset to act on, and
burying "the monitor is dead" among the things it failed to find would be the
same mistake in a new place.

Note what this does NOT fix: the Zabbix URL on this instance is a dev
placeholder. The board will now say so instead of implying the cartridges are
full, but somebody still has to point it at a real server - and it is worth
checking what production points at, since nothing would have shown there either.

Frontend 314 passed (13 new), full suite 2178. The response shape changed from
a list to an envelope; no test depended on the old shape.
This commit is contained in:
cproudlock
2026-08-29 21:16:28 -04:00
parent 13c46af5ce
commit b80bd8731f
4 changed files with 254 additions and 7 deletions

View File

@@ -5,6 +5,7 @@ import {
visibleRows, overflowCount,
queueRows, queueOverflow, contextCards, severityTotals,
QUEUEPERSOURCE, QUEUEMAX, singularise,
cardUnavailable, unavailableCards, sourceFacets,
} from './dashboardCards'
const failuresCard = {
@@ -392,3 +393,78 @@ describe('singularise', () => {
expect(singularise(undefined, 1)).toBe('')
})
})
describe('cardUnavailable', () => {
// "Nothing is low" and "we cannot tell whether anything is low" are different
// answers. The printer supplies card returned the same empty for both, so a
// dead Zabbix rendered as "Low Supplies 0" - reassurance, not information.
it('reads the reason a card gives for having no rows', () => {
const card = { _data: { rows: [], unavailable: 'Supply monitoring is not reachable' } }
expect(cardUnavailable(card)).toBe('Supply monitoring is not reachable')
})
it('is empty for a card that simply has nothing to report', () => {
expect(cardUnavailable({ _data: { rows: [] } })).toBe('')
expect(cardUnavailable({ _data: [] })).toBe('')
expect(cardUnavailable({ _data: null })).toBe('')
})
it('ignores a non-string, so bad data cannot render as a reason', () => {
expect(cardUnavailable({ _data: { rows: [], unavailable: true } })).toBe('')
})
it('shows an unavailable card even when its empty rule says hide', () => {
const card = {
empty: 'hide', render: 'exceptions',
_data: { rows: [], unavailable: 'Supply monitoring is not reachable' },
}
expect(cardVisible(card)).toBe(true)
})
it('still hides a card that is merely empty', () => {
expect(cardVisible({ empty: 'hide', render: 'exceptions', _data: { rows: [] } })).toBe(false)
})
it('still hides a card whose fetch failed, which is not the same thing', () => {
expect(cardVisible({ empty: 'hide', render: 'exceptions', _data: null })).toBe(false)
})
it('picks out the unavailable ones', () => {
const blind = { id: 'a', _data: { rows: [], unavailable: 'not reachable' } }
const fine = { id: 'b', _data: { rows: [{}] } }
expect(unavailableCards([blind, fine]).map((c) => c.id)).toEqual(['a'])
})
})
describe('sourceFacets', () => {
it('is one chip per contributing check, worst first', () => {
const facets = sourceFacets([warnA, critA, critB])
expect(facets.map((f) => f.key)).toEqual(
['geenforce-failures', 'endpoints-stability', 'computers-quiet'])
})
it('labels with the shortname, since the title is a heading', () => {
const card = { ...critA, shortname: 'Crashes' }
expect(sourceFacets([card])[0].label).toBe('Crashes')
})
it('falls back to the title when no shortname is declared', () => {
expect(sourceFacets([critA])[0].label).toBe('Endpoints with crashes')
})
it('counts every row, not just the ones the queue had room for', () => {
// The chip says how many there are; the queue caps what it shows. A chip
// reading 5 above a "3 more" line would be its own small lie.
const noisy = { ...critA, _data: Array.from({ length: 12 }, (_, i) => ({ hostname: 'H' + i })) }
expect(sourceFacets([noisy])[0].count).toBe(12)
})
it('leaves out checks with nothing to show', () => {
const empty = { ...critA, id: 'empty', _data: [] }
expect(sourceFacets([empty])).toEqual([])
})
it('leaves out info cards, which are not in the queue', () => {
expect(sourceFacets([infoA])).toEqual([])
})
})