Browse Source

perf(dashboard): comprehensive homepage performance optimization (#808)

* perf(dashboard): add cache key types and builder utilities

* perf(cache): add Redis overview cache with 10s TTL

* perf(cache): add Redis statistics cache with 30s TTL

* perf(db): rewrite statistics SQL to eliminate CROSS JOIN anti-pattern

* perf(dashboard): wire overview action to Redis cache with 10s TTL

* perf(dashboard): wire statistics action to Redis cache

Replace direct DB calls (getUserStatisticsFromDB, getKeyStatisticsFromDB,
getMixedStatisticsFromDB) with getStatisticsWithCache() in getUserStatistics().

The cache module handles Redis read-through with 30s TTL, distributed
locking, and fail-open fallback to direct DB queries.

* perf(dashboard): optimize client-side caching, polling, and lazy load charts

- Remove cache: no-store from fetchLeaderboard to respect s-maxage=60
- Reduce overview polling from 5s to 15s with staleTime: 10_000
- Add staleTime: 30_000 and keepPreviousData to statistics query
- Add staleTime: 60_000 to all 3 leaderboard queries
- Lazy load StatisticsChartCard via next/dynamic with ssr: false

* perf(db): add PG indexes for dashboard query optimization

* perf(dashboard): expand SSR prefetch to include overview data

* perf(db): commit migration artifacts for dashboard query indexes

* test(dashboard): add unit tests for performance optimization modules

* test(dashboard): add unit tests for performance optimization modules

* test(actions): mock redis lifecycle in provider undo tests

* fix(i18n): use fullwidth parentheses in zh-TW dashboard labels

* fix(dashboard): address all bugbot comments from PR #808

- Replace O(N) redis.keys() with cursor-based scanPattern() in
  invalidateStatisticsCache (issue 1)
- Fix lock not released when queryDatabase throws: move del(lockKey) to
  finally block in both statistics-cache and overview-cache (issues 2+4)
- Wrap setex in inner try/catch so Redis write failure doesn't trigger
  double DB query via outer catch (issues 3+4)
- Guard queryDatabase against undefined userId for keys/mixed modes
  (issue 5)
- Remove duplicate buildCacheKey; use buildStatisticsCacheKey from
  dashboard-cache.ts throughout (issue 6)
- Add TypeScript overloads to buildOverviewCacheKey preventing
  overview:user:undefined keys at compile time (issue 7)
- Replace hardcoded Chinese sentinel "其他用户" with "__others__" and
  map it to i18n key othersAggregate in 5 locales (issue 8)
- Extract duplicated Redis in-memory mock into shared
  tests/unit/actions/redis-mock-utils.ts (issue 9)

* chore: format code (dashboard-perf-optimization-df4337e)

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Ding 2 days ago
parent
commit
e8539541ea

+ 2 - 0
drizzle/0071_purple_captain_midlands.sql

@@ -0,0 +1,2 @@
+CREATE INDEX "idx_message_request_user_created_at_cost_stats" ON "message_request" USING btree ("user_id","created_at","cost_usd") WHERE "message_request"."deleted_at" IS NULL AND ("message_request"."blocked_by" IS NULL OR "message_request"."blocked_by" <> 'warmup');--> statement-breakpoint
+CREATE INDEX "idx_message_request_provider_created_at_active" ON "message_request" USING btree ("provider_id","created_at") WHERE "message_request"."deleted_at" IS NULL AND ("message_request"."blocked_by" IS NULL OR "message_request"."blocked_by" <> 'warmup');

+ 3301 - 0
drizzle/meta/0071_snapshot.json

@@ -0,0 +1,3301 @@
+{
+  "id": "0612bb83-c19b-4507-a304-47fedf9d0b61",
+  "prevId": "36940835-849c-47f5-9cbf-15a6c250499a",
+  "version": "7",
+  "dialect": "postgresql",
+  "tables": {
+    "public.error_rules": {
+      "name": "error_rules",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "pattern": {
+          "name": "pattern",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "match_type": {
+          "name": "match_type",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'regex'"
+        },
+        "category": {
+          "name": "category",
+          "type": "varchar(50)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "override_response": {
+          "name": "override_response",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "override_status_code": {
+          "name": "override_status_code",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "is_default": {
+          "name": "is_default",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "priority": {
+          "name": "priority",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 0
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {
+        "idx_error_rules_enabled": {
+          "name": "idx_error_rules_enabled",
+          "columns": [
+            {
+              "expression": "is_enabled",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "priority",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "unique_pattern": {
+          "name": "unique_pattern",
+          "columns": [
+            {
+              "expression": "pattern",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": true,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_category": {
+          "name": "idx_category",
+          "columns": [
+            {
+              "expression": "category",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_match_type": {
+          "name": "idx_match_type",
+          "columns": [
+            {
+              "expression": "match_type",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.keys": {
+      "name": "keys",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "user_id": {
+          "name": "user_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "key": {
+          "name": "key",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": true
+        },
+        "expires_at": {
+          "name": "expires_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "can_login_web_ui": {
+          "name": "can_login_web_ui",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": false
+        },
+        "limit_5h_usd": {
+          "name": "limit_5h_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_daily_usd": {
+          "name": "limit_daily_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "daily_reset_mode": {
+          "name": "daily_reset_mode",
+          "type": "daily_reset_mode",
+          "typeSchema": "public",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'fixed'"
+        },
+        "daily_reset_time": {
+          "name": "daily_reset_time",
+          "type": "varchar(5)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'00:00'"
+        },
+        "limit_weekly_usd": {
+          "name": "limit_weekly_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_monthly_usd": {
+          "name": "limit_monthly_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_total_usd": {
+          "name": "limit_total_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_concurrent_sessions": {
+          "name": "limit_concurrent_sessions",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 0
+        },
+        "provider_group": {
+          "name": "provider_group",
+          "type": "varchar(200)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'default'"
+        },
+        "cache_ttl_preference": {
+          "name": "cache_ttl_preference",
+          "type": "varchar(10)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "deleted_at": {
+          "name": "deleted_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {
+        "idx_keys_user_id": {
+          "name": "idx_keys_user_id",
+          "columns": [
+            {
+              "expression": "user_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_keys_key": {
+          "name": "idx_keys_key",
+          "columns": [
+            {
+              "expression": "key",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_keys_created_at": {
+          "name": "idx_keys_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_keys_deleted_at": {
+          "name": "idx_keys_deleted_at",
+          "columns": [
+            {
+              "expression": "deleted_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.message_request": {
+      "name": "message_request",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "provider_id": {
+          "name": "provider_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "user_id": {
+          "name": "user_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "key": {
+          "name": "key",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "model": {
+          "name": "model",
+          "type": "varchar(128)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "duration_ms": {
+          "name": "duration_ms",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cost_usd": {
+          "name": "cost_usd",
+          "type": "numeric(21, 15)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'0'"
+        },
+        "cost_multiplier": {
+          "name": "cost_multiplier",
+          "type": "numeric(10, 4)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "session_id": {
+          "name": "session_id",
+          "type": "varchar(64)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "request_sequence": {
+          "name": "request_sequence",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 1
+        },
+        "provider_chain": {
+          "name": "provider_chain",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "status_code": {
+          "name": "status_code",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "api_type": {
+          "name": "api_type",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "endpoint": {
+          "name": "endpoint",
+          "type": "varchar(256)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "original_model": {
+          "name": "original_model",
+          "type": "varchar(128)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "input_tokens": {
+          "name": "input_tokens",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "output_tokens": {
+          "name": "output_tokens",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "ttfb_ms": {
+          "name": "ttfb_ms",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cache_creation_input_tokens": {
+          "name": "cache_creation_input_tokens",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cache_read_input_tokens": {
+          "name": "cache_read_input_tokens",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cache_creation_5m_input_tokens": {
+          "name": "cache_creation_5m_input_tokens",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cache_creation_1h_input_tokens": {
+          "name": "cache_creation_1h_input_tokens",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cache_ttl_applied": {
+          "name": "cache_ttl_applied",
+          "type": "varchar(10)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "context_1m_applied": {
+          "name": "context_1m_applied",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": false
+        },
+        "swap_cache_ttl_applied": {
+          "name": "swap_cache_ttl_applied",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": false
+        },
+        "special_settings": {
+          "name": "special_settings",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "error_message": {
+          "name": "error_message",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "error_stack": {
+          "name": "error_stack",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "error_cause": {
+          "name": "error_cause",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "blocked_by": {
+          "name": "blocked_by",
+          "type": "varchar(50)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "blocked_reason": {
+          "name": "blocked_reason",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "user_agent": {
+          "name": "user_agent",
+          "type": "varchar(512)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "messages_count": {
+          "name": "messages_count",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "deleted_at": {
+          "name": "deleted_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {
+        "idx_message_request_user_date_cost": {
+          "name": "idx_message_request_user_date_cost",
+          "columns": [
+            {
+              "expression": "user_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "cost_usd",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"message_request\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_user_created_at_cost_stats": {
+          "name": "idx_message_request_user_created_at_cost_stats",
+          "columns": [
+            {
+              "expression": "user_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "cost_usd",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_user_query": {
+          "name": "idx_message_request_user_query",
+          "columns": [
+            {
+              "expression": "user_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"message_request\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_provider_created_at_active": {
+          "name": "idx_message_request_provider_created_at_active",
+          "columns": [
+            {
+              "expression": "provider_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_session_id": {
+          "name": "idx_message_request_session_id",
+          "columns": [
+            {
+              "expression": "session_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"message_request\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_session_id_prefix": {
+          "name": "idx_message_request_session_id_prefix",
+          "columns": [
+            {
+              "expression": "\"session_id\" varchar_pattern_ops",
+              "asc": true,
+              "isExpression": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_session_seq": {
+          "name": "idx_message_request_session_seq",
+          "columns": [
+            {
+              "expression": "session_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "request_sequence",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"message_request\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_endpoint": {
+          "name": "idx_message_request_endpoint",
+          "columns": [
+            {
+              "expression": "endpoint",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"message_request\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_blocked_by": {
+          "name": "idx_message_request_blocked_by",
+          "columns": [
+            {
+              "expression": "blocked_by",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"message_request\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_provider_id": {
+          "name": "idx_message_request_provider_id",
+          "columns": [
+            {
+              "expression": "provider_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_user_id": {
+          "name": "idx_message_request_user_id",
+          "columns": [
+            {
+              "expression": "user_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_key": {
+          "name": "idx_message_request_key",
+          "columns": [
+            {
+              "expression": "key",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_key_created_at_id": {
+          "name": "idx_message_request_key_created_at_id",
+          "columns": [
+            {
+              "expression": "key",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": false,
+              "nulls": "last"
+            },
+            {
+              "expression": "id",
+              "isExpression": false,
+              "asc": false,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"message_request\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_key_model_active": {
+          "name": "idx_message_request_key_model_active",
+          "columns": [
+            {
+              "expression": "key",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "model",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"model\" IS NOT NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_key_endpoint_active": {
+          "name": "idx_message_request_key_endpoint_active",
+          "columns": [
+            {
+              "expression": "key",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "endpoint",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"endpoint\" IS NOT NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_created_at_id_active": {
+          "name": "idx_message_request_created_at_id_active",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": false,
+              "nulls": "last"
+            },
+            {
+              "expression": "id",
+              "isExpression": false,
+              "asc": false,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"message_request\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_model_active": {
+          "name": "idx_message_request_model_active",
+          "columns": [
+            {
+              "expression": "model",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"model\" IS NOT NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_status_code_active": {
+          "name": "idx_message_request_status_code_active",
+          "columns": [
+            {
+              "expression": "status_code",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"status_code\" IS NOT NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_created_at": {
+          "name": "idx_message_request_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_deleted_at": {
+          "name": "idx_message_request_deleted_at",
+          "columns": [
+            {
+              "expression": "deleted_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.model_prices": {
+      "name": "model_prices",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "model_name": {
+          "name": "model_name",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "price_data": {
+          "name": "price_data",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "source": {
+          "name": "source",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'litellm'"
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {
+        "idx_model_prices_latest": {
+          "name": "idx_model_prices_latest",
+          "columns": [
+            {
+              "expression": "model_name",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": false,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_model_prices_model_name": {
+          "name": "idx_model_prices_model_name",
+          "columns": [
+            {
+              "expression": "model_name",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_model_prices_created_at": {
+          "name": "idx_model_prices_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": false,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_model_prices_source": {
+          "name": "idx_model_prices_source",
+          "columns": [
+            {
+              "expression": "source",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.notification_settings": {
+      "name": "notification_settings",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "enabled": {
+          "name": "enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "use_legacy_mode": {
+          "name": "use_legacy_mode",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "circuit_breaker_enabled": {
+          "name": "circuit_breaker_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "circuit_breaker_webhook": {
+          "name": "circuit_breaker_webhook",
+          "type": "varchar(512)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "daily_leaderboard_enabled": {
+          "name": "daily_leaderboard_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "daily_leaderboard_webhook": {
+          "name": "daily_leaderboard_webhook",
+          "type": "varchar(512)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "daily_leaderboard_time": {
+          "name": "daily_leaderboard_time",
+          "type": "varchar(10)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'09:00'"
+        },
+        "daily_leaderboard_top_n": {
+          "name": "daily_leaderboard_top_n",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 5
+        },
+        "cost_alert_enabled": {
+          "name": "cost_alert_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "cost_alert_webhook": {
+          "name": "cost_alert_webhook",
+          "type": "varchar(512)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cost_alert_threshold": {
+          "name": "cost_alert_threshold",
+          "type": "numeric(5, 2)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'0.80'"
+        },
+        "cost_alert_check_interval": {
+          "name": "cost_alert_check_interval",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 60
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.notification_target_bindings": {
+      "name": "notification_target_bindings",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "notification_type": {
+          "name": "notification_type",
+          "type": "notification_type",
+          "typeSchema": "public",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "target_id": {
+          "name": "target_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "schedule_cron": {
+          "name": "schedule_cron",
+          "type": "varchar(100)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "schedule_timezone": {
+          "name": "schedule_timezone",
+          "type": "varchar(50)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "template_override": {
+          "name": "template_override",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {
+        "unique_notification_target_binding": {
+          "name": "unique_notification_target_binding",
+          "columns": [
+            {
+              "expression": "notification_type",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "target_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": true,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_notification_bindings_type": {
+          "name": "idx_notification_bindings_type",
+          "columns": [
+            {
+              "expression": "notification_type",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "is_enabled",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_notification_bindings_target": {
+          "name": "idx_notification_bindings_target",
+          "columns": [
+            {
+              "expression": "target_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "is_enabled",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {
+        "notification_target_bindings_target_id_webhook_targets_id_fk": {
+          "name": "notification_target_bindings_target_id_webhook_targets_id_fk",
+          "tableFrom": "notification_target_bindings",
+          "tableTo": "webhook_targets",
+          "columnsFrom": [
+            "target_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "cascade",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.provider_endpoint_probe_logs": {
+      "name": "provider_endpoint_probe_logs",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "endpoint_id": {
+          "name": "endpoint_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "source": {
+          "name": "source",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'scheduled'"
+        },
+        "ok": {
+          "name": "ok",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "status_code": {
+          "name": "status_code",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "latency_ms": {
+          "name": "latency_ms",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "error_type": {
+          "name": "error_type",
+          "type": "varchar(64)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "error_message": {
+          "name": "error_message",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {
+        "idx_provider_endpoint_probe_logs_endpoint_created_at": {
+          "name": "idx_provider_endpoint_probe_logs_endpoint_created_at",
+          "columns": [
+            {
+              "expression": "endpoint_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": false,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_provider_endpoint_probe_logs_created_at": {
+          "name": "idx_provider_endpoint_probe_logs_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {
+        "provider_endpoint_probe_logs_endpoint_id_provider_endpoints_id_fk": {
+          "name": "provider_endpoint_probe_logs_endpoint_id_provider_endpoints_id_fk",
+          "tableFrom": "provider_endpoint_probe_logs",
+          "tableTo": "provider_endpoints",
+          "columnsFrom": [
+            "endpoint_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "cascade",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.provider_endpoints": {
+      "name": "provider_endpoints",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "vendor_id": {
+          "name": "vendor_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "provider_type": {
+          "name": "provider_type",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'claude'"
+        },
+        "url": {
+          "name": "url",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "label": {
+          "name": "label",
+          "type": "varchar(200)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "sort_order": {
+          "name": "sort_order",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 0
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "last_probed_at": {
+          "name": "last_probed_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "last_probe_ok": {
+          "name": "last_probe_ok",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "last_probe_status_code": {
+          "name": "last_probe_status_code",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "last_probe_latency_ms": {
+          "name": "last_probe_latency_ms",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "last_probe_error_type": {
+          "name": "last_probe_error_type",
+          "type": "varchar(64)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "last_probe_error_message": {
+          "name": "last_probe_error_message",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "deleted_at": {
+          "name": "deleted_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {
+        "uniq_provider_endpoints_vendor_type_url": {
+          "name": "uniq_provider_endpoints_vendor_type_url",
+          "columns": [
+            {
+              "expression": "vendor_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "provider_type",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "url",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": true,
+          "where": "\"provider_endpoints\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_provider_endpoints_vendor_type": {
+          "name": "idx_provider_endpoints_vendor_type",
+          "columns": [
+            {
+              "expression": "vendor_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "provider_type",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"provider_endpoints\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_provider_endpoints_enabled": {
+          "name": "idx_provider_endpoints_enabled",
+          "columns": [
+            {
+              "expression": "is_enabled",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "vendor_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "provider_type",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"provider_endpoints\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_provider_endpoints_pick_enabled": {
+          "name": "idx_provider_endpoints_pick_enabled",
+          "columns": [
+            {
+              "expression": "vendor_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "provider_type",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "is_enabled",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "sort_order",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"provider_endpoints\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_provider_endpoints_created_at": {
+          "name": "idx_provider_endpoints_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_provider_endpoints_deleted_at": {
+          "name": "idx_provider_endpoints_deleted_at",
+          "columns": [
+            {
+              "expression": "deleted_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {
+        "provider_endpoints_vendor_id_provider_vendors_id_fk": {
+          "name": "provider_endpoints_vendor_id_provider_vendors_id_fk",
+          "tableFrom": "provider_endpoints",
+          "tableTo": "provider_vendors",
+          "columnsFrom": [
+            "vendor_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "cascade",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.provider_vendors": {
+      "name": "provider_vendors",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "website_domain": {
+          "name": "website_domain",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "display_name": {
+          "name": "display_name",
+          "type": "varchar(200)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "website_url": {
+          "name": "website_url",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "favicon_url": {
+          "name": "favicon_url",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {
+        "uniq_provider_vendors_website_domain": {
+          "name": "uniq_provider_vendors_website_domain",
+          "columns": [
+            {
+              "expression": "website_domain",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": true,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_provider_vendors_created_at": {
+          "name": "idx_provider_vendors_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.providers": {
+      "name": "providers",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "url": {
+          "name": "url",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "key": {
+          "name": "key",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "provider_vendor_id": {
+          "name": "provider_vendor_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "weight": {
+          "name": "weight",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 1
+        },
+        "priority": {
+          "name": "priority",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 0
+        },
+        "group_priorities": {
+          "name": "group_priorities",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'null'::jsonb"
+        },
+        "cost_multiplier": {
+          "name": "cost_multiplier",
+          "type": "numeric(10, 4)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'1.0'"
+        },
+        "group_tag": {
+          "name": "group_tag",
+          "type": "varchar(50)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "provider_type": {
+          "name": "provider_type",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'claude'"
+        },
+        "preserve_client_ip": {
+          "name": "preserve_client_ip",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "model_redirects": {
+          "name": "model_redirects",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "allowed_models": {
+          "name": "allowed_models",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'null'::jsonb"
+        },
+        "join_claude_pool": {
+          "name": "join_claude_pool",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": false
+        },
+        "codex_instructions_strategy": {
+          "name": "codex_instructions_strategy",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'auto'"
+        },
+        "mcp_passthrough_type": {
+          "name": "mcp_passthrough_type",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'none'"
+        },
+        "mcp_passthrough_url": {
+          "name": "mcp_passthrough_url",
+          "type": "varchar(512)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_5h_usd": {
+          "name": "limit_5h_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_daily_usd": {
+          "name": "limit_daily_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "daily_reset_mode": {
+          "name": "daily_reset_mode",
+          "type": "daily_reset_mode",
+          "typeSchema": "public",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'fixed'"
+        },
+        "daily_reset_time": {
+          "name": "daily_reset_time",
+          "type": "varchar(5)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'00:00'"
+        },
+        "limit_weekly_usd": {
+          "name": "limit_weekly_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_monthly_usd": {
+          "name": "limit_monthly_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_total_usd": {
+          "name": "limit_total_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "total_cost_reset_at": {
+          "name": "total_cost_reset_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_concurrent_sessions": {
+          "name": "limit_concurrent_sessions",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 0
+        },
+        "max_retry_attempts": {
+          "name": "max_retry_attempts",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "circuit_breaker_failure_threshold": {
+          "name": "circuit_breaker_failure_threshold",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 5
+        },
+        "circuit_breaker_open_duration": {
+          "name": "circuit_breaker_open_duration",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 1800000
+        },
+        "circuit_breaker_half_open_success_threshold": {
+          "name": "circuit_breaker_half_open_success_threshold",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 2
+        },
+        "proxy_url": {
+          "name": "proxy_url",
+          "type": "varchar(512)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "proxy_fallback_to_direct": {
+          "name": "proxy_fallback_to_direct",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": false
+        },
+        "first_byte_timeout_streaming_ms": {
+          "name": "first_byte_timeout_streaming_ms",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 0
+        },
+        "streaming_idle_timeout_ms": {
+          "name": "streaming_idle_timeout_ms",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 0
+        },
+        "request_timeout_non_streaming_ms": {
+          "name": "request_timeout_non_streaming_ms",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 0
+        },
+        "website_url": {
+          "name": "website_url",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "favicon_url": {
+          "name": "favicon_url",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cache_ttl_preference": {
+          "name": "cache_ttl_preference",
+          "type": "varchar(10)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "swap_cache_ttl_billing": {
+          "name": "swap_cache_ttl_billing",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "context_1m_preference": {
+          "name": "context_1m_preference",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "codex_reasoning_effort_preference": {
+          "name": "codex_reasoning_effort_preference",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "codex_reasoning_summary_preference": {
+          "name": "codex_reasoning_summary_preference",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "codex_text_verbosity_preference": {
+          "name": "codex_text_verbosity_preference",
+          "type": "varchar(10)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "codex_parallel_tool_calls_preference": {
+          "name": "codex_parallel_tool_calls_preference",
+          "type": "varchar(10)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "anthropic_max_tokens_preference": {
+          "name": "anthropic_max_tokens_preference",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "anthropic_thinking_budget_preference": {
+          "name": "anthropic_thinking_budget_preference",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "anthropic_adaptive_thinking": {
+          "name": "anthropic_adaptive_thinking",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'null'::jsonb"
+        },
+        "gemini_google_search_preference": {
+          "name": "gemini_google_search_preference",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "tpm": {
+          "name": "tpm",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 0
+        },
+        "rpm": {
+          "name": "rpm",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 0
+        },
+        "rpd": {
+          "name": "rpd",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 0
+        },
+        "cc": {
+          "name": "cc",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 0
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "deleted_at": {
+          "name": "deleted_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {
+        "idx_providers_enabled_priority": {
+          "name": "idx_providers_enabled_priority",
+          "columns": [
+            {
+              "expression": "is_enabled",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "priority",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "weight",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"providers\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_providers_group": {
+          "name": "idx_providers_group",
+          "columns": [
+            {
+              "expression": "group_tag",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"providers\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_providers_vendor_type_url_active": {
+          "name": "idx_providers_vendor_type_url_active",
+          "columns": [
+            {
+              "expression": "provider_vendor_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "provider_type",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "url",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"providers\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_providers_created_at": {
+          "name": "idx_providers_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_providers_deleted_at": {
+          "name": "idx_providers_deleted_at",
+          "columns": [
+            {
+              "expression": "deleted_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_providers_vendor_type": {
+          "name": "idx_providers_vendor_type",
+          "columns": [
+            {
+              "expression": "provider_vendor_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "provider_type",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"providers\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_providers_enabled_vendor_type": {
+          "name": "idx_providers_enabled_vendor_type",
+          "columns": [
+            {
+              "expression": "provider_vendor_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "provider_type",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"providers\".\"deleted_at\" IS NULL AND \"providers\".\"is_enabled\" = true AND \"providers\".\"provider_vendor_id\" IS NOT NULL AND \"providers\".\"provider_vendor_id\" > 0",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {
+        "providers_provider_vendor_id_provider_vendors_id_fk": {
+          "name": "providers_provider_vendor_id_provider_vendors_id_fk",
+          "tableFrom": "providers",
+          "tableTo": "provider_vendors",
+          "columnsFrom": [
+            "provider_vendor_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "restrict",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.request_filters": {
+      "name": "request_filters",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar(100)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "scope": {
+          "name": "scope",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "action": {
+          "name": "action",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "match_type": {
+          "name": "match_type",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "target": {
+          "name": "target",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "replacement": {
+          "name": "replacement",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "priority": {
+          "name": "priority",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 0
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "binding_type": {
+          "name": "binding_type",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'global'"
+        },
+        "provider_ids": {
+          "name": "provider_ids",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "group_tags": {
+          "name": "group_tags",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {
+        "idx_request_filters_enabled": {
+          "name": "idx_request_filters_enabled",
+          "columns": [
+            {
+              "expression": "is_enabled",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "priority",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_request_filters_scope": {
+          "name": "idx_request_filters_scope",
+          "columns": [
+            {
+              "expression": "scope",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_request_filters_action": {
+          "name": "idx_request_filters_action",
+          "columns": [
+            {
+              "expression": "action",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_request_filters_binding": {
+          "name": "idx_request_filters_binding",
+          "columns": [
+            {
+              "expression": "is_enabled",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "binding_type",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.sensitive_words": {
+      "name": "sensitive_words",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "word": {
+          "name": "word",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "match_type": {
+          "name": "match_type",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'contains'"
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {
+        "idx_sensitive_words_enabled": {
+          "name": "idx_sensitive_words_enabled",
+          "columns": [
+            {
+              "expression": "is_enabled",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "match_type",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_sensitive_words_created_at": {
+          "name": "idx_sensitive_words_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.system_settings": {
+      "name": "system_settings",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "site_title": {
+          "name": "site_title",
+          "type": "varchar(128)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'Claude Code Hub'"
+        },
+        "allow_global_usage_view": {
+          "name": "allow_global_usage_view",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "currency_display": {
+          "name": "currency_display",
+          "type": "varchar(10)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'USD'"
+        },
+        "billing_model_source": {
+          "name": "billing_model_source",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'original'"
+        },
+        "timezone": {
+          "name": "timezone",
+          "type": "varchar(64)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "enable_auto_cleanup": {
+          "name": "enable_auto_cleanup",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": false
+        },
+        "cleanup_retention_days": {
+          "name": "cleanup_retention_days",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 30
+        },
+        "cleanup_schedule": {
+          "name": "cleanup_schedule",
+          "type": "varchar(50)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'0 2 * * *'"
+        },
+        "cleanup_batch_size": {
+          "name": "cleanup_batch_size",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 10000
+        },
+        "enable_client_version_check": {
+          "name": "enable_client_version_check",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "verbose_provider_error": {
+          "name": "verbose_provider_error",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "enable_http2": {
+          "name": "enable_http2",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "intercept_anthropic_warmup_requests": {
+          "name": "intercept_anthropic_warmup_requests",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "enable_thinking_signature_rectifier": {
+          "name": "enable_thinking_signature_rectifier",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "enable_thinking_budget_rectifier": {
+          "name": "enable_thinking_budget_rectifier",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "enable_billing_header_rectifier": {
+          "name": "enable_billing_header_rectifier",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "enable_codex_session_id_completion": {
+          "name": "enable_codex_session_id_completion",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "enable_claude_metadata_user_id_injection": {
+          "name": "enable_claude_metadata_user_id_injection",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "enable_response_fixer": {
+          "name": "enable_response_fixer",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "response_fixer_config": {
+          "name": "response_fixer_config",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'{\"fixTruncatedJson\":true,\"fixSseFormat\":true,\"fixEncoding\":true,\"maxJsonDepth\":200,\"maxFixSize\":1048576}'::jsonb"
+        },
+        "quota_db_refresh_interval_seconds": {
+          "name": "quota_db_refresh_interval_seconds",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 10
+        },
+        "quota_lease_percent_5h": {
+          "name": "quota_lease_percent_5h",
+          "type": "numeric(5, 4)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'0.05'"
+        },
+        "quota_lease_percent_daily": {
+          "name": "quota_lease_percent_daily",
+          "type": "numeric(5, 4)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'0.05'"
+        },
+        "quota_lease_percent_weekly": {
+          "name": "quota_lease_percent_weekly",
+          "type": "numeric(5, 4)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'0.05'"
+        },
+        "quota_lease_percent_monthly": {
+          "name": "quota_lease_percent_monthly",
+          "type": "numeric(5, 4)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'0.05'"
+        },
+        "quota_lease_cap_usd": {
+          "name": "quota_lease_cap_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.users": {
+      "name": "users",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "role": {
+          "name": "role",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'user'"
+        },
+        "rpm_limit": {
+          "name": "rpm_limit",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "daily_limit_usd": {
+          "name": "daily_limit_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "provider_group": {
+          "name": "provider_group",
+          "type": "varchar(200)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'default'"
+        },
+        "tags": {
+          "name": "tags",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'[]'::jsonb"
+        },
+        "limit_5h_usd": {
+          "name": "limit_5h_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_weekly_usd": {
+          "name": "limit_weekly_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_monthly_usd": {
+          "name": "limit_monthly_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_total_usd": {
+          "name": "limit_total_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_concurrent_sessions": {
+          "name": "limit_concurrent_sessions",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "daily_reset_mode": {
+          "name": "daily_reset_mode",
+          "type": "daily_reset_mode",
+          "typeSchema": "public",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'fixed'"
+        },
+        "daily_reset_time": {
+          "name": "daily_reset_time",
+          "type": "varchar(5)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'00:00'"
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "expires_at": {
+          "name": "expires_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "allowed_clients": {
+          "name": "allowed_clients",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'[]'::jsonb"
+        },
+        "allowed_models": {
+          "name": "allowed_models",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'[]'::jsonb"
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "deleted_at": {
+          "name": "deleted_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {
+        "idx_users_active_role_sort": {
+          "name": "idx_users_active_role_sort",
+          "columns": [
+            {
+              "expression": "deleted_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "role",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"users\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_users_enabled_expires_at": {
+          "name": "idx_users_enabled_expires_at",
+          "columns": [
+            {
+              "expression": "is_enabled",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "expires_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"users\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_users_tags_gin": {
+          "name": "idx_users_tags_gin",
+          "columns": [
+            {
+              "expression": "tags",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"users\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "gin",
+          "with": {}
+        },
+        "idx_users_created_at": {
+          "name": "idx_users_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_users_deleted_at": {
+          "name": "idx_users_deleted_at",
+          "columns": [
+            {
+              "expression": "deleted_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.webhook_targets": {
+      "name": "webhook_targets",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar(100)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "provider_type": {
+          "name": "provider_type",
+          "type": "webhook_provider_type",
+          "typeSchema": "public",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "webhook_url": {
+          "name": "webhook_url",
+          "type": "varchar(1024)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "telegram_bot_token": {
+          "name": "telegram_bot_token",
+          "type": "varchar(256)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "telegram_chat_id": {
+          "name": "telegram_chat_id",
+          "type": "varchar(64)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "dingtalk_secret": {
+          "name": "dingtalk_secret",
+          "type": "varchar(256)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "custom_template": {
+          "name": "custom_template",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "custom_headers": {
+          "name": "custom_headers",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "proxy_url": {
+          "name": "proxy_url",
+          "type": "varchar(512)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "proxy_fallback_to_direct": {
+          "name": "proxy_fallback_to_direct",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": false
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "last_test_at": {
+          "name": "last_test_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "last_test_result": {
+          "name": "last_test_result",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    }
+  },
+  "enums": {
+    "public.daily_reset_mode": {
+      "name": "daily_reset_mode",
+      "schema": "public",
+      "values": [
+        "fixed",
+        "rolling"
+      ]
+    },
+    "public.notification_type": {
+      "name": "notification_type",
+      "schema": "public",
+      "values": [
+        "circuit_breaker",
+        "daily_leaderboard",
+        "cost_alert"
+      ]
+    },
+    "public.webhook_provider_type": {
+      "name": "webhook_provider_type",
+      "schema": "public",
+      "values": [
+        "wechat",
+        "feishu",
+        "dingtalk",
+        "telegram",
+        "custom"
+      ]
+    }
+  },
+  "schemas": {},
+  "sequences": {},
+  "roles": {},
+  "policies": {},
+  "views": {},
+  "_meta": {
+    "columns": {},
+    "schemas": {},
+    "tables": {}
+  }
+}

+ 7 - 0
drizzle/meta/_journal.json

@@ -498,6 +498,13 @@
       "when": 1771233193254,
       "tag": "0070_stormy_exiles",
       "breakpoints": true
+    },
+    {
+      "idx": 71,
+      "version": "7",
+      "when": 1771491614848,
+      "tag": "0071_purple_captain_midlands",
+      "breakpoints": true
     }
   ]
 }

+ 2 - 1
messages/en/dashboard.json

@@ -706,7 +706,8 @@
     "states": {
       "noData": "No statistics data available",
       "fetchFailed": "Failed to fetch statistics"
-    }
+    },
+    "othersAggregate": "Other Users"
   },
   "errors": {
     "fetchSystemSettingsFailed": "Failed to fetch system settings",

+ 2 - 1
messages/ja/dashboard.json

@@ -704,7 +704,8 @@
     "states": {
       "noData": "統計データなし",
       "fetchFailed": "統計データの取得に失敗しました"
-    }
+    },
+    "othersAggregate": "その他のユーザー"
   },
   "errors": {
     "fetchSystemSettingsFailed": "システム設定の取得に失敗しました",

+ 2 - 1
messages/ru/dashboard.json

@@ -707,7 +707,8 @@
     "states": {
       "noData": "Нет статистических данных",
       "fetchFailed": "Не удалось получить статистические данные"
-    }
+    },
+    "othersAggregate": "Другие пользователи"
   },
   "errors": {
     "fetchSystemSettingsFailed": "Не удалось получить параметры системы",

+ 2 - 1
messages/zh-CN/dashboard.json

@@ -706,7 +706,8 @@
     "states": {
       "noData": "暂无统计数据",
       "fetchFailed": "获取统计数据失败"
-    }
+    },
+    "othersAggregate": "其他用户"
   },
   "errors": {
     "fetchSystemSettingsFailed": "获取系统设置失败",

+ 4 - 3
messages/zh-TW/dashboard.json

@@ -280,7 +280,7 @@
         "cacheWrite1h": "快取寫入(1h)",
         "cacheRead": "快取讀取",
         "cacheTtl": "快取 TTL",
-        "cacheTtlSwapped": "計費 TTL (已互換)",
+        "cacheTtlSwapped": "計費 TTL(已互換)",
         "multiplier": "供應商倍率",
         "totalCost": "總費用",
         "context1m": "1M 上下文",
@@ -366,7 +366,7 @@
       "cacheWrite1h": "快取寫入(1h)",
       "cacheRead": "快取讀取",
       "cacheTtl": "快取 TTL",
-      "cacheTtlSwapped": "計費 TTL (已互換)",
+      "cacheTtlSwapped": "計費 TTL(已互換)",
       "multiplier": "供應商倍率",
       "totalCost": "總費用",
       "context1m": "1M 上下文長度",
@@ -704,7 +704,8 @@
     "states": {
       "noData": "暫無統計資料",
       "fetchFailed": "取得統計資料失敗"
-    }
+    },
+    "othersAggregate": "其他使用者"
   },
   "errors": {
     "fetchSystemSettingsFailed": "取得系統設定失敗",

+ 2 - 2
src/actions/overview.ts

@@ -2,7 +2,7 @@
 
 import { getSession } from "@/lib/auth";
 import { logger } from "@/lib/logger";
-import { getOverviewMetricsWithComparison } from "@/repository/overview";
+import { getOverviewWithCache } from "@/lib/redis";
 import { getSystemSettings } from "@/repository/system-config";
 import { getConcurrentSessions as getConcurrentSessionsCount } from "./concurrent-sessions";
 import type { ActionResult } from "./types";
@@ -59,7 +59,7 @@ export async function getOverviewData(): Promise<ActionResult<OverviewData>> {
     const [concurrentResult, metricsData] = await Promise.all([
       // 并发数只有管理员能看全站的
       isAdmin ? getConcurrentSessionsCount() : Promise.resolve({ ok: true as const, data: 0 }),
-      getOverviewMetricsWithComparison(userId),
+      getOverviewWithCache(userId),
     ]);
 
     const concurrentSessions = concurrentResult.ok ? concurrentResult.data : 0;

+ 16 - 16
src/actions/statistics.ts

@@ -2,14 +2,9 @@
 
 import { getSession } from "@/lib/auth";
 import { logger } from "@/lib/logger";
+import { getStatisticsWithCache } from "@/lib/redis";
 import { formatCostForStorage } from "@/lib/utils/currency";
-import {
-  getActiveKeysForUserFromDB,
-  getActiveUsersFromDB,
-  getKeyStatisticsFromDB,
-  getMixedStatisticsFromDB,
-  getUserStatisticsFromDB,
-} from "@/repository/statistics";
+import { getActiveKeysForUserFromDB, getActiveUsersFromDB } from "@/repository/statistics";
 import { getSystemSettings } from "@/repository/system-config";
 import type {
   ChartDataItem,
@@ -67,31 +62,36 @@ export async function getUserStatistics(
 
     if (mode === "users") {
       // Admin: 显示所有用户
-      const [userStats, userList] = await Promise.all([
-        getUserStatisticsFromDB(timeRange),
+      const [cachedData, userList] = await Promise.all([
+        getStatisticsWithCache(timeRange, "users"),
         getActiveUsersFromDB(),
       ]);
-      statsData = userStats;
+      statsData = cachedData as DatabaseStatRow[];
       entities = userList;
     } else if (mode === "mixed") {
       // 非 Admin + allowGlobalUsageView: 自己的密钥明细 + 其他用户汇总
-      const [ownKeysList, mixedData] = await Promise.all([
+      const [ownKeysList, cachedData] = await Promise.all([
         getActiveKeysForUserFromDB(session.user.id),
-        getMixedStatisticsFromDB(session.user.id, timeRange),
+        getStatisticsWithCache(timeRange, "mixed", session.user.id),
       ]);
 
+      const mixedData = cachedData as {
+        ownKeys: DatabaseKeyStatRow[];
+        othersAggregate: DatabaseStatRow[];
+      };
+
       // 合并数据:自己的密钥 + 其他用户的虚拟条目
       statsData = [...mixedData.ownKeys, ...mixedData.othersAggregate];
 
       // 合并实体列表:自己的密钥 + 其他用户虚拟实体
-      entities = [...ownKeysList, { id: -1, name: "其他用户" }];
+      entities = [...ownKeysList, { id: -1, name: "__others__" }];
     } else {
       // 非 Admin + !allowGlobalUsageView: 仅显示自己的密钥
-      const [keyStats, keyList] = await Promise.all([
-        getKeyStatisticsFromDB(session.user.id, timeRange),
+      const [cachedData, keyList] = await Promise.all([
+        getStatisticsWithCache(timeRange, "keys", session.user.id),
         getActiveKeysForUserFromDB(session.user.id),
       ]);
-      statsData = keyStats;
+      statsData = cachedData as DatabaseKeyStatRow[];
       entities = keyList;
     }
 

+ 17 - 4
src/app/[locale]/dashboard/_components/bento/dashboard-bento.tsx

@@ -1,7 +1,8 @@
 "use client";
 
-import { useQuery } from "@tanstack/react-query";
+import { keepPreviousData, useQuery } from "@tanstack/react-query";
 import { Activity, Clock, DollarSign, TrendingUp } from "lucide-react";
+import dynamic from "next/dynamic";
 import { useTranslations } from "next-intl";
 import { useMemo, useState } from "react";
 import { getActiveSessions } from "@/actions/active-sessions";
@@ -23,7 +24,11 @@ import { BentoGrid } from "./bento-grid";
 import { LeaderboardCard } from "./leaderboard-card";
 import { LiveSessionsPanel } from "./live-sessions-panel";
 import { BentoMetricCard } from "./metric-card";
-import { StatisticsChartCard } from "./statistics-chart-card";
+
+const StatisticsChartCard = dynamic(
+  () => import("./statistics-chart-card").then((mod) => ({ default: mod.StatisticsChartCard })),
+  { ssr: false }
+);
 
 const REFRESH_INTERVAL = 5000;
 
@@ -32,6 +37,7 @@ interface DashboardBentoProps {
   currencyCode: CurrencyCode;
   allowGlobalUsageView: boolean;
   initialStatistics?: UserStatisticsData;
+  initialOverview?: OverviewData;
 }
 
 interface LeaderboardData {
@@ -62,7 +68,6 @@ async function fetchStatistics(timeRange: TimeRange): Promise<UserStatisticsData
 
 async function fetchLeaderboard(scope: "user" | "provider" | "model"): Promise<LeaderboardData[]> {
   const res = await fetch(`/api/leaderboard?period=daily&scope=${scope}`, {
-    cache: "no-store",
     credentials: "include",
   });
   if (!res.ok) throw new Error("Failed to fetch leaderboard");
@@ -110,6 +115,7 @@ export function DashboardBento({
   currencyCode,
   allowGlobalUsageView,
   initialStatistics,
+  initialOverview,
 }: DashboardBentoProps) {
   const t = useTranslations("customs");
   const tl = useTranslations("dashboard.leaderboard");
@@ -120,7 +126,9 @@ export function DashboardBento({
   const { data: overview } = useQuery<OverviewData>({
     queryKey: ["overview-data"],
     queryFn: fetchOverviewData,
-    refetchInterval: REFRESH_INTERVAL,
+    refetchInterval: 15_000,
+    staleTime: 10_000,
+    initialData: initialOverview,
   });
 
   // Active sessions
@@ -136,6 +144,8 @@ export function DashboardBento({
     queryKey: ["statistics", timeRange],
     queryFn: () => fetchStatistics(timeRange),
     initialData: timeRange === DEFAULT_TIME_RANGE ? initialStatistics : undefined,
+    staleTime: 30_000,
+    placeholderData: keepPreviousData,
   });
 
   // Leaderboards
@@ -145,6 +155,7 @@ export function DashboardBento({
     queryKey: ["leaderboard", "user"],
     queryFn: () => fetchLeaderboard("user"),
     enabled: isAdmin || allowGlobalUsageView,
+    staleTime: 60_000,
   });
 
   const { data: providerLeaderboard = [], isLoading: providerLeaderboardLoading } = useQuery<
@@ -153,6 +164,7 @@ export function DashboardBento({
     queryKey: ["leaderboard", "provider"],
     queryFn: () => fetchLeaderboard("provider"),
     enabled: isAdmin || allowGlobalUsageView,
+    staleTime: 60_000,
   });
 
   const { data: modelLeaderboard = [], isLoading: modelLeaderboardLoading } = useQuery<
@@ -161,6 +173,7 @@ export function DashboardBento({
     queryKey: ["leaderboard", "model"],
     queryFn: () => fetchLeaderboard("model"),
     enabled: isAdmin || allowGlobalUsageView,
+    staleTime: 60_000,
   });
 
   const metrics = overview || {

+ 9 - 3
src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx

@@ -79,7 +79,7 @@ export function StatisticsChartCard({
     };
     data.users.forEach((user, index) => {
       config[user.dataKey] = {
-        label: user.name,
+        label: user.name === "__others__" ? t("othersAggregate") : user.name,
         color: getUserColor(index),
       };
     });
@@ -337,7 +337,11 @@ export function StatisticsChartCard({
                                   className="h-2 w-2 rounded-full flex-shrink-0"
                                   style={{ backgroundColor: entry.color }}
                                 />
-                                <span className="truncate">{displayUser?.name || baseKey}</span>
+                                <span className="truncate">
+                                  {displayUser?.name === "__others__"
+                                    ? t("othersAggregate")
+                                    : displayUser?.name || baseKey}
+                                </span>
                               </div>
                               <span className="font-mono font-medium">
                                 {activeChart === "cost"
@@ -444,7 +448,9 @@ export function StatisticsChartCard({
                         className="h-2 w-2 rounded-full flex-shrink-0"
                         style={{ backgroundColor: color }}
                       />
-                      <span className="font-medium truncate max-w-[80px]">{user.name}</span>
+                      <span className="font-medium truncate max-w-[80px]">
+                        {user.name === "__others__" ? t("othersAggregate") : user.name}
+                      </span>
                       <span className="text-muted-foreground">
                         {activeChart === "cost"
                           ? formatCurrency(userTotal?.cost ?? 0, currencyCode)

+ 4 - 1
src/app/[locale]/dashboard/_components/dashboard-bento-sections.tsx

@@ -1,4 +1,5 @@
 import { cache } from "react";
+import { getOverviewData } from "@/actions/overview";
 import { getUserStatistics } from "@/actions/statistics";
 import { getSystemSettings } from "@/repository/system-config";
 import { DEFAULT_TIME_RANGE } from "@/types/statistics";
@@ -11,9 +12,10 @@ interface DashboardBentoSectionProps {
 }
 
 export async function DashboardBentoSection({ isAdmin }: DashboardBentoSectionProps) {
-  const [systemSettings, statistics] = await Promise.all([
+  const [systemSettings, statistics, overviewResult] = await Promise.all([
     getCachedSystemSettings(),
     getUserStatistics(DEFAULT_TIME_RANGE),
+    getOverviewData(),
   ]);
 
   return (
@@ -22,6 +24,7 @@ export async function DashboardBentoSection({ isAdmin }: DashboardBentoSectionPr
       currencyCode={systemSettings.currencyDisplay}
       allowGlobalUsageView={systemSettings.allowGlobalUsageView}
       initialStatistics={statistics.ok ? statistics.data : undefined}
+      initialOverview={overviewResult.ok ? overviewResult.data : undefined}
     />
   );
 }

+ 6 - 3
src/app/[locale]/dashboard/_components/statistics/chart.tsx

@@ -117,7 +117,7 @@ export function UserStatisticsChart({
 
     data.users.forEach((user, index) => {
       config[user.dataKey] = {
-        label: user.name,
+        label: user.name === "__others__" ? t("othersAggregate") : user.name,
         color: getUserColor(index),
       };
     });
@@ -466,7 +466,10 @@ export function UserStatisticsChart({
                                     style={{ backgroundColor: color }}
                                   />
                                   <span className="font-medium truncate">
-                                    {displayUser?.name || baseKey}:
+                                    {displayUser?.name === "__others__"
+                                      ? t("othersAggregate")
+                                      : displayUser?.name || baseKey}
+                                    :
                                   </span>
                                 </div>
                                 <span className="ml-auto font-mono flex-shrink-0">
@@ -566,7 +569,7 @@ export function UserStatisticsChart({
                               aria-hidden="true"
                             />
                             <span className="text-xs font-medium text-foreground truncate max-w-12">
-                              {user.name}
+                              {user.name === "__others__" ? t("othersAggregate") : user.name}
                             </span>
                           </div>
 

+ 6 - 0
src/drizzle/schema.ts

@@ -483,8 +483,14 @@ export const messageRequest = pgTable('message_request', {
 }, (table) => ({
   // 优化统计查询的复合索引(用户+时间+费用)
   messageRequestUserDateCostIdx: index('idx_message_request_user_date_cost').on(table.userId, table.createdAt, table.costUsd).where(sql`${table.deletedAt} IS NULL`),
+  messageRequestUserCreatedAtCostStatsIdx: index('idx_message_request_user_created_at_cost_stats')
+    .on(table.userId, table.createdAt, table.costUsd)
+    .where(sql`${table.deletedAt} IS NULL AND (${table.blockedBy} IS NULL OR ${table.blockedBy} <> 'warmup')`),
   // 优化用户查询的复合索引(按创建时间倒序)
   messageRequestUserQueryIdx: index('idx_message_request_user_query').on(table.userId, table.createdAt).where(sql`${table.deletedAt} IS NULL`),
+  messageRequestProviderCreatedAtActiveIdx: index('idx_message_request_provider_created_at_active')
+    .on(table.providerId, table.createdAt)
+    .where(sql`${table.deletedAt} IS NULL AND (${table.blockedBy} IS NULL OR ${table.blockedBy} <> 'warmup')`),
   // Session 查询索引(按 session 聚合查看对话)
   messageRequestSessionIdIdx: index('idx_message_request_session_id').on(table.sessionId).where(sql`${table.deletedAt} IS NULL`),
   // Session ID 前缀查询索引(LIKE 'prefix%',可稳定命中 B-tree)

+ 2 - 0
src/lib/redis/index.ts

@@ -2,5 +2,7 @@ import "server-only";
 
 export { closeRedis, getRedisClient } from "./client";
 export { getLeaderboardWithCache, invalidateLeaderboardCache } from "./leaderboard-cache";
+export { getOverviewWithCache, invalidateOverviewCache } from "./overview-cache";
 export { scanPattern } from "./scan-helper";
 export { getActiveConcurrentSessions } from "./session-stats";
+export { getStatisticsWithCache, invalidateStatisticsCache } from "./statistics-cache";

+ 94 - 0
src/lib/redis/overview-cache.ts

@@ -0,0 +1,94 @@
+import { logger } from "@/lib/logger";
+import {
+  getOverviewMetricsWithComparison,
+  type OverviewMetricsWithComparison,
+} from "@/repository/overview";
+import { getRedisClient } from "./client";
+
+const CACHE_TTL = 10;
+const LOCK_TTL = 5;
+const LOCK_WAIT_MS = 100;
+
+function buildCacheKey(userId?: number): string {
+  return userId !== undefined ? `overview:user:${userId}` : "overview:global";
+}
+
+/**
+ * Get overview metrics with Redis caching (10s TTL).
+ * Fail-open: Redis unavailable -> direct DB query.
+ * Thundering herd protection via lock key.
+ */
+export async function getOverviewWithCache(
+  userId?: number
+): Promise<OverviewMetricsWithComparison> {
+  const redis = getRedisClient();
+  const cacheKey = buildCacheKey(userId);
+  const lockKey = `${cacheKey}:lock`;
+
+  if (!redis) {
+    return await getOverviewMetricsWithComparison(userId);
+  }
+
+  let lockAcquired = false;
+  let data: OverviewMetricsWithComparison | undefined;
+
+  try {
+    // 1. Try cache hit
+    const cached = await redis.get(cacheKey);
+    if (cached) {
+      return JSON.parse(cached) as OverviewMetricsWithComparison;
+    }
+
+    // 2. Acquire lock (prevent thundering herd)
+    const lockResult = await redis.set(lockKey, "1", "EX", LOCK_TTL, "NX");
+    lockAcquired = lockResult === "OK";
+
+    if (!lockAcquired) {
+      // Another instance is computing -- wait briefly and retry cache
+      await new Promise((resolve) => setTimeout(resolve, LOCK_WAIT_MS));
+      const retried = await redis.get(cacheKey);
+      if (retried) return JSON.parse(retried) as OverviewMetricsWithComparison;
+      // Still nothing -- fallback to direct query
+      return await getOverviewMetricsWithComparison(userId);
+    }
+
+    // 3. Cache miss -- query DB
+    data = await getOverviewMetricsWithComparison(userId);
+
+    // 4. Store in cache with TTL (best-effort)
+    try {
+      await redis.setex(cacheKey, CACHE_TTL, JSON.stringify(data));
+    } catch (writeErr) {
+      logger.warn("[OverviewCache] Failed to write cache", { cacheKey, error: writeErr });
+    }
+
+    return data;
+  } catch (error) {
+    logger.warn("[OverviewCache] Redis error, fallback to direct query", { userId, error });
+    return data ?? (await getOverviewMetricsWithComparison(userId));
+  } finally {
+    if (lockAcquired) {
+      await redis
+        .del(lockKey)
+        .catch((err) =>
+          logger.warn("[OverviewCache] Failed to release lock", { lockKey, error: err })
+        );
+    }
+  }
+}
+
+/**
+ * Invalidate overview cache for a specific user or global scope.
+ */
+export async function invalidateOverviewCache(userId?: number): Promise<void> {
+  const redis = getRedisClient();
+  if (!redis) return;
+
+  const cacheKey = buildCacheKey(userId);
+  try {
+    await redis.del(cacheKey);
+    logger.info("[OverviewCache] Cache invalidated", { userId, cacheKey });
+  } catch (error) {
+    logger.error("[OverviewCache] Failed to invalidate cache", { userId, error });
+  }
+}

+ 185 - 0
src/lib/redis/statistics-cache.ts

@@ -0,0 +1,185 @@
+import { logger } from "@/lib/logger";
+import {
+  getKeyStatisticsFromDB,
+  getMixedStatisticsFromDB,
+  getUserStatisticsFromDB,
+} from "@/repository/statistics";
+import { buildStatisticsCacheKey } from "@/types/dashboard-cache";
+import type { DatabaseKeyStatRow, DatabaseStatRow, TimeRange } from "@/types/statistics";
+import { getRedisClient } from "./client";
+import { scanPattern } from "./scan-helper";
+
+const CACHE_TTL = 30;
+const LOCK_TTL = 5;
+
+type MixedStatisticsResult = {
+  ownKeys: DatabaseKeyStatRow[];
+  othersAggregate: DatabaseStatRow[];
+};
+
+type StatisticsCacheData = DatabaseStatRow[] | DatabaseKeyStatRow[] | MixedStatisticsResult;
+
+function sleep(ms: number): Promise<void> {
+  return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+async function queryDatabase(
+  timeRange: TimeRange,
+  mode: "users" | "keys" | "mixed",
+  userId?: number
+): Promise<StatisticsCacheData> {
+  if ((mode === "keys" || mode === "mixed") && userId === undefined) {
+    throw new Error(`queryDatabase: userId required for mode="${mode}"`);
+  }
+  switch (mode) {
+    case "users":
+      return await getUserStatisticsFromDB(timeRange);
+    case "keys":
+      return await getKeyStatisticsFromDB(userId!, timeRange);
+    case "mixed":
+      return await getMixedStatisticsFromDB(userId!, timeRange);
+  }
+}
+
+/**
+ * Statistics data with Redis caching (30s TTL).
+ *
+ * Strategy:
+ * 1. Read from Redis cache first
+ * 2. On cache miss, acquire distributed lock to prevent thundering herd
+ * 3. Requests that fail to acquire lock wait and retry (up to 5s)
+ * 4. Fail-open: Redis unavailable -> direct DB query
+ */
+export async function getStatisticsWithCache(
+  timeRange: TimeRange,
+  mode: "users" | "keys" | "mixed",
+  userId?: number
+): Promise<StatisticsCacheData> {
+  const redis = getRedisClient();
+
+  if (!redis) {
+    logger.warn("[StatisticsCache] Redis not available, fallback to direct query", {
+      timeRange,
+      mode,
+      userId,
+    });
+    return await queryDatabase(timeRange, mode, userId);
+  }
+
+  const cacheKey = buildStatisticsCacheKey(timeRange, mode, userId);
+  const lockKey = `${cacheKey}:lock`;
+
+  let locked = false;
+  let data: StatisticsCacheData | undefined;
+
+  try {
+    // 1. Try cache
+    const cached = await redis.get(cacheKey);
+    if (cached) {
+      logger.debug("[StatisticsCache] Cache hit", { timeRange, mode, cacheKey });
+      return JSON.parse(cached) as StatisticsCacheData;
+    }
+
+    // 2. Cache miss - acquire lock (SET NX EX)
+    const lockResult = await redis.set(lockKey, "1", "EX", LOCK_TTL, "NX");
+    locked = lockResult === "OK";
+
+    if (locked) {
+      logger.debug("[StatisticsCache] Acquired lock, computing", { timeRange, mode, lockKey });
+
+      data = await queryDatabase(timeRange, mode, userId);
+
+      try {
+        await redis.setex(cacheKey, CACHE_TTL, JSON.stringify(data));
+      } catch (writeErr) {
+        logger.warn("[StatisticsCache] Failed to write cache", { cacheKey, error: writeErr });
+      }
+
+      logger.info("[StatisticsCache] Cache updated", {
+        timeRange,
+        mode,
+        userId,
+        cacheKey,
+        ttl: CACHE_TTL,
+      });
+
+      return data;
+    }
+
+    // 3. Lock held by another request - wait and retry (up to 50 x 100ms = 5s)
+    logger.debug("[StatisticsCache] Lock held by another request, retrying", { timeRange, mode });
+
+    for (let i = 0; i < 50; i++) {
+      await sleep(100);
+
+      const retried = await redis.get(cacheKey);
+      if (retried) {
+        logger.debug("[StatisticsCache] Cache hit after retry", {
+          timeRange,
+          mode,
+          retries: i + 1,
+        });
+        return JSON.parse(retried) as StatisticsCacheData;
+      }
+    }
+
+    // Retry timeout - fallback to direct DB
+    logger.warn("[StatisticsCache] Retry timeout, fallback to direct query", { timeRange, mode });
+    return await queryDatabase(timeRange, mode, userId);
+  } catch (error) {
+    logger.error("[StatisticsCache] Redis error, fallback to direct query", {
+      timeRange,
+      mode,
+      error,
+    });
+    return data ?? (await queryDatabase(timeRange, mode, userId));
+  } finally {
+    if (locked) {
+      await redis
+        .del(lockKey)
+        .catch((err) =>
+          logger.warn("[StatisticsCache] Failed to release lock", { lockKey, error: err })
+        );
+    }
+  }
+}
+
+/**
+ * Invalidate statistics cache.
+ *
+ * - If timeRange provided: delete specific cache key
+ * - If timeRange undefined: delete all time ranges for the scope using pattern match
+ */
+export async function invalidateStatisticsCache(
+  timeRange?: TimeRange,
+  userId?: number
+): Promise<void> {
+  const redis = getRedisClient();
+  if (!redis) {
+    return;
+  }
+
+  const scope = userId !== undefined ? `${userId}` : "global";
+
+  try {
+    if (timeRange) {
+      const modes = ["users", "keys", "mixed"] as const;
+      const keysToDelete = modes.map((m) => buildStatisticsCacheKey(timeRange, m, userId));
+      await redis.del(...keysToDelete);
+      logger.info("[StatisticsCache] Cache invalidated", { timeRange, scope, keysToDelete });
+    } else {
+      const pattern = `statistics:*:*:${scope}`;
+      const matchedKeys = await scanPattern(redis, pattern);
+      if (matchedKeys.length > 0) {
+        await redis.del(...matchedKeys);
+      }
+      logger.info("[StatisticsCache] Cache invalidated (all timeRanges)", {
+        scope,
+        pattern,
+        deletedCount: matchedKeys.length,
+      });
+    }
+  } catch (error) {
+    logger.error("[StatisticsCache] Failed to invalidate cache", { timeRange, scope, error });
+  }
+}

+ 301 - 634
src/repository/statistics.ts

@@ -42,173 +42,257 @@ async function getKeyStringByIdCached(keyId: number): Promise<string | null> {
   return keyString;
 }
 
-/**
- * 根据时间范围获取用户消费和API调用统计
- * 注意:这个函数使用原生SQL,因为涉及到PostgreSQL特定的generate_series函数
- */
-export async function getUserStatisticsFromDB(timeRange: TimeRange): Promise<DatabaseStatRow[]> {
-  const timezone = await resolveSystemTimezone();
-  let query;
-
+type SqlTimeRangeConfig = {
+  startTs: ReturnType<typeof sql>;
+  endTs: ReturnType<typeof sql>;
+  bucketExpr: ReturnType<typeof sql>;
+  bucketSeriesQuery: ReturnType<typeof sql>;
+};
+
+type TimeBucketValue = Date | string | null;
+
+type UserBucketStatsRow = {
+  user_id: number;
+  user_name: string;
+  bucket: TimeBucketValue;
+  api_calls: number | string | null;
+  total_cost: string | number | null;
+};
+
+type KeyBucketStatsRow = {
+  key_id: number;
+  key_name: string;
+  bucket: TimeBucketValue;
+  api_calls: number | string | null;
+  total_cost: string | number | null;
+};
+
+type MixedOthersBucketStatsRow = {
+  bucket: TimeBucketValue;
+  api_calls: number | string | null;
+  total_cost: string | number | null;
+};
+
+type RuntimeDatabaseStatRow = Omit<DatabaseStatRow, "date"> & { date: Date };
+type RuntimeDatabaseKeyStatRow = Omit<DatabaseKeyStatRow, "date"> & { date: Date };
+
+function getTimeRangeSqlConfig(timeRange: TimeRange, timezone: string): SqlTimeRangeConfig {
   switch (timeRange) {
     case "today":
-      // 今天(小时分辨率)
-      query = sql`
-        WITH hour_range AS (
+      return {
+        startTs: sql`(DATE_TRUNC('day', CURRENT_TIMESTAMP AT TIME ZONE ${timezone}) AT TIME ZONE ${timezone})`,
+        endTs: sql`((DATE_TRUNC('day', CURRENT_TIMESTAMP AT TIME ZONE ${timezone}) + INTERVAL '1 day') AT TIME ZONE ${timezone})`,
+        bucketExpr: sql`DATE_TRUNC('hour', message_request.created_at AT TIME ZONE ${timezone})`,
+        bucketSeriesQuery: sql`
           SELECT generate_series(
-            DATE_TRUNC('day', TIMEZONE(${timezone}, NOW())),
-            DATE_TRUNC('day', TIMEZONE(${timezone}, NOW())) + INTERVAL '23 hours',
+            DATE_TRUNC('day', CURRENT_TIMESTAMP AT TIME ZONE ${timezone}),
+            DATE_TRUNC('day', CURRENT_TIMESTAMP AT TIME ZONE ${timezone}) + INTERVAL '23 hours',
             '1 hour'::interval
-          ) AS hour
-        ),
-        hourly_stats AS (
-          SELECT
-            u.id AS user_id,
-            u.name AS user_name,
-            hr.hour,
-            COUNT(mr.id) AS api_calls,
-            COALESCE(SUM(mr.cost_usd), 0) AS total_cost
-          FROM users u
-          CROSS JOIN hour_range hr
-          LEFT JOIN message_request mr ON u.id = mr.user_id
-            AND mr.created_at >= (DATE_TRUNC('day', TIMEZONE(${timezone}, NOW())) AT TIME ZONE ${timezone})
-            AND mr.created_at < ((DATE_TRUNC('day', TIMEZONE(${timezone}, NOW())) + INTERVAL '1 day') AT TIME ZONE ${timezone})
-            AND DATE_TRUNC('hour', mr.created_at AT TIME ZONE ${timezone}) = hr.hour
-            AND mr.deleted_at IS NULL AND (mr.blocked_by IS NULL OR mr.blocked_by <> 'warmup')
-          WHERE u.deleted_at IS NULL
-          GROUP BY u.id, u.name, hr.hour
-        )
-        SELECT
-          user_id,
-          user_name,
-          hour AS date,
-          api_calls::integer,
-          total_cost::numeric
-        FROM hourly_stats
-        ORDER BY hour ASC, user_name ASC
-      `;
-      break;
-
+          ) AS bucket
+        `,
+      };
     case "7days":
-      // 过去7天(天分辨率)
-      query = sql`
-        WITH date_range AS (
+      return {
+        startTs: sql`((DATE_TRUNC('day', CURRENT_TIMESTAMP AT TIME ZONE ${timezone}) - INTERVAL '6 days') AT TIME ZONE ${timezone})`,
+        endTs: sql`((DATE_TRUNC('day', CURRENT_TIMESTAMP AT TIME ZONE ${timezone}) + INTERVAL '1 day') AT TIME ZONE ${timezone})`,
+        bucketExpr: sql`DATE_TRUNC('day', message_request.created_at AT TIME ZONE ${timezone})`,
+        bucketSeriesQuery: sql`
           SELECT generate_series(
             (CURRENT_TIMESTAMP AT TIME ZONE ${timezone})::date - INTERVAL '6 days',
             (CURRENT_TIMESTAMP AT TIME ZONE ${timezone})::date,
             '1 day'::interval
-          )::date AS date
-        ),
-        daily_stats AS (
-          SELECT
-            u.id AS user_id,
-            u.name AS user_name,
-            dr.date,
-            COUNT(mr.id) AS api_calls,
-            COALESCE(SUM(mr.cost_usd), 0) AS total_cost
-          FROM users u
-          CROSS JOIN date_range dr
-          LEFT JOIN message_request mr ON u.id = mr.user_id
-            AND mr.created_at >= ((DATE_TRUNC('day', CURRENT_TIMESTAMP AT TIME ZONE ${timezone}) - INTERVAL '6 days') AT TIME ZONE ${timezone})
-            AND mr.created_at < ((DATE_TRUNC('day', CURRENT_TIMESTAMP AT TIME ZONE ${timezone}) + INTERVAL '1 day') AT TIME ZONE ${timezone})
-            AND (mr.created_at AT TIME ZONE ${timezone})::date = dr.date
-            AND mr.deleted_at IS NULL AND (mr.blocked_by IS NULL OR mr.blocked_by <> 'warmup')
-          WHERE u.deleted_at IS NULL
-          GROUP BY u.id, u.name, dr.date
-        )
-        SELECT
-          user_id,
-          user_name,
-          date,
-          api_calls::integer,
-          total_cost::numeric
-        FROM daily_stats
-        ORDER BY date ASC, user_name ASC
-      `;
-      break;
-
+          ) AS bucket
+        `,
+      };
     case "30days":
-      // 过去 30 天(天分辨率)
-      query = sql`
-        WITH date_range AS (
+      return {
+        startTs: sql`((DATE_TRUNC('day', CURRENT_TIMESTAMP AT TIME ZONE ${timezone}) - INTERVAL '29 days') AT TIME ZONE ${timezone})`,
+        endTs: sql`((DATE_TRUNC('day', CURRENT_TIMESTAMP AT TIME ZONE ${timezone}) + INTERVAL '1 day') AT TIME ZONE ${timezone})`,
+        bucketExpr: sql`DATE_TRUNC('day', message_request.created_at AT TIME ZONE ${timezone})`,
+        bucketSeriesQuery: sql`
           SELECT generate_series(
             (CURRENT_TIMESTAMP AT TIME ZONE ${timezone})::date - INTERVAL '29 days',
             (CURRENT_TIMESTAMP AT TIME ZONE ${timezone})::date,
             '1 day'::interval
-          )::date AS date
-        ),
-        daily_stats AS (
-          SELECT
-            u.id AS user_id,
-            u.name AS user_name,
-            dr.date,
-            COUNT(mr.id) AS api_calls,
-            COALESCE(SUM(mr.cost_usd), 0) AS total_cost
-          FROM users u
-          CROSS JOIN date_range dr
-          LEFT JOIN message_request mr ON u.id = mr.user_id
-            AND mr.created_at >= ((DATE_TRUNC('day', CURRENT_TIMESTAMP AT TIME ZONE ${timezone}) - INTERVAL '29 days') AT TIME ZONE ${timezone})
-            AND mr.created_at < ((DATE_TRUNC('day', CURRENT_TIMESTAMP AT TIME ZONE ${timezone}) + INTERVAL '1 day') AT TIME ZONE ${timezone})
-            AND (mr.created_at AT TIME ZONE ${timezone})::date = dr.date
-            AND mr.deleted_at IS NULL AND (mr.blocked_by IS NULL OR mr.blocked_by <> 'warmup')
-          WHERE u.deleted_at IS NULL
-          GROUP BY u.id, u.name, dr.date
-        )
-        SELECT
-          user_id,
-          user_name,
-          date,
-          api_calls::integer,
-          total_cost::numeric
-        FROM daily_stats
-        ORDER BY date ASC, user_name ASC
-      `;
-      break;
-
+          ) AS bucket
+        `,
+      };
     case "thisMonth":
-      // 本月(天分辨率,从本月第一天到今天)
-      query = sql`
-        WITH date_range AS (
+      return {
+        startTs: sql`((DATE_TRUNC('month', CURRENT_TIMESTAMP AT TIME ZONE ${timezone})) AT TIME ZONE ${timezone})`,
+        endTs: sql`((DATE_TRUNC('day', CURRENT_TIMESTAMP AT TIME ZONE ${timezone}) + INTERVAL '1 day') AT TIME ZONE ${timezone})`,
+        bucketExpr: sql`DATE_TRUNC('day', message_request.created_at AT TIME ZONE ${timezone})`,
+        bucketSeriesQuery: sql`
           SELECT generate_series(
             DATE_TRUNC('month', CURRENT_TIMESTAMP AT TIME ZONE ${timezone})::date,
             (CURRENT_TIMESTAMP AT TIME ZONE ${timezone})::date,
             '1 day'::interval
-          )::date AS date
-        ),
-        daily_stats AS (
-          SELECT
-            u.id AS user_id,
-            u.name AS user_name,
-            dr.date,
-            COUNT(mr.id) AS api_calls,
-            COALESCE(SUM(mr.cost_usd), 0) AS total_cost
-          FROM users u
-          CROSS JOIN date_range dr
-          LEFT JOIN message_request mr ON u.id = mr.user_id
-            AND mr.created_at >= ((DATE_TRUNC('month', CURRENT_TIMESTAMP AT TIME ZONE ${timezone})) AT TIME ZONE ${timezone})
-            AND mr.created_at < ((DATE_TRUNC('day', CURRENT_TIMESTAMP AT TIME ZONE ${timezone}) + INTERVAL '1 day') AT TIME ZONE ${timezone})
-            AND (mr.created_at AT TIME ZONE ${timezone})::date = dr.date
-            AND mr.deleted_at IS NULL AND (mr.blocked_by IS NULL OR mr.blocked_by <> 'warmup')
-          WHERE u.deleted_at IS NULL
-          GROUP BY u.id, u.name, dr.date
-        )
-        SELECT
-          user_id,
-          user_name,
-          date,
-          api_calls::integer,
-          total_cost::numeric
-        FROM daily_stats
-        ORDER BY date ASC, user_name ASC
-      `;
-      break;
-
+          ) AS bucket
+        `,
+      };
     default:
       throw new Error(`Unsupported time range: ${timeRange}`);
   }
+}
 
-  const result = await db.execute(query);
-  return Array.from(result) as unknown as DatabaseStatRow[];
+function normalizeBucketDate(value: TimeBucketValue): Date | null {
+  if (!value) return null;
+  const parsed = value instanceof Date ? new Date(value.getTime()) : new Date(value);
+  return Number.isNaN(parsed.getTime()) ? null : parsed;
+}
+
+function normalizeApiCalls(value: number | string | null): number {
+  const normalized = Number(value ?? 0);
+  return Number.isFinite(normalized) ? normalized : 0;
+}
+
+function normalizeTotalCost(value: string | number | null): string | number {
+  if (value === null || value === undefined) return 0;
+  if (typeof value === "number") return Number.isFinite(value) ? value : 0;
+  return value;
+}
+
+async function getTimeBuckets(timeRange: TimeRange, timezone: string): Promise<Date[]> {
+  const { bucketSeriesQuery } = getTimeRangeSqlConfig(timeRange, timezone);
+  const result = await db.execute(bucketSeriesQuery);
+  return (Array.from(result) as Array<{ bucket: TimeBucketValue }>)
+    .map((row) => normalizeBucketDate(row.bucket))
+    .filter((bucket): bucket is Date => bucket !== null)
+    .sort((a, b) => a.getTime() - b.getTime());
+}
+
+function zeroFillUserStats(
+  dbRows: UserBucketStatsRow[],
+  allUsers: Array<{ id: number; name: string }>,
+  buckets: Date[]
+): RuntimeDatabaseStatRow[] {
+  const rowMap = new Map<string, { api_calls: number; total_cost: string | number }>();
+  for (const row of dbRows) {
+    const bucket = normalizeBucketDate(row.bucket);
+    if (!bucket) continue;
+
+    rowMap.set(`${row.user_id}:${bucket.getTime()}`, {
+      api_calls: normalizeApiCalls(row.api_calls),
+      total_cost: normalizeTotalCost(row.total_cost),
+    });
+  }
+
+  const sortedUsers = [...allUsers].sort((a, b) => a.name.localeCompare(b.name));
+  const filledRows: RuntimeDatabaseStatRow[] = [];
+
+  for (const bucket of buckets) {
+    const bucketTime = bucket.getTime();
+    for (const user of sortedUsers) {
+      const row = rowMap.get(`${user.id}:${bucketTime}`);
+      filledRows.push({
+        user_id: user.id,
+        user_name: user.name,
+        date: new Date(bucketTime),
+        api_calls: row?.api_calls ?? 0,
+        total_cost: row?.total_cost ?? 0,
+      });
+    }
+  }
+
+  return filledRows;
+}
+
+function zeroFillKeyStats(
+  dbRows: KeyBucketStatsRow[],
+  allKeys: Array<{ id: number; name: string }>,
+  buckets: Date[]
+): RuntimeDatabaseKeyStatRow[] {
+  const rowMap = new Map<string, { api_calls: number; total_cost: string | number }>();
+  for (const row of dbRows) {
+    const bucket = normalizeBucketDate(row.bucket);
+    if (!bucket) continue;
+
+    rowMap.set(`${row.key_id}:${bucket.getTime()}`, {
+      api_calls: normalizeApiCalls(row.api_calls),
+      total_cost: normalizeTotalCost(row.total_cost),
+    });
+  }
+
+  const sortedKeys = [...allKeys].sort((a, b) => a.name.localeCompare(b.name));
+  const filledRows: RuntimeDatabaseKeyStatRow[] = [];
+
+  for (const bucket of buckets) {
+    const bucketTime = bucket.getTime();
+    for (const key of sortedKeys) {
+      const row = rowMap.get(`${key.id}:${bucketTime}`);
+      filledRows.push({
+        key_id: key.id,
+        key_name: key.name,
+        date: new Date(bucketTime),
+        api_calls: row?.api_calls ?? 0,
+        total_cost: row?.total_cost ?? 0,
+      });
+    }
+  }
+
+  return filledRows;
+}
+
+function zeroFillMixedOthersStats(
+  dbRows: MixedOthersBucketStatsRow[],
+  buckets: Date[]
+): RuntimeDatabaseStatRow[] {
+  const rowMap = new Map<number, { api_calls: number; total_cost: string | number }>();
+  for (const row of dbRows) {
+    const bucket = normalizeBucketDate(row.bucket);
+    if (!bucket) continue;
+
+    rowMap.set(bucket.getTime(), {
+      api_calls: normalizeApiCalls(row.api_calls),
+      total_cost: normalizeTotalCost(row.total_cost),
+    });
+  }
+
+  return buckets.map((bucket) => {
+    const row = rowMap.get(bucket.getTime());
+    return {
+      user_id: -1,
+      user_name: "__others__",
+      date: new Date(bucket.getTime()),
+      api_calls: row?.api_calls ?? 0,
+      total_cost: row?.total_cost ?? 0,
+    };
+  });
+}
+
+/**
+ * 根据时间范围获取用户消费和API调用统计
+ */
+export async function getUserStatisticsFromDB(timeRange: TimeRange): Promise<DatabaseStatRow[]> {
+  const timezone = await resolveSystemTimezone();
+  const { startTs, endTs, bucketExpr } = getTimeRangeSqlConfig(timeRange, timezone);
+
+  const statsQuery = sql`
+    SELECT
+      u.id AS user_id,
+      u.name AS user_name,
+      ${bucketExpr} AS bucket,
+      COUNT(message_request.id) AS api_calls,
+      COALESCE(SUM(message_request.cost_usd), 0) AS total_cost
+    FROM users u
+    LEFT JOIN message_request ON u.id = message_request.user_id
+      AND message_request.created_at >= ${startTs}
+      AND message_request.created_at < ${endTs}
+      AND message_request.deleted_at IS NULL
+      AND ${EXCLUDE_WARMUP_CONDITION}
+    WHERE u.deleted_at IS NULL
+    GROUP BY u.id, u.name, ${bucketExpr}
+    ORDER BY bucket ASC, u.name ASC
+  `;
+
+  const [users, buckets, statsResult] = await Promise.all([
+    getActiveUsersFromDB(),
+    getTimeBuckets(timeRange, timezone),
+    db.execute(statsQuery),
+  ]);
+
+  const rows = Array.from(statsResult) as UserBucketStatsRow[];
+  return zeroFillUserStats(rows, users, buckets) as unknown as DatabaseStatRow[];
 }
 
 /**
@@ -234,187 +318,36 @@ export async function getKeyStatisticsFromDB(
   timeRange: TimeRange
 ): Promise<DatabaseKeyStatRow[]> {
   const timezone = await resolveSystemTimezone();
-  let query;
+  const { startTs, endTs, bucketExpr } = getTimeRangeSqlConfig(timeRange, timezone);
 
-  switch (timeRange) {
-    case "today":
-      query = sql`
-        WITH hour_range AS (
-          SELECT generate_series(
-            DATE_TRUNC('day', TIMEZONE(${timezone}, NOW())),
-            DATE_TRUNC('day', TIMEZONE(${timezone}, NOW())) + INTERVAL '23 hours',
-            '1 hour'::interval
-          ) AS hour
-        ),
-        user_keys AS (
-          SELECT id, name, key
-          FROM keys
-          WHERE user_id = ${userId}
-            AND deleted_at IS NULL
-        ),
-        hourly_stats AS (
-          SELECT
-            k.id AS key_id,
-            k.name AS key_name,
-            hr.hour,
-            COUNT(mr.id) AS api_calls,
-            COALESCE(SUM(mr.cost_usd), 0) AS total_cost
-          FROM user_keys k
-          CROSS JOIN hour_range hr
-          LEFT JOIN message_request mr ON mr.key = k.key
-            AND mr.user_id = ${userId}
-            AND mr.created_at >= (DATE_TRUNC('day', TIMEZONE(${timezone}, NOW())) AT TIME ZONE ${timezone})
-            AND mr.created_at < ((DATE_TRUNC('day', TIMEZONE(${timezone}, NOW())) + INTERVAL '1 day') AT TIME ZONE ${timezone})
-            AND DATE_TRUNC('hour', mr.created_at AT TIME ZONE ${timezone}) = hr.hour
-            AND mr.deleted_at IS NULL AND (mr.blocked_by IS NULL OR mr.blocked_by <> 'warmup')
-          GROUP BY k.id, k.name, hr.hour
-        )
-        SELECT
-          key_id,
-          key_name,
-          hour AS date,
-          api_calls::integer,
-          total_cost::numeric
-        FROM hourly_stats
-        ORDER BY hour ASC, key_name ASC
-      `;
-      break;
-
-    case "7days":
-      query = sql`
-        WITH date_range AS (
-          SELECT generate_series(
-            (CURRENT_TIMESTAMP AT TIME ZONE ${timezone})::date - INTERVAL '6 days',
-            (CURRENT_TIMESTAMP AT TIME ZONE ${timezone})::date,
-            '1 day'::interval
-          )::date AS date
-        ),
-        user_keys AS (
-          SELECT id, name, key
-          FROM keys
-          WHERE user_id = ${userId}
-            AND deleted_at IS NULL
-        ),
-        daily_stats AS (
-          SELECT
-            k.id AS key_id,
-            k.name AS key_name,
-            dr.date,
-            COUNT(mr.id) AS api_calls,
-            COALESCE(SUM(mr.cost_usd), 0) AS total_cost
-          FROM user_keys k
-          CROSS JOIN date_range dr
-          LEFT JOIN message_request mr ON mr.key = k.key
-            AND mr.user_id = ${userId}
-            AND mr.created_at >= ((DATE_TRUNC('day', CURRENT_TIMESTAMP AT TIME ZONE ${timezone}) - INTERVAL '6 days') AT TIME ZONE ${timezone})
-            AND mr.created_at < ((DATE_TRUNC('day', CURRENT_TIMESTAMP AT TIME ZONE ${timezone}) + INTERVAL '1 day') AT TIME ZONE ${timezone})
-            AND (mr.created_at AT TIME ZONE ${timezone})::date = dr.date
-            AND mr.deleted_at IS NULL AND (mr.blocked_by IS NULL OR mr.blocked_by <> 'warmup')
-          GROUP BY k.id, k.name, dr.date
-        )
-        SELECT
-          key_id,
-          key_name,
-          date,
-          api_calls::integer,
-          total_cost::numeric
-        FROM daily_stats
-        ORDER BY date ASC, key_name ASC
-      `;
-      break;
-
-    case "30days":
-      query = sql`
-        WITH date_range AS (
-          SELECT generate_series(
-            (CURRENT_TIMESTAMP AT TIME ZONE ${timezone})::date - INTERVAL '29 days',
-            (CURRENT_TIMESTAMP AT TIME ZONE ${timezone})::date,
-            '1 day'::interval
-          )::date AS date
-        ),
-        user_keys AS (
-          SELECT id, name, key
-          FROM keys
-          WHERE user_id = ${userId}
-            AND deleted_at IS NULL
-        ),
-        daily_stats AS (
-          SELECT
-            k.id AS key_id,
-            k.name AS key_name,
-            dr.date,
-            COUNT(mr.id) AS api_calls,
-            COALESCE(SUM(mr.cost_usd), 0) AS total_cost
-          FROM user_keys k
-          CROSS JOIN date_range dr
-          LEFT JOIN message_request mr ON mr.key = k.key
-            AND mr.user_id = ${userId}
-            AND mr.created_at >= ((DATE_TRUNC('day', CURRENT_TIMESTAMP AT TIME ZONE ${timezone}) - INTERVAL '29 days') AT TIME ZONE ${timezone})
-            AND mr.created_at < ((DATE_TRUNC('day', CURRENT_TIMESTAMP AT TIME ZONE ${timezone}) + INTERVAL '1 day') AT TIME ZONE ${timezone})
-            AND (mr.created_at AT TIME ZONE ${timezone})::date = dr.date
-            AND mr.deleted_at IS NULL AND (mr.blocked_by IS NULL OR mr.blocked_by <> 'warmup')
-          GROUP BY k.id, k.name, dr.date
-        )
-        SELECT
-          key_id,
-          key_name,
-          date,
-          api_calls::integer,
-          total_cost::numeric
-        FROM daily_stats
-        ORDER BY date ASC, key_name ASC
-      `;
-      break;
-
-    case "thisMonth":
-      query = sql`
-        WITH date_range AS (
-          SELECT generate_series(
-            DATE_TRUNC('month', CURRENT_TIMESTAMP AT TIME ZONE ${timezone})::date,
-            (CURRENT_TIMESTAMP AT TIME ZONE ${timezone})::date,
-            '1 day'::interval
-          )::date AS date
-        ),
-        user_keys AS (
-          SELECT id, name, key
-          FROM keys
-          WHERE user_id = ${userId}
-            AND deleted_at IS NULL
-        ),
-        daily_stats AS (
-          SELECT
-            k.id AS key_id,
-            k.name AS key_name,
-            dr.date,
-            COUNT(mr.id) AS api_calls,
-            COALESCE(SUM(mr.cost_usd), 0) AS total_cost
-          FROM user_keys k
-          CROSS JOIN date_range dr
-          LEFT JOIN message_request mr ON mr.key = k.key
-            AND mr.user_id = ${userId}
-            AND mr.created_at >= ((DATE_TRUNC('month', CURRENT_TIMESTAMP AT TIME ZONE ${timezone})) AT TIME ZONE ${timezone})
-            AND mr.created_at < ((DATE_TRUNC('day', CURRENT_TIMESTAMP AT TIME ZONE ${timezone}) + INTERVAL '1 day') AT TIME ZONE ${timezone})
-            AND (mr.created_at AT TIME ZONE ${timezone})::date = dr.date
-            AND mr.deleted_at IS NULL AND (mr.blocked_by IS NULL OR mr.blocked_by <> 'warmup')
-          GROUP BY k.id, k.name, dr.date
-        )
-        SELECT
-          key_id,
-          key_name,
-          date,
-          api_calls::integer,
-          total_cost::numeric
-        FROM daily_stats
-        ORDER BY date ASC, key_name ASC
-      `;
-      break;
+  const statsQuery = sql`
+    SELECT
+      k.id AS key_id,
+      k.name AS key_name,
+      ${bucketExpr} AS bucket,
+      COUNT(message_request.id) AS api_calls,
+      COALESCE(SUM(message_request.cost_usd), 0) AS total_cost
+    FROM keys k
+    LEFT JOIN message_request ON message_request.key = k.key
+      AND message_request.user_id = ${userId}
+      AND message_request.created_at >= ${startTs}
+      AND message_request.created_at < ${endTs}
+      AND message_request.deleted_at IS NULL
+      AND ${EXCLUDE_WARMUP_CONDITION}
+    WHERE k.user_id = ${userId}
+      AND k.deleted_at IS NULL
+    GROUP BY k.id, k.name, ${bucketExpr}
+    ORDER BY bucket ASC, k.name ASC
+  `;
 
-    default:
-      throw new Error(`Unsupported time range: ${timeRange}`);
-  }
+  const [activeKeys, buckets, statsResult] = await Promise.all([
+    getActiveKeysForUserFromDB(userId),
+    getTimeBuckets(timeRange, timezone),
+    db.execute(statsQuery),
+  ]);
 
-  const result = await db.execute(query);
-  return Array.from(result) as unknown as DatabaseKeyStatRow[];
+  const rows = Array.from(statsResult) as KeyBucketStatsRow[];
+  return zeroFillKeyStats(rows, activeKeys, buckets) as unknown as DatabaseKeyStatRow[];
 }
 
 /**
@@ -445,326 +378,60 @@ export async function getMixedStatisticsFromDB(
   othersAggregate: DatabaseStatRow[];
 }> {
   const timezone = await resolveSystemTimezone();
-  let ownKeysQuery;
-  let othersQuery;
-
-  switch (timeRange) {
-    case "today":
-      // 自己的密钥明细(小时分辨率)
-      ownKeysQuery = sql`
-        WITH hour_range AS (
-          SELECT generate_series(
-            DATE_TRUNC('day', TIMEZONE(${timezone}, NOW())),
-            DATE_TRUNC('day', TIMEZONE(${timezone}, NOW())) + INTERVAL '23 hours',
-            '1 hour'::interval
-          ) AS hour
-        ),
-        user_keys AS (
-          SELECT id, name, key
-          FROM keys
-          WHERE user_id = ${userId}
-            AND deleted_at IS NULL
-        ),
-         hourly_stats AS (
-           SELECT
-             k.id AS key_id,
-             k.name AS key_name,
-             hr.hour,
-             COUNT(mr.id) AS api_calls,
-             COALESCE(SUM(mr.cost_usd), 0) AS total_cost
-           FROM user_keys k
-           CROSS JOIN hour_range hr
-           LEFT JOIN message_request mr ON mr.key = k.key
-             AND mr.user_id = ${userId}
-             AND mr.created_at >= (DATE_TRUNC('day', TIMEZONE(${timezone}, NOW())) AT TIME ZONE ${timezone})
-             AND mr.created_at < ((DATE_TRUNC('day', TIMEZONE(${timezone}, NOW())) + INTERVAL '1 day') AT TIME ZONE ${timezone})
-             AND DATE_TRUNC('hour', mr.created_at AT TIME ZONE ${timezone}) = hr.hour
-             AND mr.deleted_at IS NULL AND (mr.blocked_by IS NULL OR mr.blocked_by <> 'warmup')
-           GROUP BY k.id, k.name, hr.hour
-         )
-        SELECT
-          key_id,
-          key_name,
-          hour AS date,
-          api_calls::integer,
-          total_cost::numeric
-        FROM hourly_stats
-        ORDER BY hour ASC, key_name ASC
-      `;
-
-      // 其他用户汇总(小时分辨率)
-      othersQuery = sql`
-        WITH hour_range AS (
-          SELECT generate_series(
-            DATE_TRUNC('day', TIMEZONE(${timezone}, NOW())),
-            DATE_TRUNC('day', TIMEZONE(${timezone}, NOW())) + INTERVAL '23 hours',
-            '1 hour'::interval
-          ) AS hour
-        ),
-         hourly_stats AS (
-           SELECT
-             hr.hour,
-             COUNT(mr.id) AS api_calls,
-             COALESCE(SUM(mr.cost_usd), 0) AS total_cost
-           FROM hour_range hr
-            LEFT JOIN message_request mr ON DATE_TRUNC('hour', mr.created_at AT TIME ZONE ${timezone}) = hr.hour
-             AND mr.created_at >= (DATE_TRUNC('day', TIMEZONE(${timezone}, NOW())) AT TIME ZONE ${timezone})
-             AND mr.created_at < ((DATE_TRUNC('day', TIMEZONE(${timezone}, NOW())) + INTERVAL '1 day') AT TIME ZONE ${timezone})
-             AND mr.user_id != ${userId}
-             AND mr.deleted_at IS NULL AND (mr.blocked_by IS NULL OR mr.blocked_by <> 'warmup')
-           GROUP BY hr.hour
-         )
-        SELECT
-          -1 AS user_id,
-          '其他用户' AS user_name,
-          hour AS date,
-          api_calls::integer,
-          total_cost::numeric
-        FROM hourly_stats
-        ORDER BY hour ASC
-      `;
-      break;
-
-    case "7days":
-      // 自己的密钥明细(天分辨率)
-      ownKeysQuery = sql`
-        WITH date_range AS (
-          SELECT generate_series(
-            (CURRENT_TIMESTAMP AT TIME ZONE ${timezone})::date - INTERVAL '6 days',
-            (CURRENT_TIMESTAMP AT TIME ZONE ${timezone})::date,
-            '1 day'::interval
-          )::date AS date
-        ),
-        user_keys AS (
-          SELECT id, name, key
-          FROM keys
-          WHERE user_id = ${userId}
-            AND deleted_at IS NULL
-        ),
-         daily_stats AS (
-           SELECT
-             k.id AS key_id,
-             k.name AS key_name,
-             dr.date,
-             COUNT(mr.id) AS api_calls,
-             COALESCE(SUM(mr.cost_usd), 0) AS total_cost
-           FROM user_keys k
-           CROSS JOIN date_range dr
-           LEFT JOIN message_request mr ON mr.key = k.key
-             AND mr.user_id = ${userId}
-             AND mr.created_at >= ((DATE_TRUNC('day', CURRENT_TIMESTAMP AT TIME ZONE ${timezone}) - INTERVAL '6 days') AT TIME ZONE ${timezone})
-             AND mr.created_at < ((DATE_TRUNC('day', CURRENT_TIMESTAMP AT TIME ZONE ${timezone}) + INTERVAL '1 day') AT TIME ZONE ${timezone})
-             AND (mr.created_at AT TIME ZONE ${timezone})::date = dr.date
-             AND mr.deleted_at IS NULL AND (mr.blocked_by IS NULL OR mr.blocked_by <> 'warmup')
-           GROUP BY k.id, k.name, dr.date
-         )
-        SELECT
-          key_id,
-          key_name,
-          date,
-          api_calls::integer,
-          total_cost::numeric
-        FROM daily_stats
-        ORDER BY date ASC, key_name ASC
-      `;
-
-      // 其他用户汇总(天分辨率)
-      othersQuery = sql`
-        WITH date_range AS (
-          SELECT generate_series(
-            (CURRENT_TIMESTAMP AT TIME ZONE ${timezone})::date - INTERVAL '6 days',
-            (CURRENT_TIMESTAMP AT TIME ZONE ${timezone})::date,
-            '1 day'::interval
-          )::date AS date
-        ),
-         daily_stats AS (
-           SELECT
-             dr.date,
-             COUNT(mr.id) AS api_calls,
-             COALESCE(SUM(mr.cost_usd), 0) AS total_cost
-           FROM date_range dr
-            LEFT JOIN message_request mr ON (mr.created_at AT TIME ZONE ${timezone})::date = dr.date
-             AND mr.created_at >= ((DATE_TRUNC('day', CURRENT_TIMESTAMP AT TIME ZONE ${timezone}) - INTERVAL '6 days') AT TIME ZONE ${timezone})
-             AND mr.created_at < ((DATE_TRUNC('day', CURRENT_TIMESTAMP AT TIME ZONE ${timezone}) + INTERVAL '1 day') AT TIME ZONE ${timezone})
-             AND mr.user_id != ${userId}
-             AND mr.deleted_at IS NULL AND (mr.blocked_by IS NULL OR mr.blocked_by <> 'warmup')
-           GROUP BY dr.date
-         )
-        SELECT
-          -1 AS user_id,
-          '其他用户' AS user_name,
-          date,
-          api_calls::integer,
-          total_cost::numeric
-        FROM daily_stats
-        ORDER BY date ASC
-      `;
-      break;
-
-    case "30days":
-      // 自己的密钥明细(天分辨率)
-      ownKeysQuery = sql`
-        WITH date_range AS (
-          SELECT generate_series(
-            (CURRENT_TIMESTAMP AT TIME ZONE ${timezone})::date - INTERVAL '29 days',
-            (CURRENT_TIMESTAMP AT TIME ZONE ${timezone})::date,
-            '1 day'::interval
-          )::date AS date
-        ),
-        user_keys AS (
-          SELECT id, name, key
-          FROM keys
-          WHERE user_id = ${userId}
-            AND deleted_at IS NULL
-        ),
-         daily_stats AS (
-           SELECT
-             k.id AS key_id,
-             k.name AS key_name,
-             dr.date,
-             COUNT(mr.id) AS api_calls,
-             COALESCE(SUM(mr.cost_usd), 0) AS total_cost
-           FROM user_keys k
-           CROSS JOIN date_range dr
-           LEFT JOIN message_request mr ON mr.key = k.key
-             AND mr.user_id = ${userId}
-             AND mr.created_at >= ((DATE_TRUNC('day', CURRENT_TIMESTAMP AT TIME ZONE ${timezone}) - INTERVAL '29 days') AT TIME ZONE ${timezone})
-             AND mr.created_at < ((DATE_TRUNC('day', CURRENT_TIMESTAMP AT TIME ZONE ${timezone}) + INTERVAL '1 day') AT TIME ZONE ${timezone})
-             AND (mr.created_at AT TIME ZONE ${timezone})::date = dr.date
-             AND mr.deleted_at IS NULL AND (mr.blocked_by IS NULL OR mr.blocked_by <> 'warmup')
-           GROUP BY k.id, k.name, dr.date
-         )
-        SELECT
-          key_id,
-          key_name,
-          date,
-          api_calls::integer,
-          total_cost::numeric
-        FROM daily_stats
-        ORDER BY date ASC, key_name ASC
-      `;
-
-      // 其他用户汇总(天分辨率)
-      othersQuery = sql`
-        WITH date_range AS (
-          SELECT generate_series(
-            (CURRENT_TIMESTAMP AT TIME ZONE ${timezone})::date - INTERVAL '29 days',
-            (CURRENT_TIMESTAMP AT TIME ZONE ${timezone})::date,
-            '1 day'::interval
-          )::date AS date
-        ),
-         daily_stats AS (
-           SELECT
-             dr.date,
-             COUNT(mr.id) AS api_calls,
-             COALESCE(SUM(mr.cost_usd), 0) AS total_cost
-           FROM date_range dr
-            LEFT JOIN message_request mr ON (mr.created_at AT TIME ZONE ${timezone})::date = dr.date
-             AND mr.created_at >= ((DATE_TRUNC('day', CURRENT_TIMESTAMP AT TIME ZONE ${timezone}) - INTERVAL '29 days') AT TIME ZONE ${timezone})
-             AND mr.created_at < ((DATE_TRUNC('day', CURRENT_TIMESTAMP AT TIME ZONE ${timezone}) + INTERVAL '1 day') AT TIME ZONE ${timezone})
-             AND mr.user_id != ${userId}
-             AND mr.deleted_at IS NULL AND (mr.blocked_by IS NULL OR mr.blocked_by <> 'warmup')
-           GROUP BY dr.date
-         )
-        SELECT
-          -1 AS user_id,
-          '其他用户' AS user_name,
-          date,
-          api_calls::integer,
-          total_cost::numeric
-        FROM daily_stats
-        ORDER BY date ASC
-      `;
-      break;
+  const { startTs, endTs, bucketExpr } = getTimeRangeSqlConfig(timeRange, timezone);
 
-    case "thisMonth":
-      // 自己的密钥明细(天分辨率,本月)
-      ownKeysQuery = sql`
-        WITH date_range AS (
-          SELECT generate_series(
-            DATE_TRUNC('month', CURRENT_TIMESTAMP AT TIME ZONE ${timezone})::date,
-            (CURRENT_TIMESTAMP AT TIME ZONE ${timezone})::date,
-            '1 day'::interval
-          )::date AS date
-        ),
-        user_keys AS (
-          SELECT id, name, key
-          FROM keys
-          WHERE user_id = ${userId}
-            AND deleted_at IS NULL
-        ),
-         daily_stats AS (
-           SELECT
-             k.id AS key_id,
-             k.name AS key_name,
-             dr.date,
-             COUNT(mr.id) AS api_calls,
-             COALESCE(SUM(mr.cost_usd), 0) AS total_cost
-           FROM user_keys k
-           CROSS JOIN date_range dr
-           LEFT JOIN message_request mr ON mr.key = k.key
-             AND mr.user_id = ${userId}
-             AND mr.created_at >= ((DATE_TRUNC('month', CURRENT_TIMESTAMP AT TIME ZONE ${timezone})) AT TIME ZONE ${timezone})
-             AND mr.created_at < ((DATE_TRUNC('day', CURRENT_TIMESTAMP AT TIME ZONE ${timezone}) + INTERVAL '1 day') AT TIME ZONE ${timezone})
-             AND (mr.created_at AT TIME ZONE ${timezone})::date = dr.date
-             AND mr.deleted_at IS NULL AND (mr.blocked_by IS NULL OR mr.blocked_by <> 'warmup')
-           GROUP BY k.id, k.name, dr.date
-         )
-        SELECT
-          key_id,
-          key_name,
-          date,
-          api_calls::integer,
-          total_cost::numeric
-        FROM daily_stats
-        ORDER BY date ASC, key_name ASC
-      `;
-
-      // 其他用户汇总(天分辨率,本月)
-      othersQuery = sql`
-        WITH date_range AS (
-          SELECT generate_series(
-            DATE_TRUNC('month', CURRENT_TIMESTAMP AT TIME ZONE ${timezone})::date,
-            (CURRENT_TIMESTAMP AT TIME ZONE ${timezone})::date,
-            '1 day'::interval
-          )::date AS date
-        ),
-         daily_stats AS (
-           SELECT
-             dr.date,
-             COUNT(mr.id) AS api_calls,
-             COALESCE(SUM(mr.cost_usd), 0) AS total_cost
-           FROM date_range dr
-            LEFT JOIN message_request mr ON (mr.created_at AT TIME ZONE ${timezone})::date = dr.date
-             AND mr.created_at >= ((DATE_TRUNC('month', CURRENT_TIMESTAMP AT TIME ZONE ${timezone})) AT TIME ZONE ${timezone})
-             AND mr.created_at < ((DATE_TRUNC('day', CURRENT_TIMESTAMP AT TIME ZONE ${timezone}) + INTERVAL '1 day') AT TIME ZONE ${timezone})
-             AND mr.user_id != ${userId}
-             AND mr.deleted_at IS NULL AND (mr.blocked_by IS NULL OR mr.blocked_by <> 'warmup')
-           GROUP BY dr.date
-         )
-        SELECT
-          -1 AS user_id,
-          '其他用户' AS user_name,
-          date,
-          api_calls::integer,
-          total_cost::numeric
-        FROM daily_stats
-        ORDER BY date ASC
-      `;
-      break;
+  const ownKeysQuery = sql`
+    SELECT
+      k.id AS key_id,
+      k.name AS key_name,
+      ${bucketExpr} AS bucket,
+      COUNT(message_request.id) AS api_calls,
+      COALESCE(SUM(message_request.cost_usd), 0) AS total_cost
+    FROM keys k
+    LEFT JOIN message_request ON message_request.key = k.key
+      AND message_request.user_id = ${userId}
+      AND message_request.created_at >= ${startTs}
+      AND message_request.created_at < ${endTs}
+      AND message_request.deleted_at IS NULL
+      AND ${EXCLUDE_WARMUP_CONDITION}
+    WHERE k.user_id = ${userId}
+      AND k.deleted_at IS NULL
+    GROUP BY k.id, k.name, ${bucketExpr}
+    ORDER BY bucket ASC, k.name ASC
+  `;
 
-    default:
-      throw new Error(`Unsupported time range: ${timeRange}`);
-  }
+  const othersQuery = sql`
+    SELECT
+      ${bucketExpr} AS bucket,
+      COUNT(message_request.id) AS api_calls,
+      COALESCE(SUM(message_request.cost_usd), 0) AS total_cost
+    FROM message_request
+    WHERE message_request.user_id <> ${userId}
+      AND message_request.created_at >= ${startTs}
+      AND message_request.created_at < ${endTs}
+      AND message_request.deleted_at IS NULL
+      AND ${EXCLUDE_WARMUP_CONDITION}
+    GROUP BY ${bucketExpr}
+    ORDER BY bucket ASC
+  `;
 
-  const [ownKeysResult, othersResult] = await Promise.all([
+  const [activeKeys, buckets, ownKeysResult, othersResult] = await Promise.all([
+    getActiveKeysForUserFromDB(userId),
+    getTimeBuckets(timeRange, timezone),
     db.execute(ownKeysQuery),
     db.execute(othersQuery),
   ]);
 
   return {
-    ownKeys: Array.from(ownKeysResult) as unknown as DatabaseKeyStatRow[],
-    othersAggregate: Array.from(othersResult) as unknown as DatabaseStatRow[],
+    ownKeys: zeroFillKeyStats(
+      Array.from(ownKeysResult) as KeyBucketStatsRow[],
+      activeKeys,
+      buckets
+    ) as unknown as DatabaseKeyStatRow[],
+    othersAggregate: zeroFillMixedOthersStats(
+      Array.from(othersResult) as MixedOthersBucketStatsRow[],
+      buckets
+    ) as unknown as DatabaseStatRow[],
   };
 }
 

+ 26 - 0
src/types/dashboard-cache.ts

@@ -0,0 +1,26 @@
+import type { TimeRange } from "@/types/statistics";
+
+export type OverviewCacheKey = {
+  scope: "global" | "user";
+  userId?: number;
+};
+
+export type StatisticsCacheKey = {
+  timeRange: TimeRange;
+  mode: "users" | "keys" | "mixed";
+  userId?: number;
+};
+
+export function buildOverviewCacheKey(scope: "global"): string;
+export function buildOverviewCacheKey(scope: "user", userId: number): string;
+export function buildOverviewCacheKey(scope: "global" | "user", userId?: number): string {
+  return scope === "global" ? "overview:global" : `overview:user:${userId}`;
+}
+
+export function buildStatisticsCacheKey(
+  timeRange: TimeRange,
+  mode: "users" | "keys" | "mixed",
+  userId?: number
+): string {
+  return `statistics:${timeRange}:${mode}:${userId ?? "global"}`;
+}

+ 5 - 0
tests/unit/actions/provider-undo-delete.test.ts

@@ -1,5 +1,6 @@
 import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
 import { PROVIDER_BATCH_PATCH_ERROR_CODES } from "../../../src/lib/provider-batch-patch-error-codes";
+import { buildRedisMock, createRedisStore } from "./redis-mock-utils";
 
 const getSessionMock = vi.fn();
 const deleteProvidersBatchMock = vi.fn();
@@ -7,6 +8,7 @@ const restoreProvidersBatchMock = vi.fn();
 const publishCacheInvalidationMock = vi.fn();
 const clearProviderStateMock = vi.fn();
 const clearConfigCacheMock = vi.fn();
+const { store: redisStore, mocks: redisMocks } = createRedisStore();
 
 vi.mock("@/lib/auth", () => ({
   getSession: getSessionMock,
@@ -33,6 +35,8 @@ vi.mock("@/lib/circuit-breaker", () => ({
   getAllHealthStatusAsync: vi.fn(),
 }));
 
+vi.mock("@/lib/redis/client", () => buildRedisMock(redisMocks));
+
 vi.mock("@/lib/logger", () => ({
   logger: {
     trace: vi.fn(),
@@ -47,6 +51,7 @@ describe("Provider Delete Undo Actions", () => {
   beforeEach(() => {
     vi.clearAllMocks();
     vi.resetModules();
+    redisStore.clear();
     getSessionMock.mockResolvedValue({ user: { id: 1, role: "admin" } });
     deleteProvidersBatchMock.mockResolvedValue(2);
     restoreProvidersBatchMock.mockResolvedValue(2);

+ 5 - 0
tests/unit/actions/provider-undo-edit.test.ts

@@ -1,5 +1,6 @@
 import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
 import { PROVIDER_BATCH_PATCH_ERROR_CODES } from "../../../src/lib/provider-batch-patch-error-codes";
+import { buildRedisMock, createRedisStore } from "./redis-mock-utils";
 
 const getSessionMock = vi.fn();
 const findProviderByIdMock = vi.fn();
@@ -10,6 +11,7 @@ const clearProviderStateMock = vi.fn();
 const clearConfigCacheMock = vi.fn();
 const saveProviderCircuitConfigMock = vi.fn();
 const deleteProviderCircuitConfigMock = vi.fn();
+const { store: redisStore, mocks: redisMocks } = createRedisStore();
 
 vi.mock("@/lib/auth", () => ({
   getSession: getSessionMock,
@@ -43,6 +45,8 @@ vi.mock("@/lib/redis/circuit-breaker-config", () => ({
   deleteProviderCircuitConfig: deleteProviderCircuitConfigMock,
 }));
 
+vi.mock("@/lib/redis/client", () => buildRedisMock(redisMocks));
+
 vi.mock("@/lib/logger", () => ({
   logger: {
     trace: vi.fn(),
@@ -118,6 +122,7 @@ describe("Provider Single Edit Undo Actions", () => {
   beforeEach(() => {
     vi.clearAllMocks();
     vi.resetModules();
+    redisStore.clear();
     getSessionMock.mockResolvedValue({ user: { id: 1, role: "admin" } });
     findProviderByIdMock.mockResolvedValue(makeProvider(1, { name: "Before Name", key: "sk-old" }));
     updateProviderMock.mockResolvedValue(makeProvider(1, { name: "After Name", key: "sk-new" }));

+ 54 - 0
tests/unit/actions/providers-apply-engine.test.ts

@@ -5,6 +5,45 @@ const getSessionMock = vi.fn();
 const findAllProvidersFreshMock = vi.fn();
 const updateProvidersBatchMock = vi.fn();
 const publishCacheInvalidationMock = vi.fn();
+const redisStore = new Map<string, { value: string; expiresAt: number }>();
+
+function readRedisValue(key: string): string | null {
+  const entry = redisStore.get(key);
+  if (!entry) {
+    return null;
+  }
+
+  if (entry.expiresAt <= Date.now()) {
+    redisStore.delete(key);
+    return null;
+  }
+
+  return entry.value;
+}
+
+const redisSetexMock = vi.fn(async (key: string, ttlSeconds: number, value: string) => {
+  redisStore.set(key, {
+    value,
+    expiresAt: Date.now() + ttlSeconds * 1000,
+  });
+  return "OK";
+});
+
+const redisGetMock = vi.fn(async (key: string) => readRedisValue(key));
+
+const redisDelMock = vi.fn(async (key: string) => {
+  const existed = redisStore.delete(key);
+  return existed ? 1 : 0;
+});
+
+const redisEvalMock = vi.fn(async (_script: string, _numKeys: number, key: string) => {
+  const value = readRedisValue(key);
+  if (value === null) {
+    return null;
+  }
+  redisStore.delete(key);
+  return value;
+});
 
 vi.mock("@/lib/auth", () => ({
   getSession: getSessionMock,
@@ -20,6 +59,16 @@ vi.mock("@/lib/cache/provider-cache", () => ({
   publishProviderCacheInvalidation: publishCacheInvalidationMock,
 }));
 
+vi.mock("@/lib/redis/client", () => ({
+  getRedisClient: () => ({
+    status: "ready",
+    setex: redisSetexMock,
+    get: redisGetMock,
+    del: redisDelMock,
+    eval: redisEvalMock,
+  }),
+}));
+
 vi.mock("@/lib/circuit-breaker", () => ({
   clearProviderState: vi.fn(),
   clearConfigCache: vi.fn(),
@@ -102,6 +151,11 @@ describe("Apply Provider Batch Patch Engine", () => {
   beforeEach(() => {
     vi.clearAllMocks();
     vi.resetModules();
+    redisStore.clear();
+    redisSetexMock.mockClear();
+    redisGetMock.mockClear();
+    redisDelMock.mockClear();
+    redisEvalMock.mockClear();
     getSessionMock.mockResolvedValue({ user: { id: 1, role: "admin" } });
     findAllProvidersFreshMock.mockResolvedValue([]);
     updateProvidersBatchMock.mockResolvedValue(0);

+ 5 - 0
tests/unit/actions/providers-patch-actions-contract.test.ts

@@ -1,9 +1,11 @@
 import { beforeEach, describe, expect, it, vi } from "vitest";
 import { PROVIDER_BATCH_PATCH_ERROR_CODES } from "@/lib/provider-batch-patch-error-codes";
+import { buildRedisMock, createRedisStore } from "./redis-mock-utils";
 
 const getSessionMock = vi.fn();
 const findAllProvidersFreshMock = vi.fn();
 const updateProvidersBatchMock = vi.fn();
+const { store: redisStore, mocks: redisMocks } = createRedisStore();
 
 vi.mock("@/lib/auth", () => ({
   getSession: getSessionMock,
@@ -19,6 +21,8 @@ vi.mock("@/lib/cache/provider-cache", () => ({
   publishProviderCacheInvalidation: vi.fn(),
 }));
 
+vi.mock("@/lib/redis/client", () => buildRedisMock(redisMocks));
+
 vi.mock("@/lib/circuit-breaker", () => ({
   clearProviderState: vi.fn(),
   clearConfigCache: vi.fn(),
@@ -100,6 +104,7 @@ describe("Provider Batch Patch Action Contracts", () => {
   beforeEach(() => {
     vi.clearAllMocks();
     vi.resetModules();
+    redisStore.clear();
     getSessionMock.mockResolvedValue({ user: { id: 1, role: "admin" } });
     findAllProvidersFreshMock.mockResolvedValue([]);
     updateProvidersBatchMock.mockResolvedValue(0);

+ 5 - 0
tests/unit/actions/providers-undo-engine.test.ts

@@ -1,11 +1,13 @@
 // @vitest-environment node
 import { beforeEach, describe, expect, it, vi } from "vitest";
 import { PROVIDER_BATCH_PATCH_ERROR_CODES } from "@/lib/provider-batch-patch-error-codes";
+import { buildRedisMock, createRedisStore } from "./redis-mock-utils";
 
 const getSessionMock = vi.fn();
 const findAllProvidersFreshMock = vi.fn();
 const updateProvidersBatchMock = vi.fn();
 const publishCacheInvalidationMock = vi.fn();
+const { store: redisStore, mocks: redisMocks } = createRedisStore();
 
 vi.mock("@/lib/auth", () => ({
   getSession: getSessionMock,
@@ -21,6 +23,8 @@ vi.mock("@/lib/cache/provider-cache", () => ({
   publishProviderCacheInvalidation: publishCacheInvalidationMock,
 }));
 
+vi.mock("@/lib/redis/client", () => buildRedisMock(redisMocks));
+
 vi.mock("@/lib/circuit-breaker", () => ({
   clearProviderState: vi.fn(),
   clearConfigCache: vi.fn(),
@@ -103,6 +107,7 @@ describe("Undo Provider Batch Patch Engine", () => {
   beforeEach(() => {
     vi.clearAllMocks();
     vi.resetModules();
+    redisStore.clear();
     getSessionMock.mockResolvedValue({ user: { id: 1, role: "admin" } });
     findAllProvidersFreshMock.mockResolvedValue([]);
     updateProvidersBatchMock.mockResolvedValue(0);

+ 48 - 0
tests/unit/actions/redis-mock-utils.ts

@@ -0,0 +1,48 @@
+import { vi } from "vitest";
+
+export function createRedisStore() {
+  const store = new Map<string, { value: string; expiresAt: number }>();
+
+  function readValue(key: string): string | null {
+    const entry = store.get(key);
+    if (!entry) return null;
+    if (entry.expiresAt <= Date.now()) {
+      store.delete(key);
+      return null;
+    }
+    return entry.value;
+  }
+
+  const setex = vi.fn(async (key: string, ttlSeconds: number, value: string) => {
+    store.set(key, { value, expiresAt: Date.now() + ttlSeconds * 1000 });
+    return "OK";
+  });
+
+  const get = vi.fn(async (key: string) => readValue(key));
+
+  const del = vi.fn(async (key: string) => {
+    const existed = store.delete(key);
+    return existed ? 1 : 0;
+  });
+
+  const evalScript = vi.fn(async (_script: string, _numKeys: number, key: string) => {
+    const value = readValue(key);
+    if (value === null) return null;
+    store.delete(key);
+    return value;
+  });
+
+  return { store, mocks: { setex, get, del, eval: evalScript } };
+}
+
+export function buildRedisMock(mocks: ReturnType<typeof createRedisStore>["mocks"]) {
+  return {
+    getRedisClient: () => ({
+      status: "ready",
+      setex: mocks.setex,
+      get: mocks.get,
+      del: mocks.del,
+      eval: mocks.eval,
+    }),
+  };
+}

+ 36 - 0
tests/unit/dashboard/dashboard-cache-keys.test.ts

@@ -0,0 +1,36 @@
+import { describe, expect, it } from "vitest";
+import { buildOverviewCacheKey, buildStatisticsCacheKey } from "@/types/dashboard-cache";
+import type { TimeRange } from "@/types/statistics";
+
+describe("buildOverviewCacheKey", () => {
+  it("returns 'overview:global' for global scope", () => {
+    expect(buildOverviewCacheKey("global")).toBe("overview:global");
+  });
+
+  it("returns 'overview:user:42' for user scope with userId=42", () => {
+    expect(buildOverviewCacheKey("user", 42)).toBe("overview:user:42");
+  });
+});
+
+describe("buildStatisticsCacheKey", () => {
+  it("returns correct key for today/users/global", () => {
+    expect(buildStatisticsCacheKey("today", "users")).toBe("statistics:today:users:global");
+  });
+
+  it("returns correct key with userId", () => {
+    expect(buildStatisticsCacheKey("7days", "keys", 42)).toBe("statistics:7days:keys:42");
+  });
+
+  it("handles all TimeRange values", () => {
+    const timeRanges: TimeRange[] = ["today", "7days", "30days", "thisMonth"];
+    const keys = timeRanges.map((timeRange) => buildStatisticsCacheKey(timeRange, "users"));
+
+    expect(keys).toEqual([
+      "statistics:today:users:global",
+      "statistics:7days:users:global",
+      "statistics:30days:users:global",
+      "statistics:thisMonth:users:global",
+    ]);
+    expect(new Set(keys).size).toBe(timeRanges.length);
+  });
+});

+ 210 - 0
tests/unit/redis/overview-cache.test.ts

@@ -0,0 +1,210 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { getRedisClient } from "@/lib/redis/client";
+import { getOverviewWithCache, invalidateOverviewCache } from "@/lib/redis/overview-cache";
+import {
+  getOverviewMetricsWithComparison,
+  type OverviewMetricsWithComparison,
+} from "@/repository/overview";
+
+vi.mock("@/lib/logger", () => ({
+  logger: {
+    debug: vi.fn(),
+    info: vi.fn(),
+    warn: vi.fn(),
+    error: vi.fn(),
+  },
+}));
+
+vi.mock("@/lib/redis/client", () => ({
+  getRedisClient: vi.fn(),
+}));
+
+vi.mock("@/repository/overview", () => ({
+  getOverviewMetricsWithComparison: vi.fn(),
+}));
+
+type RedisMock = {
+  get: ReturnType<typeof vi.fn>;
+  set: ReturnType<typeof vi.fn>;
+  setex: ReturnType<typeof vi.fn>;
+  del: ReturnType<typeof vi.fn>;
+};
+
+function createRedisMock(): RedisMock {
+  return {
+    get: vi.fn(),
+    set: vi.fn(),
+    setex: vi.fn(),
+    del: vi.fn(),
+  };
+}
+
+function createOverviewData(): OverviewMetricsWithComparison {
+  return {
+    todayRequests: 100,
+    todayCost: 12.34,
+    avgResponseTime: 210,
+    todayErrorRate: 1.25,
+    yesterdaySamePeriodRequests: 80,
+    yesterdaySamePeriodCost: 10.1,
+    yesterdaySamePeriodAvgResponseTime: 230,
+    recentMinuteRequests: 3,
+  };
+}
+
+describe("getOverviewWithCache", () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
+
+  it("returns cached data on cache hit (no DB call)", async () => {
+    const data = createOverviewData();
+    const redis = createRedisMock();
+    redis.get.mockResolvedValueOnce(JSON.stringify(data));
+
+    vi.mocked(getRedisClient).mockReturnValue(
+      redis as unknown as NonNullable<ReturnType<typeof getRedisClient>>
+    );
+
+    const result = await getOverviewWithCache();
+
+    expect(result).toEqual(data);
+    expect(redis.get).toHaveBeenCalledWith("overview:global");
+    expect(getOverviewMetricsWithComparison).not.toHaveBeenCalled();
+  });
+
+  it("calls DB on cache miss, stores in Redis with 10s TTL", async () => {
+    const data = createOverviewData();
+    const redis = createRedisMock();
+    redis.get.mockResolvedValueOnce(null);
+    redis.set.mockResolvedValueOnce("OK");
+    redis.setex.mockResolvedValueOnce("OK");
+    redis.del.mockResolvedValueOnce(1);
+
+    vi.mocked(getRedisClient).mockReturnValue(
+      redis as unknown as NonNullable<ReturnType<typeof getRedisClient>>
+    );
+    vi.mocked(getOverviewMetricsWithComparison).mockResolvedValueOnce(data);
+
+    const result = await getOverviewWithCache(42);
+
+    expect(result).toEqual(data);
+    expect(getOverviewMetricsWithComparison).toHaveBeenCalledWith(42);
+    expect(redis.set).toHaveBeenCalledWith("overview:user:42:lock", "1", "EX", 5, "NX");
+    expect(redis.setex).toHaveBeenCalledWith("overview:user:42", 10, JSON.stringify(data));
+    expect(redis.del).toHaveBeenCalledWith("overview:user:42:lock");
+  });
+
+  it("falls back to direct DB query when Redis is unavailable (null client)", async () => {
+    const data = createOverviewData();
+    vi.mocked(getRedisClient).mockReturnValue(null);
+    vi.mocked(getOverviewMetricsWithComparison).mockResolvedValueOnce(data);
+
+    const result = await getOverviewWithCache(7);
+
+    expect(result).toEqual(data);
+    expect(getOverviewMetricsWithComparison).toHaveBeenCalledWith(7);
+  });
+
+  it("falls back to direct DB query on Redis error", async () => {
+    const data = createOverviewData();
+    const redis = createRedisMock();
+    redis.get.mockRejectedValueOnce(new Error("redis read failed"));
+
+    vi.mocked(getRedisClient).mockReturnValue(
+      redis as unknown as NonNullable<ReturnType<typeof getRedisClient>>
+    );
+    vi.mocked(getOverviewMetricsWithComparison).mockResolvedValueOnce(data);
+
+    const result = await getOverviewWithCache();
+
+    expect(result).toEqual(data);
+    expect(getOverviewMetricsWithComparison).toHaveBeenCalledWith(undefined);
+  });
+
+  it("falls back to direct DB query when lock is held and retry is still empty", async () => {
+    vi.useFakeTimers();
+    try {
+      const data = createOverviewData();
+      const redis = createRedisMock();
+      redis.get.mockResolvedValueOnce(null).mockResolvedValueOnce(null);
+      redis.set.mockResolvedValueOnce(null);
+
+      vi.mocked(getRedisClient).mockReturnValue(
+        redis as unknown as NonNullable<ReturnType<typeof getRedisClient>>
+      );
+      vi.mocked(getOverviewMetricsWithComparison).mockResolvedValueOnce(data);
+
+      const pending = getOverviewWithCache(99);
+      await vi.advanceTimersByTimeAsync(100);
+      const result = await pending;
+
+      expect(result).toEqual(data);
+      expect(redis.set).toHaveBeenCalledWith("overview:user:99:lock", "1", "EX", 5, "NX");
+      expect(redis.get).toHaveBeenNthCalledWith(1, "overview:user:99");
+      expect(redis.get).toHaveBeenNthCalledWith(2, "overview:user:99");
+      expect(getOverviewMetricsWithComparison).toHaveBeenCalledWith(99);
+    } finally {
+      vi.useRealTimers();
+    }
+  });
+
+  it("uses different cache keys for global vs user scope", async () => {
+    const redis = createRedisMock();
+    const data = createOverviewData();
+
+    redis.get.mockResolvedValue(null);
+    redis.set.mockResolvedValue("OK");
+    redis.setex.mockResolvedValue("OK");
+    redis.del.mockResolvedValue(1);
+
+    vi.mocked(getRedisClient).mockReturnValue(
+      redis as unknown as NonNullable<ReturnType<typeof getRedisClient>>
+    );
+    vi.mocked(getOverviewMetricsWithComparison).mockResolvedValue(data);
+
+    await getOverviewWithCache();
+    await getOverviewWithCache(42);
+
+    expect(redis.get).toHaveBeenNthCalledWith(1, "overview:global");
+    expect(redis.get).toHaveBeenNthCalledWith(2, "overview:user:42");
+    expect(redis.setex).toHaveBeenNthCalledWith(1, "overview:global", 10, JSON.stringify(data));
+    expect(redis.setex).toHaveBeenNthCalledWith(2, "overview:user:42", 10, JSON.stringify(data));
+  });
+});
+
+describe("invalidateOverviewCache", () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
+
+  it("deletes the correct cache key", async () => {
+    const redis = createRedisMock();
+    redis.del.mockResolvedValueOnce(1);
+
+    vi.mocked(getRedisClient).mockReturnValue(
+      redis as unknown as NonNullable<ReturnType<typeof getRedisClient>>
+    );
+
+    await invalidateOverviewCache(42);
+
+    expect(redis.del).toHaveBeenCalledWith("overview:user:42");
+  });
+
+  it("does nothing when Redis is unavailable", async () => {
+    vi.mocked(getRedisClient).mockReturnValue(null);
+
+    await expect(invalidateOverviewCache(42)).resolves.toBeUndefined();
+  });
+
+  it("swallows Redis errors during invalidation", async () => {
+    const redis = createRedisMock();
+    redis.del.mockRejectedValueOnce(new Error("delete failed"));
+
+    vi.mocked(getRedisClient).mockReturnValue(
+      redis as unknown as NonNullable<ReturnType<typeof getRedisClient>>
+    );
+
+    await expect(invalidateOverviewCache()).resolves.toBeUndefined();
+  });
+});

+ 369 - 0
tests/unit/redis/statistics-cache.test.ts

@@ -0,0 +1,369 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { getRedisClient } from "@/lib/redis/client";
+import { getStatisticsWithCache, invalidateStatisticsCache } from "@/lib/redis/statistics-cache";
+import {
+  getKeyStatisticsFromDB,
+  getMixedStatisticsFromDB,
+  getUserStatisticsFromDB,
+} from "@/repository/statistics";
+import type { DatabaseKeyStatRow, DatabaseStatRow } from "@/types/statistics";
+
+vi.mock("@/lib/logger", () => ({
+  logger: {
+    debug: vi.fn(),
+    info: vi.fn(),
+    warn: vi.fn(),
+    error: vi.fn(),
+  },
+}));
+
+vi.mock("@/lib/redis/client", () => ({
+  getRedisClient: vi.fn(),
+}));
+
+vi.mock("@/repository/statistics", () => ({
+  getUserStatisticsFromDB: vi.fn(),
+  getKeyStatisticsFromDB: vi.fn(),
+  getMixedStatisticsFromDB: vi.fn(),
+}));
+
+type RedisMock = {
+  get: ReturnType<typeof vi.fn>;
+  set: ReturnType<typeof vi.fn>;
+  setex: ReturnType<typeof vi.fn>;
+  del: ReturnType<typeof vi.fn>;
+  scan: ReturnType<typeof vi.fn>;
+};
+
+function createRedisMock(): RedisMock {
+  return {
+    get: vi.fn(),
+    set: vi.fn(),
+    setex: vi.fn(),
+    del: vi.fn(),
+    scan: vi.fn(),
+  };
+}
+
+function createUserStats(): DatabaseStatRow[] {
+  return [
+    {
+      user_id: 1,
+      user_name: "alice",
+      date: "2026-02-19",
+      api_calls: 10,
+      total_cost: "1.23",
+    },
+  ];
+}
+
+function createKeyStats(): DatabaseKeyStatRow[] {
+  return [
+    {
+      key_id: 100,
+      key_name: "test-key",
+      date: "2026-02-19",
+      api_calls: 6,
+      total_cost: "0.56",
+    },
+  ];
+}
+
+describe("getStatisticsWithCache", () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
+
+  it("returns cached data on cache hit", async () => {
+    const redis = createRedisMock();
+    const cached = createUserStats();
+    redis.get.mockResolvedValueOnce(JSON.stringify(cached));
+
+    vi.mocked(getRedisClient).mockReturnValue(
+      redis as unknown as NonNullable<ReturnType<typeof getRedisClient>>
+    );
+
+    const result = await getStatisticsWithCache("today", "users");
+
+    expect(result).toEqual(cached);
+    expect(redis.get).toHaveBeenCalledWith("statistics:today:users:global");
+    expect(getUserStatisticsFromDB).not.toHaveBeenCalled();
+    expect(getKeyStatisticsFromDB).not.toHaveBeenCalled();
+    expect(getMixedStatisticsFromDB).not.toHaveBeenCalled();
+  });
+
+  it("calls getUserStatisticsFromDB for mode=users on cache miss", async () => {
+    const redis = createRedisMock();
+    const rows = createUserStats();
+    redis.get.mockResolvedValueOnce(null);
+    redis.set.mockResolvedValueOnce("OK");
+    redis.setex.mockResolvedValueOnce("OK");
+    redis.del.mockResolvedValueOnce(1);
+
+    vi.mocked(getRedisClient).mockReturnValue(
+      redis as unknown as NonNullable<ReturnType<typeof getRedisClient>>
+    );
+    vi.mocked(getUserStatisticsFromDB).mockResolvedValueOnce(rows);
+
+    const result = await getStatisticsWithCache("today", "users");
+
+    expect(result).toEqual(rows);
+    expect(getUserStatisticsFromDB).toHaveBeenCalledWith("today");
+    expect(getKeyStatisticsFromDB).not.toHaveBeenCalled();
+    expect(getMixedStatisticsFromDB).not.toHaveBeenCalled();
+  });
+
+  it("calls getKeyStatisticsFromDB for mode=keys on cache miss", async () => {
+    const redis = createRedisMock();
+    const rows = createKeyStats();
+    redis.get.mockResolvedValueOnce(null);
+    redis.set.mockResolvedValueOnce("OK");
+    redis.setex.mockResolvedValueOnce("OK");
+    redis.del.mockResolvedValueOnce(1);
+
+    vi.mocked(getRedisClient).mockReturnValue(
+      redis as unknown as NonNullable<ReturnType<typeof getRedisClient>>
+    );
+    vi.mocked(getKeyStatisticsFromDB).mockResolvedValueOnce(rows);
+
+    const result = await getStatisticsWithCache("7days", "keys", 42);
+
+    expect(result).toEqual(rows);
+    expect(getKeyStatisticsFromDB).toHaveBeenCalledWith(42, "7days");
+    expect(getUserStatisticsFromDB).not.toHaveBeenCalled();
+    expect(getMixedStatisticsFromDB).not.toHaveBeenCalled();
+  });
+
+  it("calls getMixedStatisticsFromDB for mode=mixed on cache miss", async () => {
+    const redis = createRedisMock();
+    const mixedResult = {
+      ownKeys: createKeyStats(),
+      othersAggregate: createUserStats(),
+    };
+    redis.get.mockResolvedValueOnce(null);
+    redis.set.mockResolvedValueOnce("OK");
+    redis.setex.mockResolvedValueOnce("OK");
+    redis.del.mockResolvedValueOnce(1);
+
+    vi.mocked(getRedisClient).mockReturnValue(
+      redis as unknown as NonNullable<ReturnType<typeof getRedisClient>>
+    );
+    vi.mocked(getMixedStatisticsFromDB).mockResolvedValueOnce(mixedResult);
+
+    const result = await getStatisticsWithCache("30days", "mixed", 42);
+
+    expect(result).toEqual(mixedResult);
+    expect(getMixedStatisticsFromDB).toHaveBeenCalledWith(42, "30days");
+    expect(getUserStatisticsFromDB).not.toHaveBeenCalled();
+    expect(getKeyStatisticsFromDB).not.toHaveBeenCalled();
+  });
+
+  it("stores result with 30s TTL", async () => {
+    const redis = createRedisMock();
+    const rows = createUserStats();
+    redis.get.mockResolvedValueOnce(null);
+    redis.set.mockResolvedValueOnce("OK");
+    redis.setex.mockResolvedValueOnce("OK");
+    redis.del.mockResolvedValueOnce(1);
+
+    vi.mocked(getRedisClient).mockReturnValue(
+      redis as unknown as NonNullable<ReturnType<typeof getRedisClient>>
+    );
+    vi.mocked(getUserStatisticsFromDB).mockResolvedValueOnce(rows);
+
+    await getStatisticsWithCache("today", "users");
+
+    expect(redis.setex).toHaveBeenCalledWith(
+      "statistics:today:users:global",
+      30,
+      JSON.stringify(rows)
+    );
+  });
+
+  it("falls back to direct DB on Redis unavailable", async () => {
+    const rows = createUserStats();
+    vi.mocked(getRedisClient).mockReturnValue(null);
+    vi.mocked(getUserStatisticsFromDB).mockResolvedValueOnce(rows);
+
+    const result = await getStatisticsWithCache("today", "users");
+
+    expect(result).toEqual(rows);
+    expect(getUserStatisticsFromDB).toHaveBeenCalledWith("today");
+  });
+
+  it("uses retry path and returns cached data when lock is held", async () => {
+    vi.useFakeTimers();
+    try {
+      const redis = createRedisMock();
+      const rows = createUserStats();
+      redis.get.mockResolvedValueOnce(null).mockResolvedValueOnce(JSON.stringify(rows));
+      redis.set.mockResolvedValueOnce(null);
+
+      vi.mocked(getRedisClient).mockReturnValue(
+        redis as unknown as NonNullable<ReturnType<typeof getRedisClient>>
+      );
+
+      const pending = getStatisticsWithCache("today", "users");
+      await vi.advanceTimersByTimeAsync(100);
+      const result = await pending;
+
+      expect(result).toEqual(rows);
+      expect(redis.set).toHaveBeenCalledWith(
+        "statistics:today:users:global:lock",
+        "1",
+        "EX",
+        5,
+        "NX"
+      );
+      expect(getUserStatisticsFromDB).not.toHaveBeenCalled();
+    } finally {
+      vi.useRealTimers();
+    }
+  });
+
+  it("falls back to direct DB when retry times out", async () => {
+    vi.useFakeTimers();
+    try {
+      const redis = createRedisMock();
+      const rows = createUserStats();
+      redis.get.mockResolvedValue(null);
+      redis.set.mockResolvedValueOnce(null);
+
+      vi.mocked(getRedisClient).mockReturnValue(
+        redis as unknown as NonNullable<ReturnType<typeof getRedisClient>>
+      );
+      vi.mocked(getUserStatisticsFromDB).mockResolvedValueOnce(rows);
+
+      const pending = getStatisticsWithCache("today", "users");
+      await vi.advanceTimersByTimeAsync(5100);
+      const result = await pending;
+
+      expect(result).toEqual(rows);
+      expect(getUserStatisticsFromDB).toHaveBeenCalledWith("today");
+    } finally {
+      vi.useRealTimers();
+    }
+  });
+
+  it("falls back to direct DB on Redis error", async () => {
+    const redis = createRedisMock();
+    const rows = createUserStats();
+    redis.get.mockRejectedValueOnce(new Error("redis get failed"));
+
+    vi.mocked(getRedisClient).mockReturnValue(
+      redis as unknown as NonNullable<ReturnType<typeof getRedisClient>>
+    );
+    vi.mocked(getUserStatisticsFromDB).mockResolvedValueOnce(rows);
+
+    const result = await getStatisticsWithCache("today", "users");
+
+    expect(result).toEqual(rows);
+    expect(getUserStatisticsFromDB).toHaveBeenCalledWith("today");
+  });
+
+  it("uses different cache keys for different timeRanges", async () => {
+    const redis = createRedisMock();
+    const rows = createUserStats();
+    redis.get.mockResolvedValue(JSON.stringify(rows));
+
+    vi.mocked(getRedisClient).mockReturnValue(
+      redis as unknown as NonNullable<ReturnType<typeof getRedisClient>>
+    );
+
+    await getStatisticsWithCache("today", "users");
+    await getStatisticsWithCache("7days", "users");
+
+    expect(redis.get).toHaveBeenNthCalledWith(1, "statistics:today:users:global");
+    expect(redis.get).toHaveBeenNthCalledWith(2, "statistics:7days:users:global");
+  });
+
+  it("uses different cache keys for global vs user scope", async () => {
+    const redis = createRedisMock();
+    const rows = createUserStats();
+    redis.get.mockResolvedValue(JSON.stringify(rows));
+
+    vi.mocked(getRedisClient).mockReturnValue(
+      redis as unknown as NonNullable<ReturnType<typeof getRedisClient>>
+    );
+
+    await getStatisticsWithCache("today", "users");
+    await getStatisticsWithCache("today", "users", 42);
+
+    expect(redis.get).toHaveBeenNthCalledWith(1, "statistics:today:users:global");
+    expect(redis.get).toHaveBeenNthCalledWith(2, "statistics:today:users:42");
+  });
+});
+
+describe("invalidateStatisticsCache", () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
+
+  it("deletes all mode keys for a given timeRange", async () => {
+    const redis = createRedisMock();
+    redis.del.mockResolvedValueOnce(3);
+
+    vi.mocked(getRedisClient).mockReturnValue(
+      redis as unknown as NonNullable<ReturnType<typeof getRedisClient>>
+    );
+
+    await invalidateStatisticsCache("today", 42);
+
+    expect(redis.del).toHaveBeenCalledWith(
+      "statistics:today:users:42",
+      "statistics:today:keys:42",
+      "statistics:today:mixed:42"
+    );
+  });
+
+  it("deletes all keys for scope when timeRange is undefined", async () => {
+    const redis = createRedisMock();
+    const matchedKeys = [
+      "statistics:today:users:global",
+      "statistics:7days:keys:global",
+      "statistics:30days:mixed:global",
+    ];
+    redis.scan.mockResolvedValueOnce(["0", matchedKeys]);
+    redis.del.mockResolvedValueOnce(matchedKeys.length);
+
+    vi.mocked(getRedisClient).mockReturnValue(
+      redis as unknown as NonNullable<ReturnType<typeof getRedisClient>>
+    );
+
+    await invalidateStatisticsCache(undefined, undefined);
+
+    expect(redis.scan).toHaveBeenCalledWith("0", "MATCH", "statistics:*:*:global", "COUNT", 100);
+    expect(redis.del).toHaveBeenCalledWith(...matchedKeys);
+  });
+
+  it("does nothing when Redis is unavailable", async () => {
+    vi.mocked(getRedisClient).mockReturnValue(null);
+
+    await expect(invalidateStatisticsCache("today", 42)).resolves.toBeUndefined();
+  });
+
+  it("does not call del when wildcard query returns no key", async () => {
+    const redis = createRedisMock();
+    redis.scan.mockResolvedValueOnce(["0", []]);
+
+    vi.mocked(getRedisClient).mockReturnValue(
+      redis as unknown as NonNullable<ReturnType<typeof getRedisClient>>
+    );
+
+    await invalidateStatisticsCache(undefined, 42);
+
+    expect(redis.scan).toHaveBeenCalledWith("0", "MATCH", "statistics:*:*:42", "COUNT", 100);
+    expect(redis.del).not.toHaveBeenCalled();
+  });
+
+  it("swallows Redis errors during invalidation", async () => {
+    const redis = createRedisMock();
+    redis.del.mockRejectedValueOnce(new Error("delete failed"));
+
+    vi.mocked(getRedisClient).mockReturnValue(
+      redis as unknown as NonNullable<ReturnType<typeof getRedisClient>>
+    );
+
+    await expect(invalidateStatisticsCache("today", 42)).resolves.toBeUndefined();
+  });
+});