/** * Admin plugin management page. * URL: /admin/plugins * Tabbed view: "Installed" lists plugins with controls, "Browse" searches the registry. * @see specs/prd-web.md Section M13 */ 'use client' import { useState, useEffect, useCallback } from 'react' import { PuzzlePiece, MagnifyingGlass } from '@phosphor-icons/react' import { cn } from '@/lib/utils' import { AdminLayout } from '@/components/admin/admin-layout' import { ErrorAlert } from '@/components/error-alert' import { PluginCard } from '@/components/admin/plugins/plugin-card' import { RegistryPluginCard } from '@/components/admin/plugins/registry-plugin-card' import { PluginSettingsModal } from '@/components/admin/plugins/plugin-settings-modal' import { DependencyWarningDialog } from '@/components/admin/plugins/dependency-warning-dialog' import { usePluginManagement } from '@/hooks/admin/use-plugin-management' import { useRegistrySearch } from '@/hooks/admin/use-registry-search' import { useAuth } from '@/hooks/use-auth' import { installPlugin, getFeaturedPlugins } from '@/lib/api/client' import type { RegistryPlugin } from '@/lib/api/types' type PluginTab = 'installed' | 'browse' export default function AdminPluginsPage() { const { getAccessToken } = useAuth() const [tab, setTab] = useState('installed') const [searchQuery, setSearchQuery] = useState('') const [installingName, setInstallingName] = useState(null) const [installError, setInstallError] = useState(null) const [registryVersions, setRegistryVersions] = useState>(new Map()) const { plugins, loading, settingsPlugin, setSettingsPlugin, dependencyWarning, setDependencyWarning, loadError, actionError, setActionError, fetchPlugins, handleToggle, confirmDisable, handleSaveSettings, handleUninstall, settingsSaveStatus, } = usePluginManagement() const registry = useRegistrySearch() // Fetch featured plugins on mount to get registry versions for update comparison useEffect(() => { async function loadRegistryVersions() { try { const response = await getFeaturedPlugins() const versions = new Map() for (const plugin of response.plugins) { versions.set(plugin.name, plugin.version) } setRegistryVersions(versions) } catch { // Non-critical: version comparison is a nice-to-have } } void loadRegistryVersions() }, []) const installedNames = new Set(plugins.map((p) => p.name)) const handleSearch = useCallback(() => { void registry.search({ q: searchQuery || undefined }) }, [registry, searchQuery]) const handleInstall = useCallback( async (plugin: RegistryPlugin) => { setInstallingName(plugin.name) setInstallError(null) try { await installPlugin(plugin.name, plugin.version, getAccessToken() ?? '') await fetchPlugins() } catch { setInstallError(`Failed to install ${plugin.displayName}. Please try again.`) } finally { setInstallingName(null) } }, [getAccessToken, fetchPlugins] ) const hasUpdate = useCallback( (pluginName: string, installedVersion: string): boolean => { const registryVersion = registryVersions.get(pluginName) if (!registryVersion) return false return registryVersion !== installedVersion }, [registryVersions] ) return (

Plugins

{tab === 'installed' && (
{loadError && ( void fetchPlugins()} /> )} {actionError && ( setActionError(null)} /> )} {loading && (
{[1, 2, 3].map((i) => (
))}
)} {!loading && !loadError && plugins.length === 0 && (
)} {!loading && plugins.length > 0 && (
{plugins.map((plugin) => (
setSettingsPlugin(p)} onToggle={(p) => void handleToggle(p)} onUninstall={(p) => void handleUninstall(p)} /> {hasUpdate(plugin.name, plugin.version) && ( Update available )}
))}
)}
)} {tab === 'browse' && (
{installError && ( setInstallError(null)} /> )} {registry.error && } {registry.loading && (
{[1, 2, 3].map((i) => (
))}
)} {!registry.loading && registry.hasSearched && registry.results.length === 0 && !registry.error && (
)} {!registry.loading && !registry.hasSearched && (
)} {!registry.loading && registry.results.length > 0 && (
{registry.results.map((plugin) => ( void handleInstall(p)} installing={installingName === plugin.name} /> ))}
)}
)}
{settingsPlugin && ( setSettingsPlugin(null)} onSave={(settings) => void handleSaveSettings(settings)} saveStatus={settingsSaveStatus} /> )} {dependencyWarning && ( void confirmDisable()} onCancel={() => setDependencyWarning(null)} /> )} ) }