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" > -
|
-
+ {{ 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(/ |
|---|