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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/).

## [Unreleased]

Make the dashboard follow what each solver can actually do: panels and action buttons are now derived from the solver adapter instead of being shown for every solver.

### Changed
- Dashboard panels and action buttons are derived from what the solver adapter implements, rather than declared: a panel appears when the adapter provides what feeds it (`find_residuals_files`, `list_probe_files`, or a non-empty `compare_kinds` / `performance_columns` / `control_actions`), and the Restart, Stop, control and Open GUI controls follow the same rule. Solvers other than code_saturne lose the panels and buttons they could never feed: code_aster and the stub solver now show Status, Compare, Log Tail and Recent Errors only. code_saturne is unchanged. Adapters can no longer declare `dashboard_panels`, which now raises `TypeError` at import time
- `csauto doctor` reports the panels and capabilities derived for the configured solver

### Fixed
- Requesting a restart on a solver without restart support returned HTTP 500 "Launch error", a client error reported as a server fault; it now returns HTTP 400 naming the solver
- Live control on a solver declaring no control action reported "Invalid action (expected one of [])"; both the API and the CLI now name the solver
- code_aster's Compare panel offered an empty file selector; it now offers `doe_row.csv`

## [0.5.0] - 2026-08-03

Add code_aster as a second supported solver: generate, run, and monitor finite-element campaigns alongside Code_Saturne.
Expand Down
2 changes: 2 additions & 0 deletions csauto/control.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ def control_case(
solver-specific and applied by the adapter.
"""
adapter = adapter or _default_adapter()
if not adapter.control_actions:
raise ValueError(f"Solver {adapter.name!r} does not support live control.")
if action not in adapter.control_actions:
raise ValueError(f"Invalid control action: {action!r} (expected one of {sorted(adapter.control_actions)})")

Expand Down
4 changes: 4 additions & 0 deletions csauto/fastapi_routes/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ def api_control_case(
authorization: str | None = Header(default=None),
) -> dict[str, str]:
ctx.require_auth(x_csauto_token, authorization)
ctx.require_capability("control")
if payload.action not in ctx.adapter.control_actions:
raise ctx.http_exception_cls(
status_code=400, detail=f"Invalid action (expected one of {sorted(ctx.adapter.control_actions)})"
Expand Down Expand Up @@ -380,6 +381,7 @@ def api_open_gui(
authorization: str | None = Header(default=None),
) -> dict[str, str]:
ctx.require_auth(x_csauto_token, authorization)
ctx.require_capability("gui")
case_id, case_dir = ctx.validated_case_dir(payload.case)
if not os.environ.get("DISPLAY"):
raise ctx.http_exception_cls(status_code=500, detail="DISPLAY not set on server")
Expand Down Expand Up @@ -448,6 +450,8 @@ def api_run_case(
payload,
ctx.http_exception_cls,
)
if restart:
ctx.require_capability("restart")

try:
runtime_selection = resolve_runtime(
Expand Down
8 changes: 8 additions & 0 deletions csauto/fastapi_routes/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,14 @@ def require_auth(self, x_csauto_token: str | None = None, authorization: str | N
if request_token(headers) != self.api_token:
raise self.http_exception_cls(status_code=401, detail="Unauthorized")

def require_capability(self, capability: str) -> None:
"""Refuse an action the selected solver cannot perform, before doing any work."""
if capability not in self.adapter.capabilities:
raise self.http_exception_cls(
status_code=400,
detail=f"Solver {self.adapter.name!r} does not support {capability}",
)

def validate_case(self, case: object) -> str:
try:
return validate_case_id(case, self.runs_dir, self.runs_root)
Expand Down
4 changes: 4 additions & 0 deletions csauto/fastapi_routes/observability.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,10 @@ class CompareKindModel(BaseModel):
class AppConfigModel(BaseModel):
solver: str
panels: list[str]
capabilities: list[str]
compare_kinds: list[CompareKindModel]
error_files: list[str]
control_actions: list[str]

class RecentErrorItemModel(BaseModel):
case_id: str
Expand Down Expand Up @@ -144,8 +146,10 @@ def api_app_config(
return {
"solver": ctx.adapter.name,
"panels": list(ctx.adapter.dashboard_panels),
"capabilities": sorted(ctx.adapter.capabilities),
"compare_kinds": [{"value": kind.value, "label": kind.label} for kind in ctx.adapter.compare_kinds],
"error_files": list(ctx.adapter.anomaly_file_names),
"control_actions": sorted(ctx.adapter.control_actions),
}

@app.get("/api/restart_origin", response_model=RestartOriginResponse)
Expand Down
4 changes: 4 additions & 0 deletions csauto/maintenance.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ def add(level: str, message: str) -> None:
)
)

add("ok", f"solver {adapter.name}: panels {', '.join(adapter.dashboard_panels)}")
capability_names = sorted(adapter.capabilities)
add("ok", f"solver {adapter.name}: capabilities {', '.join(capability_names) if capability_names else 'none'}")

solver_bin_name = adapter.native_bin_name
_rt = (runtime or "auto").strip().lower()
# Override legacy flags with explicit runtime when provided
Expand Down
57 changes: 56 additions & 1 deletion csauto/solvers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ class PerfColumn(NamedTuple):

ALL_DASHBOARD_PANELS = ("status", "residuals", "probes", "performance", "compare", "tail", "errors")

CAPABILITY_RESIDUALS = "residuals"
CAPABILITY_PROBES = "probes"
CAPABILITY_PERFORMANCE = "performance"
CAPABILITY_COMPARE = "compare"
CAPABILITY_CONTROL = "control"
CAPABILITY_RESTART = "restart"
CAPABILITY_GUI = "gui"

# Panels any run feeds, whatever the solver: status comes from the registry,
# tail and errors from the csauto.stdout / csauto.stderr launcher logs.
ALWAYS_ON_PANELS = ("status", "tail", "errors")


@runtime_checkable
class SolverAdapter(Protocol):
Expand All @@ -55,6 +67,7 @@ class SolverAdapter(Protocol):
performance_fields: tuple[str, ...]
performance_columns: tuple[PerfColumn, ...]
dashboard_panels: tuple[str, ...]
capabilities: frozenset[str]
compare_kinds: tuple[CompareKind, ...]
default_compare_kind: str
control_actions: frozenset[str]
Expand Down Expand Up @@ -171,10 +184,52 @@ class SolverAdapterBase(ABC):
anomaly_file_names: ClassVar[tuple[str, ...]] = ("csauto.stderr", "csauto.stdout")
cleanup_log_names: ClassVar[frozenset[str]] = frozenset({"csauto.stdout", "csauto.stderr"})
performance_columns: ClassVar[tuple[PerfColumn, ...]] = ()
dashboard_panels: ClassVar[tuple[str, ...]] = ALL_DASHBOARD_PANELS
compare_kinds: ClassVar[tuple[CompareKind, ...]] = ()
control_actions: ClassVar[frozenset[str]] = frozenset()

def __init_subclass__(cls, **kwargs: Any) -> None:
super().__init_subclass__(**kwargs)
for reserved in ("dashboard_panels", "capabilities"):
if reserved in cls.__dict__:
raise TypeError(
f"{cls.__name__} must not declare {reserved!r}: it is derived "
f"from what the adapter implements (see docs/adding-a-solver.md)."
)

def _provides(self, method_name: str) -> bool:
"""True when the adapter defines its own version instead of the empty base default."""
return getattr(type(self), method_name) is not getattr(SolverAdapterBase, method_name)

@property
def capabilities(self) -> frozenset[str]:
"""What this adapter can actually do, derived from what it provides.

Two forms, chosen per capability according to what the UI consumes:
a redefined method, or an existing declaration that is no longer empty.
"""
caps: set[str] = set()
if self._provides("find_residuals_files"):
caps.add(CAPABILITY_RESIDUALS)
if self._provides("list_probe_files"):
caps.add(CAPABILITY_PROBES)
if self._provides("build_restart_args"):
caps.add(CAPABILITY_RESTART)
if self._provides("gui_argv"):
caps.add(CAPABILITY_GUI)
if self.compare_kinds:
caps.add(CAPABILITY_COMPARE)
if self.performance_columns:
caps.add(CAPABILITY_PERFORMANCE)
if self.control_actions:
caps.add(CAPABILITY_CONTROL)
return frozenset(caps)

@property
def dashboard_panels(self) -> tuple[str, ...]:
"""Feedable panels, in ALL_DASHBOARD_PANELS display order."""
caps = self.capabilities
return tuple(panel for panel in ALL_DASHBOARD_PANELS if panel in ALWAYS_ON_PANELS or panel in caps)

@property
def default_compare_kind(self) -> str:
"""The first declared compare kind, so CLI/API defaults follow the UI order."""
Expand Down
7 changes: 6 additions & 1 deletion csauto/solvers/code_aster.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from ..execution import RUNTIME_DOCKER, RUNTIME_NATIVE, RUNTIME_SINGULARITY, RuntimeSelection, shared_dir_symlink_mounts
from ..logs import _is_recent, _parse_start_time, read_tail_lines
from ..registry import STATUS_DONE, STATUS_FAILED
from .base import SolverAdapterBase
from .base import CompareKind, SolverAdapterBase

CODE_ASTER_EXPORT_EXTENSION = "export"

Expand All @@ -34,6 +34,11 @@ class CodeAsterAdapter(SolverAdapterBase):
"MESH",
"RESU",
)
# doe_row.csv is written into every generated case by the generic DOE code,
# so it is comparable whatever the solver. The .export file would be a better
# candidate but its name varies per case (find_setup_file globs *.export)
# while compare_kinds expects fixed names.
compare_kinds: ClassVar[tuple[CompareKind, ...]] = (CompareKind("doe_row.csv", "doe_row.csv"),)

def build_run_command(
self,
Expand Down
37 changes: 32 additions & 5 deletions docs/adding-a-solver.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,39 @@ Then declare the conventions that differ from the base defaults:
do not declare those either.
- `control_actions` — the live-control directives your `apply_control`
implements (code_saturne declares `stop`, `extend`, `checkpoint`, `flush`).
- `dashboard_panels` — which dashboard panels the UI renders. Defaults to all
of them; trim it when a panel cannot be fed by your solver.
## Dashboard panels are derived, not declared

The web UI reads `dashboard_panels`, `compare_kinds`, and `anomaly_file_names`
from `/api/app_config` and the timing columns from `/api/perf`, so these
declarations reshape the dashboard without any frontend change.
You never declare which panels your solver gets: `dashboard_panels` and
`capabilities` are computed from what your adapter actually provides, and
declaring either of them raises `TypeError` at import time.

| Capability | Granted when your adapter |
|---|---|
| `residuals` | overrides `find_residuals_files` |
| `probes` | overrides `list_probe_files` |
| `restart` | overrides `build_restart_args` |
| `gui` | overrides `gui_argv` |
| `compare` | declares a non-empty `compare_kinds` |
| `performance` | declares a non-empty `performance_columns` |
| `control` | declares a non-empty `control_actions` |

The `status`, `tail` and `errors` panels are always present: they are fed by the
registry and by the `csauto.stdout` / `csauto.stderr` launcher logs, which exist
for every solver.

Implement what your solver can feed and the panel appears; implement nothing and
the panel, along with its action buttons, is absent from the dashboard and
refused by the API with a 400. Run `csauto doctor RUNS` to see what was derived:

```
[OK] solver code_aster: panels status, compare, tail, errors
[OK] solver code_aster: capabilities compare
```

The web UI reads `panels`, `capabilities`, `control_actions`, `compare_kinds`
and `anomaly_file_names` from `/api/app_config`, and the timing columns from
`/api/perf`, so these declarations reshape the dashboard without any frontend
change.

Everything else is optional. Useful overrides, from most to least common:

Expand Down
17 changes: 11 additions & 6 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,17 @@ An adapter supplies:
file catalog (`locate_case_file`), results listing, cleanup log names.
- **Analytics**: residuals/probes/performance discovery and parsing (SVG
rendering stays generic in `svg_utils.py`).
- **Dashboard surface**: `dashboard_panels` (which panels the UI renders),
`performance_columns` (Timing Snapshot columns, from which the performance
CSV export keys derive), `compare_kinds` (the compare panel's file list,
whose first entry is the default), and `anomaly_file_names` doubling as the
Recent Errors file list — served to the frontend via `/api/app_config` and
`/api/perf`.
- **Dashboard surface**: derived, not declared. `capabilities` is computed from
what the adapter provides (an overridden `find_residuals_files`,
`list_probe_files`, `build_restart_args` or `gui_argv`, or a non-empty
`compare_kinds`, `performance_columns` or `control_actions`), and
`dashboard_panels` follows from it. Both are served to the frontend by
`/api/app_config` and enforced server side by
`FastAPIContext.require_capability`. An adapter that declares either attribute
raises `TypeError` at import time. `performance_columns` also drives the
Timing Snapshot columns and the performance CSV export keys, `compare_kinds`
the compare panel's file list (first entry is the default), and
`anomaly_file_names` doubles as the Recent Errors file list.
- **Control**: `apply_control` for live stop/extend/checkpoint/flush
directives, with `control_actions` declaring what the solver supports.
- **Doctor**: `doctor_checks` for solver-specific environment validation.
Expand Down
1 change: 1 addition & 0 deletions frontend/dist/_app/immutable/chunks/BI00lc_u.js

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion frontend/dist/_app/immutable/chunks/eeJ2SmtH.js

This file was deleted.

Loading
Loading