feat(pro): generalize pro tier to multi-surface extension points
Move the pro mount decnet/services/pro/ -> decnet/pro/ so the Professional tier
can contribute to more than honeypots. The core wires each surface only when
decnet/pro/ is present (absence stays the entitlement gate):
* services — registry scans decnet/pro/services/ (was decnet/services/pro/)
* API routes — decnet/pro/routes.py exposes ROUTERS, mounted under /api/v1
* web pages — Vite aliases @pro to the pro frontend (community -> empty stub),
App.tsx maps proRoutes into <Route>s, Layout renders a
PROFESSIONAL nav group; both tree-shake out of the community build
Frontend gate mirrors the existing VITE_DECNET_DEVELOPER tree-shake pattern.
Tests: registry + router seams (backend), empty-stub contract (frontend).
This commit is contained in:
@@ -10,6 +10,7 @@ import { ToastProvider } from './components/Toasts/ToastProvider';
|
||||
import { useToast } from './components/Toasts/useToast';
|
||||
import { useGlobalHotkeys } from './hooks/useGlobalHotkeys';
|
||||
import { isDeveloperMode } from './lib/devGate';
|
||||
import { proRoutes } from '@pro';
|
||||
|
||||
// Page components are code-split per route. Each lands as its own
|
||||
// chunk and only downloads when the user navigates to that path —
|
||||
@@ -148,6 +149,10 @@ const AuthedShell: React.FC<AuthedShellProps> = ({ onLogout, onSearch, searchQue
|
||||
{isDeveloperMode() && (
|
||||
<Route path="/theme-lab" element={<ThemeLab />} />
|
||||
)}
|
||||
{/* Professional-tier pages. Empty in the community build (@pro -> stub). */}
|
||||
{proRoutes.map((r) => (
|
||||
<Route key={r.path} path={r.path} element={r.element} />
|
||||
))}
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from '../icons';
|
||||
import { prefetchRoute } from '../routePrefetch';
|
||||
import { useThemeToggle } from '../lib/useThemeToggle';
|
||||
import { proRoutes } from '@pro';
|
||||
import './Layout.css';
|
||||
|
||||
type ThreatLevel = 'nominal' | 'elevated' | 'critical';
|
||||
@@ -160,6 +161,13 @@ const Layout: React.FC<LayoutProps> = ({
|
||||
<NavItem to="/swarm/hosts" icon={<HardDrive size={18} />} label="SWARM Hosts" open={sidebarOpen} indent />
|
||||
<NavItem to="/swarm-updates" icon={<Package size={18} />} label="Remote Updates" open={sidebarOpen} indent />
|
||||
</NavGroup>
|
||||
{proRoutes.length > 0 && (
|
||||
<NavGroup label="PROFESSIONAL" icon={<Zap size={20} />} open={sidebarOpen}>
|
||||
{proRoutes.map((r) => (
|
||||
<NavItem key={r.path} to={r.path} icon={r.icon} label={r.label} open={sidebarOpen} indent />
|
||||
))}
|
||||
</NavGroup>
|
||||
)}
|
||||
<NavItem to="/config" icon={<Settings size={20} />} label="Config" open={sidebarOpen} />
|
||||
</nav>
|
||||
|
||||
|
||||
10
decnet_web/src/pro/pro.test.ts
Normal file
10
decnet_web/src/pro/pro.test.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
import { proRoutes } from '@pro';
|
||||
|
||||
// In the community build, `@pro` resolves to the stub: no Professional pages,
|
||||
// so App's route map and Layout's nav group both tree-shake to nothing.
|
||||
describe('pro tier — community build', () => {
|
||||
it('ships no pro routes', () => {
|
||||
expect(proRoutes).toEqual([]);
|
||||
});
|
||||
});
|
||||
8
decnet_web/src/pro/stub.ts
Normal file
8
decnet_web/src/pro/stub.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Community build: no Professional pages. `@pro` resolves here unless the build
|
||||
// sets VITE_DECNET_PRO=1 with decnet/pro/web/ present, in which case Vite
|
||||
// aliases `@pro` to the real registry. proRoutes being empty lets the router
|
||||
// and nav tree-shake the pro surface out of the community bundle.
|
||||
import type { ProRoute } from './types';
|
||||
|
||||
export const proRoutes: ProRoute[] = [];
|
||||
15
decnet_web/src/pro/types.ts
Normal file
15
decnet_web/src/pro/types.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Contract for Professional-tier UI pages. The pro build aliases `@pro` to the
|
||||
// real registry in decnet/pro/web/; the community build resolves it to ./stub.
|
||||
import type { ReactElement, ReactNode } from 'react';
|
||||
|
||||
export interface ProRoute {
|
||||
/** Router path, e.g. "/pro/intel". Convention: prefix pro routes with /pro. */
|
||||
path: string;
|
||||
/** Sidebar label. */
|
||||
label: string;
|
||||
/** Sidebar icon (lucide-react element), optional. */
|
||||
icon?: ReactNode;
|
||||
/** Page element rendered at `path`. May be a lazy component (App wraps Suspense). */
|
||||
element: ReactElement;
|
||||
}
|
||||
Reference in New Issue
Block a user