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
@@ -12,10 +12,19 @@ export interface EnvironmentBackground {
fileName: string | null;
}
export interface EnvironmentPage {
headerLabel: string;
eyebrow: string;
title: string;
description: string;
primaryWorkspaceId: string | null;
secondaryWorkspaceId: string | null;
background: EnvironmentBackground;
}
export interface EnvironmentSettings {
revision: number;
headerLabels: Record<RootId, string>;
backgrounds: Record<EnvironmentSurfaceId, EnvironmentBackground>;
pages: Record<EnvironmentSurfaceId, EnvironmentPage>;
}
export interface UploadedEnvironmentMedia {
@@ -30,7 +39,6 @@ export interface UploadedEnvironmentMedia {
const surfaceIds: readonly EnvironmentSurfaceId[] = [
"home",
"center",
"fleet",
"observation",
"missions",
@@ -39,8 +47,6 @@ const surfaceIds: readonly EnvironmentSurfaceId[] = [
"polygon",
];
const rootIds = surfaceIds.filter((value): value is RootId => value !== "home");
function emptyBackground(): EnvironmentBackground {
return {
enabled: false,
@@ -51,15 +57,37 @@ function emptyBackground(): EnvironmentBackground {
};
}
const defaultQuickActions: Record<
EnvironmentSurfaceId,
readonly [string | null, string | null]
> = {
home: ["spatial-scene", "local-device"],
fleet: ["contour-health", "local-device"],
observation: ["spatial-scene", "cameras"],
missions: ["mission-planner", null],
data: ["recordings", "datasets"],
system: ["modules", "integrations"],
polygon: ["lab-archive", null],
};
export function defaultEnvironmentSettings(): EnvironmentSettings {
const rootById = new Map(roots.map((root) => [root.id, root]));
return {
revision: 0,
headerLabels: Object.fromEntries(
roots.map((root) => [root.id, root.label]),
) as Record<RootId, string>,
backgrounds: Object.fromEntries(
surfaceIds.map((surfaceId) => [surfaceId, emptyBackground()]),
) as Record<EnvironmentSurfaceId, EnvironmentBackground>,
pages: Object.fromEntries(surfaceIds.map((surfaceId) => {
const root = surfaceId === "home" ? null : rootById.get(surfaceId) ?? null;
const [primaryWorkspaceId, secondaryWorkspaceId] = defaultQuickActions[surfaceId];
return [surfaceId, {
headerLabel: root?.label ?? "Mission Core",
eyebrow: root?.eyebrow ?? "NODEDC / MISSION CORE",
title: root?.title ?? "Mission Core",
description: root?.statement
?? "Наблюдение, планирование и корректировка миссий в одной модульной рабочей области.",
primaryWorkspaceId,
secondaryWorkspaceId,
background: emptyBackground(),
}];
})) as Record<EnvironmentSurfaceId, EnvironmentPage>,
};
}
@@ -70,7 +98,11 @@ function requireRecord(value: unknown, path: string): Record<string, unknown> {
return value as Record<string, unknown>;
}
function requireString(value: unknown, path: string, nullable = false): string | null {
function requireString(
value: unknown,
path: string,
nullable = false,
): string | null {
if (nullable && value === null) return null;
if (typeof value !== "string" || !value.trim()) {
throw new Error(`${path} должен быть непустой строкой.`);
@@ -80,7 +112,9 @@ function requireString(value: unknown, path: string, nullable = false): string |
function decodeBackground(value: unknown, path: string): EnvironmentBackground {
const record = requireRecord(value, path);
if (typeof record.enabled !== "boolean") throw new Error(`${path}.enabled должен быть boolean.`);
if (typeof record.enabled !== "boolean") {
throw new Error(`${path}.enabled должен быть boolean.`);
}
if (!["file", "url"].includes(String(record.source))) {
throw new Error(`${path}.source не поддерживается.`);
}
@@ -99,9 +133,30 @@ function decodeBackground(value: unknown, path: string): EnvironmentBackground {
};
}
function decodePage(value: unknown, path: string): EnvironmentPage {
const record = requireRecord(value, path);
return {
headerLabel: requireString(record.header_label, `${path}.header_label`)!,
eyebrow: requireString(record.eyebrow, `${path}.eyebrow`)!,
title: requireString(record.title, `${path}.title`)!,
description: requireString(record.description, `${path}.description`)!,
primaryWorkspaceId: requireString(
record.primary_workspace_id,
`${path}.primary_workspace_id`,
true,
),
secondaryWorkspaceId: requireString(
record.secondary_workspace_id,
`${path}.secondary_workspace_id`,
true,
),
background: decodeBackground(record.background, `${path}.background`),
};
}
export function decodeEnvironmentSettings(value: unknown): EnvironmentSettings {
const record = requireRecord(value, "environment");
if (record.schema_version !== "missioncore.operator-environment/v1") {
if (record.schema_version !== "missioncore.operator-environment/v2") {
throw new Error("Версия настроек окружения не поддерживается.");
}
if (
@@ -111,36 +166,35 @@ export function decodeEnvironmentSettings(value: unknown): EnvironmentSettings {
) {
throw new Error("Ревизия настроек окружения некорректна.");
}
const labels = requireRecord(record.header_labels, "environment.header_labels");
const backgrounds = requireRecord(record.backgrounds, "environment.backgrounds");
const pages = requireRecord(record.pages, "environment.pages");
return {
revision: record.revision,
headerLabels: Object.fromEntries(rootIds.map((rootId) => [
rootId,
requireString(labels[rootId], `environment.header_labels.${rootId}`),
])) as Record<RootId, string>,
backgrounds: Object.fromEntries(surfaceIds.map((surfaceId) => [
pages: Object.fromEntries(surfaceIds.map((surfaceId) => [
surfaceId,
decodeBackground(
backgrounds[surfaceId],
`environment.backgrounds.${surfaceId}`,
),
])) as Record<EnvironmentSurfaceId, EnvironmentBackground>,
decodePage(pages[surfaceId], `environment.pages.${surfaceId}`),
])) as Record<EnvironmentSurfaceId, EnvironmentPage>,
};
}
export function encodeEnvironmentSettings(settings: EnvironmentSettings): unknown {
return {
revision: settings.revision,
header_labels: settings.headerLabels,
backgrounds: Object.fromEntries(surfaceIds.map((surfaceId) => {
const background = settings.backgrounds[surfaceId];
pages: Object.fromEntries(surfaceIds.map((surfaceId) => {
const page = settings.pages[surfaceId];
return [surfaceId, {
enabled: background.enabled,
source: background.source,
url: background.url,
media_kind: background.mediaKind,
file_name: background.fileName,
header_label: page.headerLabel,
eyebrow: page.eyebrow,
title: page.title,
description: page.description,
primary_workspace_id: page.primaryWorkspaceId,
secondary_workspace_id: page.secondaryWorkspaceId,
background: {
enabled: page.background.enabled,
source: page.background.source,
url: page.background.url,
media_kind: page.background.mediaKind,
file_name: page.background.fileName,
},
}];
})),
};
@@ -182,11 +236,13 @@ export function cloneEnvironmentSettings(
): EnvironmentSettings {
return {
revision: settings.revision,
headerLabels: { ...settings.headerLabels },
backgrounds: Object.fromEntries(surfaceIds.map((surfaceId) => [
pages: Object.fromEntries(surfaceIds.map((surfaceId) => [
surfaceId,
{ ...settings.backgrounds[surfaceId] },
])) as Record<EnvironmentSurfaceId, EnvironmentBackground>,
{
...settings.pages[surfaceId],
background: { ...settings.pages[surfaceId].background },
},
])) as Record<EnvironmentSurfaceId, EnvironmentPage>,
};
}