feat(control-station): configure page-specific shell
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user