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
@@ -168,6 +168,26 @@ impl DataBackends {
}
}
pub async fn rebuild_overview_buckets(
&self,
input: &StatsHourlyAggregationInput,
) -> Result<usize, DataLayerError> {
match self.sql_backend() {
Some(backend) => backend.rebuild_overview_buckets(input).await,
None => Ok(0),
}
}
pub async fn drain_overview_dirty_events(
&self,
now: chrono::DateTime<chrono::Utc>,
) -> Result<u64, DataLayerError> {
match self.sql_backend() {
Some(backend) => backend.drain_overview_dirty_events(now).await,
None => Ok(0),
}
}
pub async fn aggregate_stats_hourly(
&self,
input: &StatsHourlyAggregationInput,
@@ -417,6 +437,30 @@ impl<'a> SqlBackendRef<'a> {
}
}
async fn rebuild_overview_buckets(
self,
input: &StatsHourlyAggregationInput,
) -> Result<usize, DataLayerError> {
match self {
#[cfg(feature = "postgres")]
Self::Postgres(postgres) => postgres.rebuild_overview_buckets(input).await,
#[cfg(not(feature = "postgres"))]
Self::Disabled(_) => unreachable!("a SQL backend cannot exist without a driver"),
}
}
async fn drain_overview_dirty_events(
self,
now: chrono::DateTime<chrono::Utc>,
) -> Result<u64, DataLayerError> {
match self {
#[cfg(feature = "postgres")]
Self::Postgres(postgres) => postgres.drain_overview_dirty_events(now).await,
#[cfg(not(feature = "postgres"))]
Self::Disabled(_) => unreachable!("a SQL backend cannot exist without a driver"),
}
}
async fn aggregate_stats_hourly(
self,
input: &StatsHourlyAggregationInput,
@@ -13,6 +13,32 @@ mod sql;
use self::sql::*;
impl PostgresBackend {
pub async fn drain_overview_dirty_events(
&self,
now: DateTime<Utc>,
) -> Result<u64, DataLayerError> {
let repository = aether_data_postgres::SqlxUsageReadRepository::new(self.pool().clone());
let merged = repository.merge_overview_dirty_events().await?;
let retained = repository.maintain_dashboard_projection(now, 1_000).await?;
Ok(merged + u64::from(retained))
}
pub async fn rebuild_overview_buckets(
&self,
input: &StatsHourlyAggregationInput,
) -> Result<usize, DataLayerError> {
let repository = aether_data_postgres::SqlxUsageReadRepository::new(self.pool().clone());
let cleaned = repository
.maintain_dashboard_projection(input.aggregated_at, 1_000)
.await?;
let rebuilt = repository
.rebuild_overview_buckets(input.target_hour_utc + chrono::Duration::hours(1), 8)
.await?;
// Retention work uses the worker's existing bounded catch-up loop too,
// so a busy installation can retire more than one batch per hour.
Ok(rebuilt + usize::from(cleaned))
}
pub async fn aggregate_stats_hourly(
&self,
input: &StatsHourlyAggregationInput,