feat: consolidate media service, add merchant users page, fix metrics overlap

- Merge ImageService into MediaService with WebP variant generation,
  DB-backed storage stats, and module-driven media usage discovery
  via new MediaUsageProviderProtocol
- Add merchant users admin page with scoped user listing, stats
  endpoint, template, JS, and i18n strings (de/en/fr/lb)
- Fix merchant user metrics so Owners and Team Members are mutually
  exclusive (filter team_members on user_type="member" and exclude
  owner IDs) ensuring stat cards add up correctly
- Update billing and monitoring services to use media_service
- Update subscription-billing and feature-gating docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-07 21:17:11 +01:00
parent 4cb2bda575
commit 2250054ba2
30 changed files with 1220 additions and 805 deletions

View File

@@ -45,6 +45,7 @@ if TYPE_CHECKING:
from pydantic import BaseModel
from app.modules.contracts.audit import AuditProviderProtocol
from app.modules.contracts.cms import MediaUsageProviderProtocol
from app.modules.contracts.features import FeatureProviderProtocol
from app.modules.contracts.metrics import MetricsProviderProtocol
from app.modules.contracts.widgets import DashboardWidgetProviderProtocol
@@ -477,6 +478,14 @@ class ModuleDefinition:
# The provider will be discovered by billing's FeatureAggregator service.
feature_provider: "Callable[[], FeatureProviderProtocol] | None" = None
# =========================================================================
# Media Usage Provider (Module-Driven Media Usage Tracking)
# =========================================================================
# Callable that returns a MediaUsageProviderProtocol implementation.
# Modules that use media files (catalog, etc.) can register a provider
# to report where media is being used.
media_usage_provider: "Callable[[], MediaUsageProviderProtocol] | None" = None
# =========================================================================
# Menu Item Methods (Legacy - uses menu_items dict of IDs)
# =========================================================================
@@ -929,6 +938,24 @@ class ModuleDefinition:
return None
return self.feature_provider()
# =========================================================================
# Media Usage Provider Methods
# =========================================================================
def has_media_usage_provider(self) -> bool:
"""Check if this module has a media usage provider."""
return self.media_usage_provider is not None
def get_media_usage_provider_instance(self) -> "MediaUsageProviderProtocol | None":
"""Get the media usage provider instance for this module.
Returns:
MediaUsageProviderProtocol instance, or None
"""
if self.media_usage_provider is None:
return None
return self.media_usage_provider()
# =========================================================================
# Magic Methods
# =========================================================================