feat: implement DashboardWidgetProvider pattern for modular dashboard widgets

Add protocol-based widget system following the MetricsProvider pattern:

- Create DashboardWidgetProviderProtocol in contracts/widgets.py
- Add WidgetAggregatorService in core to discover and aggregate widgets
- Implement MarketplaceWidgetProvider for recent_imports widget
- Implement TenancyWidgetProvider for recent_vendors widget
- Update admin dashboard to use widget_aggregator
- Add widget_provider field to ModuleDefinition

Architecture documentation:
- Add widget-provider-pattern.md with implementation guide
- Add cross-module-import-rules.md enforcing core/optional separation
- Update module-system.md with widget_provider and import rules

This enables modules to provide rich dashboard widgets without core modules
importing from optional modules, maintaining true module independence.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-04 19:01:23 +01:00
parent a8fae0fbc7
commit 3e38db79aa
13 changed files with 1906 additions and 25 deletions

View File

@@ -45,6 +45,7 @@ if TYPE_CHECKING:
from pydantic import BaseModel
from app.modules.contracts.metrics import MetricsProviderProtocol
from app.modules.contracts.widgets import DashboardWidgetProviderProtocol
from app.modules.enums import FrontendType
@@ -413,6 +414,26 @@ class ModuleDefinition:
# The provider will be discovered by core's StatsAggregator service.
metrics_provider: "Callable[[], MetricsProviderProtocol] | None" = None
# =========================================================================
# Widget Provider (Module-Driven Dashboard Widgets)
# =========================================================================
# Callable that returns a DashboardWidgetProviderProtocol implementation.
# Use a callable (factory function) to enable lazy loading and avoid
# circular imports. Each module can provide its own widgets for dashboards.
#
# Example:
# def _get_widget_provider():
# from app.modules.orders.services.order_widgets import order_widget_provider
# return order_widget_provider
#
# orders_module = ModuleDefinition(
# code="orders",
# widget_provider=_get_widget_provider,
# )
#
# The provider will be discovered by core's WidgetAggregator service.
widget_provider: "Callable[[], DashboardWidgetProviderProtocol] | None" = None
# =========================================================================
# Menu Item Methods (Legacy - uses menu_items dict of IDs)
# =========================================================================
@@ -799,6 +820,28 @@ class ModuleDefinition:
return None
return self.metrics_provider()
# =========================================================================
# Widget Provider Methods
# =========================================================================
def has_widget_provider(self) -> bool:
"""Check if this module has a widget provider."""
return self.widget_provider is not None
def get_widget_provider_instance(self) -> "DashboardWidgetProviderProtocol | None":
"""
Get the widget provider instance for this module.
Calls the widget_provider factory function to get the provider.
Returns None if no provider is configured.
Returns:
DashboardWidgetProviderProtocol instance, or None
"""
if self.widget_provider is None:
return None
return self.widget_provider()
# =========================================================================
# Magic Methods
# =========================================================================