diff --git a/ROADMAP.md b/ROADMAP.md
index aecb625b..e8d0c59d 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -728,6 +728,15 @@ ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind
- [x] Settings button → `/system/` hub (was `/system/profile`)
- [x] App switcher "Manage All Apps" link → `/system/apps`
+**Empty State & System Route Accessibility (P1.12.1):**
+- [x] "Create App" button always shown in empty state (even when config loading fails)
+- [x] "System Settings" link always shown alongside "Create App" in empty state
+- [x] Top-level `/system/*` routes accessible without any app context (promoted to main routes)
+- [x] Top-level `/create-app` route accessible without any app context
+- [x] Sidebar fallback navigation with system menu items when no apps are configured
+- [x] System pages (`SystemHubPage`, `AppManagementPage`) handle missing `appName` gracefully
+- [x] Login/Register/Forgot password pages remain always accessible regardless of app state
+
**Routes:**
- [x] `/system/` → SystemHubPage
- [x] `/system/apps` → AppManagementPage
diff --git a/apps/console/src/App.tsx b/apps/console/src/App.tsx
index 3d6a0e06..610ab8f1 100644
--- a/apps/console/src/App.tsx
+++ b/apps/console/src/App.tsx
@@ -1,7 +1,7 @@
import { BrowserRouter, Routes, Route, Navigate, useNavigate, useLocation, useSearchParams } from 'react-router-dom';
import { useState, useEffect, useCallback, lazy, Suspense, useMemo, type ReactNode } from 'react';
import { ModalForm } from '@object-ui/plugin-form';
-import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
+import { Empty, EmptyTitle, EmptyDescription, Button } from '@object-ui/components';
import { toast } from 'sonner';
import { SchemaRendererProvider, useActionRunner, useGlobalUndo } from '@object-ui/react';
import type { ConnectionState } from './dataSource';
@@ -261,28 +261,49 @@ export function AppContent() {
// Allow create-app route even when no active app exists
const isCreateAppRoute = location.pathname.endsWith('/create-app');
- if (!activeApp && !isCreateAppRoute) return (
+ // Check if we're on a system route (accessible without an active app)
+ const isSystemRoute = location.pathname.includes('/system');
+
+ if (!activeApp && !isCreateAppRoute && !isSystemRoute) return (
No Apps Configured
- No applications have been registered.
-
+
+ No applications have been registered. Create your first app or visit System Settings to configure your environment.
+
+
+
+
+
);
// When on create-app without an active app, render a minimal layout with just the wizard
- if (!activeApp && isCreateAppRoute) {
+ if (!activeApp && (isCreateAppRoute || isSystemRoute)) {
return (
}>
} />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
);
@@ -455,22 +476,51 @@ function RootRedirect() {
{error ? 'Failed to Load Configuration' : 'No Apps Configured'}
- {error ? error.message : 'No applications have been registered. Check your ObjectStack configuration.'}
+ {error
+ ? 'There was an error loading the configuration. You can still create an app or access System Settings.'
+ : 'No applications have been registered. Create your first app or configure your system.'}
- {!error && (
-
);
}
+/**
+ * SystemRoutes — Top-level system admin routes accessible without any app context.
+ * Provides a minimal layout with system navigation sidebar.
+ */
+function SystemRoutes() {
+ return (
+ }>
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+ );
+}
+
export function App() {
return (
@@ -483,6 +533,24 @@ export function App() {
} />
} />
} />
+ {/* Top-level system routes — accessible without any app */}
+ } loadingFallback={}>
+
+
+
+
+ } />
+ {/* Top-level create-app — accessible without any app */}
+ } loadingFallback={}>
+
+ }>
+
+
+
+
+ } />
} loadingFallback={}>
diff --git a/apps/console/src/__tests__/EmptyStateSystemRoutes.test.tsx b/apps/console/src/__tests__/EmptyStateSystemRoutes.test.tsx
new file mode 100644
index 00000000..e6eb0ce8
--- /dev/null
+++ b/apps/console/src/__tests__/EmptyStateSystemRoutes.test.tsx
@@ -0,0 +1,209 @@
+/**
+ * Empty State & System Routes Tests
+ *
+ * Validates the empty state behavior when no apps are configured
+ * and the availability of system routes and create-app entry points.
+ *
+ * Requirements:
+ * - "Create App" button always visible in empty state (even on error)
+ * - "System Settings" link always visible in empty state
+ * - System routes accessible without app context
+ * - Login/Register/Forgot password always accessible
+ */
+
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, waitFor } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import { MemoryRouter, Routes, Route } from 'react-router-dom';
+import { AppContent } from '../App';
+
+// --- Mocks ---
+
+// Mock MetadataProvider with NO apps (empty state)
+vi.mock('../context/MetadataProvider', () => ({
+ MetadataProvider: ({ children }: any) => <>{children}>,
+ useMetadata: () => ({
+ apps: [],
+ objects: [],
+ dashboards: [],
+ reports: [],
+ pages: [],
+ loading: false,
+ error: null,
+ refresh: vi.fn(),
+ }),
+}));
+
+// Mock AdapterProvider
+const MockAdapterInstance = {
+ find: vi.fn().mockResolvedValue([]),
+ findOne: vi.fn(),
+ create: vi.fn(),
+ update: vi.fn(),
+ delete: vi.fn(),
+ connect: vi.fn().mockResolvedValue(true),
+ onConnectionStateChange: vi.fn().mockReturnValue(() => {}),
+ getConnectionState: vi.fn().mockReturnValue('connected'),
+ discovery: {},
+};
+
+vi.mock('../context/AdapterProvider', () => ({
+ AdapterProvider: ({ children }: any) => <>{children}>,
+ useAdapter: () => MockAdapterInstance,
+}));
+
+vi.mock('../dataSource', () => {
+ const MockAdapter = class {
+ find = vi.fn().mockResolvedValue([]);
+ findOne = vi.fn();
+ create = vi.fn();
+ update = vi.fn();
+ delete = vi.fn();
+ connect = vi.fn().mockResolvedValue(true);
+ onConnectionStateChange = vi.fn().mockReturnValue(() => {});
+ getConnectionState = vi.fn().mockReturnValue('connected');
+ discovery = {};
+ };
+ return {
+ ObjectStackAdapter: MockAdapter,
+ ObjectStackDataSource: MockAdapter,
+ };
+});
+
+// Mock child components to simplify testing
+vi.mock('../components/ObjectView', () => ({
+ ObjectView: () =>