feat(foundry): productionize Cesium Map Page and platform runtime

This commit is contained in:
Codex
2026-07-16 02:25:58 +03:00
parent 1a2ca8c82c
commit 5e8c1cc3fc
41 changed files with 6977 additions and 179 deletions
+174 -11
View File
@@ -124,6 +124,27 @@ interface StoredLayout {
};
}
interface FoundrySessionProfile {
user: {
id: string;
email: string;
displayName: string;
avatarUrl: string | null;
initials: string;
} | null;
profileUrl: string | null;
access?: {
role: "admin" | "user";
};
}
interface CesiumIonSecretStatus {
configured: boolean;
updatedAt: string | null;
updatedBy: string | null;
verification?: "verified" | "failed" | "not-configured";
}
const materialDefaults: Record<NodedcTheme, MaterialDraft> = {
dark: {
panelHex: "#151517",
@@ -307,6 +328,12 @@ export function CatalogApp() {
const [applicationDraft, setApplicationDraft] = useState<ApplicationManifestV01 | null>(null);
const [applicationSaveState, setApplicationSaveState] = useState<ApplicationDraftSaveState>("idle");
const [applicationError, setApplicationError] = useState("");
const [sessionProfile, setSessionProfile] = useState<FoundrySessionProfile | null>(null);
const [platformSettingsOpen, setPlatformSettingsOpen] = useState(false);
const [cesiumIonToken, setCesiumIonToken] = useState("");
const [cesiumIonStatus, setCesiumIonStatus] = useState<CesiumIonSecretStatus | null>(null);
const [cesiumIonSaveState, setCesiumIonSaveState] = useState<"idle" | "loading" | "saving" | "saved" | "error">("idle");
const [cesiumIonError, setCesiumIonError] = useState("");
const [mapTemplateLayout, setMapTemplateLayout] = useState<MapPageLayout | null>(null);
const [mapTemplateSaveState, setMapTemplateSaveState] = useState<"idle" | "loading" | "saving" | "saved" | "error">("loading");
const mapTemplatePreviewRef = useRef<MapFixturePreviewHandle>(null);
@@ -355,8 +382,11 @@ export function CatalogApp() {
const [notes, setNotes] = useState("Общий контракт компонентов без доменной логики приложения.");
const [mediaSource, setMediaSource] = useState<"file" | "url">("file");
const [mediaUrl, setMediaUrl] = useState("");
const [mediaFileName, setMediaFileName] = useState("launcher-stage.mp4");
const [fileMediaSrc, setFileMediaSrc] = useState("/launcher-stage.mp4");
// Match the persisted production design-profile default from first paint.
// This prevents the packaged pink sample clip from flashing before
// /api/layout returns the same saved media configuration.
const [mediaFileName, setMediaFileName] = useState("possible shapes.mp4");
const [fileMediaSrc, setFileMediaSrc] = useState("/uploads/1783715822132-possible-shapes.mp4");
const [mediaError, setMediaError] = useState("");
const [mediaVisible, setMediaVisible] = useState(true);
const mediaObjectUrlRef = useRef<string | null>(null);
@@ -431,6 +461,71 @@ export function CatalogApp() {
applyFaviconAssets(faviconAssets);
}, [faviconAssets]);
useEffect(() => {
let active = true;
fetch("/api/session/profile", { cache: "no-store" })
.then((response) => response.ok ? response.json() as Promise<FoundrySessionProfile> : null)
.then((profile) => { if (active) setSessionProfile(profile); })
.catch(() => { if (active) setSessionProfile(null); });
return () => { active = false; };
}, []);
const isFoundryAdmin = sessionProfile?.access?.role === "admin";
const openPlatformSettings = () => {
setPlatformSettingsOpen(true);
setCesiumIonToken("");
setCesiumIonError("");
setCesiumIonSaveState("loading");
void fetch("/api/platform-settings/cesium-ion", { cache: "no-store" })
.then(async (response) => {
if (!response.ok) throw new Error("platform_settings_load_failed");
return await response.json() as CesiumIonSecretStatus;
})
.then((status) => {
setCesiumIonStatus(status);
setCesiumIonSaveState("idle");
})
.catch(() => setCesiumIonSaveState("error"));
};
const closePlatformSettings = () => {
setPlatformSettingsOpen(false);
setCesiumIonToken("");
setCesiumIonError("");
};
const saveCesiumIonToken = async () => {
const token = cesiumIonToken.trim();
if (token.length < 16) return;
setCesiumIonSaveState("saving");
setCesiumIonError("");
try {
const response = await fetch("/api/platform-settings/cesium-ion", {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify({ token }),
});
if (!response.ok) {
const result = await response.json().catch(() => null) as { error?: string } | null;
throw new Error(result?.error || "platform_settings_save_failed");
}
setCesiumIonStatus(await response.json() as CesiumIonSecretStatus);
setCesiumIonToken("");
setCesiumIonSaveState("saved");
} catch (error) {
const code = error instanceof Error ? error.message : "platform_settings_save_failed";
setCesiumIonError(code === "cesium_ion_token_verification_failed"
? "Token не прошёл проверку Cesium Ion для terrain, imagery или 3D Buildings. Значение не сохранено."
: code === "map_gateway_admin_unauthorized"
? "Внутренний signing profile Gateway не совпадает с Foundry. Это исправляется только runner-managed deployment, не вводом значений в интерфейс."
: code === "map_gateway_admin_not_configured"
? "Runner-managed signing file Gateway недоступен."
: "Не удалось применить token. Проверьте права администратора и доступность Gateway.");
setCesiumIonSaveState("error");
}
};
useEffect(() => {
let active = true;
fetch("/api/layout", { cache: "no-store" })
@@ -1291,7 +1386,7 @@ export function CatalogApp() {
return (
<div className="catalog-glass-lab">
<section className="catalog-glass-stage">
<video key={stageMediaSrc} autoPlay muted loop playsInline poster="/launcher-stage-poster.png" aria-hidden="true"><source src={stageMediaSrc} type="video/mp4" /></video>
<video key={stageMediaSrc} autoPlay muted loop playsInline aria-hidden="true"><source src={stageMediaSrc} type="video/mp4" /></video>
<div className="catalog-glass-stage__shade" />
<GlassMaterialSurface className="catalog-glass-stage__sample">
<span>CANONICAL GLASS</span>
@@ -1616,7 +1711,15 @@ export function CatalogApp() {
}));
return (
<div className="catalog-application-page">
<MapFixturePreview key={page.id} ref={applicationMapPreviewRef} initialLayout={page.layout?.map ?? null} features={page.features} expanded />
<MapFixturePreview
key={page.id}
ref={applicationMapPreviewRef}
applicationId={applicationDraft.id}
pageId={page.id}
initialLayout={page.layout?.map ?? null}
features={page.features}
expanded
/>
{applicationMode === "edit" ? (
<SettingsCard eyebrow="PAGE SETTINGS" title={page.title} description={`${template.title} · ${template.version}`}>
<div className="catalog-application-draft__features">
@@ -1652,7 +1755,7 @@ export function CatalogApp() {
<>
<HeaderWorkspace kind="mark" label="NODE.DC Design" imageUrl={headerMarkSrc} />
<HeaderNavigation
label="Рабочая область Module Studio"
label="Рабочая область Module Foundry"
value={guidelineOpen ? studioContext : undefined}
items={[
{ value: "visual", label: "Visual Library" },
@@ -1673,15 +1776,23 @@ export function CatalogApp() {
right={
<HeaderProfile>
<IconButton label="Уведомления"><Icon name="inbox" size={20} strokeWidth={1.7} /></IconButton>
<HeaderProfileButton>Профиль</HeaderProfileButton>
<HeaderAvatar label="DC" />
<HeaderProfileButton
title={sessionProfile?.user?.displayName || "Профиль NODE.DC"}
onClick={() => { if (sessionProfile?.profileUrl) window.location.assign(sessionProfile.profileUrl); }}
>
{sessionProfile?.user?.displayName || "Профиль"}
</HeaderProfileButton>
<HeaderAvatar
label={sessionProfile?.user?.displayName || sessionProfile?.user?.initials || "DC"}
imageUrl={sessionProfile?.user?.avatarUrl || undefined}
/>
</HeaderProfile>
}
/>
}
stage={
<section className="catalog-launcher-stage">
<video key={stageMediaSrc} className="catalog-launcher-stage__media" hidden={!mediaVisible} autoPlay muted loop playsInline poster="/launcher-stage-poster.png" aria-hidden="true">
<video key={stageMediaSrc} className="catalog-launcher-stage__media" hidden={!mediaVisible} autoPlay muted loop playsInline aria-hidden="true">
<source src={stageMediaSrc} type="video/mp4" />
</video>
<div className="catalog-launcher-stage__shade" />
@@ -1696,11 +1807,13 @@ export function CatalogApp() {
<AdminNavigationPanel
eyebrow="NODE.DC"
title={studioContext === "applications" ? "Applications" : studioContext === "pages" ? "Page Library" : "Visual Library"}
closeLabel="Закрыть Module Studio"
closeLabel="Закрыть Module Foundry"
navigationLabel={studioContext === "applications" ? "Application Drafts" : studioContext === "pages" ? "Page Templates" : "Разделы Visual Library"}
onClose={closeGuideline}
headerActions={studioContext === "applications" ? (
<IconButton label="Создать модуль" onClick={() => setCreateModuleOpen(true)}><Icon name="plus" /></IconButton>
) : studioContext === "pages" && isFoundryAdmin ? (
<IconButton label="Настройки Platform" onClick={openPlatformSettings}><Icon name="settings" /></IconButton>
) : undefined}
contextSlot={studioContext === "applications" ? (
<Select
@@ -1746,7 +1859,7 @@ export function CatalogApp() {
footer={
<>
<span className="nodedc-admin-panel__nav-icon" aria-hidden="true"><Icon name={studioContext === "applications" ? "apps" : studioContext === "pages" ? "globe" : "shield"} /></span>
<span>{studioContext === "applications" ? activeApplication ? `${activeApplication.metadata.name} · ${activeApplication.version}` : `${applicationSummaries.length} drafts` : studioContext === "pages" ? `${pageTemplates.length} templates` : "Design System 0.6.0"}</span>
<span>{studioContext === "applications" ? activeApplication ? `${activeApplication.metadata.name} · ${activeApplication.version}` : `${applicationSummaries.length} drafts` : studioContext === "pages" ? `${pageTemplates.length} templates` : "Design Guideline 0.7.0"}</span>
</>
}
/>
@@ -1923,7 +2036,7 @@ export function CatalogApp() {
<ConfirmationModal
open={deleteModuleOpen}
title="Удалить модуль?"
description={<><strong>{applicationDraft?.metadata.name}</strong><p>Draft будет убран из Module Studio. Опубликованные releases эта операция не затрагивает.</p></>}
description={<><strong>{applicationDraft?.metadata.name}</strong><p>Draft будет убран из Module Foundry. Опубликованные releases эта операция не затрагивает.</p></>}
confirmLabel="Удалить draft"
pendingLabel="Удаление…"
danger
@@ -1942,6 +2055,56 @@ export function CatalogApp() {
onConfirm={confirmPageRemoval}
/>
<Window
open={platformSettingsOpen}
title="Настройки Platform"
subtitle="Map Gateway · provider credentials"
size="sm"
onClose={closePlatformSettings}
footer={
<>
<Button variant="ghost" onClick={closePlatformSettings}>Отмена</Button>
<WindowFooterActions>
<Button
variant="primary"
shape="pill"
icon={<Icon name="save" />}
disabled={cesiumIonSaveState === "loading" || cesiumIonSaveState === "saving" || cesiumIonToken.trim().length < 16}
onClick={() => { void saveCesiumIonToken(); }}
>{cesiumIonSaveState === "saving" ? "Применение…" : "Применить"}</Button>
</WindowFooterActions>
</>
}
>
<div className="catalog-form">
<SettingsCard
title="Cesium Ion"
description="Master token хранится только в закрытом хранилище Platform Map Gateway. После применения он не отображается и не попадает в browser/env или artifact."
>
<ControlRow label="Состояние">
<strong>{cesiumIonSaveState === "loading" ? "Проверяем…" : cesiumIonStatus?.configured ? "Настроен" : "Не настроен"}</strong>
</ControlRow>
{cesiumIonStatus?.configured ? <ControlRow label="Проверка provider"><strong>{cesiumIonStatus.verification === "verified" ? "Пройдена: terrain и imagery доступны" : cesiumIonStatus.verification === "failed" ? "Не пройдена: проверьте token" : "Будет выполнена при следующем применении"}</strong></ControlRow> : null}
{cesiumIonStatus?.updatedAt ? <small>Последнее изменение: {new Date(cesiumIonStatus.updatedAt).toLocaleString()}</small> : null}
</SettingsCard>
<TextField
type="password"
autoComplete="new-password"
label="Cesium Ion token"
hint="обязательно"
description="Вставьте новый token. Существующее значение намеренно нельзя прочитать обратно."
value={cesiumIonToken}
onChange={(event) => {
setCesiumIonToken(event.target.value);
if (cesiumIonSaveState === "saved" || cesiumIonSaveState === "error") setCesiumIonSaveState("idle");
setCesiumIonError("");
}}
/>
{cesiumIonSaveState === "saved" ? <small className="catalog-application-draft__status">Token применён в Platform Map Gateway.</small> : null}
{cesiumIonSaveState === "error" ? <small className="catalog-application-draft__status" data-state="error">{cesiumIonError}</small> : null}
</div>
</Window>
<Window
open={createModalOpen}
title="Создать проект"