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:
curo1305
2026-04-18 02:09:50 +02:00
parent 2d7207b62f
commit 00466a9801
29 changed files with 1373 additions and 52 deletions
+44 -1
View File
@@ -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>