Minimal RBAC + Feature Flags + Developer Mode

- 4 roles: SuperAdmin, Operator, Reviewer, PilotUser
- Capabilities (not pages): mission.create, artifact.view, decision.approve, etc.
- Feature flags: ENABLE_REPLAY, ENABLE_MODEL_TRAINING, etc.
- Developer Mode toggle in UI (activated by permission)
- New rule: All features must link to module, capability, and role

Next: Deploy pilot environment
This commit is contained in:
Bernt
2026-07-02 16:59:00 +00:00
parent c5a42506c1
commit 207185e5fb
4 changed files with 181 additions and 1 deletions
+88
View File
@@ -0,0 +1,88 @@
/**
* Capabilities — minimal RBAC for Pilot 001-010
*
* Not pages — capabilities (what user CAN do).
* Roles are combinations of capabilities.
*/
export type Capability =
// Mission
| 'mission.create'
| 'mission.edit'
| 'mission.assign'
| 'mission.view'
// Artifact
| 'artifact.view'
| 'artifact.download'
| 'artifact.delete'
// Decision
| 'decision.review'
| 'decision.approve'
// Model (Developer Mode)
| 'model.train'
| 'model.deploy'
// Economy
| 'economy.view'
| 'economy.edit'
// Admin
| 'admin.users'
| 'admin.settings'
| 'admin.developerMode';
export interface Role {
readonly id: string;
readonly name: string;
readonly capabilities: Capability[];
}
export const Roles: Record<string, Role> = {
superAdmin: {
id: 'super_admin',
name: 'Super Admin',
capabilities: [
'mission.create', 'mission.edit', 'mission.assign', 'mission.view',
'artifact.view', 'artifact.download', 'artifact.delete',
'decision.review', 'decision.approve',
'model.train', 'model.deploy',
'economy.view', 'economy.edit',
'admin.users', 'admin.settings', 'admin.developerMode',
],
},
operator: {
id: 'operator',
name: 'Operator',
capabilities: [
'mission.create', 'mission.edit', 'mission.assign', 'mission.view',
'artifact.view', 'artifact.download',
'decision.review',
'model.train', 'model.deploy',
'economy.view',
'admin.developerMode',
],
},
reviewer: {
id: 'reviewer',
name: 'Reviewer',
capabilities: [
'mission.view',
'artifact.view', 'artifact.download',
'decision.review', 'decision.approve',
],
},
pilotUser: {
id: 'pilot_user',
name: 'Pilot User',
capabilities: [
'mission.create', 'mission.view',
'artifact.view',
],
},
};
export function hasCapability(role: Role, capability: Capability): boolean {
return role.capabilities.includes(capability);
}
export function hasAnyCapability(role: Role, capabilities: Capability[]): boolean {
return capabilities.some(c => hasCapability(role, c));
}
+50
View File
@@ -0,0 +1,50 @@
/**
* Feature Flags — enable features without new releases
*
* Pilot 001-010: Control which modules are visible.
*/
export type FeatureFlag =
| 'ENABLE_REPLAY'
| 'ENABLE_DATASET_EXPLORER'
| 'ENABLE_MODEL_TRAINING'
| 'ENABLE_HOTSPOTS'
| 'ENABLE_ECONOMIC_ENGINE'
| 'ENABLE_DEVELOPER_MODE';
export interface FeatureFlags {
readonly flags: Record<FeatureFlag, boolean>;
}
export const DefaultFeatureFlags: FeatureFlags = {
flags: {
ENABLE_REPLAY: false,
ENABLE_DATASET_EXPLORER: true,
ENABLE_MODEL_TRAINING: false,
ENABLE_HOTSPOTS: false,
ENABLE_ECONOMIC_ENGINE: false,
ENABLE_DEVELOPER_MODE: false,
},
};
export function isEnabled(flags: FeatureFlags, flag: FeatureFlag): boolean {
return flags.flags[flag] ?? false;
}
export function enable(flags: FeatureFlags, flag: FeatureFlag): FeatureFlags {
return {
flags: {
...flags.flags,
[flag]: true,
},
};
}
export function disable(flags: FeatureFlags, flag: FeatureFlag): FeatureFlags {
return {
flags: {
...flags.flags,
[flag]: false,
},
};
}
+18
View File
@@ -88,6 +88,24 @@ export {
} from './mission/knowledge-gap'; } from './mission/knowledge-gap';
export * from './common/enums'; export * from './common/enums';
// Auth
export {
Capability,
Role,
Roles,
hasCapability,
hasAnyCapability,
} from './auth/capabilities';
export {
FeatureFlag,
FeatureFlags,
DefaultFeatureFlags,
isEnabled,
enable,
disable,
} from './auth/feature-flags';
export * from './common/errors'; export * from './common/errors';
// Artifacts // Artifacts
+25 -1
View File
@@ -1,4 +1,5 @@
import { Routes, Route, Link } from 'react-router-dom'; import { Routes, Route, Link } from 'react-router-dom';
import { useState } from 'react';
import DatasetExplorer from './pages/DatasetExplorer'; import DatasetExplorer from './pages/DatasetExplorer';
import ArtifactViewer from './pages/ArtifactViewer'; import ArtifactViewer from './pages/ArtifactViewer';
import MissionUpload from './pages/MissionUpload'; import MissionUpload from './pages/MissionUpload';
@@ -8,10 +9,26 @@ import PilotChecklist from './pages/PilotChecklist';
import ReadinessDashboard from './pages/ReadinessDashboard'; import ReadinessDashboard from './pages/ReadinessDashboard';
function App() { function App() {
const [developerMode, setDeveloperMode] = useState(false);
return ( return (
<div style={{ fontFamily: 'system-ui, sans-serif', maxWidth: 1200, margin: '0 auto', padding: 20 }}> <div style={{ fontFamily: 'system-ui, sans-serif', maxWidth: 1200, margin: '0 auto', padding: 20 }}>
<header style={{ borderBottom: '2px solid #333', paddingBottom: 20, marginBottom: 20 }}> <header style={{ borderBottom: '2px solid #333', paddingBottom: 20, marginBottom: 20 }}>
<h1>LandveX Intelligence Lab</h1> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h1>LandveX {developerMode ? '(Developer Mode)' : ''}</h1>
<button
onClick={() => setDeveloperMode(!developerMode)}
style={{
padding: '8px 16px',
background: developerMode ? '#ffd700' : '#f0f0f0',
border: '1px solid #ccc',
borderRadius: 4,
cursor: 'pointer',
}}
>
{developerMode ? '🔓 Developer' : '🔒 Normal'}
</button>
</div>
<nav style={{ display: 'flex', gap: 20, marginTop: 10 }}> <nav style={{ display: 'flex', gap: 20, marginTop: 10 }}>
<Link to="/">Dataset Explorer</Link> <Link to="/">Dataset Explorer</Link>
<Link to="/upload">Mission Upload</Link> <Link to="/upload">Mission Upload</Link>
@@ -19,6 +36,13 @@ function App() {
<Link to="/health">Health</Link> <Link to="/health">Health</Link>
<Link to="/pilot">Pilot 001</Link> <Link to="/pilot">Pilot 001</Link>
<Link to="/readiness">Readiness</Link> <Link to="/readiness">Readiness</Link>
{developerMode && (
<>
<Link to="/datasets">Datasets</Link>
<Link to="/models">Models</Link>
<Link to="/training">Training</Link>
</>
)}
</nav> </nav>
</header> </header>