feat: implement metrics provider pattern for modular dashboard statistics

This commit introduces a protocol-based metrics architecture that allows
each module to provide its own statistics for dashboards without creating
cross-module dependencies.

Key changes:
- Add MetricsProviderProtocol and MetricValue dataclass in contracts module
- Add StatsAggregatorService in core module that discovers and aggregates
  metrics from all enabled modules
- Implement metrics providers for all modules:
  - tenancy: vendor/user counts, team members, domains
  - customers: customer counts
  - cms: pages, media files
  - catalog: products
  - inventory: stock levels
  - orders: order counts, revenue
  - marketplace: import jobs, staging products
- Update dashboard routes to use StatsAggregator instead of direct imports
- Fix VendorPlatform junction table usage (Vendor.platform_id doesn't exist)
- Add comprehensive documentation for the pattern

This architecture ensures:
- Dashboards always work (aggregator in core)
- Each module owns its metrics (no cross-module coupling)
- Optional modules are truly optional (can be removed without breaking app)
- Multi-platform vendors are properly supported via VendorPlatform table

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-03 21:11:29 +01:00
parent a76128e016
commit a8fae0fbc7
28 changed files with 3745 additions and 269 deletions

View File

@@ -0,0 +1,253 @@
# app/modules/cms/services/cms_metrics.py
"""
Metrics provider for the CMS module.
Provides metrics for:
- Content pages
- Media files
- Themes
"""
import logging
from typing import TYPE_CHECKING
from sqlalchemy import func
from sqlalchemy.orm import Session
from app.modules.contracts.metrics import (
MetricValue,
MetricsContext,
MetricsProviderProtocol,
)
if TYPE_CHECKING:
pass
logger = logging.getLogger(__name__)
class CMSMetricsProvider:
"""
Metrics provider for CMS module.
Provides content management metrics for vendor and platform dashboards.
"""
@property
def metrics_category(self) -> str:
return "cms"
def get_vendor_metrics(
self,
db: Session,
vendor_id: int,
context: MetricsContext | None = None,
) -> list[MetricValue]:
"""
Get CMS metrics for a specific vendor.
Provides:
- Total content pages
- Published pages
- Media files count
- Theme status
"""
from app.modules.cms.models import ContentPage, MediaFile, VendorTheme
try:
# Content pages
total_pages = (
db.query(ContentPage).filter(ContentPage.vendor_id == vendor_id).count()
)
published_pages = (
db.query(ContentPage)
.filter(
ContentPage.vendor_id == vendor_id,
ContentPage.is_published == True,
)
.count()
)
# Media files
media_count = (
db.query(MediaFile).filter(MediaFile.vendor_id == vendor_id).count()
)
# Total media size (in MB)
total_media_size = (
db.query(func.sum(MediaFile.file_size))
.filter(MediaFile.vendor_id == vendor_id)
.scalar()
or 0
)
total_media_size_mb = round(total_media_size / (1024 * 1024), 2)
# Theme configured
has_theme = (
db.query(VendorTheme).filter(VendorTheme.vendor_id == vendor_id).first()
is not None
)
return [
MetricValue(
key="cms.total_pages",
value=total_pages,
label="Total Pages",
category="cms",
icon="file-text",
description="Total content pages created",
),
MetricValue(
key="cms.published_pages",
value=published_pages,
label="Published Pages",
category="cms",
icon="globe",
description="Content pages that are published",
),
MetricValue(
key="cms.media_count",
value=media_count,
label="Media Files",
category="cms",
icon="image",
description="Files in media library",
),
MetricValue(
key="cms.media_size",
value=total_media_size_mb,
label="Media Size",
category="cms",
icon="hard-drive",
unit="MB",
description="Total storage used by media",
),
MetricValue(
key="cms.has_theme",
value=1 if has_theme else 0,
label="Theme Configured",
category="cms",
icon="palette",
description="Whether a custom theme is configured",
),
]
except Exception as e:
logger.warning(f"Failed to get CMS vendor metrics: {e}")
return []
def get_platform_metrics(
self,
db: Session,
platform_id: int,
context: MetricsContext | None = None,
) -> list[MetricValue]:
"""
Get CMS metrics aggregated for a platform.
Aggregates content management data across all vendors.
"""
from app.modules.cms.models import ContentPage, MediaFile, VendorTheme
from app.modules.tenancy.models import VendorPlatform
try:
# Get all vendor IDs for this platform using VendorPlatform junction table
vendor_ids = (
db.query(VendorPlatform.vendor_id)
.filter(
VendorPlatform.platform_id == platform_id,
VendorPlatform.is_active == True,
)
.subquery()
)
# Content pages
total_pages = (
db.query(ContentPage)
.filter(ContentPage.vendor_id.in_(vendor_ids))
.count()
)
published_pages = (
db.query(ContentPage)
.filter(
ContentPage.vendor_id.in_(vendor_ids),
ContentPage.is_published == True,
)
.count()
)
# Media files
media_count = (
db.query(MediaFile).filter(MediaFile.vendor_id.in_(vendor_ids)).count()
)
# Total media size (in GB for platform-level)
total_media_size = (
db.query(func.sum(MediaFile.file_size))
.filter(MediaFile.vendor_id.in_(vendor_ids))
.scalar()
or 0
)
total_media_size_gb = round(total_media_size / (1024 * 1024 * 1024), 2)
# Vendors with themes
vendors_with_themes = (
db.query(func.count(func.distinct(VendorTheme.vendor_id)))
.filter(VendorTheme.vendor_id.in_(vendor_ids))
.scalar()
or 0
)
return [
MetricValue(
key="cms.total_pages",
value=total_pages,
label="Total Pages",
category="cms",
icon="file-text",
description="Total content pages across all vendors",
),
MetricValue(
key="cms.published_pages",
value=published_pages,
label="Published Pages",
category="cms",
icon="globe",
description="Published content pages across all vendors",
),
MetricValue(
key="cms.media_count",
value=media_count,
label="Media Files",
category="cms",
icon="image",
description="Total media files across all vendors",
),
MetricValue(
key="cms.media_size",
value=total_media_size_gb,
label="Total Media Size",
category="cms",
icon="hard-drive",
unit="GB",
description="Total storage used by media",
),
MetricValue(
key="cms.vendors_with_themes",
value=vendors_with_themes,
label="Themed Vendors",
category="cms",
icon="palette",
description="Vendors with custom themes",
),
]
except Exception as e:
logger.warning(f"Failed to get CMS platform metrics: {e}")
return []
# Singleton instance
cms_metrics_provider = CMSMetricsProvider()
__all__ = ["CMSMetricsProvider", "cms_metrics_provider"]