feat(M4): System-Rueckbau — Dashboard-Inhalte als MiniApps, Core = reiner Host (#362)
Check Cross-Plugin Imports / check (push) Has been cancelled

- system_miniapps.py: audit_activity (audit:read, settings max_items) + system_metrics (settings:read) als Core-Apps in Registry
- base.py-Fix: native Manifest-MiniApps reichen component durch (M1-Luecke)
- contacts-Manifest: contacts_stats (ContactsStatsWidget, contacts:read, show_companies/show_persons)
- Seed-Fix: nur renderbare Apps (component) landen im Dashboard-Layout
- Frontend: ContactsStatsWidget, AuditActivityWidget, SystemMetricsWidget; MiniAppHost-Registry +3
- Dashboard.tsx = reiner Host (26 Z.); Page-Tests auf Pure-Host umgeschrieben
- Tests: M4 7/7 (TDD rot->gruen), Backend-Regression 46/46, Vitest 22/22, tsc clean, build OK
This commit is contained in:
Agent Zero
2026-08-30 22:22:20 +02:00
parent 9e254176c9
commit 3c496f4b6a
15 changed files with 463 additions and 237 deletions
+63
View File
@@ -0,0 +1,63 @@
"""Core-owned system MiniApps (Phase M4).
Host-level MiniApps that are not owned by a single plugin: audit activity
feed and system metrics. They register in the universal registry with
``plugin_name="system"`` at app startup and unregister with the registry
reset (tests) — they never depend on plugin activation state.
Permissions follow the owning data source:
- audit_activity -> audit:read (audit log route guard, CORE_PERMISSIONS)
- system_metrics -> settings:read (Roadmap M4; the /system/dashboard
endpoint itself stays require_admin — the widget degrades gracefully
with a permission hint for non-admins)
"""
from __future__ import annotations
from app.plugins.miniapp_registry import get_miniapp_registry
SYSTEM_PLUGIN_NAME = "system"
def register_system_miniapps() -> None:
"""Register the core system MiniApps in the universal registry."""
registry = get_miniapp_registry()
registry.register(
app_id="audit_activity",
name="Aktivitäten",
icon="History",
description="Letzte Aktivitäten aus dem Audit-Log (Benutzer, Aktion, Zeitpunkt).",
plugin_name=SYSTEM_PLUGIN_NAME,
permission="audit:read",
settings_schema={
"fields": [
{
"name": "max_items",
"label": "Max. Einträge",
"type": "number",
"default": 10,
}
]
},
col_span=2,
row_span=1,
hosts=["chat", "dashboard", "window"],
component="@/components/dashboard/AuditActivityWidget",
order=40,
)
registry.register(
app_id="system_metrics",
name="System Status",
icon="Server",
description="Datenbank-, Redis-, Worker- und API-Metriken (Administration).",
plugin_name=SYSTEM_PLUGIN_NAME,
permission="settings:read",
settings_schema={},
col_span=2,
row_span=1,
hosts=["chat", "dashboard", "window"],
component="@/components/dashboard/SystemMetricsWidget",
order=50,
)
+5
View File
@@ -218,6 +218,11 @@ async def lifespan(app: FastAPI):
registry.initialize(get_migration_engine(), app)
registry.discover_builtins()
# Core system MiniApps (Phase M4): host-level, independent of plugin state
from app.core.system_miniapps import register_system_miniapps
register_system_miniapps()
# Install discovered builtin plugins and activate only those marked active in DB
from sqlalchemy import select as sa_select
from sqlalchemy.ext.asyncio import async_sessionmaker
+1
View File
@@ -116,6 +116,7 @@ class BasePlugin(ABC):
col_span=getattr(m, "col_span", 1),
row_span=getattr(m, "row_span", 1),
hosts=getattr(m, "hosts", None),
component=getattr(m, "component", ""),
order=getattr(m, "order", 100),
)
+21
View File
@@ -13,6 +13,7 @@ from app.plugins.manifest import (
FrontendDashboardWidget,
FrontendMenuItem,
FrontendPageRoute,
MiniAppContribution,
PluginManifest,
PluginRouteDef,
)
@@ -60,6 +61,26 @@ class ContactsPlugin(BasePlugin):
],
events=[],
migrations=[],
miniapps=[
MiniAppContribution(
app_id="contacts_stats",
name="Kontakt-Zähler",
icon="Building2",
description="Firmen- und Kontakt-Zähler (persönliche StatCards).",
permission="contacts:read",
settings_schema={
"fields": [
{"name": "show_companies", "label": "Firmen anzeigen", "type": "boolean", "default": True},
{"name": "show_persons", "label": "Personen anzeigen", "type": "boolean", "default": True},
]
},
col_span=2,
row_span=1,
hosts=["chat", "dashboard", "window"],
component="@/components/dashboard/ContactsStatsWidget",
order=5,
),
],
dashboard_widgets=[
FrontendDashboardWidget(
id="recent_contacts",
+3 -1
View File
@@ -110,7 +110,9 @@ async def _seed_default_dashboard(
apps = [
a
for a in registry.list_apps(host="dashboard")
if user_permits(current_user, a) and "dashboard" in (a.get("hosts") or [])
if user_permits(current_user, a)
and "dashboard" in (a.get("hosts") or [])
and a.get("component") # renderable only (M4: chat apps stay off layouts)
]
apps.sort(key=lambda a: a.get("order", 100))