Answer API errors in the envelope, say when supplies are unread, badge each chain
Three more findings from the 2026-09-03 run.
Werkzeug answered 405, 415 and a malformed JSON body itself, in HTML. A client
written against the documented {status,data,meta} contract got a page of markup
with no error code, which is indistinguishable from the server having fallen
over. The 405 had a second edge: on a route the caller cannot otherwise see, a
method-not-allowed CONFIRMS the path exists, so a gated kiosk route meant to
read as absent announced itself by refusing the wrong verb. All three now
answer in the envelope, and only under /api/ - everything else is the SPA and
keeps the framework's behaviour, deep links included.
The Toner Report rendered "0 Printers Checked / 0 Low / 0 Critical" and "No
printers match the selected filter" while the API it had just called said
supply monitoring was unreachable. Zeros read as a clean bill of health for a
fleet nobody has looked at. The page now carries the reason the API gave and
suppresses the counts, because a number nobody measured is not a result. Same
defect shape as the supplies card fixed on 2026-08-29; `unavailable` was
already in the payload and the page ignored it.
Backup revisions badged "current" per (asset, kind). NTLARS settings describe a
MACHINE but are read off the controlling PC, so a machine with two reporting
PCs has two independent chains of the same kind - and only the higher revision
id was badged, so the second PC's newest revision showed as history and lost
its last-verified stamp. The machine looked like one source re-reporting rather
than two. sourcehostname is the chain; a revision without one forms its own,
which keeps the old behaviour for everything that has only ever had one source.
Nine tests. The envelope ones verified by planting: remove the three handlers
and three fail.
Scope: product
This commit is contained in:
@@ -116,13 +116,25 @@ def asset_revisions(assetid):
|
||||
|
||||
revisions = query.order_by(BackupRevision.backuprevisionid.desc()).all()
|
||||
|
||||
# "Latest" is per kind, not per asset, so an unfiltered listing still marks
|
||||
# the current revision of each kind correctly.
|
||||
# "Latest" is per kind AND per source chain. Per kind alone was wrong on
|
||||
# the case this exists for: NTLARS settings describe a machine but are read
|
||||
# off the controlling PC, so a machine with two reporting PCs has two
|
||||
# independent chains of the same kind. Marking only the highest revision id
|
||||
# meant the second PC's current revision was badged as history and lost its
|
||||
# "last verified" stamp - the machine looked like it had one source that
|
||||
# kept re-reporting, rather than two.
|
||||
#
|
||||
# sourcehostname is the chain: it records which PC produced the revision.
|
||||
# A revision with none (an older row, or a share-drop with no host) forms
|
||||
# its own chain under the empty key, which keeps the per-kind behaviour for
|
||||
# everything that has only ever had one source.
|
||||
seen = set()
|
||||
out = []
|
||||
for revision in revisions:
|
||||
islatest = revision.backupkind not in seen
|
||||
seen.add(revision.backupkind)
|
||||
chain = (revision.backupkind,
|
||||
(revision.sourcehostname or '').strip().lower())
|
||||
islatest = chain not in seen
|
||||
seen.add(chain)
|
||||
out.append(_summary(revision, islatest=islatest))
|
||||
return success_response(out)
|
||||
|
||||
|
||||
@@ -17,8 +17,18 @@
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<!-- Cannot see the supplies: say so instead of reporting zeros. -->
|
||||
<div v-if="unavailable" class="card blind-notice">
|
||||
<strong>Supply levels are not being read.</strong>
|
||||
<p>{{ unavailable }}</p>
|
||||
<p class="muted">
|
||||
The counts below are not a result - nothing was measured. Check the
|
||||
supply monitoring configuration in Settings.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Summary Bar -->
|
||||
<div class="summary-bar">
|
||||
<div v-if="!unavailable" class="summary-bar">
|
||||
<div class="summary-stat card">
|
||||
<div class="stat-value">{{ summary.total_checked }}</div>
|
||||
<div class="stat-label">Printers Checked</div>
|
||||
@@ -122,6 +132,9 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p v-else-if="unavailable" class="empty-state">
|
||||
No supply data was read, so there is nothing to list.
|
||||
</p>
|
||||
<p v-else class="empty-state">No printers match the selected filter.</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -156,6 +169,7 @@ const loading = ref(true)
|
||||
const error = ref(null)
|
||||
const printers = ref([])
|
||||
const summary = ref({ total_checked: 0, low: 0, critical: 0 })
|
||||
const unavailable = ref('')
|
||||
const filter = ref('all')
|
||||
|
||||
const filterOptions = [
|
||||
@@ -239,6 +253,11 @@ onMounted(async () => {
|
||||
const data = response.data.data
|
||||
printers.value = data.printers || []
|
||||
summary.value = data.summary || { total_checked: 0, low: 0, critical: 0 }
|
||||
// The API says so explicitly when it cannot see supplies at all. Without
|
||||
// this the page rendered the zeros it was given - "0 Printers Checked,
|
||||
// 0 Low, 0 Critical" and "No printers match the selected filter" - which
|
||||
// reads as a clean bill of health for a fleet nobody has looked at.
|
||||
unavailable.value = data.unavailable || ''
|
||||
} catch (err) {
|
||||
console.error('Error loading toner report:', err)
|
||||
error.value = 'Failed to load supply data. Zabbix may not be configured or reachable.'
|
||||
@@ -249,6 +268,13 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.blind-notice {
|
||||
border-left: 3px solid var(--warning);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.blind-notice p {
|
||||
margin: 0.4rem 0 0 0;
|
||||
}
|
||||
.summary-bar {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
|
||||
Reference in New Issue
Block a user