diff --git a/packages/app-shell/src/console/AppContent.tsx b/packages/app-shell/src/console/AppContent.tsx index 56a90be17..cb0e9ae5d 100644 --- a/packages/app-shell/src/console/AppContent.tsx +++ b/packages/app-shell/src/console/AppContent.tsx @@ -346,8 +346,14 @@ export function AppContent({ extraRoutes, extraRoutesNoApp }: AppContentProps = const isCreateAppRoute = location.pathname.endsWith('/create-app'); const isSystemRoute = location.pathname.includes('/system'); - - if (!activeApp && !isCreateAppRoute && !isSystemRoute) return ( + // The metadata designer (Studio) must be reachable even with no active app — + // a brand-new env where AI just drafted everything has ZERO published apps, + // so without this exemption the "no apps configured" guard below would block + // the very surface you need to REVIEW & PUBLISH those first drafts (a + // chicken-and-egg that stranded the AI magic-moment loop). + const isMetadataRoute = location.pathname.includes('/metadata'); + + if (!activeApp && !isCreateAppRoute && !isSystemRoute && !isMetadataRoute) return (
{t('empty.noAppsConfigured')} @@ -366,7 +372,7 @@ export function AppContent({ extraRoutes, extraRoutesNoApp }: AppContentProps =
); - if (!activeApp && (isCreateAppRoute || isSystemRoute)) { + if (!activeApp && (isCreateAppRoute || isSystemRoute || isMetadataRoute)) { return ( }> @@ -374,6 +380,14 @@ export function AppContent({ extraRoutes, extraRoutesNoApp }: AppContentProps = } /> } /> } /> + {/* Studio / metadata designer — reachable with no active app so a + fresh env can review + publish its first (AI-authored) drafts. */} + } /> + } /> + } /> + } /> + } /> + } /> {extraRoutesNoApp} diff --git a/packages/app-shell/src/console/home/HomePage.tsx b/packages/app-shell/src/console/home/HomePage.tsx index f403b9aa1..c70127670 100644 --- a/packages/app-shell/src/console/home/HomePage.tsx +++ b/packages/app-shell/src/console/home/HomePage.tsx @@ -27,7 +27,9 @@ import { AppCard } from './AppCard'; import { RecentApps } from './RecentApps'; import { StarredApps } from './StarredApps'; import { Empty, EmptyTitle, EmptyDescription, Button } from '@object-ui/components'; -import { Plus, Settings, Sparkles, Star, Clock, ArrowDown, Store, LayoutGrid, ShieldAlert, X } from 'lucide-react'; +import { Plus, Settings, Sparkles, Star, Clock, ArrowDown, Store, LayoutGrid, ShieldAlert, X, UploadCloud } from 'lucide-react'; +import { useMetadataClient } from '../../views/metadata-admin/useMetadata'; +import { toast } from 'sonner'; function pickGreetingKey(hour: number): string { if (hour < 5) return 'home.greetingNight'; @@ -102,6 +104,82 @@ function StatPill({ ); } +/** + * Pending-drafts banner — closes the AI magic-moment loop. After the metadata + * assistant drafts objects/views/apps (ADR-0033 draft-gated authoring), nothing + * is live until the human publishes. Without this, a user who just had AI build + * their whole system landed back on an empty-looking home with no trace of it + * and no path to publish. This surfaces the pending drafts and routes to the + * designer to review + publish. Disappears automatically once published + * (listDrafts → 0). + */ +function PendingDraftsBanner({ t }: { t: (key: string, opts?: any) => string }) { + const client = useMetadataClient(); + const [count, setCount] = useState(0); + const [pkgIds, setPkgIds] = useState([]); + const [publishing, setPublishing] = useState(false); + + useEffect(() => { + let cancelled = false; + Promise.resolve(client.listDrafts?.({})) + .then((drafts) => { + if (cancelled || !Array.isArray(drafts)) return; + setCount(drafts.length); + setPkgIds([...new Set(drafts.map((d: any) => d.packageId).filter(Boolean) as string[])]); + }) + .catch(() => { /* drafts unsupported / error → don't show */ }); + return () => { cancelled = true; }; + }, [client]); + + // One-click publish: promote the draft package(s) directly so a brand-new + // user reaches "it's live" without hunting for a designer. (Pre-PMF activation + // > the draft-review gate; the review path can return when it matters.) + const publish = async () => { + setPublishing(true); + try { + const ids = pkgIds.length + ? pkgIds + : [...new Set((((await client.listDrafts?.({})) as any[]) || []).map((d) => d.packageId).filter(Boolean) as string[])]; + if (ids.length === 0) throw new Error('no draft packages'); + for (const id of ids) { + const res = await fetch(`/api/v1/packages/${encodeURIComponent(id)}/publish-drafts`, { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: JSON.stringify({}), + }); + if (!res.ok) throw new Error((await res.text().catch(() => '')) || `HTTP ${res.status}`); + } + toast.success(t('home.pendingDrafts.published', { defaultValue: 'Published! Your changes are live.' })); + setCount(0); + // Surface the now-live app — reload so the populated home shows it. + setTimeout(() => { try { window.location.reload(); } catch { /* ignore */ } }, 700); + } catch (e) { + toast.error(`${t('home.pendingDrafts.publishFailed', { defaultValue: 'Publish failed' })}: ${(e as Error).message}`); + setPublishing(false); + } + }; + + if (count <= 0) return null; + return ( +
+
+
+ +

+ {t('home.pendingDrafts.message', { count, defaultValue: 'You have {{count}} unpublished change(s) — publish to make them live.' })} +

+ +
+
+
+ ); +} + /** * Dismissible nudge to set a local recovery password — shown when the user * signed in via SSO and has no local credential yet. We no longer force this @@ -183,6 +261,7 @@ export function HomePage() { if (activeApps.length === 0) { return (
+
@@ -228,6 +307,7 @@ export function HomePage() { the gradient display name in the hero. */} + {/* Hero */} diff --git a/packages/i18n/src/locales/en.ts b/packages/i18n/src/locales/en.ts index aa6a01749..ffb1b8e4e 100644 --- a/packages/i18n/src/locales/en.ts +++ b/packages/i18n/src/locales/en.ts @@ -1460,6 +1460,13 @@ const en = { cta: 'Set password', dismiss: 'Dismiss', }, + pendingDrafts: { + message: 'You have {{count}} unpublished change(s) — publish to make them live.', + cta: 'Publish', + publishing: 'Publishing…', + published: 'Published! Your changes are live.', + publishFailed: 'Publish failed', + }, createFirstApp: 'Create app manually', systemSettings: 'System Settings', browseMarketplace: 'Browse App Marketplace', diff --git a/packages/i18n/src/locales/zh.ts b/packages/i18n/src/locales/zh.ts index 7e60f6520..f9f68c972 100644 --- a/packages/i18n/src/locales/zh.ts +++ b/packages/i18n/src/locales/zh.ts @@ -1473,6 +1473,13 @@ const zh = { cta: '设置密码', dismiss: '关闭', }, + pendingDrafts: { + message: '你有 {{count}} 项未发布的更改 —— 点击发布即可生效。', + cta: '发布', + publishing: '发布中…', + published: '已发布!更改已生效。', + publishFailed: '发布失败', + }, createFirstApp: '手动创建应用', systemSettings: '系统设置', browseMarketplace: '浏览应用市场',