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
@@ -426,6 +426,10 @@ columns = ["user_id", "created_at"]
name = "idx_payment_orders_status"
columns = ["status"]
[[table.payment_orders.indexes]]
name = "idx_payment_orders_status_credited_user"
columns = ["status", "credited_at", "user_id"]
[[table.payment_orders.indexes]]
name = "idx_payment_orders_gateway_order_id"
columns = ["gateway_order_id"]
@@ -928,6 +928,26 @@ columns = ["processed_at", "created_at", "id"]
name = "ix_usage_counter_deltas_request_kind"
columns = ["request_id", "kind", "target_id"]
[[table.usage.columns]]
name = "failure_origin"
type = "text"
nullable = true
[[table.usage.columns]]
name = "failure_stage"
type = "text"
nullable = true
[[table.usage.columns]]
name = "failure_reason"
type = "text"
nullable = true
[[table.usage.columns]]
name = "failure_schema_version"
type = "int32"
nullable = true
[table.usage_settlement_snapshots]
domain = "usage"
order = 20
@@ -943,6 +963,52 @@ name = "billing_status"
type = "text"
length = 64
[[table.usage_settlement_snapshots.columns]]
name = "quota_covered_amount_usd"
type = "decimal_money"
nullable = true
driver.postgres.type = "numeric(20,8)"
[[table.usage_settlement_snapshots.columns]]
name = "wallet_consumed_amount_usd"
type = "decimal_money"
nullable = true
driver.postgres.type = "numeric(20,8)"
[[table.usage_settlement_snapshots.columns]]
name = "wallet_debit_amount_usd"
type = "decimal_money"
nullable = true
driver.postgres.type = "numeric(20,8)"
[[table.usage_settlement_snapshots.columns]]
name = "wallet_recharge_debit_usd"
type = "decimal_money"
nullable = true
driver.postgres.type = "numeric(20,8)"
[[table.usage_settlement_snapshots.columns]]
name = "wallet_gift_debit_usd"
type = "decimal_money"
nullable = true
driver.postgres.type = "numeric(20,8)"
[[table.usage_settlement_snapshots.columns]]
name = "wallet_overdraft_usd"
type = "decimal_money"
nullable = true
driver.postgres.type = "numeric(20,8)"
[[table.usage_settlement_snapshots.columns]]
name = "allocation_schema_version"
type = "int32"
nullable = true
[[table.usage_settlement_snapshots.columns]]
name = "allocation_status"
type = "text"
nullable = true
[[table.usage_settlement_snapshots.columns]]
name = "wallet_id"
type = "text_id"
@@ -0,0 +1,175 @@
[table.usage_attribution_snapshots]
domain = "usage"
primary_key = ["request_id"]
[[table.usage_attribution_snapshots.columns]]
name = "request_id"
type = "text"
[[table.usage_attribution_snapshots.columns]]
name = "actor_user_id"
type = "text"
nullable = true
[[table.usage_attribution_snapshots.columns]]
name = "credential_owner_id"
type = "text"
nullable = true
[[table.usage_attribution_snapshots.columns]]
name = "attribution_kind"
type = "text"
default = "unknown"
[[table.usage_attribution_snapshots.columns]]
name = "attribution_source"
type = "text"
default = "unknown"
[[table.usage_attribution_snapshots.columns]]
name = "record_kind"
type = "text"
default = "request"
[[table.usage_attribution_snapshots.columns]]
name = "parent_request_id"
type = "text"
nullable = true
[[table.usage_attribution_snapshots.columns]]
name = "schema_version"
type = "int32"
default = 1
[[table.usage_attribution_snapshots.columns]]
name = "attribution_revision"
type = "int64"
default = 1
[[table.usage_attribution_snapshots.columns]]
name = "recorded_at"
type = "timestamp"
default = { raw = "NOW()" }
[[table.usage_attribution_snapshots.indexes]]
name = "ix_usage_attribution_actor_request"
columns = ["actor_user_id", "request_id"]
[[table.usage_attribution_snapshots.indexes]]
name = "ix_usage_attribution_owner_request"
columns = ["credential_owner_id", "request_id"]
[table.stats_bucket_state]
domain = "stats"
primary_key = ["projection_version","granularity","bucket_start"]
[[table.stats_bucket_state.columns]]
name = "projection_version"
type = "text"
[[table.stats_bucket_state.columns]]
name = "granularity"
type = "text"
[[table.stats_bucket_state.columns]]
name = "bucket_start"
type = "timestamp"
[[table.stats_bucket_state.columns]]
name = "source_revision"
type = "int64"
default = 0
[[table.stats_bucket_state.columns]]
name = "built_revision"
type = "int64"
default = -1
[[table.stats_bucket_state.columns]]
name = "coverage_status"
type = "text"
default = "unbuilt"
[[table.stats_bucket_state.columns]]
name = "built_at"
type = "timestamp"
nullable = true
[[table.stats_bucket_state.columns]]
name = "last_error"
type = "text"
nullable = true
[[table.stats_bucket_state.columns]]
name = "last_failed_at"
type = "timestamp"
nullable = true
[table.stats_overview_hourly]
domain = "stats"
primary_key = ["projection_version","bucket_start","dimensions"]
[[table.stats_overview_hourly.columns]]
name = "projection_version"
type = "text"
[[table.stats_overview_hourly.columns]]
name = "bucket_start"
type = "timestamp"
[[table.stats_overview_hourly.columns]]
name = "dimensions"
type = "json"
[[table.stats_overview_hourly.columns]]
name = "metrics"
type = "json"
[table.stats_overview_daily]
domain = "stats"
primary_key = ["projection_version","bucket_start","dimensions"]
[[table.stats_overview_daily.columns]]
name = "projection_version"
type = "text"
[[table.stats_overview_daily.columns]]
name = "bucket_start"
type = "timestamp"
[[table.stats_overview_daily.columns]]
name = "dimensions"
type = "json"
[[table.stats_overview_daily.columns]]
name = "metrics"
type = "json"
[table.stats_overview_dirty_events]
domain = "stats"
primary_key = ["transaction_id", "projection_version", "granularity", "bucket_start"]
[[table.stats_overview_dirty_events.columns]]
name = "transaction_id"
type = "int64"
[[table.stats_overview_dirty_events.columns]]
name = "projection_version"
type = "text"
[[table.stats_overview_dirty_events.columns]]
name = "granularity"
type = "text"
[[table.stats_overview_dirty_events.columns]]
name = "bucket_start"
type = "timestamp"
[[table.stats_overview_dirty_events.columns]]
name = "unrecoverable"
type = "bool"
default = false
[[table.stats_overview_dirty_events.indexes]]
name = "ix_stats_overview_dirty_events_bucket"
columns = ["projection_version", "granularity", "bucket_start"]
@@ -0,0 +1,187 @@
# Future-only dashboard aggregates. Trigger functions and activation data live in
# migration 20260919000000_add_future_dashboard_summary.sql; no history is backfilled.
[table.dashboard_activity_hour]
domain = "stats"
primary_key = ["bucket_start", "shard"]
[[table.dashboard_activity_hour.columns]]
name = "bucket_start"
type = "timestamp"
[[table.dashboard_activity_hour.columns]]
name = "shard"
type = "int32"
driver.postgres.type = "smallint"
[[table.dashboard_activity_hour.columns]]
name = "request_count"
type = "int64"
[table.dashboard_stats_state]
domain = "stats"
primary_key = ["singleton"]
[[table.dashboard_stats_state.columns]]
name = "singleton"
type = "bool"
default = true
[[table.dashboard_stats_state.columns]]
name = "stats_since"
type = "timestamp"
[[table.dashboard_stats_state.columns]]
name = "contributions_cleanup_cursor"
type = "text"
length = 100
nullable = true
[table.dashboard_activity_minute]
domain = "stats"
primary_key = ["bucket_start", "shard"]
[[table.dashboard_activity_minute.columns]]
name = "bucket_start"
type = "timestamp"
[[table.dashboard_activity_minute.columns]]
name = "shard"
type = "int32"
driver.postgres.type = "smallint"
[[table.dashboard_activity_minute.columns]]
name = "request_count"
type = "int64"
[table.dashboard_request_contributions]
domain = "stats"
primary_key = ["request_id"]
[[table.dashboard_request_contributions.columns]]
name = "request_id"
type = "text"
length = 100
[[table.dashboard_request_contributions.columns]]
name = "created_at"
type = "timestamp"
[[table.dashboard_request_contributions.columns]]
name = "actor_user_id"
type = "text"
nullable = true
length = 255
[[table.dashboard_request_contributions.columns]]
name = "metrics"
type = "json"
[table.dashboard_stats_total]
domain = "stats"
primary_key = ["shard"]
[[table.dashboard_stats_total.columns]]
name = "shard"
type = "int32"
driver.postgres.type = "smallint"
[[table.dashboard_stats_total.columns]]
name = "metrics"
type = "json"
default = { raw = "'{}'::jsonb" }
[table.dashboard_stats_minute]
domain = "stats"
primary_key = ["bucket_start","shard"]
[[table.dashboard_stats_minute.columns]]
name = "bucket_start"
type = "timestamp"
[[table.dashboard_stats_minute.columns]]
name = "shard"
type = "int32"
driver.postgres.type = "smallint"
[[table.dashboard_stats_minute.columns]]
name = "metrics"
type = "json"
default = { raw = "'{}'::jsonb" }
[table.dashboard_actor_minute]
domain = "stats"
primary_key = ["bucket_start","shard","actor_user_id"]
[[table.dashboard_actor_minute.columns]]
name = "bucket_start"
type = "timestamp"
[[table.dashboard_actor_minute.columns]]
name = "shard"
type = "int32"
driver.postgres.type = "smallint"
[[table.dashboard_actor_minute.columns]]
name = "actor_user_id"
type = "text"
length = 255
[[table.dashboard_actor_minute.columns]]
name = "request_count"
type = "int64"
[table.dashboard_stats_pending]
domain = "stats"
primary_key = ["transaction_id","request_id"]
[[table.dashboard_stats_pending.columns]]
name = "transaction_id"
type = "int64"
[[table.dashboard_stats_pending.columns]]
name = "request_id"
type = "text"
length = 100
[[table.dashboard_stats_pending.columns]]
name = "deleted_fact"
type = "json"
nullable = true
[table.dashboard_user_events_minute]
domain = "stats"
primary_key = ["bucket_start","shard"]
[[table.dashboard_user_events_minute.columns]]
name = "bucket_start"
type = "timestamp"
[[table.dashboard_user_events_minute.columns]]
name = "shard"
type = "int32"
driver.postgres.type = "smallint"
[[table.dashboard_user_events_minute.columns]]
name = "created_count"
type = "int64"
default = 0
[[table.dashboard_user_events_minute.columns]]
name = "deleted_count"
type = "int64"
default = 0
# Transaction-local events only; no committed rows are exported or restored.
[table.dashboard_user_anonymization_pending]
domain = "stats"
primary_key = ["transaction_id", "user_id"]
[[table.dashboard_user_anonymization_pending.columns]]
name = "transaction_id"
type = "int64"
[[table.dashboard_user_anonymization_pending.columns]]
name = "user_id"
type = "text"
length = 255
@@ -0,0 +1,88 @@
[table.provider_expenses]
domain = "wallet_billing"
primary_key = ["id"]
[[table.provider_expenses.columns]]
name = "id"
type = "text"
[[table.provider_expenses.columns]]
name = "client_request_id"
type = "text"
[[table.provider_expenses.columns]]
name = "provider_id"
type = "text"
[[table.provider_expenses.columns]]
name = "provider_name"
type = "text"
[[table.provider_expenses.columns]]
name = "kind"
type = "text"
[[table.provider_expenses.columns]]
name = "amount"
type = "decimal_money"
driver.postgres.type = "numeric(20,8)"
[[table.provider_expenses.columns]]
name = "currency"
type = "text"
[[table.provider_expenses.columns]]
name = "paid_at"
type = "timestamp"
[[table.provider_expenses.columns]]
name = "period_start"
type = "timestamp"
nullable = true
[[table.provider_expenses.columns]]
name = "period_end"
type = "timestamp"
nullable = true
[[table.provider_expenses.columns]]
name = "note"
type = "text"
nullable = true
[[table.provider_expenses.columns]]
name = "external_reference"
type = "text"
nullable = true
[[table.provider_expenses.columns]]
name = "created_by"
type = "text"
nullable = true
[[table.provider_expenses.columns]]
name = "created_at"
type = "timestamp"
default = { raw = "NOW()" }
[[table.provider_expenses.columns]]
name = "voided_at"
type = "timestamp"
nullable = true
[[table.provider_expenses.columns]]
name = "voided_by"
type = "text"
nullable = true
[[table.provider_expenses.uniques]]
name = "provider_expenses_client_request_id_key"
columns = ["client_request_id"]
[[table.provider_expenses.indexes]]
name = "ix_provider_expenses_paid_at"
columns = ["paid_at", "id"]
[[table.provider_expenses.indexes]]
name = "ix_provider_expenses_provider_paid_at"
columns = ["provider_id", "paid_at"]