feat(control-station): configure page-specific shell

This commit is contained in:
DCCONSTRUCTIONS
2026-07-26 17:06:04 +03:00
parent dc55ff16c9
commit 360ec39ff3
9 changed files with 619 additions and 259 deletions
+160 -47
View File
@@ -16,7 +16,6 @@ from pydantic import BaseModel, ConfigDict, Field, model_validator
EnvironmentSurfaceId = Literal[
"home",
"center",
"fleet",
"observation",
"missions",
@@ -27,8 +26,8 @@ EnvironmentSurfaceId = Literal[
EnvironmentMediaKind = Literal["image", "video"]
EnvironmentMediaSource = Literal["file", "url"]
ENVIRONMENT_SCHEMA_VERSION: Literal["missioncore.operator-environment/v1"] = (
"missioncore.operator-environment/v1"
ENVIRONMENT_SCHEMA_VERSION: Literal["missioncore.operator-environment/v2"] = (
"missioncore.operator-environment/v2"
)
ENVIRONMENT_MEDIA_SCHEMA_VERSION: Literal[
"missioncore.operator-environment-media/v1"
@@ -51,16 +50,6 @@ class StrictApiModel(BaseModel):
model_config = ConfigDict(extra="forbid")
class EnvironmentHeaderLabels(StrictApiModel):
center: str = Field(min_length=1, max_length=40)
fleet: str = Field(min_length=1, max_length=40)
observation: str = Field(min_length=1, max_length=40)
missions: str = Field(min_length=1, max_length=40)
data: str = Field(min_length=1, max_length=40)
system: str = Field(min_length=1, max_length=40)
polygon: str = Field(min_length=1, max_length=40)
class EnvironmentBackground(StrictApiModel):
enabled: bool = False
source: EnvironmentMediaSource = "file"
@@ -83,25 +72,50 @@ class EnvironmentBackground(StrictApiModel):
return self
class EnvironmentBackgrounds(StrictApiModel):
home: EnvironmentBackground
center: EnvironmentBackground
fleet: EnvironmentBackground
observation: EnvironmentBackground
missions: EnvironmentBackground
data: EnvironmentBackground
system: EnvironmentBackground
polygon: EnvironmentBackground
class EnvironmentPage(StrictApiModel):
header_label: str = Field(min_length=1, max_length=40)
eyebrow: str = Field(min_length=1, max_length=80)
title: str = Field(min_length=1, max_length=120)
description: str = Field(min_length=1, max_length=500)
primary_workspace_id: str | None = Field(
default=None,
max_length=80,
pattern=r"^[a-z0-9-]+$",
)
secondary_workspace_id: str | None = Field(
default=None,
max_length=80,
pattern=r"^[a-z0-9-]+$",
)
background: EnvironmentBackground
@model_validator(mode="after")
def validate_quick_actions(self) -> EnvironmentPage:
if (
self.primary_workspace_id is not None
and self.primary_workspace_id == self.secondary_workspace_id
):
raise ValueError("quick actions must target different workspaces")
return self
class EnvironmentPages(StrictApiModel):
home: EnvironmentPage
fleet: EnvironmentPage
observation: EnvironmentPage
missions: EnvironmentPage
data: EnvironmentPage
system: EnvironmentPage
polygon: EnvironmentPage
class EnvironmentSettingsPut(StrictApiModel):
revision: int = Field(ge=0)
header_labels: EnvironmentHeaderLabels
backgrounds: EnvironmentBackgrounds
pages: EnvironmentPages
class EnvironmentSettingsDocument(EnvironmentSettingsPut):
schema_version: Literal["missioncore.operator-environment/v1"] = ENVIRONMENT_SCHEMA_VERSION
schema_version: Literal["missioncore.operator-environment/v2"] = ENVIRONMENT_SCHEMA_VERSION
class EnvironmentMediaDocument(StrictApiModel):
@@ -121,28 +135,128 @@ def default_environment_settings() -> EnvironmentSettingsDocument:
background = EnvironmentBackground()
return EnvironmentSettingsDocument(
revision=0,
header_labels=EnvironmentHeaderLabels(
center="Центр",
fleet="Парк",
observation="Наблюдение",
missions="Миссии",
data="Данные",
system="Система",
polygon="Тестировочный контур",
),
backgrounds=EnvironmentBackgrounds(
home=background.model_copy(),
center=background.model_copy(),
fleet=background.model_copy(),
observation=background.model_copy(),
missions=background.model_copy(),
data=background.model_copy(),
system=background.model_copy(),
polygon=background.model_copy(),
pages=EnvironmentPages(
home=EnvironmentPage(
header_label="Mission Core",
eyebrow="NODEDC / MISSION CORE",
title="Mission Core",
description=(
"Наблюдение, планирование и корректировка миссий "
"в одной модульной рабочей области."
),
primary_workspace_id="spatial-scene",
secondary_workspace_id="local-device",
background=background.model_copy(),
),
fleet=EnvironmentPage(
header_label="Парк",
eyebrow="ПАРК / УСТРОЙСТВА",
title="Аппараты и устройства",
description=(
"Одинаково подключать одиночный стенд, наземную платформу "
"и будущий рой."
),
primary_workspace_id="contour-health",
secondary_workspace_id="local-device",
background=background.model_copy(),
),
observation=EnvironmentPage(
header_label="Наблюдение",
eyebrow="СИТУАЦИОННАЯ ОСВЕДОМЛЁННОСТЬ",
title="Наблюдение",
description=(
"Свести все сенсоры в синхронную и управляемую операторскую картину."
),
primary_workspace_id="spatial-scene",
secondary_workspace_id="cameras",
background=background.model_copy(),
),
missions=EnvironmentPage(
header_label="Миссии",
eyebrow="УПРАВЛЕНИЕ МИССИЯМИ",
title="Миссии",
description=(
"Собрать задачу из точек, ограничений и действий до передачи на борт."
),
primary_workspace_id="mission-planner",
secondary_workspace_id=None,
background=background.model_copy(),
),
data=EnvironmentPage(
header_label="Данные",
eyebrow="РАБОЧАЯ ОБЛАСТЬ ДАННЫХ",
title="Данные и записи",
description=(
"Хранить живой контур и воспроизводимый эксперимент "
"как одну модель данных."
),
primary_workspace_id="recordings",
secondary_workspace_id="datasets",
background=background.model_copy(),
),
system=EnvironmentPage(
header_label="Система",
eyebrow="УПРАВЛЕНИЕ ПЛАТФОРМОЙ",
title="Система",
description=(
"Подключать новые возможности модульно, "
"не связывая интерфейс с одним устройством."
),
primary_workspace_id="modules",
secondary_workspace_id="integrations",
background=background.model_copy(),
),
polygon=EnvironmentPage(
header_label="Тестировочный контур",
eyebrow="ЛАБОРАТОРНЫЕ ИССЛЕДОВАНИЯ",
title="Тестировочный контур",
description=(
"Фиксировать каждый эксперимент как проверяемую лабораторную работу."
),
primary_workspace_id="lab-archive",
secondary_workspace_id=None,
background=background.model_copy(),
),
),
)
def _upgrade_v1_environment(payload: object) -> EnvironmentSettingsDocument:
if not isinstance(payload, dict):
raise ValueError("environment document must be an object")
if payload.get("schema_version") != "missioncore.operator-environment/v1":
return EnvironmentSettingsDocument.model_validate(payload)
revision = payload.get("revision")
labels = payload.get("header_labels")
backgrounds = payload.get("backgrounds")
if not isinstance(revision, int) or not isinstance(labels, dict) or not isinstance(
backgrounds,
dict,
):
raise ValueError("legacy environment document is incomplete")
defaults = default_environment_settings()
pages: dict[str, EnvironmentPage] = {}
for surface_id, default_page in defaults.pages:
legacy_label = labels.get(surface_id)
legacy_background = backgrounds.get(surface_id)
pages[surface_id] = default_page.model_copy(
update={
"header_label": (
legacy_label
if isinstance(legacy_label, str) and legacy_label.strip()
else default_page.header_label
),
"background": EnvironmentBackground.model_validate(
legacy_background
),
},
)
return EnvironmentSettingsDocument(
revision=revision,
pages=EnvironmentPages.model_validate(pages),
)
class EnvironmentSettingsStore:
def __init__(self, root: Path) -> None:
self.root = root.expanduser().resolve()
@@ -156,7 +270,7 @@ class EnvironmentSettingsStore:
return default_environment_settings()
try:
payload = json.loads(self.settings_path.read_text(encoding="utf-8"))
return EnvironmentSettingsDocument.model_validate(payload)
return _upgrade_v1_environment(payload)
except (OSError, ValueError) as exc:
raise RuntimeError("operator environment settings are corrupt") from exc
@@ -167,8 +281,7 @@ class EnvironmentSettingsStore:
raise RuntimeError("operator environment settings revision changed")
document = EnvironmentSettingsDocument(
revision=current.revision + 1,
header_labels=request.header_labels,
backgrounds=request.backgrounds,
pages=request.pages,
)
self.root.mkdir(mode=0o700, parents=True, exist_ok=True)
temporary = self.settings_path.with_suffix(".json.tmp")
@@ -189,7 +302,7 @@ class EnvironmentSettingsStore:
return default_environment_settings()
try:
payload = json.loads(self.settings_path.read_text(encoding="utf-8"))
return EnvironmentSettingsDocument.model_validate(payload)
return _upgrade_v1_environment(payload)
except (OSError, ValueError) as exc:
raise RuntimeError("operator environment settings are corrupt") from exc