New admin tab "AI" (app/(main)/admin/_tabs/ai-policy.tsx): provider-class toggles, server model allow-list, BYOK provider allow-list, seats/usage (front-end for the already-real lib/ai/entitlement.ts), retrieval on/off, consent text + version bump. Real backend, not cosmetic: AiConsoleConfig persisted via config-manager (lib/ai/types.ts, ai-policy.json in the CONFIG dir). New GET/PUT /api/admin/ai/policy. Enforcement wired at every real chokepoint, not just the picker: /api/ai/server/chat checks classesEnabled.server and the model allow-list, /api/ai/retrieve checks retrievalEnabled, /api/ai/server/models filters by allow-list. GET /api/ai/policy folds classesEnabled into the classes list clients see. Resolved the spec's 3 open questions as recommended: BYOK allow-list stays client-side/advisory (wired into ai-assistant-settings.tsx's addProfile), tier picker stays cosmetic, master aiAssistantEnabled toggle stays in the existing Policy tab (this tab links to it instead of duplicating it). Defaults preserve today's behavior exactly (classesEnabled/allowlists all start empty/null) — turning this on changes nothing until an admin touches it.
52 lines
2.0 KiB
TypeScript
52 lines
2.0 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect } from 'react';
|
|
import { useAdminTabStore, isAdminTab } from '@/stores/admin-tab-store';
|
|
import { DashboardTab } from './_tabs/dashboard';
|
|
import { SettingsTab } from './_tabs/settings';
|
|
import { BrandingTab } from './_tabs/branding';
|
|
import { AuthTab } from './_tabs/auth';
|
|
import { PolicyTab } from './_tabs/policy';
|
|
import { AiPolicyTab } from './_tabs/ai-policy';
|
|
import { PluginsTab } from './_tabs/plugins';
|
|
import { ThemesTab } from './_tabs/themes';
|
|
import { MarketplaceTab } from './_tabs/marketplace';
|
|
import { VersionTab } from './_tabs/version';
|
|
import { TelemetryTab } from './_tabs/telemetry';
|
|
import { LogsTab } from './_tabs/logs';
|
|
|
|
export default function AdminPage() {
|
|
const activeTab = useAdminTabStore((s) => s.activeTab);
|
|
const setActiveTab = useAdminTabStore((s) => s.setActiveTab);
|
|
|
|
// Honour deep links from the old route structure: /admin?tab=settings
|
|
// (emitted by the redirect pages in /admin/<x>/page.tsx) sets the store
|
|
// once on mount, then strips the param so the URL stays at /admin and
|
|
// subsequent tab clicks don't accumulate query strings.
|
|
useEffect(() => {
|
|
if (typeof window === 'undefined') return;
|
|
const url = new URL(window.location.href);
|
|
const fromUrl = url.searchParams.get('tab');
|
|
if (isAdminTab(fromUrl)) {
|
|
setActiveTab(fromUrl);
|
|
url.searchParams.delete('tab');
|
|
window.history.replaceState(null, '', url.pathname + url.search + url.hash);
|
|
}
|
|
}, [setActiveTab]);
|
|
|
|
switch (activeTab) {
|
|
case 'dashboard': return <DashboardTab />;
|
|
case 'settings': return <SettingsTab />;
|
|
case 'branding': return <BrandingTab />;
|
|
case 'auth': return <AuthTab />;
|
|
case 'policy': return <PolicyTab />;
|
|
case 'ai-policy': return <AiPolicyTab />;
|
|
case 'plugins': return <PluginsTab />;
|
|
case 'themes': return <ThemesTab />;
|
|
case 'marketplace': return <MarketplaceTab />;
|
|
case 'version': return <VersionTab />;
|
|
case 'telemetry': return <TelemetryTab />;
|
|
case 'logs': return <LogsTab />;
|
|
}
|
|
}
|