mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
feat(plugins): plugin system scaffold — registry tables + admin panel
First slice of the plugin system. Lays down the data model and a read-only admin surface; nothing executes yet. - Migration 155: plugins, plugin_meta_migrations, plugin_error_log and plugin_settings_fields tables. Plugin data will live in a per-plugin sqlite file under /plugins-data, never in these tables. - New Nest module server/src/nest/plugins with GET /api/admin/plugins (admin-gated, returns the installed list + the runtime-enabled flag). - TREK_PLUGINS_ENABLED kill switch (config.pluginsEnabled), off by default. - Admin → Plugins tab with a read-only panel: installed list, status badges, and a clear banner when the runtime is disabled by server config. - i18n keys for the tab and panel across all locales. Install, activation, the isolated runtime and the registry browser follow in later slices.
This commit is contained in:
@@ -460,6 +460,7 @@ export const adminApi = {
|
||||
updateOidc: (data: Record<string, unknown>) => apiClient.put('/admin/oidc', data).then(r => r.data),
|
||||
addons: () => apiClient.get('/admin/addons').then(r => r.data),
|
||||
updateAddon: (id: number | string, data: Record<string, unknown>) => apiClient.put(`/admin/addons/${id}`, data).then(r => r.data),
|
||||
plugins: () => apiClient.get('/admin/plugins').then(r => r.data),
|
||||
// Local LLM (Ollama) management for the AI-parsing addon.
|
||||
llmLocalModels: (baseUrl: string): Promise<{ models: { name: string; size: number }[] }> =>
|
||||
apiClient.get('/admin/llm/local/models', { params: { baseUrl } }).then(r => r.data),
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Blocks, AlertTriangle, PackageOpen } from 'lucide-react'
|
||||
import { adminApi } from '../../api/client'
|
||||
import { useTranslation } from '../../i18n'
|
||||
|
||||
/**
|
||||
* Admin → Plugins (#plugins), M0 read-only scaffold. Lists installed plugins
|
||||
* from the registry table and shows whether the runtime is enabled
|
||||
* (TREK_PLUGINS_ENABLED). Install, activation and the registry browser arrive in
|
||||
* later milestones; this panel is intentionally inert.
|
||||
*/
|
||||
|
||||
interface PluginRow {
|
||||
id: string
|
||||
name: string
|
||||
description: string | null
|
||||
type: string
|
||||
icon: string | null
|
||||
version: string | null
|
||||
status: string
|
||||
reviewed_at: string | null
|
||||
source_repo: string | null
|
||||
}
|
||||
|
||||
const STATUS_CLASS: Record<string, string> = {
|
||||
active: 'bg-emerald-500/15 text-emerald-600',
|
||||
inactive: 'bg-surface-tertiary text-content-muted',
|
||||
disabled: 'bg-amber-500/15 text-amber-600',
|
||||
error: 'bg-rose-500/15 text-rose-600',
|
||||
incompatible: 'bg-orange-500/15 text-orange-600',
|
||||
}
|
||||
|
||||
export default function AdminPluginsPanel() {
|
||||
const { t } = useTranslation()
|
||||
const [enabled, setEnabled] = useState(false)
|
||||
const [plugins, setPlugins] = useState<PluginRow[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true
|
||||
adminApi.plugins()
|
||||
.then((d: { enabled: boolean; plugins: PluginRow[] }) => {
|
||||
if (!alive) return
|
||||
setEnabled(!!d.enabled)
|
||||
setPlugins(d.plugins || [])
|
||||
})
|
||||
.catch(() => { if (alive) setError(true) })
|
||||
.finally(() => { if (alive) setLoading(false) })
|
||||
return () => { alive = false }
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="bg-surface-card border border-edge rounded-xl overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-edge-secondary flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-xl flex items-center justify-center bg-surface-tertiary">
|
||||
<Blocks size={17} className="text-content-muted" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-base font-semibold text-content">{t('admin.plugins.title')}</h2>
|
||||
<p className="text-xs text-content-faint mt-0.5">{t('admin.plugins.subtitle')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!enabled && !loading && !error && (
|
||||
<div className="mx-6 mt-4 p-4 rounded-lg border border-amber-500/30 bg-amber-500/10 flex items-start gap-3">
|
||||
<AlertTriangle size={16} className="text-amber-600 mt-0.5 shrink-0" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-amber-700">{t('admin.plugins.disabledTitle')}</p>
|
||||
<p className="text-xs text-amber-700/90 mt-0.5">{t('admin.plugins.disabledBody')}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-6">
|
||||
{loading ? (
|
||||
<div className="py-8 text-center text-sm text-content-faint">{t('common.loading')}</div>
|
||||
) : error ? (
|
||||
<div className="py-8 text-center text-sm text-rose-600">{t('admin.plugins.loadError')}</div>
|
||||
) : plugins.length === 0 ? (
|
||||
<div className="py-10 text-center">
|
||||
<PackageOpen size={28} className="mx-auto text-content-faint mb-3" />
|
||||
<p className="text-sm text-content-muted">{t('admin.plugins.empty')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-edge-secondary">
|
||||
{plugins.map(p => (
|
||||
<div key={p.id} className="flex items-center gap-4 py-3 first:pt-0 last:pb-0">
|
||||
<div className="w-10 h-10 rounded-xl flex items-center justify-center bg-surface-tertiary shrink-0">
|
||||
<Blocks size={16} className="text-content-muted" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-content truncate">{p.name}</span>
|
||||
{p.version && <span className="text-xs text-content-faint">v{p.version}</span>}
|
||||
</div>
|
||||
{p.description && <p className="text-xs text-content-faint truncate mt-0.5">{p.description}</p>}
|
||||
</div>
|
||||
<span className={`shrink-0 rounded-full text-[10px] font-medium px-2 py-0.5 ${STATUS_CLASS[p.status] || STATUS_CLASS.inactive}`}>
|
||||
{t(`admin.plugins.status.${p.status}` as never)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="px-6 py-3 border-t border-edge-secondary bg-surface-secondary">
|
||||
<p className="text-xs text-content-faint">{t('admin.plugins.scaffoldNote')}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -11,7 +11,8 @@ import AddonManager from '../components/Admin/AddonManager'
|
||||
import PackingTemplateManager from '../components/Admin/PackingTemplateManager'
|
||||
import AuditLogPanel from '../components/Admin/AuditLogPanel'
|
||||
import AdminMcpTokensPanel from '../components/Admin/AdminMcpTokensPanel'
|
||||
import { Users, Map, Briefcase, Shield, FileText, SlidersHorizontal, UserCog, Puzzle, Settings as SettingsIcon, Bell, Database, ScrollText, KeyRound, GitBranch, Bug } from 'lucide-react'
|
||||
import AdminPluginsPanel from '../components/Admin/AdminPluginsPanel'
|
||||
import { Users, Map, Briefcase, Shield, FileText, SlidersHorizontal, UserCog, Puzzle, Blocks, Settings as SettingsIcon, Bell, Database, ScrollText, KeyRound, GitBranch, Bug } from 'lucide-react'
|
||||
import PageSidebar, { type PageSidebarTab } from '../components/Layout/PageSidebar'
|
||||
import { useAdmin } from './admin/useAdmin'
|
||||
import AdminUpdateBanner from './admin/AdminUpdateBanner'
|
||||
@@ -45,6 +46,7 @@ export default function AdminPage(): React.ReactElement {
|
||||
{ id: 'config', label: t('admin.tabs.config'), icon: SlidersHorizontal, group: gConfig },
|
||||
{ id: 'settings', label: t('admin.tabs.settings'), icon: SettingsIcon, group: gConfig },
|
||||
{ id: 'addons', label: t('admin.tabs.addons'), icon: Puzzle, group: gConfig },
|
||||
{ id: 'plugins', label: t('admin.tabs.plugins'), icon: Blocks, group: gConfig },
|
||||
{ id: 'notifications', label: t('admin.tabs.notifications'), icon: Bell, group: gIntegration },
|
||||
...(mcpEnabled ? [{ id: 'mcp-tokens', label: t('admin.tabs.mcpTokens'), icon: KeyRound, group: gIntegration }] : []),
|
||||
{ id: 'github', label: t('admin.tabs.github'), icon: GitBranch, group: gIntegration },
|
||||
@@ -157,6 +159,8 @@ export default function AdminPage(): React.ReactElement {
|
||||
|
||||
{activeTab === 'mcp-tokens' && <AdminMcpTokensPanel />}
|
||||
|
||||
{activeTab === 'plugins' && <AdminPluginsPanel />}
|
||||
|
||||
{activeTab === 'github' && <GitHubPanel isPrerelease={updateInfo?.is_prerelease ?? false} />}
|
||||
|
||||
{activeTab === 'defaults' && <DefaultUserSettingsTab />}
|
||||
|
||||
Reference in New Issue
Block a user