import { useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { Bot, Check, Copy, KeyRound, RefreshCw, ShieldCheck, UserPlus, Users, } from 'lucide-react'; import { TEAM_LABELS, TEAM_ROLES, TEAMS, type Team, type TeamRole } from '@pig/core'; import { api, get, patch, post, relativeTime } from '@/lib/api'; import { Badge, Button, Card, CardContent, CardHeader, CardTitle, Input } from '@/components/ui'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Switch } from '@/components/ui/switch'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { IntegrationSettings } from './IntegrationSettings'; interface AdminRuntimeSettings { piggyModel: string; piggyInferenceBase: string; piggyEnabled: boolean; primeComputeBase: string; primeApiKey: { configured: boolean; source: 'database' | 'environment' | null; updatedAt: string | null; encryptionReady: boolean; }; primeSyncEnabled: boolean; primeSyncIntervalMinutes: number; updatedAt: string; } interface Invite { id: string; email: string | null; team: Team | null; role: TeamRole; usesRemaining: number; expiresAt: string | null; createdAt: string; status: 'active' | 'used' | 'expired' | 'revoked'; } interface Member { id: string; name: string; email: string; title: string | null; isPlatformAdmin: boolean; adminSource: 'environment' | 'database' | null; memberships: { team: Team; role: TeamRole }[]; } export function AdminSettings() { const { data, isLoading } = useQuery({ queryKey: ['admin-settings'], queryFn: () => get('/api/admin/settings'), }); return (

Platform control plane

Admin only

Configure intelligence, inventory sync, workspace entry, and team authority.

Runtime Invites Access Integrations {isLoading || !data ?

Loading runtime settings…

: }
); } function RuntimeForm({ settings }: { settings: AdminRuntimeSettings }) { const queryClient = useQueryClient(); const [model, setModel] = useState(settings.piggyModel); const [inferenceBase, setInferenceBase] = useState(settings.piggyInferenceBase); const [piggyEnabled, setPiggyEnabled] = useState(settings.piggyEnabled); const [syncEnabled, setSyncEnabled] = useState(settings.primeSyncEnabled); const [interval, setIntervalValue] = useState(String(settings.primeSyncIntervalMinutes)); const [primeApiKey, setPrimeApiKey] = useState(''); const [clearKey, setClearKey] = useState(false); const [message, setMessage] = useState(null); const save = useMutation({ mutationFn: () => patch('/api/admin/settings', { piggyModel: model, piggyInferenceBase: inferenceBase, piggyEnabled, primeSyncEnabled: syncEnabled, primeSyncIntervalMinutes: Number(interval), ...(primeApiKey ? { primeApiKey } : {}), ...(clearKey ? { clearPrimeApiKey: true } : {}), }), onSuccess: () => { setPrimeApiKey(''); setClearKey(false); setMessage('Runtime settings saved. Prime sync is reloading in the background.'); void queryClient.invalidateQueries({ queryKey: ['admin-settings'] }); }, }); return (
{ event.preventDefault(); setMessage(null); save.mutate(); }}>
Piggy intelligence

Inference is deliberately isolated from the compute API.

Prime inventory

Compute endpoint: {settings.primeComputeBase}

{settings.primeApiKey.configured ? 'Credential configured' : 'Credential missing'} {settings.primeApiKey.source ? {settings.primeApiKey.source} source : null} {settings.primeApiKey.updatedAt ? updated {relativeTime(settings.primeApiKey.updatedAt)} : null}
{settings.primeApiKey.source === 'database' ? : null}
{save.error ?

{save.error.message}

: null} {message ?

{message}

: null}
); } function ToggleRow({ id, label, description, checked, onCheckedChange }: { id: string; label: string; description: string; checked: boolean; onCheckedChange(value: boolean): void }) { return

{description}

; } function InviteManager() { const queryClient = useQueryClient(); const { data = [] } = useQuery({ queryKey: ['admin-invites'], queryFn: () => get('/api/admin/invites') }); const [email, setEmail] = useState(''); const [team, setTeam] = useState('any'); const [role, setRole] = useState('member'); const [uses, setUses] = useState('1'); const [expiresAt, setExpiresAt] = useState(''); const [issuedCode, setIssuedCode] = useState(null); const create = useMutation({ mutationFn: () => post('/api/admin/invites', { ...(email ? { email } : {}), ...(team !== 'any' ? { team } : {}), role, usesRemaining: Number(uses), ...(expiresAt ? { expiresAt: new Date(expiresAt).toISOString() } : {}) }), onSuccess: (invite) => { setIssuedCode(invite.code); setEmail(''); void queryClient.invalidateQueries({ queryKey: ['admin-invites'] }); }, }); const revoke = useMutation({ mutationFn: (id: string) => api(`/api/admin/invites/${id}`, { method: 'DELETE' }), onSuccess: () => void queryClient.invalidateQueries({ queryKey: ['admin-invites'] }) }); return
Issue an invite

Codes gate PIG registration. They never open registration on the shared identity provider.

{ event.preventDefault(); setIssuedCode(null); create.mutate(); }}>
{create.error ?

{create.error.message}

: null} {issuedCode ?

Shown once. Send it through a secure channel.

{issuedCode}
: null}
Invite ledger

Only metadata remains visible after issuance.

{data.length === 0 ?

No invites issued yet.

: data.map((invite) =>

{invite.email ?? 'Workspace invite'}

{invite.status}

{invite.team ? TEAM_LABELS[invite.team] : 'Team chosen at signup'} · {invite.role} · {invite.usesRemaining} use{invite.usesRemaining === 1 ? '' : 's'} left

{invite.status === 'active' ? : null}
)}
; } function MemberManager() { const { data = [] } = useQuery({ queryKey: ['admin-members'], queryFn: () => get('/api/admin/members') }); return

Team and role administration

Roles are team-scoped. Platform administration is a separate grant.

{data.map((member) => )}
; } function MemberAccess({ member }: { member: Member }) { const queryClient = useQueryClient(); const [isPlatformAdmin, setIsPlatformAdmin] = useState(member.isPlatformAdmin); const [roles, setRoles] = useState>>(() => Object.fromEntries(member.memberships.map(({ team, role }) => [team, role]))); const save = useMutation({ mutationFn: () => patch(`/api/admin/members/${member.id}/access`, { isPlatformAdmin, memberships: TEAMS.flatMap((team) => roles[team] ? [{ team, role: roles[team] }] : []) }), onSuccess: () => void queryClient.invalidateQueries({ queryKey: ['admin-members'] }) }); return

{member.name}

{member.isPlatformAdmin ? Platform admin : null}

{member.email}

{TEAMS.map((team) => )}
{member.adminSource === 'environment' ?

Pinned by environment

: null}
{save.error ?

{save.error.message}

: null}
; }