diff --git a/docs/notes/endpoint-telemetry-and-ui-phases.md b/docs/notes/endpoint-telemetry-and-ui-phases.md index 5e39258e..4423331a 100644 --- a/docs/notes/endpoint-telemetry-and-ui-phases.md +++ b/docs/notes/endpoint-telemetry-and-ui-phases.md @@ -423,6 +423,52 @@ would be the behaviour the removed comment falsely claimed. --- +## Simulation suite - required check (from a real dev-environment failure) + +The suite must assert that CODE AND SCHEMA ARE NEVER SERVED OUT OF STEP, and +must do it by bringing the image UP, not by building a schema from the models. + +Observed 2026-09-05 in dev, after rolling containers for the Phase 3/4 UI work: +every query against `endpointstates` returned 500 with + + (1054, "Unknown column 'endpointstates.diskhealthjson' in 'field list'") + +The Phase 2 migrations (endpoints 0014, 0015) were written and committed WITH +their models, correctly. They had simply never been applied to that database. +`flask plugin upgrade-all` fixed it in one command. + +Why nothing caught it: + +- The pytest suite builds its schema from the models, so model and schema are + by construction in step. This class of fault is INVISIBLE to it. A green test + run says nothing about whether a deployed database has the columns. +- Nothing in the startup path checks. `create_app()` and `wsgi.py` do no + migration-state validation, so the container reports HEALTHY while every + query against the affected table 500s. The health check passes because it + does not touch the table. +- `flask db preflight` exists but is manual and pre-migration; it is not a + guard on serving traffic. + +The failure is also SILENT IN THE UI in a way that misdirects: a panel whose +endpoint 500s is dropped from `populatedGroups`, so its whole TAB disappears +from PC Details. The visible symptom was a missing "Service/Process Monitors" +tab, which reads as a UI regression in the tab-vocabulary code and sends you +to entirely the wrong file. Nothing on the page mentions the database. + +So the suite needs, at minimum: + +1. A start-up case that runs the real image against a database deliberately + left one migration behind, and asserts the mismatch is reported LOUDLY - + not a healthy container serving 500s. +2. Coverage that the documented order (`flask db upgrade`, then + `flask plugin upgrade-all`, THEN serve) is what the container actually does. +3. A UI-level assertion that a 500ing panel endpoint does not silently remove a + tab with no explanation on screen. + +Worth considering separately from the suite: a startup guard that refuses to +serve, or at least logs at ERROR, when a model references a column the database +does not have. Cheap here, expensive on a fleet rollout. + ## Open questions - answer before the relevant phase 1. ~~LAPS~~ - RESOLVED: not a feature request. The user was illustrating why the diff --git a/frontend/src/components/PluginAssetPanels.vue b/frontend/src/components/PluginAssetPanels.vue index 9966ad9d..acb7130f 100644 --- a/frontend/src/components/PluginAssetPanels.vue +++ b/frontend/src/components/PluginAssetPanels.vue @@ -126,7 +126,10 @@ v-if="((tabSections(panel)[activeTab(panel)] || {}).columns || []).length" class="pap-table-wrap" > - +
- + +
@@ -184,6 +190,9 @@
{{ col.label }}
+
+ {{ panel.empty || 'Nothing to show.' }} +
diff --git a/frontend/src/components/emptyTablePanel.spec.js b/frontend/src/components/emptyTablePanel.spec.js new file mode 100644 index 00000000..a727deea --- /dev/null +++ b/frontend/src/components/emptyTablePanel.spec.js @@ -0,0 +1,51 @@ +import { describe, it, expect } from 'vitest' +import { readFileSync } from 'fs' +import { resolve } from 'path' + +// A `table` panel with no rows used to render a bare header row over an empty +// body, which reads as a broken card rather than as an answer. `list` had +// honoured panel.empty for a long time; `table` had not, for no reason anybody +// wrote down. Found on a live page where Network Interfaces and Listening +// Ports both showed column headers and nothing else. +// +// Asserted against the SOURCE because the condition lives in the template, not +// in an exported helper - the same reason panelGroups.spec.js reads source. + +const source = readFileSync( + resolve(__dirname, 'PluginAssetPanels.vue'), 'utf8', +) + +function tableBlock() { + const start = source.indexOf("panel.render === 'table'") + expect(start).toBeGreaterThan(-1) + // Up to the next render branch, so we only look at the table path. + const end = source.indexOf("panel.render === 'badge'", start) + expect(end).toBeGreaterThan(start) + return source.slice(start, end) +} + +describe('an empty table panel', () => { + it('does not render the table element when there are no rows', () => { + expect(tableBlock()).toMatch(/]*v-if="rows\(panel\)\.length"/) + }) + + it('falls back to the panel-declared empty message', () => { + expect(tableBlock()).toContain('panel.empty') + }) + + it('keeps a default message for a panel that declares none', () => { + // An empty card with no words in it is indistinguishable from a card that + // failed to load. + expect(tableBlock()).toMatch(/panel\.empty \|\| '[^']+'/) + }) +}) + +describe('an empty table SECTION inside a tabs panel', () => { + it('also suppresses its header row', () => { + // The section path was added with the Storage merge and had the same bug. + const start = source.indexOf("panel.render === 'tabs'") + const end = source.indexOf("panel.render === 'keyvalue'", start) + const block = source.slice(start, end) + expect(block).toMatch(/