feat(foundry): productionize Cesium Map Page and platform runtime
This commit is contained in:
+174
-11
@@ -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="Создать проект"
|
||||
|
||||
Reference in New Issue
Block a user