From 6799c270191a05aac7fa47442d0597d5e15da98e Mon Sep 17 00:00:00 2001 From: gkennos Date: Sat, 1 Aug 2026 23:31:12 +1000 Subject: [PATCH] Fix operator page layout so controller-only pages do not render their widget repr or cover the shared workbench. --- README.md | 10 +++---- src/groundskeeping/themes/groundskeeping.tcss | 8 +++++ src/groundskeeping/widgets/workbench.py | 29 ++++++++++--------- tests/test_app_smoke.py | 13 +++++++++ 4 files changed, 41 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index ab8798c..780a5d7 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ The package provides the shared operating frame. A consumer application provides A `groundskeeping` app starts with one `OperatorAppSpec`. -The spec names the app, orders the pages, registers the actions, and supplies the policies that decide whether work may run. It is the composition root: everything application-specific should arrive there from the consumer, not through global lookup inside the shared package. +The spec names the app, orders the pages, registers the actions, and supplies the policies that decide whether work may run. It is the composition root: application-specific services, presenters, policies, and pages should arrive there from the consumer, not through global lookup inside the shared package. Pages are owned by the consumer and are ordinary Textual widgets. The shell mounts them once, activates and deactivates them as the operator moves between tabs, and preserves page-local state. A page receives a narrow `PageContext`; it does not receive the whole app. @@ -24,7 +24,7 @@ Pages render package-owned view models such as `CatalogueItem`, `TableView`, `Tr A setup page should answer a concrete operator question: "can this environment do the work I am about to ask of it?" -The page should normally live in the consumer package and use consumer services to inspect the environment. `groundskeeping` supplies the rendering and action contracts; it should not know what "ready" means for Groundworkers, CAVA, or any future application. +The page should normally live in the consumer package and use consumer services to inspect the environment. `groundskeeping` supplies rendering and action contracts; it should not know what "ready" means for a particular application. A good setup page usually has: @@ -49,7 +49,7 @@ A shell job is work launched by this TUI process. It is not a durable processing `groundskeeping.configurator` understands the public shape of `oa-configurator` stack configuration well enough to inspect and present it safely. It can build snapshots, section views, drafts, redacted diffs, and apply intents. -It does not write TOML. Persistence belongs to the public `oa-configurator` mutation API and to the consumer's operation policy. That separation protects comments, secrets, external edits, and tenant-specific safety rules. +It does not write TOML. Persistence belongs to the public `oa-configurator` mutation API and to the consumer's operation policy. That separation protects comments, secrets, external edits, and environment-specific safety rules. Consumer applications can add `ConfigResourceAdapter` implementations for resource types that need better labels, choices, validation, verification, or post-apply effects. @@ -59,9 +59,9 @@ Telemetry has a headless core and Textual widgets layered above it. `groundskeeping.telemetry` contains source protocols, availability, normalized metrics, snapshots, and sampling runtime. It must remain free of Textual imports so collectors can be tested and reused outside a running app. -`groundskeeping.widgets.telemetry` renders snapshots. Widgets should bind to metric keys and capabilities, not concrete provider classes. A GPU card, for example, should care about accelerator utilisation and memory metrics; it should not need to know whether the source is NVIDIA, Apple Silicon, or something added later. +`groundskeeping.widgets.telemetry` renders snapshots. Widgets should bind to metric keys and capabilities, not concrete provider classes. An accelerator card, for example, should care about utilisation and memory metrics; it should not need to know which collector produced them. -Consumers own domain telemetry: queue depth, pipeline progress, database state, workload throughput, and tuning interpretation. +Consumers own domain telemetry: queue depth, pipeline progress, database state, workload throughput, tuning interpretation, and any other application-specific signal. ## Ownership Boundary diff --git a/src/groundskeeping/themes/groundskeeping.tcss b/src/groundskeeping/themes/groundskeeping.tcss index ef26e84..7282ce1 100644 --- a/src/groundskeeping/themes/groundskeeping.tcss +++ b/src/groundskeeping/themes/groundskeeping.tcss @@ -39,6 +39,8 @@ Screen { .operator-page.-active { display: block; + height: 0; + max-height: 0; } .section { @@ -60,13 +62,19 @@ Screen { height: 1fr; } +#workbench-main { + height: 1fr; +} + #catalogue-panel { width: 36; max-width: 40%; + height: 1fr; } #workbench-right { width: 1fr; + height: 1fr; } #result-panel { diff --git a/src/groundskeeping/widgets/workbench.py b/src/groundskeeping/widgets/workbench.py index ae176f3..97729dd 100644 --- a/src/groundskeeping/widgets/workbench.py +++ b/src/groundskeeping/widgets/workbench.py @@ -39,20 +39,21 @@ def __init__(self) -> None: self._loading_view: LoadingView | None = None def compose(self) -> ComposeResult: - with Vertical(id="catalogue-panel", classes="section"): - yield Tree("Catalogue", id="catalogue") - with Vertical(id="workbench-right"): - with Vertical(id="result-panel", classes="section"): - with Horizontal(id="result-header"): - yield Static("", id="result-status") - yield Static("Select a catalogue item to inspect it.", id="result-summary") - yield DataTable(id="result-table") - yield Tree("Result details", id="result-tree") - yield EmptyState("", id="result-empty") - yield LoadingState("", id="result-loading") - with Vertical(id="context-panel", classes="section"): - yield TextArea(id="context") - yield DataTable(id="context-table") + with Horizontal(id="workbench-main"): + with Vertical(id="catalogue-panel", classes="section"): + yield Tree("Catalogue", id="catalogue") + with Vertical(id="workbench-right"): + with Vertical(id="result-panel", classes="section"): + with Horizontal(id="result-header"): + yield Static("", id="result-status") + yield Static("Select a catalogue item to inspect it.", id="result-summary") + yield DataTable(id="result-table") + yield Tree("Result details", id="result-tree") + yield EmptyState("", id="result-empty") + yield LoadingState("", id="result-loading") + with Vertical(id="context-panel", classes="section"): + yield TextArea(id="context") + yield DataTable(id="context-table") def on_mount(self) -> None: self.query_one("#catalogue-panel").border_title = "Catalogue" diff --git a/tests/test_app_smoke.py b/tests/test_app_smoke.py index 8c2c6d0..a93c58c 100644 --- a/tests/test_app_smoke.py +++ b/tests/test_app_smoke.py @@ -13,6 +13,19 @@ async def run() -> None: async with app.run_test() as pilot: assert app.registry.keys() == ("overview", "config", "telemetry") assert app.query_one("#workspace-title") is not None + assert app.query_one(".operator-page.-active").region.height == 0 + assert app.query_one("#workbench").region.height > 0 + catalogue_panel = app.query_one("#catalogue-panel") + workbench_right = app.query_one("#workbench-right") + result_panel = app.query_one("#result-panel") + context_panel = app.query_one("#context-panel") + + assert catalogue_panel.region.height > 0 + assert workbench_right.region.height > 0 + assert catalogue_panel.region.x < workbench_right.region.x + assert catalogue_panel.region.y == workbench_right.region.y + assert result_panel.region.y < context_panel.region.y + assert result_panel.region.x == context_panel.region.x == workbench_right.region.x await pilot.press("q") asyncio.run(run())