feat: revamp analytics dashboards and harden database migrations

Add dashboard and overview analytics, health monitoring, provider expense tracking, and announcement updates across the gateway and frontend.

Keep schema migrations free of historical backfills while preserving automatic backfill execution. Bound migration deadlines, run schema preparation before Compose replacement, and anonymize deleted dashboard users.

Include the current documentation cleanup and regression coverage.
This commit is contained in:
elky
2026-10-01 11:48:17 +08:00
parent 60b89cc840
commit 066ea87d72
327 changed files with 31728 additions and 20645 deletions
@@ -41,6 +41,9 @@ use super::{
VideoTaskStatusCount, WalletDailyUsageAggregationInput, WalletDailyUsageAggregationResult,
WalletLookupKey, WalletMutationOutcome,
};
use aether_data_contracts::repository::billing::{
ProviderExpenseInput, ProviderExpensePage, ProviderExpenseQuery, ProviderExpenseRecord,
};
use aether_data_contracts::repository::usage::{
PendingUsageCleanupSummary, ProviderApiKeyWindowUsageRequest,
StoredProviderApiKeyWindowUsageSummary, StoredUsageDailySummary, UsageAuditListQuery,
@@ -364,6 +367,26 @@ impl GatewayDataState {
}
}
pub(crate) async fn rebuild_overview_buckets(
&self,
input: &aether_data::StatsHourlyAggregationInput,
) -> Result<usize, DataLayerError> {
match &self.backends {
Some(backends) => backends.rebuild_overview_buckets(input).await,
None => Ok(0),
}
}
pub(crate) async fn drain_overview_dirty_events(
&self,
now: chrono::DateTime<chrono::Utc>,
) -> Result<u64, DataLayerError> {
match &self.backends {
Some(backends) => backends.drain_overview_dirty_events(now).await,
None => Ok(0),
}
}
pub(crate) async fn aggregate_stats_daily(
&self,
input: &aether_data::StatsDailyAggregationInput,
@@ -384,6 +407,18 @@ impl GatewayDataState {
}
}
pub(crate) async fn list_user_announcements(
&self,
user_id: &str,
query: &aether_data::repository::announcements::UserAnnouncementListQuery,
) -> Result<aether_data::repository::announcements::StoredUserAnnouncementPage, DataLayerError>
{
match &self.announcement_reader {
Some(repository) => repository.list_user_announcements(user_id, query).await,
None => Ok(Default::default()),
}
}
pub(crate) async fn find_announcement_by_id(
&self,
announcement_id: &str,
@@ -1659,6 +1694,60 @@ impl GatewayDataState {
}
}
pub(crate) async fn query_dashboard_summary(
&self,
query: &aether_data_contracts::repository::usage::UsageDashboardAnalyticsQuery,
) -> Result<aether_data_contracts::repository::usage::StoredDashboardSummary, DataLayerError>
{
match &self.usage_reader {
Some(repository) => repository.query_dashboard_summary(query).await,
None => Err(DataLayerError::InvalidInput(
"dashboard summary repository is unavailable".into(),
)),
}
}
pub(crate) async fn query_dashboard_analytics(
&self,
query: &aether_data_contracts::repository::usage::UsageDashboardAnalyticsQuery,
) -> Result<
aether_data_contracts::repository::usage::StoredUsageDashboardAnalytics,
DataLayerError,
> {
match &self.usage_reader {
Some(repository) => repository.query_dashboard_analytics(query).await,
None => Err(DataLayerError::InvalidInput(
"usage analytics repository is unavailable".into(),
)),
}
}
pub(crate) async fn query_usage_analytics(
&self,
query: &aether_data_contracts::repository::usage::UsageAnalyticsQuery,
) -> Result<aether_data_contracts::repository::usage::StoredUsageAnalytics, DataLayerError>
{
match &self.usage_reader {
Some(repository) => repository.query_usage_analytics(query).await,
None => Err(DataLayerError::InvalidInput(
"usage analytics repository is unavailable".into(),
)),
}
}
pub(crate) async fn summarize_health_observations(
&self,
query: &aether_data_contracts::repository::usage::HealthObservationQuery,
) -> Result<aether_data_contracts::repository::usage::HealthObservationSummary, DataLayerError>
{
match &self.usage_reader {
Some(repository) => repository.summarize_health_observations(query).await,
None => Err(DataLayerError::InvalidInput(
"health observations repository is unavailable".into(),
)),
}
}
pub(crate) async fn summarize_usage_audits(
&self,
query: &aether_data_contracts::repository::usage::UsageAuditSummaryQuery,
@@ -2747,6 +2836,35 @@ impl GatewayDataState {
}
}
pub(crate) async fn list_provider_expenses(
&self,
query: &ProviderExpenseQuery,
) -> Result<Option<ProviderExpensePage>, DataLayerError> {
match &self.billing_reader {
Some(repo) => repo.list_provider_expenses(query).await,
None => Ok(None),
}
}
pub(crate) async fn create_provider_expense(
&self,
input: &ProviderExpenseInput,
) -> Result<AdminBillingMutationOutcome<ProviderExpenseRecord>, DataLayerError> {
match &self.billing_reader {
Some(repo) => repo.create_provider_expense(input).await,
None => Ok(AdminBillingMutationOutcome::Unavailable),
}
}
pub(crate) async fn void_provider_expense(
&self,
id: &str,
operator: Option<&str>,
) -> Result<AdminBillingMutationOutcome<ProviderExpenseRecord>, DataLayerError> {
match &self.billing_reader {
Some(repo) => repo.void_provider_expense(id, operator).await,
None => Ok(AdminBillingMutationOutcome::Unavailable),
}
}
pub(crate) async fn list_billing_plans(
&self,
include_disabled: bool,
@@ -2819,6 +2937,21 @@ impl GatewayDataState {
}
}
pub(crate) async fn list_user_plan_entitlements_with_history(
&self,
user_id: &str,
include_inactive: bool,
) -> Result<Option<Vec<UserPlanEntitlementRecord>>, DataLayerError> {
match &self.billing_reader {
Some(repository) => {
repository
.list_user_plan_entitlements_with_history(user_id, include_inactive)
.await
}
None => Ok(None),
}
}
pub(crate) async fn revoke_user_plan_entitlement(
&self,
user_id: &str,