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

@@ -0,0 +1,310 @@
# Cross-Module Import Rules
This document defines the strict import rules that ensure the module system remains decoupled, testable, and resilient. These rules are critical for maintaining a truly modular architecture.
## Core Principle
**Core modules NEVER import from optional modules.**
This is the fundamental rule that enables optional modules to be truly optional. When a core module imports from an optional module:
- The app crashes if that module is disabled
- You can't test core functionality in isolation
- You create a hidden dependency that violates the architecture
## Module Classification
### Core Modules (Always Enabled)
- `contracts` - Protocols and interfaces
- `core` - Dashboard, settings, profile
- `tenancy` - Platform, company, vendor, admin user management
- `cms` - Content pages, media library
- `customers` - Customer database
- `billing` - Subscriptions, tier limits
- `payments` - Payment gateway integrations
- `messaging` - Email, notifications
### Optional Modules (Per-Platform)
- `analytics` - Reports, dashboards
- `cart` - Shopping cart
- `catalog` - Product browsing
- `checkout` - Cart-to-order conversion
- `inventory` - Stock management
- `loyalty` - Loyalty programs
- `marketplace` - Letzshop integration
- `orders` - Order management
## Import Rules Matrix
| From \ To | Core | Optional | Contracts |
|-----------|------|----------|-----------|
| **Core** | :white_check_mark: | :x: FORBIDDEN | :white_check_mark: |
| **Optional** | :white_check_mark: | :warning: With care | :white_check_mark: |
| **Contracts** | :x: | :x: | :white_check_mark: |
### Explanation
1. **Core → Core**: Allowed. Core modules can import from each other (e.g., billing imports from tenancy)
2. **Core → Optional**: **FORBIDDEN**. This is the most important rule. Core modules must never have direct imports from optional modules.
3. **Core → Contracts**: Allowed. Contracts define shared protocols and data structures.
4. **Optional → Core**: Allowed. Optional modules can use core functionality.
5. **Optional → Optional**: Allowed with care. Check dependencies in `definition.py` to ensure proper ordering.
6. **Optional → Contracts**: Allowed. This is how optional modules implement protocols.
7. **Contracts → Anything**: Contracts should only depend on stdlib/typing/Protocol. No module imports.
## Anti-Patterns (DO NOT DO)
### Direct Import from Optional Module
```python
# app/modules/core/routes/api/admin_dashboard.py
# BAD: Core importing from optional module
from app.modules.marketplace.services import marketplace_service
from app.modules.analytics.services import stats_service
@router.get("/dashboard")
def get_dashboard():
# This crashes if marketplace is disabled!
imports = marketplace_service.get_recent_imports()
```
### Conditional Import with ismodule_enabled Check
```python
# BAD: Still creates import-time dependency
from app.modules.service import module_service
if module_service.is_module_enabled(db, platform_id, "marketplace"):
from app.modules.marketplace.services import marketplace_service # Still loaded!
imports = marketplace_service.get_recent_imports()
```
### Type Hints from Optional Modules
```python
# BAD: Type hint forces import
from app.modules.marketplace.models import MarketplaceImportJob
def process_import(job: MarketplaceImportJob) -> None: # Crashes if disabled
...
```
## Approved Patterns
### 1. Provider Protocol Pattern (Metrics & Widgets)
Use the provider protocol pattern for cross-module data:
```python
# app/modules/core/services/stats_aggregator.py
# GOOD: Import only protocols and contracts
from app.modules.contracts.metrics import MetricsProviderProtocol, MetricValue
class StatsAggregatorService:
def _get_enabled_providers(self, db, platform_id):
# Discover providers from MODULES registry
from app.modules.registry import MODULES
for module in MODULES.values():
if module.has_metrics_provider():
# Provider is called through protocol, not direct import
provider = module.get_metrics_provider_instance()
yield provider
```
### 2. Context Provider Pattern
Modules contribute context without direct imports:
```python
# app/modules/core/utils.py
def get_context_for_frontend(frontend_type, request, db, platform):
# GOOD: Discover and call providers through registry
for module in get_enabled_modules(db, platform.id):
if module.has_context_provider(frontend_type):
context.update(module.get_context_contribution(...))
```
### 3. Lazy Factory Functions
Use factory functions in module definitions:
```python
# app/modules/marketplace/definition.py
def _get_widget_provider():
"""Lazy import to avoid circular imports."""
from app.modules.marketplace.services.marketplace_widgets import marketplace_widget_provider
return marketplace_widget_provider
marketplace_module = ModuleDefinition(
code="marketplace",
widget_provider=_get_widget_provider, # Called lazily when needed
)
```
### 4. Type Checking Only Imports
Use `TYPE_CHECKING` for type hints without runtime dependency:
```python
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from app.modules.marketplace.models import MarketplaceImportJob
def process_import(job: "MarketplaceImportJob") -> None:
# Works at runtime even if marketplace is disabled
...
```
### 5. Registry-Based Discovery
Discover modules through the registry, not imports:
```python
# GOOD: Discovery through registry
from app.modules.registry import MODULES
def get_module_if_enabled(db, platform_id, module_code):
module = MODULES.get(module_code)
if module and module_service.is_module_enabled(db, platform_id, module_code):
return module
return None
```
## Architecture Validation
The architecture validator (`scripts/validate_architecture.py`) includes rules to detect violations:
| Rule | Severity | Description |
|------|----------|-------------|
| IMPORT-001 | ERROR | Core module imports from optional module |
| IMPORT-002 | WARNING | Optional module imports from unrelated optional module |
| IMPORT-003 | INFO | Consider using protocol pattern instead of direct import |
Run validation:
```bash
python scripts/validate_architecture.py
```
## Provider Pattern Summary
### When to Use Each Pattern
| Need | Pattern | Location |
|------|---------|----------|
| Numeric statistics | MetricsProvider | `contracts/metrics.py` |
| Dashboard widgets | WidgetProvider | `contracts/widgets.py` |
| Page template context | Context Provider | `definition.py` |
| Service access | Protocol + DI | `contracts/{module}.py` |
### Provider Flow
```
┌─────────────────────────────────────────────────────────────────────┐
│ CONTRACTS LAYER │
│ (Shared protocol definitions) │
│ │
│ MetricsProviderProtocol DashboardWidgetProviderProtocol │
│ ContentServiceProtocol ServiceProtocol │
└─────────────────────────────────────────────────────────────────────┘
┌───────────────┼───────────────┐
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ CORE MODULE │ │ OPTIONAL MODULE 1│ │ OPTIONAL MODULE 2│
│ │ │ │ │ │
│ StatsAggregator │ │ OrderMetrics │ │ CatalogMetrics │
│ WidgetAggregator │ │ (implements) │ │ (implements) │
│ (discovers) │ │ │ │ │
└──────────────────┘ └──────────────────┘ └──────────────────┘
│ │ │
│ │ │
▼ ▼ ▼
┌──────────────────────────────────────────────────────────────┐
│ MODULE REGISTRY │
│ │
│ MODULES = { │
│ "orders": ModuleDefinition(metrics_provider=...), │
│ "catalog": ModuleDefinition(metrics_provider=...), │
│ } │
└──────────────────────────────────────────────────────────────┘
```
## Testing Without Dependencies
Because core doesn't depend on optional modules, you can test core in isolation:
```python
# tests/unit/core/test_stats_aggregator.py
from unittest.mock import MagicMock
from app.modules.contracts.metrics import MetricValue, MetricsProviderProtocol
class MockMetricsProvider:
"""Mock that implements the protocol without needing real modules."""
@property
def metrics_category(self) -> str:
return "test"
def get_vendor_metrics(self, db, vendor_id, context=None):
return [MetricValue(key="test.value", value=42, label="Test", category="test")]
def test_stats_aggregator_with_mock_provider():
# Test core without any optional modules
mock_provider = MockMetricsProvider()
...
```
## Consequences of Violations
### If Core Imports Optional Module
1. **Import Error**: App fails to start if module code has syntax errors
2. **Runtime Crash**: App crashes when disabled module is accessed
3. **Hidden Dependency**: Module appears optional but isn't
4. **Testing Difficulty**: Can't test core without all modules
5. **Deployment Issues**: Can't deploy minimal configurations
### Detection
Add these checks to CI:
```bash
# Check for forbidden imports
grep -r "from app.modules.marketplace" app/modules/core/ && exit 1
grep -r "from app.modules.analytics" app/modules/core/ && exit 1
grep -r "from app.modules.orders" app/modules/core/ && exit 1
```
## Summary
| Rule | Enforcement |
|------|-------------|
| Core → Optional = FORBIDDEN | Architecture validation, CI checks |
| Use Protocol pattern | Code review, documentation |
| Lazy factory functions | Required for definition.py |
| TYPE_CHECKING imports | Required for type hints across modules |
| Registry-based discovery | Required for all cross-module access |
Following these rules ensures:
- Modules can be truly enabled/disabled per platform
- Testing can be done in isolation
- New modules can be added without modifying core
- The app remains stable when modules fail
## Related Documentation
- [Module System Architecture](module-system.md) - Module structure and auto-discovery
- [Metrics Provider Pattern](metrics-provider-pattern.md) - Numeric statistics architecture
- [Widget Provider Pattern](widget-provider-pattern.md) - Dashboard widgets architecture
- [Architecture Violations Status](architecture-violations-status.md) - Current violation tracking

View File

@@ -25,12 +25,13 @@ The Wizamart platform uses a **plug-and-play modular architecture** where module
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ CORE MODULES (Always Enabled) │ │
│ │ core tenancy cms customers │ │
│ │ contracts │ core tenancy cms customers │ billing │ │ │
│ │ payments │ messaging │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ OPTIONAL MODULES (Per-Platform) │ │
│ │ payments │ billing │ inventory │ ordersmarketplace │ ...│ │
│ │ analytics │ inventory │ catalogcart │ checkout │ ... │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
@@ -72,7 +73,7 @@ touch app/modules/mymodule/exceptions.py
## Three-Tier Classification
### Core Modules (5)
### Core Modules (8)
Core modules are **always enabled** and cannot be disabled. They provide fundamental platform functionality.
@@ -83,24 +84,29 @@ Core modules are **always enabled** and cannot be disabled. They provide fundame
| `cms` | Content pages, media library, themes | Content management | 5 |
| `customers` | Customer database, profiles, segmentation | Customer data management | 4 |
| `tenancy` | Platform, company, vendor, admin user management | Multi-tenant infrastructure | 4 |
| `billing` | Platform subscriptions, tier limits, vendor invoices | Subscription management, tier-based feature gating | 5 |
| `payments` | Payment gateway integrations (Stripe, PayPal, etc.) | Payment processing, required for billing | 3 |
| `messaging` | Messages, notifications, email templates | Email for registration, password reset, notifications | 3 |
### Optional Modules (11)
**Why these are core:**
- **billing**: Tier limits affect many features (team size, product limits, email providers). Subscription management is fundamental.
- **payments**: Required by billing for subscription payment processing.
- **messaging**: Email is required for user registration, password reset, and team invitations.
### Optional Modules (8)
Optional modules can be **enabled or disabled per platform**. They provide additional functionality that may not be needed by all platforms.
| Module | Dependencies | Description | Permissions |
|--------|--------------|-------------|-------------|
| `analytics` | - | Reports, dashboards | 3 |
| `billing` | `payments` | Platform subscriptions, vendor invoices | 5 |
| `analytics` | - | Reports, dashboards, advanced statistics | 3 |
| `cart` | `inventory` | Shopping cart management, session-based carts | 2 |
| `catalog` | `inventory` | Customer-facing product browsing | 6 |
| `checkout` | `cart`, `orders`, `payments`, `customers` | Cart-to-order conversion, checkout flow | 2 |
| `checkout` | `cart`, `orders`, `customers` | Cart-to-order conversion, checkout flow | 2 |
| `inventory` | - | Stock management, locations | 3 |
| `loyalty` | `customers` | Stamp/points loyalty programs, wallet integration | 4 |
| `marketplace` | `inventory` | Letzshop integration | 3 |
| `messaging` | - | Messages, notifications | 3 |
| `orders` | `payments` | Order management, customer checkout | 4 |
| `payments` | - | Payment gateway integrations (Stripe, PayPal, etc.) | 3 |
| `marketplace` | `inventory` | Letzshop integration, product import/export | 3 |
| `orders` | - | Order management, customer checkout | 4 |
### Internal Modules (2)
@@ -243,6 +249,7 @@ analytics_module = ModuleDefinition(
| `is_internal` | `bool` | Admin-only if True |
| `is_self_contained` | `bool` | Uses self-contained structure |
| `metrics_provider` | `Callable` | Factory function returning MetricsProviderProtocol (see [Metrics Provider Pattern](metrics-provider-pattern.md)) |
| `widget_provider` | `Callable` | Factory function returning DashboardWidgetProviderProtocol (see [Widget Provider Pattern](widget-provider-pattern.md)) |
## Route Auto-Discovery
@@ -308,22 +315,27 @@ The Framework Layer provides infrastructure that modules depend on. These are **
Modules can depend on other modules. When enabling a module, its dependencies are automatically enabled.
```
payments
↙ ↘
billing orders ←──┐
inventory │ │
↓ cart │
marketplace ↘ │
checkout
CORE MODULES (always enabled):
┌─────────────────────────────────────────────────────────┐
contracts core tenancy cms customers │
billing ← payments messaging
└─────────────────────────────────────────────────────────┘
OPTIONAL MODULES (dependencies shown):
inventory
↙ ↓ ↘
catalog cart marketplace
checkout ← orders
```
**Dependency Rules:**
1. Core modules cannot depend on optional modules
1. **Core modules NEVER import from optional modules** (see [Cross-Module Import Rules](cross-module-import-rules.md))
2. Enabling a module auto-enables its dependencies
3. Disabling a module auto-disables modules that depend on it
4. Circular dependencies are not allowed
5. Use protocol patterns (Metrics/Widget Provider) for cross-module data
## Module Registry
@@ -347,7 +359,7 @@ billing = get_module("billing")
tier = get_module_tier("billing") # Returns "optional"
# Get all core module codes
core_codes = get_core_module_codes() # {"core", "tenancy", "cms", "customers"}
core_codes = get_core_module_codes() # {"contracts", "core", "tenancy", "cms", "customers", "billing", "payments", "messaging"}
```
## Module Service
@@ -1252,16 +1264,20 @@ python scripts/validate_architecture.py
### Don't
- Create circular dependencies
- Make core modules depend on optional modules
- **Make core modules import from optional modules** (use provider patterns instead)
- Put framework-level code in modules
- Skip migration naming conventions
- Forget `__init__.py` in tasks directory
- Manually register modules in registry.py (use auto-discovery)
- Import optional modules at the top of core module files
- Use direct imports when a protocol pattern exists
## Related Documentation
- [Creating Modules](../development/creating-modules.md) - Step-by-step guide
- [Cross-Module Import Rules](cross-module-import-rules.md) - Import constraints and patterns
- [Metrics Provider Pattern](metrics-provider-pattern.md) - Dashboard statistics architecture
- [Widget Provider Pattern](widget-provider-pattern.md) - Dashboard widgets architecture
- [Menu Management](menu-management.md) - Sidebar configuration
- [Observability](observability.md) - Health checks integration
- [Feature Gating](../implementation/feature-gating-system.md) - Tier-based access

View File

@@ -0,0 +1,449 @@
# Widget Provider Pattern
The widget provider pattern enables modules to provide dashboard widgets (lists of recent items, breakdowns) without creating cross-module dependencies. This pattern extends the [Metrics Provider Pattern](metrics-provider-pattern.md) to support richer widget data.
## Overview
```
┌─────────────────────────────────────────────────────────────────────┐
│ Dashboard Request │
│ (Admin Dashboard or Vendor Dashboard) │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ WidgetAggregatorService │
│ (app/modules/core/services/widget_aggregator.py) │
│ │
│ • Discovers WidgetProviders from all enabled modules │
│ • Calls get_vendor_widgets() or get_platform_widgets() │
│ • Returns categorized widgets dict │
└─────────────────────────────────────────────────────────────────────┘
┌───────────────────────┼───────────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│tenancy_widgets│ │market_widgets │ │catalog_widgets│
│ (enabled) │ │ (enabled) │ │ (disabled) │
└───────┬───────┘ └───────┬───────┘ └───────────────┘
│ │ │
▼ ▼ × (skipped)
┌───────────────┐ ┌───────────────┐
│recent_vendors │ │recent_imports │
│ (ListWidget) │ │ (ListWidget) │
└───────────────┘ └───────────────┘
│ │
└───────────┬───────────┘
┌─────────────────────────────────┐
│ Categorized Widgets │
│{"tenancy": [...], "marketplace": [...]}│
└─────────────────────────────────┘
```
## Architectural Principle
**Core defines contracts, modules implement them, core discovers and aggregates.**
This ensures:
- Core modules **never import** from optional modules
- Optional modules remain truly optional (can be removed without breaking app)
- Dashboard always works with partial data when modules are disabled
## Key Components
### 1. WidgetContext Dataclass
Context for widget queries (date ranges, limits):
```python
# app/modules/contracts/widgets.py
from dataclasses import dataclass
from datetime import datetime
@dataclass
class WidgetContext:
date_from: datetime | None = None
date_to: datetime | None = None
limit: int = 5 # Max items for list widgets
include_details: bool = False
```
### 2. Widget Item Types
Items that populate widgets:
```python
@dataclass
class WidgetListItem:
"""Single item in a list widget (recent vendors, orders, imports)."""
id: int | str
title: str
subtitle: str | None = None
status: str | None = None # "success", "warning", "error", "neutral"
timestamp: datetime | None = None
url: str | None = None
metadata: dict[str, Any] = field(default_factory=dict)
@dataclass
class WidgetBreakdownItem:
"""Single row in a breakdown widget (stats by marketplace, category)."""
label: str
value: int | float
secondary_value: int | float | None = None
percentage: float | None = None
icon: str | None = None
```
### 3. Widget Containers
Containers that hold widget items:
```python
@dataclass
class ListWidget:
"""Widget containing a list of items."""
items: list[WidgetListItem]
total_count: int | None = None
view_all_url: str | None = None
@dataclass
class BreakdownWidget:
"""Widget containing grouped statistics."""
items: list[WidgetBreakdownItem]
total: int | float | None = None
WidgetData = ListWidget | BreakdownWidget
```
### 4. DashboardWidget (Main Envelope)
The standard widget envelope with metadata:
```python
@dataclass
class DashboardWidget:
key: str # "marketplace.recent_imports"
widget_type: str # "list" or "breakdown"
title: str # "Recent Imports"
category: str # "marketplace"
data: WidgetData
icon: str | None = None # Lucide icon name
description: str | None = None
order: int = 100 # Display order (lower = higher priority)
```
### 5. DashboardWidgetProviderProtocol
Protocol that modules implement:
```python
@runtime_checkable
class DashboardWidgetProviderProtocol(Protocol):
@property
def widgets_category(self) -> str:
"""Category name (e.g., "marketplace", "orders")."""
...
def get_vendor_widgets(
self, db: Session, vendor_id: int, context: WidgetContext | None = None
) -> list[DashboardWidget]:
"""Get widgets for vendor dashboard."""
...
def get_platform_widgets(
self, db: Session, platform_id: int, context: WidgetContext | None = None
) -> list[DashboardWidget]:
"""Get widgets for admin/platform dashboard."""
...
```
### 6. WidgetAggregatorService
Central service in core that discovers and aggregates widgets:
```python
# app/modules/core/services/widget_aggregator.py
class WidgetAggregatorService:
def _get_enabled_providers(self, db, platform_id):
# Iterate MODULES registry
# Skip modules without widget_provider
# Check module enablement (except core)
# Return (module, provider) tuples
def get_vendor_dashboard_widgets(
self, db, vendor_id, platform_id, context=None
) -> dict[str, list[DashboardWidget]]:
"""Returns widgets grouped by category for vendor dashboard."""
def get_admin_dashboard_widgets(
self, db, platform_id, context=None
) -> dict[str, list[DashboardWidget]]:
"""Returns widgets grouped by category for admin dashboard."""
def get_widgets_flat(
self, db, platform_id, vendor_id=None, context=None
) -> list[DashboardWidget]:
"""Returns flat list sorted by order."""
widget_aggregator = WidgetAggregatorService()
```
## Implementing a Widget Provider
### Step 1: Create the Widget Provider Class
```python
# app/modules/marketplace/services/marketplace_widgets.py
import logging
from sqlalchemy.orm import Session
from app.modules.contracts.widgets import (
DashboardWidget,
ListWidget,
WidgetContext,
WidgetListItem,
)
logger = logging.getLogger(__name__)
class MarketplaceWidgetProvider:
"""Widget provider for marketplace module."""
@property
def widgets_category(self) -> str:
return "marketplace"
def _map_status_to_display(self, status: str) -> str:
"""Map job status to widget status indicator."""
status_map = {
"pending": "neutral",
"processing": "warning",
"completed": "success",
"completed_with_errors": "warning",
"failed": "error",
}
return status_map.get(status, "neutral")
def get_vendor_widgets(
self,
db: Session,
vendor_id: int,
context: WidgetContext | None = None,
) -> list[DashboardWidget]:
"""Get marketplace widgets for a vendor dashboard."""
from app.modules.marketplace.models import MarketplaceImportJob
limit = context.limit if context else 5
try:
jobs = (
db.query(MarketplaceImportJob)
.filter(MarketplaceImportJob.vendor_id == vendor_id)
.order_by(MarketplaceImportJob.created_at.desc())
.limit(limit)
.all()
)
items = [
WidgetListItem(
id=job.id,
title=f"Import #{job.id}",
subtitle=f"{job.marketplace} - {job.language.upper()}",
status=self._map_status_to_display(job.status),
timestamp=job.created_at,
url=f"/vendor/marketplace/imports/{job.id}",
metadata={
"total_processed": job.total_processed or 0,
"imported_count": job.imported_count or 0,
"error_count": job.error_count or 0,
},
)
for job in jobs
]
return [
DashboardWidget(
key="marketplace.recent_imports",
widget_type="list",
title="Recent Imports",
category="marketplace",
data=ListWidget(items=items),
icon="download",
order=20,
)
]
except Exception as e:
logger.warning(f"Failed to get marketplace vendor widgets: {e}")
return []
def get_platform_widgets(
self,
db: Session,
platform_id: int,
context: WidgetContext | None = None,
) -> list[DashboardWidget]:
"""Get marketplace widgets for the admin dashboard."""
# Similar implementation for platform-wide metrics
# Uses VendorPlatform junction table to filter by platform
...
# Singleton instance
marketplace_widget_provider = MarketplaceWidgetProvider()
```
### Step 2: Register in Module Definition
```python
# app/modules/marketplace/definition.py
from app.modules.base import ModuleDefinition
def _get_widget_provider():
"""Lazy import to avoid circular imports."""
from app.modules.marketplace.services.marketplace_widgets import marketplace_widget_provider
return marketplace_widget_provider
marketplace_module = ModuleDefinition(
code="marketplace",
name="Marketplace (Letzshop)",
# ... other config ...
# Register the widget provider
widget_provider=_get_widget_provider,
)
```
### Step 3: Widgets Appear Automatically
When the module is enabled, its widgets automatically appear in dashboards.
## Translation Handling
Translations are handled **by the optional module itself**, not by core:
```python
from app.core.i18n import gettext as _
def get_platform_widgets(self, db, platform_id, context=None):
return [
DashboardWidget(
key="marketplace.recent_imports",
title=_("Recent Imports"), # Module translates
description=_("Latest import jobs"), # Module translates
# ...
)
]
```
Core receives already-translated strings and doesn't need translation logic.
## Available Widget Providers
| Module | Category | Widgets Provided |
|--------|----------|------------------|
| **tenancy** | `tenancy` | recent_vendors (ListWidget) |
| **marketplace** | `marketplace` | recent_imports (ListWidget) |
| **orders** | `orders` | recent_orders (ListWidget) - future |
| **customers** | `customers` | recent_customers (ListWidget) - future |
## Comparison with Metrics Provider
| Aspect | MetricsProvider | DashboardWidgetProvider |
|--------|-----------------|------------------------|
| Protocol location | `contracts/metrics.py` | `contracts/widgets.py` |
| Data unit | `MetricValue` | `DashboardWidget` |
| Context | `MetricsContext` | `WidgetContext` |
| Aggregator | `StatsAggregatorService` | `WidgetAggregatorService` |
| Registration field | `metrics_provider` | `widget_provider` |
| Scope methods | `get_vendor_metrics`, `get_platform_metrics` | `get_vendor_widgets`, `get_platform_widgets` |
| Use case | Numeric statistics | Lists, breakdowns, rich data |
## Dashboard Usage Example
```python
# app/modules/core/routes/api/admin_dashboard.py
from app.modules.core.services.widget_aggregator import widget_aggregator
@admin_dashboard_router.get("")
def get_admin_dashboard(...):
# Get widgets from all enabled modules
widgets = widget_aggregator.get_admin_dashboard_widgets(db, platform_id)
# Extract for backward compatibility
recent_imports = []
if "marketplace" in widgets:
for widget in widgets["marketplace"]:
if widget.key == "marketplace.recent_imports":
recent_imports = [item_to_dict(i) for i in widget.data.items]
return AdminDashboardResponse(
# ... existing fields ...
recent_imports=recent_imports,
)
```
## Multi-Platform Architecture
Always use `VendorPlatform` junction table for platform-level queries:
```python
from app.modules.tenancy.models import VendorPlatform
vendor_ids = (
db.query(VendorPlatform.vendor_id)
.filter(VendorPlatform.platform_id == platform_id)
.subquery()
)
jobs = (
db.query(MarketplaceImportJob)
.filter(MarketplaceImportJob.vendor_id.in_(vendor_ids))
.order_by(MarketplaceImportJob.created_at.desc())
.limit(limit)
.all()
)
```
## Error Handling
Widget providers are wrapped in try/except to prevent one failing module from breaking the entire dashboard:
```python
try:
widgets = provider.get_platform_widgets(db, platform_id, context)
except Exception as e:
logger.warning(f"Failed to get {provider.widgets_category} widgets: {e}")
widgets = [] # Continue with empty widgets for this module
```
## Best Practices
### Do
- Use lazy imports inside widget methods to avoid circular imports
- Always use `VendorPlatform` junction table for platform-level queries
- Return empty list on error, don't raise exceptions
- Log warnings for debugging but don't crash
- Include helpful descriptions and icons for UI
- Handle translations in the module, not in core
### Don't
- Import from optional modules at the top of core module files
- Assume `Vendor.platform_id` exists (it doesn't!)
- Let exceptions propagate from widget providers
- Create hard dependencies between core and optional modules
- Rely on core to translate widget strings
## Related Documentation
- [Metrics Provider Pattern](metrics-provider-pattern.md) - Numeric statistics architecture
- [Module System Architecture](module-system.md) - Module structure and auto-discovery
- [Multi-Tenant Architecture](multi-tenant.md) - Platform/vendor/company hierarchy
- [Cross-Module Import Rules](cross-module-import-rules.md) - Import constraints