Add generic plugin architecture and watch-directory feature
Introduces a manifest contract so feature containers self-describe their settings (JSON Schema + access rules). Backend and frontend gain generic plugin proxy and dynamic Extensions UI with zero feature-specific code. Doc-service is the first plugin consumer: exposes /plugin/manifest and /plugin/settings, adds a watchdog-based file watcher that auto-ingests PDFs from a mounted directory, maps subfolders to categories, supports AI-suggested folder/filename (user-confirmed), and enforces a no-remove policy. Access is gated by is_superuser or doc-service-admin group. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,11 +16,12 @@ import {
|
||||
Users,
|
||||
UsersRound,
|
||||
Palette,
|
||||
Puzzle,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import ThemeToggle from "@/components/ThemeToggle";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { getMe, listCategories } from "@/api/client";
|
||||
import { getMe, getPlugins, listCategories } from "@/api/client";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export default function Sidebar() {
|
||||
@@ -56,6 +57,14 @@ export default function Sidebar() {
|
||||
enabled: appsOpen && docsOpen && !!user,
|
||||
});
|
||||
|
||||
const { data: plugins = [] } = useQuery({
|
||||
queryKey: ["plugins"],
|
||||
queryFn: getPlugins,
|
||||
enabled: !!user,
|
||||
// Empty array on 404/error — regular users simply see no plugins
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const navItemClass = (isActive: boolean) =>
|
||||
cn(
|
||||
"flex items-center rounded-lg transition-colors",
|
||||
@@ -209,6 +218,40 @@ export default function Sidebar() {
|
||||
)}
|
||||
</NavLink>
|
||||
|
||||
{/* Extensions — visible only when the user has accessible plugins */}
|
||||
{plugins.length > 0 && (
|
||||
<div>
|
||||
{sidebarExpanded ? (
|
||||
<>
|
||||
<div className="px-3 py-1.5">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-muted">
|
||||
Extensions
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
{plugins.map((plugin) => (
|
||||
<NavLink
|
||||
key={plugin.id}
|
||||
to={`/settings/plugins/${plugin.id}`}
|
||||
className={({ isActive }) => subItemClass(isActive)}
|
||||
>
|
||||
<Puzzle className="h-4 w-4 shrink-0" />
|
||||
<span className="whitespace-nowrap truncate">{plugin.name}</span>
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<NavLink
|
||||
to={`/settings/plugins/${plugins[0].id}`}
|
||||
className={({ isActive }) => navItemClass(isActive)}
|
||||
>
|
||||
<Puzzle className="h-5 w-5 shrink-0" />
|
||||
</NavLink>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Admin — expandable */}
|
||||
{user?.is_admin && (
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user