feat(foundry): productionize Cesium Map Page and platform runtime
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { forwardRef, lazy, Suspense, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, type CSSProperties, type PointerEvent } from "react";
|
||||
import { Button, Checker, ColorField, ControlRow, GlassSurface, Icon, IconButton, Inspector, RangeControl, Window } from "@nodedc/ui-react";
|
||||
import type { MapCameraView, MapGatewayHealth, MapPresentation } from "./CesiumMapRenderer.js";
|
||||
import type { MapCameraView, MapGatewayHealth, MapPresentation, MapProviderStatus } from "./CesiumMapRenderer.js";
|
||||
import { mapRuntimeEntityId, useMapDataProductRuntime } from "./useMapDataProductRuntime.js";
|
||||
import sceneFixture from "../../../registry/fixtures/map/map-operational-v0.1.json";
|
||||
|
||||
const CesiumMapRenderer = lazy(() => import("./CesiumMapRenderer.js").then((module) => ({ default: module.CesiumMapRenderer })));
|
||||
@@ -8,6 +9,67 @@ const CesiumMapRenderer = lazy(() => import("./CesiumMapRenderer.js").then((modu
|
||||
type PreviewFeatures = { inspector?: boolean; toolbar?: boolean; assistant?: boolean };
|
||||
export type MapPageSettings = Omit<MapPresentation, "cacheRefresh">;
|
||||
type MapRuntimeConfig = { gatewayHealthUrl?: string | null; resourceProxyBase?: string | null };
|
||||
type GatewayCheckState = "idle" | "checking" | "ready" | "stale" | "error";
|
||||
type GatewayHealthOrder = { nextEpoch: number; latestStartedEpoch: number };
|
||||
const RENDERER_GATEWAY_HEALTH_EPOCH = 1;
|
||||
|
||||
function beginGatewayHealthEpoch(order: GatewayHealthOrder) {
|
||||
order.nextEpoch += 1;
|
||||
order.latestStartedEpoch = order.nextEpoch;
|
||||
return order.nextEpoch;
|
||||
}
|
||||
|
||||
function isLatestGatewayHealthEpoch(order: GatewayHealthOrder, epoch: number) {
|
||||
return order.latestStartedEpoch === epoch;
|
||||
}
|
||||
|
||||
function safeGatewayCheckCode(value: unknown, fallback = "gateway_not_ready") {
|
||||
const code = value instanceof Error && value.message ? value.message : fallback;
|
||||
return code.replace(/[^A-Za-z0-9_.:-]/g, "_").slice(0, 80) || fallback;
|
||||
}
|
||||
|
||||
function gatewayCheckMessage(code: string, stale: boolean) {
|
||||
if (code === "persistent_cache_unavailable") {
|
||||
return "Persistent TileCache не подключён: карта не должна продолжать работу с локальной временной папкой.";
|
||||
}
|
||||
const prefix = stale ? "Текущая проверка не прошла" : "Проверка Platform Map Gateway не прошла";
|
||||
const suffix = stale
|
||||
? "Показаны последние успешно полученные данные TileCache."
|
||||
: "TileCache и runtime profile не изменялись.";
|
||||
return `${prefix} (${code}). ${suffix}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider-neutral visual binding. Foundry stores this on an Application page
|
||||
* instance; a future data binding resolves the live source behind `source`.
|
||||
*/
|
||||
export type MapPinBinding = {
|
||||
id: string;
|
||||
subjectId: string;
|
||||
kind: "elevated-spike";
|
||||
label: string;
|
||||
status: string;
|
||||
coordinates: { longitude: number; latitude: number; heightMeters: number };
|
||||
source: { entityId: string; streamId: string; displayFields: string[] };
|
||||
attributes: Record<string, string | number | boolean>;
|
||||
};
|
||||
|
||||
/**
|
||||
* A renderer-neutral declaration of an entity stream assigned to this page.
|
||||
*
|
||||
* It intentionally contains no provider endpoint, tenant/connection scope,
|
||||
* credential reference, or browser token. The Foundry runtime resolves the
|
||||
* matching server-side consumer grant from the application/page/binding
|
||||
* target before it asks the External Data Plane for a snapshot or patches.
|
||||
*/
|
||||
export type MapDataProductBinding = {
|
||||
id: string;
|
||||
dataProductId: string;
|
||||
slotId: string;
|
||||
delivery: "snapshot+patch";
|
||||
semanticTypes: string[];
|
||||
fieldProjection: string[];
|
||||
};
|
||||
|
||||
export type MapPageLayout = {
|
||||
schemaVersion: 1;
|
||||
@@ -15,6 +77,8 @@ export type MapPageLayout = {
|
||||
settings: MapPageSettings;
|
||||
mapHeight: number;
|
||||
camera: MapCameraView;
|
||||
pinBindings: MapPinBinding[];
|
||||
dataProductBindings: MapDataProductBinding[];
|
||||
savedAt?: string;
|
||||
};
|
||||
|
||||
@@ -26,6 +90,7 @@ const initialMapSettings: MapPageSettings = {
|
||||
imagerySource: "cesium-live",
|
||||
imageryVisible: true,
|
||||
cacheEnabled: true,
|
||||
cacheNoOverwrite: true,
|
||||
terrainEnabled: true,
|
||||
terrainExaggeration: 1,
|
||||
monochrome: false,
|
||||
@@ -82,23 +147,73 @@ const fallbackMapCamera: MapCameraView = {
|
||||
roll: 0,
|
||||
};
|
||||
|
||||
const initialProviderStatus: MapProviderStatus = {
|
||||
imagery: "loading",
|
||||
terrain: "loading",
|
||||
buildings: "loading",
|
||||
errors: {},
|
||||
};
|
||||
|
||||
const providerStateLabel: Record<MapProviderStatus["imagery"], string> = {
|
||||
loading: "загружается",
|
||||
ready: "готов",
|
||||
error: "недоступен",
|
||||
"not-configured": "не настроен",
|
||||
};
|
||||
|
||||
export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||
features?: PreviewFeatures;
|
||||
expanded?: boolean;
|
||||
initialLayout?: MapPageLayout | null;
|
||||
}>(function MapFixturePreview({ features = { inspector: true, toolbar: true, assistant: false }, expanded = false, initialLayout = null }, ref) {
|
||||
const selectable = useMemo(() => [
|
||||
applicationId?: string;
|
||||
pageId?: string;
|
||||
}>(function MapFixturePreview({ features = { inspector: true, toolbar: true, assistant: false }, expanded = false, initialLayout = null, applicationId, pageId }, ref) {
|
||||
const fixtureSelectable = useMemo(() => [
|
||||
...sceneFixture.scene.movingObjects.map((entity) => ({ id: entity.id, title: entity.label.text, kind: entity.objectType, status: entity.status })),
|
||||
...sceneFixture.scene.stations.map((entity) => ({ id: entity.id, title: entity.label.text, kind: `${entity.stationType} station`, status: undefined })),
|
||||
], []);
|
||||
const [selectedId, setSelectedId] = useState(sceneFixture.selection.entityId ?? selectable[0]?.id);
|
||||
const [selectedId, setSelectedId] = useState(sceneFixture.selection.entityId ?? fixtureSelectable[0]?.id);
|
||||
const [inspectorOpen, setInspectorOpen] = useState(false);
|
||||
const [layersOpen, setLayersOpen] = useState(false);
|
||||
const [toolbarOpen, setToolbarOpen] = useState(Boolean(features.toolbar));
|
||||
const [assistantOpen, setAssistantOpen] = useState(false);
|
||||
const [mapSettings, setMapSettings] = useState<MapPageSettings>(() => ({ ...initialMapSettings, ...initialLayout?.settings }));
|
||||
const [mapSettings, setMapSettings] = useState<MapPageSettings>(() => ({
|
||||
...initialMapSettings,
|
||||
...initialLayout?.settings,
|
||||
// Layouts saved before the cache policy field existed retain the safe
|
||||
// append-only default when they are opened again.
|
||||
cacheNoOverwrite: initialLayout?.settings?.cacheNoOverwrite ?? true,
|
||||
}));
|
||||
const [mapHeight, setMapHeight] = useState(() => initialLayout?.mapHeight ?? (expanded ? 620 : 470));
|
||||
const [mapCamera, setMapCamera] = useState<MapCameraView>(initialLayout?.camera ?? fallbackMapCamera);
|
||||
// Map pin bindings belong to the application page instance. They are kept
|
||||
// intact when a human changes camera or visual settings and presses Save.
|
||||
const [pinBindings] = useState<MapPinBinding[]>(() => initialLayout?.pinBindings ?? []);
|
||||
// Data-product bindings are provisioned by Foundry MCP / Platform and do
|
||||
// not belong to the visual inspector. Preserve them verbatim when a human
|
||||
// edits camera or presentation settings and saves the page layout.
|
||||
const [dataProductBindings] = useState<MapDataProductBinding[]>(() => initialLayout?.dataProductBindings ?? []);
|
||||
const runtimeBindings = useMapDataProductRuntime({
|
||||
applicationId,
|
||||
pageId,
|
||||
bindings: dataProductBindings,
|
||||
enabled: Boolean(applicationId && pageId),
|
||||
});
|
||||
const selectable = useMemo(() => [
|
||||
...fixtureSelectable,
|
||||
...runtimeBindings.flatMap((binding) => binding.facts.map((fact) => {
|
||||
const attributes = fact.attributes;
|
||||
const label = [attributes.label, attributes.name, attributes.title, attributes.subject_id]
|
||||
.find((value) => typeof value === "string" && value.trim());
|
||||
const status = typeof attributes.status === "string" ? attributes.status : undefined;
|
||||
return {
|
||||
id: mapRuntimeEntityId(binding.bindingId, fact),
|
||||
title: typeof label === "string" ? label : fact.sourceId,
|
||||
kind: fact.semanticType,
|
||||
status,
|
||||
};
|
||||
})),
|
||||
], [fixtureSelectable, runtimeBindings]);
|
||||
// The header Save action can be pressed immediately after Cesium finishes
|
||||
// constructing the scene. Keep the last camera synchronously as well as in
|
||||
// state, so the imperative page-layout contract never waits for React's
|
||||
@@ -106,16 +221,42 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||
const mapCameraRef = useRef<MapCameraView>(initialLayout?.camera ?? fallbackMapCamera);
|
||||
const [rendererRevision, setRendererRevision] = useState(0);
|
||||
const [gatewayHealth, setGatewayHealth] = useState<MapGatewayHealth | null>(null);
|
||||
const gatewayHealthRef = useRef<MapGatewayHealth | null>(null);
|
||||
// The renderer owns epoch 1 as a bootstrap health source. As soon as the UI
|
||||
// starts an explicit verification, its higher epoch becomes authoritative;
|
||||
// a slower renderer request can no longer overwrite that newer result.
|
||||
const gatewayHealthOrderRef = useRef<GatewayHealthOrder>({
|
||||
nextEpoch: RENDERER_GATEWAY_HEALTH_EPOCH,
|
||||
latestStartedEpoch: RENDERER_GATEWAY_HEALTH_EPOCH,
|
||||
});
|
||||
const gatewayCheckRequestRef = useRef<{ id: symbol; controller: AbortController; promise: Promise<void> } | null>(null);
|
||||
const [gatewayEndpoint, setGatewayEndpoint] = useState<string | null>(null);
|
||||
const [gatewayCheckState, setGatewayCheckState] = useState<"idle" | "checking" | "ready" | "error">("idle");
|
||||
const [gatewayCheckState, setGatewayCheckState] = useState<GatewayCheckState>("idle");
|
||||
const [gatewayCheckError, setGatewayCheckError] = useState<string | null>(null);
|
||||
const [gatewayLastVerifiedAt, setGatewayLastVerifiedAt] = useState<Date | null>(null);
|
||||
const [providerStatus, setProviderStatus] = useState<MapProviderStatus>(initialProviderStatus);
|
||||
const [cacheRefresh, setCacheRefresh] = useState(false);
|
||||
const selected = selectable.find((entity) => entity.id === selectedId) ?? selectable[0];
|
||||
const presentation: MapPresentation = { ...mapSettings, cacheRefresh: false };
|
||||
const presentation: MapPresentation = { ...mapSettings, cacheRefresh };
|
||||
const updateMapSettings = (patch: Partial<MapPageSettings>) => setMapSettings((current) => ({ ...current, ...patch }));
|
||||
const setCacheEnabled = (cacheEnabled: boolean) => {
|
||||
updateMapSettings({ cacheEnabled });
|
||||
setRendererRevision((value) => value + 1);
|
||||
};
|
||||
const setCacheNoOverwrite = (cacheNoOverwrite: boolean) => {
|
||||
updateMapSettings({ cacheNoOverwrite });
|
||||
setCacheRefresh(false);
|
||||
setRendererRevision((value) => value + 1);
|
||||
};
|
||||
const refreshCurrentViewport = () => {
|
||||
if (!mapSettings.cacheEnabled) return;
|
||||
setCacheRefresh(true);
|
||||
setRendererRevision((value) => value + 1);
|
||||
};
|
||||
const handleCacheRefreshConsumed = useCallback(() => {
|
||||
setCacheRefresh(false);
|
||||
setRendererRevision((value) => value + 1);
|
||||
}, []);
|
||||
|
||||
const handleCameraChange = useCallback((camera: MapCameraView) => {
|
||||
mapCameraRef.current = camera;
|
||||
@@ -129,50 +270,132 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||
settings: mapSettings,
|
||||
mapHeight: Math.round(mapHeight),
|
||||
camera: mapCameraRef.current ?? mapCamera,
|
||||
pinBindings,
|
||||
dataProductBindings,
|
||||
}),
|
||||
}), [mapCamera, mapHeight, mapSettings]);
|
||||
}), [dataProductBindings, mapCamera, mapHeight, mapSettings, pinBindings]);
|
||||
|
||||
const handleSelect = useCallback((entityId: string) => {
|
||||
if (!selectable.some((entity) => entity.id === entityId)) return;
|
||||
setSelectedId(entityId);
|
||||
}, [selectable]);
|
||||
|
||||
const verifyGateway = useCallback(async () => {
|
||||
const rememberGatewayHealth = useCallback((health: MapGatewayHealth) => {
|
||||
gatewayHealthRef.current = health;
|
||||
setGatewayHealth(health);
|
||||
setGatewayLastVerifiedAt(new Date());
|
||||
}, []);
|
||||
|
||||
const beginGatewayHealthVerification = useCallback(() => {
|
||||
return beginGatewayHealthEpoch(gatewayHealthOrderRef.current);
|
||||
}, []);
|
||||
|
||||
const isLatestGatewayHealthRequest = useCallback((epoch: number) => (
|
||||
isLatestGatewayHealthEpoch(gatewayHealthOrderRef.current, epoch)
|
||||
), []);
|
||||
|
||||
const handleRendererGatewayHealth = useCallback((health: MapGatewayHealth | null) => {
|
||||
if (!isLatestGatewayHealthRequest(RENDERER_GATEWAY_HEALTH_EPOCH)) return;
|
||||
if (health?.cache?.persistent === true) {
|
||||
rememberGatewayHealth(health);
|
||||
setGatewayCheckError(null);
|
||||
setGatewayCheckState("ready");
|
||||
return;
|
||||
}
|
||||
const stale = Boolean(gatewayHealthRef.current);
|
||||
const code = health ? "persistent_cache_unavailable" : "gateway_health_unavailable";
|
||||
setGatewayCheckError(gatewayCheckMessage(code, stale));
|
||||
setGatewayCheckState(stale ? "stale" : "error");
|
||||
}, [isLatestGatewayHealthRequest, rememberGatewayHealth]);
|
||||
|
||||
const verifyGateway = useCallback(() => {
|
||||
const pending = gatewayCheckRequestRef.current;
|
||||
if (pending) return pending.promise;
|
||||
const epoch = beginGatewayHealthVerification();
|
||||
const id = Symbol("gateway-check");
|
||||
const controller = new AbortController();
|
||||
let timedOut = false;
|
||||
let stage: "runtime" | "gateway_health" = "runtime";
|
||||
setGatewayCheckState("checking");
|
||||
setGatewayCheckError(null);
|
||||
try {
|
||||
const runtimeResponse = await fetch("/api/map/runtime-config");
|
||||
const runtime = runtimeResponse.ok ? await runtimeResponse.json() as MapRuntimeConfig : null;
|
||||
if (!runtime?.gatewayHealthUrl) throw new Error("gateway_not_configured");
|
||||
const healthResponse = await fetch(runtime.gatewayHealthUrl);
|
||||
if (!healthResponse.ok) throw new Error("gateway_not_ready");
|
||||
const health = await healthResponse.json() as MapGatewayHealth;
|
||||
if (health.cache?.persistent !== true) throw new Error("persistent_cache_unavailable");
|
||||
setGatewayHealth(health);
|
||||
setGatewayEndpoint(new URL(runtime.gatewayHealthUrl).origin);
|
||||
setGatewayCheckState("ready");
|
||||
} catch (error) {
|
||||
const code = error instanceof Error ? error.message : "gateway_not_ready";
|
||||
setGatewayHealth(null);
|
||||
setGatewayEndpoint(null);
|
||||
setGatewayCheckError(code === "persistent_cache_unavailable"
|
||||
? "Persistent TileCache не подключён: карта не должна продолжать работу с локальной временной папкой."
|
||||
: "Platform Map Gateway недоступен: проверьте runtime profile и persistent volume.");
|
||||
setGatewayCheckState("error");
|
||||
}
|
||||
}, []);
|
||||
const timeout = window.setTimeout(() => {
|
||||
timedOut = true;
|
||||
controller.abort();
|
||||
}, 10_000);
|
||||
const promise = (async () => {
|
||||
try {
|
||||
const runtimeResponse = await fetch("/api/map/runtime-config", { cache: "no-store", signal: controller.signal });
|
||||
if (!runtimeResponse.ok) throw new Error(`runtime_http_${runtimeResponse.status}`);
|
||||
let runtime: MapRuntimeConfig;
|
||||
try {
|
||||
runtime = await runtimeResponse.json() as MapRuntimeConfig;
|
||||
} catch {
|
||||
throw new Error("runtime_invalid_response");
|
||||
}
|
||||
if (!runtime?.gatewayHealthUrl) throw new Error("gateway_not_configured");
|
||||
stage = "gateway_health";
|
||||
const healthResponse = await fetch(runtime.gatewayHealthUrl, { cache: "no-store", signal: controller.signal });
|
||||
if (!healthResponse.ok) throw new Error(`gateway_health_http_${healthResponse.status}`);
|
||||
let health: MapGatewayHealth;
|
||||
try {
|
||||
health = await healthResponse.json() as MapGatewayHealth;
|
||||
} catch {
|
||||
throw new Error("gateway_health_invalid_response");
|
||||
}
|
||||
if (health.cache?.persistent !== true) throw new Error("persistent_cache_unavailable");
|
||||
if (!isLatestGatewayHealthRequest(epoch)) return;
|
||||
rememberGatewayHealth(health);
|
||||
// Runtime configuration deliberately exposes a same-origin relative
|
||||
// route. Resolve it against the current browser origin before displaying
|
||||
// the connection; new URL("/api/…") without a base throws and used to
|
||||
// turn a healthy Gateway into a false "unavailable" state.
|
||||
setGatewayEndpoint(new URL(runtime.gatewayHealthUrl, window.location.origin).origin);
|
||||
setGatewayCheckState("ready");
|
||||
} catch (error) {
|
||||
if (controller.signal.aborted && !timedOut) return;
|
||||
if (!isLatestGatewayHealthRequest(epoch)) return;
|
||||
const code = timedOut
|
||||
? "gateway_check_timeout"
|
||||
: error instanceof TypeError
|
||||
? `${stage}_network_error`
|
||||
: safeGatewayCheckCode(error);
|
||||
const stale = Boolean(gatewayHealthRef.current);
|
||||
if (!stale) setGatewayEndpoint(null);
|
||||
setGatewayCheckError(gatewayCheckMessage(code, stale));
|
||||
setGatewayCheckState(stale ? "stale" : "error");
|
||||
} finally {
|
||||
window.clearTimeout(timeout);
|
||||
if (gatewayCheckRequestRef.current?.id === id) gatewayCheckRequestRef.current = null;
|
||||
}
|
||||
})();
|
||||
gatewayCheckRequestRef.current = { id, controller, promise };
|
||||
return promise;
|
||||
}, [beginGatewayHealthVerification, isLatestGatewayHealthRequest, rememberGatewayHealth]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!inspectorOpen && !layersOpen) return;
|
||||
void verifyGateway();
|
||||
const interval = window.setInterval(() => void verifyGateway(), 5000);
|
||||
return () => window.clearInterval(interval);
|
||||
const interval = window.setInterval(() => void verifyGateway(), 15_000);
|
||||
return () => {
|
||||
window.clearInterval(interval);
|
||||
const pending = gatewayCheckRequestRef.current;
|
||||
if (pending) {
|
||||
gatewayCheckRequestRef.current = null;
|
||||
pending.controller.abort();
|
||||
}
|
||||
};
|
||||
}, [inspectorOpen, layersOpen, verifyGateway]);
|
||||
|
||||
const liveCacheStatus = gatewayHealth?.cache;
|
||||
const liveCacheSummary = liveCacheStatus
|
||||
? `${liveCacheStatus.entries ?? 0} объектов · ${Math.round((liveCacheStatus.bytes ?? 0) / 1024 / 1024)} MB`
|
||||
? `${liveCacheStatus.entries ?? 0} объектов · ${Math.round((liveCacheStatus.bytes ?? 0) / 1024 / 1024)} / ${Math.round((liveCacheStatus.maxBytes ?? 0) / 1024 / 1024) || "?"} MB`
|
||||
: "индекс ещё не получен";
|
||||
const transportDiagnostic = gatewayHealth?.diagnostics?.lastFailure
|
||||
? `Последняя transport-ошибка: ${gatewayHealth.diagnostics.lastFailure}${gatewayHealth.diagnostics.lastFailureAt ? ` · ${new Date(gatewayHealth.diagnostics.lastFailureAt).toLocaleTimeString()}` : ""}`
|
||||
: null;
|
||||
const gatewayHealthAge = gatewayCheckState === "stale" && gatewayLastVerifiedAt
|
||||
? `Последняя успешная проверка: ${gatewayLastVerifiedAt.toLocaleTimeString()}`
|
||||
: null;
|
||||
|
||||
const startResize = (event: PointerEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
@@ -201,6 +424,8 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||
content: <>
|
||||
<ControlRow label="Подложка"><strong>Cesium World Imagery</strong></ControlRow>
|
||||
<small className="catalog-map-inspector__note">Текущий официальный provider. Другие provider-слои появятся только после отдельного asset-контракта Platform.</small>
|
||||
<ControlRow label="Live providers"><span>Imagery: {providerStateLabel[providerStatus.imagery]} · Terrain: {providerStateLabel[providerStatus.terrain]} · 3D: {providerStateLabel[providerStatus.buildings]}</span></ControlRow>
|
||||
{Object.entries(providerStatus.errors).map(([provider, error]) => <small key={provider} className="catalog-map-inspector__note catalog-map-inspector__note--error" role="alert">{provider}: {error}</small>)}
|
||||
<Checker checked={mapSettings.terrainEnabled} label="Terrain" description="Рельеф — отдельный слой под imagery." onChange={(terrainEnabled) => updateMapSettings({ terrainEnabled })} />
|
||||
<RangeControl label="Вертикальное преувеличение рельефа" value={mapSettings.terrainExaggeration * 100} min={25} max={300} formatValue={(value) => `${(value / 100).toFixed(2)}×`} onChange={(value) => updateMapSettings({ terrainExaggeration: value / 100 })} />
|
||||
<Checker checked={mapSettings.monochrome} label="Монохромная поверхность" onChange={(monochrome) => updateMapSettings({ monochrome })} />
|
||||
@@ -275,16 +500,22 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||
description: "Platform Map Gateway",
|
||||
group: "Хранение",
|
||||
content: <>
|
||||
<small className="catalog-map-inspector__note">По умолчанию — официальный live-маршрут. Включите запись, чтобы одновременно смотреть карту и пополнять серверный cache.</small>
|
||||
<small className="catalog-map-inspector__note">Общий persistent cache Platform: он не принадлежит приложению, странице или пользователю.</small>
|
||||
<Checker className="catalog-map-inspector__cache-toggle" checked={mapSettings.cacheEnabled} label="Кэшировать live-данные" onChange={setCacheEnabled} />
|
||||
<ControlRow label="Режим"><strong>{mapSettings.cacheEnabled ? "Live + Cache" : "Live"}</strong></ControlRow>
|
||||
<Checker checked={mapSettings.cacheNoOverwrite} disabled={!mapSettings.cacheEnabled} label="Не перезаписывать уже полученный cache" description="Cache hit отдаётся как есть; новый tile записывается только при miss." onChange={setCacheNoOverwrite} />
|
||||
<ControlRow label="Режим"><strong>{mapSettings.cacheEnabled ? mapSettings.cacheNoOverwrite ? "Live + Cache · append-only" : "Live + Cache · обновление разрешено" : "Live без persistent cache"}</strong></ControlRow>
|
||||
<ControlRow label="Хранилище"><strong>Platform Map Gateway</strong></ControlRow>
|
||||
<ControlRow label="Подключение"><span>{gatewayEndpoint ?? "runtime profile · не проверено"}</span></ControlRow>
|
||||
<ControlRow label="Записано"><strong>{liveCacheSummary}</strong></ControlRow>
|
||||
<ControlRow label="Политика"><strong>{gatewayHealth?.cache?.writePolicy ?? "append-only · проверяется"}</strong></ControlRow>
|
||||
<Button variant="secondary" shape="pill" icon={<Icon name="refresh" />} onClick={refreshCurrentViewport} disabled={!mapSettings.cacheEnabled || cacheRefresh}> {cacheRefresh ? "Обновляем viewport…" : "Обновить текущий viewport"}</Button>
|
||||
<Button variant="secondary" shape="pill" icon={<Icon name="refresh" />} onClick={() => void verifyGateway()} disabled={gatewayCheckState === "checking"}>{gatewayCheckState === "checking" ? "Проверяем Gateway…" : "Проверить подключение"}</Button>
|
||||
<small className="catalog-map-inspector__note">Live Cache: {liveCacheSummary} · {gatewayHealth?.cache?.mode ?? "проверяется"}</small>
|
||||
<small className="catalog-map-inspector__note">{mapSettings.cacheEnabled ? "Hybrid: provider остаётся официальным, новые tiles сохраняются в серверный cache." : "Real-time: provider остаётся официальным, чтение и запись persistent cache выключены."}</small>
|
||||
{gatewayCheckState === "error" ? <small className="catalog-map-inspector__note catalog-map-inspector__note--error" role="alert">{gatewayCheckError}</small> : null}
|
||||
{transportDiagnostic ? <small className="catalog-map-inspector__note catalog-map-inspector__note--error" role="alert">{transportDiagnostic}</small> : null}
|
||||
{gatewayHealthAge ? <small className="catalog-map-inspector__note">{gatewayHealthAge}</small> : null}
|
||||
<small className="catalog-map-inspector__note">{mapSettings.cacheEnabled ? mapSettings.cacheNoOverwrite ? "Новые miss дописываются; при заполнении объёма Gateway продолжит live-маршрут без удаления прежних tiles." : "Новые запросы этого Application могут явно обновлять уже записанные tiles." : "Real-time: provider остаётся официальным, чтение и запись persistent cache выключены."}</small>
|
||||
{gatewayHealth?.cache?.atCapacity ? <small className="catalog-map-inspector__note catalog-map-inspector__note--error" role="alert">TileCache заполнен: новые tiles показываются live, но не записываются. Существующий cache не удаляется.</small> : null}
|
||||
{gatewayCheckState === "error" || gatewayCheckState === "stale" ? <small className="catalog-map-inspector__note catalog-map-inspector__note--error" role="alert">{gatewayCheckError}</small> : null}
|
||||
</>,
|
||||
},
|
||||
{
|
||||
@@ -306,7 +537,7 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||
aria-label="Map Page Cesium adapter"
|
||||
>
|
||||
<Suspense fallback={<div className="catalog-map-fixture__loading">Загрузка карты…</div>}>
|
||||
<CesiumMapRenderer key={rendererRevision} onSelect={handleSelect} onGatewayHealth={setGatewayHealth} onCameraChange={handleCameraChange} initialCamera={mapCamera ?? undefined} presentation={presentation} />
|
||||
<CesiumMapRenderer key={rendererRevision} onSelect={handleSelect} onGatewayHealth={handleRendererGatewayHealth} onProviderStatus={setProviderStatus} onCameraChange={handleCameraChange} onCacheRefreshConsumed={handleCacheRefreshConsumed} initialCamera={mapCamera ?? undefined} presentation={presentation} runtimeBindings={runtimeBindings} />
|
||||
</Suspense>
|
||||
|
||||
<div className="catalog-map-fixture__actions">
|
||||
@@ -319,12 +550,20 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||
{layersOpen ? (
|
||||
<GlassSurface className="catalog-map-fixture__layers" tone="strong" radius="card" padding="sm" aria-label="Настройки слоёв карты">
|
||||
<div className="catalog-map-fixture__layers-head"><strong>Слои карты</strong><IconButton label="Закрыть слои" onClick={() => setLayersOpen(false)}><Icon name="close" /></IconButton></div>
|
||||
<div className="catalog-map-fixture__provider"><strong>Cesium World Imagery</strong><small>официальный live provider</small></div>
|
||||
<div className="catalog-map-fixture__provider">
|
||||
<strong>Cesium World Imagery</strong>
|
||||
<small>официальный live provider · imagery: {providerStateLabel[providerStatus.imagery]} · terrain: {providerStateLabel[providerStatus.terrain]}</small>
|
||||
</div>
|
||||
{Object.entries(providerStatus.errors).map(([provider, error]) => <small key={provider} className="catalog-map-inspector__note catalog-map-inspector__note--error" role="alert">{provider}: {error}</small>)}
|
||||
<Checker checked={mapSettings.terrainEnabled} label="Terrain" onChange={(terrainEnabled) => updateMapSettings({ terrainEnabled })} />
|
||||
<Checker checked={mapSettings.buildingsVisible} label="3D здания" onChange={(buildingsVisible) => updateMapSettings({ buildingsVisible })} />
|
||||
<Checker checked={mapSettings.gridVisible} label="Планетарная сетка" onChange={(gridVisible) => updateMapSettings({ gridVisible })} />
|
||||
<small className="catalog-map-inspector__note">Live Cache: {liveCacheSummary}</small>
|
||||
{transportDiagnostic ? <small className="catalog-map-inspector__note catalog-map-inspector__note--error" role="alert">{transportDiagnostic}</small> : null}
|
||||
{gatewayHealthAge ? <small className="catalog-map-inspector__note">{gatewayHealthAge}</small> : null}
|
||||
{gatewayCheckState === "error" || gatewayCheckState === "stale" ? <small className="catalog-map-inspector__note catalog-map-inspector__note--error" role="alert">{gatewayCheckError}</small> : null}
|
||||
<Checker className="catalog-map-fixture__cache-toggle" checked={mapSettings.cacheEnabled} label="Кэшировать live-данные" onChange={setCacheEnabled} />
|
||||
<Checker className="catalog-map-fixture__cache-toggle" checked={mapSettings.cacheNoOverwrite} disabled={!mapSettings.cacheEnabled} label="Не перезаписывать cache" onChange={setCacheNoOverwrite} />
|
||||
</GlassSurface>
|
||||
) : null}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user