Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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:

Expand All @@ -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.

Expand All @@ -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

Expand Down
8 changes: 8 additions & 0 deletions src/groundskeeping/themes/groundskeeping.tcss
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ Screen {

.operator-page.-active {
display: block;
height: 0;
max-height: 0;
}

.section {
Expand All @@ -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 {
Expand Down
29 changes: 15 additions & 14 deletions src/groundskeeping/widgets/workbench.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
13 changes: 13 additions & 0 deletions tests/test_app_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Loading