diff --git a/src/app.css b/src/app.css
index cf82633..92c5991 100644
--- a/src/app.css
+++ b/src/app.css
@@ -186,3 +186,46 @@ body {
color: #0f766e;
font-weight: 600;
}
+
+.list-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ margin-bottom: 14px;
+}
+
+.list-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 12px;
+}
+
+.list-item {
+ border: 1px solid #dbe1ec;
+ border-radius: 12px;
+ padding: 12px;
+ background: #fff;
+}
+
+.list-item h3 {
+ margin: 0 0 8px;
+ font-size: 16px;
+}
+
+.list-item p {
+ margin: 4px 0;
+ font-size: 13px;
+ color: #475569;
+}
+
+@media (max-width: 1000px) {
+ .list-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .list-header {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+}
diff --git a/src/components/AdminSidebar.tsx b/src/components/AdminSidebar.tsx
index 2f75bd9..fe0d350 100644
--- a/src/components/AdminSidebar.tsx
+++ b/src/components/AdminSidebar.tsx
@@ -2,8 +2,11 @@ import { A, useLocation } from '@solidjs/router';
const links = [
{ href: '/admin/runtime-roles/new', label: 'Create Role' },
+ { href: '/admin/runtime-roles', label: 'Manage Roles' },
{ href: '/admin/role-ui-configs/new', label: 'Create Dashboard' },
+ { href: '/admin/role-ui-configs', label: 'Manage Dashboards' },
{ href: '/admin/onboarding-schemas/new', label: 'Create Onboarding Flow' },
+ { href: '/admin/onboarding-schemas', label: 'Manage Onboarding Flows' },
];
export default function AdminSidebar() {
@@ -14,11 +17,20 @@ export default function AdminSidebar() {
- {links.map((item) => (
-
- {item.label}
-
- ))}
+ {links.map((item) => {
+ const isActive =
+ location.pathname === item.href ||
+ (item.href !== '/admin/runtime-roles' &&
+ item.href !== '/admin/role-ui-configs' &&
+ item.href !== '/admin/onboarding-schemas' &&
+ location.pathname.startsWith(item.href.replace('/new', '')));
+
+ return (
+
+ {item.label}
+
+ );
+ })}
Same UI flow, simpler builder experience.
);
diff --git a/src/lib/runtime/storage.ts b/src/lib/runtime/storage.ts
index 9de987d..dc63a64 100644
--- a/src/lib/runtime/storage.ts
+++ b/src/lib/runtime/storage.ts
@@ -4,17 +4,30 @@ export type RuntimeRecordStatus = 'draft' | 'published';
const KEY = 'nxtgauge_admin_runtime_builder_v1';
type RuntimeStore = {
- role: Record;
- dashboard: Record;
- onboarding: Record;
+ role: Record>;
+ dashboard: Record>;
+ onboarding: Record>;
};
+export type RuntimeStoredRecord = {
+ status: RuntimeRecordStatus;
+ updatedAt: string;
+ payload: T;
+};
+
+export type RuntimeListItem = RuntimeStoredRecord & {
+ key: string;
+};
+
+function emptyStore(): RuntimeStore {
+ return { role: {}, dashboard: {}, onboarding: {} };
+}
+
function readStore(): RuntimeStore {
- if (typeof window === 'undefined') {
- return { role: {}, dashboard: {}, onboarding: {} };
- }
+ if (typeof window === 'undefined') return emptyStore();
const raw = window.localStorage.getItem(KEY);
- if (!raw) return { role: {}, dashboard: {}, onboarding: {} };
+ if (!raw) return emptyStore();
+
try {
const parsed = JSON.parse(raw) as Partial;
return {
@@ -23,7 +36,7 @@ function readStore(): RuntimeStore {
onboarding: parsed.onboarding || {},
};
} catch {
- return { role: {}, dashboard: {}, onboarding: {} };
+ return emptyStore();
}
}
@@ -33,9 +46,11 @@ function writeStore(store: RuntimeStore) {
}
export function saveRuntimeConfig(type: RuntimeRecordType, key: string, payload: unknown, status: RuntimeRecordStatus) {
+ const normalizedKey = key.trim();
+ if (!normalizedKey) return;
+
const store = readStore();
- const bucket = store[type];
- bucket[key] = {
+ store[type][normalizedKey] = {
status,
updatedAt: new Date().toISOString(),
payload,
@@ -43,3 +58,22 @@ export function saveRuntimeConfig(type: RuntimeRecordType, key: string, payload:
writeStore(store);
}
+export function listRuntimeConfigs(type: RuntimeRecordType): Array> {
+ const store = readStore();
+ return Object.entries(store[type])
+ .map(([key, value]) => ({ key, ...(value as RuntimeStoredRecord) }))
+ .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
+}
+
+export function getRuntimeConfig(type: RuntimeRecordType, key: string): RuntimeStoredRecord | null {
+ const record = readStore()[type][key];
+ return (record as RuntimeStoredRecord) || null;
+}
+
+export function deleteRuntimeConfig(type: RuntimeRecordType, key: string): boolean {
+ const store = readStore();
+ if (!store[type][key]) return false;
+ delete store[type][key];
+ writeStore(store);
+ return true;
+}
diff --git a/src/routes/admin/onboarding-schemas/[schemaId].tsx b/src/routes/admin/onboarding-schemas/[schemaId].tsx
new file mode 100644
index 0000000..4ede867
--- /dev/null
+++ b/src/routes/admin/onboarding-schemas/[schemaId].tsx
@@ -0,0 +1,81 @@
+import { useParams } from '@solidjs/router';
+import { createSignal, onMount } from 'solid-js';
+import AdminShell from '~/components/AdminShell';
+import { getRuntimeConfig, saveRuntimeConfig } from '~/lib/runtime/storage';
+import type { RuntimeOnboardingConfig } from '~/lib/runtime/types';
+
+export default function EditOnboardingPage() {
+ const params = useParams();
+ const [config, setConfig] = createSignal(null);
+ const [statusMessage, setStatusMessage] = createSignal('');
+
+ onMount(() => {
+ const existing = getRuntimeConfig('onboarding', params.schemaId);
+ setConfig(existing?.payload || null);
+ });
+
+ const persist = (status: 'draft' | 'published') => {
+ const payload = config();
+ if (!payload) return;
+ saveRuntimeConfig('onboarding', payload.schemaId, payload, status);
+ setStatusMessage(status === 'draft' ? 'Draft saved in runtime storage.' : 'Onboarding schema published in runtime storage.');
+ };
+
+ return (
+
+ Edit Onboarding Flow
+ Update this runtime onboarding schema without changing source code.
+ {!config() ? (
+ Onboarding schema not found in local runtime storage.
+ ) : (
+
+
+
+ Runtime Config Preview
+ {JSON.stringify(config(), null, 2)}
+
+
+ )}
+
+ );
+}
diff --git a/src/routes/admin/onboarding-schemas/index.tsx b/src/routes/admin/onboarding-schemas/index.tsx
new file mode 100644
index 0000000..bbee02d
--- /dev/null
+++ b/src/routes/admin/onboarding-schemas/index.tsx
@@ -0,0 +1,48 @@
+import { A } from '@solidjs/router';
+import { createSignal, onMount } from 'solid-js';
+import AdminShell from '~/components/AdminShell';
+import { deleteRuntimeConfig, listRuntimeConfigs, type RuntimeListItem } from '~/lib/runtime/storage';
+import type { RuntimeOnboardingConfig } from '~/lib/runtime/types';
+
+export default function ManageOnboardingPage() {
+ const [items, setItems] = createSignal>>([]);
+
+ const load = () => setItems(listRuntimeConfigs('onboarding'));
+
+ onMount(load);
+
+ const onDelete = (key: string) => {
+ deleteRuntimeConfig('onboarding', key);
+ load();
+ };
+
+ return (
+
+ Manage Onboarding Flows
+ Edit or remove runtime onboarding schemas saved from the builder.
+
+
+ {items().length === 0 ? (
+ No runtime onboarding schemas found yet.
+ ) : (
+
+ {items().map((item) => (
+
+ {item.key}
+ Status: {item.status}
+ Updated: {new Date(item.updatedAt).toLocaleString()}
+
+
Edit
+
+
+
+ ))}
+
+ )}
+
+
+ );
+}
diff --git a/src/routes/admin/role-ui-configs/[roleKey].tsx b/src/routes/admin/role-ui-configs/[roleKey].tsx
new file mode 100644
index 0000000..81b4285
--- /dev/null
+++ b/src/routes/admin/role-ui-configs/[roleKey].tsx
@@ -0,0 +1,89 @@
+import { useParams } from '@solidjs/router';
+import { createSignal, onMount } from 'solid-js';
+import AdminShell from '~/components/AdminShell';
+import { getRuntimeConfig, saveRuntimeConfig } from '~/lib/runtime/storage';
+import type { RuntimeDashboardConfig } from '~/lib/runtime/types';
+
+export default function EditDashboardPage() {
+ const params = useParams();
+ const [config, setConfig] = createSignal(null);
+ const [statusMessage, setStatusMessage] = createSignal('');
+
+ onMount(() => {
+ const existing = getRuntimeConfig('dashboard', params.roleKey);
+ setConfig(existing?.payload || null);
+ });
+
+ const persist = (status: 'draft' | 'published') => {
+ const payload = config();
+ if (!payload) return;
+ saveRuntimeConfig('dashboard', payload.roleKey, payload, status);
+ setStatusMessage(status === 'draft' ? 'Draft saved in runtime storage.' : 'Dashboard config published in runtime storage.');
+ };
+
+ return (
+
+ Edit Dashboard
+ Update dashboard runtime config without changing source code.
+ {!config() ? (
+ Dashboard config not found in local runtime storage.
+ ) : (
+
+
+ Dashboard Builder
+
+
+ setConfig({ ...config()!, roleKey: e.currentTarget.value.toUpperCase() })} />
+
+
+
+
+
+
+
+
+
+
+
+ {statusMessage() && {statusMessage()}
}
+
+
+ Runtime Config Preview
+ {JSON.stringify(config(), null, 2)}
+
+
+ )}
+
+ );
+}
diff --git a/src/routes/admin/role-ui-configs/index.tsx b/src/routes/admin/role-ui-configs/index.tsx
new file mode 100644
index 0000000..1732e50
--- /dev/null
+++ b/src/routes/admin/role-ui-configs/index.tsx
@@ -0,0 +1,48 @@
+import { A } from '@solidjs/router';
+import { createSignal, onMount } from 'solid-js';
+import AdminShell from '~/components/AdminShell';
+import { deleteRuntimeConfig, listRuntimeConfigs, type RuntimeListItem } from '~/lib/runtime/storage';
+import type { RuntimeDashboardConfig } from '~/lib/runtime/types';
+
+export default function ManageDashboardsPage() {
+ const [items, setItems] = createSignal>>([]);
+
+ const load = () => setItems(listRuntimeConfigs('dashboard'));
+
+ onMount(load);
+
+ const onDelete = (key: string) => {
+ deleteRuntimeConfig('dashboard', key);
+ load();
+ };
+
+ return (
+
+ Manage Dashboards
+ Edit or remove runtime dashboard configs saved from the builder.
+
+
+ {items().length === 0 ? (
+ No runtime dashboard configs found yet.
+ ) : (
+
+ {items().map((item) => (
+
+ {item.key}
+ Status: {item.status}
+ Updated: {new Date(item.updatedAt).toLocaleString()}
+
+
Edit
+
+
+
+ ))}
+
+ )}
+
+
+ );
+}
diff --git a/src/routes/admin/runtime-roles/[roleKey].tsx b/src/routes/admin/runtime-roles/[roleKey].tsx
new file mode 100644
index 0000000..f742147
--- /dev/null
+++ b/src/routes/admin/runtime-roles/[roleKey].tsx
@@ -0,0 +1,81 @@
+import { useParams } from '@solidjs/router';
+import { createSignal, onMount } from 'solid-js';
+import AdminShell from '~/components/AdminShell';
+import { getRuntimeConfig, saveRuntimeConfig } from '~/lib/runtime/storage';
+import type { RuntimeRoleConfig } from '~/lib/runtime/types';
+
+export default function EditRolePage() {
+ const params = useParams();
+ const [config, setConfig] = createSignal(null);
+ const [statusMessage, setStatusMessage] = createSignal('');
+
+ onMount(() => {
+ const existing = getRuntimeConfig('role', params.roleKey);
+ setConfig(existing?.payload || null);
+ });
+
+ const persist = (status: 'draft' | 'published') => {
+ const payload = config();
+ if (!payload) return;
+ saveRuntimeConfig('role', payload.roleKey, payload, status);
+ setStatusMessage(status === 'draft' ? 'Draft saved in runtime storage.' : 'Role config published in runtime storage.');
+ };
+
+ return (
+
+ Edit Role
+ Update this runtime role without changing source code.
+ {!config() ? (
+ Role not found in local runtime storage.
+ ) : (
+
+
+
+ Runtime Config Preview
+ {JSON.stringify(config(), null, 2)}
+
+
+ )}
+
+ );
+}
diff --git a/src/routes/admin/runtime-roles/index.tsx b/src/routes/admin/runtime-roles/index.tsx
new file mode 100644
index 0000000..8c501a3
--- /dev/null
+++ b/src/routes/admin/runtime-roles/index.tsx
@@ -0,0 +1,48 @@
+import { A } from '@solidjs/router';
+import { createSignal, onMount } from 'solid-js';
+import AdminShell from '~/components/AdminShell';
+import { deleteRuntimeConfig, listRuntimeConfigs, type RuntimeListItem } from '~/lib/runtime/storage';
+import type { RuntimeRoleConfig } from '~/lib/runtime/types';
+
+export default function ManageRolesPage() {
+ const [items, setItems] = createSignal>>([]);
+
+ const load = () => setItems(listRuntimeConfigs('role'));
+
+ onMount(load);
+
+ const onDelete = (key: string) => {
+ deleteRuntimeConfig('role', key);
+ load();
+ };
+
+ return (
+
+ Manage Roles
+ Edit or remove runtime role configs saved from the builder.
+
+
+ {items().length === 0 ? (
+ No runtime role configs found yet.
+ ) : (
+
+ {items().map((item) => (
+
+ {item.key}
+ Status: {item.status}
+ Updated: {new Date(item.updatedAt).toLocaleString()}
+
+
Edit
+
+
+
+ ))}
+
+ )}
+
+
+ );
+}