feat(frontend): Q1+Q2 — Plugin-Routen kommen aus Manifesten, statische Duplikate entfernt
Check Cross-Plugin Imports / check (push) Has been cancelled

Phase Q1 (Seiten-Routen) + Q2 (Settings-Routen): Die Plugin-Manifeste sind
ab sofort die einzige Quelle fuer Plugin-Frontend-Routen. routes/index.tsx
enthaelt nur noch Core-Routen + die StartLayout-Hub-Baeume (/agents,
/automation, /logs, /help — verschachtelte Sub-Navigation).

- PluginRouteRenderer: variante 'settings' rendert settings_pages mit
  bare Sub-Segments (Descendant-Matching im /settings-Subtree); Variante
  'pages' (Default) behaelt absolute Pfade. Getrennte Entry-Listen
  verhindern Pfad-Kollisionen (settings 'mail' vs. page '/mail').
- Entfernt: 14 statische AppShell-Plugin-Routen + 9 statische
  Settings-Routen + 20 tote Lazy-Imports (search, calendar+kanban, dms,
  dms/trash, mail, mail/settings, reports, tasks, communication,
  workflows, import-export, tags, wiki, roles, users, groups,
  notifications, ai, ai-proactive, automation, documents).
- Manifeste ergaenzt: Calendar +/calendar/kanban, Tags +/tags (+ Menue-Item,
  Route war sonst unerreichbar), Automation: /workflows-Permission auf
  workflows:read (Paritaet zur ersetzten statischen Route).
- Automation: tote flache Manifest-Eintraeger fuer /agents + /automation
  entfernt (StartLayout-Hub-Baeume gewinnen diese Pfade immer — die
  Eintraege matchten nie).
- Komponenten-Map regeneriert (37 Eintraege, CalendarKanban + Tags neu).

Verifikation: tsc exit 0; production build exit 0; Vitest Dashboard +
MiniAppWindow + pluginStore 35/35; Backend-Regressionen Route-Order,
M5-MiniApps, N4-Scope, N3-Filtering 49/49; compileall sauber;
Cross-Plugin-Checker 497 Dateien / 0 verbotene Imports; ruff clean.
This commit is contained in:
Agent Zero
2026-09-13 08:56:29 +02:00
parent 895f85dde0
commit b666fe5b4c
6 changed files with 81 additions and 72 deletions
@@ -22,7 +22,19 @@ import { PluginPage } from './PluginLoader';
* route group in routes/index.tsx:
* { path: '*', element: <PluginRouteRenderer /> }
*/
export function PluginRouteRenderer() {
/**
* variant:
* - 'pages' (default): renders manifest page_routes. Mounted at the
* AppShell catch-all — descendant <Routes> match the full pathname,
* so entries keep their absolute paths (e.g. '/mail').
* - 'settings': renders manifest settings_pages. Mounted INSIDE the
* /settings subtree catch-all — descendant matching works on the
* REMAINING pathname ('/mail' after the '/settings' prefix is stripped),
* so entries keep the bare settings sub-segment ('mail'). Mixing both
* entry kinds in one list would create path collisions (e.g. settings
* 'mail' vs. page '/mail' rendering the inbox).
*/
export function PluginRouteRenderer({ variant = 'pages' }: { variant?: 'pages' | 'settings' } = {}) {
const location = useLocation();
const manifests = usePluginStore((s) => s.manifests);
const loaded = usePluginStore((s) => s.loaded);
@@ -30,16 +42,33 @@ export function PluginRouteRenderer() {
// Flatten manifests → route entries with their owning plugin attached.
// Attaching the name here (instead of a later find()) stays correct even
// if two plugins declare overlapping paths.
const entries = useMemo(
() =>
manifests.flatMap((m) =>
m.page_routes.map((r) => ({
route: r,
pluginName: m.display_name || m.name || r.path,
type RendererEntry = {
route: { path: string; component: string; permission?: string };
pluginName: string;
};
const entries = useMemo<RendererEntry[]>(() => {
if (variant === 'settings') {
return manifests.flatMap((m) =>
(Array.isArray(m.settings_pages) ? m.settings_pages : []).map((p) => ({
route: {
// bare sub-segment ('mail') — descendant matching strips the
// matched '/settings' prefix; tolerate 'settings/mail' too.
path: p.path.replace(/^\/?settings\//, ''),
component: p.component,
permission: p.permission || undefined,
},
pluginName: m.display_name || m.name || p.path,
}))
),
[manifests]
);
);
}
return manifests.flatMap((m) =>
m.page_routes.map((r) => ({
route: r,
pluginName: m.display_name || m.name || r.path,
}))
);
}, [manifests, variant]);
// If manifests haven't loaded yet, show a spinner (not null/blank)
if (!loaded) {