diff --git a/.changeset/10520-orphaned-console-pages.md b/.changeset/10520-orphaned-console-pages.md
new file mode 100644
index 0000000000..eb439a1d1d
--- /dev/null
+++ b/.changeset/10520-orphaned-console-pages.md
@@ -0,0 +1,15 @@
+---
+'@object-ui/console': minor
+---
+
+Framework navigation can now reach the console pages that the retired System Hub card wall used to be the only link to (objectui#10520).
+
+Navigation reaches a console page only through a `type: 'component'` item whose `componentRef` is a component-registry key, never through a console path. The console registers three new keys:
+
+- `audit:log` renders the Audit Log page. It stays beside the `sys_audit_log` object view because its detail drawer pretty-prints the before and after JSON of a change, which the object's record page shows as raw text.
+- `ai:approvals` renders the AI Approvals page, which lists the whole AI pending-action queue.
+- `developer:integrations` renders the Integrations & APIs page, which shows the environment's REST base URL, the generated REST endpoints of each business object and a cURL sample that sends the `x-api-key` header.
+
+The keys are additive. `/apps/:app/system/audit-log`, `/apps/:app/system/ai-approvals` and `/apps/:app/developer/integrations` still render their pages, so bookmarks and deep links keep working. The navigation entries that name the new keys belong to the framework, not to this package.
+
+**Breaking:** the Developer Hub page and its route, `/apps/:app/developer`, are removed. The hub was a wall of four cards, and each card's destination is now a registry key: `developer:api-console`, `developer:flow-runs` and `developer:public-forms`, which the framework's Studio app already names in its Developer group, and `developer:integrations`. A bookmark to `/apps/:app/developer` no longer shows the hub. Inside an app it now reaches the generic object route, which answers that no object named `developer` exists. With no apps configured it lands where it did before this change. The four routes under `/apps/:app/developer/` are unchanged.
diff --git a/apps/console/src/AppContent.tsx b/apps/console/src/AppContent.tsx
index 1c0c3469f2..6f6294b3d9 100644
--- a/apps/console/src/AppContent.tsx
+++ b/apps/console/src/AppContent.tsx
@@ -22,7 +22,6 @@ const AiPendingActionsPage = lazy(() => import('./pages/system/AiPendingActionsP
const AuditLogPage = lazy(() => import('./pages/system/AuditLogPage').then(m => ({ default: m.AuditLogPage })));
const SettingsHub = lazy(() => import('./pages/settings/SettingsHub').then(m => ({ default: m.SettingsHub })));
const SettingsView = lazy(() => import('./pages/settings/SettingsView').then(m => ({ default: m.SettingsView })));
-const DeveloperHubPage = lazy(() => import('./pages/developer/DeveloperHubPage').then(m => ({ default: m.DeveloperHubPage })));
const ApiConsolePage = lazy(() => import('./pages/developer/ApiConsolePage').then(m => ({ default: m.ApiConsolePage })));
const FlowRunsPage = lazy(() => import('./pages/developer/FlowRunsPage').then(m => ({ default: m.FlowRunsPage })));
const PublicFormsPage = lazy(() => import('./pages/developer/PublicFormsPage').then(m => ({ default: m.PublicFormsPage })));
@@ -209,7 +208,13 @@ export const systemRoutes = (
}>} />
}>} />
}>} />
- }>} />
+ {/* No bare `developer` route (objectui#10520): the Developer Hub card wall
+ retired once all four of its destinations were `developer:*` registry
+ keys that framework navigation can name. A `/developer` bookmark falls
+ through to app-shell's generic `:objectName` route instead of a blank
+ screen; what it lands on is measured in
+ `__tests__/AppContent.systemHubRoutes.test.tsx`. The sub-page routes
+ below stay for bookmarks and deep links. */}
}>} />
}>} />
}>} />
diff --git a/apps/console/src/__tests__/AppContent.systemHubRoutes.test.tsx b/apps/console/src/__tests__/AppContent.systemHubRoutes.test.tsx
index 3c769b1fa3..660c9b410b 100644
--- a/apps/console/src/__tests__/AppContent.systemHubRoutes.test.tsx
+++ b/apps/console/src/__tests__/AppContent.systemHubRoutes.test.tsx
@@ -150,6 +150,15 @@ vi.mock('../pages/settings/SettingsHub', () => ({
SettingsHub: () =>
settings hub
,
}));
+/**
+ * One of the retired Developer Hub's destinations (objectui#10520), stubbed
+ * for the same reason: it sits behind `React.lazy`, and its real body fetches
+ * the object list.
+ */
+vi.mock('../pages/developer/IntegrationsPage', () => ({
+ IntegrationsPage: () => integrations
,
+}));
+
/** Echoes `:objectName` — this is the probe the whole fix is measured against. */
vi.mock('../../../../packages/app-shell/src/views/ObjectView', () => ({
ObjectView: () => {
@@ -439,3 +448,50 @@ describe('the bare system landing forwards onto the settings hub (objectui#3743)
expect(screen.queryByTestId('create-first-app-btn')).not.toBeInTheDocument();
});
});
+
+describe('the retired Developer Hub URL (objectui#10520)', () => {
+ /**
+ * `/apps/:app/developer` rendered `DeveloperHubPage`, a card wall of four
+ * links whose only in-app link was a card on the System Hub. objectui#10520
+ * retired it once all four destinations were `developer:*` registry keys, and
+ * deliberately added no redirect in its place: the URL was never blank
+ * without it. With the host's `developer` route gone, the segment falls
+ * through to app-shell's generic `:objectName` route, i.e. the URL now means
+ * what `/apps/:app/` means everywhere else. The `ObjectView` probe
+ * above echoes the name it was handed; the real `ObjectView` answers a name
+ * that is not an object with its own "object not found" empty state.
+ *
+ * Put the `developer` route (and its page) back and this pin goes red: the
+ * hub renders, and the `ObjectView` probe never does.
+ */
+ it('with an active app, a /developer bookmark lands on the generic object route in place, never a blank screen', async () => {
+ renderConsoleAt('/apps/setup/developer');
+
+ expect(await screen.findByTestId('object-view')).toHaveTextContent('developer');
+ expect(chain).toEqual(['/apps/setup/developer']);
+ expect(screen.getByTestId('console-layout')).toHaveAttribute('data-active-app', 'setup');
+ expect(screen.queryByText('Page not found')).not.toBeInTheDocument();
+ });
+
+ it('MEASUREMENT: with zero apps, the bookmark reaches the no-apps empty state, as it did before the retirement', async () => {
+ // Not a consequence of this change: `isSystemRoute` keys on a `system`
+ // segment, so on a zero-app deployment `/developer` never reached the host
+ // fragment, and the hub route was unreachable there all along.
+ metadataApps = [];
+ renderConsoleAt('/apps/setup/developer');
+
+ expect(await screen.findByTestId('create-first-app-btn')).toBeInTheDocument();
+ expect(chain).toEqual(['/apps/setup/developer']);
+ });
+
+ it('the sub-page routes it linked to still resolve (bookmarks and deep links carry them)', async () => {
+ // One of the four, driven end to end: the rest share the same fragment and
+ // the same shape. It must NOT fall through to the object route the bare
+ // segment now reaches.
+ renderConsoleAt('/apps/setup/developer/integrations');
+
+ expect(await screen.findByTestId('integrations-page')).toBeInTheDocument();
+ expect(screen.queryByTestId('object-view')).not.toBeInTheDocument();
+ expect(screen.queryByTestId('record-detail-view')).not.toBeInTheDocument();
+ });
+});
diff --git a/apps/console/src/__tests__/orphanedPageComponentRefs-10520.test.tsx b/apps/console/src/__tests__/orphanedPageComponentRefs-10520.test.tsx
new file mode 100644
index 0000000000..484c7a4f86
--- /dev/null
+++ b/apps/console/src/__tests__/orphanedPageComponentRefs-10520.test.tsx
@@ -0,0 +1,228 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * `audit:log`, `ai:approvals` and `developer:integrations`: the
+ * component-registry keys for the console pages that lost their only in-app
+ * link when the System Hub card wall retired (objectui#10520).
+ *
+ * ## What this file is for
+ *
+ * Framework navigation reaches a console page only through a
+ * `type: 'component'` item whose `componentRef` is a registry key, never a
+ * console path. `AuditLogPage`, `AiPendingActionsPage` and `IntegrationsPage`
+ * had a route but no key, so no navigation entry could name them. This file
+ * pins, per key, the properties a nav item exercises, in the order it
+ * exercises them:
+ *
+ * 1. the key is registered at all, by importing the registration module the
+ * way `main.tsx` does (a side effect);
+ * 2. the key addresses the `component//` URL, built from the ref
+ * through the same helper the sidebar uses rather than spelled out;
+ * 3. that URL, rendered by app-shell's REAL `DefaultAppContent`, reaches the
+ * page. Its `component/:ns/:name/*` route is what serves `ComponentNavView`
+ * in the shipped console, so a key that is registered but spelled
+ * differently from the URL fails here, not only in a browser;
+ * 4. the key is ADDITIVE: the page's standalone route, from this host's REAL
+ * `systemRoutes` fragment (imported, never transcribed), still renders it,
+ * because bookmarks and deep links carry it.
+ *
+ * Plus the premise the Developer Hub's retirement rests on: all four of its
+ * cards' destinations are registered `developer:*` keys.
+ *
+ * ## Why the whole `DefaultAppContent`, and not `ComponentNavView` alone
+ *
+ * `ComponentNavView` is not on app-shell's barrel, and importing it by source
+ * path puts app-shell's file into this app's `tsc` program under this app's
+ * compiler options, which it is not written against. The route table that
+ * serves it IS exported, so the probe goes through that: the same lazy
+ * `ComponentNavView`, the same registry, the same URL grammar as production.
+ *
+ * ## Scope of the stubs
+ *
+ * The three pages are stubbed at the exact specifiers the registration modules
+ * and `AppContent` lazy-import, so the registry entries and the routes under
+ * test are the production ones, Suspense wrappers included. The shell around
+ * them is stubbed the way `AppContent.systemHubRoutes.test.tsx` stubs it, for
+ * the reasons given there.
+ */
+
+import '@testing-library/jest-dom/vitest';
+import { describe, it, expect, vi } from 'vitest';
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import { MemoryRouter, Routes, Route, useParams } from 'react-router-dom';
+
+// AGENTS.md §测试纪律: the lazily-imported app-shell pages are stubbed so no
+// unbounded `import()` races a bounded assertion window. The paths are relative
+// to app-shell's OWN source tree (`@object-ui/app-shell` is aliased to
+// `packages/app-shell/src` by `apps/console/vite.config.ts`).
+vi.mock('../../../../packages/app-shell/src/views/metadata-admin', () => ({
+ MetadataDirectoryPage: () => ,
+ StudioHomePage: () => ,
+ MetadataResourceListPage: () => ,
+ MetadataResourceEditPage: () => ,
+ MetadataResourceHistoryPage: () => ,
+ MetadataDiagnosticsPage: () => ,
+ MetadataResourceRouter: () => ,
+ registerMetadataResource: () => {},
+}));
+vi.mock('../../../../packages/app-shell/src/console/marketplace/MarketplacePage', () => ({
+ MarketplacePage: () => ,
+}));
+vi.mock('../../../../packages/app-shell/src/console/marketplace/MarketplaceInstalledPage', () => ({
+ MarketplaceInstalledPage: () => ,
+}));
+vi.mock('../../../../packages/app-shell/src/console/marketplace/MarketplacePackagePage', () => ({
+ MarketplacePackagePage: () => ,
+}));
+vi.mock('@object-ui/plugin-designer', async (importOriginal) => ({
+ ...(await importOriginal()),
+ CreateAppPage: () => ,
+ EditAppPage: () => ,
+ DashboardDesignPage: () => ,
+}));
+vi.mock('../../../../packages/app-shell/src/layout/ConsoleLayout', () => ({
+ ConsoleLayout: ({ activeAppName, children }: { activeAppName?: string; children?: React.ReactNode }) => (
+
+ {children}
+
+ ),
+}));
+vi.mock('../../../../packages/app-shell/src/chrome/CommandPalette', () => ({ CommandPalette: () => null }));
+vi.mock('../../../../packages/app-shell/src/chrome/KeyboardShortcutsDialog', () => ({ KeyboardShortcutsDialog: () => null }));
+vi.mock('../../../../packages/app-shell/src/chrome/OnboardingWalkthrough', () => ({ OnboardingWalkthrough: () => null }));
+vi.mock('../../../../packages/app-shell/src/views/ObjectView', () => ({
+ ObjectView: () => ,
+}));
+
+vi.mock('@object-ui/i18n', async (importOriginal) => ({
+ ...(await importOriginal>()),
+ useObjectTranslation: () => ({
+ t: (key: string, options?: Record) => String(options?.defaultValue ?? key),
+ }),
+ useObjectLabel: () => ({
+ objectLabel: ({ label }: { label?: string }) => label,
+ }),
+}));
+vi.mock('@object-ui/auth', async (importOriginal) => ({
+ ...(await importOriginal>()),
+ useAuth: () => ({
+ user: null,
+ getAuthConfig: async () => ({ features: {} }),
+ activeOrganization: null,
+ }),
+ useWorkspaceAdminStatus: () => ({ isAdmin: true, isResolved: true }),
+}));
+const dataSourceStub = {
+ onConnectionStateChange: () => () => {},
+ getConnectionState: () => 'connected',
+};
+vi.mock('../../../../packages/app-shell/src/providers/AdapterProvider', async (importOriginal) => ({
+ ...(await importOriginal>()),
+ useAdapter: () => dataSourceStub,
+}));
+vi.mock('../../../../packages/app-shell/src/providers/MetadataProvider', async (importOriginal) => ({
+ ...(await importOriginal>()),
+ useMetadata: () => ({
+ apps: [
+ { name: 'setup', label: 'Setup', isDefault: true, navigation: [] },
+ { name: 'studio', label: 'Studio', navigation: [] },
+ ],
+ objects: [],
+ loading: false,
+ ensureType: undefined,
+ error: null,
+ refresh: vi.fn(async () => {}),
+ }),
+}));
+
+/**
+ * The probes stand in for the three pages at the exact specifiers
+ * `registerSystemComponents`, `registerDeveloperComponents` and `AppContent`
+ * lazy-import. Each echoes `:appName`, which `IntegrationsPage` builds its
+ * "Open API Console" link from.
+ */
+function probe(testId: string) {
+ return function Probe() {
+ const { appName } = useParams<{ appName?: string }>();
+ return {appName ?? '(none)'}
;
+ };
+}
+vi.mock('../pages/system/AuditLogPage', () => ({ AuditLogPage: probe('audit-log-probe') }));
+vi.mock('../pages/system/AiPendingActionsPage', () => ({ AiPendingActionsPage: probe('ai-approvals-probe') }));
+vi.mock('../pages/developer/IntegrationsPage', () => ({ IntegrationsPage: probe('integrations-probe') }));
+
+import { DefaultAppContent, getAppComponent, componentRefToUrlSegments } from '@object-ui/app-shell';
+import { systemRoutes } from '../AppContent';
+
+// Side-effect imports: these are the modules under test, imported the way
+// `main.tsx` imports them.
+import '../registerSystemComponents';
+import '../registerDeveloperComponents';
+
+/** The `component/...` path the sidebar builds for a `componentRef` nav item. */
+const componentPath = (ref: string) => `component/${componentRefToUrlSegments(ref).join('/')}`;
+
+function renderConsoleAt(url: string) {
+ return render(
+
+
+ }
+ />
+
+ ,
+ );
+}
+
+describe.each([
+ { ref: 'audit:log', segments: ['audit', 'log'], probeId: 'audit-log-probe', app: 'setup', route: 'system/audit-log' },
+ { ref: 'ai:approvals', segments: ['ai', 'approvals'], probeId: 'ai-approvals-probe', app: 'setup', route: 'system/ai-approvals' },
+ {
+ ref: 'developer:integrations',
+ segments: ['developer', 'integrations'],
+ probeId: 'integrations-probe',
+ app: 'studio',
+ route: 'developer/integrations',
+ },
+])('$ref component ref (objectui#10520)', ({ ref, segments, probeId, app, route }) => {
+ it('is registered by the module `main.tsx` side-effect-imports', () => {
+ expect(getAppComponent(ref)).toBeDefined();
+ expect(getAppComponent(ref)?.source).toBe('@object-ui/console');
+ });
+
+ it(`addresses the component/${segments.join('/')} URL segments`, () => {
+ expect(componentRefToUrlSegments(ref)).toEqual(segments);
+ expect(componentPath(ref)).toBe(`component/${segments.join('/')}`);
+ });
+
+ it('the component URL reaches the page through the shipped route table', async () => {
+ renderConsoleAt(`/apps/${app}/${componentPath(ref)}`);
+
+ expect(await screen.findByTestId(probeId)).toHaveTextContent(app);
+ expect(screen.queryByText('Component not registered')).not.toBeInTheDocument();
+ });
+
+ it(`the standalone ${route} route keeps rendering the page (the key is additive)`, async () => {
+ renderConsoleAt(`/apps/${app}/${route}`);
+
+ expect(await screen.findByTestId(probeId)).toHaveTextContent(app);
+ });
+});
+
+describe('the retired Developer Hub (objectui#10520)', () => {
+ it('every one of its four destinations is a registered developer:* key', () => {
+ // The hub's four cards, by the key a navigation entry would name. This is
+ // the premise its retirement rests on: were any of them unkeyed, the hub
+ // would still be that page's only way in.
+ for (const ref of [
+ 'developer:integrations',
+ 'developer:api-console',
+ 'developer:flow-runs',
+ 'developer:public-forms',
+ ]) {
+ expect(getAppComponent(ref), ref).toBeDefined();
+ }
+ });
+});
diff --git a/apps/console/src/main.tsx b/apps/console/src/main.tsx
index af83552ae9..93f3b70e23 100644
--- a/apps/console/src/main.tsx
+++ b/apps/console/src/main.tsx
@@ -41,6 +41,9 @@ import './registerAccountComponents';
// Register `approvals:*` component refs (the Approvals Inbox entry).
import './registerApprovalsComponents';
+// Register `audit:log` and `ai:approvals` component refs (objectui#10520).
+import './registerSystemComponents';
+
// (Per-type metadata-admin override for `object` was removed: the
// `object` type now uses the same generic ResourceListPage as every
// other metadata type for visual consistency. The visual ObjectManager
diff --git a/apps/console/src/pages/developer/DeveloperHubPage.tsx b/apps/console/src/pages/developer/DeveloperHubPage.tsx
deleted file mode 100644
index 7bceb2a939..0000000000
--- a/apps/console/src/pages/developer/DeveloperHubPage.tsx
+++ /dev/null
@@ -1,101 +0,0 @@
-/**
- * Developer Hub Page
- *
- * Entry point for developer-oriented tools migrated from the standalone
- * studio app: REST API console, flow test runs, and public form management.
- */
-
-import { useNavigate, useParams } from 'react-router-dom';
-import {
- Card,
- CardContent,
- CardHeader,
- CardTitle,
- CardDescription,
-} from '@object-ui/components';
-import { Terminal, Workflow, FileText, Plug } from 'lucide-react';
-
-interface HubCard {
- title: string;
- description: string;
- icon: React.ElementType;
- href: string;
-}
-
-export function DeveloperHubPage() {
- const navigate = useNavigate();
- const { appName } = useParams();
- const basePath = appName ? `/apps/${appName}` : '';
-
- const cards: HubCard[] = [
- {
- title: 'Integrations & APIs',
- description: 'Your app as a REST API — base URL, endpoints, and connect samples',
- icon: Plug,
- href: `${basePath}/developer/integrations`,
- },
- {
- title: 'API Console',
- description: 'Inspect and exercise REST endpoints with auto-discovered services',
- icon: Terminal,
- href: `${basePath}/developer/api-console`,
- },
- {
- title: 'Flow Runs',
- description: 'Trigger flows with sample inputs and inspect recent run history',
- icon: Workflow,
- href: `${basePath}/developer/flow-runs`,
- },
- {
- title: 'Public Forms',
- description: 'Publish, share, and embed FormViews available to anonymous users',
- icon: FileText,
- href: `${basePath}/developer/public-forms`,
- },
- ];
-
- return (
-
-
-
Developer
-
- Tools for building, debugging, and sharing ObjectStack apps
-
-
-
-
- {cards.map((card) => {
- const Icon = card.icon;
- return (
-
navigate(card.href)}
- data-testid={`hub-card-${card.title.toLowerCase().replace(/\s+/g, '-')}`}
- role="link"
- tabIndex={0}
- aria-label={`${card.title}: ${card.description}`}
- onKeyDown={(e: React.KeyboardEvent) => {
- if (e.key === 'Enter' || e.key === ' ') {
- e.preventDefault();
- navigate(card.href);
- }
- }}
- >
-
-
-
-
-
- {card.title}
- {card.description}
-
-
-
-
- );
- })}
-
-
- );
-}
diff --git a/apps/console/src/pages/system/AiPendingActionsPage.tsx b/apps/console/src/pages/system/AiPendingActionsPage.tsx
index d9957f97d7..116b105e7f 100644
--- a/apps/console/src/pages/system/AiPendingActionsPage.tsx
+++ b/apps/console/src/pages/system/AiPendingActionsPage.tsx
@@ -5,8 +5,13 @@
* exposed by `@objectstack/service-ai` at `/api/v1/ai/pending-actions/*`.
*
* Thin page wrapper around the shared `AiPendingActionsInbox` component
- * shipped from `@object-ui/plugin-chatbot`. The same component renders in
- * Studio's assistant builder panel — keeping a single source of truth.
+ * shipped from `@object-ui/plugin-chatbot`. When objectui#10520 read the tree,
+ * this page was that component's only mount and so the only surface listing
+ * the whole queue (the chat shows approval cards for its own conversation
+ * only); that is a one-time reading, and nothing re-derives it. It is
+ * served at the standalone route `/system/ai-approvals` and registered as the
+ * `ai:approvals` component ref that framework navigation can name
+ * (`registerSystemComponents.tsx`, objectui#10520).
*/
import { useMemo } from 'react';
diff --git a/apps/console/src/pages/system/AuditLogPage.tsx b/apps/console/src/pages/system/AuditLogPage.tsx
index f75b0a6499..ab8999cc9d 100644
--- a/apps/console/src/pages/system/AuditLogPage.tsx
+++ b/apps/console/src/pages/system/AuditLogPage.tsx
@@ -1,8 +1,10 @@
/**
* Audit Log Page (system route)
*
- * Read-only browser for `sys_audit_log` records, surfaced via the System
- * Hub card at `/system/audit-log`. Talks to the framework REST endpoint
+ * Read-only browser for `sys_audit_log` records, served at the standalone
+ * route `/system/audit-log` and registered as the `audit:log` component ref
+ * that framework navigation can name (`registerSystemComponents.tsx`,
+ * objectui#10520). Talks to the framework REST endpoint
* `/api/v1/data/sys_audit_log` with standard ObjectQL filter params.
*
* Field shape mirrors framework/packages/platform-objects/src/audit/
diff --git a/apps/console/src/registerDeveloperComponents.tsx b/apps/console/src/registerDeveloperComponents.tsx
index 186d5b149c..5d11bc4c63 100644
--- a/apps/console/src/registerDeveloperComponents.tsx
+++ b/apps/console/src/registerDeveloperComponents.tsx
@@ -3,18 +3,30 @@
/**
* Developer component registrations.
*
- * Binds the `developer:*` registry keys (referenced from the framework's
- * default developer navigation group) to the lazy-loaded console pages
- * that already host these tools.
+ * Binds the `developer:*` registry keys to the lazy-loaded console pages
+ * that already host these tools. The framework's Studio app names the first
+ * three from its Developer navigation group.
*
* URL shape resolved by `ComponentNavView`:
* developer:api-console → /apps//component/developer/api-console
* developer:flow-runs → /apps//component/developer/flow-runs
* developer:public-forms → /apps//component/developer/public-forms
+ * developer:integrations → /apps//component/developer/integrations
*
- * The existing standalone `/developer/*` routes in `AppContent.tsx` are
- * left in place — they remain reachable from the Console-shell Developer
- * Hub. This module only wires the metadata-driven entry from app sidebars.
+ * `developer:integrations` (objectui#10520): the Integrations & APIs page
+ * was linked from exactly one place, a card on the console's Developer Hub
+ * page, and that hub had no in-app link of its own once the System Hub card
+ * wall retired (objectui#3743). With this key, all four of the hub's
+ * destinations are registry keys, so the hub page is retired and navigation
+ * is the one way in. The page is kept, not retired: when objectui#10520
+ * compared it with Studio, no Studio surface showed the environment's base URL
+ * or the `x-api-key` cURL sample it carries (a one-time reading that nothing
+ * re-derives).
+ *
+ * The standalone `/developer/*` routes in `AppContent.tsx` stay in place,
+ * because bookmarks and deep links carry them. This module only wires the
+ * metadata-driven entry from app sidebars. Pinned in
+ * `__tests__/orphanedPageComponentRefs-10520.test.tsx`.
*/
import { lazy, Suspense } from 'react';
@@ -29,6 +41,9 @@ const FlowRunsPage = lazy(() =>
const PublicFormsPage = lazy(() =>
import('./pages/developer/PublicFormsPage').then((m) => ({ default: m.PublicFormsPage })),
);
+const IntegrationsPage = lazy(() =>
+ import('./pages/developer/IntegrationsPage').then((m) => ({ default: m.IntegrationsPage })),
+);
function DeveloperFallback({ label }: { label: string }) {
return Loading {label}…
;
@@ -66,3 +81,14 @@ registerAppComponent({
),
});
+
+registerAppComponent({
+ ref: 'developer:integrations',
+ label: 'Integrations & APIs',
+ source: '@object-ui/console',
+ component: (props: any) => (
+ }>
+
+
+ ),
+});
diff --git a/apps/console/src/registerSystemComponents.tsx b/apps/console/src/registerSystemComponents.tsx
new file mode 100644
index 0000000000..b44a6d5937
--- /dev/null
+++ b/apps/console/src/registerSystemComponents.tsx
@@ -0,0 +1,99 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * System-page component registrations (objectui#10520).
+ *
+ * Binds registry keys to two console pages under `pages/system/` that
+ * framework navigation can otherwise not reach, so app metadata can point a
+ * nav item at them declaratively:
+ *
+ * ```ts
+ * { type: 'component', componentRef: 'audit:log' }
+ * { type: 'component', componentRef: 'ai:approvals' }
+ * ```
+ *
+ * URL shape resolved by `ComponentNavView`:
+ * audit:log → /apps//component/audit/log
+ * ai:approvals → /apps//component/ai/approvals
+ *
+ * ## Why these two need a key
+ *
+ * Both pages were linked from exactly one place in the app: the System Hub
+ * card wall, which objectui#3743 retired so that navigation is the single
+ * source of truth. Framework navigation reaches a console page only through a
+ * `type: 'component'` item whose `componentRef` is a registry key, never a
+ * console path, so a page with no key has no way back into navigation.
+ *
+ * Both are KEPT rather than retired, because no navigation-reachable surface
+ * covered everything they offer when objectui#10520 compared them capability
+ * by capability. That comparison is a one-time reading, and nothing
+ * re-derives it:
+ *
+ * - `AuditLogPage`: the `sys_audit_log` object view (Setup's Audit Logs
+ * entry) gives filterable, paginated list views, but its record page shows
+ * `old_value` / `new_value` as raw JSON text. The page's detail drawer
+ * pretty-prints the before and after payloads, and the metadata, side by
+ * side.
+ * - `AiPendingActionsPage`: the only surface that lists the whole AI
+ * pending-action queue. The chat renders approval cards for its own
+ * conversation only, and the build debug drawer for its own build.
+ *
+ * ## Namespaces
+ *
+ * Each key is named for the capability that owns the data, the way
+ * `approvals:inbox` is, and not for the console path the page happens to be
+ * mounted at. `audit` is the capability that owns `sys_audit_log`
+ * (`plugin-audit`, which registers the `audit` kernel service). `ai` is the
+ * service that serves `/api/v1/ai/pending-actions`. `ai:approvals` lives in
+ * this app-component registry, which is separate from the SDUI block registry
+ * that holds the protocol's `ai:*` block types.
+ *
+ * ## The standalone routes stay
+ *
+ * `system/audit-log` and `system/ai-approvals` (declared in `AppContent.tsx`'s
+ * `systemRoutes` fragment) are NOT replaced: bookmarks and deep links carry
+ * them. These component refs are purely additive. Pinned in
+ * `__tests__/orphanedPageComponentRefs-10520.test.tsx`.
+ */
+
+import { lazy, Suspense } from 'react';
+import { registerAppComponent } from '@object-ui/app-shell';
+import { useObjectTranslation } from '@object-ui/i18n';
+
+const AuditLogPage = lazy(() =>
+ import('./pages/system/AuditLogPage').then((m) => ({ default: m.AuditLogPage })),
+);
+const AiPendingActionsPage = lazy(() =>
+ import('./pages/system/AiPendingActionsPage').then((m) => ({ default: m.AiPendingActionsPage })),
+);
+
+function SystemPageFallback() {
+ const { t } = useObjectTranslation();
+ return (
+
+ {t('common.loading', { defaultValue: 'Loading…' })}
+
+ );
+}
+
+registerAppComponent({
+ ref: 'audit:log',
+ label: 'Audit Log',
+ source: '@object-ui/console',
+ component: (props: any) => (
+ }>
+
+
+ ),
+});
+
+registerAppComponent({
+ ref: 'ai:approvals',
+ label: 'AI Approvals',
+ source: '@object-ui/console',
+ component: (props: any) => (
+ }>
+
+
+ ),
+});